diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 1ce236f94180845906057bb546377742a8a2c0ca..dfdea1ca5b21b5256a9bd420630d2a6f4677fc25 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -10,10 +10,8 @@ use settings::{Settings, SettingsStore, update_settings_file}; use std::sync::Arc; use theme::{ThemeMode, ThemeSettings}; use ui::{ - ButtonCommon as _, ButtonSize, ButtonStyle, Clickable as _, Color, Divider, FluentBuilder, - Headline, InteractiveElement, KeyBinding, Label, LabelCommon, ParentElement as _, - StatefulInteractiveElement, Styled, ToggleButton, Toggleable as _, Vector, VectorName, div, - h_flex, rems, v_container, v_flex, + Divider, FluentBuilder, Headline, KeyBinding, ParentElement as _, StatefulInteractiveElement, + ToggleButtonGroup, ToggleButtonSimple, Vector, VectorName, prelude::*, rems_from_px, }; use workspace::{ AppState, Workspace, WorkspaceId, @@ -222,39 +220,30 @@ impl Onboarding { fn render_basics_page(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let theme_mode = read_theme_selection(cx); - v_container().child( - h_flex() - .items_center() - .justify_between() - .child(Label::new("Theme")) - .child( - h_flex() - .rounded_md() - .child( - ToggleButton::new("light", "Light") - .style(ButtonStyle::Filled) - .size(ButtonSize::Large) - .toggle_state(theme_mode == ThemeMode::Light) - .on_click(|_, _, cx| write_theme_selection(ThemeMode::Light, cx)) - .first(), - ) - .child( - ToggleButton::new("dark", "Dark") - .style(ButtonStyle::Filled) - .size(ButtonSize::Large) - .toggle_state(theme_mode == ThemeMode::Dark) - .on_click(|_, _, cx| write_theme_selection(ThemeMode::Dark, cx)) - .last(), - ) - .child( - ToggleButton::new("system", "System") - .style(ButtonStyle::Filled) - .size(ButtonSize::Large) - .toggle_state(theme_mode == ThemeMode::System) - .on_click(|_, _, cx| write_theme_selection(ThemeMode::System, cx)) - .middle(), - ), - ), + 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.)), + ), ) } diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index eca23fe6f7584ce30fdabd5e9c1445a53ec65a5f..c6cf7ac62cd43b8e3e8611d81175a9397e0ad88f 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -1,6 +1,6 @@ use gpui::{AnyView, ClickEvent}; -use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, prelude::*}; +use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, TintColor, prelude::*}; /// The position of a [`ToggleButton`] within a group of buttons. #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -290,3 +290,627 @@ impl Component for ToggleButton { ) } } + +mod private { + pub trait Sealed {} +} + +pub trait ButtonBuilder: 'static + private::Sealed { + fn label(&self) -> impl Into; + fn icon(&self) -> Option; + fn on_click(self) -> Box; +} + +pub struct ToggleButtonSimple { + label: SharedString, + on_click: Box, +} + +impl ToggleButtonSimple { + pub fn new( + label: impl Into, + on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + Self { + label: label.into(), + on_click: Box::new(on_click), + } + } +} + +impl private::Sealed for ToggleButtonSimple {} + +impl ButtonBuilder for ToggleButtonSimple { + fn label(&self) -> impl Into { + self.label.clone() + } + + fn icon(&self) -> Option { + None + } + + fn on_click(self) -> Box { + self.on_click + } +} + +pub struct ToggleButtonWithIcon { + label: SharedString, + icon: IconName, + on_click: Box, +} + +impl ToggleButtonWithIcon { + pub fn new( + label: impl Into, + icon: IconName, + on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + Self { + label: label.into(), + icon, + on_click: Box::new(on_click), + } + } +} + +impl private::Sealed for ToggleButtonWithIcon {} + +impl ButtonBuilder for ToggleButtonWithIcon { + fn label(&self) -> impl Into { + self.label.clone() + } + + fn icon(&self) -> Option { + Some(self.icon) + } + + fn on_click(self) -> Box { + self.on_click + } +} + +struct ToggleButtonRow { + items: Vec, + index_offset: usize, + last_item_idx: usize, + is_last_row: bool, +} + +impl ToggleButtonRow { + fn new(items: Vec, index_offset: usize, is_last_row: bool) -> Self { + Self { + index_offset, + last_item_idx: index_offset + items.len() - 1, + is_last_row, + items, + } + } +} + +enum ToggleButtonGroupRows { + Single(Vec), + Multiple(Vec, Vec), +} + +impl ToggleButtonGroupRows { + fn items(self) -> impl IntoIterator> { + match self { + ToggleButtonGroupRows::Single(items) => { + vec![ToggleButtonRow::new(items, 0, true)] + } + ToggleButtonGroupRows::Multiple(first_row, second_row) => { + let row_len = first_row.len(); + vec![ + ToggleButtonRow::new(first_row, 0, false), + ToggleButtonRow::new(second_row, row_len, true), + ] + } + } + } +} + +#[derive(Clone, Copy, PartialEq)] +pub enum ToggleButtonGroupStyle { + Transparent, + Filled, + Outlined, +} + +#[derive(IntoElement)] +pub struct ToggleButtonGroup +where + T: ButtonBuilder, +{ + group_name: SharedString, + rows: ToggleButtonGroupRows, + style: ToggleButtonGroupStyle, + button_width: Rems, + selected_index: usize, +} + +impl ToggleButtonGroup { + pub fn single_row( + group_name: impl Into, + buttons: impl IntoIterator, + ) -> Self { + Self { + group_name: group_name.into(), + rows: ToggleButtonGroupRows::Single(Vec::from_iter(buttons)), + style: ToggleButtonGroupStyle::Transparent, + button_width: rems_from_px(100.), + selected_index: 0, + } + } + + pub fn multiple_rows( + group_name: impl Into, + first_row: [T; ROWS], + second_row: [T; ROWS], + ) -> Self { + Self { + group_name: group_name.into(), + rows: ToggleButtonGroupRows::Multiple( + Vec::from_iter(first_row), + Vec::from_iter(second_row), + ), + style: ToggleButtonGroupStyle::Transparent, + button_width: rems_from_px(100.), + selected_index: 0, + } + } + + pub fn style(mut self, style: ToggleButtonGroupStyle) -> Self { + self.style = style; + self + } + + pub fn button_width(mut self, button_width: Rems) -> Self { + self.button_width = button_width; + self + } + + pub fn selected_index(mut self, index: usize) -> Self { + self.selected_index = index; + self + } +} + +impl RenderOnce for ToggleButtonGroup { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let rows = self.rows.items().into_iter().map(|row| { + ( + row.items + .into_iter() + .enumerate() + .map(move |(index, item)| (index + row.index_offset, row.last_item_idx, item)) + .map(|(index, last_item_idx, item)| { + ( + ButtonLike::new((self.group_name.clone(), index)) + .when(index == self.selected_index, |this| { + this.toggle_state(true) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + }) + .rounding(None) + .when(self.style == ToggleButtonGroupStyle::Filled, |button| { + button.style(ButtonStyle::Filled) + }) + .child( + h_flex() + .min_w(self.button_width) + .gap_1p5() + .justify_center() + .when_some(item.icon(), |this, icon| { + this.child(Icon::new(icon).size(IconSize::XSmall).map( + |this| { + if index == self.selected_index { + this.color(Color::Accent) + } else { + this.color(Color::Muted) + } + }, + )) + }) + .child( + Label::new(item.label()) + .when(index == self.selected_index, |this| { + this.color(Color::Accent) + }), + ), + ) + .on_click(item.on_click()), + index == last_item_idx, + ) + }), + row.is_last_row, + ) + }); + + let is_outlined_or_filled = self.style == ToggleButtonGroupStyle::Outlined + || self.style == ToggleButtonGroupStyle::Filled; + let is_transparent = self.style == ToggleButtonGroupStyle::Transparent; + let border_color = cx.theme().colors().border.opacity(0.6); + + v_flex() + .rounded_md() + .overflow_hidden() + .map(|this| { + if is_transparent { + this.gap_px() + } else { + this.border_1().border_color(border_color) + } + }) + .children(rows.map(|(items, last_row)| { + h_flex() + .when(!is_outlined_or_filled, |this| this.gap_px()) + .when(is_outlined_or_filled && !last_row, |this| { + this.border_b_1().border_color(border_color) + }) + .children(items.map(|(item, last_item)| { + div() + .when(is_outlined_or_filled && !last_item, |this| { + this.border_r_1().border_color(border_color) + }) + .child(item) + })) + })) + } +} + +fn register_toggle_button_group() { + component::register_component::>(); +} + +component::__private::inventory::submit! { + component::ComponentFn::new(register_toggle_button_group) +} + +impl Component for ToggleButtonGroup { + fn name() -> &'static str { + "ToggleButtonGroup" + } + + fn scope() -> ComponentScope { + ComponentScope::Input + } + + fn sort_name() -> &'static str { + "ButtonG" + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option { + Some( + v_flex() + .gap_6() + .children(vec![example_group_with_title( + "Transparent Variant", + vec![ + single_example( + "Single Row Group", + ToggleButtonGroup::single_row( + "single_row_test", + [ + ToggleButtonSimple::new("First", |_, _, _| {}), + ToggleButtonSimple::new("Second", |_, _, _| {}), + ToggleButtonSimple::new("Third", |_, _, _| {}), + ], + ) + .selected_index(1) + .button_width(rems_from_px(100.)) + .into_any_element(), + ), + single_example( + "Single Row Group with icons", + ToggleButtonGroup::single_row( + "single_row_test_icon", + [ + ToggleButtonWithIcon::new( + "First", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Second", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Third", + IconName::AiZed, + |_, _, _| {}, + ), + ], + ) + .selected_index(1) + .button_width(rems_from_px(100.)) + .into_any_element(), + ), + single_example( + "Multiple Row Group", + ToggleButtonGroup::multiple_rows( + "multiple_row_test", + [ + ToggleButtonSimple::new("First", |_, _, _| {}), + ToggleButtonSimple::new("Second", |_, _, _| {}), + ToggleButtonSimple::new("Third", |_, _, _| {}), + ], + [ + ToggleButtonSimple::new("Fourth", |_, _, _| {}), + ToggleButtonSimple::new("Fifth", |_, _, _| {}), + ToggleButtonSimple::new("Sixth", |_, _, _| {}), + ], + ) + .selected_index(3) + .button_width(rems_from_px(100.)) + .into_any_element(), + ), + single_example( + "Multiple Row Group with Icons", + ToggleButtonGroup::multiple_rows( + "multiple_row_test_icons", + [ + ToggleButtonWithIcon::new( + "First", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Second", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Third", + IconName::AiZed, + |_, _, _| {}, + ), + ], + [ + ToggleButtonWithIcon::new( + "Fourth", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Fifth", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Sixth", + IconName::AiZed, + |_, _, _| {}, + ), + ], + ) + .selected_index(3) + .button_width(rems_from_px(100.)) + .into_any_element(), + ), + ], + )]) + .children(vec![example_group_with_title( + "Outlined Variant", + vec![ + single_example( + "Single Row Group", + ToggleButtonGroup::single_row( + "single_row_test_outline", + [ + ToggleButtonSimple::new("First", |_, _, _| {}), + ToggleButtonSimple::new("Second", |_, _, _| {}), + ToggleButtonSimple::new("Third", |_, _, _| {}), + ], + ) + .selected_index(1) + .style(ToggleButtonGroupStyle::Outlined) + .into_any_element(), + ), + single_example( + "Single Row Group with icons", + ToggleButtonGroup::single_row( + "single_row_test_icon_outlined", + [ + ToggleButtonWithIcon::new( + "First", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Second", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Third", + IconName::AiZed, + |_, _, _| {}, + ), + ], + ) + .selected_index(1) + .button_width(rems_from_px(100.)) + .style(ToggleButtonGroupStyle::Outlined) + .into_any_element(), + ), + single_example( + "Multiple Row Group", + ToggleButtonGroup::multiple_rows( + "multiple_row_test", + [ + ToggleButtonSimple::new("First", |_, _, _| {}), + ToggleButtonSimple::new("Second", |_, _, _| {}), + ToggleButtonSimple::new("Third", |_, _, _| {}), + ], + [ + ToggleButtonSimple::new("Fourth", |_, _, _| {}), + ToggleButtonSimple::new("Fifth", |_, _, _| {}), + ToggleButtonSimple::new("Sixth", |_, _, _| {}), + ], + ) + .selected_index(3) + .button_width(rems_from_px(100.)) + .style(ToggleButtonGroupStyle::Outlined) + .into_any_element(), + ), + single_example( + "Multiple Row Group with Icons", + ToggleButtonGroup::multiple_rows( + "multiple_row_test", + [ + ToggleButtonWithIcon::new( + "First", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Second", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Third", + IconName::AiZed, + |_, _, _| {}, + ), + ], + [ + ToggleButtonWithIcon::new( + "Fourth", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Fifth", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Sixth", + IconName::AiZed, + |_, _, _| {}, + ), + ], + ) + .selected_index(3) + .button_width(rems_from_px(100.)) + .style(ToggleButtonGroupStyle::Outlined) + .into_any_element(), + ), + ], + )]) + .children(vec![example_group_with_title( + "Filled Variant", + vec![ + single_example( + "Single Row Group", + ToggleButtonGroup::single_row( + "single_row_test_outline", + [ + ToggleButtonSimple::new("First", |_, _, _| {}), + ToggleButtonSimple::new("Second", |_, _, _| {}), + ToggleButtonSimple::new("Third", |_, _, _| {}), + ], + ) + .selected_index(2) + .style(ToggleButtonGroupStyle::Filled) + .into_any_element(), + ), + single_example( + "Single Row Group with icons", + ToggleButtonGroup::single_row( + "single_row_test_icon_outlined", + [ + ToggleButtonWithIcon::new( + "First", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Second", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Third", + IconName::AiZed, + |_, _, _| {}, + ), + ], + ) + .selected_index(1) + .button_width(rems_from_px(100.)) + .style(ToggleButtonGroupStyle::Filled) + .into_any_element(), + ), + single_example( + "Multiple Row Group", + ToggleButtonGroup::multiple_rows( + "multiple_row_test", + [ + ToggleButtonSimple::new("First", |_, _, _| {}), + ToggleButtonSimple::new("Second", |_, _, _| {}), + ToggleButtonSimple::new("Third", |_, _, _| {}), + ], + [ + ToggleButtonSimple::new("Fourth", |_, _, _| {}), + ToggleButtonSimple::new("Fifth", |_, _, _| {}), + ToggleButtonSimple::new("Sixth", |_, _, _| {}), + ], + ) + .selected_index(3) + .button_width(rems_from_px(100.)) + .style(ToggleButtonGroupStyle::Filled) + .into_any_element(), + ), + single_example( + "Multiple Row Group with Icons", + ToggleButtonGroup::multiple_rows( + "multiple_row_test", + [ + ToggleButtonWithIcon::new( + "First", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Second", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Third", + IconName::AiZed, + |_, _, _| {}, + ), + ], + [ + ToggleButtonWithIcon::new( + "Fourth", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Fifth", + IconName::AiZed, + |_, _, _| {}, + ), + ToggleButtonWithIcon::new( + "Sixth", + IconName::AiZed, + |_, _, _| {}, + ), + ], + ) + .selected_index(3) + .button_width(rems_from_px(100.)) + .style(ToggleButtonGroupStyle::Filled) + .into_any_element(), + ), + ], + )]) + .into_any_element(), + ) + } +}