Tune styles and disclosable elements

Mikayla created

Change summary

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

Detailed changes

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<Client>, 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<Self>) {
+    fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
         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<Self>) {
+    fn collapse_selected_channel(
+        &mut self,
+        _: &CollapseSelectedChannel,
+        cx: &mut ViewContext<Self>,
+    ) {
+        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<Self>) {
+        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<Self>) {
         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>) {
+        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,

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<Interactive<ButtonStyle<SvgStyle>>>;
+pub type IconButtonStyle = Interactive<ButtonStyle<SvgStyle>>;
+pub type ToggleIconButtonStyle = Toggleable<IconButtonStyle>;
 
 pub trait ComponentExt<C: SafeStylable> {
     fn toggleable(self, active: bool) -> Toggle<C, ()>;
@@ -27,17 +28,19 @@ impl<C: SafeStylable> ComponentExt<C> 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<S> {
-        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<C: SafeStylable> Component for Disclosable<C, DisclosureStyle<C::Style>> {
         fn render<V: gpui::View>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
             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()
         }
     }

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

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

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