@@ -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<Self>) -> 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.)),
+ ),
)
}
@@ -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<SharedString>;
+ fn icon(&self) -> Option<IconName>;
+ fn on_click(self) -> Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
+}
+
+pub struct ToggleButtonSimple {
+ label: SharedString,
+ on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
+}
+
+impl ToggleButtonSimple {
+ pub fn new(
+ label: impl Into<SharedString>,
+ 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<SharedString> {
+ self.label.clone()
+ }
+
+ fn icon(&self) -> Option<IconName> {
+ None
+ }
+
+ fn on_click(self) -> Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> {
+ self.on_click
+ }
+}
+
+pub struct ToggleButtonWithIcon {
+ label: SharedString,
+ icon: IconName,
+ on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
+}
+
+impl ToggleButtonWithIcon {
+ pub fn new(
+ label: impl Into<SharedString>,
+ 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<SharedString> {
+ self.label.clone()
+ }
+
+ fn icon(&self) -> Option<IconName> {
+ Some(self.icon)
+ }
+
+ fn on_click(self) -> Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> {
+ self.on_click
+ }
+}
+
+struct ToggleButtonRow<T: ButtonBuilder> {
+ items: Vec<T>,
+ index_offset: usize,
+ last_item_idx: usize,
+ is_last_row: bool,
+}
+
+impl<T: ButtonBuilder> ToggleButtonRow<T> {
+ fn new(items: Vec<T>, 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<T: ButtonBuilder> {
+ Single(Vec<T>),
+ Multiple(Vec<T>, Vec<T>),
+}
+
+impl<T: ButtonBuilder> ToggleButtonGroupRows<T> {
+ fn items(self) -> impl IntoIterator<Item = ToggleButtonRow<T>> {
+ 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<T>
+where
+ T: ButtonBuilder,
+{
+ group_name: SharedString,
+ rows: ToggleButtonGroupRows<T>,
+ style: ToggleButtonGroupStyle,
+ button_width: Rems,
+ selected_index: usize,
+}
+
+impl<T: ButtonBuilder> ToggleButtonGroup<T> {
+ pub fn single_row(
+ group_name: impl Into<SharedString>,
+ buttons: impl IntoIterator<Item = T>,
+ ) -> 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<const ROWS: usize>(
+ group_name: impl Into<SharedString>,
+ 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<T: ButtonBuilder> RenderOnce for ToggleButtonGroup<T> {
+ 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::<ToggleButtonGroup<ToggleButtonSimple>>();
+}
+
+component::__private::inventory::submit! {
+ component::ComponentFn::new(register_toggle_button_group)
+}
+
+impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
+ 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<AnyElement> {
+ 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(),
+ )
+ }
+}