diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index 06e1b625bc..eae741afde 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -1,7 +1,7 @@ use crate::{ history::SearchHistory, mode::{next_mode, SearchMode}, - search_bar::{render_nav_button, render_search_mode_button}, + search_bar::render_nav_button, ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, @@ -21,7 +21,7 @@ use settings::Settings; use std::{any::Any, sync::Arc}; use theme::ThemeSettings; -use ui::{h_stack, prelude::*, Icon, IconButton, IconElement, Tooltip}; +use ui::{h_stack, prelude::*, Icon, IconButton, IconElement, ToggleButton, Tooltip}; use util::ResultExt; use workspace::{ item::ItemHandle, @@ -165,11 +165,6 @@ impl Render for BufferSearchBar { editor.set_placeholder_text("Replace with...", cx); }); - let search_button_for_mode = |mode| { - let is_active = self.current_mode == mode; - - render_search_mode_button(mode, is_active) - }; let match_count = self .active_searchable_item .as_ref() @@ -257,8 +252,40 @@ impl Render for BufferSearchBar { .flex_none() .child( h_stack() - .child(search_button_for_mode(SearchMode::Text)) - .child(search_button_for_mode(SearchMode::Regex)), + .child( + ToggleButton::new("search-mode-text", SearchMode::Text.label()) + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .selected(self.current_mode == SearchMode::Text) + .on_click(cx.listener(move |_, _event, cx| { + cx.dispatch_action(SearchMode::Text.action()) + })) + .tooltip(|cx| { + Tooltip::for_action( + SearchMode::Text.tooltip(), + &*SearchMode::Text.action(), + cx, + ) + }) + .first(), + ) + .child( + ToggleButton::new("search-mode-regex", SearchMode::Regex.label()) + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .selected(self.current_mode == SearchMode::Regex) + .on_click(cx.listener(move |_, _event, cx| { + cx.dispatch_action(SearchMode::Regex.action()) + })) + .tooltip(|cx| { + Tooltip::for_action( + SearchMode::Regex.tooltip(), + &*SearchMode::Regex.action(), + cx, + ) + }) + .last(), + ), ) .when(supported_options.replacement, |this| { this.child( diff --git a/crates/search2/src/search_bar.rs b/crates/search2/src/search_bar.rs index dcc46ac228..628be3112e 100644 --- a/crates/search2/src/search_bar.rs +++ b/crates/search2/src/search_bar.rs @@ -1,8 +1,6 @@ use gpui::{Action, IntoElement}; +use ui::IconButton; use ui::{prelude::*, Tooltip}; -use ui::{Button, IconButton}; - -use crate::mode::SearchMode; pub(super) fn render_nav_button( icon: ui::Icon, @@ -18,19 +16,3 @@ pub(super) fn render_nav_button( .tooltip(move |cx| Tooltip::for_action(tooltip, action, cx)) .disabled(!active) } - -pub(crate) fn render_search_mode_button(mode: SearchMode, is_active: bool) -> Button { - Button::new(mode.label(), mode.label()) - .selected(is_active) - .on_click({ - let action = mode.action(); - move |_, cx| { - cx.dispatch_action(action.boxed_clone()); - } - }) - .tooltip({ - let action = mode.action(); - let tooltip_text = mode.tooltip(); - move |cx| Tooltip::for_action(tooltip_text.clone(), &*action, cx) - }) -} diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index bd87e1fde9..82dffd42fd 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 25e88201f4..71aaf7780c 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 19fa2b48a4..44e18e850d 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, @@ -226,6 +233,7 @@ impl ButtonStyle { /// that are consistently sized with buttons. #[derive(Default, PartialEq, Clone, Copy)] pub enum ButtonSize { + Large, #[default] Default, Compact, @@ -235,6 +243,7 @@ pub enum ButtonSize { impl ButtonSize { fn height(self) -> Rems { match self { + ButtonSize::Large => rems(32. / 16.), ButtonSize::Default => rems(22. / 16.), ButtonSize::Compact => rems(18. / 16.), ButtonSize::None => rems(16. / 16.), @@ -256,6 +265,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 +281,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,9 +372,14 @@ 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::Large => this.px_2(), ButtonSize::Default | ButtonSize::Compact => this.px_1(), ButtonSize::None => this, }) 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 0000000000..39a517111b --- /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 f02787a1db..f321a7525f 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 0000000000..0a4ea5f83e --- /dev/null +++ b/crates/ui2/src/components/stories/toggle_button.rs @@ -0,0 +1,97 @@ +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) + .size(ButtonSize::Large) + .first(), + ) + .child( + ToggleButton::new(2, "Banana") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .middle(), + ) + .child( + ToggleButton::new(3, "Cherry") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .middle(), + ) + .child( + ToggleButton::new(4, "Dragonfruit") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .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) + .size(ButtonSize::Large) + .first(), + ) + .child( + ToggleButton::new(2, "Banana") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .selected(true) + .middle(), + ) + .child( + ToggleButton::new(3, "Cherry") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .middle(), + ) + .child( + ToggleButton::new(4, "Dragonfruit") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .last(), + ), + ) + .description("Displays a group of toggle buttons.") + .usage(""), + ), + ) + .into_element() + } +}