diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs new file mode 100644 index 0000000000..efb2000e06 --- /dev/null +++ b/crates/onboarding/src/basics_page.rs @@ -0,0 +1,103 @@ +use fs::Fs; +use gpui::{App, IntoElement, Window}; +use settings::{Settings, update_settings_file}; +use theme::{ThemeMode, ThemeSettings}; +use ui::{SwitchField, ToggleButtonGroup, ToggleButtonSimple, ToggleButtonWithIcon, prelude::*}; + +fn read_theme_selection(cx: &App) -> ThemeMode { + let settings = ThemeSettings::get_global(cx); + settings + .theme_selection + .as_ref() + .and_then(|selection| selection.mode()) + .unwrap_or_default() +} + +fn write_theme_selection(theme_mode: ThemeMode, cx: &App) { + let fs = ::global(cx); + + update_settings_file::(fs, cx, move |settings, _| { + settings.set_mode(theme_mode); + }); +} + +fn render_theme_section(cx: &mut App) -> impl IntoElement { + let theme_mode = read_theme_selection(cx); + + h_flex().justify_between().child(Label::new("Theme")).child( + ToggleButtonGroup::single_row( + "theme-selector-onboarding", + [ + ToggleButtonSimple::new("Light", |_, _, cx| { + write_theme_selection(ThemeMode::Light, cx) + }), + ToggleButtonSimple::new("Dark", |_, _, cx| { + write_theme_selection(ThemeMode::Dark, cx) + }), + ToggleButtonSimple::new("System", |_, _, cx| { + write_theme_selection(ThemeMode::System, cx) + }), + ], + ) + .selected_index(match theme_mode { + ThemeMode::Light => 0, + ThemeMode::Dark => 1, + ThemeMode::System => 2, + }) + .style(ui::ToggleButtonGroupStyle::Outlined) + .button_width(rems_from_px(64.)), + ) +} + +fn render_telemetry_section() -> impl IntoElement { + v_flex() + .gap_3() + .child(Label::new("Telemetry").size(LabelSize::Large)) + .child(SwitchField::new( + "vim_mode", + "Help Improve Zed", + "Sending anonymous usage data helps us build the right features and create the best experience.", + ui::ToggleState::Selected, + |_, _, _| {}, + )) + .child(SwitchField::new( + "vim_mode", + "Help Fix Zed", + "Send crash reports so we can fix critical issues fast.", + ui::ToggleState::Selected, + |_, _, _| {}, + )) +} + +pub(crate) fn render_basics_page(_: &mut Window, cx: &mut App) -> impl IntoElement { + v_flex() + .gap_6() + .child(render_theme_section(cx)) + .child( + v_flex().gap_2().child(Label::new("Base Keymap")).child( + ToggleButtonGroup::two_rows( + "multiple_row_test", + [ + ToggleButtonWithIcon::new("VS Code", IconName::AiZed, |_, _, _| {}), + ToggleButtonWithIcon::new("Jetbrains", IconName::AiZed, |_, _, _| {}), + ToggleButtonWithIcon::new("Sublime Text", IconName::AiZed, |_, _, _| {}), + ], + [ + ToggleButtonWithIcon::new("Atom", IconName::AiZed, |_, _, _| {}), + ToggleButtonWithIcon::new("Emacs", IconName::AiZed, |_, _, _| {}), + ToggleButtonWithIcon::new("Cursor (Beta)", IconName::AiZed, |_, _, _| {}), + ], + ) + .button_width(rems_from_px(230.)) + .style(ui::ToggleButtonGroupStyle::Outlined) + ), + ) + .child(v_flex().justify_center().child(div().h_0().child("hack").invisible()).child(SwitchField::new( + "vim_mode", + "Vim Mode", + "Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.", + ui::ToggleState::Selected, + |_, _, _| {}, + ))) + .child(render_telemetry_section()) +} diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index f100f61464..3fb9aaf0cc 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -6,10 +6,8 @@ use project::project_settings::ProjectSettings; use settings::{Settings as _, update_settings_file}; use theme::{FontFamilyCache, FontFamilyName, ThemeSettings}; use ui::{ - Clickable, ContextMenu, DropdownMenu, IconButton, Label, LabelCommon, LabelSize, - NumericStepper, ParentElement, SharedString, Styled, SwitchColor, SwitchField, - ToggleButtonGroup, ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, div, h_flex, px, - v_flex, + ButtonLike, ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup, + ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, prelude::*, }; use crate::{ImportCursorSettings, ImportVsCodeSettings}; @@ -118,153 +116,212 @@ fn write_buffer_font_family(font_family: SharedString, cx: &mut App) { }); } -pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement { +fn render_import_settings_section() -> impl IntoElement { + v_flex() + .gap_4() + .child( + v_flex() + .child(Label::new("Import Settings").size(LabelSize::Large)) + .child( + Label::new("Automatically pull your settings from other editors.") + .color(Color::Muted), + ), + ) + .child( + h_flex() + .w_full() + .gap_4() + .child( + h_flex().w_full().child( + ButtonLike::new("import_vs_code") + .full_width() + .style(ButtonStyle::Outlined) + .size(ButtonSize::Large) + .child( + h_flex() + .w_full() + .gap_1p5() + .px_1() + .child( + Icon::new(IconName::Sparkle) + .color(Color::Muted) + .size(IconSize::XSmall), + ) + .child(Label::new("VS Code")), + ) + .on_click(|_, window, cx| { + window.dispatch_action( + ImportVsCodeSettings::default().boxed_clone(), + cx, + ) + }), + ), + ) + .child( + h_flex().w_full().child( + ButtonLike::new("import_cursor") + .full_width() + .style(ButtonStyle::Outlined) + .size(ButtonSize::Large) + .child( + h_flex() + .w_full() + .gap_1p5() + .px_1() + .child( + Icon::new(IconName::Sparkle) + .color(Color::Muted) + .size(IconSize::XSmall), + ) + .child(Label::new("Cursor")), + ) + .on_click(|_, window, cx| { + window.dispatch_action( + ImportCursorSettings::default().boxed_clone(), + cx, + ) + }), + ), + ), + ) +} + +fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl IntoElement { let theme_settings = ThemeSettings::get_global(cx); let ui_font_size = theme_settings.ui_font_size(cx); let font_family = theme_settings.buffer_font.family.clone(); let buffer_font_size = theme_settings.buffer_font_size(cx); - v_flex() + h_flex() + .w_full() .gap_4() - .child(Label::new("Import Settings").size(LabelSize::Large)) .child( - Label::new("Automatically pull your settings from other editors.") - .size(LabelSize::Small), - ) - .child( - h_flex() + v_flex() + .w_full() + .gap_1() + .child(Label::new("UI Font")) .child( - IconButton::new("import-vs-code-settings", ui::IconName::Code).on_click( - |_, window, cx| { - window - .dispatch_action(ImportVsCodeSettings::default().boxed_clone(), cx) - }, - ), - ) - .child( - IconButton::new("import-cursor-settings", ui::IconName::CursorIBeam).on_click( - |_, window, cx| { - window - .dispatch_action(ImportCursorSettings::default().boxed_clone(), cx) - }, - ), - ), - ) - .child(Label::new("Popular Settings").size(LabelSize::Large)) - .child( - h_flex() - .gap_4() - .justify_between() - .child( - v_flex() + h_flex() + .w_full() .justify_between() - .gap_1() - .child(Label::new("UI Font")) + .gap_2() .child( - h_flex() - .justify_between() - .gap_2() - .child(div().min_w(px(120.)).child(DropdownMenu::new( - "ui-font-family", - theme_settings.ui_font.family.clone(), - ContextMenu::build(window, cx, |mut menu, _, cx| { - let font_family_cache = FontFamilyCache::global(cx); + DropdownMenu::new( + "ui-font-family", + theme_settings.ui_font.family.clone(), + ContextMenu::build(window, cx, |mut menu, _, cx| { + let font_family_cache = FontFamilyCache::global(cx); - for font_name in font_family_cache.list_font_families(cx) { - menu = menu.custom_entry( - { - let font_name = font_name.clone(); - move |_window, _cx| { - Label::new(font_name.clone()) - .into_any_element() - } - }, - { - let font_name = font_name.clone(); - move |_window, cx| { - write_ui_font_family(font_name.clone(), cx); - } - }, - ) - } + for font_name in font_family_cache.list_font_families(cx) { + menu = menu.custom_entry( + { + let font_name = font_name.clone(); + move |_window, _cx| { + Label::new(font_name.clone()).into_any_element() + } + }, + { + let font_name = font_name.clone(); + move |_window, cx| { + write_ui_font_family(font_name.clone(), cx); + } + }, + ) + } - menu - }), - ))) - .child( - NumericStepper::new( - "ui-font-size", - ui_font_size.to_string(), - move |_, _, cx| { - write_ui_font_size(ui_font_size - px(1.), cx); - }, - move |_, _, cx| { - write_ui_font_size(ui_font_size + px(1.), cx); - }, - ) - .border(), - ), - ), - ) - .child( - v_flex() - .justify_between() - .gap_1() - .child(Label::new("Editor Font")) + menu + }), + ) + .style(ui::DropdownStyle::Outlined) + .full_width(true), + ) .child( - h_flex() - .justify_between() - .gap_2() - .child(DropdownMenu::new( - "buffer-font-family", - font_family, - ContextMenu::build(window, cx, |mut menu, _, cx| { - let font_family_cache = FontFamilyCache::global(cx); - - for font_name in font_family_cache.list_font_families(cx) { - menu = menu.custom_entry( - { - let font_name = font_name.clone(); - move |_window, _cx| { - Label::new(font_name.clone()) - .into_any_element() - } - }, - { - let font_name = font_name.clone(); - move |_window, cx| { - write_buffer_font_family( - font_name.clone(), - cx, - ); - } - }, - ) - } - - menu - }), - )) - .child( - NumericStepper::new( - "buffer-font-size", - buffer_font_size.to_string(), - move |_, _, cx| { - write_buffer_font_size(buffer_font_size - px(1.), cx); - }, - move |_, _, cx| { - write_buffer_font_size(buffer_font_size + px(1.), cx); - }, - ) - .border(), - ), + NumericStepper::new( + "ui-font-size", + ui_font_size.to_string(), + move |_, _, cx| { + write_ui_font_size(ui_font_size - px(1.), cx); + }, + move |_, _, cx| { + write_ui_font_size(ui_font_size + px(1.), cx); + }, + ) + .style(ui::NumericStepperStyle::Outlined), ), ), ) + .child( + v_flex() + .w_full() + .gap_1() + .child(Label::new("Editor Font")) + .child( + h_flex() + .w_full() + .justify_between() + .gap_2() + .child( + DropdownMenu::new( + "buffer-font-family", + font_family, + ContextMenu::build(window, cx, |mut menu, _, cx| { + let font_family_cache = FontFamilyCache::global(cx); + + for font_name in font_family_cache.list_font_families(cx) { + menu = menu.custom_entry( + { + let font_name = font_name.clone(); + move |_window, _cx| { + Label::new(font_name.clone()).into_any_element() + } + }, + { + let font_name = font_name.clone(); + move |_window, cx| { + write_buffer_font_family(font_name.clone(), cx); + } + }, + ) + } + + menu + }), + ) + .style(ui::DropdownStyle::Outlined) + .full_width(true), + ) + .child( + NumericStepper::new( + "buffer-font-size", + buffer_font_size.to_string(), + move |_, _, cx| { + write_buffer_font_size(buffer_font_size - px(1.), cx); + }, + move |_, _, cx| { + write_buffer_font_size(buffer_font_size + px(1.), cx); + }, + ) + .style(ui::NumericStepperStyle::Outlined), + ), + ), + ) +} + +fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl IntoElement { + v_flex() + .gap_5() + .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8()) + .child(render_font_customization_section(window, cx)) .child( h_flex() + .items_start() .justify_between() - .child(Label::new("Mini Map")) + .child( + v_flex().child(Label::new("Mini Map")).child( + Label::new("See a high-level overview of your source code.") + .color(Color::Muted), + ), + ) .child( ToggleButtonGroup::single_row( "onboarding-show-mini-map", @@ -289,36 +346,37 @@ pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl Int .button_width(ui::rems_from_px(64.)), ), ) - .child( - SwitchField::new( - "onboarding-enable-inlay-hints", - "Inlay Hints", - "See parameter names for function and method calls inline.", - if read_inlay_hints(cx) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - |toggle_state, _, cx| { - write_inlay_hints(toggle_state == &ToggleState::Selected, cx); - }, - ) - .color(SwitchColor::Accent), - ) - .child( - SwitchField::new( - "onboarding-git-blame-switch", - "Git Blame", - "See who committed each line on a given file.", - if read_git_blame(cx) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - |toggle_state, _, cx| { - set_git_blame(toggle_state == &ToggleState::Selected, cx); - }, - ) - .color(SwitchColor::Accent), - ) + .child(SwitchField::new( + "onboarding-enable-inlay-hints", + "Inlay Hints", + "See parameter names for function and method calls inline.", + if read_inlay_hints(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + write_inlay_hints(toggle_state == &ToggleState::Selected, cx); + }, + )) + .child(SwitchField::new( + "onboarding-git-blame-switch", + "Git Blame", + "See who committed each line on a given file.", + if read_git_blame(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + set_git_blame(toggle_state == &ToggleState::Selected, cx); + }, + )) +} + +pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement { + v_flex() + .gap_4() + .child(render_import_settings_section()) + .child(render_popular_settings_section(window, cx)) } diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 75b6fbf912..69b9301302 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -10,13 +10,9 @@ use gpui::{ }; use schemars::JsonSchema; use serde::Deserialize; -use settings::{Settings, SettingsStore, VsCodeSettingsSource, update_settings_file}; +use settings::{SettingsStore, VsCodeSettingsSource}; use std::sync::Arc; -use theme::{ThemeMode, ThemeSettings}; -use ui::{ - Divider, FluentBuilder, Headline, KeyBinding, ParentElement as _, StatefulInteractiveElement, - ToggleButtonGroup, ToggleButtonSimple, Vector, VectorName, prelude::*, rems_from_px, -}; +use ui::{FluentBuilder, KeyBinding, Vector, VectorName, prelude::*, rems_from_px}; use workspace::{ AppState, Workspace, WorkspaceId, dock::DockPosition, @@ -24,6 +20,7 @@ use workspace::{ open_new, with_active_or_new_workspace, }; +mod basics_page; mod editing_page; mod welcome; @@ -205,23 +202,6 @@ pub fn show_onboarding_view(app_state: Arc, cx: &mut App) -> Task ThemeMode { - let settings = ThemeSettings::get_global(cx); - settings - .theme_selection - .as_ref() - .and_then(|selection| selection.mode()) - .unwrap_or_default() -} - -fn write_theme_selection(theme_mode: ThemeMode, cx: &App) { - let fs = ::global(cx); - - update_settings_file::(fs, cx, move |settings, _| { - settings.set_mode(theme_mode); - }); -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum SelectedPage { Basics, @@ -246,7 +226,7 @@ impl Onboarding { }) } - fn render_page_nav( + fn render_nav_button( &mut self, page: SelectedPage, _: &mut Window, @@ -257,54 +237,119 @@ impl Onboarding { SelectedPage::Editing => "Editing", SelectedPage::AiSetup => "AI Setup", }; + let binding = match page { SelectedPage::Basics => { KeyBinding::new(vec![gpui::Keystroke::parse("cmd-1").unwrap()], cx) + .map(|kb| kb.size(rems_from_px(12.))) } SelectedPage::Editing => { KeyBinding::new(vec![gpui::Keystroke::parse("cmd-2").unwrap()], cx) + .map(|kb| kb.size(rems_from_px(12.))) } SelectedPage::AiSetup => { KeyBinding::new(vec![gpui::Keystroke::parse("cmd-3").unwrap()], cx) + .map(|kb| kb.size(rems_from_px(12.))) } }; + let selected = self.selected_page == page; + h_flex() .id(text) - .rounded_sm() - .child(text) - .child(binding) - .h_8() + .relative() + .w_full() .gap_2() .px_2() .py_0p5() - .w_full() .justify_between() - .map(|this| { - if selected { - this.bg(Color::Selected.color(cx)) - .border_l_1() - .border_color(Color::Accent.color(cx)) - } else { - this.text_color(Color::Muted.color(cx)) - } + .rounded_sm() + .when(selected, |this| { + this.child( + div() + .h_4() + .w_px() + .bg(cx.theme().colors().text_accent) + .absolute() + .left_0(), + ) }) - .hover(|style| { + .hover(|style| style.bg(cx.theme().colors().element_hover)) + .child(Label::new(text).map(|this| { if selected { - style.bg(Color::Selected.color(cx).opacity(0.6)) + this.color(Color::Default) } else { - style.bg(Color::Selected.color(cx).opacity(0.3)) + this.color(Color::Muted) } - }) + })) + .child(binding) .on_click(cx.listener(move |this, _, _, cx| { this.selected_page = page; cx.notify(); })) } + fn render_nav(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .h_full() + .w(rems_from_px(220.)) + .flex_shrink_0() + .gap_4() + .justify_between() + .child( + v_flex() + .gap_6() + .child( + h_flex() + .px_2() + .gap_4() + .child(Vector::square(VectorName::ZedLogo, rems(2.5))) + .child( + v_flex() + .child( + Headline::new("Welcome to Zed").size(HeadlineSize::Small), + ) + .child( + Label::new("The editor for what's next") + .color(Color::Muted) + .size(LabelSize::Small) + .italic(), + ), + ), + ) + .child( + v_flex() + .gap_4() + .child( + v_flex() + .py_4() + .border_y_1() + .border_color(cx.theme().colors().border_variant.opacity(0.5)) + .gap_1() + .children([ + self.render_nav_button(SelectedPage::Basics, window, cx) + .into_element(), + self.render_nav_button(SelectedPage::Editing, window, cx) + .into_element(), + self.render_nav_button(SelectedPage::AiSetup, window, cx) + .into_element(), + ]), + ) + .child(Button::new("skip_all", "Skip All")), + ), + ) + .child( + Button::new("sign_in", "Sign In") + .style(ButtonStyle::Outlined) + .full_width(), + ) + } + fn render_page(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { match self.selected_page { - SelectedPage::Basics => self.render_basics_page(window, cx).into_any_element(), + SelectedPage::Basics => { + crate::basics_page::render_basics_page(window, cx).into_any_element() + } SelectedPage::Editing => { crate::editing_page::render_editing_page(window, cx).into_any_element() } @@ -312,36 +357,6 @@ impl Onboarding { } } - fn render_basics_page(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let theme_mode = read_theme_selection(cx); - - v_flex().child( - h_flex().justify_between().child(Label::new("Theme")).child( - ToggleButtonGroup::single_row( - "theme-selector-onboarding", - [ - ToggleButtonSimple::new("Light", |_, _, cx| { - write_theme_selection(ThemeMode::Light, cx) - }), - ToggleButtonSimple::new("Dark", |_, _, cx| { - write_theme_selection(ThemeMode::Dark, cx) - }), - ToggleButtonSimple::new("System", |_, _, cx| { - write_theme_selection(ThemeMode::System, cx) - }), - ], - ) - .selected_index(match theme_mode { - ThemeMode::Light => 0, - ThemeMode::Dark => 1, - ThemeMode::System => 2, - }) - .style(ui::ToggleButtonGroupStyle::Outlined) - .button_width(rems_from_px(64.)), - ), - ) - } - fn render_ai_setup_page(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div().child("ai setup page") } @@ -352,44 +367,27 @@ impl Render for Onboarding { h_flex() .image_cache(gpui::retain_all("onboarding-page")) .key_context("onboarding-page") - .px_24() - .py_12() - .items_start() + .size_full() + .bg(cx.theme().colors().editor_background) .child( - v_flex() - .w_1_3() - .h_full() + h_flex() + .max_w(rems_from_px(1100.)) + .size_full() + .m_auto() + .py_20() + .px_12() + .items_start() + .gap_12() + .child(self.render_nav(window, cx)) .child( - h_flex() - .pt_0p5() - .child(Vector::square(VectorName::ZedLogo, rems(2.))) - .child( - v_flex() - .left_1() - .items_center() - .child(Headline::new("Welcome to Zed")) - .child( - Label::new("The editor for what's next") - .color(Color::Muted) - .italic(), - ), - ), - ) - .p_1() - .child(Divider::horizontal()) - .child( - v_flex().gap_1().children([ - self.render_page_nav(SelectedPage::Basics, window, cx) - .into_element(), - self.render_page_nav(SelectedPage::Editing, window, cx) - .into_element(), - self.render_page_nav(SelectedPage::AiSetup, window, cx) - .into_element(), - ]), + div() + .pl_12() + .border_l_1() + .border_color(cx.theme().colors().border_variant.opacity(0.5)) + .size_full() + .child(self.render_page(window, cx)), ), ) - .child(div().child(Divider::vertical()).h_full()) - .child(div().w_2_3().h_full().child(self.render_page(window, cx))) } } diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs index 189fac930f..cdb98086ca 100644 --- a/crates/ui/src/components/dropdown_menu.rs +++ b/crates/ui/src/components/dropdown_menu.rs @@ -8,6 +8,7 @@ use super::PopoverMenuHandle; pub enum DropdownStyle { #[default] Solid, + Outlined, Ghost, } @@ -147,6 +148,23 @@ impl Component for DropdownMenu { ), ], ), + example_group_with_title( + "Styles", + vec![ + single_example( + "Outlined", + DropdownMenu::new("outlined", "Outlined Dropdown", menu.clone()) + .style(DropdownStyle::Outlined) + .into_any_element(), + ), + single_example( + "Ghost", + DropdownMenu::new("ghost", "Ghost Dropdown", menu.clone()) + .style(DropdownStyle::Ghost) + .into_any_element(), + ), + ], + ), example_group_with_title( "States", vec![single_example( @@ -170,10 +188,13 @@ pub struct DropdownTriggerStyle { impl DropdownTriggerStyle { pub fn for_style(style: DropdownStyle, cx: &App) -> Self { let colors = cx.theme().colors(); + let bg = match style { DropdownStyle::Solid => colors.editor_background, + DropdownStyle::Outlined => colors.surface_background, DropdownStyle::Ghost => colors.ghost_element_background, }; + Self { bg } } } @@ -244,17 +265,24 @@ impl RenderOnce for DropdownMenuTrigger { let disabled = self.disabled; let style = DropdownTriggerStyle::for_style(self.style, cx); + let is_outlined = matches!(self.style, DropdownStyle::Outlined); h_flex() .id("dropdown-menu-trigger") - .justify_between() - .rounded_sm() - .bg(style.bg) + .min_w_20() .pl_2() .pr_1p5() .py_0p5() .gap_2() - .min_w_20() + .justify_between() + .rounded_sm() + .bg(style.bg) + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .when(is_outlined, |this| { + this.border_1() + .border_color(cx.theme().colors().border) + .overflow_hidden() + }) .map(|el| { if self.full_width { el.w_full() diff --git a/crates/ui/src/components/numeric_stepper.rs b/crates/ui/src/components/numeric_stepper.rs index ae80681732..5a84633d1b 100644 --- a/crates/ui/src/components/numeric_stepper.rs +++ b/crates/ui/src/components/numeric_stepper.rs @@ -1,17 +1,24 @@ use gpui::ClickEvent; -use crate::{Divider, IconButtonShape, prelude::*}; +use crate::{IconButtonShape, prelude::*}; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum NumericStepperStyle { + Outlined, + #[default] + Ghost, +} #[derive(IntoElement, RegisterComponent)] pub struct NumericStepper { id: ElementId, value: SharedString, + style: NumericStepperStyle, on_decrement: Box, on_increment: Box, /// Whether to reserve space for the reset button. reserve_space_for_reset: bool, on_reset: Option>, - border: bool, } impl NumericStepper { @@ -24,14 +31,19 @@ impl NumericStepper { Self { id: id.into(), value: value.into(), + style: NumericStepperStyle::default(), on_decrement: Box::new(on_decrement), on_increment: Box::new(on_increment), - border: false, reserve_space_for_reset: false, on_reset: None, } } + pub fn style(mut self, style: NumericStepperStyle) -> Self { + self.style = style; + self + } + pub fn reserve_space_for_reset(mut self, reserve_space_for_reset: bool) -> Self { self.reserve_space_for_reset = reserve_space_for_reset; self @@ -44,11 +56,6 @@ impl NumericStepper { self.on_reset = Some(Box::new(on_reset)); self } - - pub fn border(mut self) -> Self { - self.border = true; - self - } } impl RenderOnce for NumericStepper { @@ -56,6 +63,8 @@ impl RenderOnce for NumericStepper { let shape = IconButtonShape::Square; let icon_size = IconSize::Small; + let is_outlined = matches!(self.style, NumericStepperStyle::Outlined); + h_flex() .id(self.id) .gap_1() @@ -81,31 +90,65 @@ impl RenderOnce for NumericStepper { .child( h_flex() .gap_1() - .when(self.border, |this| { - this.border_1().border_color(cx.theme().colors().border) - }) - .px_1() .rounded_sm() - .bg(cx.theme().colors().editor_background) - .child( - IconButton::new("decrement", IconName::Dash) - .shape(shape) - .icon_size(icon_size) - .on_click(self.on_decrement), - ) - .when(self.border, |this| { - this.child(Divider::vertical().color(super::DividerColor::Border)) + .map(|this| { + if is_outlined { + this.overflow_hidden() + .bg(cx.theme().colors().surface_background) + .border_1() + .border_color(cx.theme().colors().border) + } else { + this.px_1().bg(cx.theme().colors().editor_background) + } }) - .child(Label::new(self.value)) - .when(self.border, |this| { - this.child(Divider::vertical().color(super::DividerColor::Border)) + .map(|decrement| { + if is_outlined { + decrement.child( + h_flex() + .id("decrement_button") + .p_1p5() + .size_full() + .justify_center() + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .border_r_1() + .border_color(cx.theme().colors().border) + .child(Icon::new(IconName::Dash).size(IconSize::Small)) + .on_click(self.on_decrement), + ) + } else { + decrement.child( + IconButton::new("decrement", IconName::Dash) + .shape(shape) + .icon_size(icon_size) + .on_click(self.on_decrement), + ) + } }) - .child( - IconButton::new("increment", IconName::Plus) - .shape(shape) - .icon_size(icon_size) - .on_click(self.on_increment), - ), + .when(is_outlined, |this| this) + .child(Label::new(self.value).mx_3()) + .map(|increment| { + if is_outlined { + increment.child( + h_flex() + .id("increment_button") + .p_1p5() + .size_full() + .justify_center() + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .border_l_1() + .border_color(cx.theme().colors().border) + .child(Icon::new(IconName::Plus).size(IconSize::Small)) + .on_click(self.on_increment), + ) + } else { + increment.child( + IconButton::new("increment", IconName::Dash) + .shape(shape) + .icon_size(icon_size) + .on_click(self.on_increment), + ) + } + }), ) } } @@ -116,7 +159,7 @@ impl Component for NumericStepper { } fn name() -> &'static str { - "NumericStepper" + "Numeric Stepper" } fn sort_name() -> &'static str { @@ -124,33 +167,39 @@ impl Component for NumericStepper { } fn description() -> Option<&'static str> { - Some("A button used to increment or decrement a numeric value. ") + Some("A button used to increment or decrement a numeric value.") } fn preview(_window: &mut Window, _cx: &mut App) -> Option { Some( v_flex() - .child(single_example( - "Borderless", - NumericStepper::new( - "numeric-stepper-component-preview", - "10", - move |_, _, _| {}, - move |_, _, _| {}, - ) - .into_any_element(), - )) - .child(single_example( - "Border", - NumericStepper::new( - "numeric-stepper-with-border-component-preview", - "10", - move |_, _, _| {}, - move |_, _, _| {}, - ) - .border() - .into_any_element(), - )) + .gap_6() + .children(vec![example_group_with_title( + "Styles", + vec![ + single_example( + "Default", + NumericStepper::new( + "numeric-stepper-component-preview", + "10", + move |_, _, _| {}, + move |_, _, _| {}, + ) + .into_any_element(), + ), + single_example( + "Outlined", + NumericStepper::new( + "numeric-stepper-with-border-component-preview", + "10", + move |_, _, _| {}, + move |_, _, _| {}, + ) + .style(NumericStepperStyle::Outlined) + .into_any_element(), + ), + ], + )]) .into_any_element(), ) }