From 69fc2d0950ae00c337dc2e27d994c9e128bc8dad Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 28 Aug 2023 15:03:52 -0700 Subject: [PATCH] WIP: Add a stateful button --- crates/component_test/src/component_test.rs | 13 +- crates/gpui/src/elements/component.rs | 11 ++ .../gpui/src/elements/mouse_event_handler.rs | 9 + crates/gpui/src/scene/mouse_region.rs | 25 +++ crates/search/src/search.rs | 6 +- crates/theme/src/components.rs | 154 ++++++++++++++---- crates/theme/src/theme.rs | 15 +- 7 files changed, 197 insertions(+), 36 deletions(-) diff --git a/crates/component_test/src/component_test.rs b/crates/component_test/src/component_test.rs index 9f6b4918b9e..076ddfa9585 100644 --- a/crates/component_test/src/component_test.rs +++ b/crates/component_test/src/component_test.rs @@ -4,7 +4,7 @@ use gpui::{ AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use project::Project; -use theme::components::{action_button::Button, label::Label, ComponentExt}; +use theme::components::{action_button::ActionButton, button::Button, label::Label, ComponentExt}; use workspace::{ item::Item, register_deserializable_item, ItemId, Pane, PaneBackdrop, Workspace, WorkspaceId, }; @@ -66,14 +66,14 @@ impl View for ComponentTest { Flex::column() .with_spacing(10.) .with_child( - Button::action(NoAction) + ActionButton::action(NoAction) .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone()) .with_contents(Label::new("Click me!")) .with_style(theme.component_test.button.clone()) .element(), ) .with_child( - Button::action(ToggleToggle) + ActionButton::action(ToggleToggle) .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone()) .with_contents(Label::new("Toggle me!")) .toggleable(self.toggled) @@ -86,6 +86,13 @@ impl View for ComponentTest { .with_style(theme.component_test.disclosure.clone()) .element(), ) + .with_child( + Button::new(|click, this, cx| println!("Clicked! {:?}", click)) + .with_contents(Label::new("Print click to console!")) + .disclosable(Some(self.disclosed), Box::new(ToggleDisclosure)) + .with_style(theme.component_test.disclosure.clone()) + .element(), + ) .constrained() .with_width(200.) .aligned() diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index c8800d18a16..00f22617d3e 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -149,6 +149,17 @@ impl StatefulSafeStylable for C { } } +/// converting from stateful to stateless +impl> SafeStylable for C { + type Style = C::Style; + + type Output = C::Output; + + fn with_style(self, style: Self::Style) -> Self::Output { + self.with_style(style) + } +} + // A helper for converting stateless components into stateful ones pub struct StatefulAdapter { component: C, diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index b1140a69604..f6deab9d7ef 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -166,6 +166,15 @@ impl MouseEventHandler { self } + pub fn on_click_dynamic( + mut self, + button: MouseButton, + handler: Box) + 'static>, + ) -> Self { + self.handlers = self.handlers.on_click_dynamic(button, handler); + self + } + pub fn on_click_out( mut self, button: MouseButton, diff --git a/crates/gpui/src/scene/mouse_region.rs b/crates/gpui/src/scene/mouse_region.rs index d19ffdcb871..33232221ae1 100644 --- a/crates/gpui/src/scene/mouse_region.rs +++ b/crates/gpui/src/scene/mouse_region.rs @@ -420,6 +420,31 @@ impl HandlerSet { self } + pub fn on_click_dynamic( + mut self, + button: MouseButton, + handler: Box) + 'static>, + ) -> Self + where + V: 'static, + { + self.insert(MouseEvent::click_disc(), Some(button), + Rc::new(move |region_event, view, cx, view_id| { + if let MouseEvent::Click(e) = region_event { + let view = view.downcast_mut().unwrap(); + let mut cx = ViewContext::mutable(cx, view_id); + let mut cx = EventContext::new(&mut cx); + handler(e, view, &mut cx); + cx.handled + } else { + panic!( + "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Click, found {:?}", + region_event); + } + })); + self + } + pub fn on_click_out(mut self, button: MouseButton, handler: F) -> Self where V: 'static, diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 47f7f485c48..479ddcf6931 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -8,7 +8,9 @@ use gpui::{ pub use mode::SearchMode; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; -use theme::components::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle}; +use theme::components::{ + action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle, +}; pub mod buffer_search; mod history; @@ -89,7 +91,7 @@ impl SearchOptions { tooltip_style: TooltipStyle, button_style: ToggleIconButtonStyle, ) -> AnyElement { - Button::dynamic_action(self.to_toggle_action()) + ActionButton::dynamic_action(self.to_toggle_action()) .with_tooltip(format!("Toggle {}", self.label()), tooltip_style) .with_contents(Svg::new(self.icon())) .toggleable(active) diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index 9011821b772..e886f00abac 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -1,8 +1,8 @@ use gpui::{elements::SafeStylable, Action}; -use crate::{Interactive, Toggleable}; +use crate::{ButtonStyle, Interactive, Toggleable}; -use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle}; +use self::{disclosure::Disclosable, svg::SvgStyle, toggle::Toggle}; pub type IconButtonStyle = Interactive>; pub type ToggleIconButtonStyle = Toggleable; @@ -25,6 +25,115 @@ impl ComponentExt for C { } } +pub mod button { + use std::borrow::Cow; + + use gpui::{ + elements::{MouseEventHandler, StatefulComponent, StatefulSafeStylable, TooltipStyle}, + platform::{CursorStyle, MouseButton}, + scene::MouseClick, + Action, Element, EventContext, TypeTag, + }; + + use crate::{ButtonStyle, Interactive}; + + pub struct Button { + handler: Box)>, + tooltip: Option<(Cow<'static, str>, TooltipStyle, Option>)>, + tag: TypeTag, + contents: C, + style: Interactive, + } + + impl Button { + pub fn new(handler: F) -> Button + where + F: Fn(MouseClick, &mut V, &mut EventContext) + 'static, + { + Self { + contents: (), + tag: TypeTag::new::(), + handler: Box::new(handler), + style: Interactive::new_blank(), + tooltip: None, + } + } + + pub fn with_tooltip( + mut self, + tooltip: impl Into>, + tooltip_style: TooltipStyle, + keybinding: Option>, + ) -> Self { + self.tooltip = Some((tooltip.into(), tooltip_style, keybinding)); + self + } + + pub fn with_contents>(self, contents: C) -> Button { + Button { + handler: self.handler, + tag: self.tag, + style: self.style, + tooltip: self.tooltip, + contents, + } + } + } + + impl> StatefulSafeStylable for Button { + type Style = Interactive>; + type Output = Button>; + + fn with_style(self, style: Self::Style) -> Self::Output { + Button { + handler: self.handler, + tag: self.tag, + contents: self.contents, + tooltip: self.tooltip, + style, + } + } + } + + impl> StatefulComponent + for Button> + { + fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + let mut button = 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_dynamic(MouseButton::Left, self.handler) + .with_cursor_style(CursorStyle::PointingHand) + .into_any(); + + if let Some((tooltip, style, action)) = self.tooltip { + button = button + .with_dynamic_tooltip(self.tag, 0, tooltip, action, style, cx) + .into_any() + } + + button + } + } +} + pub mod disclosure { use gpui::{ @@ -34,7 +143,7 @@ pub mod disclosure { use schemars::JsonSchema; use serde_derive::Deserialize; - use super::{action_button::Button, svg::Svg, IconButtonStyle}; + use super::{action_button::ActionButton, svg::Svg, IconButtonStyle}; #[derive(Clone, Default, Deserialize, JsonSchema)] pub struct DisclosureStyle { @@ -104,7 +213,7 @@ pub mod disclosure { Flex::row() .with_spacing(self.style.spacing) .with_child(if let Some(disclosed) = self.disclosed { - Button::dynamic_action(self.action) + ActionButton::dynamic_action(self.action) .with_id(self.id) .with_contents(Svg::new(if disclosed { "icons/file_icons/chevron_down.svg" @@ -184,29 +293,14 @@ pub mod action_button { use std::borrow::Cow; use gpui::{ - elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle}, + elements::{Component, MouseEventHandler, SafeStylable, TooltipStyle}, platform::{CursorStyle, MouseButton}, Action, Element, TypeTag, }; - use schemars::JsonSchema; - use serde_derive::Deserialize; - use crate::Interactive; + use crate::{ButtonStyle, Interactive}; - #[derive(Clone, Deserialize, Default, JsonSchema)] - pub struct ButtonStyle { - #[serde(flatten)] - pub container: ContainerStyle, - // TODO: These are incorrect for the intended usage of the buttons. - // The size should be constant, but putting them here duplicates them - // across the states the buttons can be in - pub button_width: Option, - pub button_height: Option, - #[serde(flatten)] - contents: C, - } - - pub struct Button { + pub struct ActionButton { action: Box, tooltip: Option<(Cow<'static, str>, TooltipStyle)>, tag: TypeTag, @@ -215,8 +309,8 @@ pub mod action_button { style: Interactive, } - impl Button<(), ()> { - pub fn dynamic_action(action: Box) -> Button<(), ()> { + impl ActionButton<(), ()> { + pub fn dynamic_action(action: Box) -> ActionButton<(), ()> { Self { contents: (), tag: action.type_tag(), @@ -245,8 +339,8 @@ pub mod action_button { self } - pub fn with_contents(self, contents: C) -> Button { - Button { + pub fn with_contents(self, contents: C) -> ActionButton { + ActionButton { action: self.action, tag: self.tag, style: self.style, @@ -257,12 +351,12 @@ pub mod action_button { } } - impl SafeStylable for Button { + impl SafeStylable for ActionButton { type Style = Interactive>; - type Output = Button>; + type Output = ActionButton>; fn with_style(self, style: Self::Style) -> Self::Output { - Button { + ActionButton { action: self.action, tag: self.tag, contents: self.contents, @@ -273,7 +367,7 @@ pub mod action_button { } } - impl Component for Button> { + impl Component for ActionButton> { fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { let mut button = MouseEventHandler::new_dynamic(self.tag, self.id, cx, |state, cx| { let style = self.style.style_for(state); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9005fc9757a..bb077d7844e 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -3,7 +3,7 @@ mod theme_registry; mod theme_settings; pub mod ui; -use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle}; +use components::{disclosure::DisclosureStyle, ToggleIconButtonStyle}; use gpui::{ color::Color, elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle}, @@ -987,6 +987,19 @@ impl Toggleable> { } } +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct ButtonStyle { + #[serde(flatten)] + pub container: ContainerStyle, + // TODO: These are incorrect for the intended usage of the buttons. + // The size should be constant, but putting them here duplicates them + // across the states the buttons can be in + pub button_width: Option, + pub button_height: Option, + #[serde(flatten)] + contents: C, +} + impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { fn deserialize(deserializer: D) -> Result where