From 21fa6090b8bc8d270b203b41544ad66d69727efb 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/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(-) create mode 100644 crates/theme/src/components.rs diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 0c5b5717d6..4078cb572d 100644 --- a/crates/search/src/buffer_search.rs +++ b/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(), ) diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index ec6f97b04d..c31ea6f2f8 100644 --- a/crates/search/src/search.rs +++ b/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 { match *self { SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord), @@ -78,7 +86,6 @@ impl SearchOptions { pub fn as_button( &self, active: bool, - icon: &str, tooltip_style: TooltipStyle, button_style: ToggleIconButtonStyle, ) -> AnyElement { @@ -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() diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs new file mode 100644 index 0000000000..fce7ad825c --- /dev/null +++ b/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>>; + +pub trait ComponentExt { + fn toggleable(self, active: bool) -> Toggle; +} + +impl ComponentExt for C { + fn toggleable(self, active: bool) -> Toggle { + Toggle::new(self, active) + } +} + +pub mod toggle { + use gpui::elements::{GeneralComponent, StyleableComponent}; + + use crate::Toggleable; + + pub struct Toggle { + style: S, + active: bool, + component: C, + } + + impl Toggle { + pub fn new(component: C, active: bool) -> Self { + Toggle { + active, + component, + style: (), + } + } + } + + impl StyleableComponent for Toggle { + type Style = Toggleable; + + type Output = Toggle; + + fn with_style(self, style: Self::Style) -> Self::Output { + Toggle { + active: self.active, + component: self.component, + style, + } + } + } + + impl GeneralComponent for Toggle> { + fn render( + self, + v: &mut V, + cx: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + 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 { + action: Box, + tooltip: Cow<'static, str>, + tooltip_style: TooltipStyle, + tag: TypeTag, + contents: C, + style: Interactive, + } + + #[derive(Clone, Deserialize, Default, JsonSchema)] + pub struct ButtonStyle { + #[serde(flatten)] + container: ContainerStyle, + button_width: Option, + button_height: Option, + #[serde(flatten)] + contents: C, + } + + impl ActionButton<(), ()> { + pub fn new_dynamic( + action: Box, + tooltip: impl Into>, + tooltip_style: TooltipStyle, + ) -> Self { + Self { + contents: (), + tag: action.type_tag(), + style: Interactive::new_blank(), + tooltip: tooltip.into(), + tooltip_style, + action, + } + } + + pub fn new( + action: A, + tooltip: impl Into>, + tooltip_style: TooltipStyle, + ) -> Self { + Self::new_dynamic(Box::new(action), tooltip, tooltip_style) + } + + pub fn with_contents(self, contents: C) -> ActionButton { + ActionButton { + action: self.action, + tag: self.tag, + style: self.style, + tooltip: self.tooltip, + tooltip_style: self.tooltip_style, + contents, + } + } + } + + impl StyleableComponent for ActionButton { + type Style = Interactive>; + type Output = ActionButton>; + + 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 GeneralComponent for ActionButton> { + fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + 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(deserializer: D) -> Result + 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 { + path: Cow<'static, str>, + style: S, + } + + impl Svg<()> { + pub fn new(path: impl Into>) -> Self { + Self { + path: path.into(), + style: (), + } + } + } + + impl StyleableComponent for Svg<()> { + type Style = SvgStyle; + + type Output = Svg; + + fn with_style(self, style: Self::Style) -> Self::Output { + Svg { + path: self.path, + style, + } + } + } + + impl GeneralComponent for Svg { + fn render( + self, + _: &mut V, + _: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + 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 { + text: Cow<'static, str>, + style: S, + } + + impl Label<()> { + pub fn new(text: impl Into>) -> Self { + Self { + text: text.into(), + style: (), + } + } + } + + impl StyleableComponent for Label<()> { + type Style = LabelStyle; + + type Output = Label; + + fn with_style(self, style: Self::Style) -> Self::Output { + Label { + text: self.text, + style, + } + } + } + + impl GeneralComponent for Label { + fn render( + self, + _: &mut V, + _: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + gpui::elements::Label::new(self.text, self.style).into_any() + } + } +}