Avoid flicker in flexible width agent panel's size when resizing workspace (#52519)

Max Brunsfeld created

This improves the rendering of flexible-width panels so that they do not
lag behind by one frame when tracking workspace size changes. I've also
simplified the code for panel size management in the workspace.

Release Notes:

- N/A

Change summary

crates/workspace/src/dock.rs      |  77 +-----------
crates/workspace/src/workspace.rs | 197 +++++++++++++++++---------------
2 files changed, 113 insertions(+), 161 deletions(-)

Detailed changes

crates/workspace/src/dock.rs 🔗

@@ -776,17 +776,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
         }
@@ -947,28 +939,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,
@@ -988,41 +958,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()
@@ -1091,8 +1030,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(),
@@ -1102,8 +1043,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

crates/workspace/src/workspace.rs 🔗

@@ -2208,30 +2208,29 @@ impl Workspace {
         did_set
     }
 
-    pub fn flexible_dock_size(
-        &self,
-        position: DockPosition,
-        ratio: f32,
-        window: &Window,
-        cx: &App,
-    ) -> Option<Pixels> {
-        if position.axis() != Axis::Horizontal {
-            return None;
+    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();
+
+        if position.axis() == Axis::Horizontal
+            && panel.supports_flexible_size(window, cx)
+            && let Some(ratio) = size_state
+                .flexible_size_ratio
+                .or_else(|| self.default_flexible_dock_ratio(position))
+            && let Some(available_width) =
+                self.available_width_for_horizontal_dock(position, window, cx)
+        {
+            return Some((available_width * ratio.clamp(0.0, 1.0)).max(RESIZE_HANDLE_SIZE));
         }
 
-        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))
-    }
-
-    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)
+        Some(
+            size_state
+                .size
+                .unwrap_or_else(|| panel.default_size(window, cx)),
+        )
     }
 
     pub fn flexible_dock_ratio_for_size(
@@ -4908,10 +4907,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() {
@@ -7274,14 +7270,46 @@ 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 {
+                if let Some(ratio) = size_state
+                    .and_then(|state| state.flexible_size_ratio)
+                    .or_else(|| self.default_flexible_dock_ratio(position))
+                    && panel.supports_flexible_size(window, cx)
+                {
+                    let ratio = ratio.clamp(0.001, 0.999);
+                    let grow = ratio / (1.0 - ratio);
+                    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>> {
@@ -7351,18 +7379,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),
         }
     }
 
@@ -7806,14 +7833,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(
@@ -7828,22 +7851,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 {
@@ -12286,11 +12305,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.);
@@ -12298,11 +12314,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.));
@@ -12322,9 +12335,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);
@@ -12351,9 +12363,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.));
@@ -12361,9 +12372,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!(
@@ -12533,9 +12543,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!(
@@ -12557,9 +12566,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!(
@@ -12578,15 +12586,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;
@@ -12650,8 +12656,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);
@@ -12684,7 +12690,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.)
             );
 
@@ -12722,8 +12733,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());
@@ -12739,8 +12750,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| {