sidebar: Move toggle to the status bar instead (#51916)

Danilo Leal created

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

Change summary

Cargo.lock                                          |  1 
crates/platform_title_bar/src/platform_title_bar.rs | 15 ---
crates/sidebar/src/sidebar.rs                       | 39 ++++----
crates/title_bar/Cargo.toml                         |  1 
crates/title_bar/src/title_bar.rs                   | 62 +-------------
crates/workspace/src/multi_workspace.rs             | 30 ++++++
crates/workspace/src/status_bar.rs                  | 54 +++++++++++-
crates/workspace/src/workspace.rs                   | 10 ++
crates/zed/src/visual_test_runner.rs                |  8 
crates/zed/src/zed.rs                               |  4 
10 files changed, 113 insertions(+), 111 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -17772,7 +17772,6 @@ dependencies = [
  "client",
  "cloud_api_types",
  "db",
- "feature_flags",
  "git_ui",
  "gpui",
  "icons",

crates/platform_title_bar/src/platform_title_bar.rs 🔗

@@ -32,7 +32,6 @@ pub struct PlatformTitleBar {
     should_move: bool,
     system_window_tabs: Entity<SystemWindowTabs>,
     workspace_sidebar_open: bool,
-    sidebar_has_notifications: bool,
 }
 
 impl PlatformTitleBar {
@@ -47,7 +46,6 @@ impl PlatformTitleBar {
             should_move: false,
             system_window_tabs,
             workspace_sidebar_open: false,
-            sidebar_has_notifications: false,
         }
     }
 
@@ -83,19 +81,6 @@ impl PlatformTitleBar {
         cx.notify();
     }
 
-    pub fn sidebar_has_notifications(&self) -> bool {
-        self.sidebar_has_notifications
-    }
-
-    pub fn set_sidebar_has_notifications(
-        &mut self,
-        has_notifications: bool,
-        cx: &mut Context<Self>,
-    ) {
-        self.sidebar_has_notifications = has_notifications;
-        cx.notify();
-    }
-
     pub fn is_multi_workspace_enabled(cx: &App) -> bool {
         cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
     }

crates/sidebar/src/sidebar.rs 🔗

@@ -2551,23 +2551,19 @@ impl Sidebar {
                         this.pl(px(ui::utils::TRAFFIC_LIGHT_PADDING))
                     })
                     .pr_1p5()
+                    .gap_1()
                     .border_b_1()
                     .border_color(cx.theme().colors().border)
-                    .justify_between()
-                    .child(self.render_sidebar_toggle_button(cx))
+                    .justify_end()
                     .child(
-                        h_flex()
-                            .gap_0p5()
-                            .child(
-                                IconButton::new("archive", IconName::Archive)
-                                    .icon_size(IconSize::Small)
-                                    .tooltip(Tooltip::text("View Archived Threads"))
-                                    .on_click(cx.listener(|this, _, window, cx| {
-                                        this.show_archive(window, cx);
-                                    })),
-                            )
-                            .child(self.render_recent_projects_button(cx)),
-                    ),
+                        IconButton::new("archive", IconName::Archive)
+                            .icon_size(IconSize::Small)
+                            .tooltip(Tooltip::text("View Archived Threads"))
+                            .on_click(cx.listener(|this, _, window, cx| {
+                                this.show_archive(window, cx);
+                            })),
+                    )
+                    .child(self.render_recent_projects_button(cx)),
             )
             .when(!empty_state, |this| {
                 this.child(
@@ -2612,9 +2608,7 @@ impl Sidebar {
     }
 
     fn render_sidebar_toggle_button(&self, _cx: &mut Context<Self>) -> impl IntoElement {
-        let icon = IconName::ThreadsSidebarLeftOpen;
-
-        IconButton::new("sidebar-close-toggle", icon)
+        IconButton::new("sidebar-close-toggle", IconName::ThreadsSidebarLeftOpen)
             .icon_size(IconSize::Small)
             .tooltip(Tooltip::element(move |_window, cx| {
                 v_flex()
@@ -2816,6 +2810,13 @@ impl Render for Sidebar {
                     }),
                 SidebarView::Archive(archive_view) => this.child(archive_view.clone()),
             })
+            .child(
+                h_flex()
+                    .p_1()
+                    .border_t_1()
+                    .border_color(cx.theme().colors().border_variant)
+                    .child(self.render_sidebar_toggle_button(cx)),
+            )
     }
 }
 
@@ -2874,8 +2875,8 @@ mod tests {
         let multi_workspace = multi_workspace.clone();
         let sidebar =
             cx.update(|window, cx| cx.new(|cx| Sidebar::new(multi_workspace.clone(), window, cx)));
-        multi_workspace.update(cx, |mw, _cx| {
-            mw.register_sidebar(sidebar.clone());
+        multi_workspace.update(cx, |mw, cx| {
+            mw.register_sidebar(sidebar.clone(), cx);
         });
         cx.run_until_parked();
         sidebar

crates/title_bar/Cargo.toml 🔗

@@ -38,7 +38,6 @@ chrono.workspace = true
 client.workspace = true
 cloud_api_types.workspace = true
 db.workspace = true
-feature_flags.workspace = true
 git_ui.workspace = true
 gpui = { workspace = true, features = ["screen-capture"] }
 icons.workspace = true

crates/title_bar/src/title_bar.rs 🔗

@@ -25,16 +25,14 @@ use auto_update::AutoUpdateStatus;
 use call::ActiveCall;
 use client::{Client, UserStore, zed_urls};
 use cloud_api_types::Plan;
-use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
+
 use gpui::{
     Action, AnyElement, App, Context, Corner, Element, Empty, Entity, Focusable,
     InteractiveElement, IntoElement, MouseButton, ParentElement, Render,
     StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, actions, div,
 };
 use onboarding_banner::OnboardingBanner;
-use project::{
-    DisableAiSettings, Project, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees,
-};
+use project::{Project, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees};
 use remote::RemoteConnectionOptions;
 use settings::Settings;
 use settings::WorktreeId;
@@ -43,14 +41,13 @@ use std::sync::Arc;
 use theme::ActiveTheme;
 use title_bar_settings::TitleBarSettings;
 use ui::{
-    Avatar, ButtonLike, ContextMenu, Divider, IconWithIndicator, Indicator, PopoverMenu,
-    PopoverMenuHandle, TintColor, Tooltip, prelude::*, utils::platform_title_bar_height,
+    Avatar, ButtonLike, ContextMenu, IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle,
+    TintColor, Tooltip, prelude::*, utils::platform_title_bar_height,
 };
 use update_version::UpdateVersion;
 use util::ResultExt;
 use workspace::{
-    MultiWorkspace, ToggleWorkspaceSidebar, ToggleWorktreeSecurity, Workspace, WorkspaceId,
-    notifications::NotifyResultExt,
+    MultiWorkspace, ToggleWorktreeSecurity, Workspace, WorkspaceId, notifications::NotifyResultExt,
 };
 use zed_actions::OpenRemote;
 
@@ -198,7 +195,6 @@ impl Render for TitleBar {
                     let mut render_project_items = title_bar_settings.show_branch_name
                         || title_bar_settings.show_project_items;
                     title_bar
-                        .children(self.render_workspace_sidebar_toggle(window, cx))
                         .when_some(
                             self.application_menu.clone().filter(|_| !show_menus),
                             |title_bar, menu| {
@@ -402,19 +398,15 @@ impl TitleBar {
                     };
 
                     let is_open = multi_workspace.read(cx).sidebar_open();
-                    let has_notifications = multi_workspace.read(cx).sidebar_has_notifications(cx);
                     platform_titlebar.update(cx, |titlebar, cx| {
                         titlebar.set_workspace_sidebar_open(is_open, cx);
-                        titlebar.set_sidebar_has_notifications(has_notifications, cx);
                     });
 
                     let platform_titlebar = platform_titlebar.clone();
                     let subscription = cx.observe(&multi_workspace, move |mw, cx| {
                         let is_open = mw.read(cx).sidebar_open();
-                        let has_notifications = mw.read(cx).sidebar_has_notifications(cx);
                         platform_titlebar.update(cx, |titlebar, cx| {
                             titlebar.set_workspace_sidebar_open(is_open, cx);
-                            titlebar.set_sidebar_has_notifications(has_notifications, cx);
                         });
                     });
 
@@ -723,50 +715,6 @@ impl TitleBar {
         )
     }
 
-    fn render_workspace_sidebar_toggle(
-        &self,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<AnyElement> {
-        if !cx.has_flag::<AgentV2FeatureFlag>() || DisableAiSettings::get_global(cx).disable_ai {
-            return None;
-        }
-
-        let is_sidebar_open = self.platform_titlebar.read(cx).is_workspace_sidebar_open();
-
-        if is_sidebar_open {
-            return None;
-        }
-
-        let has_notifications = self.platform_titlebar.read(cx).sidebar_has_notifications();
-
-        Some(
-            h_flex()
-                .h_full()
-                .gap_0p5()
-                .child(
-                    IconButton::new(
-                        "toggle-workspace-sidebar",
-                        IconName::ThreadsSidebarLeftClosed,
-                    )
-                    .icon_size(IconSize::Small)
-                    .when(has_notifications, |button| {
-                        button
-                            .indicator(Indicator::dot().color(Color::Accent))
-                            .indicator_border_color(Some(cx.theme().colors().title_bar_background))
-                    })
-                    .tooltip(move |_, cx| {
-                        Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx)
-                    })
-                    .on_click(|_, window, cx| {
-                        window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
-                    }),
-                )
-                .child(Divider::vertical().color(ui::DividerColor::Border))
-                .into_any_element(),
-        )
-    }
-
     fn render_project_name(
         &self,
         name: Option<SharedString>,

crates/workspace/src/multi_workspace.rs 🔗

@@ -169,7 +169,23 @@ impl MultiWorkspace {
         }
     }
 
-    pub fn register_sidebar<T: Sidebar>(&mut self, sidebar: Entity<T>) {
+    pub fn register_sidebar<T: Sidebar>(&mut self, sidebar: Entity<T>, cx: &mut Context<Self>) {
+        self._subscriptions
+            .push(cx.observe(&sidebar, |this, _, cx| {
+                let has_notifications = this.sidebar_has_notifications(cx);
+                let is_open = this.sidebar_open;
+                let show_toggle = this.multi_workspace_enabled(cx);
+                for workspace in &this.workspaces {
+                    workspace.update(cx, |workspace, cx| {
+                        workspace.set_workspace_sidebar_open(
+                            is_open,
+                            has_notifications,
+                            show_toggle,
+                            cx,
+                        );
+                    });
+                }
+            }));
         self.sidebar = Some(Box::new(sidebar));
     }
 
@@ -256,9 +272,11 @@ impl MultiWorkspace {
     pub fn open_sidebar(&mut self, cx: &mut Context<Self>) {
         self.sidebar_open = true;
         let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
+        let has_notifications = self.sidebar_has_notifications(cx);
+        let show_toggle = self.multi_workspace_enabled(cx);
         for workspace in &self.workspaces {
             workspace.update(cx, |workspace, cx| {
-                workspace.set_workspace_sidebar_open(true, cx);
+                workspace.set_workspace_sidebar_open(true, has_notifications, show_toggle, cx);
                 workspace.set_sidebar_focus_handle(sidebar_focus_handle.clone());
             });
         }
@@ -268,9 +286,11 @@ impl MultiWorkspace {
 
     fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         self.sidebar_open = false;
+        let has_notifications = self.sidebar_has_notifications(cx);
+        let show_toggle = self.multi_workspace_enabled(cx);
         for workspace in &self.workspaces {
             workspace.update(cx, |workspace, cx| {
-                workspace.set_workspace_sidebar_open(false, cx);
+                workspace.set_workspace_sidebar_open(false, has_notifications, show_toggle, cx);
                 workspace.set_sidebar_focus_handle(None);
             });
         }
@@ -367,8 +387,10 @@ impl MultiWorkspace {
         } else {
             if self.sidebar_open {
                 let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
+                let has_notifications = self.sidebar_has_notifications(cx);
+                let show_toggle = self.multi_workspace_enabled(cx);
                 workspace.update(cx, |workspace, cx| {
-                    workspace.set_workspace_sidebar_open(true, cx);
+                    workspace.set_workspace_sidebar_open(true, has_notifications, show_toggle, cx);
                     workspace.set_sidebar_focus_handle(sidebar_focus_handle);
                 });
             }

crates/workspace/src/status_bar.rs 🔗

@@ -1,11 +1,11 @@
-use crate::{ItemHandle, Pane};
+use crate::{ItemHandle, Pane, ToggleWorkspaceSidebar};
 use gpui::{
-    AnyView, App, Context, Decorations, Entity, IntoElement, ParentElement, Render, Styled,
+    Action, AnyView, App, Context, Decorations, Entity, IntoElement, ParentElement, Render, Styled,
     Subscription, Window,
 };
 use std::any::TypeId;
 use theme::CLIENT_SIDE_DECORATION_ROUNDING;
-use ui::{h_flex, prelude::*};
+use ui::{Divider, Indicator, Tooltip, prelude::*};
 use util::ResultExt;
 
 pub trait StatusItemView: Render {
@@ -35,6 +35,8 @@ pub struct StatusBar {
     active_pane: Entity<Pane>,
     _observe_active_pane: Subscription,
     workspace_sidebar_open: bool,
+    sidebar_has_notifications: bool,
+    show_sidebar_toggle: bool,
 }
 
 impl Render for StatusBar {
@@ -43,8 +45,7 @@ impl Render for StatusBar {
             .w_full()
             .justify_between()
             .gap(DynamicSpacing::Base08.rems(cx))
-            .py(DynamicSpacing::Base04.rems(cx))
-            .px(DynamicSpacing::Base06.rems(cx))
+            .p(DynamicSpacing::Base04.rems(cx))
             .bg(cx.theme().colors().status_bar_background)
             .map(|el| match window.window_decorations() {
                 Decorations::Server => el,
@@ -61,17 +62,21 @@ impl Render for StatusBar {
                     .border_b(px(1.0))
                     .border_color(cx.theme().colors().status_bar_background),
             })
-            .child(self.render_left_tools())
+            .child(self.render_left_tools(cx))
             .child(self.render_right_tools())
     }
 }
 
 impl StatusBar {
-    fn render_left_tools(&self) -> impl IntoElement {
+    fn render_left_tools(&self, cx: &mut Context<Self>) -> impl IntoElement {
         h_flex()
             .gap_1()
             .min_w_0()
             .overflow_x_hidden()
+            .when(
+                self.show_sidebar_toggle && !self.workspace_sidebar_open,
+                |this| this.child(self.render_sidebar_toggle(cx)),
+            )
             .children(self.left_items.iter().map(|item| item.to_any()))
     }
 
@@ -82,6 +87,29 @@ impl StatusBar {
             .overflow_x_hidden()
             .children(self.right_items.iter().rev().map(|item| item.to_any()))
     }
+
+    fn render_sidebar_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
+        h_flex()
+            .gap_0p5()
+            .child(
+                IconButton::new(
+                    "toggle-workspace-sidebar",
+                    IconName::ThreadsSidebarLeftClosed,
+                )
+                .icon_size(IconSize::Small)
+                .when(self.sidebar_has_notifications, |this| {
+                    this.indicator(Indicator::dot().color(Color::Accent))
+                        .indicator_border_color(Some(cx.theme().colors().status_bar_background))
+                })
+                .tooltip(move |_, cx| {
+                    Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx)
+                })
+                .on_click(|_, window, cx| {
+                    window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
+                }),
+            )
+            .child(Divider::vertical().color(ui::DividerColor::Border))
+    }
 }
 
 impl StatusBar {
@@ -94,6 +122,8 @@ impl StatusBar {
                 this.update_active_pane_item(window, cx)
             }),
             workspace_sidebar_open: false,
+            sidebar_has_notifications: false,
+            show_sidebar_toggle: false,
         };
         this.update_active_pane_item(window, cx);
         this
@@ -104,6 +134,16 @@ impl StatusBar {
         cx.notify();
     }
 
+    pub fn set_sidebar_has_notifications(&mut self, has: bool, cx: &mut Context<Self>) {
+        self.sidebar_has_notifications = has;
+        cx.notify();
+    }
+
+    pub fn set_show_sidebar_toggle(&mut self, show: bool, cx: &mut Context<Self>) {
+        self.show_sidebar_toggle = show;
+        cx.notify();
+    }
+
     pub fn add_left_item<T>(&mut self, item: Entity<T>, window: &mut Window, cx: &mut Context<Self>)
     where
         T: 'static + StatusItemView,

crates/workspace/src/workspace.rs 🔗

@@ -2160,9 +2160,17 @@ impl Workspace {
         &self.status_bar
     }
 
-    pub fn set_workspace_sidebar_open(&self, open: bool, cx: &mut App) {
+    pub fn set_workspace_sidebar_open(
+        &self,
+        open: bool,
+        has_notifications: bool,
+        show_toggle: bool,
+        cx: &mut App,
+    ) {
         self.status_bar.update(cx, |status_bar, cx| {
             status_bar.set_workspace_sidebar_open(open, cx);
+            status_bar.set_sidebar_has_notifications(has_notifications, cx);
+            status_bar.set_show_sidebar_toggle(show_toggle, cx);
         });
     }
 

crates/zed/src/visual_test_runner.rs 🔗

@@ -2659,8 +2659,8 @@ fn run_multi_workspace_sidebar_visual_tests(
         .context("Failed to create sidebar")?;
 
     multi_workspace_window
-        .update(cx, |multi_workspace, _window, _cx| {
-            multi_workspace.register_sidebar(sidebar.clone());
+        .update(cx, |multi_workspace, _window, cx| {
+            multi_workspace.register_sidebar(sidebar.clone(), cx);
         })
         .context("Failed to register sidebar")?;
 
@@ -3191,8 +3191,8 @@ edition = "2021"
         .context("Failed to create sidebar")?;
 
     workspace_window
-        .update(cx, |multi_workspace, _window, _cx| {
-            multi_workspace.register_sidebar(sidebar.clone());
+        .update(cx, |multi_workspace, _window, cx| {
+            multi_workspace.register_sidebar(sidebar.clone(), cx);
         })
         .context("Failed to register sidebar")?;
 

crates/zed/src/zed.rs 🔗

@@ -397,8 +397,8 @@ pub fn initialize_workspace(
                 .update(cx, |_, window, cx| {
                     let sidebar =
                         cx.new(|cx| Sidebar::new(multi_workspace_handle.clone(), window, cx));
-                    multi_workspace_handle.update(cx, |multi_workspace, _cx| {
-                        multi_workspace.register_sidebar(sidebar);
+                    multi_workspace_handle.update(cx, |multi_workspace, cx| {
+                        multi_workspace.register_sidebar(sidebar, cx);
                     });
                 })
                 .ok();