Preserve panel size when re-docking between left and right

Nathan Sobo created

Change summary

crates/gpui/src/elements.rs               |  7 --
crates/gpui/src/elements/resizable.rs     | 45 ++++++++++++---------
crates/project_panel/src/project_panel.rs |  8 +--
crates/settings/src/settings.rs           |  5 +
crates/workspace/src/dock.rs              | 42 ++++++++++++++++++--
crates/workspace/src/workspace.rs         | 51 ++++++++++++++++++++++--
6 files changed, 117 insertions(+), 41 deletions(-)

Detailed changes

crates/gpui/src/elements.rs πŸ”—

@@ -196,12 +196,7 @@ pub trait Element<V: View>: 'static {
     where
         Self: 'static + Sized,
     {
-        Resizable::new(
-            self.into_any(),
-            side,
-            size,
-            on_resize
-       )
+        Resizable::new(self.into_any(), side, size, on_resize)
     }
 }
 

crates/gpui/src/elements/resizable.rs πŸ”—

@@ -7,8 +7,8 @@ use crate::{
     geometry::rect::RectF,
     platform::{CursorStyle, MouseButton},
     scene::MouseDrag,
-    AnyElement, Axis, Element, LayoutContext, MouseRegion, SceneBuilder, View,
-    ViewContext, SizeConstraint,
+    AnyElement, Axis, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View,
+    ViewContext,
 };
 
 #[derive(Copy, Clone, Debug)]
@@ -73,7 +73,7 @@ pub struct Resizable<V: View> {
     child: AnyElement<V>,
     handle_side: HandleSide,
     handle_size: f32,
-    on_resize: Rc<RefCell<dyn FnMut(&mut V, f32, &mut ViewContext<V>)>>
+    on_resize: Rc<RefCell<dyn FnMut(&mut V, f32, &mut ViewContext<V>)>>,
 }
 
 const DEFAULT_HANDLE_SIZE: f32 = 4.0;
@@ -83,7 +83,7 @@ impl<V: View> Resizable<V> {
         child: AnyElement<V>,
         handle_side: HandleSide,
         size: f32,
-        on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>)
+        on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>),
     ) -> Self {
         let child = match handle_side.axis() {
             Axis::Horizontal => child.constrained().with_max_width(size),
@@ -133,22 +133,29 @@ impl<V: View> Element<V> for Resizable<V> {
 
         enum ResizeHandle {}
         scene.push_mouse_region(
-            MouseRegion::new::<ResizeHandle>(cx.view_id(), self.handle_side as usize, handle_region)
-                .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
-                .on_drag(MouseButton::Left, {
-                    let bounds = bounds.clone();
-                    let side = self.handle_side;
-                    let prev_size = side.relevant_component(bounds.size());
-                    let min_size = side.relevant_component(constraint.min);
-                    let max_size = side.relevant_component(constraint.max);
-                    let on_resize = self.on_resize.clone();
-                    move |event, view: &mut V, cx| {
-                        let new_size = min_size.max(prev_size + side.compute_delta(event)).min(max_size).round();
-                        if new_size != prev_size {
-                            on_resize.borrow_mut()(view, new_size, cx);
-                        }
+            MouseRegion::new::<ResizeHandle>(
+                cx.view_id(),
+                self.handle_side as usize,
+                handle_region,
+            )
+            .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
+            .on_drag(MouseButton::Left, {
+                let bounds = bounds.clone();
+                let side = self.handle_side;
+                let prev_size = side.relevant_component(bounds.size());
+                let min_size = side.relevant_component(constraint.min);
+                let max_size = side.relevant_component(constraint.max);
+                let on_resize = self.on_resize.clone();
+                move |event, view: &mut V, cx| {
+                    let new_size = min_size
+                        .max(prev_size + side.compute_delta(event))
+                        .min(max_size)
+                        .round();
+                    if new_size != prev_size {
+                        on_resize.borrow_mut()(view, new_size, cx);
                     }
-                }),
+                }
+            }),
         );
 
         scene.push_cursor_region(crate::CursorRegion {

crates/project_panel/src/project_panel.rs πŸ”—

@@ -12,8 +12,8 @@ use gpui::{
     geometry::vector::Vector2F,
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton, PromptLevel},
-    AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext,
-    ViewHandle, WeakViewHandle,
+    AnyElement, AppContext, Axis, ClipboardItem, Element, Entity, ModelHandle, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
@@ -1347,9 +1347,7 @@ impl Entity for ProjectPanel {
 impl workspace::dock::Panel for ProjectPanel {
     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
         let settings = cx.global::<Settings>();
-        match settings
-            .project_panel
-            .dock {
+        match settings.project_panel.dock {
             settings::ProjectPanelDockPosition::Left => DockPosition::Left,
             settings::ProjectPanelDockPosition::Right => DockPosition::Right,
         }

crates/settings/src/settings.rs πŸ”—

@@ -599,7 +599,10 @@ impl Settings {
         }
         self.editor_overrides = data.editor;
         merge(&mut self.project_panel.dock, data.project_panel.dock);
-        merge(&mut self.project_panel.default_width, data.project_panel.default_width);
+        merge(
+            &mut self.project_panel.default_width,
+            data.project_panel.default_width,
+        );
         self.git_overrides = data.git.unwrap_or_default();
         self.journal_overrides = data.journal;
         self.terminal_defaults.font_size = data.terminal.font_size;

crates/workspace/src/dock.rs πŸ”—

@@ -2,7 +2,8 @@ use crate::{StatusItemView, Workspace};
 use context_menu::{ContextMenu, ContextMenuItem};
 use gpui::{
     elements::*, impl_actions, platform::CursorStyle, platform::MouseButton, AnyViewHandle,
-    AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+    AppContext, Axis, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
+    WindowContext,
 };
 use serde::Deserialize;
 use settings::Settings;
@@ -117,6 +118,13 @@ impl DockPosition {
             Self::Right => HandleSide::Left,
         }
     }
+
+    pub fn axis(&self) -> Axis {
+        match self {
+            Self::Left | Self::Right => Axis::Horizontal,
+            Self::Bottom => Axis::Vertical,
+        }
+    }
 }
 
 struct PanelEntry {
@@ -247,6 +255,23 @@ impl Dock {
         }
     }
 
+    pub fn panel_size(&self, panel: &dyn PanelHandle) -> Option<f32> {
+        self.panel_entries
+            .iter()
+            .find(|entry| entry.panel.id() == panel.id())
+            .map(|entry| entry.size)
+    }
+
+    pub fn resize_panel(&mut self, panel: &dyn PanelHandle, size: f32) {
+        let entry = self
+            .panel_entries
+            .iter_mut()
+            .find(|entry| entry.panel.id() == panel.id());
+        if let Some(entry) = entry {
+            entry.size = size;
+        }
+    }
+
     pub fn active_panel_size(&self) -> Option<f32> {
         if self.is_open {
             self.panel_entries
@@ -281,9 +306,13 @@ impl View for Dock {
             ChildView::new(active_panel.as_any(), cx)
                 .contained()
                 .with_style(style.container)
-                .resizable(self.position.to_resize_handle_side(), size, |dock: &mut Self, size, cx|  {
-                    dock.resize_active_panel(size, cx);
-                })
+                .resizable(
+                    self.position.to_resize_handle_side(),
+                    size,
+                    |dock: &mut Self, size, cx| {
+                        dock.resize_active_panel(size, cx);
+                    },
+                )
                 .into_any()
         } else {
             Empty::new().into_any()
@@ -487,7 +516,10 @@ pub(crate) mod test {
         }
 
         fn default_size(&self, _: &WindowContext) -> f32 {
-            300.
+            match self.position.axis() {
+                Axis::Horizontal => 300.,
+                Axis::Vertical => 200.,
+            }
         }
 
         fn icon_path(&self) -> &'static str {

crates/workspace/src/workspace.rs πŸ”—

@@ -852,10 +852,18 @@ impl Workspace {
 
         cx.subscribe(&panel, {
             let mut dock = dock.clone();
+            let mut prev_position = panel.position(cx);
             move |this, panel, event, cx| {
                 if T::should_change_position_on_event(event) {
+                    let new_position = panel.read(cx).position(cx);
                     let mut was_visible = false;
+                    let mut size = None;
                     dock.update(cx, |dock, cx| {
+                        if new_position.axis() == prev_position.axis() {
+                            size = dock.panel_size(&panel);
+                        }
+                        prev_position = new_position;
+
                         was_visible = dock.is_open()
                             && dock
                                 .active_panel()
@@ -869,7 +877,11 @@ impl Workspace {
                     }
                     .clone();
                     dock.update(cx, |dock, cx| {
-                        dock.add_panel(panel, cx);
+                        dock.add_panel(panel.clone(), cx);
+                        if let Some(size) = size {
+                            dock.resize_panel(&panel, size);
+                        }
+
                         if was_visible {
                             dock.set_open(true, cx);
                             dock.activate_panel(dock.panels_len() - 1, cx);
@@ -3678,10 +3690,17 @@ mod tests {
                 .right_dock()
                 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
 
+            let left_dock = workspace.left_dock();
             assert_eq!(
-                workspace.left_dock().read(cx).active_panel().unwrap().id(),
+                left_dock.read(cx).active_panel().unwrap().id(),
                 panel_1.id()
             );
+            assert_eq!(
+                left_dock.read(cx).active_panel_size().unwrap(),
+                panel_1.default_size(cx)
+            );
+
+            left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
             assert_eq!(
                 workspace.right_dock().read(cx).active_panel().unwrap().id(),
                 panel_2.id()
@@ -3700,10 +3719,12 @@ mod tests {
             // Since it was the only panel on the left, the left dock should now be closed.
             assert!(!workspace.left_dock().read(cx).is_open());
             assert!(workspace.left_dock().read(cx).active_panel().is_none());
+            let right_dock = workspace.right_dock();
             assert_eq!(
-                workspace.right_dock().read(cx).active_panel().unwrap().id(),
+                right_dock.read(cx).active_panel().unwrap().id(),
                 panel_1.id()
             );
+            assert_eq!(right_dock.read(cx).active_panel_size().unwrap(), 1337.);
 
             // Now we move panel_2Β to the left
             panel_2.set_position(DockPosition::Left, cx);
@@ -3727,13 +3748,33 @@ mod tests {
 
         workspace.update(cx, |workspace, cx| {
             // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
-            assert!(workspace.left_dock().read(cx).is_open());
+            let left_dock = workspace.left_dock();
+            assert!(left_dock.read(cx).is_open());
             assert_eq!(
-                workspace.left_dock().read(cx).active_panel().unwrap().id(),
+                left_dock.read(cx).active_panel().unwrap().id(),
                 panel_1.id()
             );
+            assert_eq!(left_dock.read(cx).active_panel_size().unwrap(), 1337.);
             // And right the dock should be closed as it no longer has any panels.
             assert!(!workspace.right_dock().read(cx).is_open());
+
+            // Now we move panel_1 to the bottom
+            panel_1.set_position(DockPosition::Bottom, cx);
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            // Since panel_1 was visible on the left, we close the left dock.
+            assert!(!workspace.left_dock().read(cx).is_open());
+            // The bottom dock is sized based on the panel's default size,
+            // since the panel orientation changed from vertical to horizontal.
+            assert_eq!(
+                workspace
+                    .bottom_dock()
+                    .read(cx)
+                    .active_panel_size()
+                    .unwrap(),
+                panel_1.default_size(cx),
+            );
         });
     }
 }