Add disclosable components into channels

Mikayla created

Rename components to more closely match their purpose

Change summary

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(-)

Detailed changes

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()
     }

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<Client>, 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<Self>,
     subscriptions: Vec<Subscription>,
     collapsed_sections: Vec<Section>,
+    collapsed_channels: Vec<ChannelId>,
     workspace: WeakViewHandle<Workspace>,
     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<Self> {
         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<Self>,
     ) -> AnyElement<Self> {
         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, _>(channel.id as usize, cx, |state, cx| {
-            Flex::row()
+            Flex::<Self>::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<Self>) {
+        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<Self>) {
         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<S> {
-        disclosure: ToggleIconButtonStyle,
-        spacing: f32,
-        content: S,
-    }
-
-    pub struct Disclosable<C, S> {
-        disclosed: bool,
-        action: Box<dyn Action>,
-        content: C,
-        style: S,
-    }
-
-    impl Disclosable<(), ()> {
-        fn new<C>(disclosed: bool, content: C, action: Box<dyn Action>) -> Disclosable<C, ()> {
-            Disclosable {
-                disclosed,
-                content,
-                action,
-                style: (),
-            }
-        }
-    }
-
-    impl<C: GeneralStyleableComponent> GeneralStyleableComponent for Disclosable<C, ()> {
-        type Style = DisclosureStyle<C::Style>;
-
-        type Output = Disclosable<C, Self::Style>;
-
-        fn with_style(self, style: Self::Style) -> Self::Output {
-            Disclosable {
-                disclosed: self.disclosed,
-                action: self.action,
-                content: self.content,
-                style,
-            }
-        }
-    }
-
-    impl<C: GeneralStyleableComponent> GeneralComponent for Disclosable<C, DisclosureStyle<C::Style>> {
-        fn render<V: gpui::View>(
-            self,
-            v: &mut V,
-            cx: &mut gpui::ViewContext<V>,
-        ) -> gpui::AnyElement<V> {
-            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<dyn Action>) -> Disclosable<Self, ()>
-        where
-            Self: Sized;
-    }
-
-    impl<C: GeneralStyleableComponent> DisclosureExt for C {
-        fn disclosable(self, disclosed: bool, action: Box<dyn Action>) -> Disclosable<Self, ()> {
-            Disclosable::new(disclosed, self, action)
-        }
-    }
-}

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<V: View> Component<V> for ToggleableButton<V> {
+    impl<V: View> StatefulComponent<V> for ToggleableButton<V> {
         fn render(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
             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<V: View> Component<V> for Button<V> {
+    impl<V: View> StatefulComponent<V> for Button<V> {
         fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
             let click_handler = self.click_handler;
 

crates/gpui/src/elements.rs 🔗

@@ -230,19 +230,26 @@ pub trait Element<V: View>: 'static {
         MouseEventHandler::for_child::<Tag>(self.into_any(), region_id)
     }
 
-    fn component(self) -> ElementAdapter<V>
+    fn stateful_component(self) -> ElementAdapter<V>
     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<DynamicElementAdapter>
+    where
+        Self: Sized,
+    {
+        DynamicElementAdapter::new(self.into_any()).stylable()
+    }
 }
 
 pub trait RenderElement {

crates/gpui/src/elements/component.rs 🔗

@@ -9,35 +9,35 @@ use crate::{
 
 use super::Empty;
 
-pub trait GeneralComponent {
+pub trait Component {
     fn render<V: View>(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
 
-    fn element<V: View>(self) -> ComponentAdapter<V, Self>
+    fn element<V: View>(self) -> StatefulAdapter<V, Self>
     where
         Self: Sized,
     {
-        ComponentAdapter::new(self)
+        StatefulAdapter::new(self)
     }
 
-    fn stylable(self) -> GeneralStylableComponentAdapter<Self>
+    fn stylable(self) -> StylableAdapter<Self>
     where
         Self: Sized,
     {
-        GeneralStylableComponentAdapter::new(self)
+        StylableAdapter::new(self)
     }
 }
 
-pub struct GeneralStylableComponentAdapter<C: GeneralComponent> {
+pub struct StylableAdapter<C: Component> {
     component: C,
 }
 
-impl<C: GeneralComponent> GeneralStylableComponentAdapter<C> {
+impl<C: Component> StylableAdapter<C> {
     pub fn new(component: C) -> Self {
         Self { component }
     }
 }
 
-impl<C: GeneralComponent> GeneralStyleableComponent for GeneralStylableComponentAdapter<C> {
+impl<C: Component> StyleableComponent for StylableAdapter<C> {
     type Style = ();
 
     type Output = C;
@@ -47,20 +47,20 @@ impl<C: GeneralComponent> 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<V: View>(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
         Empty::new().into_any()
     }
 }
 
-impl GeneralStyleableComponent for () {
+impl StyleableComponent for () {
     type Style = ();
     type Output = ();
 
@@ -69,54 +69,54 @@ impl GeneralStyleableComponent for () {
     }
 }
 
-pub trait StyleableComponent<V: View> {
+pub trait StatefulStyleableComponent<V: View> {
     type Style: Clone;
-    type Output: Component<V>;
+    type Output: StatefulComponent<V>;
 
-    fn c_with_style(self, style: Self::Style) -> Self::Output;
+    fn stateful_with_style(self, style: Self::Style) -> Self::Output;
 }
 
-impl<V: View, C: GeneralStyleableComponent> StyleableComponent<V> for C {
+impl<V: View, C: StyleableComponent> StatefulStyleableComponent<V> 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<V: View> {
+pub trait StatefulComponent<V: View> {
     fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
 
-    fn c_element(self) -> ComponentAdapter<V, Self>
+    fn stateful_element(self) -> StatefulAdapter<V, Self>
     where
         Self: Sized,
     {
-        ComponentAdapter::new(self)
+        StatefulAdapter::new(self)
     }
 
-    fn c_styleable(self) -> StylableComponentAdapter<Self, V>
+    fn stateful_styleable(self) -> StatefulStylableAdapter<Self, V>
     where
         Self: Sized,
     {
-        StylableComponentAdapter::new(self)
+        StatefulStylableAdapter::new(self)
     }
 }
 
-impl<V: View, C: GeneralComponent> Component<V> for C {
+impl<V: View, C: Component> StatefulComponent<V> for C {
     fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
         self.render(v, cx)
     }
 }
 
 // StylableComponent -> Component
-pub struct StylableComponentAdapter<C: Component<V>, V: View> {
+pub struct StatefulStylableAdapter<C: StatefulComponent<V>, V: View> {
     component: C,
     phantom: std::marker::PhantomData<V>,
 }
 
-impl<C: Component<V>, V: View> StylableComponentAdapter<C, V> {
+impl<C: StatefulComponent<V>, V: View> StatefulStylableAdapter<C, V> {
     pub fn new(component: C) -> Self {
         Self {
             component,
@@ -125,12 +125,14 @@ impl<C: Component<V>, V: View> StylableComponentAdapter<C, V> {
     }
 }
 
-impl<C: Component<V>, V: View> StyleableComponent<V> for StylableComponentAdapter<C, V> {
+impl<C: StatefulComponent<V>, V: View> StatefulStyleableComponent<V>
+    for StatefulStylableAdapter<C, V>
+{
     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<V: View>(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
         let element = self
             .element
@@ -174,20 +176,20 @@ impl<V: View> ElementAdapter<V> {
     }
 }
 
-impl<V: View> Component<V> for ElementAdapter<V> {
+impl<V: View> StatefulComponent<V> for ElementAdapter<V> {
     fn render(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
         self.element
     }
 }
 
 // Component -> Element
-pub struct ComponentAdapter<V: View, E> {
+pub struct StatefulAdapter<V: View, E> {
     component: Option<E>,
     element: Option<AnyElement<V>>,
     phantom: PhantomData<V>,
 }
 
-impl<E, V: View> ComponentAdapter<V, E> {
+impl<E, V: View> StatefulAdapter<V, E> {
     pub fn new(e: E) -> Self {
         Self {
             component: Some(e),
@@ -197,7 +199,7 @@ impl<E, V: View> ComponentAdapter<V, E> {
     }
 }
 
-impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
+impl<V: View, C: StatefulComponent<V> + 'static> Element<V> for StatefulAdapter<V, C> {
     type LayoutState = ();
 
     type PaintState = ();

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<V: View> {

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()
     }
 }

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<Interactive<ButtonStyle<SvgStyle>>>;
 
-pub trait ComponentExt<C: GeneralStyleableComponent> {
+pub trait ComponentExt<C: StyleableComponent> {
     fn toggleable(self, active: bool) -> Toggle<C, ()>;
+    fn disclosable(
+        self,
+        disclosed: Option<bool>,
+        action: Box<dyn Action>,
+        id: usize,
+    ) -> Disclosable<C, ()>;
 }
 
-impl<C: GeneralStyleableComponent> ComponentExt<C> for C {
+impl<C: StyleableComponent> ComponentExt<C> for C {
     fn toggleable(self, active: bool) -> Toggle<C, ()> {
         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<bool>,
+        action: Box<dyn Action>,
+        id: usize,
+    ) -> Disclosable<C, ()> {
+        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<S> {
+        pub button: ToggleIconButtonStyle,
+        pub spacing: f32,
+        #[serde(flatten)]
+        content: S,
+    }
+
+    impl<S> DisclosureStyle<S> {
+        pub fn button_space(&self) -> f32 {
+            self.spacing + self.button.button_width.unwrap()
+        }
+    }
+
+    pub struct Disclosable<C, S> {
+        disclosed: Option<bool>,
+        action: Box<dyn Action>,
+        id: usize,
+        content: C,
+        style: S,
+    }
+
+    impl Disclosable<(), ()> {
+        pub fn new<C>(
+            disclosed: Option<bool>,
+            content: C,
+            action: Box<dyn Action>,
+            id: usize,
+        ) -> Disclosable<C, ()> {
+            Disclosable {
+                disclosed,
+                content,
+                action,
+                id,
+                style: (),
+            }
+        }
+    }
+
+    impl<C: StyleableComponent> StyleableComponent for Disclosable<C, ()> {
+        type Style = DisclosureStyle<C::Style>;
+
+        type Output = Disclosable<C, Self::Style>;
+
+        fn with_style(self, style: Self::Style) -> Self::Output {
+            Disclosable {
+                disclosed: self.disclosed,
+                action: self.action,
+                content: self.content,
+                id: self.id,
+                style,
+            }
+        }
+    }
+
+    impl<C: StyleableComponent> Component for Disclosable<C, DisclosureStyle<C::Style>> {
+        fn render<V: gpui::View>(
+            self,
+            v: &mut V,
+            cx: &mut gpui::ViewContext<V>,
+        ) -> gpui::AnyElement<V> {
+            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<C: GeneralStyleableComponent> Toggle<C, ()> {
+    impl<C: StyleableComponent> Toggle<C, ()> {
         pub fn new(component: C, active: bool) -> Self {
             Toggle {
                 active,
@@ -37,7 +161,7 @@ pub mod toggle {
         }
     }
 
-    impl<C: GeneralStyleableComponent> GeneralStyleableComponent for Toggle<C, ()> {
+    impl<C: StyleableComponent> StyleableComponent for Toggle<C, ()> {
         type Style = Toggleable<C::Style>;
 
         type Output = Toggle<C, Self::Style>;
@@ -51,7 +175,7 @@ pub mod toggle {
         }
     }
 
-    impl<C: GeneralStyleableComponent> GeneralComponent for Toggle<C, Toggleable<C::Style>> {
+    impl<C: StyleableComponent> Component for Toggle<C, Toggleable<C::Style>> {
         fn render<V: gpui::View>(
             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<C> {
+        #[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<f32>,
+        pub button_height: Option<f32>,
+        #[serde(flatten)]
+        contents: C,
+    }
+
     pub struct ActionButton<C, S> {
         action: Box<dyn Action>,
         tooltip: Option<(Cow<'static, str>, TooltipStyle)>,
         tag: TypeTag,
+        id: usize,
         contents: C,
         style: Interactive<S>,
     }
 
-    #[derive(Clone, Deserialize, Default, JsonSchema)]
-    pub struct ButtonStyle<C> {
-        #[serde(flatten)]
-        container: ContainerStyle,
-        button_width: Option<f32>,
-        button_height: Option<f32>,
-        #[serde(flatten)]
-        contents: C,
-    }
-
     impl ActionButton<(), ()> {
         pub fn new_dynamic(action: Box<dyn Action>) -> 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<C: GeneralStyleableComponent>(
-            self,
-            contents: C,
-        ) -> ActionButton<C, ()> {
+        pub fn with_id(mut self, id: usize) -> Self {
+            self.id = id;
+            self
+        }
+
+        pub fn with_contents<C: StyleableComponent>(self, contents: C) -> ActionButton<C, ()> {
             ActionButton {
                 action: self.action,
                 tag: self.tag,
                 style: self.style,
                 tooltip: self.tooltip,
+                id: self.id,
                 contents,
             }
         }
     }
 
-    impl<C: GeneralStyleableComponent> GeneralStyleableComponent for ActionButton<C, ()> {
+    impl<C: StyleableComponent> StyleableComponent for ActionButton<C, ()> {
         type Style = Interactive<ButtonStyle<C::Style>>;
         type Output = ActionButton<C, ButtonStyle<C::Style>>;
 
@@ -146,15 +277,15 @@ pub mod action_button {
                 tag: self.tag,
                 contents: self.contents,
                 tooltip: self.tooltip,
-
+                id: self.id,
                 style,
             }
         }
     }
 
-    impl<C: GeneralStyleableComponent> GeneralComponent for ActionButton<C, ButtonStyle<C::Style>> {
+    impl<C: StyleableComponent> Component for ActionButton<C, ButtonStyle<C::Style>> {
         fn render<V: View>(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
-            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<S> {
-        path: Cow<'static, str>,
+        path: Option<Cow<'static, str>>,
         style: S,
     }
 
     impl Svg<()> {
         pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
             Self {
-                path: path.into(),
+                path: Some(path.into()),
+                style: (),
+            }
+        }
+
+        pub fn optional(path: Option<impl Into<Cow<'static, str>>>) -> Self {
+            Self {
+                path: path.map(Into::into),
                 style: (),
             }
         }
     }
 
-    impl GeneralStyleableComponent for Svg<()> {
+    impl StyleableComponent for Svg<()> {
         type Style = SvgStyle;
 
         type Output = Svg<SvgStyle>;
@@ -278,18 +430,23 @@ pub mod svg {
         }
     }
 
-    impl GeneralComponent for Svg<SvgStyle> {
+    impl Component for Svg<SvgStyle> {
         fn render<V: gpui::View>(
             self,
             _: &mut V,
             _: &mut gpui::ViewContext<V>,
         ) -> gpui::AnyElement<V> {
-            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<LabelStyle>;
@@ -329,7 +486,7 @@ pub mod label {
         }
     }
 
-    impl GeneralComponent for Label<LabelStyle> {
+    impl Component for Label<LabelStyle> {
         fn render<V: gpui::View>(
             self,
             _: &mut V,

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<Interactive<ContainedText>>,
     pub list_empty_icon: Icon,
     pub list_empty_label_container: ContainerStyle,
@@ -890,6 +891,14 @@ pub struct Interactive<T> {
     pub disabled: Option<T>,
 }
 
+impl<T> Deref for Interactive<T> {
+    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<T> {
     inactive: T,
 }
 
+impl<T> Deref for Toggleable<T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        &self.inactive
+    }
+}
+
 impl Toggleable<()> {
     pub fn new_blank() -> Self {
         Self {

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),