Add settings for removing the assistant and collaboration panel buttons

Mikayla and max created

Add a not-logged-in state to the collaboration panel

co-authored-by: max <max@zed.dev>

Change summary

assets/settings/default.json                        |  10 
crates/ai/src/assistant.rs                          |   7 
crates/ai/src/assistant_settings.rs                 |   2 
crates/collab_ui/src/collab_panel.rs                |  54 +++
crates/collab_ui/src/collab_panel/panel_settings.rs |  18 
crates/project_panel/src/project_panel.rs           |   4 
crates/terminal_view/src/terminal_panel.rs          |   4 
crates/theme/src/theme.rs                           |   1 
crates/workspace/src/dock.rs                        | 181 +++++++-------
styles/src/style_tree/collab_panel.ts               |  31 ++
10 files changed, 196 insertions(+), 116 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -122,13 +122,17 @@
     // Amount of indentation for nested items.
     "indent_size": 20
   },
-  "channels_panel": {
+  "collaboration_panel": {
+    // Whether to show the collaboration panel button in the status bar.
+    "button": true,
     // Where to dock channels panel. Can be 'left' or 'right'.
     "dock": "left",
     // Default width of the channels panel.
     "default_width": 240
   },
   "assistant": {
+    // Whether to show the assistant panel button in the status bar.
+    "button": true,
     // Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
     "dock": "right",
     // Default width when the assistant is docked to the left or right.
@@ -220,7 +224,9 @@
   "copilot": {
     // The set of glob patterns for which copilot should be disabled
     // in any matching file.
-    "disabled_globs": [".env"]
+    "disabled_globs": [
+      ".env"
+    ]
   },
   // Settings specific to journaling
   "journal": {

crates/ai/src/assistant.rs 🔗

@@ -192,6 +192,7 @@ impl AssistantPanel {
                                 old_dock_position = new_dock_position;
                                 cx.emit(AssistantPanelEvent::DockPositionChanged);
                             }
+                            cx.notify();
                         })];
 
                     this
@@ -790,8 +791,10 @@ impl Panel for AssistantPanel {
         }
     }
 
-    fn icon_path(&self) -> &'static str {
-        "icons/robot_14.svg"
+    fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
+        settings::get::<AssistantSettings>(cx)
+            .button
+            .then(|| "icons/robot_14.svg")
     }
 
     fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {

crates/ai/src/assistant_settings.rs 🔗

@@ -13,6 +13,7 @@ pub enum AssistantDockPosition {
 
 #[derive(Deserialize, Debug)]
 pub struct AssistantSettings {
+    pub button: bool,
     pub dock: AssistantDockPosition,
     pub default_width: f32,
     pub default_height: f32,
@@ -20,6 +21,7 @@ pub struct AssistantSettings {
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct AssistantSettingsContent {
+    pub button: Option<bool>,
     pub dock: Option<AssistantDockPosition>,
     pub default_width: Option<f32>,
     pub default_height: Option<f32>,

crates/collab_ui/src/collab_panel.rs 🔗

@@ -27,7 +27,7 @@ use gpui::{
     Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
-use panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings};
+use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings};
 use project::{Fs, Project};
 use serde_derive::{Deserialize, Serialize};
 use settings::SettingsStore;
@@ -65,7 +65,7 @@ impl_actions!(collab_panel, [RemoveChannel, NewChannel, AddMember]);
 const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel";
 
 pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
-    settings::register::<panel_settings::ChannelsPanelSettings>(cx);
+    settings::register::<panel_settings::CollaborationPanelSettings>(cx);
     contact_finder::init(cx);
     channel_modal::init(cx);
 
@@ -95,6 +95,7 @@ pub struct CollabPanel {
     entries: Vec<ListEntry>,
     selection: Option<usize>,
     user_store: ModelHandle<UserStore>,
+    client: Arc<Client>,
     channel_store: ModelHandle<ChannelStore>,
     project: ModelHandle<Project>,
     match_candidates: Vec<StringMatchCandidate>,
@@ -320,6 +321,7 @@ impl CollabPanel {
                 match_candidates: Vec::default(),
                 collapsed_sections: Vec::default(),
                 workspace: workspace.weak_handle(),
+                client: workspace.app_state().client.clone(),
                 list_state,
             };
             this.update_entries(cx);
@@ -334,6 +336,7 @@ impl CollabPanel {
                             old_dock_position = new_dock_position;
                             cx.emit(Event::DockPositionChanged);
                         }
+                        cx.notify();
                     }),
                 );
 
@@ -1862,6 +1865,31 @@ impl View for CollabPanel {
     fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
         let theme = &theme::current(cx).collab_panel;
 
+        if self.user_store.read(cx).current_user().is_none() {
+            enum LogInButton {}
+
+            return Flex::column()
+                .with_child(
+                    MouseEventHandler::<LogInButton, _>::new(0, cx, |state, _| {
+                        let button = theme.log_in_button.style_for(state);
+                        Label::new("Sign in to collaborate", button.text.clone())
+                            .contained()
+                            .with_style(button.container)
+                    })
+                    .on_click(MouseButton::Left, |_, this, cx| {
+                        let client = this.client.clone();
+                        cx.spawn(|_, cx| async move {
+                            client.authenticate_and_connect(true, &cx).await.log_err()
+                        })
+                        .detach();
+                    })
+                    .with_cursor_style(CursorStyle::PointingHand),
+                )
+                .contained()
+                .with_style(theme.container)
+                .into_any();
+        }
+
         enum PanelFocus {}
         MouseEventHandler::<PanelFocus, _>::new(0, cx, |_, cx| {
             Stack::new()
@@ -1901,9 +1929,9 @@ impl View for CollabPanel {
 
 impl Panel for CollabPanel {
     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-        match settings::get::<ChannelsPanelSettings>(cx).dock {
-            ChannelsPanelDockPosition::Left => DockPosition::Left,
-            ChannelsPanelDockPosition::Right => DockPosition::Right,
+        match settings::get::<CollaborationPanelSettings>(cx).dock {
+            CollaborationPanelDockPosition::Left => DockPosition::Left,
+            CollaborationPanelDockPosition::Right => DockPosition::Right,
         }
     }
 
@@ -1912,13 +1940,15 @@ impl Panel for CollabPanel {
     }
 
     fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
-        settings::update_settings_file::<ChannelsPanelSettings>(
+        settings::update_settings_file::<CollaborationPanelSettings>(
             self.fs.clone(),
             cx,
             move |settings| {
                 let dock = match position {
-                    DockPosition::Left | DockPosition::Bottom => ChannelsPanelDockPosition::Left,
-                    DockPosition::Right => ChannelsPanelDockPosition::Right,
+                    DockPosition::Left | DockPosition::Bottom => {
+                        CollaborationPanelDockPosition::Left
+                    }
+                    DockPosition::Right => CollaborationPanelDockPosition::Right,
                 };
                 settings.dock = Some(dock);
             },
@@ -1927,7 +1957,7 @@ impl Panel for CollabPanel {
 
     fn size(&self, cx: &gpui::WindowContext) -> f32 {
         self.width
-            .unwrap_or_else(|| settings::get::<ChannelsPanelSettings>(cx).default_width)
+            .unwrap_or_else(|| settings::get::<CollaborationPanelSettings>(cx).default_width)
     }
 
     fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
@@ -1936,8 +1966,10 @@ impl Panel for CollabPanel {
         cx.notify();
     }
 
-    fn icon_path(&self) -> &'static str {
-        "icons/radix/person.svg"
+    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
+        settings::get::<CollaborationPanelSettings>(cx)
+            .button
+            .then(|| "icons/radix/person.svg")
     }
 
     fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {

crates/collab_ui/src/collab_panel/panel_settings.rs 🔗

@@ -5,27 +5,29 @@ use settings::Setting;
 
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
-pub enum ChannelsPanelDockPosition {
+pub enum CollaborationPanelDockPosition {
     Left,
     Right,
 }
 
 #[derive(Deserialize, Debug)]
-pub struct ChannelsPanelSettings {
-    pub dock: ChannelsPanelDockPosition,
+pub struct CollaborationPanelSettings {
+    pub button: bool,
+    pub dock: CollaborationPanelDockPosition,
     pub default_width: f32,
 }
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-pub struct ChannelsPanelSettingsContent {
-    pub dock: Option<ChannelsPanelDockPosition>,
+pub struct CollaborationPanelSettingsContent {
+    pub button: Option<bool>,
+    pub dock: Option<CollaborationPanelDockPosition>,
     pub default_width: Option<f32>,
 }
 
-impl Setting for ChannelsPanelSettings {
-    const KEY: Option<&'static str> = Some("channels_panel");
+impl Setting for CollaborationPanelSettings {
+    const KEY: Option<&'static str> = Some("collaboration_panel");
 
-    type FileContent = ChannelsPanelSettingsContent;
+    type FileContent = CollaborationPanelSettingsContent;
 
     fn load(
         default_value: &Self::FileContent,

crates/project_panel/src/project_panel.rs 🔗

@@ -1657,8 +1657,8 @@ impl workspace::dock::Panel for ProjectPanel {
         cx.notify();
     }
 
-    fn icon_path(&self) -> &'static str {
-        "icons/folder_tree_16.svg"
+    fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
+        Some("icons/folder_tree_16.svg")
     }
 
     fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -396,8 +396,8 @@ impl Panel for TerminalPanel {
         }
     }
 
-    fn icon_path(&self) -> &'static str {
-        "icons/terminal_12.svg"
+    fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
+        Some("icons/terminal_12.svg")
     }
 
     fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {

crates/theme/src/theme.rs 🔗

@@ -220,6 +220,7 @@ pub struct CopilotAuthAuthorized {
 pub struct CollabPanel {
     #[serde(flatten)]
     pub container: ContainerStyle,
+    pub log_in_button: Interactive<ContainedText>,
     pub channel_hash: Icon,
     pub channel_modal: ChannelModal,
     pub user_query_editor: FieldEditor,

crates/workspace/src/dock.rs 🔗

@@ -14,7 +14,7 @@ pub trait Panel: View {
     fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
     fn size(&self, cx: &WindowContext) -> f32;
     fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>);
-    fn icon_path(&self) -> &'static str;
+    fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
     fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>);
     fn icon_label(&self, _: &WindowContext) -> Option<String> {
         None
@@ -51,7 +51,7 @@ pub trait PanelHandle {
     fn set_active(&self, active: bool, cx: &mut WindowContext);
     fn size(&self, cx: &WindowContext) -> f32;
     fn set_size(&self, size: f32, cx: &mut WindowContext);
-    fn icon_path(&self, cx: &WindowContext) -> &'static str;
+    fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
     fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
     fn icon_label(&self, cx: &WindowContext) -> Option<String>;
     fn has_focus(&self, cx: &WindowContext) -> bool;
@@ -98,8 +98,8 @@ where
         self.update(cx, |this, cx| this.set_active(active, cx))
     }
 
-    fn icon_path(&self, cx: &WindowContext) -> &'static str {
-        self.read(cx).icon_path()
+    fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
+        self.read(cx).icon_path(cx)
     }
 
     fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>) {
@@ -490,8 +490,9 @@ impl View for PanelButtons {
             .map(|item| (item.panel.clone(), item.context_menu.clone()))
             .collect::<Vec<_>>();
         Flex::row()
-            .with_children(panels.into_iter().enumerate().map(
+            .with_children(panels.into_iter().enumerate().filter_map(
                 |(panel_ix, (view, context_menu))| {
+                    let icon_path = view.icon_path(cx)?;
                     let is_active = is_open && panel_ix == active_ix;
                     let (tooltip, tooltip_action) = if is_active {
                         (
@@ -505,94 +506,96 @@ impl View for PanelButtons {
                     } else {
                         view.icon_tooltip(cx)
                     };
-                    Stack::new()
-                        .with_child(
-                            MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
-                                let style = button_style.in_state(is_active);
-
-                                let style = style.style_for(state);
-                                Flex::row()
-                                    .with_child(
-                                        Svg::new(view.icon_path(cx))
-                                            .with_color(style.icon_color)
-                                            .constrained()
-                                            .with_width(style.icon_size)
-                                            .aligned(),
-                                    )
-                                    .with_children(if let Some(label) = view.icon_label(cx) {
-                                        Some(
-                                            Label::new(label, style.label.text.clone())
-                                                .contained()
-                                                .with_style(style.label.container)
+                    Some(
+                        Stack::new()
+                            .with_child(
+                                MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
+                                    let style = button_style.in_state(is_active);
+
+                                    let style = style.style_for(state);
+                                    Flex::row()
+                                        .with_child(
+                                            Svg::new(icon_path)
+                                                .with_color(style.icon_color)
+                                                .constrained()
+                                                .with_width(style.icon_size)
                                                 .aligned(),
                                         )
-                                    } else {
-                                        None
-                                    })
-                                    .constrained()
-                                    .with_height(style.icon_size)
-                                    .contained()
-                                    .with_style(style.container)
-                            })
-                            .with_cursor_style(CursorStyle::PointingHand)
-                            .on_click(MouseButton::Left, {
-                                let tooltip_action =
-                                    tooltip_action.as_ref().map(|action| action.boxed_clone());
-                                move |_, this, cx| {
-                                    if let Some(tooltip_action) = &tooltip_action {
-                                        let window_id = cx.window_id();
-                                        let view_id = this.workspace.id();
-                                        let tooltip_action = tooltip_action.boxed_clone();
-                                        cx.spawn(|_, mut cx| async move {
-                                            cx.dispatch_action(
-                                                window_id,
-                                                view_id,
-                                                &*tooltip_action,
+                                        .with_children(if let Some(label) = view.icon_label(cx) {
+                                            Some(
+                                                Label::new(label, style.label.text.clone())
+                                                    .contained()
+                                                    .with_style(style.label.container)
+                                                    .aligned(),
                                             )
-                                            .ok();
+                                        } else {
+                                            None
                                         })
-                                        .detach();
-                                    }
-                                }
-                            })
-                            .on_click(MouseButton::Right, {
-                                let view = view.clone();
-                                let menu = context_menu.clone();
-                                move |_, _, cx| {
-                                    const POSITIONS: [DockPosition; 3] = [
-                                        DockPosition::Left,
-                                        DockPosition::Right,
-                                        DockPosition::Bottom,
-                                    ];
-
-                                    menu.update(cx, |menu, cx| {
-                                        let items = POSITIONS
-                                            .into_iter()
-                                            .filter(|position| {
-                                                *position != dock_position
-                                                    && view.position_is_valid(*position, cx)
-                                            })
-                                            .map(|position| {
-                                                let view = view.clone();
-                                                ContextMenuItem::handler(
-                                                    format!("Dock {}", position.to_label()),
-                                                    move |cx| view.set_position(position, cx),
+                                        .constrained()
+                                        .with_height(style.icon_size)
+                                        .contained()
+                                        .with_style(style.container)
+                                })
+                                .with_cursor_style(CursorStyle::PointingHand)
+                                .on_click(MouseButton::Left, {
+                                    let tooltip_action =
+                                        tooltip_action.as_ref().map(|action| action.boxed_clone());
+                                    move |_, this, cx| {
+                                        if let Some(tooltip_action) = &tooltip_action {
+                                            let window_id = cx.window_id();
+                                            let view_id = this.workspace.id();
+                                            let tooltip_action = tooltip_action.boxed_clone();
+                                            cx.spawn(|_, mut cx| async move {
+                                                cx.dispatch_action(
+                                                    window_id,
+                                                    view_id,
+                                                    &*tooltip_action,
                                                 )
+                                                .ok();
                                             })
-                                            .collect();
-                                        menu.show(Default::default(), menu_corner, items, cx);
-                                    })
-                                }
-                            })
-                            .with_tooltip::<Self>(
-                                panel_ix,
-                                tooltip,
-                                tooltip_action,
-                                tooltip_style.clone(),
-                                cx,
-                            ),
-                        )
-                        .with_child(ChildView::new(&context_menu, cx))
+                                            .detach();
+                                        }
+                                    }
+                                })
+                                .on_click(MouseButton::Right, {
+                                    let view = view.clone();
+                                    let menu = context_menu.clone();
+                                    move |_, _, cx| {
+                                        const POSITIONS: [DockPosition; 3] = [
+                                            DockPosition::Left,
+                                            DockPosition::Right,
+                                            DockPosition::Bottom,
+                                        ];
+
+                                        menu.update(cx, |menu, cx| {
+                                            let items = POSITIONS
+                                                .into_iter()
+                                                .filter(|position| {
+                                                    *position != dock_position
+                                                        && view.position_is_valid(*position, cx)
+                                                })
+                                                .map(|position| {
+                                                    let view = view.clone();
+                                                    ContextMenuItem::handler(
+                                                        format!("Dock {}", position.to_label()),
+                                                        move |cx| view.set_position(position, cx),
+                                                    )
+                                                })
+                                                .collect();
+                                            menu.show(Default::default(), menu_corner, items, cx);
+                                        })
+                                    }
+                                })
+                                .with_tooltip::<Self>(
+                                    panel_ix,
+                                    tooltip,
+                                    tooltip_action,
+                                    tooltip_style.clone(),
+                                    cx,
+                                ),
+                            )
+                            .with_child(ChildView::new(&context_menu, cx)),
+                    )
                 },
             ))
             .contained()
@@ -702,8 +705,8 @@ pub mod test {
             self.size = size;
         }
 
-        fn icon_path(&self) -> &'static str {
-            "icons/test_panel.svg"
+        fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
+            Some("icons/test_panel.svg")
         }
 
         fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {

styles/src/style_tree/collab_panel.ts 🔗

@@ -67,6 +67,37 @@ export default function contacts_panel(): any {
 
     return {
         channel_modal: channel_modal(),
+        log_in_button: interactive({
+            base: {
+                background: background(theme.middle),
+                border: border(theme.middle, "active"),
+                corner_radius: 4,
+                margin: {
+                    top: 16,
+                    left: 16,
+                    right: 16,
+                },
+                padding: {
+                    top: 3,
+                    bottom: 3,
+                    left: 7,
+                    right: 7,
+                },
+                ...text(theme.middle, "sans", "default", { size: "sm" }),
+            },
+            state: {
+                hovered: {
+                    ...text(theme.middle, "sans", "default", { size: "sm" }),
+                    background: background(theme.middle, "hovered"),
+                    border: border(theme.middle, "active"),
+                },
+                clicked: {
+                    ...text(theme.middle, "sans", "default", { size: "sm" }),
+                    background: background(theme.middle, "pressed"),
+                    border: border(theme.middle, "active"),
+                },
+            },
+        }),
         background: background(layer),
         padding: {
             top: 12,