onboarding: Refine page and component designs (#35387)

Includes adding new variants to the Dropdown and Numeric Stepper
components.

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2025-07-31 02:32:18 -03:00 committed by GitHub
parent b1a7993544
commit 5488398986
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 563 additions and 327 deletions

View file

@ -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 = <dyn Fs>::global(cx);
update_settings_file::<ThemeSettings>(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())
}

View file

@ -6,10 +6,8 @@ use project::project_settings::ProjectSettings;
use settings::{Settings as _, update_settings_file}; use settings::{Settings as _, update_settings_file};
use theme::{FontFamilyCache, FontFamilyName, ThemeSettings}; use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
use ui::{ use ui::{
Clickable, ContextMenu, DropdownMenu, IconButton, Label, LabelCommon, LabelSize, ButtonLike, ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup,
NumericStepper, ParentElement, SharedString, Styled, SwitchColor, SwitchField, ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, prelude::*,
ToggleButtonGroup, ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, div, h_flex, px,
v_flex,
}; };
use crate::{ImportCursorSettings, ImportVsCodeSettings}; 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 theme_settings = ThemeSettings::get_global(cx);
let ui_font_size = theme_settings.ui_font_size(cx); let ui_font_size = theme_settings.ui_font_size(cx);
let font_family = theme_settings.buffer_font.family.clone(); let font_family = theme_settings.buffer_font.family.clone();
let buffer_font_size = theme_settings.buffer_font_size(cx); let buffer_font_size = theme_settings.buffer_font_size(cx);
v_flex() h_flex()
.w_full()
.gap_4() .gap_4()
.child(Label::new("Import Settings").size(LabelSize::Large))
.child( .child(
Label::new("Automatically pull your settings from other editors.") v_flex()
.size(LabelSize::Small), .w_full()
) .gap_1()
.child( .child(Label::new("UI Font"))
h_flex()
.child( .child(
IconButton::new("import-vs-code-settings", ui::IconName::Code).on_click( h_flex()
|_, window, cx| { .w_full()
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()
.justify_between() .justify_between()
.gap_1() .gap_2()
.child(Label::new("UI Font"))
.child( .child(
h_flex() DropdownMenu::new(
.justify_between() "ui-font-family",
.gap_2() theme_settings.ui_font.family.clone(),
.child(div().min_w(px(120.)).child(DropdownMenu::new( ContextMenu::build(window, cx, |mut menu, _, cx| {
"ui-font-family", let font_family_cache = FontFamilyCache::global(cx);
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) { for font_name in font_family_cache.list_font_families(cx) {
menu = menu.custom_entry( menu = menu.custom_entry(
{ {
let font_name = font_name.clone(); let font_name = font_name.clone();
move |_window, _cx| { move |_window, _cx| {
Label::new(font_name.clone()) Label::new(font_name.clone()).into_any_element()
.into_any_element() }
} },
}, {
{ let font_name = font_name.clone();
let font_name = font_name.clone(); move |_window, cx| {
move |_window, cx| { write_ui_font_family(font_name.clone(), cx);
write_ui_font_family(font_name.clone(), cx); }
} },
}, )
) }
}
menu menu
}), }),
))) )
.child( .style(ui::DropdownStyle::Outlined)
NumericStepper::new( .full_width(true),
"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"))
.child( .child(
h_flex() NumericStepper::new(
.justify_between() "ui-font-size",
.gap_2() ui_font_size.to_string(),
.child(DropdownMenu::new( move |_, _, cx| {
"buffer-font-family", write_ui_font_size(ui_font_size - px(1.), cx);
font_family, },
ContextMenu::build(window, cx, |mut menu, _, cx| { move |_, _, cx| {
let font_family_cache = FontFamilyCache::global(cx); write_ui_font_size(ui_font_size + px(1.), cx);
},
for font_name in font_family_cache.list_font_families(cx) { )
menu = menu.custom_entry( .style(ui::NumericStepperStyle::Outlined),
{
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(),
),
), ),
), ),
) )
.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( .child(
h_flex() h_flex()
.items_start()
.justify_between() .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( .child(
ToggleButtonGroup::single_row( ToggleButtonGroup::single_row(
"onboarding-show-mini-map", "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.)), .button_width(ui::rems_from_px(64.)),
), ),
) )
.child( .child(SwitchField::new(
SwitchField::new( "onboarding-enable-inlay-hints",
"onboarding-enable-inlay-hints", "Inlay Hints",
"Inlay Hints", "See parameter names for function and method calls inline.",
"See parameter names for function and method calls inline.", if read_inlay_hints(cx) {
if read_inlay_hints(cx) { ui::ToggleState::Selected
ui::ToggleState::Selected } else {
} else { ui::ToggleState::Unselected
ui::ToggleState::Unselected },
}, |toggle_state, _, cx| {
|toggle_state, _, cx| { write_inlay_hints(toggle_state == &ToggleState::Selected, cx);
write_inlay_hints(toggle_state == &ToggleState::Selected, cx); },
}, ))
) .child(SwitchField::new(
.color(SwitchColor::Accent), "onboarding-git-blame-switch",
) "Git Blame",
.child( "See who committed each line on a given file.",
SwitchField::new( if read_git_blame(cx) {
"onboarding-git-blame-switch", ui::ToggleState::Selected
"Git Blame", } else {
"See who committed each line on a given file.", ui::ToggleState::Unselected
if read_git_blame(cx) { },
ui::ToggleState::Selected |toggle_state, _, cx| {
} else { set_git_blame(toggle_state == &ToggleState::Selected, cx);
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()
.color(SwitchColor::Accent), .gap_4()
) .child(render_import_settings_section())
.child(render_popular_settings_section(window, cx))
} }

View file

@ -10,13 +10,9 @@ use gpui::{
}; };
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use settings::{Settings, SettingsStore, VsCodeSettingsSource, update_settings_file}; use settings::{SettingsStore, VsCodeSettingsSource};
use std::sync::Arc; use std::sync::Arc;
use theme::{ThemeMode, ThemeSettings}; use ui::{FluentBuilder, KeyBinding, Vector, VectorName, prelude::*, rems_from_px};
use ui::{
Divider, FluentBuilder, Headline, KeyBinding, ParentElement as _, StatefulInteractiveElement,
ToggleButtonGroup, ToggleButtonSimple, Vector, VectorName, prelude::*, rems_from_px,
};
use workspace::{ use workspace::{
AppState, Workspace, WorkspaceId, AppState, Workspace, WorkspaceId,
dock::DockPosition, dock::DockPosition,
@ -24,6 +20,7 @@ use workspace::{
open_new, with_active_or_new_workspace, open_new, with_active_or_new_workspace,
}; };
mod basics_page;
mod editing_page; mod editing_page;
mod welcome; mod welcome;
@ -205,23 +202,6 @@ pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyh
) )
} }
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 = <dyn Fs>::global(cx);
update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
settings.set_mode(theme_mode);
});
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SelectedPage { enum SelectedPage {
Basics, Basics,
@ -246,7 +226,7 @@ impl Onboarding {
}) })
} }
fn render_page_nav( fn render_nav_button(
&mut self, &mut self,
page: SelectedPage, page: SelectedPage,
_: &mut Window, _: &mut Window,
@ -257,54 +237,119 @@ impl Onboarding {
SelectedPage::Editing => "Editing", SelectedPage::Editing => "Editing",
SelectedPage::AiSetup => "AI Setup", SelectedPage::AiSetup => "AI Setup",
}; };
let binding = match page { let binding = match page {
SelectedPage::Basics => { SelectedPage::Basics => {
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-1").unwrap()], cx) KeyBinding::new(vec![gpui::Keystroke::parse("cmd-1").unwrap()], cx)
.map(|kb| kb.size(rems_from_px(12.)))
} }
SelectedPage::Editing => { SelectedPage::Editing => {
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-2").unwrap()], cx) KeyBinding::new(vec![gpui::Keystroke::parse("cmd-2").unwrap()], cx)
.map(|kb| kb.size(rems_from_px(12.)))
} }
SelectedPage::AiSetup => { SelectedPage::AiSetup => {
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-3").unwrap()], cx) KeyBinding::new(vec![gpui::Keystroke::parse("cmd-3").unwrap()], cx)
.map(|kb| kb.size(rems_from_px(12.)))
} }
}; };
let selected = self.selected_page == page; let selected = self.selected_page == page;
h_flex() h_flex()
.id(text) .id(text)
.rounded_sm() .relative()
.child(text) .w_full()
.child(binding)
.h_8()
.gap_2() .gap_2()
.px_2() .px_2()
.py_0p5() .py_0p5()
.w_full()
.justify_between() .justify_between()
.map(|this| { .rounded_sm()
if selected { .when(selected, |this| {
this.bg(Color::Selected.color(cx)) this.child(
.border_l_1() div()
.border_color(Color::Accent.color(cx)) .h_4()
} else { .w_px()
this.text_color(Color::Muted.color(cx)) .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 { if selected {
style.bg(Color::Selected.color(cx).opacity(0.6)) this.color(Color::Default)
} else { } else {
style.bg(Color::Selected.color(cx).opacity(0.3)) this.color(Color::Muted)
} }
}) }))
.child(binding)
.on_click(cx.listener(move |this, _, _, cx| { .on_click(cx.listener(move |this, _, _, cx| {
this.selected_page = page; this.selected_page = page;
cx.notify(); cx.notify();
})) }))
} }
fn render_nav(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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<Self>) -> AnyElement { fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
match self.selected_page { 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 => { SelectedPage::Editing => {
crate::editing_page::render_editing_page(window, cx).into_any_element() 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<Self>) -> 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<Self>) -> impl IntoElement { fn render_ai_setup_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().child("ai setup page") div().child("ai setup page")
} }
@ -352,44 +367,27 @@ impl Render for Onboarding {
h_flex() h_flex()
.image_cache(gpui::retain_all("onboarding-page")) .image_cache(gpui::retain_all("onboarding-page"))
.key_context("onboarding-page") .key_context("onboarding-page")
.px_24() .size_full()
.py_12() .bg(cx.theme().colors().editor_background)
.items_start()
.child( .child(
v_flex() h_flex()
.w_1_3() .max_w(rems_from_px(1100.))
.h_full() .size_full()
.m_auto()
.py_20()
.px_12()
.items_start()
.gap_12()
.child(self.render_nav(window, cx))
.child( .child(
h_flex() div()
.pt_0p5() .pl_12()
.child(Vector::square(VectorName::ZedLogo, rems(2.))) .border_l_1()
.child( .border_color(cx.theme().colors().border_variant.opacity(0.5))
v_flex() .size_full()
.left_1() .child(self.render_page(window, cx)),
.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(),
]),
), ),
) )
.child(div().child(Divider::vertical()).h_full())
.child(div().w_2_3().h_full().child(self.render_page(window, cx)))
} }
} }

View file

@ -8,6 +8,7 @@ use super::PopoverMenuHandle;
pub enum DropdownStyle { pub enum DropdownStyle {
#[default] #[default]
Solid, Solid,
Outlined,
Ghost, 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( example_group_with_title(
"States", "States",
vec![single_example( vec![single_example(
@ -170,10 +188,13 @@ pub struct DropdownTriggerStyle {
impl DropdownTriggerStyle { impl DropdownTriggerStyle {
pub fn for_style(style: DropdownStyle, cx: &App) -> Self { pub fn for_style(style: DropdownStyle, cx: &App) -> Self {
let colors = cx.theme().colors(); let colors = cx.theme().colors();
let bg = match style { let bg = match style {
DropdownStyle::Solid => colors.editor_background, DropdownStyle::Solid => colors.editor_background,
DropdownStyle::Outlined => colors.surface_background,
DropdownStyle::Ghost => colors.ghost_element_background, DropdownStyle::Ghost => colors.ghost_element_background,
}; };
Self { bg } Self { bg }
} }
} }
@ -244,17 +265,24 @@ impl RenderOnce for DropdownMenuTrigger {
let disabled = self.disabled; let disabled = self.disabled;
let style = DropdownTriggerStyle::for_style(self.style, cx); let style = DropdownTriggerStyle::for_style(self.style, cx);
let is_outlined = matches!(self.style, DropdownStyle::Outlined);
h_flex() h_flex()
.id("dropdown-menu-trigger") .id("dropdown-menu-trigger")
.justify_between() .min_w_20()
.rounded_sm()
.bg(style.bg)
.pl_2() .pl_2()
.pr_1p5() .pr_1p5()
.py_0p5() .py_0p5()
.gap_2() .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| { .map(|el| {
if self.full_width { if self.full_width {
el.w_full() el.w_full()

View file

@ -1,17 +1,24 @@
use gpui::ClickEvent; 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)] #[derive(IntoElement, RegisterComponent)]
pub struct NumericStepper { pub struct NumericStepper {
id: ElementId, id: ElementId,
value: SharedString, value: SharedString,
style: NumericStepperStyle,
on_decrement: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>, on_decrement: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_increment: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>, on_increment: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
/// Whether to reserve space for the reset button. /// Whether to reserve space for the reset button.
reserve_space_for_reset: bool, reserve_space_for_reset: bool,
on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>, on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
border: bool,
} }
impl NumericStepper { impl NumericStepper {
@ -24,14 +31,19 @@ impl NumericStepper {
Self { Self {
id: id.into(), id: id.into(),
value: value.into(), value: value.into(),
style: NumericStepperStyle::default(),
on_decrement: Box::new(on_decrement), on_decrement: Box::new(on_decrement),
on_increment: Box::new(on_increment), on_increment: Box::new(on_increment),
border: false,
reserve_space_for_reset: false, reserve_space_for_reset: false,
on_reset: None, 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 { pub fn reserve_space_for_reset(mut self, reserve_space_for_reset: bool) -> Self {
self.reserve_space_for_reset = reserve_space_for_reset; self.reserve_space_for_reset = reserve_space_for_reset;
self self
@ -44,11 +56,6 @@ impl NumericStepper {
self.on_reset = Some(Box::new(on_reset)); self.on_reset = Some(Box::new(on_reset));
self self
} }
pub fn border(mut self) -> Self {
self.border = true;
self
}
} }
impl RenderOnce for NumericStepper { impl RenderOnce for NumericStepper {
@ -56,6 +63,8 @@ impl RenderOnce for NumericStepper {
let shape = IconButtonShape::Square; let shape = IconButtonShape::Square;
let icon_size = IconSize::Small; let icon_size = IconSize::Small;
let is_outlined = matches!(self.style, NumericStepperStyle::Outlined);
h_flex() h_flex()
.id(self.id) .id(self.id)
.gap_1() .gap_1()
@ -81,31 +90,65 @@ impl RenderOnce for NumericStepper {
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
.when(self.border, |this| {
this.border_1().border_color(cx.theme().colors().border)
})
.px_1()
.rounded_sm() .rounded_sm()
.bg(cx.theme().colors().editor_background) .map(|this| {
.child( if is_outlined {
IconButton::new("decrement", IconName::Dash) this.overflow_hidden()
.shape(shape) .bg(cx.theme().colors().surface_background)
.icon_size(icon_size) .border_1()
.on_click(self.on_decrement), .border_color(cx.theme().colors().border)
) } else {
.when(self.border, |this| { this.px_1().bg(cx.theme().colors().editor_background)
this.child(Divider::vertical().color(super::DividerColor::Border)) }
}) })
.child(Label::new(self.value)) .map(|decrement| {
.when(self.border, |this| { if is_outlined {
this.child(Divider::vertical().color(super::DividerColor::Border)) 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( .when(is_outlined, |this| this)
IconButton::new("increment", IconName::Plus) .child(Label::new(self.value).mx_3())
.shape(shape) .map(|increment| {
.icon_size(icon_size) if is_outlined {
.on_click(self.on_increment), 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 { fn name() -> &'static str {
"NumericStepper" "Numeric Stepper"
} }
fn sort_name() -> &'static str { fn sort_name() -> &'static str {
@ -124,33 +167,39 @@ impl Component for NumericStepper {
} }
fn description() -> Option<&'static str> { 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<AnyElement> { fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some( Some(
v_flex() v_flex()
.child(single_example( .gap_6()
"Borderless", .children(vec![example_group_with_title(
NumericStepper::new( "Styles",
"numeric-stepper-component-preview", vec![
"10", single_example(
move |_, _, _| {}, "Default",
move |_, _, _| {}, NumericStepper::new(
) "numeric-stepper-component-preview",
.into_any_element(), "10",
)) move |_, _, _| {},
.child(single_example( move |_, _, _| {},
"Border", )
NumericStepper::new( .into_any_element(),
"numeric-stepper-with-border-component-preview", ),
"10", single_example(
move |_, _, _| {}, "Outlined",
move |_, _, _| {}, NumericStepper::new(
) "numeric-stepper-with-border-component-preview",
.border() "10",
.into_any_element(), move |_, _, _| {},
)) move |_, _, _| {},
)
.style(NumericStepperStyle::Outlined)
.into_any_element(),
),
],
)])
.into_any_element(), .into_any_element(),
) )
} }