From 3178adefdeab488fb3068c198d8670030e6cf433 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 18 Aug 2023 14:54:05 -0700 Subject: [PATCH 1/6] WIP: Add disclosable channels --- crates/collab_ui/src/collab_panel.rs | 91 ++++++++++++++++++++++++++- crates/gpui/src/elements.rs | 7 +++ crates/gpui/src/elements/component.rs | 7 +++ crates/search/src/search.rs | 21 +++---- crates/theme/src/components.rs | 46 +++++++------- 5 files changed, 136 insertions(+), 36 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index c49011b86b7b17d2d73c7171676962ddef9e9cb7..16de60e735a04d5c3cecb742e7cead60320b4ae6 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -8,6 +8,7 @@ use client::{ proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore, }; +use components::DisclosureExt; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; @@ -16,7 +17,7 @@ use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, elements::{ - Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState, + Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, Svg, }, geometry::{ @@ -1615,6 +1616,10 @@ impl CollabPanel { this.deploy_channel_context_menu(Some(e.position), channel_id, cx); }) .with_cursor_style(CursorStyle::PointingHand) + .component() + .styleable() + .disclosable() + .into_element() .into_any() } @@ -2522,3 +2527,87 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen .contained() .with_style(style.container) } + +mod components { + + use gpui::{ + elements::{Empty, Flex, GeneralComponent, ParentElement, StyleableComponent}, + Action, Element, + }; + use theme::components::{ + action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle, + }; + + #[derive(Clone)] + struct DisclosureStyle { + disclosure: ToggleIconButtonStyle, + spacing: f32, + content: S, + } + + struct Disclosable { + disclosed: bool, + action: Box, + content: C, + style: S, + } + + impl Disclosable<(), ()> { + fn new(disclosed: bool, content: C, action: Box) -> Disclosable { + Disclosable { + disclosed, + content, + action, + style: (), + } + } + } + + impl StyleableComponent for Disclosable { + type Style = DisclosureStyle; + + type Output = Disclosable; + + fn with_style(self, style: Self::Style) -> Self::Output { + Disclosable { + disclosed: self.disclosed, + action: self.action, + content: self.content, + style, + } + } + } + + impl GeneralComponent for Disclosable> { + fn render( + self, + v: &mut V, + cx: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + Flex::row() + .with_child( + ActionButton::new_dynamic(self.action) + .with_contents(Svg::new("path")) + .toggleable(self.disclosed) + .with_style(self.style.disclosure) + .element(), + ) + .with_child(Empty::new().constrained().with_width(self.style.spacing)) + .with_child(self.content.with_style(self.style.content).render(v, cx)) + .align_children_center() + .into_any() + } + } + + pub trait DisclosureExt { + fn disclosable(self, disclosed: bool, action: Box) -> Disclosable + where + Self: Sized; + } + + impl DisclosureExt for C { + fn disclosable(self, disclosed: bool, action: Box) -> Disclosable { + Disclosable::new(disclosed, self, action) + } + } +} diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 03caae8dd9b6591741de99a5610049caf6991760..f7697d6fc13b08ac09a84ed5e2051c9cec286b6d 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -229,6 +229,13 @@ pub trait Element: 'static { { MouseEventHandler::for_child::(self.into_any(), region_id) } + + fn component(self) -> ElementAdapter + where + Self: Sized, + { + ElementAdapter::new(self.into_any()) + } } pub trait RenderElement { diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index e2770c014859c902cc86fe1f04590f81595c94c5..ee4702a6fad308feaa148884af848a4e3148ce2e 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -50,6 +50,13 @@ pub trait Component { { ComponentAdapter::new(self) } + + fn styleable(self) -> StylableComponentAdapter + where + Self: Sized, + { + StylableComponentAdapter::new(self) + } } impl Component for C { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 8d8c02c8d7e0fbff84a509ba8a9fbf6233965adb..2dc45e397394b84671533471e7c93937d9469354 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::ActionButton, ComponentExt, ToggleIconButtonStyle}; +use theme::components::{ + action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle, +}; pub mod buffer_search; mod history; @@ -89,15 +91,12 @@ impl SearchOptions { 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(self.icon())) - .toggleable(active) - .with_style(button_style) - .element() - .into_any() + ActionButton::new_dynamic(self.to_toggle_action()) + .with_tooltip(format!("Toggle {}", self.label()), tooltip_style) + .with_contents(Svg::new(self.icon())) + .toggleable(active) + .with_style(button_style) + .element() + .into_any() } } diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index fce7ad825cc2ede0a8501dba5ac4fe7ff520bc08..1e395405cbb506596248d51ac5b387ca2a654e83 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -81,8 +81,7 @@ pub mod action_button { pub struct ActionButton { action: Box, - tooltip: Cow<'static, str>, - tooltip_style: TooltipStyle, + tooltip: Option<(Cow<'static, str>, TooltipStyle)>, tag: TypeTag, contents: C, style: Interactive, @@ -99,27 +98,27 @@ pub mod action_button { } impl ActionButton<(), ()> { - pub fn new_dynamic( - action: Box, - tooltip: impl Into>, - tooltip_style: TooltipStyle, - ) -> Self { + pub fn new_dynamic(action: Box) -> Self { Self { contents: (), tag: action.type_tag(), style: Interactive::new_blank(), - tooltip: tooltip.into(), - tooltip_style, + tooltip: None, action, } } - pub fn new( - action: A, + pub fn new(action: A) -> Self { + Self::new_dynamic(Box::new(action)) + } + + pub fn with_tooltip( + mut self, tooltip: impl Into>, tooltip_style: TooltipStyle, ) -> Self { - Self::new_dynamic(Box::new(action), tooltip, tooltip_style) + self.tooltip = Some((tooltip.into(), tooltip_style)); + self } pub fn with_contents(self, contents: C) -> ActionButton { @@ -128,7 +127,6 @@ pub mod action_button { tag: self.tag, style: self.style, tooltip: self.tooltip, - tooltip_style: self.tooltip_style, contents, } } @@ -144,7 +142,7 @@ pub mod action_button { tag: self.tag, contents: self.contents, tooltip: self.tooltip, - tooltip_style: self.tooltip_style, + style, } } @@ -152,7 +150,7 @@ pub mod action_button { 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 mut button = MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| { let style = self.style.style_for(state); let mut contents = self .contents @@ -180,15 +178,15 @@ pub mod action_button { } }) .with_cursor_style(CursorStyle::PointingHand) - .with_dynamic_tooltip( - self.tag, - 0, - self.tooltip, - Some(self.action), - self.tooltip_style, - cx, - ) - .into_any() + .into_any(); + + if let Some((tooltip, style)) = self.tooltip { + button = button + .with_dynamic_tooltip(self.tag, 0, tooltip, Some(self.action), style, cx) + .into_any() + } + + button } } } From 2d37128693b40b3793d4ca6238e93b5e24c2ec88 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 18 Aug 2023 19:02:27 -0700 Subject: [PATCH 2/6] Actually get it compiling, omg --- crates/collab_ui/src/collab_panel.rs | 38 +++++++----- crates/gpui/examples/components.rs | 4 +- crates/gpui/src/elements.rs | 7 +++ crates/gpui/src/elements/component.rs | 85 ++++++++++++++++++++++++--- crates/search/src/search.rs | 4 +- crates/theme/src/components.rs | 34 ++++++----- 6 files changed, 131 insertions(+), 41 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 16de60e735a04d5c3cecb742e7cead60320b4ae6..f35c07d5aa0a9e98a8a24fee93c895308f7a09bb 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -8,7 +8,6 @@ use client::{ proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore, }; -use components::DisclosureExt; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; @@ -17,8 +16,9 @@ use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, elements::{ - Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, - MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, Svg, + Canvas, ChildView, Empty, Flex, GeneralComponent, GeneralStyleableComponent, Image, Label, + List, ListOffset, ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, + ParentElement, Stack, Svg, }, geometry::{ rect::RectF, @@ -44,7 +44,10 @@ use workspace::{ Workspace, }; -use crate::face_pile::FacePile; +use crate::{ + collab_panel::components::{DisclosureExt, DisclosureStyle}, + face_pile::FacePile, +}; use channel_modal::ChannelModal; use self::contact_finder::ContactFinder; @@ -1616,10 +1619,17 @@ impl CollabPanel { this.deploy_channel_context_menu(Some(e.position), channel_id, cx); }) .with_cursor_style(CursorStyle::PointingHand) - .component() - .styleable() - .disclosable() - .into_element() + .dynamic_component() + .stylable() + .disclosable(true, Box::new(RemoveChannel { channel_id: 0 })) + .with_style({ + fn style() -> DisclosureStyle<()> { + todo!() + } + + style() + }) + .element() .into_any() } @@ -2531,7 +2541,7 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen mod components { use gpui::{ - elements::{Empty, Flex, GeneralComponent, ParentElement, StyleableComponent}, + elements::{Empty, Flex, GeneralComponent, GeneralStyleableComponent, ParentElement}, Action, Element, }; use theme::components::{ @@ -2539,13 +2549,13 @@ mod components { }; #[derive(Clone)] - struct DisclosureStyle { + pub struct DisclosureStyle { disclosure: ToggleIconButtonStyle, spacing: f32, content: S, } - struct Disclosable { + pub struct Disclosable { disclosed: bool, action: Box, content: C, @@ -2563,7 +2573,7 @@ mod components { } } - impl StyleableComponent for Disclosable { + impl GeneralStyleableComponent for Disclosable { type Style = DisclosureStyle; type Output = Disclosable; @@ -2578,7 +2588,7 @@ mod components { } } - impl GeneralComponent for Disclosable> { + impl GeneralComponent for Disclosable> { fn render( self, v: &mut V, @@ -2605,7 +2615,7 @@ mod components { Self: Sized; } - impl DisclosureExt for C { + impl DisclosureExt for C { fn disclosable(self, disclosed: bool, action: Box) -> Disclosable { Disclosable::new(disclosed, self, action) } diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs index ad38b5893c48a831c245ed61300d9c92d7319383..aeeab8101d1e926f287e72c9fd22a44af8d645df 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())) - .element(), + .c_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()), }) - .element(), + .c_element(), ) .expanded() .contained() diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index f7697d6fc13b08ac09a84ed5e2051c9cec286b6d..c0484ef9395a9e25e82384b30aa5fdc88d81bf07 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -236,6 +236,13 @@ pub trait Element: 'static { { ElementAdapter::new(self.into_any()) } + + fn dynamic_component(self) -> DynamicElementAdapter + where + Self: Sized, + { + DynamicElementAdapter::new(self.into_any()) + } } pub trait RenderElement { diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index ee4702a6fad308feaa148884af848a4e3148ce2e..f5a4180d7f379c059a175c731311ca46360f0132 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -1,4 +1,4 @@ -use std::marker::PhantomData; +use std::{any::Any, marker::PhantomData}; use pathfinder_geometry::{rect::RectF, vector::Vector2F}; @@ -11,15 +11,43 @@ use super::Empty; pub trait GeneralComponent { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; + fn element(self) -> ComponentAdapter where Self: Sized, { ComponentAdapter::new(self) } + + fn stylable(self) -> GeneralStylableComponentAdapter + where + Self: Sized, + { + GeneralStylableComponentAdapter::new(self) + } +} + +pub struct GeneralStylableComponentAdapter { + component: C, +} + +impl GeneralStylableComponentAdapter { + pub fn new(component: C) -> Self { + Self { component } + } +} + +impl GeneralStyleableComponent for GeneralStylableComponentAdapter { + type Style = (); + + type Output = C; + + fn with_style(self, _: Self::Style) -> Self::Output { + self.component + } } -pub trait StyleableComponent { +pub trait GeneralStyleableComponent { type Style: Clone; type Output: GeneralComponent; @@ -32,7 +60,7 @@ impl GeneralComponent for () { } } -impl StyleableComponent for () { +impl GeneralStyleableComponent for () { type Style = (); type Output = (); @@ -41,17 +69,34 @@ impl StyleableComponent for () { } } +pub trait StyleableComponent { + type Style: Clone; + type Output: Component; + + fn c_with_style(self, style: Self::Style) -> Self::Output; +} + +impl StyleableComponent for C { + type Style = C::Style; + + type Output = C::Output; + + fn c_with_style(self, style: Self::Style) -> Self::Output { + self.with_style(style) + } +} + pub trait Component { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; - fn element(self) -> ComponentAdapter + fn c_element(self) -> ComponentAdapter where Self: Sized, { ComponentAdapter::new(self) } - fn styleable(self) -> StylableComponentAdapter + fn c_styleable(self) -> StylableComponentAdapter where Self: Sized, { @@ -65,7 +110,7 @@ impl Component for C { } } -// StylableComponent -> GeneralComponent +// StylableComponent -> Component pub struct StylableComponentAdapter, V: View> { component: C, phantom: std::marker::PhantomData, @@ -80,16 +125,40 @@ impl, V: View> StylableComponentAdapter { } } -impl StyleableComponent for StylableComponentAdapter { +impl, V: View> StyleableComponent for StylableComponentAdapter { type Style = (); type Output = C; - fn with_style(self, _: Self::Style) -> Self::Output { + fn c_with_style(self, _: Self::Style) -> Self::Output { self.component } } +// Element -> GeneralComponent + +pub struct DynamicElementAdapter { + element: Box, +} + +impl DynamicElementAdapter { + pub fn new(element: AnyElement) -> Self { + DynamicElementAdapter { + element: Box::new(element) as Box, + } + } +} + +impl GeneralComponent for DynamicElementAdapter { + fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { + let element = self + .element + .downcast::>() + .expect("Don't move elements out of their view :("); + *element + } +} + // Element -> Component pub struct ElementAdapter { element: AnyElement, diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 2dc45e397394b84671533471e7c93937d9469354..b3167efe507f6ae36fb870412793d38d39ae4e20 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -2,7 +2,7 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; use gpui::{ actions, - elements::{Component, StyleableComponent, TooltipStyle}, + elements::{Component, GeneralStyleableComponent, TooltipStyle}, Action, AnyElement, AppContext, Element, View, }; pub use mode::SearchMode; @@ -96,7 +96,7 @@ impl SearchOptions { .with_contents(Svg::new(self.icon())) .toggleable(active) .with_style(button_style) - .element() + .c_element() .into_any() } } diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index 1e395405cbb506596248d51ac5b387ca2a654e83..3679b1fa4f1c1313d4fa8340d518d9949c5d0a31 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -1,4 +1,4 @@ -use gpui::elements::StyleableComponent; +use gpui::elements::GeneralStyleableComponent; use crate::{Interactive, Toggleable}; @@ -6,18 +6,18 @@ use self::{action_button::ButtonStyle, svg::SvgStyle, toggle::Toggle}; pub type ToggleIconButtonStyle = Toggleable>>; -pub trait ComponentExt { +pub trait ComponentExt { fn toggleable(self, active: bool) -> Toggle; } -impl ComponentExt for C { +impl ComponentExt for C { fn toggleable(self, active: bool) -> Toggle { Toggle::new(self, active) } } pub mod toggle { - use gpui::elements::{GeneralComponent, StyleableComponent}; + use gpui::elements::{GeneralComponent, GeneralStyleableComponent}; use crate::Toggleable; @@ -27,7 +27,7 @@ pub mod toggle { component: C, } - impl Toggle { + impl Toggle { pub fn new(component: C, active: bool) -> Self { Toggle { active, @@ -37,7 +37,7 @@ pub mod toggle { } } - impl StyleableComponent for Toggle { + impl GeneralStyleableComponent for Toggle { type Style = Toggleable; type Output = Toggle; @@ -51,7 +51,7 @@ pub mod toggle { } } - impl GeneralComponent for Toggle> { + impl GeneralComponent for Toggle> { fn render( self, v: &mut V, @@ -69,7 +69,8 @@ pub mod action_button { use gpui::{ elements::{ - ContainerStyle, GeneralComponent, MouseEventHandler, StyleableComponent, TooltipStyle, + ContainerStyle, GeneralComponent, GeneralStyleableComponent, MouseEventHandler, + TooltipStyle, }, platform::{CursorStyle, MouseButton}, Action, Element, TypeTag, View, @@ -121,7 +122,10 @@ pub mod action_button { self } - pub fn with_contents(self, contents: C) -> ActionButton { + pub fn with_contents( + self, + contents: C, + ) -> ActionButton { ActionButton { action: self.action, tag: self.tag, @@ -132,7 +136,7 @@ pub mod action_button { } } - impl StyleableComponent for ActionButton { + impl GeneralStyleableComponent for ActionButton { type Style = Interactive>; type Output = ActionButton>; @@ -148,7 +152,7 @@ pub mod action_button { } } - impl GeneralComponent for ActionButton> { + impl GeneralComponent for ActionButton> { 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); @@ -195,7 +199,7 @@ pub mod svg { use std::borrow::Cow; use gpui::{ - elements::{GeneralComponent, StyleableComponent}, + elements::{GeneralComponent, GeneralStyleableComponent}, Element, }; use schemars::JsonSchema; @@ -261,7 +265,7 @@ pub mod svg { } } - impl StyleableComponent for Svg<()> { + impl GeneralStyleableComponent for Svg<()> { type Style = SvgStyle; type Output = Svg; @@ -294,7 +298,7 @@ pub mod label { use std::borrow::Cow; use gpui::{ - elements::{GeneralComponent, LabelStyle, StyleableComponent}, + elements::{GeneralComponent, GeneralStyleableComponent, LabelStyle}, Element, }; @@ -312,7 +316,7 @@ pub mod label { } } - impl StyleableComponent for Label<()> { + impl GeneralStyleableComponent for Label<()> { type Style = LabelStyle; type Output = Label; From bd3ab82dac7d59f19a104416b29976d070946004 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 19 Aug 2023 05:18:53 -0700 Subject: [PATCH 3/6] Add disclosable components into channels Rename components to more closely match their purpose --- crates/client/src/channel_store.rs | 10 ++ crates/collab_ui/src/collab_panel.rs | 170 +++++++----------- crates/gpui/examples/components.rs | 14 +- crates/gpui/src/elements.rs | 11 +- crates/gpui/src/elements/component.rs | 66 +++---- crates/gpui/src/elements/container.rs | 8 + crates/search/src/search.rs | 4 +- crates/theme/src/components.rs | 245 +++++++++++++++++++++----- crates/theme/src/theme.rs | 21 ++- styles/src/style_tree/collab_panel.ts | 4 + 10 files changed, 359 insertions(+), 194 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 03d334a9defcc05255874c75349f894d1b9bc1f7..6352ac791edaa0bf50704e4e42bf41069031be55 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -114,6 +114,16 @@ impl ChannelStore { } } + pub fn has_children(&self, channel_id: ChannelId) -> bool { + self.channel_paths.iter().any(|path| { + if let Some(ix) = path.iter().position(|id| *id == channel_id) { + path.len() > ix + 1 + } else { + false + } + }) + } + pub fn channel_count(&self) -> usize { self.channel_paths.len() } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index f35c07d5aa0a9e98a8a24fee93c895308f7a09bb..41c87094d2c27c561c1c0ddb5548f461cdfdb511 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -16,9 +16,9 @@ use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, elements::{ - Canvas, ChildView, Empty, Flex, GeneralComponent, GeneralStyleableComponent, Image, Label, - List, ListOffset, ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, - ParentElement, Stack, Svg, + Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, + MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, + StyleableComponent, Svg, }, geometry::{ rect::RectF, @@ -36,7 +36,7 @@ use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; use staff_mode::StaffMode; use std::{borrow::Cow, mem, sync::Arc}; -use theme::IconButton; +use theme::{components::ComponentExt, IconButton}; use util::{iife, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -44,10 +44,7 @@ use workspace::{ Workspace, }; -use crate::{ - collab_panel::components::{DisclosureExt, DisclosureStyle}, - face_pile::FacePile, -}; +use crate::face_pile::FacePile; use channel_modal::ChannelModal; use self::contact_finder::ContactFinder; @@ -57,6 +54,11 @@ struct RemoveChannel { channel_id: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct ToggleCollapsed { + channel_id: u64, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct NewChannel { channel_id: u64, @@ -86,7 +88,8 @@ impl_actions!( NewChannel, InviteMembers, ManageMembers, - RenameChannel + RenameChannel, + ToggleCollapsed ] ); @@ -109,6 +112,7 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::manage_members); cx.add_action(CollabPanel::rename_selected_channel); cx.add_action(CollabPanel::rename_channel); + cx.add_action(CollabPanel::toggle_channel_collapsed); } #[derive(Debug)] @@ -151,6 +155,7 @@ pub struct CollabPanel { list_state: ListState, subscriptions: Vec, collapsed_sections: Vec
, + collapsed_channels: Vec, workspace: WeakViewHandle, context_menu_on_selected: bool, } @@ -402,6 +407,7 @@ impl CollabPanel { subscriptions: Vec::default(), match_candidates: Vec::default(), collapsed_sections: vec![Section::Offline], + collapsed_channels: Vec::default(), workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), context_menu_on_selected: true, @@ -661,10 +667,24 @@ impl CollabPanel { self.entries.push(ListEntry::ChannelEditor { depth: 0 }); } } + let mut collapse_depth = None; for mat in matches { let (depth, channel) = channel_store.channel_at_index(mat.candidate_id).unwrap(); + if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else if let Some(collapsed_depth) = collapse_depth { + if depth > collapsed_depth { + continue; + } + if self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else { + collapse_depth = None; + } + } + match &self.channel_editing_state { Some(ChannelEditingState::Create { parent_id, .. }) if *parent_id == Some(channel.id) => @@ -1483,6 +1503,11 @@ impl CollabPanel { cx: &AppContext, ) -> AnyElement { Flex::row() + .with_child( + Empty::new() + .constrained() + .with_width(theme.collab_panel.disclosure.button_space()), + ) .with_child( Svg::new("icons/hash.svg") .with_color(theme.collab_panel.channel_hash.color) @@ -1541,6 +1566,10 @@ impl CollabPanel { cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; + let has_children = self.channel_store.read(cx).has_children(channel_id); + let disclosed = + has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok()); + let is_active = iife!({ let call_channel = ActiveCall::global(cx) .read(cx) @@ -1554,7 +1583,7 @@ impl CollabPanel { const FACEPILE_LIMIT: usize = 3; MouseEventHandler::new::(channel.id as usize, cx, |state, cx| { - Flex::row() + Flex::::row() .with_child( Svg::new("icons/hash.svg") .with_color(theme.channel_hash.color) @@ -1603,6 +1632,14 @@ impl CollabPanel { } }) .align_children_center() + .styleable_component() + .disclosable( + disclosed, + Box::new(ToggleCollapsed { channel_id }), + channel_id as usize, + ) + .with_style(theme.disclosure.clone()) + .element() .constrained() .with_height(theme.row_height) .contained() @@ -1619,17 +1656,6 @@ impl CollabPanel { this.deploy_channel_context_menu(Some(e.position), channel_id, cx); }) .with_cursor_style(CursorStyle::PointingHand) - .dynamic_component() - .stylable() - .disclosable(true, Box::new(RemoveChannel { channel_id: 0 })) - .with_style({ - fn style() -> DisclosureStyle<()> { - todo!() - } - - style() - }) - .element() .into_any() } @@ -2024,6 +2050,24 @@ impl CollabPanel { self.update_entries(false, cx); } + fn toggle_channel_collapsed(&mut self, action: &ToggleCollapsed, cx: &mut ViewContext) { + let channel_id = action.channel_id; + match self.collapsed_channels.binary_search(&channel_id) { + Ok(ix) => { + self.collapsed_channels.remove(ix); + } + Err(ix) => { + self.collapsed_channels.insert(ix, channel_id); + } + }; + self.update_entries(false, cx); + cx.notify(); + } + + fn is_channel_collapsed(&self, channel: ChannelId) -> bool { + self.collapsed_channels.binary_search(&channel).is_ok() + } + fn leave_call(cx: &mut ViewContext) { ActiveCall::global(cx) .update(cx, |call, cx| call.hang_up(cx)) @@ -2537,87 +2581,3 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen .contained() .with_style(style.container) } - -mod components { - - use gpui::{ - elements::{Empty, Flex, GeneralComponent, GeneralStyleableComponent, ParentElement}, - Action, Element, - }; - use theme::components::{ - action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle, - }; - - #[derive(Clone)] - pub struct DisclosureStyle { - disclosure: ToggleIconButtonStyle, - spacing: f32, - content: S, - } - - pub struct Disclosable { - disclosed: bool, - action: Box, - content: C, - style: S, - } - - impl Disclosable<(), ()> { - fn new(disclosed: bool, content: C, action: Box) -> Disclosable { - Disclosable { - disclosed, - content, - action, - style: (), - } - } - } - - impl GeneralStyleableComponent for Disclosable { - type Style = DisclosureStyle; - - type Output = Disclosable; - - fn with_style(self, style: Self::Style) -> Self::Output { - Disclosable { - disclosed: self.disclosed, - action: self.action, - content: self.content, - style, - } - } - } - - impl GeneralComponent for Disclosable> { - fn render( - self, - v: &mut V, - cx: &mut gpui::ViewContext, - ) -> gpui::AnyElement { - Flex::row() - .with_child( - ActionButton::new_dynamic(self.action) - .with_contents(Svg::new("path")) - .toggleable(self.disclosed) - .with_style(self.style.disclosure) - .element(), - ) - .with_child(Empty::new().constrained().with_width(self.style.spacing)) - .with_child(self.content.with_style(self.style.content).render(v, cx)) - .align_children_center() - .into_any() - } - } - - pub trait DisclosureExt { - fn disclosable(self, disclosed: bool, action: Box) -> Disclosable - where - Self: Sized; - } - - impl DisclosureExt for C { - fn disclosable(self, disclosed: bool, action: Box) -> Disclosable { - Disclosable::new(disclosed, self, action) - } - } -} diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs index aeeab8101d1e926f287e72c9fd22a44af8d645df..5aa648bd6573e6e2cec46a06eced88fe08effd94 100644 --- a/crates/gpui/examples/components.rs +++ b/crates/gpui/examples/components.rs @@ -2,7 +2,7 @@ use button_component::Button; use gpui::{ color::Color, - elements::{Component, ContainerStyle, Flex, Label, ParentElement}, + elements::{ContainerStyle, Flex, Label, ParentElement, StatefulComponent}, fonts::{self, TextStyle}, platform::WindowOptions, AnyElement, App, Element, Entity, View, ViewContext, @@ -72,7 +72,7 @@ impl View for TestView { TextStyle::for_color(Color::blue()), ) .with_style(ButtonStyle::fill(Color::yellow())) - .c_element(), + .stateful_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()), }) - .c_element(), + .stateful_element(), ) .expanded() .contained() @@ -114,7 +114,7 @@ mod theme { // Component creation: mod toggleable_button { use gpui::{ - elements::{Component, ContainerStyle, LabelStyle}, + elements::{ContainerStyle, LabelStyle, StatefulComponent}, scene::MouseClick, EventContext, View, }; @@ -156,7 +156,7 @@ mod toggleable_button { } } - impl Component for ToggleableButton { + impl StatefulComponent for ToggleableButton { fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { let button = if let Some(style) = self.style { self.button.with_style(*style.style_for(self.active)) @@ -171,7 +171,7 @@ mod toggleable_button { mod button_component { use gpui::{ - elements::{Component, ContainerStyle, Label, LabelStyle, MouseEventHandler}, + elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler, StatefulComponent}, platform::MouseButton, scene::MouseClick, AnyElement, Element, EventContext, TypeTag, View, ViewContext, @@ -212,7 +212,7 @@ mod button_component { } } - impl Component for Button { + impl StatefulComponent for Button { fn render(self, _: &mut V, cx: &mut ViewContext) -> AnyElement { let click_handler = self.click_handler; diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index c0484ef9395a9e25e82384b30aa5fdc88d81bf07..4fcf3815f58f328ff564f9e893b3c14ac81a4b6a 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -230,19 +230,26 @@ pub trait Element: 'static { MouseEventHandler::for_child::(self.into_any(), region_id) } - fn component(self) -> ElementAdapter + fn stateful_component(self) -> ElementAdapter where Self: Sized, { ElementAdapter::new(self.into_any()) } - fn dynamic_component(self) -> DynamicElementAdapter + fn component(self) -> DynamicElementAdapter where Self: Sized, { DynamicElementAdapter::new(self.into_any()) } + + fn styleable_component(self) -> StylableAdapter + where + Self: Sized, + { + DynamicElementAdapter::new(self.into_any()).stylable() + } } pub trait RenderElement { diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index f5a4180d7f379c059a175c731311ca46360f0132..703d6eca075ee59e04409c14c9a58fd2f64bc35a 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -9,35 +9,35 @@ use crate::{ use super::Empty; -pub trait GeneralComponent { +pub trait Component { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; - fn element(self) -> ComponentAdapter + fn element(self) -> StatefulAdapter where Self: Sized, { - ComponentAdapter::new(self) + StatefulAdapter::new(self) } - fn stylable(self) -> GeneralStylableComponentAdapter + fn stylable(self) -> StylableAdapter where Self: Sized, { - GeneralStylableComponentAdapter::new(self) + StylableAdapter::new(self) } } -pub struct GeneralStylableComponentAdapter { +pub struct StylableAdapter { component: C, } -impl GeneralStylableComponentAdapter { +impl StylableAdapter { pub fn new(component: C) -> Self { Self { component } } } -impl GeneralStyleableComponent for GeneralStylableComponentAdapter { +impl StyleableComponent for StylableAdapter { type Style = (); type Output = C; @@ -47,20 +47,20 @@ impl GeneralStyleableComponent for GeneralStylableComponent } } -pub trait GeneralStyleableComponent { +pub trait StyleableComponent { type Style: Clone; - type Output: GeneralComponent; + type Output: Component; fn with_style(self, style: Self::Style) -> Self::Output; } -impl GeneralComponent for () { +impl Component for () { fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { Empty::new().into_any() } } -impl GeneralStyleableComponent for () { +impl StyleableComponent for () { type Style = (); type Output = (); @@ -69,54 +69,54 @@ impl GeneralStyleableComponent for () { } } -pub trait StyleableComponent { +pub trait StatefulStyleableComponent { type Style: Clone; - type Output: Component; + type Output: StatefulComponent; - fn c_with_style(self, style: Self::Style) -> Self::Output; + fn stateful_with_style(self, style: Self::Style) -> Self::Output; } -impl StyleableComponent for C { +impl StatefulStyleableComponent for C { type Style = C::Style; type Output = C::Output; - fn c_with_style(self, style: Self::Style) -> Self::Output { + fn stateful_with_style(self, style: Self::Style) -> Self::Output { self.with_style(style) } } -pub trait Component { +pub trait StatefulComponent { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; - fn c_element(self) -> ComponentAdapter + fn stateful_element(self) -> StatefulAdapter where Self: Sized, { - ComponentAdapter::new(self) + StatefulAdapter::new(self) } - fn c_styleable(self) -> StylableComponentAdapter + fn stateful_styleable(self) -> StatefulStylableAdapter where Self: Sized, { - StylableComponentAdapter::new(self) + StatefulStylableAdapter::new(self) } } -impl Component for C { +impl StatefulComponent for C { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement { self.render(v, cx) } } // StylableComponent -> Component -pub struct StylableComponentAdapter, V: View> { +pub struct StatefulStylableAdapter, V: View> { component: C, phantom: std::marker::PhantomData, } -impl, V: View> StylableComponentAdapter { +impl, V: View> StatefulStylableAdapter { pub fn new(component: C) -> Self { Self { component, @@ -125,12 +125,14 @@ impl, V: View> StylableComponentAdapter { } } -impl, V: View> StyleableComponent for StylableComponentAdapter { +impl, V: View> StatefulStyleableComponent + for StatefulStylableAdapter +{ type Style = (); type Output = C; - fn c_with_style(self, _: Self::Style) -> Self::Output { + fn stateful_with_style(self, _: Self::Style) -> Self::Output { self.component } } @@ -149,7 +151,7 @@ impl DynamicElementAdapter { } } -impl GeneralComponent for DynamicElementAdapter { +impl Component for DynamicElementAdapter { fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { let element = self .element @@ -174,20 +176,20 @@ impl ElementAdapter { } } -impl Component for ElementAdapter { +impl StatefulComponent for ElementAdapter { fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { self.element } } // Component -> Element -pub struct ComponentAdapter { +pub struct StatefulAdapter { component: Option, element: Option>, phantom: PhantomData, } -impl ComponentAdapter { +impl StatefulAdapter { pub fn new(e: E) -> Self { Self { component: Some(e), @@ -197,7 +199,7 @@ impl ComponentAdapter { } } -impl + 'static> Element for ComponentAdapter { +impl + 'static> Element for StatefulAdapter { type LayoutState = (); type PaintState = (); diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index bb1366b4e767d72b32cd15ad49e8e429cab4fc7b..68d3e57610587d9984d36e8202eca343b6ba9c9f 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -45,6 +45,14 @@ impl ContainerStyle { ..Default::default() } } + + pub fn additional_length(&self) -> f32 { + self.padding.left + + self.padding.right + + self.border.width * 2. + + self.margin.left + + self.margin.right + } } pub struct Container { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index b3167efe507f6ae36fb870412793d38d39ae4e20..2dc45e397394b84671533471e7c93937d9469354 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -2,7 +2,7 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; use gpui::{ actions, - elements::{Component, GeneralStyleableComponent, TooltipStyle}, + elements::{Component, StyleableComponent, TooltipStyle}, Action, AnyElement, AppContext, Element, View, }; pub use mode::SearchMode; @@ -96,7 +96,7 @@ impl SearchOptions { .with_contents(Svg::new(self.icon())) .toggleable(active) .with_style(button_style) - .c_element() + .element() .into_any() } } diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index 3679b1fa4f1c1313d4fa8340d518d9949c5d0a31..6f9242199f6037e4f504f7cc0cf5d92707249a41 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -1,23 +1,147 @@ -use gpui::elements::GeneralStyleableComponent; +use gpui::{elements::StyleableComponent, Action}; use crate::{Interactive, Toggleable}; -use self::{action_button::ButtonStyle, svg::SvgStyle, toggle::Toggle}; +use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle}; 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; } -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) + } +} + +pub mod disclosure { + + use gpui::{ + elements::{Component, Empty, Flex, ParentElement, StyleableComponent}, + Action, Element, + }; + use schemars::JsonSchema; + use serde_derive::Deserialize; + + use super::{action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle}; + + #[derive(Clone, Default, Deserialize, JsonSchema)] + pub struct DisclosureStyle { + pub button: ToggleIconButtonStyle, + pub spacing: f32, + #[serde(flatten)] + content: S, + } + + impl DisclosureStyle { + pub fn button_space(&self) -> f32 { + self.spacing + self.button.button_width.unwrap() + } + } + + pub struct Disclosable { + disclosed: Option, + action: Box, + id: usize, + content: C, + style: S, + } + + impl Disclosable<(), ()> { + pub fn new( + disclosed: Option, + content: C, + action: Box, + id: usize, + ) -> Disclosable { + Disclosable { + disclosed, + content, + action, + id, + style: (), + } + } + } + + impl StyleableComponent for Disclosable { + type Style = DisclosureStyle; + + type Output = Disclosable; + + fn with_style(self, style: Self::Style) -> Self::Output { + Disclosable { + disclosed: self.disclosed, + action: self.action, + content: self.content, + id: self.id, + style, + } + } + } + + impl Component for Disclosable> { + fn render( + self, + v: &mut V, + cx: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + Flex::row() + .with_child(if let Some(disclosed) = self.disclosed { + ActionButton::new_dynamic(self.action) + .with_id(self.id) + .with_contents(Svg::new(if disclosed { + "icons/file_icons/chevron_down.svg" + } else { + "icons/file_icons/chevron_right.svg" + })) + .toggleable(disclosed) + .with_style(self.style.button) + .element() + .into_any() + } else { + Empty::new() + .into_any() + .constrained() + // TODO: Why is this optional at all? + .with_width(self.style.button.button_width.unwrap()) + .into_any() + }) + .with_child(Empty::new().constrained().with_width(self.style.spacing)) + .with_child( + self.content + .with_style(self.style.content) + .render(v, cx) + .flex(1., true), + ) + .align_children_center() + .into_any() + } + } } pub mod toggle { - use gpui::elements::{GeneralComponent, GeneralStyleableComponent}; + use gpui::elements::{Component, StyleableComponent}; use crate::Toggleable; @@ -27,7 +151,7 @@ pub mod toggle { component: C, } - impl Toggle { + impl Toggle { pub fn new(component: C, active: bool) -> Self { Toggle { active, @@ -37,7 +161,7 @@ pub mod toggle { } } - impl GeneralStyleableComponent for Toggle { + impl StyleableComponent for Toggle { type Style = Toggleable; type Output = Toggle; @@ -51,7 +175,7 @@ pub mod toggle { } } - impl GeneralComponent for Toggle> { + impl Component for Toggle> { fn render( self, v: &mut V, @@ -69,8 +193,7 @@ pub mod action_button { use gpui::{ elements::{ - ContainerStyle, GeneralComponent, GeneralStyleableComponent, MouseEventHandler, - TooltipStyle, + Component, ContainerStyle, MouseEventHandler, StyleableComponent, TooltipStyle, }, platform::{CursorStyle, MouseButton}, Action, Element, TypeTag, View, @@ -80,24 +203,28 @@ pub mod action_button { use crate::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 ActionButton { action: Box, tooltip: Option<(Cow<'static, str>, TooltipStyle)>, tag: TypeTag, + id: usize, 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) -> Self { Self { @@ -105,6 +232,7 @@ pub mod action_button { tag: action.type_tag(), style: Interactive::new_blank(), tooltip: None, + id: 0, action, } } @@ -122,21 +250,24 @@ pub mod action_button { self } - pub fn with_contents( - self, - contents: C, - ) -> ActionButton { + pub fn with_id(mut self, id: usize) -> Self { + self.id = id; + self + } + + pub fn with_contents(self, contents: C) -> ActionButton { ActionButton { action: self.action, tag: self.tag, style: self.style, tooltip: self.tooltip, + id: self.id, contents, } } } - impl GeneralStyleableComponent for ActionButton { + impl StyleableComponent for ActionButton { type Style = Interactive>; type Output = ActionButton>; @@ -146,15 +277,15 @@ pub mod action_button { tag: self.tag, contents: self.contents, tooltip: self.tooltip, - + id: self.id, style, } } } - impl GeneralComponent for ActionButton> { + impl Component for ActionButton> { 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 mut button = MouseEventHandler::new_dynamic(self.tag, self.id, cx, |state, cx| { let style = self.style.style_for(state); let mut contents = self .contents @@ -177,8 +308,13 @@ pub mod action_button { .on_click(MouseButton::Left, { let action = self.action.boxed_clone(); move |_, _, cx| { - cx.window() - .dispatch_action(cx.view_id(), action.as_ref(), cx); + let window = cx.window(); + let view = cx.view_id(); + let action = action.boxed_clone(); + cx.spawn(|_, mut cx| async move { + window.dispatch_action(view, action.as_ref(), &mut cx) + }) + .detach(); } }) .with_cursor_style(CursorStyle::PointingHand) @@ -199,7 +335,7 @@ pub mod svg { use std::borrow::Cow; use gpui::{ - elements::{GeneralComponent, GeneralStyleableComponent}, + elements::{Component, Empty, StyleableComponent}, Element, }; use schemars::JsonSchema; @@ -222,6 +358,7 @@ pub mod svg { pub enum IconSize { IconSize { icon_size: f32 }, Dimensions { width: f32, height: f32 }, + IconDimensions { icon_width: f32, icon_height: f32 }, } #[derive(Deserialize)] @@ -245,6 +382,14 @@ pub mod svg { icon_height: height, color, }, + IconSize::IconDimensions { + icon_width, + icon_height, + } => SvgStyle { + icon_width, + icon_height, + color, + }, }; Ok(result) @@ -252,20 +397,27 @@ pub mod svg { } pub struct Svg { - path: Cow<'static, str>, + path: Option>, style: S, } impl Svg<()> { pub fn new(path: impl Into>) -> Self { Self { - path: path.into(), + path: Some(path.into()), + style: (), + } + } + + pub fn optional(path: Option>>) -> Self { + Self { + path: path.map(Into::into), style: (), } } } - impl GeneralStyleableComponent for Svg<()> { + impl StyleableComponent for Svg<()> { type Style = SvgStyle; type Output = Svg; @@ -278,18 +430,23 @@ pub mod svg { } } - impl GeneralComponent for Svg { + impl Component 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() + if let Some(path) = self.path { + gpui::elements::Svg::new(path) + .with_color(self.style.color) + .constrained() + } else { + Empty::new().constrained() + } + .constrained() + .with_width(self.style.icon_width) + .with_height(self.style.icon_height) + .into_any() } } } @@ -298,7 +455,7 @@ pub mod label { use std::borrow::Cow; use gpui::{ - elements::{GeneralComponent, GeneralStyleableComponent, LabelStyle}, + elements::{Component, LabelStyle, StyleableComponent}, Element, }; @@ -316,7 +473,7 @@ pub mod label { } } - impl GeneralStyleableComponent for Label<()> { + impl StyleableComponent for Label<()> { type Style = LabelStyle; type Output = Label; @@ -329,7 +486,7 @@ pub mod label { } } - impl GeneralComponent for Label { + impl Component for Label { fn render( self, _: &mut V, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 80e823632a9c10613abbf1f3f5effee615c1cd15..08da9af29912a794cf6893911416b0e2525e8d8b 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::ToggleIconButtonStyle; +use components::{disclosure::DisclosureStyle, ToggleIconButtonStyle}; use gpui::{ color::Color, elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle}, @@ -14,7 +14,7 @@ use schemars::JsonSchema; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use settings::SettingsStore; -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, ops::Deref, sync::Arc}; use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle}; pub use theme_registry::*; @@ -221,6 +221,7 @@ pub struct CopilotAuthAuthorized { pub struct CollabPanel { #[serde(flatten)] pub container: ContainerStyle, + pub disclosure: DisclosureStyle<()>, pub list_empty_state: Toggleable>, pub list_empty_icon: Icon, pub list_empty_label_container: ContainerStyle, @@ -890,6 +891,14 @@ pub struct Interactive { pub disabled: Option, } +impl Deref for Interactive { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.default + } +} + impl Interactive<()> { pub fn new_blank() -> Self { Self { @@ -907,6 +916,14 @@ pub struct Toggleable { inactive: T, } +impl Deref for Toggleable { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inactive + } +} + impl Toggleable<()> { pub fn new_blank() -> Self { Self { diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index a102ee769194d4b4e0189f5b2541de53c3408180..5242f90c8d83fcdbf842c453f468253ec4eb0f6b 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -152,6 +152,10 @@ export default function contacts_panel(): any { return { ...collab_modals(), + disclosure: { + button: toggleable_icon_button(theme, {}), + spacing: 4, + }, log_in_button: interactive({ base: { background: background(theme.middle), From e946b0a2ece63ba160804b5260a4b00433e3130e Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 19 Aug 2023 14:40:05 -0700 Subject: [PATCH 4/6] Finish building out adapters and names Document core traits Add start for a component storybook --- Cargo.lock | 11 ++ Cargo.toml | 1 + crates/collab_ui/src/collab_panel.rs | 11 +- crates/gpui/examples/components.rs | 4 +- crates/gpui/src/elements.rs | 12 +- crates/gpui/src/elements/component.rs | 192 ++++++++++++++++++-------- crates/search/src/search.rs | 8 +- crates/theme/src/components.rs | 117 +++++++--------- 8 files changed, 208 insertions(+), 148 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aab5504a86b61ae6b7c2c2c7591cc0c829ead3d4..01c74fb4ceb62f59d322df7ba8355c3c1414de52 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 7ea79138c026dc6e1bbef10b22fd925ff93f0f1b..d434f347734bfa2922eebe261031c710a6e10812 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 41c87094d2c27c561c1c0ddb5548f461cdfdb511..52711281c77f9eb6a92b64855d90a25c50615361 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 5aa648bd6573e6e2cec46a06eced88fe08effd94..d3ca0d1eccaef343058968856a0e8715849357a8 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 4fcf3815f58f328ff564f9e893b3c14ac81a4b6a..3c80ef73fe83d0d953c368c6f45a322ee47f75fb 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 703d6eca075ee59e04409c14c9a58fd2f64bc35a..e391ff9b057edb6e8740d2ec7963bcc45421d67a 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 { - type Style: Clone; - type Output: Component; +/// 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 with_style(self, style: Self::Style) -> Self::Output; + 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()) + } } -impl Component for () { - fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { - Empty::new().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) } } -impl StyleableComponent for () { - type Style = (); - type Output = (); +/// Same as stylable, but generic over a view type +pub trait StatefulStylable: StatefulComponent { + type Style: Clone; - fn with_style(self, _: Self::Style) -> Self::Output { - () - } + fn stateful_with_style(self, style: Self::Style) -> Self; } -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; - - fn stateful_element(self) -> StatefulAdapter - where - Self: Sized, - { - StatefulAdapter::new(self) - } +// A helper for converting stateless components into stateful ones +pub struct StatefulAdapter { + component: C, + phantom: std::marker::PhantomData, +} - 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 2dc45e397394b84671533471e7c93937d9469354..47f7f485c486a86d3ff3725b5e05aedb52305b36 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 6f9242199f6037e4f504f7cc0cf5d92707249a41..d9a857915c53f0f70dcb8849abbc3fea538551af 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() } } From bfd3e53dcd0912f0534a5fff9c18beafba42cca0 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 19 Aug 2023 16:29:24 -0700 Subject: [PATCH 5/6] Implement component test page --- Cargo.lock | 3 + crates/component_test/Cargo.toml | 18 +++ crates/component_test/src/component_test.rs | 122 ++++++++++++++++++++ crates/gpui/src/elements/component.rs | 8 +- crates/gpui/src/elements/flex.rs | 33 +++++- crates/theme/src/components.rs | 18 +-- crates/theme/src/theme.rs | 10 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + styles/src/style_tree/app.ts | 5 +- styles/src/style_tree/component_test.ts | 19 +++ 11 files changed, 217 insertions(+), 21 deletions(-) create mode 100644 crates/component_test/Cargo.toml create mode 100644 crates/component_test/src/component_test.rs create mode 100644 styles/src/style_tree/component_test.ts diff --git a/Cargo.lock b/Cargo.lock index 01c74fb4ceb62f59d322df7ba8355c3c1414de52..0081626cabe162ff9faad9a779ad27bbe9390005 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1566,7 +1566,9 @@ dependencies = [ name = "component_test" version = "0.1.0" dependencies = [ + "anyhow", "gpui", + "project", "settings", "theme", "util", @@ -9668,6 +9670,7 @@ dependencies = [ "collab_ui", "collections", "command_palette", + "component_test", "context_menu", "copilot", "copilot_button", diff --git a/crates/component_test/Cargo.toml b/crates/component_test/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..d714f6f72f62bf0c869439d07e937592a6ea644b --- /dev/null +++ b/crates/component_test/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "component_test" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/component_test.rs" +doctest = false + +[dependencies] +anyhow.workspace = true +gpui = { path = "../gpui" } +settings = { path = "../settings" } +util = { path = "../util" } +theme = { path = "../theme" } +workspace = { path = "../workspace" } +project = { path = "../project" } diff --git a/crates/component_test/src/component_test.rs b/crates/component_test/src/component_test.rs new file mode 100644 index 0000000000000000000000000000000000000000..30fd431290d3135ea5783e8a145be02f49b6933a --- /dev/null +++ b/crates/component_test/src/component_test.rs @@ -0,0 +1,122 @@ +use gpui::{ + actions, + color::Color, + elements::{Component, Flex, ParentElement, SafeStylable}, + AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, +}; +use project::Project; +use theme::components::{action_button::Button, label::Label, ComponentExt}; +use workspace::{ + item::Item, register_deserializable_item, ItemId, Pane, PaneBackdrop, Workspace, WorkspaceId, +}; + +pub fn init(cx: &mut AppContext) { + cx.add_action(ComponentTest::toggle_disclosure); + cx.add_action(ComponentTest::toggle_toggle); + cx.add_action(ComponentTest::deploy); + register_deserializable_item::(cx); +} + +actions!( + test, + [NoAction, ToggleDisclosure, ToggleToggle, NewComponentTest] +); + +struct ComponentTest { + disclosed: bool, + toggled: bool, +} + +impl ComponentTest { + fn new() -> Self { + Self { + disclosed: false, + toggled: false, + } + } + + fn deploy(workspace: &mut Workspace, _: &NewComponentTest, cx: &mut ViewContext) { + workspace.add_item(Box::new(cx.add_view(|_| ComponentTest::new())), cx); + } + + fn toggle_disclosure(&mut self, _: &ToggleDisclosure, cx: &mut ViewContext) { + self.disclosed = !self.disclosed; + cx.notify(); + } + + fn toggle_toggle(&mut self, _: &ToggleToggle, cx: &mut ViewContext) { + self.toggled = !self.toggled; + cx.notify(); + } +} + +impl Entity for ComponentTest { + type Event = (); +} + +impl View for ComponentTest { + fn ui_name() -> &'static str { + "Component Test" + } + + fn render(&mut self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + let theme = theme::current(cx); + + PaneBackdrop::new( + cx.view_id(), + Flex::column() + .with_spacing(10.) + .with_child( + Button::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) + .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone()) + .with_contents(Label::new("Toggle me!")) + .toggleable(self.toggled) + .with_style(theme.component_test.toggle.clone()) + .element(), + ) + .with_child( + Label::new("A disclosure") + .disclosable(Some(self.disclosed), Box::new(ToggleDisclosure)) + .with_style(theme.component_test.disclosure.clone()) + .element(), + ) + .constrained() + .with_width(200.) + .aligned() + .into_any(), + ) + .into_any() + } +} + +impl Item for ComponentTest { + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + _: &AppContext, + ) -> gpui::AnyElement { + gpui::elements::Label::new("Component test", style.label.clone()).into_any() + } + + fn serialized_item_kind() -> Option<&'static str> { + Some("ComponentTest") + } + + fn deserialize( + _project: ModelHandle, + _workspace: WeakViewHandle, + _workspace_id: WorkspaceId, + _item_id: ItemId, + cx: &mut ViewContext, + ) -> Task>> { + Task::ready(Ok(cx.add_view(|_| Self::new()))) + } +} diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index e391ff9b057edb6e8740d2ec7963bcc45421d67a..3776abcaa0ac75d725c8c98ed478e5b4a4630a01 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -128,7 +128,7 @@ impl StatefulComponent for C { pub trait StatefulStylable: StatefulComponent { type Style: Clone; - fn stateful_with_style(self, style: Self::Style) -> Self; + fn with_style(self, style: Self::Style) -> Self; } /// Same as SafeStylable, but generic over a view type @@ -136,7 +136,7 @@ pub trait StatefulSafeStylable { type Style: Clone; type Output: StatefulComponent; - fn stateful_with_style(self, style: Self::Style) -> Self::Output; + fn with_style(self, style: Self::Style) -> Self::Output; } /// Converting from stateless to stateful @@ -145,7 +145,7 @@ impl StatefulSafeStylable for C { type Output = C::Output; - fn stateful_with_style(self, style: Self::Style) -> Self::Output { + fn with_style(self, style: Self::Style) -> Self::Output { self.with_style(style) } } @@ -192,7 +192,7 @@ impl, V: View> StatefulSafeStylable for StatefulStyla type Output = C; - fn stateful_with_style(self, _: Self::Style) -> Self::Output { + fn with_style(self, _: Self::Style) -> Self::Output { self.component } } diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 3000b9575d5824ea42fd09cb5d10de4353434b25..9d175afc03ae3183772a1299bbfdabf22362f6da 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -22,6 +22,7 @@ pub struct Flex { children: Vec>, scroll_state: Option<(ElementStateHandle>, usize)>, child_alignment: f32, + spacing: f32, } impl Flex { @@ -31,6 +32,7 @@ impl Flex { children: Default::default(), scroll_state: None, child_alignment: -1., + spacing: 0., } } @@ -51,6 +53,11 @@ impl Flex { self } + pub fn with_spacing(mut self, spacing: f32) -> Self { + self.spacing = spacing; + self + } + pub fn scrollable( mut self, element_id: usize, @@ -81,7 +88,8 @@ impl Flex { cx: &mut LayoutContext, ) { let cross_axis = self.axis.invert(); - for child in &mut self.children { + let last = self.children.len() - 1; + for (ix, child) in &mut self.children.iter_mut().enumerate() { if let Some(metadata) = child.metadata::() { if let Some((flex, expanded)) = metadata.flex { if expanded != layout_expanded { @@ -93,6 +101,10 @@ impl Flex { } else { let space_per_flex = *remaining_space / *remaining_flex; space_per_flex * flex + } - if ix == 0 || ix == last { + self.spacing / 2. + } else { + self.spacing }; let child_min = if expanded { child_max } else { 0. }; let child_constraint = match self.axis { @@ -137,7 +149,8 @@ impl Element for Flex { let cross_axis = self.axis.invert(); let mut cross_axis_max: f32 = 0.0; - for child in &mut self.children { + let last = self.children.len().saturating_sub(1); + for (ix, child) in &mut self.children.iter_mut().enumerate() { let metadata = child.metadata::(); contains_float |= metadata.map_or(false, |metadata| metadata.float); @@ -155,7 +168,12 @@ impl Element for Flex { ), }; let size = child.layout(child_constraint, view, cx); - fixed_space += size.along(self.axis); + fixed_space += size.along(self.axis) + + if ix == 0 || ix == last { + self.spacing / 2. + } else { + self.spacing + }; cross_axis_max = cross_axis_max.max(size.along(cross_axis)); } } @@ -315,7 +333,8 @@ impl Element for Flex { } } - for child in &mut self.children { + let last = self.children.len().saturating_sub(1); + for (ix, child) in &mut self.children.iter_mut().enumerate() { if remaining_space > 0. { if let Some(metadata) = child.metadata::() { if metadata.float { @@ -353,9 +372,11 @@ impl Element for Flex { child.paint(scene, aligned_child_origin, visible_bounds, view, cx); + let spacing = if ix == last { 0. } else { self.spacing }; + match self.axis { - Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0), - Axis::Vertical => child_origin += vec2f(0.0, child.size().y()), + Axis::Horizontal => child_origin += vec2f(child.size().x() + spacing, 0.0), + Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + spacing), } } diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index d9a857915c53f0f70dcb8849abbc3fea538551af..fc208954cf6ed7773b2f80e1e4cde540343e8d8a 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -74,8 +74,9 @@ pub mod disclosure { } impl Disclosable { - pub fn with_id(self, id: usize) -> Disclosable { - Disclosable { id, ..self } + pub fn with_id(mut self, id: usize) -> Disclosable { + self.id = id; + self } } @@ -181,7 +182,7 @@ pub mod action_button { use gpui::{ elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle}, platform::{CursorStyle, MouseButton}, - Action, Element, TypeTag, View, + Action, Element, EventContext, TypeTag, View, }; use schemars::JsonSchema; use serde_derive::Deserialize; @@ -211,14 +212,14 @@ pub mod action_button { } impl Button<(), ()> { - pub fn dynamic_action(action: Box) -> Self { + pub fn dynamic_action(action: Box) -> Button<(), ()> { Self { contents: (), tag: action.type_tag(), + action, style: Interactive::new_blank(), tooltip: None, id: 0, - action, } } @@ -292,7 +293,7 @@ pub mod action_button { }) .on_click(MouseButton::Left, { let action = self.action.boxed_clone(); - move |_, _, cx| { + move |_, _, cx: &mut EventContext| { let window = cx.window(); let view = cx.view_id(); let action = action.boxed_clone(); @@ -437,6 +438,7 @@ pub mod label { use gpui::{ elements::{Component, LabelStyle, SafeStylable}, + fonts::TextStyle, Element, }; @@ -455,14 +457,14 @@ pub mod label { } impl SafeStylable for Label<()> { - type Style = LabelStyle; + type Style = TextStyle; type Output = Label; fn with_style(self, style: Self::Style) -> Self::Output { Label { text: self.text, - style, + style: style.into(), } } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 08da9af29912a794cf6893911416b0e2525e8d8b..0f34963708deae3797b88a2217f29860b2eeb74a 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::{disclosure::DisclosureStyle, ToggleIconButtonStyle}; +use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle}; use gpui::{ color::Color, elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle}, @@ -66,6 +66,7 @@ pub struct Theme { pub feedback: FeedbackStyle, pub welcome: WelcomeStyle, pub titlebar: Titlebar, + pub component_test: ComponentTest, } #[derive(Deserialize, Default, Clone, JsonSchema)] @@ -260,6 +261,13 @@ pub struct CollabPanel { pub face_overlap: f32, } +#[derive(Deserialize, Default, JsonSchema)] +pub struct ComponentTest { + pub button: Interactive>, + pub toggle: Toggleable>>, + pub disclosure: DisclosureStyle, +} + #[derive(Deserialize, Default, JsonSchema)] pub struct TabbedModal { pub tab_button: Toggleable>, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 988648d4b155d182889273eb524b83b0e2d13d59..cb66a6b587332b20075da80a63be93bf293890ba 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -25,6 +25,7 @@ cli = { path = "../cli" } collab_ui = { path = "../collab_ui" } collections = { path = "../collections" } command_palette = { path = "../command_palette" } +component_test = { path = "../component_test" } context_menu = { path = "../context_menu" } client = { path = "../client" } clock = { path = "../clock" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index deef3b85eba1cadc377be2e9d8dd9249cb5aa636..caeaecededf585df32750555ab5a1bdd6a69e380 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -166,6 +166,7 @@ fn main() { terminal_view::init(cx); copilot::init(http.clone(), node_runtime, cx); ai::init(cx); + component_test::init(cx); cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach(); cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) diff --git a/styles/src/style_tree/app.ts b/styles/src/style_tree/app.ts index ee5e19e11137a48097c6873dfca03d00c68d2279..cf7e0538a72874674700410b9d892fd057bfa0ca 100644 --- a/styles/src/style_tree/app.ts +++ b/styles/src/style_tree/app.ts @@ -12,7 +12,6 @@ import simple_message_notification from "./simple_message_notification" import project_shared_notification from "./project_shared_notification" import tooltip from "./tooltip" import terminal from "./terminal" -import contact_finder from "./contact_finder" import collab_panel from "./collab_panel" import toolbar_dropdown_menu from "./toolbar_dropdown_menu" import incoming_call_notification from "./incoming_call_notification" @@ -22,6 +21,7 @@ import assistant from "./assistant" import { titlebar } from "./titlebar" import editor from "./editor" import feedback from "./feedback" +import component_test from "./component_test" import { useTheme } from "../common" export default function app(): any { @@ -54,6 +54,7 @@ export default function app(): any { tooltip: tooltip(), terminal: terminal(), assistant: assistant(), - feedback: feedback() + feedback: feedback(), + component_test: component_test(), } } diff --git a/styles/src/style_tree/component_test.ts b/styles/src/style_tree/component_test.ts new file mode 100644 index 0000000000000000000000000000000000000000..eadbb5c2f1750e34b4e3ba99adecd8fff575abda --- /dev/null +++ b/styles/src/style_tree/component_test.ts @@ -0,0 +1,19 @@ +import { toggle_label_button_style } from "../component/label_button" +import { useTheme } from "../common" +import { text_button } from "../component/text_button" +import { toggleable_icon_button } from "../component/icon_button" +import { text } from "./components" + +export default function contacts_panel(): any { + const theme = useTheme() + + return { + button: text_button({}), + toggle: toggle_label_button_style({ active_color: "accent" }), + disclosure: { + ...text(theme.lowest, "sans", "base"), + button: toggleable_icon_button(theme, {}), + spacing: 4, + } + } +} From 2a182b6a7b1df98ec5019bd9d8ccde2e1cd22628 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 23 Aug 2023 16:25:17 -0700 Subject: [PATCH 6/6] Tune styles and disclosable elements --- crates/collab_ui/src/collab_panel.rs | 68 +++++++++++++++++++++---- crates/theme/src/components.rs | 16 +++--- styles/src/component/icon_button.ts | 14 ++--- styles/src/style_tree/collab_panel.ts | 9 ++-- styles/src/style_tree/component_test.ts | 16 ++++-- 5 files changed, 93 insertions(+), 30 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 52711281c77f9eb6a92b64855d90a25c50615361..5623ada42dcfcb8dc9862e23039f44aa182aa232 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -55,7 +55,7 @@ struct RemoveChannel { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct ToggleCollapsed { +struct ToggleCollapse { channel_id: u64, } @@ -79,7 +79,16 @@ struct RenameChannel { channel_id: u64, } -actions!(collab_panel, [ToggleFocus, Remove, Secondary]); +actions!( + collab_panel, + [ + ToggleFocus, + Remove, + Secondary, + CollapseSelectedChannel, + ExpandSelectedChannel + ] +); impl_actions!( collab_panel, @@ -89,7 +98,7 @@ impl_actions!( InviteMembers, ManageMembers, RenameChannel, - ToggleCollapsed + ToggleCollapse ] ); @@ -113,6 +122,8 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::rename_selected_channel); cx.add_action(CollabPanel::rename_channel); cx.add_action(CollabPanel::toggle_channel_collapsed); + cx.add_action(CollabPanel::collapse_selected_channel); + cx.add_action(CollabPanel::expand_selected_channel) } #[derive(Debug)] @@ -1356,7 +1367,7 @@ impl CollabPanel { .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, this, cx| { if can_collapse { - this.toggle_expanded(section, cx); + this.toggle_section_expanded(section, cx); } }) } @@ -1633,7 +1644,7 @@ impl CollabPanel { }) .align_children_center() .styleable_component() - .disclosable(disclosed, Box::new(ToggleCollapsed { channel_id })) + .disclosable(disclosed, Box::new(ToggleCollapse { channel_id })) .with_id(channel_id as usize) .with_style(theme.disclosure.clone()) .element() @@ -1863,6 +1874,12 @@ impl CollabPanel { OverlayPositionMode::Window }); + let expand_action_name = if self.is_channel_collapsed(channel_id) { + "Expand Subchannels" + } else { + "Collapse Subchannels" + }; + context_menu.show( position.unwrap_or_default(), if self.context_menu_on_selected { @@ -1871,6 +1888,7 @@ impl CollabPanel { gpui::elements::AnchorCorner::BottomLeft }, vec![ + ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }), ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), ContextMenuItem::Separator, ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }), @@ -1950,7 +1968,7 @@ impl CollabPanel { | Section::Online | Section::Offline | Section::ChannelInvites => { - self.toggle_expanded(*section, cx); + self.toggle_section_expanded(*section, cx); } }, ListEntry::Contact { contact, calling } => { @@ -2038,7 +2056,7 @@ impl CollabPanel { } } - fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext) { + fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext) { if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { self.collapsed_sections.remove(ix); } else { @@ -2047,8 +2065,37 @@ impl CollabPanel { self.update_entries(false, cx); } - fn toggle_channel_collapsed(&mut self, action: &ToggleCollapsed, cx: &mut ViewContext) { + fn collapse_selected_channel( + &mut self, + _: &CollapseSelectedChannel, + cx: &mut ViewContext, + ) { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; + + if self.is_channel_collapsed(channel_id) { + return; + } + + self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx) + } + + fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; + + if !self.is_channel_collapsed(channel_id) { + return; + } + + self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx) + } + + fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext) { let channel_id = action.channel_id; + match self.collapsed_channels.binary_search(&channel_id) { Ok(ix) => { self.collapsed_channels.remove(ix); @@ -2057,8 +2104,9 @@ impl CollabPanel { self.collapsed_channels.insert(ix, channel_id); } }; - self.update_entries(false, cx); + self.update_entries(true, cx); cx.notify(); + cx.focus_self(); } fn is_channel_collapsed(&self, channel: ChannelId) -> bool { @@ -2104,6 +2152,8 @@ impl CollabPanel { } fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext) { + self.collapsed_channels + .retain(|&channel| channel != action.channel_id); self.channel_editing_state = Some(ChannelEditingState::Create { parent_id: Some(action.channel_id), pending_name: None, diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index fc208954cf6ed7773b2f80e1e4cde540343e8d8a..8eaab91fe69e17cfa41d6874a89195db51c11721 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -4,7 +4,8 @@ use crate::{Interactive, Toggleable}; use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle}; -pub type ToggleIconButtonStyle = Toggleable>>; +pub type IconButtonStyle = Interactive>; +pub type ToggleIconButtonStyle = Toggleable; pub trait ComponentExt { fn toggleable(self, active: bool) -> Toggle; @@ -27,17 +28,19 @@ impl ComponentExt for C { pub mod disclosure { use gpui::{ - elements::{Component, Empty, Flex, ParentElement, SafeStylable}, + elements::{Component, ContainerStyle, Empty, Flex, ParentElement, SafeStylable}, Action, Element, }; use schemars::JsonSchema; use serde_derive::Deserialize; - use super::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle}; + use super::{action_button::Button, svg::Svg, ComponentExt, IconButtonStyle}; #[derive(Clone, Default, Deserialize, JsonSchema)] pub struct DisclosureStyle { - pub button: ToggleIconButtonStyle, + pub button: IconButtonStyle, + #[serde(flatten)] + pub container: ContainerStyle, pub spacing: f32, #[serde(flatten)] content: S, @@ -99,6 +102,7 @@ pub mod disclosure { impl Component for Disclosable> { fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { Flex::row() + .with_spacing(self.style.spacing) .with_child(if let Some(disclosed) = self.disclosed { Button::dynamic_action(self.action) .with_id(self.id) @@ -107,7 +111,6 @@ pub mod disclosure { } else { "icons/file_icons/chevron_right.svg" })) - .toggleable(disclosed) .with_style(self.style.button) .element() .into_any() @@ -119,7 +122,6 @@ pub mod disclosure { .with_width(self.style.button.button_width.unwrap()) .into_any() }) - .with_child(Empty::new().constrained().with_width(self.style.spacing)) .with_child( self.content .with_style(self.style.content) @@ -127,6 +129,8 @@ pub mod disclosure { .flex(1., true), ) .align_children_center() + .contained() + .with_style(self.style.container) .into_any() } } diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index 1a2d0bcec491abdad0bc43a7c5d1599052aca622..935909afdbb714dda754f003a01ed264f1f9721c 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -44,10 +44,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO } const padding = { - top: size === Button.size.Small ? 0 : 2, - bottom: size === Button.size.Small ? 0 : 2, - left: size === Button.size.Small ? 0 : 4, - right: size === Button.size.Small ? 0 : 4, + top: size === Button.size.Small ? 2 : 2, + bottom: size === Button.size.Small ? 2 : 2, + left: size === Button.size.Small ? 2 : 4, + right: size === Button.size.Small ? 2 : 4, } return interactive({ @@ -55,10 +55,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO corner_radius: 6, padding: padding, margin: m, - icon_width: 14, + icon_width: 12, icon_height: 14, - button_width: 20, - button_height: 16, + button_width: size === Button.size.Small ? 16 : 20, + button_height: 14, }, state: { default: { diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 5242f90c8d83fcdbf842c453f468253ec4eb0f6b..07f367c8afe077fd3d0bbca3ab424ecd07006078 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -14,6 +14,7 @@ import { indicator } from "../component/indicator" export default function contacts_panel(): any { const theme = useTheme() + const CHANNEL_SPACING = 4 as const const NAME_MARGIN = 6 as const const SPACING = 12 as const const INDENT_SIZE = 8 as const @@ -153,8 +154,8 @@ export default function contacts_panel(): any { return { ...collab_modals(), disclosure: { - button: toggleable_icon_button(theme, {}), - spacing: 4, + button: icon_button({ variant: "ghost", size: "sm" }), + spacing: CHANNEL_SPACING, }, log_in_button: interactive({ base: { @@ -198,7 +199,7 @@ export default function contacts_panel(): any { add_channel_button: header_icon_button, leave_call_button: header_icon_button, row_height: ITEM_HEIGHT, - channel_indent: INDENT_SIZE * 2, + channel_indent: INDENT_SIZE * 2 + 2, section_icon_size: 14, header_row: { ...text(layer, "sans", { size: "sm", weight: "bold" }), @@ -268,7 +269,7 @@ export default function contacts_panel(): any { channel_name: { ...text(layer, "sans", { size: "sm" }), margin: { - left: NAME_MARGIN, + left: CHANNEL_SPACING, }, }, list_empty_label_container: { diff --git a/styles/src/style_tree/component_test.ts b/styles/src/style_tree/component_test.ts index eadbb5c2f1750e34b4e3ba99adecd8fff575abda..e2bb0915c172d946bfd882aa768f3db54e82cb63 100644 --- a/styles/src/style_tree/component_test.ts +++ b/styles/src/style_tree/component_test.ts @@ -1,18 +1,26 @@ -import { toggle_label_button_style } from "../component/label_button" + import { useTheme } from "../common" import { text_button } from "../component/text_button" -import { toggleable_icon_button } from "../component/icon_button" +import { icon_button } from "../component/icon_button" import { text } from "./components" +import { toggleable } from "../element" export default function contacts_panel(): any { const theme = useTheme() return { button: text_button({}), - toggle: toggle_label_button_style({ active_color: "accent" }), + toggle: toggleable({ + base: text_button({}), + state: { + active: { + ...text_button({ color: "accent" }) + } + } + }), disclosure: { ...text(theme.lowest, "sans", "base"), - button: toggleable_icon_button(theme, {}), + button: icon_button({ variant: "ghost" }), spacing: 4, } }