diff --git a/Cargo.lock b/Cargo.lock index aab5504a86..01c74fb4ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1562,6 +1562,17 @@ dependencies = [ "workspace", ] +[[package]] +name = "component_test" +version = "0.1.0" +dependencies = [ + "gpui", + "settings", + "theme", + "util", + "workspace", +] + [[package]] name = "concurrent-queue" version = "2.2.0" diff --git a/Cargo.toml b/Cargo.toml index 7ea79138c0..d434f34773 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "crates/collab_ui", "crates/collections", "crates/command_palette", + "crates/component_test", "crates/context_menu", "crates/copilot", "crates/copilot_button", diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 41c87094d2..52711281c7 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -17,8 +17,8 @@ use gpui::{ actions, elements::{ Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, - MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, - StyleableComponent, Svg, + MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable, + Stack, Svg, }, geometry::{ rect::RectF, @@ -1633,11 +1633,8 @@ impl CollabPanel { }) .align_children_center() .styleable_component() - .disclosable( - disclosed, - Box::new(ToggleCollapsed { channel_id }), - channel_id as usize, - ) + .disclosable(disclosed, Box::new(ToggleCollapsed { channel_id })) + .with_id(channel_id as usize) .with_style(theme.disclosure.clone()) .element() .constrained() diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs index 5aa648bd65..d3ca0d1ecc 100644 --- a/crates/gpui/examples/components.rs +++ b/crates/gpui/examples/components.rs @@ -72,7 +72,7 @@ impl View for TestView { TextStyle::for_color(Color::blue()), ) .with_style(ButtonStyle::fill(Color::yellow())) - .stateful_element(), + .element(), ) .with_child( ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| { @@ -84,7 +84,7 @@ impl View for TestView { inactive: ButtonStyle::fill(Color::red()), active: ButtonStyle::fill(Color::green()), }) - .stateful_element(), + .element(), ) .expanded() .contained() diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 4fcf3815f5..3c80ef73fe 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -230,25 +230,25 @@ pub trait Element: 'static { MouseEventHandler::for_child::(self.into_any(), region_id) } - fn stateful_component(self) -> ElementAdapter + fn component(self) -> StatelessElementAdapter where Self: Sized, { - ElementAdapter::new(self.into_any()) + StatelessElementAdapter::new(self.into_any()) } - fn component(self) -> DynamicElementAdapter + fn stateful_component(self) -> StatefulElementAdapter where Self: Sized, { - DynamicElementAdapter::new(self.into_any()) + StatefulElementAdapter::new(self.into_any()) } - fn styleable_component(self) -> StylableAdapter + fn styleable_component(self) -> StylableAdapter where Self: Sized, { - DynamicElementAdapter::new(self.into_any()).stylable() + StatelessElementAdapter::new(self.into_any()).stylable() } } diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index 703d6eca07..e391ff9b05 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -9,14 +9,15 @@ use crate::{ use super::Empty; +/// The core stateless component trait, simply rendering an element tree pub trait Component { - fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; + fn render(self, cx: &mut ViewContext) -> AnyElement; - fn element(self) -> StatefulAdapter + fn element(self) -> ComponentAdapter where Self: Sized, { - StatefulAdapter::new(self) + ComponentAdapter::new(self) } fn stylable(self) -> StylableAdapter @@ -25,8 +26,46 @@ pub trait Component { { StylableAdapter::new(self) } + + fn stateful(self) -> StatefulAdapter + where + Self: Sized, + { + StatefulAdapter::new(self) + } } +/// Allows a a component's styles to be rebound in a simple way. +pub trait Stylable: Component { + type Style: Clone; + + fn with_style(self, style: Self::Style) -> Self; +} + +/// This trait models the typestate pattern for a component's style, +/// enforcing at compile time that a component is only usable after +/// it has been styled while still allowing for late binding of the +/// styling information +pub trait SafeStylable { + type Style: Clone; + type Output: Component; + + fn with_style(self, style: Self::Style) -> Self::Output; +} + +/// All stylable components can trivially implement SafeStylable +impl SafeStylable for C { + type Style = C::Style; + + type Output = C; + + fn with_style(self, style: Self::Style) -> Self::Output { + self.with_style(style) + } +} + +/// Allows converting an unstylable component into a stylable one +/// by using `()` as the style type pub struct StylableAdapter { component: C, } @@ -37,7 +76,7 @@ impl StylableAdapter { } } -impl StyleableComponent for StylableAdapter { +impl SafeStylable for StylableAdapter { type Style = (); type Output = C; @@ -47,36 +86,61 @@ impl StyleableComponent for StylableAdapter { } } -pub trait StyleableComponent { +/// This is a secondary trait for components that can be styled +/// which rely on their view's state. This is useful for components that, for example, +/// want to take click handler callbacks Unfortunately, the generic bound on the +/// Component trait makes it incompatible with the stateless components above. +// So let's just replicate them for now +pub trait StatefulComponent { + fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; + + fn element(self) -> ComponentAdapter + where + Self: Sized, + { + ComponentAdapter::new(self) + } + + fn styleable(self) -> StatefulStylableAdapter + where + Self: Sized, + { + StatefulStylableAdapter::new(self) + } + + fn stateless(self) -> StatelessElementAdapter + where + Self: Sized + 'static, + { + StatelessElementAdapter::new(self.element().into_any()) + } +} + +/// It is trivial to convert stateless components to stateful components, so lets +/// do so en masse. Note that the reverse is impossible without a helper. +impl StatefulComponent for C { + fn render(self, _: &mut V, cx: &mut ViewContext) -> AnyElement { + self.render(cx) + } +} + +/// Same as stylable, but generic over a view type +pub trait StatefulStylable: StatefulComponent { type Style: Clone; - type Output: Component; - fn with_style(self, style: Self::Style) -> Self::Output; + fn stateful_with_style(self, style: Self::Style) -> Self; } -impl Component 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 StatefulStyleableComponent { +/// Same as SafeStylable, but generic over a view type +pub trait StatefulSafeStylable { type Style: Clone; type Output: StatefulComponent; fn stateful_with_style(self, style: Self::Style) -> Self::Output; } -impl StatefulStyleableComponent for C { +/// Converting from stateless to stateful +impl StatefulSafeStylable for C { type Style = C::Style; type Output = C::Output; @@ -86,31 +150,29 @@ impl StatefulStyleableComponent for C { } } -pub trait StatefulComponent { - fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; +// A helper for converting stateless components into stateful ones +pub struct StatefulAdapter { + component: C, + phantom: std::marker::PhantomData, +} - fn stateful_element(self) -> StatefulAdapter - where - Self: Sized, - { - StatefulAdapter::new(self) - } - - fn stateful_styleable(self) -> StatefulStylableAdapter - where - Self: Sized, - { - StatefulStylableAdapter::new(self) +impl StatefulAdapter { + pub fn new(component: C) -> Self { + Self { + component, + phantom: std::marker::PhantomData, + } } } -impl StatefulComponent for C { - fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement { - self.render(v, cx) +impl StatefulComponent for StatefulAdapter { + fn render(self, _: &mut V, cx: &mut ViewContext) -> AnyElement { + self.component.render(cx) } } -// StylableComponent -> Component +// A helper for converting stateful but style-less components into stylable ones +// by using `()` as the style type pub struct StatefulStylableAdapter, V: View> { component: C, phantom: std::marker::PhantomData, @@ -125,9 +187,7 @@ impl, V: View> StatefulStylableAdapter { } } -impl, V: View> StatefulStyleableComponent - for StatefulStylableAdapter -{ +impl, V: View> StatefulSafeStylable for StatefulStylableAdapter { type Style = (); type Output = C; @@ -137,37 +197,37 @@ impl, V: View> StatefulStyleableComponent } } -// Element -> GeneralComponent - -pub struct DynamicElementAdapter { +/// A way of erasing the view generic from an element, useful +/// for wrapping up an explicit element tree into stateless +/// components +pub struct StatelessElementAdapter { element: Box, } -impl DynamicElementAdapter { +impl StatelessElementAdapter { pub fn new(element: AnyElement) -> Self { - DynamicElementAdapter { + StatelessElementAdapter { element: Box::new(element) as Box, } } } -impl Component for DynamicElementAdapter { - fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { - let element = self +impl Component for StatelessElementAdapter { + fn render(self, _: &mut ViewContext) -> AnyElement { + *self .element .downcast::>() - .expect("Don't move elements out of their view :("); - *element + .expect("Don't move elements out of their view :(") } } -// Element -> Component -pub struct ElementAdapter { +// For converting elements into stateful components +pub struct StatefulElementAdapter { element: AnyElement, _phantom: std::marker::PhantomData, } -impl ElementAdapter { +impl StatefulElementAdapter { pub fn new(element: AnyElement) -> Self { Self { element, @@ -176,20 +236,35 @@ impl ElementAdapter { } } -impl StatefulComponent for ElementAdapter { +impl StatefulComponent for StatefulElementAdapter { fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { self.element } } -// Component -> Element -pub struct StatefulAdapter { +/// A convenient shorthand for creating an empty component. +impl Component for () { + fn render(self, _: &mut ViewContext) -> AnyElement { + Empty::new().into_any() + } +} + +impl Stylable for () { + type Style = (); + + fn with_style(self, _: Self::Style) -> Self { + () + } +} + +// For converting components back into Elements +pub struct ComponentAdapter { component: Option, element: Option>, phantom: PhantomData, } -impl StatefulAdapter { +impl ComponentAdapter { pub fn new(e: E) -> Self { Self { component: Some(e), @@ -199,7 +274,7 @@ impl StatefulAdapter { } } -impl + 'static> Element for StatefulAdapter { +impl + 'static> Element for ComponentAdapter { type LayoutState = (); type PaintState = (); @@ -262,6 +337,7 @@ impl + 'static> Element for StatefulAdapter< ) -> serde_json::Value { serde_json::json!({ "type": "ComponentAdapter", + "component": std::any::type_name::(), "child": self.element.as_ref().map(|el| el.debug(view, cx)), }) } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 2dc45e3973..47f7f485c4 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -2,15 +2,13 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; use gpui::{ actions, - elements::{Component, StyleableComponent, TooltipStyle}, + elements::{Component, SafeStylable, 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, svg::Svg, ComponentExt, ToggleIconButtonStyle, -}; +use theme::components::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle}; pub mod buffer_search; mod history; @@ -91,7 +89,7 @@ impl SearchOptions { tooltip_style: TooltipStyle, button_style: ToggleIconButtonStyle, ) -> AnyElement { - ActionButton::new_dynamic(self.to_toggle_action()) + Button::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 6f9242199f..d9a857915c 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -1,4 +1,4 @@ -use gpui::{elements::StyleableComponent, Action}; +use gpui::{elements::SafeStylable, Action}; use crate::{Interactive, Toggleable}; @@ -6,44 +6,34 @@ use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, t pub type ToggleIconButtonStyle = Toggleable>>; -pub trait ComponentExt { +pub trait ComponentExt { fn toggleable(self, active: bool) -> Toggle; - fn disclosable( - self, - disclosed: Option, - action: Box, - id: usize, - ) -> Disclosable; + fn disclosable(self, disclosed: Option, action: Box) -> Disclosable; } -impl ComponentExt for C { +impl ComponentExt for C { fn toggleable(self, active: bool) -> Toggle { Toggle::new(self, active) } /// Some(True) => disclosed => content is visible /// Some(false) => closed => content is hidden - /// None => No disclosure button, but reserve spacing - fn disclosable( - self, - disclosed: Option, - action: Box, - id: usize, - ) -> Disclosable { - Disclosable::new(disclosed, self, action, id) + /// None => No disclosure button, but reserve disclosure spacing + fn disclosable(self, disclosed: Option, action: Box) -> Disclosable { + Disclosable::new(disclosed, self, action) } } pub mod disclosure { use gpui::{ - elements::{Component, Empty, Flex, ParentElement, StyleableComponent}, + elements::{Component, Empty, Flex, ParentElement, SafeStylable}, Action, Element, }; use schemars::JsonSchema; use serde_derive::Deserialize; - use super::{action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle}; + use super::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle}; #[derive(Clone, Default, Deserialize, JsonSchema)] pub struct DisclosureStyle { @@ -72,19 +62,24 @@ pub mod disclosure { disclosed: Option, content: C, action: Box, - id: usize, ) -> Disclosable { Disclosable { disclosed, content, action, - id, + id: 0, style: (), } } } - impl StyleableComponent for Disclosable { + impl Disclosable { + pub fn with_id(self, id: usize) -> Disclosable { + Disclosable { id, ..self } + } + } + + impl SafeStylable for Disclosable { type Style = DisclosureStyle; type Output = Disclosable; @@ -100,15 +95,11 @@ pub mod disclosure { } } - impl Component for Disclosable> { - fn render( - self, - v: &mut V, - cx: &mut gpui::ViewContext, - ) -> gpui::AnyElement { + impl Component for Disclosable> { + fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { Flex::row() .with_child(if let Some(disclosed) = self.disclosed { - ActionButton::new_dynamic(self.action) + Button::dynamic_action(self.action) .with_id(self.id) .with_contents(Svg::new(if disclosed { "icons/file_icons/chevron_down.svg" @@ -131,7 +122,7 @@ pub mod disclosure { .with_child( self.content .with_style(self.style.content) - .render(v, cx) + .render(cx) .flex(1., true), ) .align_children_center() @@ -141,7 +132,7 @@ pub mod disclosure { } pub mod toggle { - use gpui::elements::{Component, StyleableComponent}; + use gpui::elements::{Component, SafeStylable}; use crate::Toggleable; @@ -151,7 +142,7 @@ pub mod toggle { component: C, } - impl Toggle { + impl Toggle { pub fn new(component: C, active: bool) -> Self { Toggle { active, @@ -161,7 +152,7 @@ pub mod toggle { } } - impl StyleableComponent for Toggle { + impl SafeStylable for Toggle { type Style = Toggleable; type Output = Toggle; @@ -175,15 +166,11 @@ pub mod toggle { } } - impl Component for Toggle> { - fn render( - self, - v: &mut V, - cx: &mut gpui::ViewContext, - ) -> gpui::AnyElement { + impl Component for Toggle> { + fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { self.component .with_style(self.style.in_state(self.active).clone()) - .render(v, cx) + .render(cx) } } } @@ -192,9 +179,7 @@ pub mod action_button { use std::borrow::Cow; use gpui::{ - elements::{ - Component, ContainerStyle, MouseEventHandler, StyleableComponent, TooltipStyle, - }, + elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle}, platform::{CursorStyle, MouseButton}, Action, Element, TypeTag, View, }; @@ -216,7 +201,7 @@ pub mod action_button { contents: C, } - pub struct ActionButton { + pub struct Button { action: Box, tooltip: Option<(Cow<'static, str>, TooltipStyle)>, tag: TypeTag, @@ -225,8 +210,8 @@ pub mod action_button { style: Interactive, } - impl ActionButton<(), ()> { - pub fn new_dynamic(action: Box) -> Self { + impl Button<(), ()> { + pub fn dynamic_action(action: Box) -> Self { Self { contents: (), tag: action.type_tag(), @@ -237,8 +222,8 @@ pub mod action_button { } } - pub fn new(action: A) -> Self { - Self::new_dynamic(Box::new(action)) + pub fn action(action: A) -> Self { + Self::dynamic_action(Box::new(action)) } pub fn with_tooltip( @@ -255,8 +240,8 @@ pub mod action_button { self } - pub fn with_contents(self, contents: C) -> ActionButton { - ActionButton { + pub fn with_contents(self, contents: C) -> Button { + Button { action: self.action, tag: self.tag, style: self.style, @@ -267,12 +252,12 @@ pub mod action_button { } } - impl StyleableComponent for ActionButton { + impl SafeStylable for Button { type Style = Interactive>; - type Output = ActionButton>; + type Output = Button>; fn with_style(self, style: Self::Style) -> Self::Output { - ActionButton { + Button { action: self.action, tag: self.tag, contents: self.contents, @@ -283,14 +268,14 @@ pub mod action_button { } } - impl Component for ActionButton> { - fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + impl Component for Button> { + 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); let mut contents = self .contents .with_style(style.contents.to_owned()) - .render(v, cx) + .render(cx) .contained() .with_style(style.container) .constrained(); @@ -335,7 +320,7 @@ pub mod svg { use std::borrow::Cow; use gpui::{ - elements::{Component, Empty, StyleableComponent}, + elements::{Component, Empty, SafeStylable}, Element, }; use schemars::JsonSchema; @@ -417,7 +402,7 @@ pub mod svg { } } - impl StyleableComponent for Svg<()> { + impl SafeStylable for Svg<()> { type Style = SvgStyle; type Output = Svg; @@ -431,11 +416,7 @@ pub mod svg { } impl Component for Svg { - fn render( - self, - _: &mut V, - _: &mut gpui::ViewContext, - ) -> gpui::AnyElement { + fn render(self, _: &mut gpui::ViewContext) -> gpui::AnyElement { if let Some(path) = self.path { gpui::elements::Svg::new(path) .with_color(self.style.color) @@ -455,7 +436,7 @@ pub mod label { use std::borrow::Cow; use gpui::{ - elements::{Component, LabelStyle, StyleableComponent}, + elements::{Component, LabelStyle, SafeStylable}, Element, }; @@ -473,7 +454,7 @@ pub mod label { } } - impl StyleableComponent for Label<()> { + impl SafeStylable for Label<()> { type Style = LabelStyle; type Output = Label; @@ -487,11 +468,7 @@ pub mod label { } impl Component for Label { - fn render( - self, - _: &mut V, - _: &mut gpui::ViewContext, - ) -> gpui::AnyElement { + fn render(self, _: &mut gpui::ViewContext) -> gpui::AnyElement { gpui::elements::Label::new(self.text, self.style).into_any() } }