WIP: Resizing splits (#2715)

Mikayla Maki created

We're finally doing the thing. 


TODO:
- [x] Choose an approach 
- Decided to add a new element just for the pane axis, containing a
slimmed down copy of the flex code.
- [x] Wire through callbacks and pointers so that data goes where it
needs to
- [x] Do the flex juggling math on resize
- [x] Update the flexes when updating the split tree
- [x] Restore the active_pane_magnification setting
- [x] Serialize an axis' flexes

Release Notes:
- Made the center pane group splits resizable. Note that resizing is
disabled if the `active_pane_magnification` setting is changed from
default.

Change summary

crates/editor/src/element.rs              |   6 
crates/gpui/src/app/window.rs             |  13 
crates/gpui/src/gpui.rs                   |   2 
crates/workspace/src/pane_group.rs        | 451 ++++++++++++++++++++++--
crates/workspace/src/persistence.rs       | 155 +++++--
crates/workspace/src/persistence/model.rs |  16 
crates/workspace/src/workspace.rs         |  21 
7 files changed, 558 insertions(+), 106 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -1182,8 +1182,10 @@ impl EditorElement {
         });
         scene.push_mouse_region(
             MouseRegion::new::<ScrollbarMouseHandlers>(cx.view_id(), cx.view_id(), track_bounds)
-                .on_move(move |_, editor: &mut Editor, cx| {
-                    editor.scroll_manager.show_scrollbar(cx);
+                .on_move(move |event, editor: &mut Editor, cx| {
+                    if event.pressed_button.is_none() {
+                        editor.scroll_manager.show_scrollbar(cx);
+                    }
                 })
                 .on_down(MouseButton::Left, {
                     let row_range = row_range.clone();

crates/gpui/src/app/window.rs 🔗

@@ -1268,6 +1268,19 @@ impl Vector2FExt for Vector2F {
     }
 }
 
+pub trait RectFExt {
+    fn length_along(self, axis: Axis) -> f32;
+}
+
+impl RectFExt for RectF {
+    fn length_along(self, axis: Axis) -> f32 {
+        match axis {
+            Axis::Horizontal => self.width(),
+            Axis::Vertical => self.height(),
+        }
+    }
+}
+
 #[derive(Copy, Clone, Debug)]
 pub struct SizeConstraint {
     pub min: Vector2F,

crates/gpui/src/gpui.rs 🔗

@@ -27,7 +27,7 @@ pub mod json;
 pub mod keymap_matcher;
 pub mod platform;
 pub use gpui_macros::{test, Element};
-pub use window::{Axis, SizeConstraint, Vector2FExt, WindowContext};
+pub use window::{Axis, RectFExt, SizeConstraint, Vector2FExt, WindowContext};
 
 pub use anyhow;
 pub use serde_json;

crates/workspace/src/pane_group.rs 🔗

@@ -1,6 +1,8 @@
-use std::sync::Arc;
+use std::{cell::RefCell, rc::Rc, sync::Arc};
 
-use crate::{AppState, FollowerStatesByLeader, Pane, Workspace, WorkspaceSettings};
+use crate::{
+    pane_group::element::PaneAxisElement, AppState, FollowerStatesByLeader, Pane, Workspace,
+};
 use anyhow::{anyhow, Result};
 use call::{ActiveCall, ParticipantLocation};
 use gpui::{
@@ -13,7 +15,11 @@ use project::Project;
 use serde::Deserialize;
 use theme::Theme;
 
-#[derive(Clone, Debug, Eq, PartialEq)]
+const HANDLE_HITBOX_SIZE: f32 = 4.0;
+const HORIZONTAL_MIN_SIZE: f32 = 80.;
+const VERTICAL_MIN_SIZE: f32 = 100.;
+
+#[derive(Clone, Debug, PartialEq)]
 pub struct PaneGroup {
     pub(crate) root: Member,
 }
@@ -77,6 +83,7 @@ impl PaneGroup {
     ) -> AnyElement<Workspace> {
         self.root.render(
             project,
+            0,
             theme,
             follower_states,
             active_call,
@@ -94,7 +101,7 @@ impl PaneGroup {
     }
 }
 
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
 pub(crate) enum Member {
     Axis(PaneAxis),
     Pane(ViewHandle<Pane>),
@@ -119,7 +126,7 @@ impl Member {
             Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)],
         };
 
-        Member::Axis(PaneAxis { axis, members })
+        Member::Axis(PaneAxis::new(axis, members))
     }
 
     fn contains(&self, needle: &ViewHandle<Pane>) -> bool {
@@ -132,6 +139,7 @@ impl Member {
     pub fn render(
         &self,
         project: &ModelHandle<Project>,
+        basis: usize,
         theme: &Theme,
         follower_states: &FollowerStatesByLeader,
         active_call: Option<&ModelHandle<ActiveCall>>,
@@ -272,6 +280,7 @@ impl Member {
             }
             Member::Axis(axis) => axis.render(
                 project,
+                basis + 1,
                 theme,
                 follower_states,
                 active_call,
@@ -295,13 +304,35 @@ impl Member {
     }
 }
 
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
 pub(crate) struct PaneAxis {
     pub axis: Axis,
     pub members: Vec<Member>,
+    pub flexes: Rc<RefCell<Vec<f32>>>,
 }
 
 impl PaneAxis {
+    pub fn new(axis: Axis, members: Vec<Member>) -> Self {
+        let flexes = Rc::new(RefCell::new(vec![1.; members.len()]));
+        Self {
+            axis,
+            members,
+            flexes,
+        }
+    }
+
+    pub fn load(axis: Axis, members: Vec<Member>, flexes: Option<Vec<f32>>) -> Self {
+        let flexes = flexes.unwrap_or_else(|| vec![1.; members.len()]);
+        debug_assert!(members.len() == flexes.len());
+
+        let flexes = Rc::new(RefCell::new(flexes));
+        Self {
+            axis,
+            members,
+            flexes,
+        }
+    }
+
     fn split(
         &mut self,
         old_pane: &ViewHandle<Pane>,
@@ -323,6 +354,7 @@ impl PaneAxis {
                             }
 
                             self.members.insert(idx, Member::Pane(new_pane.clone()));
+                            *self.flexes.borrow_mut() = vec![1.; self.members.len()];
                         } else {
                             *member =
                                 Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
@@ -362,6 +394,7 @@ impl PaneAxis {
         if found_pane {
             if let Some(idx) = remove_member {
                 self.members.remove(idx);
+                *self.flexes.borrow_mut() = vec![1.; self.members.len()];
             }
 
             if self.members.len() == 1 {
@@ -377,6 +410,7 @@ impl PaneAxis {
     fn render(
         &self,
         project: &ModelHandle<Project>,
+        basis: usize,
         theme: &Theme,
         follower_state: &FollowerStatesByLeader,
         active_call: Option<&ModelHandle<ActiveCall>>,
@@ -385,40 +419,50 @@ impl PaneAxis {
         app_state: &Arc<AppState>,
         cx: &mut ViewContext<Workspace>,
     ) -> AnyElement<Workspace> {
-        let last_member_ix = self.members.len() - 1;
-        Flex::new(self.axis)
-            .with_children(self.members.iter().enumerate().map(|(ix, member)| {
-                let mut flex = 1.0;
-                if member.contains(active_pane) {
-                    flex = settings::get::<WorkspaceSettings>(cx).active_pane_magnification;
-                }
+        debug_assert!(self.members.len() == self.flexes.borrow().len());
 
-                let mut member = member.render(
-                    project,
-                    theme,
-                    follower_state,
-                    active_call,
-                    active_pane,
-                    zoomed,
-                    app_state,
-                    cx,
-                );
-                if ix < last_member_ix {
-                    let mut border = theme.workspace.pane_divider;
-                    border.left = false;
-                    border.right = false;
-                    border.top = false;
-                    border.bottom = false;
-                    match self.axis {
-                        Axis::Vertical => border.bottom = true,
-                        Axis::Horizontal => border.right = true,
-                    }
-                    member = member.contained().with_border(border).into_any();
+        let mut pane_axis = PaneAxisElement::new(self.axis, basis, self.flexes.clone());
+        let mut active_pane_ix = None;
+
+        let mut members = self.members.iter().enumerate().peekable();
+        while let Some((ix, member)) = members.next() {
+            let last = members.peek().is_none();
+
+            if member.contains(active_pane) {
+                active_pane_ix = Some(ix);
+            }
+
+            let mut member = member.render(
+                project,
+                (basis + ix) * 10,
+                theme,
+                follower_state,
+                active_call,
+                active_pane,
+                zoomed,
+                app_state,
+                cx,
+            );
+
+            if !last {
+                let mut border = theme.workspace.pane_divider;
+                border.left = false;
+                border.right = false;
+                border.top = false;
+                border.bottom = false;
+
+                match self.axis {
+                    Axis::Vertical => border.bottom = true,
+                    Axis::Horizontal => border.right = true,
                 }
 
-                FlexItem::new(member).flex(flex, true)
-            }))
-            .into_any()
+                member = member.contained().with_border(border).into_any();
+            }
+
+            pane_axis = pane_axis.with_child(member.into_any());
+        }
+        pane_axis.set_active_pane(active_pane_ix);
+        pane_axis.into_any()
     }
 }
 
@@ -474,3 +518,336 @@ impl SplitDirection {
         }
     }
 }
+
+mod element {
+    use std::{cell::RefCell, ops::Range, rc::Rc};
+
+    use gpui::{
+        geometry::{
+            rect::RectF,
+            vector::{vec2f, Vector2F},
+        },
+        json::{self, ToJson},
+        platform::{CursorStyle, MouseButton},
+        AnyElement, Axis, CursorRegion, Element, LayoutContext, MouseRegion, RectFExt,
+        SceneBuilder, SizeConstraint, Vector2FExt, ViewContext,
+    };
+
+    use crate::{
+        pane_group::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE},
+        Workspace, WorkspaceSettings,
+    };
+
+    pub struct PaneAxisElement {
+        axis: Axis,
+        basis: usize,
+        active_pane_ix: Option<usize>,
+        flexes: Rc<RefCell<Vec<f32>>>,
+        children: Vec<AnyElement<Workspace>>,
+    }
+
+    impl PaneAxisElement {
+        pub fn new(axis: Axis, basis: usize, flexes: Rc<RefCell<Vec<f32>>>) -> Self {
+            Self {
+                axis,
+                basis,
+                flexes,
+                active_pane_ix: None,
+                children: Default::default(),
+            }
+        }
+
+        pub fn set_active_pane(&mut self, active_pane_ix: Option<usize>) {
+            self.active_pane_ix = active_pane_ix;
+        }
+
+        fn layout_children(
+            &mut self,
+            active_pane_magnification: f32,
+            constraint: SizeConstraint,
+            remaining_space: &mut f32,
+            remaining_flex: &mut f32,
+            cross_axis_max: &mut f32,
+            view: &mut Workspace,
+            cx: &mut LayoutContext<Workspace>,
+        ) {
+            let flexes = self.flexes.borrow();
+            let cross_axis = self.axis.invert();
+            for (ix, child) in self.children.iter_mut().enumerate() {
+                let flex = if active_pane_magnification != 1. {
+                    if let Some(active_pane_ix) = self.active_pane_ix {
+                        if ix == active_pane_ix {
+                            active_pane_magnification
+                        } else {
+                            1.
+                        }
+                    } else {
+                        1.
+                    }
+                } else {
+                    flexes[ix]
+                };
+
+                let child_size = if *remaining_flex == 0.0 {
+                    *remaining_space
+                } else {
+                    let space_per_flex = *remaining_space / *remaining_flex;
+                    space_per_flex * flex
+                };
+
+                let child_constraint = match self.axis {
+                    Axis::Horizontal => SizeConstraint::new(
+                        vec2f(child_size, constraint.min.y()),
+                        vec2f(child_size, constraint.max.y()),
+                    ),
+                    Axis::Vertical => SizeConstraint::new(
+                        vec2f(constraint.min.x(), child_size),
+                        vec2f(constraint.max.x(), child_size),
+                    ),
+                };
+                let child_size = child.layout(child_constraint, view, cx);
+                *remaining_space -= child_size.along(self.axis);
+                *remaining_flex -= flex;
+                *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
+            }
+        }
+    }
+
+    impl Extend<AnyElement<Workspace>> for PaneAxisElement {
+        fn extend<T: IntoIterator<Item = AnyElement<Workspace>>>(&mut self, children: T) {
+            self.children.extend(children);
+        }
+    }
+
+    impl Element<Workspace> for PaneAxisElement {
+        type LayoutState = f32;
+        type PaintState = ();
+
+        fn layout(
+            &mut self,
+            constraint: SizeConstraint,
+            view: &mut Workspace,
+            cx: &mut LayoutContext<Workspace>,
+        ) -> (Vector2F, Self::LayoutState) {
+            debug_assert!(self.children.len() == self.flexes.borrow().len());
+
+            let active_pane_magnification =
+                settings::get::<WorkspaceSettings>(cx).active_pane_magnification;
+
+            let mut remaining_flex = 0.;
+
+            if active_pane_magnification != 1. {
+                let active_pane_flex = self
+                    .active_pane_ix
+                    .map(|_| active_pane_magnification)
+                    .unwrap_or(1.);
+                remaining_flex += self.children.len() as f32 - 1. + active_pane_flex;
+            } else {
+                for flex in self.flexes.borrow().iter() {
+                    remaining_flex += flex;
+                }
+            }
+
+            let mut cross_axis_max: f32 = 0.0;
+            let mut remaining_space = constraint.max_along(self.axis);
+
+            if remaining_space.is_infinite() {
+                panic!("flex contains flexible children but has an infinite constraint along the flex axis");
+            }
+
+            self.layout_children(
+                active_pane_magnification,
+                constraint,
+                &mut remaining_space,
+                &mut remaining_flex,
+                &mut cross_axis_max,
+                view,
+                cx,
+            );
+
+            let mut size = match self.axis {
+                Axis::Horizontal => vec2f(constraint.max.x() - remaining_space, cross_axis_max),
+                Axis::Vertical => vec2f(cross_axis_max, constraint.max.y() - remaining_space),
+            };
+
+            if constraint.min.x().is_finite() {
+                size.set_x(size.x().max(constraint.min.x()));
+            }
+            if constraint.min.y().is_finite() {
+                size.set_y(size.y().max(constraint.min.y()));
+            }
+
+            if size.x() > constraint.max.x() {
+                size.set_x(constraint.max.x());
+            }
+            if size.y() > constraint.max.y() {
+                size.set_y(constraint.max.y());
+            }
+
+            (size, remaining_space)
+        }
+
+        fn paint(
+            &mut self,
+            scene: &mut SceneBuilder,
+            bounds: RectF,
+            visible_bounds: RectF,
+            remaining_space: &mut Self::LayoutState,
+            view: &mut Workspace,
+            cx: &mut ViewContext<Workspace>,
+        ) -> Self::PaintState {
+            let can_resize = settings::get::<WorkspaceSettings>(cx).active_pane_magnification == 1.;
+            let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
+
+            let overflowing = *remaining_space < 0.;
+            if overflowing {
+                scene.push_layer(Some(visible_bounds));
+            }
+
+            let mut child_origin = bounds.origin();
+
+            let mut children_iter = self.children.iter_mut().enumerate().peekable();
+            while let Some((ix, child)) = children_iter.next() {
+                let child_start = child_origin.clone();
+                child.paint(scene, child_origin, visible_bounds, view, cx);
+
+                match self.axis {
+                    Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
+                    Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
+                }
+
+                if let Some(Some((next_ix, next_child))) = can_resize.then(|| children_iter.peek())
+                {
+                    scene.push_stacking_context(None, None);
+
+                    let handle_origin = match self.axis {
+                        Axis::Horizontal => child_origin - vec2f(HANDLE_HITBOX_SIZE / 2., 0.0),
+                        Axis::Vertical => child_origin - vec2f(0.0, HANDLE_HITBOX_SIZE / 2.),
+                    };
+
+                    let handle_bounds = match self.axis {
+                        Axis::Horizontal => RectF::new(
+                            handle_origin,
+                            vec2f(HANDLE_HITBOX_SIZE, visible_bounds.height()),
+                        ),
+                        Axis::Vertical => RectF::new(
+                            handle_origin,
+                            vec2f(visible_bounds.width(), HANDLE_HITBOX_SIZE),
+                        ),
+                    };
+
+                    let style = match self.axis {
+                        Axis::Horizontal => CursorStyle::ResizeLeftRight,
+                        Axis::Vertical => CursorStyle::ResizeUpDown,
+                    };
+
+                    scene.push_cursor_region(CursorRegion {
+                        bounds: handle_bounds,
+                        style,
+                    });
+
+                    let axis = self.axis;
+                    let child_size = child.size();
+                    let next_child_size = next_child.size();
+                    let drag_bounds = visible_bounds.clone();
+                    let flexes = self.flexes.clone();
+                    let current_flex = flexes.borrow()[ix];
+                    let next_ix = *next_ix;
+                    let next_flex = flexes.borrow()[next_ix];
+                    enum ResizeHandle {}
+                    let mut mouse_region = MouseRegion::new::<ResizeHandle>(
+                        cx.view_id(),
+                        self.basis + ix,
+                        handle_bounds,
+                    );
+                    mouse_region = mouse_region.on_drag(
+                        MouseButton::Left,
+                        move |drag, workspace: &mut Workspace, cx| {
+                            let min_size = match axis {
+                                Axis::Horizontal => HORIZONTAL_MIN_SIZE,
+                                Axis::Vertical => VERTICAL_MIN_SIZE,
+                            };
+                            // Don't allow resizing to less than the minimum size, if elements are already too small
+                            if min_size - 1. > child_size.along(axis)
+                                || min_size - 1. > next_child_size.along(axis)
+                            {
+                                return;
+                            }
+
+                            let mut current_target_size = (drag.position - child_start).along(axis);
+
+                            let proposed_current_pixel_change =
+                                current_target_size - child_size.along(axis);
+
+                            if proposed_current_pixel_change < 0. {
+                                current_target_size = f32::max(current_target_size, min_size);
+                            } else if proposed_current_pixel_change > 0. {
+                                // TODO: cascade this change to other children if current item is at min size
+                                let next_target_size = f32::max(
+                                    next_child_size.along(axis) - proposed_current_pixel_change,
+                                    min_size,
+                                );
+                                current_target_size = f32::min(
+                                    current_target_size,
+                                    child_size.along(axis) + next_child_size.along(axis)
+                                        - next_target_size,
+                                );
+                            }
+
+                            let current_pixel_change = current_target_size - child_size.along(axis);
+                            let flex_change = current_pixel_change / drag_bounds.length_along(axis);
+                            let current_target_flex = current_flex + flex_change;
+                            let next_target_flex = next_flex - flex_change;
+
+                            let mut borrow = flexes.borrow_mut();
+                            *borrow.get_mut(ix).unwrap() = current_target_flex;
+                            *borrow.get_mut(next_ix).unwrap() = next_target_flex;
+
+                            workspace.schedule_serialize(cx);
+                            cx.notify();
+                        },
+                    );
+                    scene.push_mouse_region(mouse_region);
+
+                    scene.pop_stacking_context();
+                }
+            }
+
+            if overflowing {
+                scene.pop_layer();
+            }
+        }
+
+        fn rect_for_text_range(
+            &self,
+            range_utf16: Range<usize>,
+            _: RectF,
+            _: RectF,
+            _: &Self::LayoutState,
+            _: &Self::PaintState,
+            view: &Workspace,
+            cx: &ViewContext<Workspace>,
+        ) -> Option<RectF> {
+            self.children
+                .iter()
+                .find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
+        }
+
+        fn debug(
+            &self,
+            bounds: RectF,
+            _: &Self::LayoutState,
+            _: &Self::PaintState,
+            view: &Workspace,
+            cx: &ViewContext<Workspace>,
+        ) -> json::Value {
+            serde_json::json!({
+                "type": "PaneAxis",
+                "bounds": bounds.to_json(),
+                "axis": self.axis.to_json(),
+                "flexes": *self.flexes.borrow(),
+                "children": self.children.iter().map(|child| child.debug(view, cx)).collect::<Vec<json::Value>>()
+            })
+        }
+    }
+}

crates/workspace/src/persistence.rs 🔗

@@ -45,6 +45,7 @@ define_connection! {
     //   parent_group_id: Option<usize>, // None indicates that this is the root node
     //   position: Optiopn<usize>, // None indicates that this is the root node
     //   axis: Option<Axis>, // 'Vertical', 'Horizontal'
+    //   flexes: Option<Vec<f32>>, // A JSON array of floats
     // )
     //
     // panes(
@@ -168,7 +169,12 @@ define_connection! {
         ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
         ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
         ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
-    )];
+    ),
+    // Add pane group flex data
+    sql!(
+        ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
+    )
+    ];
 }
 
 impl WorkspaceDb {
@@ -359,38 +365,51 @@ impl WorkspaceDb {
         group_id: Option<GroupId>,
     ) -> Result<Vec<SerializedPaneGroup>> {
         type GroupKey = (Option<GroupId>, WorkspaceId);
-        type GroupOrPane = (Option<GroupId>, Option<Axis>, Option<PaneId>, Option<bool>);
+        type GroupOrPane = (
+            Option<GroupId>,
+            Option<Axis>,
+            Option<PaneId>,
+            Option<bool>,
+            Option<String>,
+        );
         self.select_bound::<GroupKey, GroupOrPane>(sql!(
-            SELECT group_id, axis, pane_id, active
+            SELECT group_id, axis, pane_id, active, flexes
                 FROM (SELECT
-                    group_id,
-                    axis,
-                    NULL as pane_id,
-                    NULL as active,
-                    position,
-                    parent_group_id,
-                    workspace_id
-                    FROM pane_groups
+                        group_id,
+                        axis,
+                        NULL as pane_id,
+                        NULL as active,
+                        position,
+                        parent_group_id,
+                        workspace_id,
+                        flexes
+                      FROM pane_groups
                     UNION
-                    SELECT
-                    NULL,
-                    NULL,
-                    center_panes.pane_id,
-                    panes.active as active,
-                    position,
-                    parent_group_id,
-                    panes.workspace_id as workspace_id
-                        FROM center_panes
-                        JOIN panes ON center_panes.pane_id = panes.pane_id)
+                      SELECT
+                        NULL,
+                        NULL,
+                        center_panes.pane_id,
+                        panes.active as active,
+                        position,
+                        parent_group_id,
+                        panes.workspace_id as workspace_id,
+                        NULL
+                      FROM center_panes
+                      JOIN panes ON center_panes.pane_id = panes.pane_id)
                 WHERE parent_group_id IS ? AND workspace_id = ?
                 ORDER BY position
         ))?((group_id, workspace_id))?
         .into_iter()
-        .map(|(group_id, axis, pane_id, active)| {
+        .map(|(group_id, axis, pane_id, active, flexes)| {
             if let Some((group_id, axis)) = group_id.zip(axis) {
+                let flexes = flexes
+                    .map(|flexes| serde_json::from_str::<Vec<f32>>(&flexes))
+                    .transpose()?;
+
                 Ok(SerializedPaneGroup::Group {
                     axis,
                     children: self.get_pane_group(workspace_id, Some(group_id))?,
+                    flexes,
                 })
             } else if let Some((pane_id, active)) = pane_id.zip(active) {
                 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
@@ -417,14 +436,34 @@ impl WorkspaceDb {
         parent: Option<(GroupId, usize)>,
     ) -> Result<()> {
         match pane_group {
-            SerializedPaneGroup::Group { axis, children } => {
+            SerializedPaneGroup::Group {
+                axis,
+                children,
+                flexes,
+            } => {
                 let (parent_id, position) = unzip_option(parent);
 
+                let flex_string = flexes
+                    .as_ref()
+                    .map(|flexes| serde_json::json!(flexes).to_string());
+
                 let group_id = conn.select_row_bound::<_, i64>(sql!(
-                    INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis)
-                    VALUES (?, ?, ?, ?)
+                    INSERT INTO pane_groups(
+                        workspace_id,
+                        parent_group_id,
+                        position,
+                        axis,
+                        flexes
+                    )
+                    VALUES (?, ?, ?, ?, ?)
                     RETURNING group_id
-                ))?((workspace_id, parent_id, position, *axis))?
+                ))?((
+                    workspace_id,
+                    parent_id,
+                    position,
+                    *axis,
+                    flex_string,
+                ))?
                 .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
 
                 for (position, group) in children.iter().enumerate() {
@@ -641,6 +680,14 @@ mod tests {
         assert_eq!(test_text_1, "test-text-1");
     }
 
+    fn group(axis: gpui::Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
+        SerializedPaneGroup::Group {
+            axis,
+            flexes: None,
+            children,
+        }
+    }
+
     #[gpui::test]
     async fn test_full_workspace_serialization() {
         env_logger::try_init().ok();
@@ -652,12 +699,12 @@ mod tests {
         //  | - - - |       |
         //  | 3,4   |       |
         //  -----------------
-        let center_group = SerializedPaneGroup::Group {
-            axis: gpui::Axis::Horizontal,
-            children: vec![
-                SerializedPaneGroup::Group {
-                    axis: gpui::Axis::Vertical,
-                    children: vec![
+        let center_group = group(
+            gpui::Axis::Horizontal,
+            vec![
+                group(
+                    gpui::Axis::Vertical,
+                    vec![
                         SerializedPaneGroup::Pane(SerializedPane::new(
                             vec![
                                 SerializedItem::new("Terminal", 5, false),
@@ -673,7 +720,7 @@ mod tests {
                             false,
                         )),
                     ],
-                },
+                ),
                 SerializedPaneGroup::Pane(SerializedPane::new(
                     vec![
                         SerializedItem::new("Terminal", 9, false),
@@ -682,7 +729,7 @@ mod tests {
                     false,
                 )),
             ],
-        };
+        );
 
         let workspace = SerializedWorkspace {
             id: 5,
@@ -811,12 +858,12 @@ mod tests {
         //  | - - - |       |
         //  | 3,4   |       |
         //  -----------------
-        let center_pane = SerializedPaneGroup::Group {
-            axis: gpui::Axis::Horizontal,
-            children: vec![
-                SerializedPaneGroup::Group {
-                    axis: gpui::Axis::Vertical,
-                    children: vec![
+        let center_pane = group(
+            gpui::Axis::Horizontal,
+            vec![
+                group(
+                    gpui::Axis::Vertical,
+                    vec![
                         SerializedPaneGroup::Pane(SerializedPane::new(
                             vec![
                                 SerializedItem::new("Terminal", 1, false),
@@ -832,7 +879,7 @@ mod tests {
                             true,
                         )),
                     ],
-                },
+                ),
                 SerializedPaneGroup::Pane(SerializedPane::new(
                     vec![
                         SerializedItem::new("Terminal", 5, true),
@@ -841,7 +888,7 @@ mod tests {
                     false,
                 )),
             ],
-        };
+        );
 
         let workspace = default_workspace(&["/tmp"], &center_pane);
 
@@ -858,12 +905,12 @@ mod tests {
 
         let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
 
-        let center_pane = SerializedPaneGroup::Group {
-            axis: gpui::Axis::Horizontal,
-            children: vec![
-                SerializedPaneGroup::Group {
-                    axis: gpui::Axis::Vertical,
-                    children: vec![
+        let center_pane = group(
+            gpui::Axis::Horizontal,
+            vec![
+                group(
+                    gpui::Axis::Vertical,
+                    vec![
                         SerializedPaneGroup::Pane(SerializedPane::new(
                             vec![
                                 SerializedItem::new("Terminal", 1, false),
@@ -879,7 +926,7 @@ mod tests {
                             true,
                         )),
                     ],
-                },
+                ),
                 SerializedPaneGroup::Pane(SerializedPane::new(
                     vec![
                         SerializedItem::new("Terminal", 5, false),
@@ -888,7 +935,7 @@ mod tests {
                     false,
                 )),
             ],
-        };
+        );
 
         let id = &["/tmp"];
 
@@ -896,9 +943,9 @@ mod tests {
 
         db.save_workspace(workspace.clone()).await;
 
-        workspace.center_group = SerializedPaneGroup::Group {
-            axis: gpui::Axis::Vertical,
-            children: vec![
+        workspace.center_group = group(
+            gpui::Axis::Vertical,
+            vec![
                 SerializedPaneGroup::Pane(SerializedPane::new(
                     vec![
                         SerializedItem::new("Terminal", 1, false),
@@ -914,7 +961,7 @@ mod tests {
                     true,
                 )),
             ],
-        };
+        );
 
         db.save_workspace(workspace.clone()).await;
 

crates/workspace/src/persistence/model.rs 🔗

@@ -127,10 +127,11 @@ impl Bind for DockData {
     }
 }
 
-#[derive(Debug, PartialEq, Eq, Clone)]
+#[derive(Debug, PartialEq, Clone)]
 pub enum SerializedPaneGroup {
     Group {
         axis: Axis,
+        flexes: Option<Vec<f32>>,
         children: Vec<SerializedPaneGroup>,
     },
     Pane(SerializedPane),
@@ -149,7 +150,7 @@ impl Default for SerializedPaneGroup {
 impl SerializedPaneGroup {
     #[async_recursion(?Send)]
     pub(crate) async fn deserialize(
-        &self,
+        self,
         project: &ModelHandle<Project>,
         workspace_id: WorkspaceId,
         workspace: &WeakViewHandle<Workspace>,
@@ -160,7 +161,11 @@ impl SerializedPaneGroup {
         Vec<Option<Box<dyn ItemHandle>>>,
     )> {
         match self {
-            SerializedPaneGroup::Group { axis, children } => {
+            SerializedPaneGroup::Group {
+                axis,
+                children,
+                flexes,
+            } => {
                 let mut current_active_pane = None;
                 let mut members = Vec::new();
                 let mut items = Vec::new();
@@ -184,10 +189,7 @@ impl SerializedPaneGroup {
                 }
 
                 Some((
-                    Member::Axis(PaneAxis {
-                        axis: *axis,
-                        members,
-                    }),
+                    Member::Axis(PaneAxis::load(axis, members, flexes)),
                     current_active_pane,
                     items,
                 ))

crates/workspace/src/workspace.rs 🔗

@@ -1,8 +1,4 @@
 pub mod dock;
-/// NOTE: Focus only 'takes' after an update has flushed_effects.
-///
-/// This may cause issues when you're trying to write tests that use workspace focus to add items at
-/// specific locations.
 pub mod item;
 pub mod notifications;
 pub mod pane;
@@ -508,6 +504,7 @@ pub struct Workspace {
     subscriptions: Vec<Subscription>,
     _apply_leader_updates: Task<Result<()>>,
     _observe_current_user: Task<Result<()>>,
+    _schedule_serialize: Option<Task<()>>,
     pane_history_timestamp: Arc<AtomicUsize>,
 }
 
@@ -722,6 +719,7 @@ impl Workspace {
             app_state,
             _observe_current_user,
             _apply_leader_updates,
+            _schedule_serialize: None,
             leader_updates_tx,
             subscriptions,
             pane_history_timestamp,
@@ -2897,6 +2895,14 @@ impl Workspace {
         cx.notify();
     }
 
+    fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
+        self._schedule_serialize = Some(cx.spawn(|this, cx| async move {
+            cx.background().timer(Duration::from_millis(100)).await;
+            this.read_with(&cx, |this, cx| this.serialize_workspace(cx))
+                .ok();
+        }));
+    }
+
     fn serialize_workspace(&self, cx: &ViewContext<Self>) {
         fn serialize_pane_handle(
             pane_handle: &ViewHandle<Pane>,
@@ -2927,12 +2933,17 @@ impl Workspace {
             cx: &AppContext,
         ) -> SerializedPaneGroup {
             match pane_group {
-                Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
+                Member::Axis(PaneAxis {
+                    axis,
+                    members,
+                    flexes,
+                }) => SerializedPaneGroup::Group {
                     axis: *axis,
                     children: members
                         .iter()
                         .map(|member| build_serialized_pane_group(member, cx))
                         .collect::<Vec<_>>(),
+                    flexes: Some(flexes.borrow().clone()),
                 },
                 Member::Pane(pane_handle) => {
                     SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))