Style collab panel (#3638)

Marshall Bowers , Nate Butler , and Marshall Bowers created

This PR styles the collab panel.

Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>

Change summary

crates/collab_ui2/src/collab_panel.rs       | 269 +++++++++++-----------
crates/ui2/src/components/disclosure.rs     |   3 
crates/ui2/src/components/list/list_item.rs |  16 
script/storybook                            |  15 +
4 files changed, 167 insertions(+), 136 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -176,11 +176,11 @@ use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
     actions, canvas, div, img, impl_actions, overlay, point, prelude::*, px, rems, serde_json,
-    size, Action, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div,
-    EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement,
-    Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render,
-    RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View,
-    ViewContext, VisualContext, WeakView,
+    size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent,
+    Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement,
+    IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad,
+    Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task,
+    View, ViewContext, VisualContext, WeakView,
 };
 use project::{Fs, Project};
 use serde_derive::{Deserialize, Serialize};
@@ -402,7 +402,7 @@ impl CollabPanel {
 
             let filter_editor = cx.build_view(|cx| {
                 let mut editor = Editor::single_line(cx);
-                editor.set_placeholder_text("Filter channels, contacts", cx);
+                editor.set_placeholder_text("Filter...", cx);
                 editor
             });
 
@@ -1157,24 +1157,20 @@ impl CollabPanel {
 
         ListItem::new(SharedString::from(user.github_login.clone()))
             .start_slot(Avatar::new(user.avatar_uri.clone()))
-            .child(
-                h_stack()
-                    .w_full()
-                    .justify_between()
-                    .child(Label::new(user.github_login.clone()))
-                    .child(if is_pending {
-                        Label::new("Calling").color(Color::Muted).into_any_element()
-                    } else if is_current_user {
-                        IconButton::new("leave-call", Icon::ArrowRight)
-                            .on_click(cx.listener(move |this, _, cx| {
-                                Self::leave_call(cx);
-                            }))
-                            .tooltip(|cx| Tooltip::text("Leave Call", cx))
-                            .into_any_element()
-                    } else {
-                        div().into_any_element()
-                    }),
-            )
+            .child(Label::new(user.github_login.clone()))
+            .end_slot(if is_pending {
+                Label::new("Calling").color(Color::Muted).into_any_element()
+            } else if is_current_user {
+                IconButton::new("leave-call", Icon::Exit)
+                    .style(ButtonStyle::Subtle)
+                    .on_click(cx.listener(move |this, _, cx| {
+                        Self::leave_call(cx);
+                    }))
+                    .tooltip(|cx| Tooltip::text("Leave Call", cx))
+                    .into_any_element()
+            } else {
+                div().into_any_element()
+            })
             .when_some(peer_id, |this, peer_id| {
                 this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
                     .on_click(cx.listener(move |this, _, cx| {
@@ -1212,8 +1208,12 @@ impl CollabPanel {
                         .detach_and_log_err(cx);
                 });
             }))
-            .start_slot(render_tree_branch(is_last, cx))
-            .child(IconButton::new(0, Icon::Folder))
+            .start_slot(
+                h_stack()
+                    .gap_1()
+                    .child(render_tree_branch(is_last, cx))
+                    .child(IconButton::new(0, Icon::Folder)),
+            )
             .child(Label::new(project_name.clone()))
             .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
 
@@ -1305,8 +1305,12 @@ impl CollabPanel {
         let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
 
         ListItem::new(("screen", id))
-            .start_slot(render_tree_branch(is_last, cx))
-            .child(IconButton::new(0, Icon::Screen))
+            .start_slot(
+                h_stack()
+                    .gap_1()
+                    .child(render_tree_branch(is_last, cx))
+                    .child(IconButton::new(0, Icon::Screen)),
+            )
             .child(Label::new("Screen"))
             .when_some(peer_id, |this, _| {
                 this.on_click(cx.listener(move |this, _, cx| {
@@ -1372,9 +1376,13 @@ impl CollabPanel {
             .on_click(cx.listener(move |this, _, cx| {
                 this.open_channel_notes(channel_id, cx);
             }))
-            .start_slot(render_tree_branch(false, cx))
-            .child(IconButton::new(0, Icon::File))
-            .child(Label::new("notes"))
+            .start_slot(
+                h_stack()
+                    .gap_1()
+                    .child(render_tree_branch(false, cx))
+                    .child(IconButton::new(0, Icon::File)),
+            )
+            .child(div().h_7().w_full().child(Label::new("notes")))
             .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
     }
 
@@ -1387,8 +1395,12 @@ impl CollabPanel {
             .on_click(cx.listener(move |this, _, cx| {
                 this.join_channel_chat(channel_id, cx);
             }))
-            .start_slot(render_tree_branch(true, cx))
-            .child(IconButton::new(0, Icon::MessageBubbles))
+            .start_slot(
+                h_stack()
+                    .gap_1()
+                    .child(render_tree_branch(false, cx))
+                    .child(IconButton::new(0, Icon::MessageBubbles)),
+            )
             .child(Label::new("chat"))
             .tooltip(move |cx| Tooltip::text("Open Chat", cx))
     }
@@ -2149,11 +2161,6 @@ impl CollabPanel {
     fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
         v_stack()
             .size_full()
-            .child(
-                div()
-                    .p_2()
-                    .child(div().rounded(px(2.0)).child(self.filter_editor.clone())),
-            )
             .child(
                 v_stack()
                     .size_full()
@@ -2223,6 +2230,14 @@ impl CollabPanel {
                         }
                     })),
             )
+            .child(
+                div().p_2().child(
+                    div()
+                        .border_primary(cx)
+                        .border_t()
+                        .child(self.filter_editor.clone()),
+                ),
+            )
     }
 
     fn render_header(
@@ -2274,22 +2289,32 @@ impl CollabPanel {
         let button = match section {
             Section::ActiveCall => channel_link.map(|channel_link| {
                 let channel_link_copy = channel_link.clone();
-                IconButton::new("channel-link", Icon::Copy)
-                    .on_click(move |_, cx| {
-                        let item = ClipboardItem::new(channel_link_copy.clone());
-                        cx.write_to_clipboard(item)
-                    })
-                    .tooltip(|cx| Tooltip::text("Copy channel link", cx))
+                div()
+                    .invisible()
+                    .group_hover("section-header", |this| this.visible())
+                    .child(
+                        IconButton::new("channel-link", Icon::Copy)
+                            .icon_size(IconSize::Small)
+                            .size(ButtonSize::None)
+                            .on_click(move |_, cx| {
+                                let item = ClipboardItem::new(channel_link_copy.clone());
+                                cx.write_to_clipboard(item)
+                            })
+                            .tooltip(|cx| Tooltip::text("Copy channel link", cx)),
+                    )
+                    .into_any_element()
             }),
             Section::Contacts => Some(
                 IconButton::new("add-contact", Icon::Plus)
                     .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
-                    .tooltip(|cx| Tooltip::text("Search for new contact", cx)),
+                    .tooltip(|cx| Tooltip::text("Search for new contact", cx))
+                    .into_any_element(),
             ),
             Section::Channels => Some(
                 IconButton::new("add-channel", Icon::Plus)
                     .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
-                    .tooltip(|cx| Tooltip::text("Create a channel", cx)),
+                    .tooltip(|cx| Tooltip::text("Create a channel", cx))
+                    .into_any_element(),
             ),
             _ => None,
         };
@@ -2304,25 +2329,18 @@ impl CollabPanel {
 
         h_stack()
             .w_full()
-            .map(|el| {
-                if can_collapse {
-                    el.child(
-                        ListItem::new(text.clone())
-                            .child(div().w_full().child(Label::new(text)))
-                            .selected(is_selected)
-                            .toggle(Some(!is_collapsed))
-                            .on_click(cx.listener(move |this, _, cx| {
-                                this.toggle_section_expanded(section, cx)
-                            })),
-                    )
-                } else {
-                    el.child(
-                        ListHeader::new(text)
-                            .when_some(button, |el, button| el.end_slot(button))
-                            .selected(is_selected),
-                    )
-                }
-            })
+            .group("section-header")
+            .child(
+                ListHeader::new(text)
+                    .toggle(if can_collapse {
+                        Some(!is_collapsed)
+                    } else {
+                        None
+                    })
+                    .inset(true)
+                    .end_slot::<AnyElement>(button)
+                    .selected(is_selected),
+            )
             .when(section == Section::Channels, |el| {
                 el.drag_over::<DraggedChannelView>(|style| {
                     style.bg(cx.theme().colors().ghost_element_hover)
@@ -2460,7 +2478,7 @@ impl CollabPanel {
                     .child(Label::new(github_login.clone()))
                     .child(h_stack().children(controls)),
             )
-            .start_slot::<Avatar>(user.avatar_uri.clone().map(|avatar| Avatar::new(avatar)))
+            .start_slot(Avatar::new(user.avatar_uri.clone()))
     }
 
     fn render_contact_placeholder(
@@ -2541,6 +2559,8 @@ impl CollabPanel {
         div()
             .id(channel_id as usize)
             .group("")
+            .flex()
+            .w_full()
             .on_drag({
                 let channel = channel.clone();
                 move |cx| {
@@ -2566,71 +2586,10 @@ impl CollabPanel {
             )
             .child(
                 ListItem::new(channel_id as usize)
-                    .indent_level(depth)
+                    // Offset the indent depth by one to give us room to show the disclosure.
+                    .indent_level(depth + 1)
                     .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to  step over the disclosure toggle
-                    .start_slot(
-                        IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
-                            .size(IconSize::Small)
-                            .color(Color::Muted),
-                    )
                     .selected(is_selected || is_active)
-                    .child(
-                        h_stack()
-                            .w_full()
-                            .justify_between()
-                            .child(
-                                h_stack()
-                                    .id(channel_id as usize)
-                                    .child(Label::new(channel.name.clone()))
-                                    .children(face_pile.map(|face_pile| face_pile.render(cx))),
-                            )
-                            .child(
-                                h_stack()
-                                    .child(
-                                        div()
-                                            .id("channel_chat")
-                                            .when(!has_messages_notification, |el| el.invisible())
-                                            .group_hover("", |style| style.visible())
-                                            .child(
-                                                IconButton::new(
-                                                    "channel_chat",
-                                                    Icon::MessageBubbles,
-                                                )
-                                                .icon_color(if has_messages_notification {
-                                                    Color::Default
-                                                } else {
-                                                    Color::Muted
-                                                })
-                                                .on_click(cx.listener(move |this, _, cx| {
-                                                    this.join_channel_chat(channel_id, cx)
-                                                }))
-                                                .tooltip(|cx| {
-                                                    Tooltip::text("Open channel chat", cx)
-                                                }),
-                                            ),
-                                    )
-                                    .child(
-                                        div()
-                                            .id("channel_notes")
-                                            .when(!has_notes_notification, |el| el.invisible())
-                                            .group_hover("", |style| style.visible())
-                                            .child(
-                                                IconButton::new("channel_notes", Icon::File)
-                                                    .icon_color(if has_notes_notification {
-                                                        Color::Default
-                                                    } else {
-                                                        Color::Muted
-                                                    })
-                                                    .on_click(cx.listener(move |this, _, cx| {
-                                                        this.open_channel_notes(channel_id, cx)
-                                                    }))
-                                                    .tooltip(|cx| {
-                                                        Tooltip::text("Open channel notes", cx)
-                                                    }),
-                                            ),
-                                    ),
-                            ),
-                    )
                     .toggle(disclosed)
                     .on_toggle(
                         cx.listener(move |this, _, cx| {
@@ -2650,7 +2609,57 @@ impl CollabPanel {
                         move |this, event: &MouseDownEvent, cx| {
                             this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
                         },
-                    )),
+                    ))
+                    .start_slot(
+                        IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
+                            .size(IconSize::Small)
+                            .color(Color::Muted),
+                    )
+                    .child(
+                        h_stack()
+                            .id(channel_id as usize)
+                            .child(Label::new(channel.name.clone()))
+                            .children(face_pile.map(|face_pile| face_pile.render(cx))),
+                    )
+                    .end_slot(
+                        h_stack()
+                            .child(
+                                div()
+                                    .id("channel_chat")
+                                    .when(!has_messages_notification, |el| el.invisible())
+                                    .group_hover("", |style| style.visible())
+                                    .child(
+                                        IconButton::new("channel_chat", Icon::MessageBubbles)
+                                            .icon_color(if has_messages_notification {
+                                                Color::Default
+                                            } else {
+                                                Color::Muted
+                                            })
+                                            .on_click(cx.listener(move |this, _, cx| {
+                                                this.join_channel_chat(channel_id, cx)
+                                            }))
+                                            .tooltip(|cx| Tooltip::text("Open channel chat", cx)),
+                                    ),
+                            )
+                            .child(
+                                div()
+                                    .id("channel_notes")
+                                    .when(!has_notes_notification, |el| el.invisible())
+                                    .group_hover("", |style| style.visible())
+                                    .child(
+                                        IconButton::new("channel_notes", Icon::File)
+                                            .icon_color(if has_notes_notification {
+                                                Color::Default
+                                            } else {
+                                                Color::Muted
+                                            })
+                                            .on_click(cx.listener(move |this, _, cx| {
+                                                this.open_channel_notes(channel_id, cx)
+                                            }))
+                                            .tooltip(|cx| Tooltip::text("Open channel notes", cx)),
+                                    ),
+                            ),
+                    ),
             )
             .tooltip(|cx| Tooltip::text("Join channel", cx))
 

crates/ui2/src/components/disclosure.rs 🔗

@@ -1,6 +1,7 @@
-use crate::{prelude::*, Color, Icon, IconButton, IconSize};
 use gpui::ClickEvent;
 
+use crate::{prelude::*, Color, Icon, IconButton, IconSize};
+
 #[derive(IntoElement)]
 pub struct Disclosure {
     is_open: bool,

crates/ui2/src/components/list/list_item.rs 🔗

@@ -1,9 +1,10 @@
-use crate::{prelude::*, Disclosure};
 use gpui::{
     px, AnyElement, AnyView, ClickEvent, Div, MouseButton, MouseDownEvent, Pixels, Stateful,
 };
 use smallvec::SmallVec;
 
+use crate::{prelude::*, Disclosure};
+
 #[derive(IntoElement)]
 pub struct ListItem {
     id: ElementId,
@@ -192,10 +193,15 @@ impl RenderOnce for ListItem {
                             this.ml(self.indent_level as f32 * self.indent_step_size)
                         }
                     })
-                    .children(
-                        self.toggle
-                            .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
-                    )
+                    .children(self.toggle.map(|is_open| {
+                        div()
+                            .flex()
+                            .absolute()
+                            .left(rems(-1.))
+                            .invisible()
+                            .group_hover("", |style| style.visible())
+                            .child(Disclosure::new(is_open).on_toggle(self.on_toggle))
+                    }))
                     .child(
                         h_stack()
                             .flex_1()

script/storybook 🔗

@@ -0,0 +1,15 @@
+#!/bin/bash
+
+# This script takes a single text input and replaces 'list_item' with the input in a cargo run command
+
+# Check if an argument is provided
+if [ "$#" -ne 1 ]; then
+  echo "Usage: $0 <component_name>"
+  exit 1
+fi
+
+# Assign the argument to a variable
+COMPONENT_NAME="$1"
+
+# Run the cargo command with the provided component name
+cargo run -p storybook2 -- components/"$COMPONENT_NAME"