Backport subsequent flexible dock PRs to v0.230.x (#53013)

Max Brunsfeld created

Release Notes:

- N/A

Change summary

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(-)

Detailed changes

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.

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,

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<LanguageModelSelection>,
@@ -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

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<Self>) {
+        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<Self>) {
         if active
             && matches!(self.active_view, ActiveView::Uninitialized)

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,

crates/settings_content/src/agent.rs 🔗

@@ -48,6 +48,10 @@ pub struct AgentSettingsContent {
     ///
     /// Default: right
     pub dock: Option<DockPosition>,
+    /// Whether the agent panel should use flexible (proportional) sizing.
+    ///
+    /// Default: true
+    pub flexible: Option<bool>,
     /// 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)
     }

crates/settings_content/src/terminal.rs 🔗

@@ -129,6 +129,10 @@ pub struct TerminalSettingsContent {
     /// Default: true
     pub button: Option<bool>,
     pub dock: Option<TerminalDockPosition>,
+    /// Whether the terminal panel should use flexible (proportional) sizing.
+    ///
+    /// Default: true
+    pub flexible: Option<bool>,
     /// Default width when the terminal is docked to the left or right.
     ///
     /// Default: 640

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.",

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,

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<Self>) {
+        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()
     }

crates/workspace/src/dock.rs 🔗

@@ -42,9 +42,19 @@ pub trait Panel: Focusable + EventEmitter<PanelEvent> + Render + Sized {
         PanelSizeState::default()
     }
     fn size_state_changed(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
-    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<Self>,
+    ) {
+    }
     fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName>;
     fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>;
     fn toggle_action(&self) -> Box<dyn Action>;
@@ -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<ui::IconName>;
     fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>;
     fn toggle_action(&self, window: &Window, cx: &App) -> Box<dyn Action>;
@@ -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<ui::IconName> {
@@ -284,7 +304,7 @@ impl DockPosition {
 pub struct PanelSizeState {
     pub size: Option<Pixels>,
     #[serde(default)]
-    pub flexible_size_ratio: Option<f32>,
+    pub flex: Option<f32>,
 }
 
 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<Pixels>,
+    flex: Option<f32>,
+    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<Pixels> {
-        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<Pixels> {
+    pub fn active_panel_size(&self) -> Option<PanelSizeState> {
         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<Pixels>,
+        current_flex: Option<f32>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        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<Pixels>,
-        ratio: Option<f32>,
+        flex: Option<f32>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         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<Pixels>,
-        ratio: Option<f32>,
+        flex: Option<f32>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        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<Self>) -> impl IntoElement {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> 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>,
+        ) {
+            self.flexible = flexible;
+        }
+
         fn icon(&self, _window: &Window, _: &App) -> Option<ui::IconName> {
             None
         }

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<Pixels> {
-        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<Dock>,
+        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<Pixels> {
+        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<Pixels> {
-        let workspace_width = self.bounds.size.width;
-        if workspace_width <= Pixels::ZERO {
-            return None;
-        }
-
+    ) -> Option<(Arc<dyn PanelHandle>, 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<f32> {
+    pub fn default_dock_flex(&self, position: DockPosition) -> Option<f32> {
         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<Entity<Workspace>> {
@@ -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<Self>,
     ) {
         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::<Vec<_>>();
 
-    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| {