From 8630557ece7e3ad0a29060f0c91ba5df68a2910d Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 17 Aug 2023 15:30:40 -0700 Subject: [PATCH] Add action button component for rendering the search options --- 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(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index b08d9501f6..ca5c2fb8b5 100644 --- a/crates/gpui/src/app.rs +++ b/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 { + self.element_state_dynamic(TypeTag::new::(), element_id, initial) + } + + pub fn element_state_dynamic( + &mut self, + tag: TypeTag, + element_id: usize, + initial: T, ) -> ElementStateHandle { let id = ElementStateId { view_id: self.view_id(), element_id, - tag: TypeId::of::(), + tag, }; self.element_states .entry(id) @@ -3331,11 +3340,20 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { ) -> ElementStateHandle { self.element_state::(element_id, T::default()) } + + pub fn default_element_state_dynamic( + &mut self, + tag: TypeTag, + element_id: usize, + ) -> ElementStateHandle { + self.element_state_dynamic::(tag, element_id, T::default()) + } } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct TypeTag { tag: TypeId, + composed: Option, #[cfg(debug_assertions)] tag_type_name: &'static str, } @@ -3344,6 +3362,7 @@ impl TypeTag { pub fn new() -> Self { Self { tag: TypeId::of::(), + composed: None, #[cfg(debug_assertions)] tag_type_name: std::any::type_name::(), } @@ -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 { diff --git a/crates/gpui/src/app/action.rs b/crates/gpui/src/app/action.rs index c6b43e489b..23eb4da730 100644 --- a/crates/gpui/src/app/action.rs +++ b/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; fn eq(&self, other: &dyn Action) -> bool; @@ -107,6 +110,10 @@ macro_rules! __impl_action { } } + fn type_tag(&self) -> $crate::TypeTag { + $crate::TypeTag::new::() + } + $from_json_fn } }; diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index f1be9b34ae..03caae8dd9 100644 --- a/crates/gpui/src/elements.rs +++ b/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: 'static { FlexItem::new(self.into_any()).float() } + fn with_dynamic_tooltip( + self, + tag: TypeTag, + id: usize, + text: impl Into>, + action: Option>, + style: TooltipStyle, + cx: &mut ViewContext, + ) -> Tooltip + where + Self: 'static + Sized, + { + Tooltip::new_dynamic(tag, id, text, action, style, self.into_any(), cx) + } fn with_tooltip( self, id: usize, diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index 2f9cc6cce6..018dc644c6 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -7,6 +7,34 @@ use crate::{ ViewContext, }; +use super::Empty; + +pub trait GeneralComponent { + fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; +} + +pub trait StyleableComponent { + type Style: Clone; + type Output: GeneralComponent; + + fn with_style(self, style: Self::Style) -> Self::Output; +} + +impl GeneralComponent for () { + fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { + Empty::new().into_any() + } +} + +impl StyleableComponent for () { + type Style = (); + type Output = (); + + fn with_style(self, _: Self::Style) -> Self::Output { + () + } +} + pub trait Component { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; @@ -18,6 +46,12 @@ pub trait Component { } } +impl Component for C { + fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement { + self.render(v, cx) + } +} + pub struct ComponentAdapter { component: Option, phantom: PhantomData, diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 0ba0110303..0ce34fcc14 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/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 Tooltip { child: AnyElement, cx: &mut ViewContext, ) -> Self { - struct ElementState(Tag); - struct MouseEventHandlerState(Tag); + Self::new_dynamic(TypeTag::new::(), id, text, action, style, child, cx) + } + + pub fn new_dynamic( + mut tag: TypeTag, + id: usize, + text: impl Into>, + action: Option>, + style: TooltipStyle, + child: AnyElement, + cx: &mut ViewContext, + ) -> Self { + tag = tag.compose(TypeTag::new::()); + let focused_view_id = cx.focused_view_id(); - let state_handle = cx.default_element_state::, Rc>(id); + let state_handle = cx.default_element_state_dynamic::>(tag, id); let state = state_handle.read(cx).clone(); let text = text.into(); @@ -95,7 +107,7 @@ impl Tooltip { } else { None }; - let child = MouseEventHandler::new::, _>(id, cx, |_, _| child) + let child = MouseEventHandler::new_dynamic(tag, id, cx, |_, _| child) .on_hover(move |e, _, cx| { let position = e.position; if e.started { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index c31236023b..0c5b5717d6 100644 --- a/crates/search/src/buffer_search.rs +++ b/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| { - options.then(|| { - let is_active = self.search_options.contains(option); - crate::search_bar::render_option_button_icon::( - 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(), diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 079a8965eb..ec6f97b04d 100644 --- a/crates/search/src/search.rs +++ b/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( + &self, + active: bool, + icon: &str, + tooltip_style: TooltipStyle, + button_style: ToggleIconButtonStyle, + ) -> AnyElement { + 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() + } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 4de0076825..80e823632a 100644 --- a/crates/theme/src/theme.rs +++ b/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>, + pub option_button_component: ToggleIconButtonStyle, pub action_button: Toggleable>, pub match_background: Color, pub match_index: ContainedText, @@ -887,12 +890,32 @@ pub struct Interactive { pub disabled: Option, } +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 { active: T, inactive: T, } +impl Toggleable<()> { + pub fn new_blank() -> Self { + Self { + active: (), + inactive: (), + } + } +} + impl Toggleable { pub fn new(active: T, inactive: T) -> Self { Self { active, inactive } diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index f4a249e74e..7f0b05731e 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -145,12 +145,12 @@ pub fn keystroke_label( .with_style(label_style.container) } -pub type ButtonStyle = Interactive; +pub type CopilotCTAButton = Interactive; pub fn cta_button( label: L, max_width: f32, - style: &ButtonStyle, + style: &CopilotCTAButton, cx: &mut ViewContext, f: F, ) -> MouseEventHandler diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 3b295af802..528b1e2029 100644 --- a/crates/workspace/src/pane.rs +++ b/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( diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 4c0df69804..4493634a8e 100644 --- a/styles/src/style_tree/search.ts +++ b/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: {