From f4036fc2fe8be08e006582e4baffe8d29345c549 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Apr 2026 11:26:34 -0700 Subject: [PATCH] Backport subsequent flexible dock PRs to v0.230.x (#53013) Release Notes: - N/A --- assets/settings/default.json | 8 + crates/agent/src/tool_permissions.rs | 1 + crates/agent_settings/src/agent_settings.rs | 2 + crates/agent_ui/src/agent_panel.rs | 15 +- crates/agent_ui/src/agent_ui.rs | 1 + crates/settings/src/vscode_import.rs | 1 + crates/settings_content/src/agent.rs | 8 + crates/settings_content/src/terminal.rs | 4 + crates/settings_ui/src/page_data.rs | 30 +- crates/terminal/src/terminal_settings.rs | 2 + crates/terminal_view/src/terminal_panel.rs | 14 + crates/workspace/src/dock.rs | 291 +++++++++------ crates/workspace/src/workspace.rs | 370 +++++++++++++------- 13 files changed, 510 insertions(+), 237 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 424ecb129d8f524428341550f9933156ced96f19..aab7d16ed6ffed8ff4ab33fc94acb16fa411f3ea 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -943,6 +943,10 @@ "button": true, // Where to dock the agent panel. Can be 'left', 'right' or 'bottom'. "dock": "right", + // Whether the agent panel should use flexible (proportional) sizing. + // + // Default: true + "flexible": true, // 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. @@ -1643,6 +1647,10 @@ "shell": "system", // Where to dock terminals panel. Can be `left`, `right`, `bottom`. "dock": "bottom", + // Whether the terminal panel should use flexible (proportional) sizing. + // + // Default: true + "flexible": true, // Default width when the terminal is docked to the left or right. "default_width": 640, // Default height when the terminal is docked to the bottom. diff --git a/crates/agent/src/tool_permissions.rs b/crates/agent/src/tool_permissions.rs index 345511c5025b25601c630c572980d44a23f724e7..8b9f81e01fce5d469d9e59ea4aa2c163b71d6f01 100644 --- a/crates/agent/src/tool_permissions.rs +++ b/crates/agent/src/tool_permissions.rs @@ -571,6 +571,7 @@ mod tests { enabled: true, button: true, dock: DockPosition::Right, + flexible: true, default_width: px(300.), default_height: px(600.), default_model: None, diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index d5d4f16eb742a92f6abf8081c43709f161ef4038..91d3a6d9c941db157bcda629830730fa1e6a311a 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -26,6 +26,7 @@ pub struct AgentSettings { pub enabled: bool, pub button: bool, pub dock: DockPosition, + pub flexible: bool, pub default_width: Pixels, pub default_height: Pixels, pub default_model: Option, @@ -409,6 +410,7 @@ impl Settings for AgentSettings { dock: agent.dock.unwrap(), default_width: px(agent.default_width.unwrap()), default_height: px(agent.default_height.unwrap()), + flexible: agent.flexible.unwrap(), default_model: Some(agent.default_model.unwrap()), inline_assistant_model: agent.inline_assistant_model, inline_assistant_use_streaming_tools: agent diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index aa88773680faee1dd7b8ceb0d60f93ecc13016c7..2160287be7f23554236db42335ff66eea000b3a3 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -3150,10 +3150,23 @@ impl Panel for AgentPanel { } } - fn supports_flexible_size(&self, _window: &Window, _cx: &App) -> bool { + fn supports_flexible_size(&self) -> bool { true } + fn has_flexible_size(&self, _window: &Window, cx: &App) -> bool { + AgentSettings::get_global(cx).flexible + } + + fn set_flexible_size(&mut self, flexible: bool, _window: &mut Window, cx: &mut Context) { + settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { + settings + .agent + .get_or_insert_default() + .set_flexible_size(flexible); + }); + } + fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context) { if active && matches!(self.active_view, ActiveView::Uninitialized) diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 1817fdbe31a4fed12caf2e5f804461aecf9da973..258fbeb95c60bfb2107e025872086f6c65e4e350 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -636,6 +636,7 @@ mod tests { enabled: true, button: true, dock: DockPosition::Right, + flexible: true, default_width: px(300.), default_height: px(600.), default_model: None, diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 414fef665a8e6841bc43242bd2f0a05147eaea1d..5500c583043d1fd88cb84d5bde6b2a7c9c139bfc 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -880,6 +880,7 @@ impl VsCodeSettings { scroll_multiplier: None, toolbar: None, show_count_badge: None, + flexible: None, }) } diff --git a/crates/settings_content/src/agent.rs b/crates/settings_content/src/agent.rs index 1b71f9b33c58b6980431d25f2af51007ae861a1c..dad4e581fab66fd5b5bfd4bee4984515dc3667fe 100644 --- a/crates/settings_content/src/agent.rs +++ b/crates/settings_content/src/agent.rs @@ -48,6 +48,10 @@ pub struct AgentSettingsContent { /// /// Default: right pub dock: Option, + /// Whether the agent panel should use flexible (proportional) sizing. + /// + /// Default: true + pub flexible: Option, /// Default width in pixels when the agent panel is docked to the left or right. /// /// Default: 640 @@ -157,6 +161,10 @@ impl AgentSettingsContent { self.dock = Some(dock); } + pub fn set_flexible_size(&mut self, flexible: bool) { + self.flexible = Some(flexible); + } + pub fn set_model(&mut self, language_model: LanguageModelSelection) { self.default_model = Some(language_model) } diff --git a/crates/settings_content/src/terminal.rs b/crates/settings_content/src/terminal.rs index 83f3b32fdd14a6ee693f775b74022af4841af0a5..643dea18d106906d242ff21d0aadbc27492fd09b 100644 --- a/crates/settings_content/src/terminal.rs +++ b/crates/settings_content/src/terminal.rs @@ -129,6 +129,10 @@ pub struct TerminalSettingsContent { /// Default: true pub button: Option, pub dock: Option, + /// Whether the terminal panel should use flexible (proportional) sizing. + /// + /// Default: true + pub flexible: Option, /// Default width when the terminal is docked to the left or right. /// /// Default: 640 diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 5fa1679532aa9ad82801e78a929a8bfd59509818..833ff1d193e8b6a1b3f96cf77ade8194457b462c 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -4952,7 +4952,7 @@ fn panels_page() -> SettingsPage { ] } - fn terminal_panel_section() -> [SettingsPageItem; 3] { + fn terminal_panel_section() -> [SettingsPageItem; 4] { [ SettingsPageItem::SectionHeader("Terminal Panel"), SettingsPageItem::SettingItem(SettingItem { @@ -4968,6 +4968,19 @@ fn panels_page() -> SettingsPage { metadata: None, files: USER, }), + SettingsPageItem::SettingItem(SettingItem { + title: "Terminal Panel Flexible Sizing", + description: "Whether the terminal panel should use flexible (proportional) sizing when docked to the left or right.", + field: Box::new(SettingField { + json_path: Some("terminal.flexible"), + pick: |settings_content| settings_content.terminal.as_ref()?.flexible.as_ref(), + write: |settings_content, value| { + settings_content.terminal.get_or_insert_default().flexible = value; + }, + }), + metadata: None, + files: USER, + }), SettingsPageItem::SettingItem(SettingItem { title: "Show Count Badge", description: "Show a badge on the terminal panel icon with the count of open terminals.", @@ -5628,7 +5641,7 @@ fn panels_page() -> SettingsPage { ] } - fn agent_panel_section() -> [SettingsPageItem; 5] { + fn agent_panel_section() -> [SettingsPageItem; 6] { [ SettingsPageItem::SectionHeader("Agent Panel"), SettingsPageItem::SettingItem(SettingItem { @@ -5657,6 +5670,19 @@ fn panels_page() -> SettingsPage { metadata: None, files: USER, }), + SettingsPageItem::SettingItem(SettingItem { + title: "Agent Panel Flexible Sizing", + description: "Whether the agent panel should use flexible (proportional) sizing when docked to the left or right.", + field: Box::new(SettingField { + json_path: Some("agent.flexible"), + pick: |settings_content| settings_content.agent.as_ref()?.flexible.as_ref(), + write: |settings_content, value| { + settings_content.agent.get_or_insert_default().flexible = value; + }, + }), + metadata: None, + files: USER, + }), SettingsPageItem::SettingItem(SettingItem { title: "Agent Panel Default Width", description: "Default width when the agent panel is docked to the left or right.", diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index f24bd5ead6cfd8cb0d4ded66a770a6040d957b72..98d6bc27163c963e41ab61770b2fe9497059e491 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -40,6 +40,7 @@ pub struct TerminalSettings { pub keep_selection_on_copy: bool, pub button: bool, pub dock: TerminalDockPosition, + pub flexible: bool, pub default_width: Pixels, pub default_height: Pixels, pub detect_venv: VenvSettings, @@ -110,6 +111,7 @@ impl settings::Settings for TerminalSettings { dock: user_content.dock.unwrap(), default_width: px(user_content.default_width.unwrap()), default_height: px(user_content.default_height.unwrap()), + flexible: user_content.flexible.unwrap(), detect_venv: project_content.detect_venv.unwrap(), scroll_multiplier: user_content.scroll_multiplier.unwrap(), max_scroll_history_lines: user_content.max_scroll_history_lines, diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 2ecc959663a082aac2d37410639f55e4ff242681..ea70b9ddca0b165679e2b29b7c35a62ba210367f 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1574,6 +1574,20 @@ impl Panel for TerminalPanel { } } + fn supports_flexible_size(&self) -> bool { + true + } + + fn has_flexible_size(&self, _window: &Window, cx: &App) -> bool { + TerminalSettings::get_global(cx).flexible + } + + fn set_flexible_size(&mut self, flexible: bool, _window: &mut Window, cx: &mut Context) { + settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { + settings.terminal.get_or_insert_default().flexible = Some(flexible); + }); + } + fn is_zoomed(&self, _window: &Window, cx: &App) -> bool { self.active_pane.read(cx).is_zoomed() } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 16d31ca699bfc984ceaa49c6bda2e38d66da13f7..576686a670ce01ed7847087c21ac7d009cd32730 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -42,9 +42,19 @@ pub trait Panel: Focusable + EventEmitter + Render + Sized { PanelSizeState::default() } fn size_state_changed(&mut self, _window: &mut Window, _cx: &mut Context) {} - fn supports_flexible_size(&self, _window: &Window, _cx: &App) -> bool { + fn supports_flexible_size(&self) -> bool { false } + fn has_flexible_size(&self, _window: &Window, _cx: &App) -> bool { + false + } + fn set_flexible_size( + &mut self, + _flexible: bool, + _window: &mut Window, + _cx: &mut Context, + ) { + } fn icon(&self, window: &Window, cx: &App) -> Option; fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>; fn toggle_action(&self) -> Box; @@ -86,7 +96,9 @@ pub trait PanelHandle: Send + Sync { fn default_size(&self, window: &Window, cx: &App) -> Pixels; fn initial_size_state(&self, window: &Window, cx: &App) -> PanelSizeState; fn size_state_changed(&self, window: &mut Window, cx: &mut App); - fn supports_flexible_size(&self, window: &Window, cx: &App) -> bool; + fn supports_flexible_size(&self, cx: &App) -> bool; + fn has_flexible_size(&self, window: &Window, cx: &App) -> bool; + fn set_flexible_size(&self, flexible: bool, window: &mut Window, cx: &mut App); fn icon(&self, window: &Window, cx: &App) -> Option; fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>; fn toggle_action(&self, window: &Window, cx: &App) -> Box; @@ -172,8 +184,16 @@ where self.update(cx, |this, cx| this.size_state_changed(window, cx)) } - fn supports_flexible_size(&self, window: &Window, cx: &App) -> bool { - self.read(cx).supports_flexible_size(window, cx) + fn supports_flexible_size(&self, cx: &App) -> bool { + self.read(cx).supports_flexible_size() + } + + fn has_flexible_size(&self, window: &Window, cx: &App) -> bool { + self.read(cx).has_flexible_size(window, cx) + } + + fn set_flexible_size(&self, flexible: bool, window: &mut Window, cx: &mut App) { + self.update(cx, |this, cx| this.set_flexible_size(flexible, window, cx)) } fn icon(&self, window: &Window, cx: &App) -> Option { @@ -284,7 +304,7 @@ impl DockPosition { pub struct PanelSizeState { pub size: Option, #[serde(default)] - pub flexible_size_ratio: Option, + pub flex: Option, } struct PanelEntry { @@ -300,6 +320,25 @@ pub struct PanelButtons { pub(crate) const PANEL_SIZE_STATE_KEY: &str = "dock_panel_size"; +fn resize_panel_entry( + position: DockPosition, + entry: &mut PanelEntry, + size: Option, + flex: Option, + window: &mut Window, + cx: &mut App, +) -> (&'static str, PanelSizeState) { + let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round()); + let use_flex = entry.panel.has_flexible_size(window, cx) && position.axis() == Axis::Horizontal; + if use_flex { + entry.size_state.flex = flex; + } else { + entry.size_state.size = size; + } + entry.panel.size_state_changed(window, cx); + (entry.panel.panel_key(), entry.size_state) +} + impl Dock { pub fn new( position: DockPosition, @@ -762,17 +801,9 @@ impl Dock { } } - pub fn panel_size(&self, panel: &dyn PanelHandle, window: &Window, cx: &App) -> Option { - self.panel_entries - .iter() - .find(|entry| entry.panel.panel_id() == panel.panel_id()) - .map(|entry| self.resolved_panel_size(entry, window, cx)) - } - - pub fn active_panel_size(&self, window: &Window, cx: &App) -> Option { + pub fn active_panel_size(&self) -> Option { if self.is_open { - self.active_panel_entry() - .map(|entry| self.resolved_panel_size(entry, window, cx)) + self.active_panel_entry().map(|entry| entry.size_state) } else { None } @@ -834,28 +865,58 @@ impl Dock { } } + pub fn toggle_panel_flexible_size( + &mut self, + panel: &dyn PanelHandle, + current_size: Option, + current_flex: Option, + window: &mut Window, + cx: &mut Context, + ) { + let Some(entry) = self + .panel_entries + .iter_mut() + .find(|entry| entry.panel.panel_id() == panel.panel_id()) + else { + return; + }; + let currently_flexible = entry.panel.has_flexible_size(window, cx); + if currently_flexible { + entry.size_state.size = current_size; + } else { + entry.size_state.flex = current_flex; + } + let panel_key = entry.panel.panel_key(); + let size_state = entry.size_state; + let workspace = self.workspace.clone(); + entry + .panel + .set_flexible_size(!currently_flexible, window, cx); + entry.panel.size_state_changed(window, cx); + cx.defer(move |cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + workspace.persist_panel_size_state(panel_key, size_state, cx); + }); + } + }); + cx.notify(); + } + pub fn resize_active_panel( &mut self, size: Option, - ratio: Option, + flex: Option, window: &mut Window, cx: &mut Context, ) { if let Some(index) = self.active_panel_index && let Some(entry) = self.panel_entries.get_mut(index) { - let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round()); + let (panel_key, size_state) = + resize_panel_entry(self.position, entry, size, flex, window, cx); - if entry.panel.supports_flexible_size(window, cx) { - entry.size_state.flexible_size_ratio = ratio; - } else { - entry.size_state.size = size; - } - - let panel_key = entry.panel.panel_key(); - let size_state = entry.size_state; let workspace = self.workspace.clone(); - entry.panel.size_state_changed(window, cx); cx.defer(move |cx| { if let Some(workspace) = workspace.upgrade() { workspace.update(cx, |workspace, cx| { @@ -870,22 +931,15 @@ impl Dock { pub fn resize_all_panels( &mut self, size: Option, - ratio: Option, + flex: Option, window: &mut Window, cx: &mut Context, ) { - let mut size_states_to_persist = Vec::new(); - - for entry in &mut self.panel_entries { - let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round()); - if entry.panel.supports_flexible_size(window, cx) { - entry.size_state.flexible_size_ratio = ratio; - } else { - entry.size_state.size = size; - } - entry.panel.size_state_changed(window, cx); - size_states_to_persist.push((entry.panel.panel_key(), entry.size_state)); - } + let size_states_to_persist: Vec<_> = self + .panel_entries + .iter_mut() + .map(|entry| resize_panel_entry(self.position, entry, size, flex, window, cx)) + .collect(); let workspace = self.workspace.clone(); cx.defer(move |cx| { @@ -919,7 +973,8 @@ impl Dock { pub fn clamp_panel_size(&mut self, max_size: Pixels, window: &Window, cx: &mut App) { let max_size = (max_size - RESIZE_HANDLE_SIZE).abs(); for entry in &mut self.panel_entries { - if entry.panel.supports_flexible_size(window, cx) { + let use_flexible = entry.panel.has_flexible_size(window, cx); + if use_flexible { continue; } @@ -933,28 +988,6 @@ impl Dock { } } - fn resolved_panel_size(&self, entry: &PanelEntry, window: &Window, cx: &App) -> Pixels { - if self.position.axis() == Axis::Horizontal - && entry.panel.supports_flexible_size(window, cx) - { - if let Some(workspace) = self.workspace.upgrade() { - let workspace = workspace.read(cx); - return resolve_panel_size( - entry.size_state, - entry.panel.as_ref(), - self.position, - workspace, - window, - cx, - ); - } - } - entry - .size_state - .size - .unwrap_or_else(|| entry.panel.default_size(window, cx)) - } - pub(crate) fn load_persisted_size_state( workspace: &Workspace, panel_key: &'static str, @@ -974,41 +1007,10 @@ impl Dock { } } -pub(crate) fn resolve_panel_size( - size_state: PanelSizeState, - panel: &dyn PanelHandle, - position: DockPosition, - workspace: &Workspace, - window: &Window, - cx: &App, -) -> Pixels { - if position.axis() == Axis::Horizontal && panel.supports_flexible_size(window, cx) { - let ratio = size_state - .flexible_size_ratio - .or_else(|| workspace.default_flexible_dock_ratio(position)); - - if let Some(ratio) = ratio { - return workspace - .flexible_dock_size(position, ratio, window, cx) - .unwrap_or_else(|| { - size_state - .size - .unwrap_or_else(|| panel.default_size(window, cx)) - }); - } - } - - size_state - .size - .unwrap_or_else(|| panel.default_size(window, cx)) -} - impl Render for Dock { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let dispatch_context = Self::dispatch_context(); if let Some(entry) = self.visible_entry() { - let size = self.resolved_panel_size(entry, window, cx); - let position = self.position; let create_resize_handle = || { let handle = div() @@ -1077,8 +1079,10 @@ impl Render for Dock { .border_color(cx.theme().colors().border) .overflow_hidden() .map(|this| match self.position().axis() { - Axis::Horizontal => this.w(size).h_full().flex_row(), - Axis::Vertical => this.h(size).w_full().flex_col(), + // Width and height are always set on the workspace wrapper in + // render_dock, so fill whatever space the wrapper provides. + Axis::Horizontal => this.w_full().h_full().flex_row(), + Axis::Vertical => this.h_full().w_full().flex_col(), }) .map(|this| match self.position() { DockPosition::Left => this.border_r_1(), @@ -1088,8 +1092,8 @@ impl Render for Dock { .child( div() .map(|this| match self.position().axis() { - Axis::Horizontal => this.min_w(size).h_full(), - Axis::Vertical => this.min_h(size).w_full(), + Axis::Horizontal => this.w_full().h_full(), + Axis::Vertical => this.h_full().w_full(), }) .child( entry @@ -1132,6 +1136,8 @@ impl Render for PanelButtons { DockPosition::Bottom | DockPosition::Right => (Corner::BottomRight, Corner::TopRight), }; + let dock_entity = self.dock.clone(); + let workspace = dock.workspace.clone(); let mut buttons: Vec<_> = dock .panel_entries .iter() @@ -1147,6 +1153,10 @@ impl Render for PanelButtons { .log_err()?; let name = entry.panel.persistent_name(); let panel = entry.panel.clone(); + let supports_flexible = panel.supports_flexible_size(cx); + let currently_flexible = panel.has_flexible_size(window, cx); + let dock_for_menu = dock_entity.clone(); + let workspace_for_menu = workspace.clone(); let is_active_button = Some(i) == active_index && is_open; let (action, tooltip) = if is_active_button { @@ -1175,20 +1185,76 @@ impl Render for PanelButtons { ]; ContextMenu::build(window, cx, |mut menu, _, cx| { + let mut has_position_entries = false; for position in POSITIONS { - if position != dock_position - && panel.position_is_valid(position, cx) - { + if panel.position_is_valid(position, cx) { + let is_current = position == dock_position; let panel = panel.clone(); - menu = menu.entry( + menu = menu.toggleable_entry( format!("Dock {}", position.label()), + is_current, + IconPosition::Start, None, move |window, cx| { - panel.set_position(position, window, cx); + if !is_current { + panel.set_position(position, window, cx); + } }, - ) + ); + has_position_entries = true; } } + if supports_flexible { + if has_position_entries { + menu = menu.separator(); + } + let panel_for_flex = panel.clone(); + let dock_for_flex = dock_for_menu.clone(); + let workspace_for_flex = workspace_for_menu.clone(); + menu = menu.toggleable_entry( + "Flex Width", + currently_flexible, + IconPosition::Start, + None, + move |window, cx| { + if !currently_flexible { + if let Some(ws) = workspace_for_flex.upgrade() { + ws.update(cx, |workspace, cx| { + workspace.toggle_dock_panel_flexible_size( + &dock_for_flex, + panel_for_flex.as_ref(), + window, + cx, + ); + }); + } + } + }, + ); + let panel_for_fixed = panel.clone(); + let dock_for_fixed = dock_for_menu.clone(); + let workspace_for_fixed = workspace_for_menu.clone(); + menu = menu.toggleable_entry( + "Fixed Width", + !currently_flexible, + IconPosition::Start, + None, + move |window, cx| { + if currently_flexible { + if let Some(ws) = workspace_for_fixed.upgrade() { + ws.update(cx, |workspace, cx| { + workspace.toggle_dock_panel_flexible_size( + &dock_for_fixed, + panel_for_fixed.as_ref(), + window, + cx, + ); + }); + } + } + }, + ); + } menu }) }) @@ -1335,14 +1401,27 @@ pub mod test { fn initial_size_state(&self, _window: &Window, _: &App) -> PanelSizeState { PanelSizeState { size: None, - flexible_size_ratio: None, + flex: None, } } - fn supports_flexible_size(&self, _window: &Window, _: &App) -> bool { + fn supports_flexible_size(&self) -> bool { self.flexible } + fn has_flexible_size(&self, _window: &Window, _: &App) -> bool { + self.flexible + } + + fn set_flexible_size( + &mut self, + flexible: bool, + _window: &mut Window, + _cx: &mut Context, + ) { + self.flexible = flexible; + } + fn icon(&self, _window: &Window, _: &App) -> Option { None } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d5cb3bf5e63ff333cd050f75536bf8de18ff372e..86c7fce3b1c8edd5be258b9d421bf57864dc1488 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -148,7 +148,7 @@ pub use workspace_settings::{ }; use zed_actions::{Spawn, feedback::FileBugReport, theme::ToggleMode}; -use crate::{item::ItemBufferKind, notifications::NotificationId}; +use crate::{dock::PanelSizeState, item::ItemBufferKind, notifications::NotificationId}; use crate::{ persistence::{ SerializedAxis, @@ -2193,33 +2193,63 @@ impl Workspace { did_set } - pub fn flexible_dock_size( + pub fn toggle_dock_panel_flexible_size( &self, - position: DockPosition, - ratio: f32, - window: &Window, - cx: &App, - ) -> Option { - if position.axis() != Axis::Horizontal { - return None; - } - - let available_width = self.available_width_for_horizontal_dock(position, window, cx)?; - Some((available_width * ratio.clamp(0.0, 1.0)).max(RESIZE_HANDLE_SIZE)) + dock: &Entity, + panel: &dyn PanelHandle, + window: &mut Window, + cx: &mut App, + ) { + let position = dock.read(cx).position(); + let current_size = self.dock_size(&dock.read(cx), window, cx); + let current_flex = + current_size.and_then(|size| self.dock_flex_for_size(position, size, window, cx)); + dock.update(cx, |dock, cx| { + dock.toggle_panel_flexible_size(panel, current_size, current_flex, window, cx); + }); } - pub fn resolved_dock_panel_size( - &self, - dock: &Dock, - panel: &dyn PanelHandle, - window: &Window, - cx: &App, - ) -> Pixels { - let size_state = dock.stored_panel_size_state(panel).unwrap_or_default(); - dock::resolve_panel_size(size_state, panel, dock.position(), self, window, cx) + fn dock_size(&self, dock: &Dock, window: &Window, cx: &App) -> Option { + let panel = dock.active_panel()?; + let size_state = dock + .stored_panel_size_state(panel.as_ref()) + .unwrap_or_default(); + let position = dock.position(); + + let use_flex = panel.has_flexible_size(window, cx); + + if position.axis() == Axis::Horizontal + && use_flex + && let Some(flex) = size_state.flex.or_else(|| self.default_dock_flex(position)) + { + let workspace_width = self.bounds.size.width; + if workspace_width <= Pixels::ZERO { + return None; + } + let flex = flex.max(0.001); + let opposite = self.opposite_dock_panel_and_size_state(position, window, cx); + if let Some(opposite_flex) = opposite.as_ref().and_then(|(_, s)| s.flex) { + // Both docks are flex items sharing the full workspace width. + let total_flex = flex + 1.0 + opposite_flex; + return Some((flex / total_flex * workspace_width).max(RESIZE_HANDLE_SIZE)); + } else { + // Opposite dock is fixed-width; flex items share (W - fixed). + let opposite_fixed = opposite + .map(|(panel, s)| s.size.unwrap_or_else(|| panel.default_size(window, cx))) + .unwrap_or_default(); + let available = (workspace_width - opposite_fixed).max(RESIZE_HANDLE_SIZE); + return Some((flex / (flex + 1.0) * available).max(RESIZE_HANDLE_SIZE)); + } + } + + Some( + size_state + .size + .unwrap_or_else(|| panel.default_size(window, cx)), + ) } - pub fn flexible_dock_ratio_for_size( + pub fn dock_flex_for_size( &self, position: DockPosition, size: Pixels, @@ -2230,45 +2260,55 @@ impl Workspace { return None; } - let available_width = self.available_width_for_horizontal_dock(position, window, cx)?; - let available_width = available_width.max(RESIZE_HANDLE_SIZE); - Some((size / available_width).clamp(0.0, 1.0)) + let workspace_width = self.bounds.size.width; + if workspace_width <= Pixels::ZERO { + return None; + } + + let opposite = self.opposite_dock_panel_and_size_state(position, window, cx); + if let Some(opposite_flex) = opposite.as_ref().and_then(|(_, s)| s.flex) { + let size = size.clamp(px(0.), workspace_width - px(1.)); + Some((size * (1.0 + opposite_flex) / (workspace_width - size)).max(0.0)) + } else { + let opposite_width = opposite + .map(|(panel, s)| s.size.unwrap_or_else(|| panel.default_size(window, cx))) + .unwrap_or_default(); + let available = (workspace_width - opposite_width).max(RESIZE_HANDLE_SIZE); + let remaining = (available - size).max(px(1.)); + Some((size / remaining).max(0.0)) + } } - fn available_width_for_horizontal_dock( + fn opposite_dock_panel_and_size_state( &self, position: DockPosition, window: &Window, cx: &App, - ) -> Option { - let workspace_width = self.bounds.size.width; - if workspace_width <= Pixels::ZERO { - return None; - } - + ) -> Option<(Arc, PanelSizeState)> { let opposite_position = match position { DockPosition::Left => DockPosition::Right, DockPosition::Right => DockPosition::Left, DockPosition::Bottom => return None, }; - let opposite_width = self - .dock_at_position(opposite_position) - .read(cx) - .stored_active_panel_size(window, cx) - .unwrap_or(Pixels::ZERO); - - Some((workspace_width - opposite_width).max(RESIZE_HANDLE_SIZE)) + let opposite_dock = self.dock_at_position(opposite_position).read(cx); + let panel = opposite_dock.visible_panel()?; + let mut size_state = opposite_dock + .stored_panel_size_state(panel.as_ref()) + .unwrap_or_default(); + if size_state.flex.is_none() && panel.has_flexible_size(window, cx) { + size_state.flex = self.default_dock_flex(opposite_position); + } + Some((panel.clone(), size_state)) } - pub fn default_flexible_dock_ratio(&self, position: DockPosition) -> Option { + pub fn default_dock_flex(&self, position: DockPosition) -> Option { if position.axis() != Axis::Horizontal { return None; } let pane = self.last_active_center_pane.clone()?.upgrade()?; - let pane_fraction = self.center.width_fraction_for_pane(&pane).unwrap_or(1.0); - Some((pane_fraction / (1.0 + pane_fraction)).clamp(0.0, 1.0)) + Some(self.center.width_fraction_for_pane(&pane).unwrap_or(1.0)) } pub fn is_edited(&self) -> bool { @@ -2294,7 +2334,7 @@ impl Workspace { load_legacy_panel_size(T::panel_key(), dock_position, self, cx).map(|size| { let state = dock::PanelSizeState { size: Some(size), - flexible_size_ratio: None, + flex: None, }; self.persist_panel_size_state(T::panel_key(), state, cx); state @@ -4892,10 +4932,7 @@ impl Workspace { if let Some(dock_entity) = active_dock { let dock = dock_entity.read(cx); - let Some(panel_size) = dock - .active_panel() - .map(|panel| self.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx)) - else { + let Some(panel_size) = self.dock_size(&dock, window, cx) else { return; }; match dock.position() { @@ -7258,14 +7295,49 @@ impl Workspace { leader_border_for_pane(follower_states, &pane, window, cx) }); - Some( - div() - .flex() - .flex_none() - .overflow_hidden() - .child(dock.clone()) - .children(leader_border), - ) + let mut container = div() + .flex() + .overflow_hidden() + .flex_none() + .child(dock.clone()) + .children(leader_border); + + // Apply sizing only when the dock is open. When closed the dock is still + // included in the element tree so its focus handle remains mounted — without + // this, toggle_panel_focus cannot focus the panel when the dock is closed. + let dock = dock.read(cx); + if let Some(panel) = dock.visible_panel() { + let size_state = dock.stored_panel_size_state(panel.as_ref()); + if position.axis() == Axis::Horizontal { + let use_flexible = panel.has_flexible_size(window, cx); + let flex_grow = if use_flexible { + size_state + .and_then(|state| state.flex) + .or_else(|| self.default_dock_flex(position)) + } else { + None + }; + if let Some(grow) = flex_grow { + let grow = grow.max(0.001); + let style = container.style(); + style.flex_grow = Some(grow); + style.flex_shrink = Some(1.0); + style.flex_basis = Some(relative(0.).into()); + } else { + let size = size_state + .and_then(|state| state.size) + .unwrap_or_else(|| panel.default_size(window, cx)); + container = container.w(size); + } + } else { + let size = size_state + .and_then(|state| state.size) + .unwrap_or_else(|| panel.default_size(window, cx)); + container = container.h(size); + } + } + + Some(container) } pub fn for_window(window: &Window, cx: &App) -> Option> { @@ -7335,18 +7407,17 @@ impl Workspace { } } - fn adjust_dock_size_by_px( + fn resize_dock( &mut self, - panel_size: Pixels, dock_pos: DockPosition, - px: Pixels, + new_size: Pixels, window: &mut Window, cx: &mut Context, ) { match dock_pos { - DockPosition::Left => self.resize_left_dock(panel_size + px, window, cx), - DockPosition::Right => self.resize_right_dock(panel_size + px, window, cx), - DockPosition::Bottom => self.resize_bottom_dock(panel_size + px, window, cx), + DockPosition::Left => self.resize_left_dock(new_size, window, cx), + DockPosition::Right => self.resize_right_dock(new_size, window, cx), + DockPosition::Bottom => self.resize_bottom_dock(new_size, window, cx), } } @@ -7363,15 +7434,15 @@ impl Workspace { } }); - let ratio = self.flexible_dock_ratio_for_size(DockPosition::Left, size, window, cx); + let flex_grow = self.dock_flex_for_size(DockPosition::Left, size, window, cx); self.left_dock.update(cx, |left_dock, cx| { if WorkspaceSettings::get_global(cx) .resize_all_panels_in_dock .contains(&DockPosition::Left) { - left_dock.resize_all_panels(Some(size), ratio, window, cx); + left_dock.resize_all_panels(Some(size), flex_grow, window, cx); } else { - left_dock.resize_active_panel(Some(size), ratio, window, cx); + left_dock.resize_active_panel(Some(size), flex_grow, window, cx); } }); } @@ -7387,15 +7458,15 @@ impl Workspace { size = workspace_width - left_dock_size } }); - let ratio = self.flexible_dock_ratio_for_size(DockPosition::Right, size, window, cx); + let flex_grow = self.dock_flex_for_size(DockPosition::Right, size, window, cx); self.right_dock.update(cx, |right_dock, cx| { if WorkspaceSettings::get_global(cx) .resize_all_panels_in_dock .contains(&DockPosition::Right) { - right_dock.resize_all_panels(Some(size), ratio, window, cx); + right_dock.resize_all_panels(Some(size), flex_grow, window, cx); } else { - right_dock.resize_active_panel(Some(size), ratio, window, cx); + right_dock.resize_active_panel(Some(size), flex_grow, window, cx); } }); } @@ -7790,14 +7861,10 @@ fn adjust_active_dock_size_by_px( return; }; let dock = active_dock.read(cx); - let Some(panel_size) = dock - .active_panel() - .map(|panel| workspace.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx)) - else { + let Some(panel_size) = workspace.dock_size(&dock, window, cx) else { return; }; - let dock_pos = dock.position(); - workspace.adjust_dock_size_by_px(panel_size, dock_pos, px, window, cx); + workspace.resize_dock(dock.position(), panel_size + px, window, cx); } fn adjust_open_docks_size_by_px( @@ -7812,22 +7879,18 @@ fn adjust_open_docks_size_by_px( .filter_map(|dock_entity| { let dock = dock_entity.read(cx); if dock.is_open() { - let panel_size = dock.active_panel().map(|panel| { - workspace.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx) - })?; let dock_pos = dock.position(); - Some((panel_size, dock_pos, px)) + let panel_size = workspace.dock_size(&dock, window, cx)?; + Some((dock_pos, panel_size + px)) } else { None } }) .collect::>(); - docks - .into_iter() - .for_each(|(panel_size, dock_pos, offset)| { - workspace.adjust_dock_size_by_px(panel_size, dock_pos, offset, window, cx); - }); + for (position, new_size) in docks { + workspace.resize_dock(position, new_size, window, cx); + } } impl Focusable for Workspace { @@ -12270,11 +12333,8 @@ mod tests { let dock = workspace.right_dock().read(cx); let workspace_width = workspace.bounds.size.width; - let initial_width = dock - .active_panel() - .map(|panel| { - workspace.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx) - }) + let initial_width = workspace + .dock_size(&dock, window, cx) .expect("flexible dock should have an initial width"); assert_eq!(initial_width, workspace_width / 2.); @@ -12282,11 +12342,8 @@ mod tests { workspace.resize_right_dock(px(300.), window, cx); let dock = workspace.right_dock().read(cx); - let resized_width = dock - .active_panel() - .map(|panel| { - workspace.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx) - }) + let resized_width = workspace + .dock_size(&dock, window, cx) .expect("flexible dock should keep its resized width"); assert_eq!(resized_width, px(300.)); @@ -12306,9 +12363,8 @@ mod tests { workspace.toggle_dock(DockPosition::Right, window, cx); let dock = workspace.right_dock().read(cx); - let reopened_width = dock - .active_panel() - .map(|panel| workspace.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx)) + let reopened_width = workspace + .dock_size(&dock, window, cx) .expect("flexible dock should restore when reopened"); assert_eq!(reopened_width, resized_width); @@ -12321,8 +12377,11 @@ mod tests { assert_eq!( right_dock .stored_panel_size_state(flexible_panel.as_ref()) - .and_then(|size_state| size_state.flexible_size_ratio), - Some(resized_width.to_f64() as f32 / workspace.bounds.size.width.to_f64() as f32) + .and_then(|size_state| size_state.flex), + Some( + resized_width.to_f64() as f32 + / (workspace.bounds.size.width - resized_width).to_f64() as f32 + ) ); }); @@ -12335,9 +12394,8 @@ mod tests { ); let dock = workspace.right_dock().read(cx); - let split_width = dock - .active_panel() - .map(|panel| workspace.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx)) + let split_width = workspace + .dock_size(&dock, window, cx) .expect("flexible dock should keep its user-resized proportion"); assert_eq!(split_width, px(300.)); @@ -12345,9 +12403,8 @@ mod tests { workspace.bounds.size.width = px(1600.); let dock = workspace.right_dock().read(cx); - let resized_window_width = dock - .active_panel() - .map(|panel| workspace.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx)) + let resized_window_width = workspace + .dock_size(&dock, window, cx) .expect("flexible dock should preserve proportional size on window resize"); assert_eq!( @@ -12458,9 +12515,7 @@ mod tests { persisted.size, None, "flexible panel should not persist a redundant pixel size" ); - let original_ratio = persisted - .flexible_size_ratio - .expect("flexible panel ratio should be persisted"); + let original_ratio = persisted.flex.expect("panel's flex should be persisted"); // Remove the panel and re-add: both size and ratio should be restored. workspace.update_in(cx, |workspace, window, cx| { @@ -12481,9 +12536,9 @@ mod tests { "re-added flexible panel should not have a persisted pixel size" ); assert_eq!( - size_state.flexible_size_ratio, + size_state.flex, Some(original_ratio), - "re-added flexible panel should restore persisted ratio" + "re-added flexible panel should restore persisted flex" ); }); } @@ -12517,9 +12572,8 @@ mod tests { workspace.toggle_dock(DockPosition::Left, window, cx); let left_dock = workspace.left_dock().read(cx); - let left_width = left_dock - .active_panel() - .map(|p| workspace.resolved_dock_panel_size(&left_dock, p.as_ref(), window, cx)) + let left_width = workspace + .dock_size(&left_dock, window, cx) .expect("left dock should have an active panel"); assert_eq!( @@ -12541,9 +12595,8 @@ mod tests { ); let left_dock = workspace.left_dock().read(cx); - let left_width = left_dock - .active_panel() - .map(|p| workspace.resolved_dock_panel_size(&left_dock, p.as_ref(), window, cx)) + let left_width = workspace + .dock_size(&left_dock, window, cx) .expect("left dock should still have an active panel after vertical split"); assert_eq!( @@ -12562,15 +12615,13 @@ mod tests { workspace.toggle_dock(DockPosition::Right, window, cx); let right_dock = workspace.right_dock().read(cx); - let right_width = right_dock - .active_panel() - .map(|p| workspace.resolved_dock_panel_size(&right_dock, p.as_ref(), window, cx)) + let right_width = workspace + .dock_size(&right_dock, window, cx) .expect("right dock should have an active panel"); let left_dock = workspace.left_dock().read(cx); - let left_width = left_dock - .active_panel() - .map(|p| workspace.resolved_dock_panel_size(&left_dock, p.as_ref(), window, cx)) + let left_width = workspace + .dock_size(&left_dock, window, cx) .expect("left dock should still have an active panel"); let available_width = workspace.bounds.size.width - right_width; @@ -12580,6 +12631,64 @@ mod tests { "flexible left panel should shrink proportionally as the right dock takes space" ); }); + + // Step 4: Toggle the right dock's panel to flexible. Now both docks use + // flex sizing and the workspace width is divided among left-flex, center + // (implicit flex 1.0), and right-flex. + workspace.update_in(cx, |workspace, window, cx| { + let right_dock = workspace.right_dock().clone(); + let right_panel = right_dock + .read(cx) + .visible_panel() + .expect("right dock should have a visible panel") + .clone(); + workspace.toggle_dock_panel_flexible_size( + &right_dock, + right_panel.as_ref(), + window, + cx, + ); + + let right_dock = right_dock.read(cx); + let right_panel = right_dock + .visible_panel() + .expect("right dock should still have a visible panel"); + assert!( + right_panel.has_flexible_size(window, cx), + "right panel should now be flexible" + ); + + let right_size_state = right_dock + .stored_panel_size_state(right_panel.as_ref()) + .expect("right panel should have a stored size state after toggling"); + let right_flex = right_size_state + .flex + .expect("right panel should have a flex value after toggling"); + + let left_dock = workspace.left_dock().read(cx); + let left_width = workspace + .dock_size(&left_dock, window, cx) + .expect("left dock should still have an active panel"); + let right_width = workspace + .dock_size(&right_dock, window, cx) + .expect("right dock should still have an active panel"); + + let left_flex = workspace + .default_dock_flex(DockPosition::Left) + .expect("left dock should have a default flex"); + + let total_flex = left_flex + 1.0 + right_flex; + let expected_left = left_flex / total_flex * workspace.bounds.size.width; + let expected_right = right_flex / total_flex * workspace.bounds.size.width; + assert_eq!( + left_width, expected_left, + "flexible left panel should share workspace width via flex ratios" + ); + assert_eq!( + right_width, expected_right, + "flexible right panel should share workspace width via flex ratios" + ); + }); } struct TestModal(FocusHandle); @@ -12634,8 +12743,8 @@ mod tests { panel_1.panel_id() ); assert_eq!( - left_dock.read(cx).active_panel_size(window, cx).unwrap(), - px(300.) + workspace.dock_size(&left_dock.read(cx), window, cx), + Some(px(300.)) ); workspace.resize_left_dock(px(1337.), window, cx); @@ -12668,7 +12777,12 @@ mod tests { panel_1.panel_id() ); assert_eq!( - right_dock.read(cx).active_panel_size(window, cx).unwrap(), + right_dock + .read(cx) + .active_panel_size() + .unwrap() + .size + .unwrap(), px(1337.) ); @@ -12706,8 +12820,8 @@ mod tests { panel_1.panel_id() ); assert_eq!( - left_dock.read(cx).active_panel_size(window, cx).unwrap(), - px(1337.) + workspace.dock_size(&left_dock.read(cx), window, cx), + Some(px(1337.)) ); // And the right dock should be closed as it no longer has any panels. assert!(!workspace.right_dock().read(cx).is_open()); @@ -12723,8 +12837,8 @@ mod tests { // since the panel orientation changed from vertical to horizontal. let bottom_dock = workspace.bottom_dock(); assert_eq!( - bottom_dock.read(cx).active_panel_size(window, cx).unwrap(), - px(300.), + workspace.dock_size(&bottom_dock.read(cx), window, cx), + Some(px(300.)) ); // Close bottom dock and move panel_1 back to the left. bottom_dock.update(cx, |bottom_dock, cx| {