Add action button component for rendering the search options

Mikayla created

Change summary

crates/gpui/src/app.rs                | 29 +++++++++++++-
crates/gpui/src/app/action.rs         |  7 +++
crates/gpui/src/elements.rs           | 18 ++++++++-
crates/gpui/src/elements/component.rs | 34 +++++++++++++++++
crates/gpui/src/elements/tooltip.rs   | 22 ++++++++--
crates/search/src/buffer_search.rs    | 31 ++++++---------
crates/search/src/search.rs           | 26 ++++++++++++
crates/theme/src/theme.rs             | 29 +++++++++++++-
crates/theme/src/ui.rs                |  4 +-
crates/workspace/src/pane.rs          |  1 
styles/src/style_tree/search.ts       | 57 +++++++++++++++++++++++++++++
11 files changed, 223 insertions(+), 35 deletions(-)

Detailed changes

crates/gpui/src/app.rs 🔗

@@ -3313,11 +3313,20 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
         &mut self,
         element_id: usize,
         initial: T,
+    ) -> ElementStateHandle<T> {
+        self.element_state_dynamic(TypeTag::new::<Tag>(), element_id, initial)
+    }
+
+    pub fn element_state_dynamic<T: 'static>(
+        &mut self,
+        tag: TypeTag,
+        element_id: usize,
+        initial: T,
     ) -> ElementStateHandle<T> {
         let id = ElementStateId {
             view_id: self.view_id(),
             element_id,
-            tag: TypeId::of::<Tag>(),
+            tag,
         };
         self.element_states
             .entry(id)
@@ -3331,11 +3340,20 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
     ) -> ElementStateHandle<T> {
         self.element_state::<Tag, T>(element_id, T::default())
     }
+
+    pub fn default_element_state_dynamic<T: 'static + Default>(
+        &mut self,
+        tag: TypeTag,
+        element_id: usize,
+    ) -> ElementStateHandle<T> {
+        self.element_state_dynamic::<T>(tag, element_id, T::default())
+    }
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
 pub struct TypeTag {
     tag: TypeId,
+    composed: Option<TypeId>,
     #[cfg(debug_assertions)]
     tag_type_name: &'static str,
 }
@@ -3344,6 +3362,7 @@ impl TypeTag {
     pub fn new<Tag: 'static>() -> Self {
         Self {
             tag: TypeId::of::<Tag>(),
+            composed: None,
             #[cfg(debug_assertions)]
             tag_type_name: std::any::type_name::<Tag>(),
         }
@@ -3352,11 +3371,17 @@ impl TypeTag {
     pub fn dynamic(tag: TypeId, #[cfg(debug_assertions)] type_name: &'static str) -> Self {
         Self {
             tag,
+            composed: None,
             #[cfg(debug_assertions)]
             tag_type_name: type_name,
         }
     }
 
+    pub fn compose(mut self, other: TypeTag) -> Self {
+        self.composed = Some(other.tag);
+        self
+    }
+
     #[cfg(debug_assertions)]
     pub(crate) fn type_name(&self) -> &'static str {
         self.tag_type_name
@@ -4751,7 +4776,7 @@ impl Hash for AnyWeakViewHandle {
 pub struct ElementStateId {
     view_id: usize,
     element_id: usize,
-    tag: TypeId,
+    tag: TypeTag,
 }
 
 pub struct ElementStateHandle<T> {

crates/gpui/src/app/action.rs 🔗

@@ -1,10 +1,13 @@
 use std::any::{Any, TypeId};
 
+use crate::TypeTag;
+
 pub trait Action: 'static {
     fn id(&self) -> TypeId;
     fn namespace(&self) -> &'static str;
     fn name(&self) -> &'static str;
     fn as_any(&self) -> &dyn Any;
+    fn type_tag(&self) -> TypeTag;
     fn boxed_clone(&self) -> Box<dyn Action>;
     fn eq(&self, other: &dyn Action) -> bool;
 
@@ -107,6 +110,10 @@ macro_rules! __impl_action {
                 }
             }
 
+            fn type_tag(&self) -> $crate::TypeTag {
+                $crate::TypeTag::new::<Self>()
+            }
+
             $from_json_fn
         }
     };

crates/gpui/src/elements.rs 🔗

@@ -34,8 +34,8 @@ use crate::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext,
-    WeakViewHandle, WindowContext,
+    json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, TypeTag, View,
+    ViewContext, WeakViewHandle, WindowContext,
 };
 use anyhow::{anyhow, Result};
 use collections::HashMap;
@@ -172,6 +172,20 @@ pub trait Element<V: View>: 'static {
         FlexItem::new(self.into_any()).float()
     }
 
+    fn with_dynamic_tooltip(
+        self,
+        tag: TypeTag,
+        id: usize,
+        text: impl Into<Cow<'static, str>>,
+        action: Option<Box<dyn Action>>,
+        style: TooltipStyle,
+        cx: &mut ViewContext<V>,
+    ) -> Tooltip<V>
+    where
+        Self: 'static + Sized,
+    {
+        Tooltip::new_dynamic(tag, id, text, action, style, self.into_any(), cx)
+    }
     fn with_tooltip<Tag: 'static>(
         self,
         id: usize,

crates/gpui/src/elements/component.rs 🔗

@@ -7,6 +7,34 @@ use crate::{
     ViewContext,
 };
 
+use super::Empty;
+
+pub trait GeneralComponent {
+    fn render<V: View>(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
+}
+
+pub trait StyleableComponent {
+    type Style: Clone;
+    type Output: GeneralComponent;
+
+    fn with_style(self, style: Self::Style) -> Self::Output;
+}
+
+impl GeneralComponent for () {
+    fn render<V: View>(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
+        Empty::new().into_any()
+    }
+}
+
+impl StyleableComponent for () {
+    type Style = ();
+    type Output = ();
+
+    fn with_style(self, _: Self::Style) -> Self::Output {
+        ()
+    }
+}
+
 pub trait Component<V: View> {
     fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
 
@@ -18,6 +46,12 @@ pub trait Component<V: View> {
     }
 }
 
+impl<V: View, C: GeneralComponent> Component<V> for C {
+    fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
+        self.render(v, cx)
+    }
+}
+
 pub struct ComponentAdapter<V, E> {
     component: Option<E>,
     phantom: PhantomData<V>,

crates/gpui/src/elements/tooltip.rs 🔗

@@ -7,7 +7,7 @@ use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::json,
     Action, Axis, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
-    Task, View, ViewContext,
+    Task, TypeTag, View, ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -61,11 +61,23 @@ impl<V: View> Tooltip<V> {
         child: AnyElement<V>,
         cx: &mut ViewContext<V>,
     ) -> Self {
-        struct ElementState<Tag>(Tag);
-        struct MouseEventHandlerState<Tag>(Tag);
+        Self::new_dynamic(TypeTag::new::<Tag>(), id, text, action, style, child, cx)
+    }
+
+    pub fn new_dynamic(
+        mut tag: TypeTag,
+        id: usize,
+        text: impl Into<Cow<'static, str>>,
+        action: Option<Box<dyn Action>>,
+        style: TooltipStyle,
+        child: AnyElement<V>,
+        cx: &mut ViewContext<V>,
+    ) -> Self {
+        tag = tag.compose(TypeTag::new::<Self>());
+
         let focused_view_id = cx.focused_view_id();
 
-        let state_handle = cx.default_element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
+        let state_handle = cx.default_element_state_dynamic::<Rc<TooltipState>>(tag, id);
         let state = state_handle.read(cx).clone();
         let text = text.into();
 
@@ -95,7 +107,7 @@ impl<V: View> Tooltip<V> {
         } else {
             None
         };
-        let child = MouseEventHandler::new::<MouseEventHandlerState<Tag>, _>(id, cx, |_, _| child)
+        let child = MouseEventHandler::new_dynamic(tag, id, cx, |_, _| child)
             .on_hover(move |e, _, cx| {
                 let position = e.position;
                 if e.started {

crates/search/src/buffer_search.rs 🔗

@@ -19,6 +19,7 @@ use gpui::{
 use project::search::SearchQuery;
 use serde::Deserialize;
 use std::{any::Any, sync::Arc};
+
 use util::ResultExt;
 use workspace::{
     item::ItemHandle,
@@ -167,23 +168,17 @@ impl View for BufferSearchBar {
                 cx,
             )
         };
-        let render_search_option =
-            |options: bool, icon, option, cx: &mut ViewContext<BufferSearchBar>| {
-                options.then(|| {
-                    let is_active = self.search_options.contains(option);
-                    crate::search_bar::render_option_button_icon::<Self>(
-                        is_active,
-                        icon,
-                        option.bits as usize,
-                        format!("Toggle {}", option.label()),
-                        option.to_toggle_action(),
-                        move |_, this, cx| {
-                            this.toggle_search_option(option, cx);
-                        },
-                        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 match_count = self
             .active_searchable_item
             .as_ref()
@@ -242,13 +237,11 @@ impl View for BufferSearchBar {
                         supported_options.case,
                         "icons/case_insensitive_12.svg",
                         SearchOptions::CASE_SENSITIVE,
-                        cx,
                     ))
                     .with_children(render_search_option(
                         supported_options.word,
                         "icons/word_search_12.svg",
                         SearchOptions::WHOLE_WORD,
-                        cx,
                     ))
                     .flex_float()
                     .contained(),

crates/search/src/search.rs 🔗

@@ -1,9 +1,14 @@
 use bitflags::bitflags;
 pub use buffer_search::BufferSearchBar;
-use gpui::{actions, Action, AppContext};
+use gpui::{
+    actions,
+    elements::{Component, StyleableComponent, TooltipStyle},
+    Action, AnyElement, AppContext, Element, View,
+};
 pub use mode::SearchMode;
 use project::search::SearchQuery;
 pub use project_search::{ProjectSearchBar, ProjectSearchView};
+use theme::components::{action_button::ActionButton, ComponentExt, ToggleIconButtonStyle};
 
 pub mod buffer_search;
 mod history;
@@ -69,4 +74,23 @@ impl SearchOptions {
         options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
         options
     }
+
+    pub fn as_button<V: View>(
+        &self,
+        active: bool,
+        icon: &str,
+        tooltip_style: TooltipStyle,
+        button_style: ToggleIconButtonStyle,
+    ) -> AnyElement<V> {
+        ActionButton::new_dynamic(
+            self.to_toggle_action(),
+            format!("Toggle {}", self.label()),
+            tooltip_style,
+        )
+        .with_contents(theme::components::svg::Svg::new(icon.to_owned()))
+        .toggleable(active)
+        .with_style(button_style)
+        .into_element()
+        .into_any()
+    }
 }

crates/theme/src/theme.rs 🔗

@@ -1,7 +1,9 @@
+pub mod components;
 mod theme_registry;
 mod theme_settings;
 pub mod ui;
 
+use components::ToggleIconButtonStyle;
 use gpui::{
     color::Color,
     elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
@@ -13,7 +15,7 @@ use serde::{de::DeserializeOwned, Deserialize};
 use serde_json::Value;
 use settings::SettingsStore;
 use std::{collections::HashMap, sync::Arc};
-use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle};
+use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle};
 
 pub use theme_registry::*;
 pub use theme_settings::*;
@@ -182,7 +184,7 @@ pub struct CopilotAuth {
     pub prompting: CopilotAuthPrompting,
     pub not_authorized: CopilotAuthNotAuthorized,
     pub authorized: CopilotAuthAuthorized,
-    pub cta_button: ButtonStyle,
+    pub cta_button: CopilotCTAButton,
     pub header: IconStyle,
 }
 
@@ -196,7 +198,7 @@ pub struct CopilotAuthPrompting {
 #[derive(Deserialize, Default, Clone, JsonSchema)]
 pub struct DeviceCode {
     pub text: TextStyle,
-    pub cta: ButtonStyle,
+    pub cta: CopilotCTAButton,
     pub left: f32,
     pub left_container: ContainerStyle,
     pub right: f32,
@@ -420,6 +422,7 @@ pub struct Search {
     pub invalid_include_exclude_editor: ContainerStyle,
     pub include_exclude_inputs: ContainedText,
     pub option_button: Toggleable<Interactive<IconButton>>,
+    pub option_button_component: ToggleIconButtonStyle,
     pub action_button: Toggleable<Interactive<ContainedText>>,
     pub match_background: Color,
     pub match_index: ContainedText,
@@ -887,12 +890,32 @@ pub struct Interactive<T> {
     pub disabled: Option<T>,
 }
 
+impl Interactive<()> {
+    pub fn new_blank() -> Self {
+        Self {
+            default: (),
+            hovered: None,
+            clicked: None,
+            disabled: None,
+        }
+    }
+}
+
 #[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
 pub struct Toggleable<T> {
     active: T,
     inactive: T,
 }
 
+impl Toggleable<()> {
+    pub fn new_blank() -> Self {
+        Self {
+            active: (),
+            inactive: (),
+        }
+    }
+}
+
 impl<T> Toggleable<T> {
     pub fn new(active: T, inactive: T) -> Self {
         Self { active, inactive }

crates/theme/src/ui.rs 🔗

@@ -145,12 +145,12 @@ pub fn keystroke_label<V: View>(
         .with_style(label_style.container)
 }
 
-pub type ButtonStyle = Interactive<ContainedText>;
+pub type CopilotCTAButton = Interactive<ContainedText>;
 
 pub fn cta_button<Tag, L, V, F>(
     label: L,
     max_width: f32,
-    style: &ButtonStyle,
+    style: &CopilotCTAButton,
     cx: &mut ViewContext<V>,
     f: F,
 ) -> MouseEventHandler<V>

crates/workspace/src/pane.rs 🔗

@@ -320,7 +320,6 @@ impl Pane {
             can_drop: Rc::new(|_, _| true),
             can_split: true,
             render_tab_bar_buttons: Rc::new(move |pane, cx| {
-                let tooltip_style = theme::current(cx).tooltip.clone();
                 Flex::row()
                     // New menu
                     .with_child(Self::render_tab_bar_button(

styles/src/style_tree/search.ts 🔗

@@ -96,6 +96,63 @@ export default function search(): any {
                 },
             },
         }),
+        option_button_component: toggleable({
+            base: interactive({
+                base: {
+                    icon_size: 14,
+                    color: foreground(theme.highest, "variant"),
+
+                    button_width: 32,
+                    background: background(theme.highest, "on"),
+                    corner_radius: 2,
+                    margin: { right: 2 },
+                    border: {
+                        width: 1., color: background(theme.highest, "on")
+                    },
+                    padding: {
+                        left: 4,
+                        right: 4,
+                        top: 4,
+                        bottom: 4,
+                    },
+                },
+                state: {
+                    hovered: {
+                        ...text(theme.highest, "mono", "variant", "hovered"),
+                        background: background(theme.highest, "on", "hovered"),
+                        border: {
+                            width: 1., color: background(theme.highest, "on", "hovered")
+                        },
+                    },
+                    clicked: {
+                        ...text(theme.highest, "mono", "variant", "pressed"),
+                        background: background(theme.highest, "on", "pressed"),
+                        border: {
+                            width: 1., color: background(theme.highest, "on", "pressed")
+                        },
+                    },
+                },
+            }),
+            state: {
+                active: {
+                    default: {
+                        icon_size: 14,
+                        button_width: 32,
+                        color: foreground(theme.highest, "variant"),
+                        background: background(theme.highest, "accent"),
+                        border: border(theme.highest, "accent"),
+                    },
+                    hovered: {
+                        background: background(theme.highest, "accent", "hovered"),
+                        border: border(theme.highest, "accent", "hovered"),
+                    },
+                    clicked: {
+                        background: background(theme.highest, "accent", "pressed"),
+                        border: border(theme.highest, "accent", "pressed"),
+                    },
+                },
+            },
+        }),
         action_button: toggleable({
             base: interactive({
                 base: {