Add action button component for rendering the search options

Mikayla created

Change summary

crates/search/src/buffer_search.rs |  37 +-
crates/search/src/search.rs        |  11 
crates/theme/src/components.rs     | 339 ++++++++++++++++++++++++++++++++
3 files changed, 365 insertions(+), 22 deletions(-)

Detailed changes

crates/search/src/buffer_search.rs 🔗

@@ -168,16 +168,13 @@ impl View for BufferSearchBar {
                 cx,
             )
         };
-        let render_search_option = |options: bool, icon, option| {
-            options.then(|| {
-                let is_active = self.search_options.contains(option);
-                option.as_button(
-                    is_active,
-                    icon,
-                    theme.tooltip.clone(),
-                    theme.search.option_button_component.clone(),
-                )
-            })
+        let search_option_button = |option| {
+            let is_active = self.search_options.contains(option);
+            option.as_button(
+                is_active,
+                theme.tooltip.clone(),
+                theme.search.option_button_component.clone(),
+            )
         };
         let match_count = self
             .active_searchable_item
@@ -233,16 +230,16 @@ impl View for BufferSearchBar {
             .with_child(ChildView::new(&self.query_editor, cx).flex(1., true))
             .with_child(
                 Flex::row()
-                    .with_children(render_search_option(
-                        supported_options.case,
-                        "icons/case_insensitive_12.svg",
-                        SearchOptions::CASE_SENSITIVE,
-                    ))
-                    .with_children(render_search_option(
-                        supported_options.word,
-                        "icons/word_search_12.svg",
-                        SearchOptions::WHOLE_WORD,
-                    ))
+                    .with_children(
+                        supported_options
+                            .case
+                            .then(|| search_option_button(SearchOptions::CASE_SENSITIVE)),
+                    )
+                    .with_children(
+                        supported_options
+                            .word
+                            .then(|| search_option_button(SearchOptions::WHOLE_WORD)),
+                    )
                     .flex_float()
                     .contained(),
             )

crates/search/src/search.rs 🔗

@@ -56,6 +56,14 @@ impl SearchOptions {
         }
     }
 
+    pub fn icon(&self) -> &'static str {
+        match *self {
+            SearchOptions::WHOLE_WORD => "icons/word_search_12.svg",
+            SearchOptions::CASE_SENSITIVE => "icons/case_insensitive_12.svg",
+            _ => panic!("{:?} is not a named SearchOption", self),
+        }
+    }
+
     pub fn to_toggle_action(&self) -> Box<dyn Action> {
         match *self {
             SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
@@ -78,7 +86,6 @@ impl SearchOptions {
     pub fn as_button<V: View>(
         &self,
         active: bool,
-        icon: &str,
         tooltip_style: TooltipStyle,
         button_style: ToggleIconButtonStyle,
     ) -> AnyElement<V> {
@@ -87,7 +94,7 @@ impl SearchOptions {
             format!("Toggle {}", self.label()),
             tooltip_style,
         )
-        .with_contents(theme::components::svg::Svg::new(icon.to_owned()))
+        .with_contents(theme::components::svg::Svg::new(self.icon()))
         .toggleable(active)
         .with_style(button_style)
         .into_element()

crates/theme/src/components.rs 🔗

@@ -0,0 +1,339 @@
+use gpui::elements::StyleableComponent;
+
+use crate::{Interactive, Toggleable};
+
+use self::{action_button::ButtonStyle, svg::SvgStyle, toggle::Toggle};
+
+pub type ToggleIconButtonStyle = Toggleable<Interactive<ButtonStyle<SvgStyle>>>;
+
+pub trait ComponentExt<C: StyleableComponent> {
+    fn toggleable(self, active: bool) -> Toggle<C, ()>;
+}
+
+impl<C: StyleableComponent> ComponentExt<C> for C {
+    fn toggleable(self, active: bool) -> Toggle<C, ()> {
+        Toggle::new(self, active)
+    }
+}
+
+pub mod toggle {
+    use gpui::elements::{GeneralComponent, StyleableComponent};
+
+    use crate::Toggleable;
+
+    pub struct Toggle<C, S> {
+        style: S,
+        active: bool,
+        component: C,
+    }
+
+    impl<C: StyleableComponent> Toggle<C, ()> {
+        pub fn new(component: C, active: bool) -> Self {
+            Toggle {
+                active,
+                component,
+                style: (),
+            }
+        }
+    }
+
+    impl<C: StyleableComponent> StyleableComponent for Toggle<C, ()> {
+        type Style = Toggleable<C::Style>;
+
+        type Output = Toggle<C, Self::Style>;
+
+        fn with_style(self, style: Self::Style) -> Self::Output {
+            Toggle {
+                active: self.active,
+                component: self.component,
+                style,
+            }
+        }
+    }
+
+    impl<C: StyleableComponent> GeneralComponent for Toggle<C, Toggleable<C::Style>> {
+        fn render<V: gpui::View>(
+            self,
+            v: &mut V,
+            cx: &mut gpui::ViewContext<V>,
+        ) -> gpui::AnyElement<V> {
+            self.component
+                .with_style(self.style.in_state(self.active).clone())
+                .render(v, cx)
+        }
+    }
+}
+
+pub mod action_button {
+    use std::borrow::Cow;
+
+    use gpui::{
+        elements::{
+            ContainerStyle, GeneralComponent, MouseEventHandler, StyleableComponent, TooltipStyle,
+        },
+        platform::{CursorStyle, MouseButton},
+        Action, Element, TypeTag, View,
+    };
+    use schemars::JsonSchema;
+    use serde_derive::Deserialize;
+
+    use crate::Interactive;
+
+    pub struct ActionButton<C, S> {
+        action: Box<dyn Action>,
+        tooltip: Cow<'static, str>,
+        tooltip_style: TooltipStyle,
+        tag: TypeTag,
+        contents: C,
+        style: Interactive<S>,
+    }
+
+    #[derive(Clone, Deserialize, Default, JsonSchema)]
+    pub struct ButtonStyle<C> {
+        #[serde(flatten)]
+        container: ContainerStyle,
+        button_width: Option<f32>,
+        button_height: Option<f32>,
+        #[serde(flatten)]
+        contents: C,
+    }
+
+    impl ActionButton<(), ()> {
+        pub fn new_dynamic(
+            action: Box<dyn Action>,
+            tooltip: impl Into<Cow<'static, str>>,
+            tooltip_style: TooltipStyle,
+        ) -> Self {
+            Self {
+                contents: (),
+                tag: action.type_tag(),
+                style: Interactive::new_blank(),
+                tooltip: tooltip.into(),
+                tooltip_style,
+                action,
+            }
+        }
+
+        pub fn new<A: Action + Clone>(
+            action: A,
+            tooltip: impl Into<Cow<'static, str>>,
+            tooltip_style: TooltipStyle,
+        ) -> Self {
+            Self::new_dynamic(Box::new(action), tooltip, tooltip_style)
+        }
+
+        pub fn with_contents<C: StyleableComponent>(self, contents: C) -> ActionButton<C, ()> {
+            ActionButton {
+                action: self.action,
+                tag: self.tag,
+                style: self.style,
+                tooltip: self.tooltip,
+                tooltip_style: self.tooltip_style,
+                contents,
+            }
+        }
+    }
+
+    impl<C: StyleableComponent> StyleableComponent for ActionButton<C, ()> {
+        type Style = Interactive<ButtonStyle<C::Style>>;
+        type Output = ActionButton<C, ButtonStyle<C::Style>>;
+
+        fn with_style(self, style: Self::Style) -> Self::Output {
+            ActionButton {
+                action: self.action,
+                tag: self.tag,
+                contents: self.contents,
+                tooltip: self.tooltip,
+                tooltip_style: self.tooltip_style,
+                style,
+            }
+        }
+    }
+
+    impl<C: StyleableComponent> GeneralComponent for ActionButton<C, ButtonStyle<C::Style>> {
+        fn render<V: View>(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
+            MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| {
+                let style = self.style.style_for(state);
+                let mut contents = self
+                    .contents
+                    .with_style(style.contents.to_owned())
+                    .render(v, cx)
+                    .contained()
+                    .with_style(style.container)
+                    .constrained();
+
+                if let Some(height) = style.button_height {
+                    contents = contents.with_height(height);
+                }
+
+                if let Some(width) = style.button_width {
+                    contents = contents.with_width(width);
+                }
+
+                contents.into_any()
+            })
+            .on_click(MouseButton::Left, {
+                let action = self.action.boxed_clone();
+                move |_, _, cx| {
+                    cx.window()
+                        .dispatch_action(cx.view_id(), action.as_ref(), cx);
+                }
+            })
+            .with_cursor_style(CursorStyle::PointingHand)
+            .with_dynamic_tooltip(
+                self.tag,
+                0,
+                self.tooltip,
+                Some(self.action),
+                self.tooltip_style,
+                cx,
+            )
+            .into_any()
+        }
+    }
+}
+
+pub mod svg {
+    use std::borrow::Cow;
+
+    use gpui::{
+        elements::{GeneralComponent, StyleableComponent},
+        Element,
+    };
+    use schemars::JsonSchema;
+    use serde::Deserialize;
+
+    #[derive(Clone, Default, JsonSchema)]
+    pub struct SvgStyle {
+        icon_width: f32,
+        icon_height: f32,
+        color: gpui::color::Color,
+    }
+
+    impl<'de> Deserialize<'de> for SvgStyle {
+        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+        where
+            D: serde::Deserializer<'de>,
+        {
+            #[derive(Deserialize)]
+            #[serde(untagged)]
+            pub enum IconSize {
+                IconSize { icon_size: f32 },
+                Dimensions { width: f32, height: f32 },
+            }
+
+            #[derive(Deserialize)]
+            struct SvgStyleHelper {
+                #[serde(flatten)]
+                size: IconSize,
+                color: gpui::color::Color,
+            }
+
+            let json = SvgStyleHelper::deserialize(deserializer)?;
+            let color = json.color;
+
+            let result = match json.size {
+                IconSize::IconSize { icon_size } => SvgStyle {
+                    icon_width: icon_size,
+                    icon_height: icon_size,
+                    color,
+                },
+                IconSize::Dimensions { width, height } => SvgStyle {
+                    icon_width: width,
+                    icon_height: height,
+                    color,
+                },
+            };
+
+            Ok(result)
+        }
+    }
+
+    pub struct Svg<S> {
+        path: Cow<'static, str>,
+        style: S,
+    }
+
+    impl Svg<()> {
+        pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
+            Self {
+                path: path.into(),
+                style: (),
+            }
+        }
+    }
+
+    impl StyleableComponent for Svg<()> {
+        type Style = SvgStyle;
+
+        type Output = Svg<SvgStyle>;
+
+        fn with_style(self, style: Self::Style) -> Self::Output {
+            Svg {
+                path: self.path,
+                style,
+            }
+        }
+    }
+
+    impl GeneralComponent for Svg<SvgStyle> {
+        fn render<V: gpui::View>(
+            self,
+            _: &mut V,
+            _: &mut gpui::ViewContext<V>,
+        ) -> gpui::AnyElement<V> {
+            gpui::elements::Svg::new(self.path)
+                .with_color(self.style.color)
+                .constrained()
+                .with_width(self.style.icon_width)
+                .with_height(self.style.icon_height)
+                .into_any()
+        }
+    }
+}
+
+pub mod label {
+    use std::borrow::Cow;
+
+    use gpui::{
+        elements::{GeneralComponent, LabelStyle, StyleableComponent},
+        Element,
+    };
+
+    pub struct Label<S> {
+        text: Cow<'static, str>,
+        style: S,
+    }
+
+    impl Label<()> {
+        pub fn new(text: impl Into<Cow<'static, str>>) -> Self {
+            Self {
+                text: text.into(),
+                style: (),
+            }
+        }
+    }
+
+    impl StyleableComponent for Label<()> {
+        type Style = LabelStyle;
+
+        type Output = Label<LabelStyle>;
+
+        fn with_style(self, style: Self::Style) -> Self::Output {
+            Label {
+                text: self.text,
+                style,
+            }
+        }
+    }
+
+    impl GeneralComponent for Label<LabelStyle> {
+        fn render<V: gpui::View>(
+            self,
+            _: &mut V,
+            _: &mut gpui::ViewContext<V>,
+        ) -> gpui::AnyElement<V> {
+            gpui::elements::Label::new(self.text, self.style).into_any()
+        }
+    }
+}