From 8eb86241f6727835bbd9837af1f169344a9aa29c Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 25 Mar 2026 18:35:10 -0700 Subject: [PATCH] Add a setting for moving the sidebar to the right (#52457) ## Context This adds a setting for controlling the sidebar side ## Self-Review Checklist - [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 --------- Co-authored-by: Eric --- Cargo.lock | 2 + assets/icons/threads_sidebar_right_closed.svg | 5 + assets/icons/threads_sidebar_right_open.svg | 5 + assets/settings/default.json | 2 + crates/agent/src/tool_permissions.rs | 1 + crates/agent_settings/src/agent_settings.rs | 16 +- crates/agent_ui/src/agent_panel.rs | 6 +- crates/agent_ui/src/agent_ui.rs | 2 +- crates/agent_ui/src/threads_archive_view.rs | 21 ++- crates/collab_ui/src/notification_panel.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/git_ui/src/git_panel.rs | 2 +- crates/icons/src/icons.rs | 2 + .../src/platform_title_bar.rs | 47 ++++-- crates/project_panel/src/project_panel.rs | 2 +- crates/settings_content/src/agent.rs | 41 +++++ crates/sidebar/Cargo.toml | 1 + crates/sidebar/src/sidebar.rs | 159 +++++++++++------- crates/terminal_view/src/terminal_panel.rs | 2 +- crates/title_bar/src/title_bar.rs | 114 ++++--------- crates/workspace/Cargo.toml | 1 + crates/workspace/src/dock.rs | 14 ++ crates/workspace/src/multi_workspace.rs | 137 +++++++++++---- crates/workspace/src/status_bar.rs | 156 ++++++++++++----- crates/workspace/src/workspace.rs | 47 ++++-- 25 files changed, 524 insertions(+), 265 deletions(-) create mode 100644 assets/icons/threads_sidebar_right_closed.svg create mode 100644 assets/icons/threads_sidebar_right_open.svg diff --git a/Cargo.lock b/Cargo.lock index dd9e9399d00845bfe2382183e6359653466cff4c..16f8dd76ab23bf274b1e6b79515fa8060f2a646f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15980,6 +15980,7 @@ dependencies = [ "action_log", "agent", "agent-client-protocol", + "agent_settings", "agent_ui", "anyhow", "assistant_text_thread", @@ -21516,6 +21517,7 @@ dependencies = [ name = "workspace" version = "0.1.0" dependencies = [ + "agent_settings", "any_vec", "anyhow", "async-recursion", diff --git a/assets/icons/threads_sidebar_right_closed.svg b/assets/icons/threads_sidebar_right_closed.svg new file mode 100644 index 0000000000000000000000000000000000000000..10fa4b792fd65b5875dcf2cadab1fc12a123ab47 --- /dev/null +++ b/assets/icons/threads_sidebar_right_closed.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/threads_sidebar_right_open.svg b/assets/icons/threads_sidebar_right_open.svg new file mode 100644 index 0000000000000000000000000000000000000000..23a01eb3f82a5866157220172c868ed9ded46033 --- /dev/null +++ b/assets/icons/threads_sidebar_right_open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/settings/default.json b/assets/settings/default.json index 6a973314fc36b3b3cc1056dbb10a629f7868d2a1..9ea3285f90885d1ab2c33717b802ac6e8ebbfe3d 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -943,6 +943,8 @@ "button": true, // Where to dock the agent panel. Can be 'left', 'right' or 'bottom'. "dock": "right", + // Where to position the sidebar. Can be 'left', 'right', or 'follow_agent'. + "sidebar_side": "follow_agent", // Default width when the agent panel is docked to the left or right. "default_width": 640, // Default height when the agent panel is docked to the bottom. diff --git a/crates/agent/src/tool_permissions.rs b/crates/agent/src/tool_permissions.rs index 345511c5025b25601c630c572980d44a23f724e7..ec22fa11da00f6f41dfbdcee283ea983fbeac1af 100644 --- a/crates/agent/src/tool_permissions.rs +++ b/crates/agent/src/tool_permissions.rs @@ -596,6 +596,7 @@ mod tests { tool_permissions, show_turn_stats: false, new_thread_location: Default::default(), + sidebar_side: Default::default(), } } diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index d5d4f16eb742a92f6abf8081c43709f161ef4038..ec0a46af0636877210d26b2c45660baae648ffea 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -12,7 +12,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{ DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection, - NewThreadLocation, NotifyWhenAgentWaiting, RegisterSetting, Settings, ToolPermissionMode, + NewThreadLocation, NotifyWhenAgentWaiting, RegisterSetting, Settings, SidebarDockPosition, + SidebarSide, ToolPermissionMode, }; pub use crate::agent_profile::*; @@ -26,6 +27,7 @@ pub struct AgentSettings { pub enabled: bool, pub button: bool, pub dock: DockPosition, + pub sidebar_side: SidebarDockPosition, pub default_width: Pixels, pub default_height: Pixels, pub default_model: Option, @@ -77,6 +79,17 @@ impl AgentSettings { return None; } + pub fn sidebar_side(&self) -> SidebarSide { + match self.sidebar_side { + SidebarDockPosition::Left => SidebarSide::Left, + SidebarDockPosition::Right => SidebarSide::Right, + SidebarDockPosition::FollowAgent => match self.dock { + DockPosition::Right => SidebarSide::Right, + _ => SidebarSide::Left, + }, + } + } + pub fn set_message_editor_max_lines(&self) -> usize { self.message_editor_min_lines * 2 } @@ -407,6 +420,7 @@ impl Settings for AgentSettings { enabled: agent.enabled.unwrap(), button: agent.button.unwrap(), dock: agent.dock.unwrap(), + sidebar_side: agent.sidebar_side.unwrap(), default_width: px(agent.default_width.unwrap()), default_height: px(agent.default_height.unwrap()), default_model: Some(agent.default_model.unwrap()), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 1015e87cef976be5584d7cb7607fd52f216babd6..ff819b3730bdaf8dc89d5c40e5fdad04b3342496 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -3186,13 +3186,17 @@ impl Panel for AgentPanel { } fn activation_priority(&self) -> u32 { - 8 + 0 } fn enabled(&self, cx: &App) -> bool { AgentSettings::get_global(cx).enabled(cx) } + fn is_agent_panel(&self) -> bool { + true + } + fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool { self.zoomed } diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 1817fdbe31a4fed12caf2e5f804461aecf9da973..2395a74c281f5b84ab1f328b175fe8385ce3fb12 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -648,7 +648,6 @@ mod tests { default_profile: AgentProfileId::default(), default_view: DefaultAgentView::Thread, profiles: Default::default(), - notify_when_agent_waiting: NotifyWhenAgentWaiting::default(), play_sound_when_agent_done: false, single_file_review: false, @@ -662,6 +661,7 @@ mod tests { tool_permissions: Default::default(), show_turn_stats: false, new_thread_location: Default::default(), + sidebar_side: Default::default(), }; cx.update(|cx| { diff --git a/crates/agent_ui/src/threads_archive_view.rs b/crates/agent_ui/src/threads_archive_view.rs index ef4e3ab5393b1045b4de15b348c3e01e07c366bc..a5c83d48b71c08f183c842cb3b5a3d8b75db4d89 100644 --- a/crates/agent_ui/src/threads_archive_view.rs +++ b/crates/agent_ui/src/threads_archive_view.rs @@ -7,6 +7,7 @@ use crate::{ use acp_thread::AgentSessionInfo; use agent::ThreadStore; use agent_client_protocol as acp; +use agent_settings::AgentSettings; use chrono::{DateTime, Datelike as _, Local, NaiveDate, TimeDelta, Utc}; use editor::Editor; use fs::Fs; @@ -17,6 +18,7 @@ use gpui::{ use itertools::Itertools as _; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious}; use project::{AgentId, AgentServerStore}; +use settings::Settings as _; use theme::ActiveTheme; use ui::{ ButtonLike, CommonAnimationExt, ContextMenu, ContextMenuEntry, Divider, HighlightedLabel, @@ -795,7 +797,12 @@ impl ThreadsArchiveView { fn render_header(&self, window: &Window, cx: &mut Context) -> impl IntoElement { let has_query = !self.filter_editor.read(cx).text(cx).is_empty(); - let traffic_lights = cfg!(target_os = "macos") && !window.is_fullscreen(); + let sidebar_on_left = matches!( + AgentSettings::get_global(cx).sidebar_side(), + settings::SidebarSide::Left + ); + let traffic_lights = + cfg!(target_os = "macos") && !window.is_fullscreen() && sidebar_on_left; let header_height = platform_title_bar_height(window); let show_focus_keybinding = self.selection.is_some() && !self.filter_editor.focus_handle(cx).is_focused(window); @@ -804,15 +811,21 @@ impl ThreadsArchiveView { .h(header_height) .mt_px() .pb_px() - .when(traffic_lights, |this| { - this.pl(px(ui::utils::TRAFFIC_LIGHT_PADDING)) + .map(|this| { + if traffic_lights { + this.pl(px(ui::utils::TRAFFIC_LIGHT_PADDING)) + } else { + this.pl_1p5() + } }) .pr_1p5() .gap_1() .justify_between() .border_b_1() .border_color(cx.theme().colors().border) - .child(Divider::vertical().color(ui::DividerColor::Border)) + .when(traffic_lights, |this| { + this.child(Divider::vertical().color(ui::DividerColor::Border)) + }) .child( h_flex() .ml_1() diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index a40dc154b0f4e498f435080572ed5a7161917ab3..d7fef4873c687ab23a25b3144ba902cf4c42c137 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -640,7 +640,7 @@ impl Panel for NotificationPanel { } fn activation_priority(&self) -> u32 { - 3 + 4 } } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 3f2ba98de7519e8343f4bc1791a6d8f7f36b3e86..c2d8a7a5478cfc9eae53f9e7a6018864865a4d1a 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1601,7 +1601,7 @@ impl Panel for DebugPanel { } fn activation_priority(&self) -> u32 { - 9 + 7 } fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context) {} diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 77d05f52f7b799bfdbb4b081e3ca2368de6fd45f..00b287f7f3d724e0fcc5275ea302c44983c9a61b 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -5818,7 +5818,7 @@ impl Panel for GitPanel { } fn activation_priority(&self) -> u32 { - 2 + 3 } } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index c8bf3b4e7708650a030218c91bb71bfd6a398635..89932125c1bfbc05202038c1abac2a6380e19e93 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -244,6 +244,8 @@ pub enum IconName { ThreadFromSummary, ThreadsSidebarLeftClosed, ThreadsSidebarLeftOpen, + ThreadsSidebarRightClosed, + ThreadsSidebarRightOpen, ThumbsDown, ThumbsUp, TodoComplete, diff --git a/crates/platform_title_bar/src/platform_title_bar.rs b/crates/platform_title_bar/src/platform_title_bar.rs index c5c9f94290c0e96a63fb71a098ea7ea29ec1e3cd..777dd9e2d905e7ba54fcb97b9912610b04c49527 100644 --- a/crates/platform_title_bar/src/platform_title_bar.rs +++ b/crates/platform_title_bar/src/platform_title_bar.rs @@ -4,8 +4,8 @@ mod system_window_tabs; use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt}; use gpui::{ Action, AnyElement, App, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement, - MouseButton, ParentElement, StatefulInteractiveElement, Styled, Window, WindowButtonLayout, - WindowControlArea, div, px, + MouseButton, ParentElement, StatefulInteractiveElement, Styled, WeakEntity, Window, + WindowButtonLayout, WindowControlArea, div, px, }; use project::DisableAiSettings; use settings::Settings; @@ -15,6 +15,7 @@ use ui::{ prelude::*, utils::{TRAFFIC_LIGHT_PADDING, platform_title_bar_height}, }; +use workspace::{MultiWorkspace, SidebarRenderState, SidebarSide}; use crate::{ platforms::{platform_linux, platform_windows}, @@ -32,7 +33,7 @@ pub struct PlatformTitleBar { should_move: bool, system_window_tabs: Entity, button_layout: Option, - workspace_sidebar_open: bool, + multi_workspace: Option>, } impl PlatformTitleBar { @@ -47,10 +48,19 @@ impl PlatformTitleBar { should_move: false, system_window_tabs, button_layout: None, - workspace_sidebar_open: false, + multi_workspace: None, } } + pub fn with_multi_workspace(mut self, multi_workspace: WeakEntity) -> Self { + self.multi_workspace = Some(multi_workspace); + self + } + + pub fn set_multi_workspace(&mut self, multi_workspace: WeakEntity) { + self.multi_workspace = Some(multi_workspace); + } + pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context) -> Hsla { if cfg!(any(target_os = "linux", target_os = "freebsd")) { if window.is_window_active() && !self.should_move { @@ -92,13 +102,12 @@ impl PlatformTitleBar { SystemWindowTabs::init(cx); } - pub fn is_workspace_sidebar_open(&self) -> bool { - self.workspace_sidebar_open - } - - pub fn set_workspace_sidebar_open(&mut self, open: bool, cx: &mut Context) { - self.workspace_sidebar_open = open; - cx.notify(); + fn sidebar_render_state(&self, cx: &App) -> SidebarRenderState { + self.multi_workspace + .as_ref() + .and_then(|mw| mw.upgrade()) + .map(|mw| mw.read(cx).sidebar_render_state(cx)) + .unwrap_or_default() } pub fn is_multi_workspace_enabled(cx: &App) -> bool { @@ -116,8 +125,7 @@ impl Render for PlatformTitleBar { let children = mem::take(&mut self.children); let button_layout = self.effective_button_layout(&decorations, cx); - let is_multiworkspace_sidebar_open = - PlatformTitleBar::is_multi_workspace_enabled(cx) && self.is_workspace_sidebar_open(); + let sidebar = self.sidebar_render_state(cx); let title_bar = h_flex() .window_control_area(WindowControlArea::Drag) @@ -168,7 +176,7 @@ impl Render for PlatformTitleBar { if window.is_fullscreen() { this.pl_2() } else if self.platform_style == PlatformStyle::Mac - && !is_multiworkspace_sidebar_open + && !(sidebar.open && sidebar.side == SidebarSide::Left) { this.pl(px(TRAFFIC_LIGHT_PADDING)) } else if let Some(button_layout) = @@ -186,11 +194,14 @@ impl Render for PlatformTitleBar { .map(|el| match decorations { Decorations::Server => el, Decorations::Client { tiling, .. } => el - .when(!(tiling.top || tiling.right), |el| { - el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) .when( - !(tiling.top || tiling.left) && !is_multiworkspace_sidebar_open, + !(tiling.top || tiling.right) + && !(sidebar.open && sidebar.side == SidebarSide::Right), + |el| el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING), + ) + .when( + !(tiling.top || tiling.left) + && !(sidebar.open && sidebar.side == SidebarSide::Left), |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING), ) // this border is to avoid a transparent gap in the rounded corners diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 68199376e8bd39cf4014bb219a305f52a46347cb..ac348d9d5bab659c115f7b6c9f1a11c4d7c951bc 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -7239,7 +7239,7 @@ impl Panel for ProjectPanel { } fn activation_priority(&self) -> u32 { - 0 + 1 } } diff --git a/crates/settings_content/src/agent.rs b/crates/settings_content/src/agent.rs index 1b71f9b33c58b6980431d25f2af51007ae861a1c..0c77957bc6a1dab2af47164cbd1f46c5dc679d37 100644 --- a/crates/settings_content/src/agent.rs +++ b/crates/settings_content/src/agent.rs @@ -33,6 +33,39 @@ pub enum NewThreadLocation { NewWorktree, } +/// Where to position the sidebar. +#[derive( + Clone, + Copy, + Debug, + Default, + PartialEq, + Eq, + Serialize, + Deserialize, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, +)] +#[serde(rename_all = "snake_case")] +pub enum SidebarDockPosition { + /// Always show the sidebar on the left side. + Left, + /// Always show the sidebar on the right side. + Right, + /// Show the sidebar on the same side as the agent panel. + #[default] + FollowAgent, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum SidebarSide { + #[default] + Left, + Right, +} + #[with_fallible_options] #[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)] pub struct AgentSettingsContent { @@ -48,6 +81,10 @@ pub struct AgentSettingsContent { /// /// Default: right pub dock: Option, + /// Where to position the sidebar. + /// + /// Default: follow_agent + pub sidebar_side: Option, /// Default width in pixels when the agent panel is docked to the left or right. /// /// Default: 640 @@ -157,6 +194,10 @@ impl AgentSettingsContent { self.dock = Some(dock); } + pub fn set_sidebar_side(&mut self, position: SidebarDockPosition) { + self.sidebar_side = Some(position); + } + pub fn set_model(&mut self, language_model: LanguageModelSelection) { self.default_model = Some(language_model) } diff --git a/crates/sidebar/Cargo.toml b/crates/sidebar/Cargo.toml index 6b4d93790236f32b0533374626e337f5c05ab75b..7fcb97a92695b5c3e9e1b32560f332d6bd6908d5 100644 --- a/crates/sidebar/Cargo.toml +++ b/crates/sidebar/Cargo.toml @@ -19,6 +19,7 @@ acp_thread.workspace = true action_log.workspace = true agent.workspace = true agent-client-protocol.workspace = true +agent_settings.workspace = true agent_ui.workspace = true anyhow.workspace = true chrono.workspace = true diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 252b306ce0af157954971c986499b372f2a2290f..501b55a73260f0d453775fc245868669c35ab406 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -1,6 +1,7 @@ use acp_thread::ThreadStatus; use action_log::DiffStats; use agent_client_protocol::{self as acp}; +use agent_settings::AgentSettings; use agent_ui::thread_metadata_store::{SidebarThreadMetadataStore, ThreadMetadata}; use agent_ui::threads_archive_view::{ ThreadsArchiveView, ThreadsArchiveViewEvent, format_history_entry_timestamp, @@ -37,7 +38,8 @@ use util::ResultExt as _; use util::path_list::PathList; use workspace::{ AddFolderToProject, FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent, Open, - Sidebar as WorkspaceSidebar, ToggleWorkspaceSidebar, Workspace, WorkspaceId, + Sidebar as WorkspaceSidebar, SidebarSide, ToggleWorkspaceSidebar, Workspace, WorkspaceId, + sidebar_side_context_menu, }; use zed_actions::OpenRecent; @@ -2874,7 +2876,9 @@ impl Sidebar { cx: &mut Context, ) -> impl IntoElement { let has_query = self.has_filter_query(cx); - let traffic_lights = cfg!(target_os = "macos") && !window.is_fullscreen(); + let sidebar_on_left = self.side(cx) == SidebarSide::Left; + let traffic_lights = + cfg!(target_os = "macos") && !window.is_fullscreen() && sidebar_on_left; let header_height = platform_title_bar_height(window); h_flex() @@ -2928,38 +2932,92 @@ impl Sidebar { } fn render_sidebar_toggle_button(&self, _cx: &mut Context) -> impl IntoElement { - IconButton::new("sidebar-close-toggle", IconName::ThreadsSidebarLeftOpen) - .icon_size(IconSize::Small) - .tooltip(Tooltip::element(move |_window, cx| { - v_flex() - .gap_1() - .child( - h_flex() - .gap_2() - .justify_between() - .child(Label::new("Toggle Sidebar")) - .child(KeyBinding::for_action(&ToggleWorkspaceSidebar, cx)), - ) - .child( - h_flex() - .pt_1() - .gap_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .justify_between() - .child(Label::new("Focus Sidebar")) - .child(KeyBinding::for_action(&FocusWorkspaceSidebar, cx)), - ) - .into_any_element() - })) - .on_click(|_, window, cx| { - if let Some(multi_workspace) = window.root::().flatten() { - multi_workspace.update(cx, |multi_workspace, cx| { - multi_workspace.close_sidebar(window, cx); - }); - } + let on_right = AgentSettings::get_global(_cx).sidebar_side() == SidebarSide::Right; + + sidebar_side_context_menu("sidebar-toggle-menu", _cx) + .anchor(if on_right { + gpui::Corner::BottomRight + } else { + gpui::Corner::BottomLeft + }) + .attach(if on_right { + gpui::Corner::TopRight + } else { + gpui::Corner::TopLeft + }) + .trigger(move |_is_active, _window, _cx| { + let icon = if on_right { + IconName::ThreadsSidebarRightOpen + } else { + IconName::ThreadsSidebarLeftOpen + }; + IconButton::new("sidebar-close-toggle", icon) + .icon_size(IconSize::Small) + .tooltip(Tooltip::element(move |_window, cx| { + v_flex() + .gap_1() + .child( + h_flex() + .gap_2() + .justify_between() + .child(Label::new("Toggle Sidebar")) + .child(KeyBinding::for_action(&ToggleWorkspaceSidebar, cx)), + ) + .child( + h_flex() + .pt_1() + .gap_2() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .justify_between() + .child(Label::new("Focus Sidebar")) + .child(KeyBinding::for_action(&FocusWorkspaceSidebar, cx)), + ) + .into_any_element() + })) + .on_click(|_, window, cx| { + if let Some(multi_workspace) = window.root::().flatten() { + multi_workspace.update(cx, |multi_workspace, cx| { + multi_workspace.close_sidebar(window, cx); + }); + } + }) }) } + + fn render_sidebar_bottom_bar(&mut self, cx: &mut Context) -> impl IntoElement { + let on_right = self.side(cx) == SidebarSide::Right; + let is_archive = matches!(self.view, SidebarView::Archive(..)); + let action_buttons = h_flex() + .gap_1() + .child( + IconButton::new("archive", IconName::Archive) + .icon_size(IconSize::Small) + .toggle_state(is_archive) + .tooltip(move |_, cx| { + Tooltip::for_action("Toggle Archived Threads", &ToggleArchive, cx) + }) + .on_click(cx.listener(|this, _, window, cx| { + this.toggle_archive(&ToggleArchive, window, cx); + })), + ) + .child(self.render_recent_projects_button(cx)); + let border_color = cx.theme().colors().border; + let toggle_button = self.render_sidebar_toggle_button(cx); + + let bar = h_flex() + .p_1() + .gap_1() + .justify_between() + .border_t_1() + .border_color(border_color); + + if on_right { + bar.child(action_buttons).child(toggle_button) + } else { + bar.child(toggle_button).child(action_buttons) + } + } } impl Sidebar { @@ -3054,6 +3112,10 @@ impl WorkspaceSidebar for Sidebar { matches!(self.view, SidebarView::ThreadList) } + fn side(&self, cx: &App) -> SidebarSide { + AgentSettings::get_global(cx).sidebar_side() + } + fn prepare_for_focus(&mut self, _window: &mut Window, cx: &mut Context) { self.selection = None; cx.notify(); @@ -3108,7 +3170,8 @@ impl Render for Sidebar { .h_full() .w(self.width) .bg(bg) - .border_r_1() + .when(self.side(cx) == SidebarSide::Left, |el| el.border_r_1()) + .when(self.side(cx) == SidebarSide::Right, |el| el.border_l_1()) .border_color(color.border) .map(|this| match &self.view { SidebarView::ThreadList => this @@ -3140,35 +3203,7 @@ impl Render for Sidebar { }), SidebarView::Archive(archive_view) => this.child(archive_view.clone()), }) - .child( - h_flex() - .p_1() - .gap_1() - .justify_between() - .border_t_1() - .border_color(cx.theme().colors().border) - .child(self.render_sidebar_toggle_button(cx)) - .child( - h_flex() - .gap_1() - .child( - IconButton::new("archive", IconName::Archive) - .icon_size(IconSize::Small) - .toggle_state(matches!(self.view, SidebarView::Archive(..))) - .tooltip(move |_, cx| { - Tooltip::for_action( - "Toggle Archived Threads", - &ToggleArchive, - cx, - ) - }) - .on_click(cx.listener(|this, _, window, cx| { - this.toggle_archive(&ToggleArchive, window, cx); - })), - ) - .child(self.render_recent_projects_button(cx)), - ), - ) + .child(self.render_sidebar_bottom_bar(cx)) } } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 2ecc959663a082aac2d37410639f55e4ff242681..4f79914eb889b4402eb86ce7ca5359d3d0e16085 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1654,7 +1654,7 @@ impl Panel for TerminalPanel { } fn activation_priority(&self) -> u32 { - 1 + 2 } } diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index b40029bf93365d2b1fbaa4902f26e2bcb24ee9af..86e15dd7284881961cbc2c43f17e603ea3d39bc3 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -81,7 +81,8 @@ pub fn init(cx: &mut App) { let Some(window) = window else { return; }; - let item = cx.new(|cx| TitleBar::new("title-bar", workspace, window, cx)); + let multi_workspace = workspace.multi_workspace().cloned(); + let item = cx.new(|cx| TitleBar::new("title-bar", workspace, multi_workspace, window, cx)); workspace.set_titlebar_item(item.into(), window, cx); workspace.register_action(|workspace, _: &SimulateUpdateAvailable, _window, cx| { @@ -161,7 +162,18 @@ pub struct TitleBar { impl Render for TitleBar { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - self.sync_multi_workspace(window, cx); + if self.multi_workspace.is_none() { + if let Some(mw) = self + .workspace + .upgrade() + .and_then(|ws| ws.read(cx).multi_workspace().cloned()) + { + self.multi_workspace = Some(mw.clone()); + self.platform_titlebar.update(cx, |titlebar, _cx| { + titlebar.set_multi_workspace(mw); + }); + } + } let title_bar_settings = *TitleBarSettings::get_global(cx); let button_layout = title_bar_settings.button_layout; @@ -308,6 +320,7 @@ impl TitleBar { pub fn new( id: impl Into, workspace: &Workspace, + multi_workspace: Option>, window: &mut Window, cx: &mut Context, ) -> Self { @@ -385,52 +398,19 @@ impl TitleBar { }); let update_version = cx.new(|cx| UpdateVersion::new(cx)); - let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx)); - - // Set up observer to sync sidebar state from MultiWorkspace to PlatformTitleBar. - { - let platform_titlebar = platform_titlebar.clone(); - let window_handle = window.window_handle(); - cx.spawn(async move |this: WeakEntity, cx| { - let Some(multi_workspace_handle) = window_handle.downcast::() - else { - return; - }; - - let _ = cx.update(|cx| { - let Ok(multi_workspace) = multi_workspace_handle.entity(cx) else { - return; - }; - - let is_open = multi_workspace.read(cx).sidebar_open(); - platform_titlebar.update(cx, |titlebar, cx| { - titlebar.set_workspace_sidebar_open(is_open, cx); - }); - - let platform_titlebar = platform_titlebar.clone(); - let subscription = cx.observe(&multi_workspace, move |mw, cx| { - let is_open = mw.read(cx).sidebar_open(); - platform_titlebar.update(cx, |titlebar, cx| { - titlebar.set_workspace_sidebar_open(is_open, cx); - }); - }); - - if let Some(this) = this.upgrade() { - this.update(cx, |this, _| { - this._subscriptions.push(subscription); - this.multi_workspace = Some(multi_workspace.downgrade()); - }); - } - }); - }) - .detach(); - } + let platform_titlebar = cx.new(|cx| { + let mut titlebar = PlatformTitleBar::new(id, cx); + if let Some(mw) = multi_workspace.clone() { + titlebar = titlebar.with_multi_workspace(mw); + } + titlebar + }); let mut this = Self { platform_titlebar, application_menu, workspace: workspace.weak_handle(), - multi_workspace: None, + multi_workspace, project, user_store, client, @@ -446,46 +426,6 @@ impl TitleBar { this } - /// Used to update the title bar state in case the workspace has - /// been moved to a new window through the threads sidebar. - fn sync_multi_workspace(&mut self, window: &mut Window, cx: &mut Context) { - let current = window - .root::() - .flatten() - .map(|mw| mw.entity_id()); - - let tracked = self - .multi_workspace - .as_ref() - .and_then(|weak| weak.upgrade()) - .map(|mw| mw.entity_id()); - - if current == tracked { - return; - } - - let Some(multi_workspace) = window.root::().flatten() else { - self.multi_workspace = None; - return; - }; - - let is_open = multi_workspace.read(cx).sidebar_open(); - self.platform_titlebar.update(cx, |titlebar, cx| { - titlebar.set_workspace_sidebar_open(is_open, cx); - }); - - let platform_titlebar = self.platform_titlebar.clone(); - let subscription = cx.observe(&multi_workspace, move |_this, mw, cx| { - let is_open = mw.read(cx).sidebar_open(); - platform_titlebar.update(cx, |titlebar, cx| { - titlebar.set_workspace_sidebar_open(is_open, cx); - }); - }); - - self.multi_workspace = Some(multi_workspace.downgrade()); - self._subscriptions.push(subscription); - } - fn worktree_count(&self, cx: &App) -> usize { self.project.read(cx).visible_worktrees(cx).count() } @@ -777,7 +717,13 @@ impl TitleBar { "Open Recent Project".to_string() }; - let is_sidebar_open = self.platform_titlebar.read(cx).is_workspace_sidebar_open(); + let is_sidebar_open = self + .multi_workspace + .as_ref() + .and_then(|mw| mw.upgrade()) + .map(|mw| mw.read(cx).sidebar_open()) + .unwrap_or(false) + && PlatformTitleBar::is_multi_workspace_enabled(cx); let is_threads_list_view_active = self .multi_workspace diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index d5e9400353eee50b3c5734a31684abdb0149caa0..fd160fd3024564d7451be0c29958cbb4a33eee38 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -27,6 +27,7 @@ test-support = [ [dependencies] any_vec.workspace = true +agent_settings.workspace = true anyhow.workspace = true async-recursion.workspace = true client.workspace = true diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 16d31ca699bfc984ceaa49c6bda2e38d66da13f7..131c02e9c885b66ddf32ed6d2a0dfb01d2764a49 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -69,6 +69,9 @@ pub trait Panel: Focusable + EventEmitter + Render + Sized { fn enabled(&self, _cx: &App) -> bool { true } + fn is_agent_panel(&self) -> bool { + false + } } pub trait PanelHandle: Send + Sync { @@ -95,6 +98,7 @@ pub trait PanelHandle: Send + Sync { fn to_any(&self) -> AnyView; fn activation_priority(&self, cx: &App) -> u32; fn enabled(&self, cx: &App) -> bool; + fn is_agent_panel(&self, cx: &App) -> bool; fn move_to_next_position(&self, window: &mut Window, cx: &mut App) { let current_position = self.position(window, cx); let next_position = [ @@ -207,6 +211,10 @@ where fn enabled(&self, cx: &App) -> bool { self.read(cx).enabled(cx) } + + fn is_agent_panel(&self, cx: &App) -> bool { + self.read(cx).is_agent_panel() + } } impl From<&dyn PanelHandle> for AnyView { @@ -720,6 +728,12 @@ impl Dock { self.panel_entries.len() } + pub fn has_agent_panel(&self, cx: &App) -> bool { + self.panel_entries + .iter() + .any(|entry| entry.panel.is_agent_panel(cx)) + } + pub fn activate_panel(&mut self, panel_ix: usize, window: &mut Window, cx: &mut Context) { if Some(panel_ix) != self.active_panel_index { if let Some(active_panel) = self.active_panel_entry() { diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs index 652331bc724a18b4f8cef20ba6ca41037af929eb..9e043e9ae7feb9f4ece21945d48d818f7345a03d 100644 --- a/crates/workspace/src/multi_workspace.rs +++ b/crates/workspace/src/multi_workspace.rs @@ -9,6 +9,7 @@ use project::DisableAiSettings; #[cfg(any(test, feature = "test-support"))] use project::Project; use settings::Settings; +pub use settings::SidebarSide; use std::future::Future; use std::path::PathBuf; use std::sync::Arc; @@ -16,6 +17,10 @@ use ui::prelude::*; use util::ResultExt; use zed_actions::agents_sidebar::MoveWorkspaceToNewWindow; +use agent_settings::AgentSettings; +use settings::SidebarDockPosition; +use ui::{ContextMenu, right_click_menu}; + const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0); use crate::{ @@ -39,6 +44,47 @@ actions!( ] ); +#[derive(Default)] +pub struct SidebarRenderState { + pub open: bool, + pub side: SidebarSide, +} + +pub fn sidebar_side_context_menu( + id: impl Into, + cx: &App, +) -> ui::RightClickMenu { + let current_position = AgentSettings::get_global(cx).sidebar_side; + right_click_menu(id).menu(move |window, cx| { + let fs = ::global(cx); + ContextMenu::build(window, cx, move |mut menu, _, _cx| { + let positions: [(SidebarDockPosition, &str); 3] = [ + (SidebarDockPosition::Left, "Left"), + (SidebarDockPosition::Right, "Right"), + (SidebarDockPosition::FollowAgent, "Follow Agent Panel"), + ]; + for (position, label) in positions { + let fs = fs.clone(); + menu = menu.toggleable_entry( + label, + position == current_position, + IconPosition::Start, + None, + move |_window, cx| { + settings::update_settings_file(fs.clone(), cx, move |settings, _cx| { + settings + .agent + .get_or_insert_default() + .set_sidebar_side(position); + }); + }, + ); + } + menu + }) + }) +} + pub enum MultiWorkspaceEvent { ActiveWorkspaceChanged, WorkspaceAdded(Entity), @@ -49,6 +95,7 @@ pub trait Sidebar: Focusable + Render + Sized { fn width(&self, cx: &App) -> Pixels; fn set_width(&mut self, width: Option, cx: &mut Context); fn has_notifications(&self, cx: &App) -> bool; + fn side(&self, _cx: &App) -> SidebarSide; fn is_threads_list_view_active(&self) -> bool { true @@ -68,6 +115,8 @@ pub trait SidebarHandle: 'static + Send + Sync { fn entity_id(&self) -> EntityId; fn is_threads_list_view_active(&self, cx: &App) -> bool; + + fn side(&self, cx: &App) -> SidebarSide; } #[derive(Clone)] @@ -116,6 +165,10 @@ impl SidebarHandle for Entity { fn is_threads_list_view_active(&self, cx: &App) -> bool { self.read(cx).is_threads_list_view_active() } + + fn side(&self, cx: &App) -> SidebarSide { + self.read(cx).side(cx) + } } pub struct MultiWorkspace { @@ -132,6 +185,19 @@ pub struct MultiWorkspace { impl EventEmitter for MultiWorkspace {} impl MultiWorkspace { + pub fn sidebar_side(&self, cx: &App) -> SidebarSide { + self.sidebar + .as_ref() + .map_or(SidebarSide::Left, |s| s.side(cx)) + } + + pub fn sidebar_render_state(&self, cx: &App) -> SidebarRenderState { + SidebarRenderState { + open: self.sidebar_open() && self.multi_workspace_enabled(cx), + side: self.sidebar_side(cx), + } + } + pub fn new(workspace: Entity, window: &mut Window, cx: &mut Context) -> Self { let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| { if let Some(task) = this._serialize_task.take() { @@ -149,6 +215,10 @@ impl MultiWorkspace { } }); Self::subscribe_to_workspace(&workspace, cx); + let weak_self = cx.weak_entity(); + workspace.update(cx, |workspace, cx| { + workspace.set_multi_workspace(weak_self, cx); + }); Self { window_id: window.window_handle().window_id(), workspaces: vec![workspace], @@ -167,20 +237,8 @@ impl MultiWorkspace { pub fn register_sidebar(&mut self, sidebar: Entity, cx: &mut Context) { 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, - ); - }); - } + .push(cx.observe(&sidebar, |_this, _, cx| { + cx.notify(); })); self.sidebar = Some(Box::new(sidebar)); } @@ -266,11 +324,8 @@ impl MultiWorkspace { pub fn open_sidebar(&mut self, cx: &mut Context) { 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, has_notifications, show_toggle, cx); + workspace.update(cx, |workspace, _cx| { workspace.set_sidebar_focus_handle(sidebar_focus_handle.clone()); }); } @@ -280,11 +335,8 @@ impl MultiWorkspace { pub fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context) { 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, has_notifications, show_toggle, cx); + workspace.update(cx, |workspace, _cx| { workspace.set_sidebar_focus_handle(None); }); } @@ -381,13 +433,14 @@ 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, has_notifications, show_toggle, cx); + workspace.update(cx, |workspace, _cx| { workspace.set_sidebar_focus_handle(sidebar_focus_handle); }); } + let weak_self = cx.weak_entity(); + workspace.update(cx, |workspace, cx| { + workspace.set_multi_workspace(weak_self, cx); + }); Self::subscribe_to_workspace(&workspace, cx); self.workspaces.push(workspace.clone()); cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace)); @@ -767,6 +820,8 @@ impl MultiWorkspace { impl Render for MultiWorkspace { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let multi_workspace_enabled = self.multi_workspace_enabled(cx); + let sidebar_side = self.sidebar_side(cx); + let sidebar_on_right = sidebar_side == SidebarSide::Right; let sidebar: Option = if multi_workspace_enabled && self.sidebar_open() { self.sidebar.as_ref().map(|sidebar_handle| { @@ -777,7 +832,12 @@ impl Render for MultiWorkspace { div() .id("sidebar-resize-handle") .absolute() - .right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.) + .when(!sidebar_on_right, |el| { + el.right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.) + }) + .when(sidebar_on_right, |el| { + el.left(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.) + }) .top(px(0.)) .h_full() .w(SIDEBAR_RESIZE_HANDLE_SIZE) @@ -817,6 +877,12 @@ impl Render for MultiWorkspace { None }; + let (left_sidebar, right_sidebar) = if sidebar_on_right { + (None, sidebar) + } else { + (sidebar, None) + }; + let ui_font = theme::setup_ui_font(window, cx); let text_color = cx.theme().colors().text; @@ -855,16 +921,23 @@ impl Render for MultiWorkspace { self.sidebar_open() && self.multi_workspace_enabled(cx), |this| { this.on_drag_move(cx.listener( - |this: &mut Self, e: &DragMoveEvent, _window, cx| { + move |this: &mut Self, + e: &DragMoveEvent, + window, + cx| { if let Some(sidebar) = &this.sidebar { - let new_width = e.event.position.x; + let new_width = if sidebar_on_right { + window.bounds().size.width - e.event.position.x + } else { + e.event.position.x + }; sidebar.set_width(Some(new_width), cx); } }, )) - .children(sidebar) }, ) + .children(left_sidebar) .child( div() .flex() @@ -873,11 +946,13 @@ impl Render for MultiWorkspace { .overflow_hidden() .child(self.workspace().clone()), ) + .children(right_sidebar) .child(self.workspace().read(cx).modal_layer.clone()), window, cx, Tiling { - left: multi_workspace_enabled && self.sidebar_open(), + left: !sidebar_on_right && multi_workspace_enabled && self.sidebar_open(), + right: sidebar_on_right && multi_workspace_enabled && self.sidebar_open(), ..Tiling::default() }, ) diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 304c6417baab6c6a9b4b6e26e8f685992c1f80db..dad5389f2f5574c773af740fd61c6c1501c2fea0 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -1,7 +1,10 @@ -use crate::{ItemHandle, MultiWorkspace, Pane, ToggleWorkspaceSidebar}; +use crate::{ + ItemHandle, MultiWorkspace, Pane, SidebarSide, ToggleWorkspaceSidebar, + sidebar_side_context_menu, +}; use gpui::{ - AnyView, App, Context, Decorations, Entity, IntoElement, ParentElement, Render, Styled, - Subscription, Window, + AnyView, App, Context, Corner, Decorations, Entity, IntoElement, ParentElement, Render, Styled, + Subscription, WeakEntity, Window, }; use std::any::TypeId; use theme::CLIENT_SIDE_DECORATION_ROUNDING; @@ -29,18 +32,45 @@ trait StatusItemViewHandle: Send { fn item_type(&self) -> TypeId; } +#[derive(Default)] +struct SidebarStatus { + open: bool, + side: SidebarSide, + has_notifications: bool, + show_toggle: bool, +} + +impl SidebarStatus { + fn query(multi_workspace: &Option>, cx: &App) -> Self { + multi_workspace + .as_ref() + .and_then(|mw| mw.upgrade()) + .map(|mw| { + let mw = mw.read(cx); + let enabled = mw.multi_workspace_enabled(cx); + Self { + open: mw.sidebar_open() && enabled, + side: mw.sidebar_side(cx), + has_notifications: mw.sidebar_has_notifications(cx), + show_toggle: enabled, + } + }) + .unwrap_or_default() + } +} + pub struct StatusBar { left_items: Vec>, right_items: Vec>, active_pane: Entity, + multi_workspace: Option>, _observe_active_pane: Subscription, - workspace_sidebar_open: bool, - sidebar_has_notifications: bool, - show_sidebar_toggle: bool, } impl Render for StatusBar { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let sidebar = SidebarStatus::query(&self.multi_workspace, cx); + h_flex() .w_full() .justify_between() @@ -50,11 +80,14 @@ impl Render for StatusBar { .map(|el| match window.window_decorations() { Decorations::Server => el, Decorations::Client { tiling, .. } => el - .when(!(tiling.bottom || tiling.right), |el| { - el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) - }) .when( - !(tiling.bottom || tiling.left) && !self.workspace_sidebar_open, + !(tiling.bottom || tiling.right) + && !(sidebar.open && sidebar.side == SidebarSide::Right), + |el| el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING), + ) + .when( + !(tiling.bottom || tiling.left) + && !(sidebar.open && sidebar.side == SidebarSide::Left), |el| el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING), ) // This border is to avoid a transparent gap in the rounded corners @@ -62,44 +95,77 @@ impl Render for StatusBar { .border_b(px(1.0)) .border_color(cx.theme().colors().status_bar_background), }) - .child(self.render_left_tools(cx)) - .child(self.render_right_tools()) + .child(self.render_left_tools(&sidebar, cx)) + .child(self.render_right_tools(&sidebar, cx)) } } impl StatusBar { - fn render_left_tools(&self, cx: &mut Context) -> impl IntoElement { + fn render_left_tools( + &self, + sidebar: &SidebarStatus, + cx: &mut Context, + ) -> 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)), + sidebar.show_toggle && !sidebar.open && sidebar.side == SidebarSide::Left, + |this| this.child(self.render_sidebar_toggle(sidebar, cx)), ) .children(self.left_items.iter().map(|item| item.to_any())) } - fn render_right_tools(&self) -> impl IntoElement { + fn render_right_tools( + &self, + sidebar: &SidebarStatus, + cx: &mut Context, + ) -> impl IntoElement { h_flex() .flex_shrink_0() .gap_1() .overflow_x_hidden() .children(self.right_items.iter().rev().map(|item| item.to_any())) + .when( + sidebar.show_toggle && !sidebar.open && sidebar.side == SidebarSide::Right, + |this| this.child(self.render_sidebar_toggle(sidebar, cx)), + ) } - fn render_sidebar_toggle(&self, cx: &mut Context) -> impl IntoElement { - h_flex() - .gap_0p5() - .child( + fn render_sidebar_toggle( + &self, + sidebar: &SidebarStatus, + cx: &mut Context, + ) -> impl IntoElement { + let on_right = sidebar.side == SidebarSide::Right; + let has_notifications = sidebar.has_notifications; + let indicator_border = cx.theme().colors().status_bar_background; + + let toggle = sidebar_side_context_menu("sidebar-status-toggle-menu", cx) + .anchor(if on_right { + Corner::BottomRight + } else { + Corner::BottomLeft + }) + .attach(if on_right { + Corner::TopRight + } else { + Corner::TopLeft + }) + .trigger(move |_is_active, _window, _cx| { IconButton::new( "toggle-workspace-sidebar", - IconName::ThreadsSidebarLeftClosed, + if on_right { + IconName::ThreadsSidebarRightClosed + } else { + IconName::ThreadsSidebarLeftClosed + }, ) .icon_size(IconSize::Small) - .when(self.sidebar_has_notifications, |this| { + .when(has_notifications, |this| { this.indicator(Indicator::dot().color(Color::Accent)) - .indicator_border_color(Some(cx.theme().colors().status_bar_background)) + .indicator_border_color(Some(indicator_border)) }) .tooltip(move |_, cx| { Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx) @@ -110,41 +176,47 @@ impl StatusBar { multi_workspace.toggle_sidebar(window, cx); }); } - }), - ) - .child(Divider::vertical().color(ui::DividerColor::Border)) + }) + }); + + h_flex() + .gap_0p5() + .when(on_right, |this| { + this.child(Divider::vertical().color(ui::DividerColor::Border)) + }) + .child(toggle) + .when(!on_right, |this| { + this.child(Divider::vertical().color(ui::DividerColor::Border)) + }) } } impl StatusBar { - pub fn new(active_pane: &Entity, window: &mut Window, cx: &mut Context) -> Self { + pub fn new( + active_pane: &Entity, + multi_workspace: Option>, + window: &mut Window, + cx: &mut Context, + ) -> Self { let mut this = Self { left_items: Default::default(), right_items: Default::default(), active_pane: active_pane.clone(), + multi_workspace, _observe_active_pane: cx.observe_in(active_pane, window, |this, _, window, cx| { 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 } - pub fn set_workspace_sidebar_open(&mut self, open: bool, cx: &mut Context) { - self.workspace_sidebar_open = open; - cx.notify(); - } - - pub fn set_sidebar_has_notifications(&mut self, has: bool, cx: &mut Context) { - self.sidebar_has_notifications = has; - cx.notify(); - } - - pub fn set_show_sidebar_toggle(&mut self, show: bool, cx: &mut Context) { - self.show_sidebar_toggle = show; + pub fn set_multi_workspace( + &mut self, + multi_workspace: WeakEntity, + cx: &mut Context, + ) { + self.multi_workspace = Some(multi_workspace); cx.notify(); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d5cb3bf5e63ff333cd050f75536bf8de18ff372e..37cac09863b2251a7c8dc259d3fb1fc68c00c07e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -29,7 +29,7 @@ pub use dock::Panel; pub use multi_workspace::{ CloseWorkspaceSidebar, DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent, NextWorkspace, PreviousWorkspace, Sidebar, SidebarHandle, - ToggleWorkspaceSidebar, + SidebarRenderState, SidebarSide, ToggleWorkspaceSidebar, sidebar_side_context_menu, }; pub use path_list::{PathList, SerializedPathList}; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; @@ -1342,6 +1342,7 @@ pub struct Workspace { removing: bool, _panels_task: Option>>, sidebar_focus_handle: Option, + multi_workspace: Option>, } impl EventEmitter for Workspace {} @@ -1626,8 +1627,13 @@ impl Workspace { let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx)); let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx)); let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx)); + let multi_workspace = window + .root::() + .flatten() + .map(|mw| mw.downgrade()); let status_bar = cx.new(|cx| { - let mut status_bar = StatusBar::new(¢er_pane.clone(), window, cx); + let mut status_bar = + StatusBar::new(¢er_pane.clone(), multi_workspace.clone(), window, cx); status_bar.add_left_item(left_dock_buttons, window, cx); status_bar.add_right_item(right_dock_buttons, window, cx); status_bar.add_right_item(bottom_dock_buttons, window, cx); @@ -1754,6 +1760,7 @@ impl Workspace { last_open_dock_positions: Vec::new(), removing: false, sidebar_focus_handle: None, + multi_workspace, } } @@ -2127,6 +2134,13 @@ impl Workspace { } } + pub fn agent_panel_position(&self, cx: &App) -> Option { + self.all_docks().into_iter().find_map(|dock| { + let dock = dock.read(cx); + dock.has_agent_panel(cx).then_some(dock.position()) + }) + } + pub fn panel_size_state(&self, cx: &App) -> Option { self.all_docks().into_iter().find_map(|dock| { let dock = dock.read(cx); @@ -2327,20 +2341,6 @@ impl Workspace { &self.status_bar } - 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); - }); - } - pub fn set_sidebar_focus_handle(&mut self, handle: Option) { self.sidebar_focus_handle = handle; } @@ -2349,6 +2349,21 @@ impl Workspace { StatusBarSettings::get_global(cx).show } + pub fn multi_workspace(&self) -> Option<&WeakEntity> { + self.multi_workspace.as_ref() + } + + pub fn set_multi_workspace( + &mut self, + multi_workspace: WeakEntity, + cx: &mut App, + ) { + self.status_bar.update(cx, |status_bar, cx| { + status_bar.set_multi_workspace(multi_workspace.clone(), cx); + }); + self.multi_workspace = Some(multi_workspace); + } + pub fn app_state(&self) -> &Arc { &self.app_state }