diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index bd87e1fde9d60b9f9f54b4a02314491340665da1..82dffd42fdf4b75916202bdff02dd0fa36cfa961 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -31,6 +31,7 @@ pub enum ComponentStory { Scroll, Tab, TabBar, + ToggleButton, Text, ViewportUnits, ZIndex, @@ -62,6 +63,7 @@ impl ComponentStory { Self::Text => TextStory::view(cx).into(), Self::Tab => cx.build_view(|_| ui::TabStory).into(), Self::TabBar => cx.build_view(|_| ui::TabBarStory).into(), + Self::ToggleButton => cx.build_view(|_| ui::ToggleButtonStory).into(), Self::ViewportUnits => cx.build_view(|_| crate::stories::ViewportUnitsStory).into(), Self::ZIndex => cx.build_view(|_| ZIndexStory).into(), Self::Picker => PickerStory::new(cx).into(), diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index 25e88201f47417860b85ffc6267408ea47b1c052..71aaf7780ccc93e11dbbb3f15975ea054917a320 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -2,7 +2,9 @@ mod button; pub(self) mod button_icon; mod button_like; mod icon_button; +mod toggle_button; pub use button::*; pub use button_like::*; pub use icon_button::*; +pub use toggle_button::*; diff --git a/crates/ui2/src/components/button/button_like.rs b/crates/ui2/src/components/button/button_like.rs index 19fa2b48a4c4768473df078c161873a55d488498..b25ac1d1ecf64dab50af7c4c3ca8e1d188968e7c 100644 --- a/crates/ui2/src/components/button/button_like.rs +++ b/crates/ui2/src/components/button/button_like.rs @@ -59,6 +59,13 @@ pub enum ButtonStyle { Transparent, } +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub(crate) enum ButtonLikeRounding { + All, + Left, + Right, +} + #[derive(Debug, Clone)] pub(crate) struct ButtonLikeStyles { pub background: Hsla, @@ -256,6 +263,7 @@ pub struct ButtonLike { pub(super) selected: bool, pub(super) width: Option, size: ButtonSize, + rounding: Option, tooltip: Option AnyView>>, on_click: Option>, children: SmallVec<[AnyElement; 2]>, @@ -271,11 +279,17 @@ impl ButtonLike { selected: false, width: None, size: ButtonSize::Default, + rounding: Some(ButtonLikeRounding::All), tooltip: None, children: SmallVec::new(), on_click: None, } } + + pub(crate) fn rounding(mut self, rounding: impl Into>) -> Self { + self.rounding = rounding.into(); + self + } } impl Disableable for ButtonLike { @@ -356,7 +370,11 @@ impl RenderOnce for ButtonLike { .flex_none() .h(self.size.height()) .when_some(self.width, |this, width| this.w(width).justify_center()) - .rounded_md() + .when_some(self.rounding, |this, rounding| match rounding { + ButtonLikeRounding::All => this.rounded_md(), + ButtonLikeRounding::Left => this.rounded_l_md(), + ButtonLikeRounding::Right => this.rounded_r_md(), + }) .gap_1() .map(|this| match self.size { ButtonSize::Default | ButtonSize::Compact => this.px_1(), diff --git a/crates/ui2/src/components/button/toggle_button.rs b/crates/ui2/src/components/button/toggle_button.rs new file mode 100644 index 0000000000000000000000000000000000000000..39a517111b9a1958afac86979f4adbbdd18c25bf --- /dev/null +++ b/crates/ui2/src/components/button/toggle_button.rs @@ -0,0 +1,128 @@ +use gpui::{AnyView, ClickEvent}; + +use crate::{prelude::*, ButtonLike, ButtonLikeRounding}; + +/// The position of a [`ToggleButton`] within a group of buttons. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum ToggleButtonPosition { + /// The toggle button is first in the group. + First, + + /// The toggle button is in the middle of the group (i.e., it is not the first or last toggle button). + Middle, + + /// The toggle button is last in the group. + Last, +} + +#[derive(IntoElement)] +pub struct ToggleButton { + base: ButtonLike, + position_in_group: Option, + label: SharedString, + label_color: Option, +} + +impl ToggleButton { + pub fn new(id: impl Into, label: impl Into) -> Self { + Self { + base: ButtonLike::new(id), + position_in_group: None, + label: label.into(), + label_color: None, + } + } + + pub fn color(mut self, label_color: impl Into>) -> Self { + self.label_color = label_color.into(); + self + } + + pub fn position_in_group(mut self, position: ToggleButtonPosition) -> Self { + self.position_in_group = Some(position); + self + } + + pub fn first(self) -> Self { + self.position_in_group(ToggleButtonPosition::First) + } + + pub fn middle(self) -> Self { + self.position_in_group(ToggleButtonPosition::Middle) + } + + pub fn last(self) -> Self { + self.position_in_group(ToggleButtonPosition::Last) + } +} + +impl Selectable for ToggleButton { + fn selected(mut self, selected: bool) -> Self { + self.base = self.base.selected(selected); + self + } +} + +impl Disableable for ToggleButton { + fn disabled(mut self, disabled: bool) -> Self { + self.base = self.base.disabled(disabled); + self + } +} + +impl Clickable for ToggleButton { + fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self { + self.base = self.base.on_click(handler); + self + } +} + +impl ButtonCommon for ToggleButton { + fn id(&self) -> &ElementId { + self.base.id() + } + + fn style(mut self, style: ButtonStyle) -> Self { + self.base = self.base.style(style); + self + } + + fn size(mut self, size: ButtonSize) -> Self { + self.base = self.base.size(size); + self + } + + fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self { + self.base = self.base.tooltip(tooltip); + self + } +} + +impl RenderOnce for ToggleButton { + type Rendered = ButtonLike; + + fn render(self, _cx: &mut WindowContext) -> Self::Rendered { + let is_disabled = self.base.disabled; + let is_selected = self.base.selected; + + let label_color = if is_disabled { + Color::Disabled + } else if is_selected { + Color::Selected + } else { + self.label_color.unwrap_or_default() + }; + + self.base + .when_some(self.position_in_group, |this, position| match position { + ToggleButtonPosition::First => this.rounding(ButtonLikeRounding::Left), + ToggleButtonPosition::Middle => this.rounding(None), + ToggleButtonPosition::Last => this.rounding(ButtonLikeRounding::Right), + }) + .child( + Label::new(self.label) + .color(label_color) + .line_height_style(LineHeightStyle::UILabel), + ) + } +} diff --git a/crates/ui2/src/components/stories.rs b/crates/ui2/src/components/stories.rs index f02787a1db6f9233985cac41de47f877b878917a..f321a7525fb00efc2e9851d77aa1780df68c3311 100644 --- a/crates/ui2/src/components/stories.rs +++ b/crates/ui2/src/components/stories.rs @@ -12,6 +12,7 @@ mod list_header; mod list_item; mod tab; mod tab_bar; +mod toggle_button; pub use avatar::*; pub use button::*; @@ -27,3 +28,4 @@ pub use list_header::*; pub use list_item::*; pub use tab::*; pub use tab_bar::*; +pub use toggle_button::*; diff --git a/crates/ui2/src/components/stories/toggle_button.rs b/crates/ui2/src/components/stories/toggle_button.rs new file mode 100644 index 0000000000000000000000000000000000000000..6fe741ada3b8059aa0d946025e4abdddeb443335 --- /dev/null +++ b/crates/ui2/src/components/stories/toggle_button.rs @@ -0,0 +1,89 @@ +use gpui::{Component, Render}; +use story::{StoryContainer, StoryItem, StorySection}; + +use crate::{prelude::*, ToggleButton}; + +pub struct ToggleButtonStory; + +impl Render for ToggleButtonStory { + type Element = Component; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + StoryContainer::new( + "Toggle Button", + "crates/ui2/src/components/stories/toggle_button.rs", + ) + .child( + StorySection::new().child( + StoryItem::new( + "Default", + ToggleButton::new("default_toggle_button", "Hello"), + ) + .description("Displays a toggle button.") + .usage(""), + ), + ) + .child( + StorySection::new().child( + StoryItem::new( + "Toggle button group", + h_stack() + .child( + ToggleButton::new(1, "Apple") + .style(ButtonStyle::Filled) + .first(), + ) + .child( + ToggleButton::new(2, "Banana") + .style(ButtonStyle::Filled) + .middle(), + ) + .child( + ToggleButton::new(3, "Cherry") + .style(ButtonStyle::Filled) + .middle(), + ) + .child( + ToggleButton::new(4, "Dragonfruit") + .style(ButtonStyle::Filled) + .last(), + ), + ) + .description("Displays a group of toggle buttons.") + .usage(""), + ), + ) + .child( + StorySection::new().child( + StoryItem::new( + "Toggle button group with selection", + h_stack() + .child( + ToggleButton::new(1, "Apple") + .style(ButtonStyle::Filled) + .first(), + ) + .child( + ToggleButton::new(2, "Banana") + .style(ButtonStyle::Filled) + .selected(true) + .middle(), + ) + .child( + ToggleButton::new(3, "Cherry") + .style(ButtonStyle::Filled) + .middle(), + ) + .child( + ToggleButton::new(4, "Dragonfruit") + .style(ButtonStyle::Filled) + .last(), + ), + ) + .description("Displays a group of toggle buttons.") + .usage(""), + ), + ) + .into_element() + } +}