Add support for resizing splits and docks, as well as utilities (#3595)

Mikayla Maki created

Making this PR to upstream changes to util and GPUI2, resizing exists
but isn't working yet.

Release Notes:

- N/A

Change summary

crates/editor2/src/element.rs                 |   26 
crates/gpui2/src/app.rs                       |    4 
crates/gpui2/src/elements/div.rs              |   22 
crates/gpui2/src/elements/text.rs             |    2 
crates/gpui2/src/executor.rs                  |    6 
crates/gpui2/src/geometry.rs                  |  110 +
crates/gpui2/src/interactive.rs               |    6 
crates/gpui2/src/platform.rs                  |    1 
crates/gpui2/src/platform/mac/platform.rs     |    8 
crates/gpui2/src/platform/test/platform.rs    |    5 
crates/gpui2/src/taffy.rs                     |    9 
crates/gpui2/src/window.rs                    |   12 
crates/ui2/src/components/right_click_menu.rs |    2 
crates/util/src/util.rs                       |   31 
crates/workspace2/src/dock.rs                 |   55 
crates/workspace2/src/pane.rs                 |    5 
crates/workspace2/src/pane_group.rs           | 1224 ++++++++++++--------
crates/workspace2/src/persistence.rs          |    6 
crates/workspace2/src/persistence/model.rs    |    6 
crates/workspace2/src/workspace2.rs           |   56 
20 files changed, 1,030 insertions(+), 566 deletions(-)

Detailed changes

crates/editor2/src/element.rs 🔗

@@ -391,9 +391,9 @@ impl EditorElement {
         let mut click_count = event.click_count;
         let modifiers = event.modifiers;
 
-        if gutter_bounds.contains_point(&event.position) {
+        if gutter_bounds.contains(&event.position) {
             click_count = 3; // Simulate triple-click when clicking the gutter to select lines
-        } else if !text_bounds.contains_point(&event.position) {
+        } else if !text_bounds.contains(&event.position) {
             return;
         }
         if !cx.was_top_layer(&event.position, stacking_order) {
@@ -439,7 +439,7 @@ impl EditorElement {
         text_bounds: Bounds<Pixels>,
         cx: &mut ViewContext<Editor>,
     ) {
-        if !text_bounds.contains_point(&event.position) {
+        if !text_bounds.contains(&event.position) {
             return;
         }
         let point_for_position = position_map.point_for_position(text_bounds, event.position);
@@ -469,7 +469,7 @@ impl EditorElement {
 
         if !pending_nonempty_selections
             && event.modifiers.command
-            && text_bounds.contains_point(&event.position)
+            && text_bounds.contains(&event.position)
             && cx.was_top_layer(&event.position, stacking_order)
         {
             let point = position_map.point_for_position(text_bounds, event.position);
@@ -531,8 +531,8 @@ impl EditorElement {
             );
         }
 
-        let text_hovered = text_bounds.contains_point(&event.position);
-        let gutter_hovered = gutter_bounds.contains_point(&event.position);
+        let text_hovered = text_bounds.contains(&event.position);
+        let gutter_hovered = gutter_bounds.contains(&event.position);
         let was_top = cx.was_top_layer(&event.position, stacking_order);
 
         editor.set_gutter_hovered(gutter_hovered, cx);
@@ -900,7 +900,7 @@ impl EditorElement {
                 bounds: text_bounds,
             }),
             |cx| {
-                if text_bounds.contains_point(&cx.mouse_position()) {
+                if text_bounds.contains(&cx.mouse_position()) {
                     if self
                         .editor
                         .read(cx)
@@ -966,7 +966,7 @@ impl EditorElement {
                                     |fold_element_state, cx| {
                                         if fold_element_state.is_active() {
                                             gpui::blue()
-                                        } else if fold_bounds.contains_point(&cx.mouse_position()) {
+                                        } else if fold_bounds.contains(&cx.mouse_position()) {
                                             gpui::black()
                                         } else {
                                             gpui::red()
@@ -1377,7 +1377,7 @@ impl EditorElement {
         }
 
         let mouse_position = cx.mouse_position();
-        if track_bounds.contains_point(&mouse_position) {
+        if track_bounds.contains(&mouse_position) {
             cx.set_cursor_style(CursorStyle::Arrow);
         }
 
@@ -1405,7 +1405,7 @@ impl EditorElement {
                         cx.stop_propagation();
                     } else {
                         editor.scroll_manager.set_is_dragging_scrollbar(false, cx);
-                        if track_bounds.contains_point(&event.position) {
+                        if track_bounds.contains(&event.position) {
                             editor.scroll_manager.show_scrollbar(cx);
                         }
                     }
@@ -1428,7 +1428,7 @@ impl EditorElement {
                 let editor = self.editor.clone();
                 move |event: &MouseDownEvent, phase, cx| {
                     editor.update(cx, |editor, cx| {
-                        if track_bounds.contains_point(&event.position) {
+                        if track_bounds.contains(&event.position) {
                             editor.scroll_manager.set_is_dragging_scrollbar(true, cx);
 
                             let y = event.position.y;
@@ -2502,10 +2502,10 @@ impl EditorElement {
                             gutter_bounds,
                             &stacking_order,
                             cx,
-                        )
+                        );
                     }),
                     MouseButton::Right => editor.update(cx, |editor, cx| {
-                        Self::mouse_right_down(editor, event, &position_map, text_bounds, cx)
+                        Self::mouse_right_down(editor, event, &position_map, text_bounds, cx);
                     }),
                     _ => {}
                 };

crates/gpui2/src/app.rs 🔗

@@ -513,6 +513,10 @@ impl AppContext {
         self.platform.path_for_auxiliary_executable(name)
     }
 
+    pub fn double_click_interval(&self) -> Duration {
+        self.platform.double_click_interval()
+    }
+
     pub fn prompt_for_paths(
         &self,
         options: PathPromptOptions,

crates/gpui2/src/elements/div.rs 🔗

@@ -761,7 +761,7 @@ pub struct InteractiveBounds {
 
 impl InteractiveBounds {
     pub fn visibly_contains(&self, point: &Point<Pixels>, cx: &WindowContext) -> bool {
-        self.bounds.contains_point(point) && cx.was_top_layer(&point, &self.stacking_order)
+        self.bounds.contains(point) && cx.was_top_layer(&point, &self.stacking_order)
     }
 }
 
@@ -860,10 +860,10 @@ impl Interactivity {
             .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx));
 
         if let Some(group_bounds) = hover_group_bounds {
-            let hovered = group_bounds.contains_point(&cx.mouse_position());
+            let hovered = group_bounds.contains(&cx.mouse_position());
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
                 if phase == DispatchPhase::Capture {
-                    if group_bounds.contains_point(&event.position) != hovered {
+                    if group_bounds.contains(&event.position) != hovered {
                         cx.notify();
                     }
                 }
@@ -875,10 +875,10 @@ impl Interactivity {
             || cx.active_drag.is_some() && !self.drag_over_styles.is_empty()
         {
             let bounds = bounds.intersect(&cx.content_mask().bounds);
-            let hovered = bounds.contains_point(&cx.mouse_position());
+            let hovered = bounds.contains(&cx.mouse_position());
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
                 if phase == DispatchPhase::Capture {
-                    if bounds.contains_point(&event.position) != hovered {
+                    if bounds.contains(&event.position) != hovered {
                         cx.notify();
                     }
                 }
@@ -1068,8 +1068,8 @@ impl Interactivity {
             let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |down: &MouseDownEvent, phase, cx| {
                 if phase == DispatchPhase::Bubble {
-                    let group = active_group_bounds
-                        .map_or(false, |bounds| bounds.contains_point(&down.position));
+                    let group =
+                        active_group_bounds.map_or(false, |bounds| bounds.contains(&down.position));
                     let element = interactive_bounds.visibly_contains(&down.position, cx);
                     if group || element {
                         *active_state.borrow_mut() = ElementClickedState { group, element };
@@ -1183,7 +1183,7 @@ impl Interactivity {
             let mouse_position = cx.mouse_position();
             if let Some(group_hover) = self.group_hover_style.as_ref() {
                 if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
-                    if group_bounds.contains_point(&mouse_position)
+                    if group_bounds.contains(&mouse_position)
                         && cx.was_top_layer(&mouse_position, cx.stacking_order())
                     {
                         style.refine(&group_hover.style);
@@ -1193,7 +1193,7 @@ impl Interactivity {
             if self.hover_style.is_some() {
                 if bounds
                     .intersect(&cx.content_mask().bounds)
-                    .contains_point(&mouse_position)
+                    .contains(&mouse_position)
                     && cx.was_top_layer(&mouse_position, cx.stacking_order())
                 {
                     style.refine(&self.hover_style);
@@ -1204,7 +1204,7 @@ impl Interactivity {
                 for (state_type, group_drag_style) in &self.group_drag_over_styles {
                     if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
                         if *state_type == drag.view.entity_type()
-                            && group_bounds.contains_point(&mouse_position)
+                            && group_bounds.contains(&mouse_position)
                         {
                             style.refine(&group_drag_style.style);
                         }
@@ -1215,7 +1215,7 @@ impl Interactivity {
                     if *state_type == drag.view.entity_type()
                         && bounds
                             .intersect(&cx.content_mask().bounds)
-                            .contains_point(&mouse_position)
+                            .contains(&mouse_position)
                     {
                         style.refine(drag_over_style);
                     }

crates/gpui2/src/elements/text.rs 🔗

@@ -253,7 +253,7 @@ impl TextState {
     }
 
     fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
-        if !bounds.contains_point(&position) {
+        if !bounds.contains(&position) {
             return None;
         }
 

crates/gpui2/src/executor.rs 🔗

@@ -57,8 +57,12 @@ where
     T: 'static,
     E: 'static + Debug,
 {
+    #[track_caller]
     pub fn detach_and_log_err(self, cx: &mut AppContext) {
-        cx.foreground_executor().spawn(self.log_err()).detach();
+        let location = core::panic::Location::caller();
+        cx.foreground_executor()
+            .spawn(self.log_tracked_err(*location))
+            .detach();
     }
 }
 

crates/gpui2/src/geometry.rs 🔗

@@ -8,6 +8,62 @@ use std::{
     ops::{Add, Div, Mul, MulAssign, Sub},
 };
 
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub enum Axis {
+    Vertical,
+    Horizontal,
+}
+
+impl Axis {
+    pub fn invert(&self) -> Self {
+        match self {
+            Axis::Vertical => Axis::Horizontal,
+            Axis::Horizontal => Axis::Vertical,
+        }
+    }
+}
+
+pub trait Along {
+    type Unit;
+
+    fn along(&self, axis: Axis) -> Self::Unit;
+
+    fn apply_along(&self, axis: Axis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self;
+}
+
+impl sqlez::bindable::StaticColumnCount for Axis {}
+impl sqlez::bindable::Bind for Axis {
+    fn bind(
+        &self,
+        statement: &sqlez::statement::Statement,
+        start_index: i32,
+    ) -> anyhow::Result<i32> {
+        match self {
+            Axis::Horizontal => "Horizontal",
+            Axis::Vertical => "Vertical",
+        }
+        .bind(statement, start_index)
+    }
+}
+
+impl sqlez::bindable::Column for Axis {
+    fn column(
+        statement: &mut sqlez::statement::Statement,
+        start_index: i32,
+    ) -> anyhow::Result<(Self, i32)> {
+        String::column(statement, start_index).and_then(|(axis_text, next_index)| {
+            Ok((
+                match axis_text.as_str() {
+                    "Horizontal" => Axis::Horizontal,
+                    "Vertical" => Axis::Vertical,
+                    _ => anyhow::bail!("Stored serialized item kind is incorrect"),
+                },
+                next_index,
+            ))
+        })
+    }
+}
+
 /// Describes a location in a 2D cartesian coordinate space.
 ///
 /// It holds two public fields, `x` and `y`, which represent the coordinates in the space.
@@ -96,6 +152,30 @@ impl<T: Clone + Debug + Default> Point<T> {
     }
 }
 
+impl<T: Clone + Debug + Default> Along for Point<T> {
+    type Unit = T;
+
+    fn along(&self, axis: Axis) -> T {
+        match axis {
+            Axis::Horizontal => self.x.clone(),
+            Axis::Vertical => self.y.clone(),
+        }
+    }
+
+    fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Point<T> {
+        match axis {
+            Axis::Horizontal => Point {
+                x: f(self.x.clone()),
+                y: self.y.clone(),
+            },
+            Axis::Vertical => Point {
+                x: self.x.clone(),
+                y: f(self.y.clone()),
+            },
+        }
+    }
+}
+
 impl Point<Pixels> {
     /// Scales the point by a given factor, which is typically derived from the resolution
     /// of a target display to ensure proper sizing of UI elements.
@@ -373,6 +453,34 @@ impl Size<Pixels> {
     }
 }
 
+impl<T> Along for Size<T>
+where
+    T: Clone + Default + Debug,
+{
+    type Unit = T;
+
+    fn along(&self, axis: Axis) -> T {
+        match axis {
+            Axis::Horizontal => self.width.clone(),
+            Axis::Vertical => self.height.clone(),
+        }
+    }
+
+    /// Returns the value of this size along the given axis.
+    fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Self {
+        match axis {
+            Axis::Horizontal => Size {
+                width: f(self.width.clone()),
+                height: self.height.clone(),
+            },
+            Axis::Vertical => Size {
+                width: self.width.clone(),
+                height: f(self.height.clone()),
+            },
+        }
+    }
+}
+
 impl<T> Size<T>
 where
     T: PartialOrd + Clone + Default + Debug,
@@ -992,7 +1100,7 @@ where
     /// assert!(bounds.contains_point(&inside_point));
     /// assert!(!bounds.contains_point(&outside_point));
     /// ```
-    pub fn contains_point(&self, point: &Point<T>) -> bool {
+    pub fn contains(&self, point: &Point<T>) -> bool {
         point.x >= self.origin.x
             && point.x <= self.origin.x.clone() + self.size.width.clone()
             && point.y >= self.origin.y

crates/gpui2/src/interactive.rs 🔗

@@ -131,6 +131,12 @@ pub struct MouseMoveEvent {
     pub modifiers: Modifiers,
 }
 
+impl MouseMoveEvent {
+    pub fn dragging(&self) -> bool {
+        self.pressed_button == Some(MouseButton::Left)
+    }
+}
+
 #[derive(Clone, Debug)]
 pub struct ScrollWheelEvent {
     pub position: Point<Pixels>,

crates/gpui2/src/platform.rs 🔗

@@ -105,6 +105,7 @@ pub(crate) trait Platform: 'static {
     fn app_version(&self) -> Result<SemanticVersion>;
     fn app_path(&self) -> Result<PathBuf>;
     fn local_timezone(&self) -> UtcOffset;
+    fn double_click_interval(&self) -> Duration;
     fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
 
     fn set_cursor_style(&self, style: CursorStyle);

crates/gpui2/src/platform/mac/platform.rs 🔗

@@ -49,6 +49,7 @@ use std::{
     rc::Rc,
     slice, str,
     sync::Arc,
+    time::Duration,
 };
 use time::UtcOffset;
 
@@ -657,6 +658,13 @@ impl Platform for MacPlatform {
         "macOS"
     }
 
+    fn double_click_interval(&self) -> Duration {
+        unsafe {
+            let double_click_interval: f64 = msg_send![class!(NSEvent), doubleClickInterval];
+            Duration::from_secs_f64(double_click_interval)
+        }
+    }
+
     fn os_version(&self) -> Result<SemanticVersion> {
         unsafe {
             let process_info = NSProcessInfo::processInfo(nil);

crates/gpui2/src/platform/test/platform.rs 🔗

@@ -12,6 +12,7 @@ use std::{
     path::PathBuf,
     rc::{Rc, Weak},
     sync::Arc,
+    time::Duration,
 };
 
 pub struct TestPlatform {
@@ -274,4 +275,8 @@ impl Platform for TestPlatform {
     fn delete_credentials(&self, _url: &str) -> Result<()> {
         Ok(())
     }
+
+    fn double_click_interval(&self) -> std::time::Duration {
+        Duration::from_millis(500)
+    }
 }

crates/gpui2/src/taffy.rs 🔗

@@ -477,3 +477,12 @@ impl From<Pixels> for AvailableSpace {
         AvailableSpace::Definite(pixels)
     }
 }
+
+impl From<Size<Pixels>> for Size<AvailableSpace> {
+    fn from(size: Size<Pixels>) -> Self {
+        Size {
+            width: AvailableSpace::Definite(size.width),
+            height: AvailableSpace::Definite(size.height),
+        }
+    }
+}

crates/gpui2/src/window.rs 🔗

@@ -60,6 +60,16 @@ pub enum DispatchPhase {
     Capture,
 }
 
+impl DispatchPhase {
+    pub fn bubble(self) -> bool {
+        self == DispatchPhase::Bubble
+    }
+
+    pub fn capture(self) -> bool {
+        self == DispatchPhase::Capture
+    }
+}
+
 type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
 type AnyMouseListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
 type AnyFocusListener = Box<dyn Fn(&FocusEvent, &mut WindowContext) + 'static>;
@@ -866,7 +876,7 @@ impl<'a> WindowContext<'a> {
     /// same layer as the given stacking order.
     pub fn was_top_layer(&self, point: &Point<Pixels>, level: &StackingOrder) -> bool {
         for (stack, bounds) in self.window.rendered_frame.depth_map.iter() {
-            if bounds.contains_point(point) {
+            if bounds.contains(point) {
                 return level.starts_with(stack) || stack.starts_with(level);
             }
         }

crates/ui2/src/components/right_click_menu.rs 🔗

@@ -137,7 +137,7 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
         cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
             if phase == DispatchPhase::Bubble
                 && event.button == MouseButton::Right
-                && bounds.contains_point(&event.position)
+                && bounds.contains(&event.position)
             {
                 cx.stop_propagation();
                 cx.prevent_default();

crates/util/src/util.rs 🔗

@@ -184,6 +184,11 @@ pub trait TryFutureExt {
     fn log_err(self) -> LogErrorFuture<Self>
     where
         Self: Sized;
+
+    fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture<Self>
+    where
+        Self: Sized;
+
     fn warn_on_err(self) -> LogErrorFuture<Self>
     where
         Self: Sized;
@@ -197,18 +202,29 @@ where
     F: Future<Output = Result<T, E>>,
     E: std::fmt::Debug,
 {
+    #[track_caller]
     fn log_err(self) -> LogErrorFuture<Self>
     where
         Self: Sized,
     {
-        LogErrorFuture(self, log::Level::Error)
+        let location = Location::caller();
+        LogErrorFuture(self, log::Level::Error, *location)
     }
 
+    fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture<Self>
+    where
+        Self: Sized,
+    {
+        LogErrorFuture(self, log::Level::Error, location)
+    }
+
+    #[track_caller]
     fn warn_on_err(self) -> LogErrorFuture<Self>
     where
         Self: Sized,
     {
-        LogErrorFuture(self, log::Level::Warn)
+        let location = Location::caller();
+        LogErrorFuture(self, log::Level::Warn, *location)
     }
 
     fn unwrap(self) -> UnwrapFuture<Self>
@@ -219,7 +235,7 @@ where
     }
 }
 
-pub struct LogErrorFuture<F>(F, log::Level);
+pub struct LogErrorFuture<F>(F, log::Level, core::panic::Location<'static>);
 
 impl<F, T, E> Future for LogErrorFuture<F>
 where
@@ -230,12 +246,19 @@ where
 
     fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
         let level = self.1;
+        let location = self.2;
         let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
         match inner.poll(cx) {
             Poll::Ready(output) => Poll::Ready(match output {
                 Ok(output) => Some(output),
                 Err(error) => {
-                    log::log!(level, "{:?}", error);
+                    log::log!(
+                        level,
+                        "{}:{}: {:?}",
+                        location.file(),
+                        location.line(),
+                        error
+                    );
                     None
                 }
             }),

crates/workspace2/src/dock.rs 🔗

@@ -1,8 +1,9 @@
-use crate::{status_bar::StatusItemView, Axis, Workspace};
+use crate::{status_bar::StatusItemView, Workspace};
+use crate::{DockClickReset, DockDragState};
 use gpui::{
-    div, px, Action, AnchorCorner, AnyView, AppContext, Div, Entity, EntityId, EventEmitter,
-    FocusHandle, FocusableView, IntoElement, ParentElement, Render, SharedString, Styled,
-    Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
+    div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, EntityId,
+    EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render,
+    SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -364,7 +365,7 @@ impl Dock {
                         this.set_open(false, cx);
                     }
                 }
-                PanelEvent::Focus => todo!(),
+                PanelEvent::Focus => {}
             }),
         ];
 
@@ -485,6 +486,48 @@ impl Render for Dock {
         if let Some(entry) = self.visible_entry() {
             let size = entry.panel.size(cx);
 
+            let mut pre_resize_handle = None;
+            let mut post_resize_handle = None;
+            let position = self.position;
+            let handler = div()
+                .id("resize-handle")
+                .bg(cx.theme().colors().border)
+                .on_mouse_down(gpui::MouseButton::Left, move |_, cx| {
+                    cx.update_global(|drag: &mut DockDragState, cx| drag.0 = Some(position))
+                })
+                .on_click(cx.listener(|v, e: &ClickEvent, cx| {
+                    if e.down.button == MouseButton::Left {
+                        cx.update_global(|state: &mut DockClickReset, cx| {
+                            if state.0.is_some() {
+                                state.0 = None;
+                                v.resize_active_panel(None, cx)
+                            } else {
+                                let double_click = cx.double_click_interval();
+                                let timer = cx.background_executor().timer(double_click);
+                                state.0 = Some(cx.spawn(|_, mut cx| async move {
+                                    timer.await;
+                                    cx.update_global(|state: &mut DockClickReset, cx| {
+                                        state.0 = None;
+                                    })
+                                    .ok();
+                                }));
+                            }
+                        })
+                    }
+                }));
+
+            match self.position() {
+                DockPosition::Left => {
+                    post_resize_handle = Some(handler.w_1().h_full().cursor_col_resize())
+                }
+                DockPosition::Bottom => {
+                    pre_resize_handle = Some(handler.w_full().h_1().cursor_row_resize())
+                }
+                DockPosition::Right => {
+                    pre_resize_handle = Some(handler.w_full().h_1().cursor_col_resize())
+                }
+            }
+
             div()
                 .border_color(cx.theme().colors().border)
                 .map(|this| match self.position().axis() {
@@ -496,7 +539,9 @@ impl Render for Dock {
                     DockPosition::Right => this.border_l(),
                     DockPosition::Bottom => this.border_t(),
                 })
+                .children(pre_resize_handle)
                 .child(entry.panel.to_any())
+                .children(post_resize_handle)
         } else {
             div()
         }

crates/workspace2/src/pane.rs 🔗

@@ -1046,10 +1046,11 @@ impl Pane {
                     {
                         pane.remove_item(item_ix, false, cx);
                     }
-                })?;
+                })
+                .ok();
             }
 
-            pane.update(&mut cx, |_, cx| cx.notify())?;
+            pane.update(&mut cx, |_, cx| cx.notify()).ok();
             Ok(())
         })
     }

crates/workspace2/src/pane_group.rs 🔗

@@ -1,13 +1,9 @@
-use crate::{AppState, FollowerState, Pane, Workspace};
-use anyhow::{anyhow, bail, Result};
+use crate::{pane_group::element::pane_axis, AppState, FollowerState, Pane, Workspace};
+use anyhow::{anyhow, Result};
 use call::{ActiveCall, ParticipantLocation};
 use collections::HashMap;
-use db::sqlez::{
-    bindable::{Bind, Column, StaticColumnCount},
-    statement::Statement,
-};
 use gpui::{
-    point, size, AnyWeakView, Bounds, Div, Entity as _, IntoElement, Model, Pixels, Point, View,
+    point, size, AnyWeakView, Axis, Bounds, Entity as _, IntoElement, Model, Pixels, Point, View,
     ViewContext,
 };
 use parking_lot::Mutex;
@@ -16,42 +12,10 @@ use serde::Deserialize;
 use std::sync::Arc;
 use ui::{prelude::*, Button};
 
-const HANDLE_HITBOX_SIZE: f32 = 4.0;
+const HANDLE_HITBOX_SIZE: f32 = 10.0; //todo!(change this back to 4)
 const HORIZONTAL_MIN_SIZE: f32 = 80.;
 const VERTICAL_MIN_SIZE: f32 = 100.;
 
-#[derive(Copy, Clone, PartialEq, Eq, Debug)]
-pub enum Axis {
-    Vertical,
-    Horizontal,
-}
-
-impl StaticColumnCount for Axis {}
-impl Bind for Axis {
-    fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
-        match self {
-            Axis::Horizontal => "Horizontal",
-            Axis::Vertical => "Vertical",
-        }
-        .bind(statement, start_index)
-    }
-}
-
-impl Column for Axis {
-    fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
-        String::column(statement, start_index).and_then(|(axis_text, next_index)| {
-            Ok((
-                match axis_text.as_str() {
-                    "Horizontal" => Axis::Horizontal,
-                    "Vertical" => Axis::Vertical,
-                    _ => bail!("Stored serialized item kind is incorrect"),
-                },
-                next_index,
-            ))
-        })
-    }
-}
-
 #[derive(Clone, PartialEq)]
 pub struct PaneGroup {
     pub(crate) root: Member,
@@ -612,7 +576,7 @@ impl PaneAxis {
 
         for (idx, member) in self.members.iter().enumerate() {
             if let Some(coordinates) = bounding_boxes[idx] {
-                if coordinates.contains_point(&coordinate) {
+                if coordinates.contains(&coordinate) {
                     return match member {
                         Member::Pane(found) => Some(found),
                         Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
@@ -632,80 +596,42 @@ impl PaneAxis {
         zoomed: Option<&AnyWeakView>,
         app_state: &Arc<AppState>,
         cx: &mut ViewContext<Workspace>,
-    ) -> Div {
+    ) -> gpui::AnyElement {
         debug_assert!(self.members.len() == self.flexes.lock().len());
+        let mut active_pane_ix = None;
 
-        div()
-            .flex()
-            .flex_auto()
-            .map(|s| match self.axis {
-                Axis::Vertical => s.flex_col(),
-                Axis::Horizontal => s.flex_row(),
-            })
-            .children(self.members.iter().enumerate().map(|(ix, member)| {
-                match member {
-                    Member::Axis(axis) => axis
-                        .render(
-                            project,
-                            basis,
-                            follower_states,
-                            active_pane,
-                            zoomed,
-                            app_state,
-                            cx,
-                        )
-                        .into_any_element(),
-                    Member::Pane(pane) => pane.clone().into_any_element(),
-                }
-            }))
-
-        // let mut pane_axis = PaneAxisElement::new(
-        //     self.axis,
-        //     basis,
-        //     self.flexes.clone(),
-        //     self.bounding_boxes.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_states,
-        //         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,
-        //         }
-
-        //         member = member.contained().with_border(border).into_any();
-        //     }
+        pane_axis(
+            self.axis,
+            basis,
+            self.flexes.clone(),
+            self.bounding_boxes.clone(),
+        )
+        .children(self.members.iter().enumerate().map(|(ix, member)| {
+            if member.contains(active_pane) {
+                active_pane_ix = Some(ix);
+            }
 
-        //     pane_axis = pane_axis.with_child(member.into_any());
-        // }
-        // pane_axis.set_active_pane(active_pane_ix);
-        // pane_axis.into_any()
+            match member {
+                Member::Axis(axis) => axis
+                    .render(
+                        project,
+                        (basis + ix) * 10,
+                        follower_states,
+                        active_pane,
+                        zoomed,
+                        app_state,
+                        cx,
+                    )
+                    .into_any_element(),
+                Member::Pane(pane) => div()
+                    .size_full()
+                    .border()
+                    .child(pane.clone())
+                    .into_any_element(),
+            }
+        }))
+        .with_active_pane(active_pane_ix)
+        .into_any_element()
     }
 }
 
@@ -767,403 +693,677 @@ impl SplitDirection {
     }
 }
 
-// mod element {
-//     // use std::{cell::RefCell, iter::from_fn, ops::Range, rc::Rc};
-
-//     // use gpui::{
-//     //     geometry::{
-//     //         rect::Bounds<Pixels>,
-//     //         vector::{vec2f, Vector2F},
-//     //     },
-//     //     json::{self, ToJson},
-//     //     platform::{CursorStyle, MouseButton},
-//     //     scene::MouseDrag,
-//     //     AnyElement, Axis, CursorRegion, Element, EventContext, MouseRegion, Bounds<Pixels>Ext,
-//     //     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>>,
-//         bounding_boxes: Rc<RefCell<Vec<Option<Bounds<Pixels>>>>>,
-//     }
-
-//     impl PaneAxisElement {
-//         pub fn new(
-//             axis: Axis,
-//             basis: usize,
-//             flexes: Rc<RefCell<Vec<f32>>>,
-//             bounding_boxes: Rc<RefCell<Vec<Option<Bounds<Pixels>>>>>,
-//         ) -> Self {
-//             Self {
-//                 axis,
-//                 basis,
-//                 flexes,
-//                 bounding_boxes,
-//                 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 ViewContext<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));
-//             }
-//         }
-
-//         fn handle_resize(
-//             flexes: Rc<RefCell<Vec<f32>>>,
-//             axis: Axis,
-//             preceding_ix: usize,
-//             child_start: Vector2F,
-//             drag_bounds: Bounds<Pixels>,
-//         ) -> impl Fn(MouseDrag, &mut Workspace, &mut EventContext<Workspace>) {
-//             let size = move |ix, flexes: &[f32]| {
-//                 drag_bounds.length_along(axis) * (flexes[ix] / flexes.len() as f32)
-//             };
-
-//             move |drag, workspace: &mut Workspace, cx| {
-//                 if drag.end {
-//                     // TODO: Clear cascading resize state
-//                     return;
-//                 }
-//                 let min_size = match axis {
-//                     Axis::Horizontal => HORIZONTAL_MIN_SIZE,
-//                     Axis::Vertical => VERTICAL_MIN_SIZE,
-//                 };
-//                 let mut flexes = flexes.borrow_mut();
-
-//                 // Don't allow resizing to less than the minimum size, if elements are already too small
-//                 if min_size - 1. > size(preceding_ix, flexes.as_slice()) {
-//                     return;
-//                 }
-
-//                 let mut proposed_current_pixel_change = (drag.position - child_start).along(axis)
-//                     - size(preceding_ix, flexes.as_slice());
-
-//                 let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
-//                     let flex_change = pixel_dx / drag_bounds.length_along(axis);
-//                     let current_target_flex = flexes[target_ix] + flex_change;
-//                     let next_target_flex =
-//                         flexes[(target_ix as isize + next) as usize] - flex_change;
-//                     (current_target_flex, next_target_flex)
-//                 };
-
-//                 let mut successors = from_fn({
-//                     let forward = proposed_current_pixel_change > 0.;
-//                     let mut ix_offset = 0;
-//                     let len = flexes.len();
-//                     move || {
-//                         let result = if forward {
-//                             (preceding_ix + 1 + ix_offset < len).then(|| preceding_ix + ix_offset)
-//                         } else {
-//                             (preceding_ix as isize - ix_offset as isize >= 0)
-//                                 .then(|| preceding_ix - ix_offset)
-//                         };
-
-//                         ix_offset += 1;
-
-//                         result
-//                     }
-//                 });
-
-//                 while proposed_current_pixel_change.abs() > 0. {
-//                     let Some(current_ix) = successors.next() else {
-//                         break;
-//                     };
-
-//                     let next_target_size = f32::max(
-//                         size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
-//                         min_size,
-//                     );
-
-//                     let current_target_size = f32::max(
-//                         size(current_ix, flexes.as_slice())
-//                             + size(current_ix + 1, flexes.as_slice())
-//                             - next_target_size,
-//                         min_size,
-//                     );
-
-//                     let current_pixel_change =
-//                         current_target_size - size(current_ix, flexes.as_slice());
-
-//                     let (current_target_flex, next_target_flex) =
-//                         flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
-
-//                     flexes[current_ix] = current_target_flex;
-//                     flexes[current_ix + 1] = next_target_flex;
-
-//                     proposed_current_pixel_change -= current_pixel_change;
-//                 }
-
-//                 workspace.schedule_serialize(cx);
-//                 cx.notify();
-//             }
-//         }
-//     }
-
-//     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 ViewContext<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,
-//             bounds: Bounds<Pixels>,
-//             visible_bounds: Bounds<Pixels>,
-//             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 {
-//                 cx.scene().push_layer(Some(visible_bounds));
-//             }
-
-//             let mut child_origin = bounds.origin();
-
-//             let mut bounding_boxes = self.bounding_boxes.borrow_mut();
-//             bounding_boxes.clear();
-
-//             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(child_origin, visible_bounds, view, cx);
-
-//                 bounding_boxes.push(Some(Bounds<Pixels>::new(child_origin, child.size())));
-
-//                 match self.axis {
-//                     Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
-//                     Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
-//                 }
-
-//                 if can_resize && children_iter.peek().is_some() {
-//                     cx.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 => Bounds<Pixels>::new(
-//                             handle_origin,
-//                             vec2f(HANDLE_HITBOX_SIZE, visible_bounds.height()),
-//                         ),
-//                         Axis::Vertical => Bounds<Pixels>::new(
-//                             handle_origin,
-//                             vec2f(visible_bounds.width(), HANDLE_HITBOX_SIZE),
-//                         ),
-//                     };
-
-//                     let style = match self.axis {
-//                         Axis::Horizontal => CursorStyle::ResizeLeftRight,
-//                         Axis::Vertical => CursorStyle::ResizeUpDown,
-//                     };
-
-//                     cx.scene().push_cursor_region(CursorRegion {
-//                         bounds: handle_bounds,
-//                         style,
-//                     });
-
-//                     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,
-//                             Self::handle_resize(
-//                                 self.flexes.clone(),
-//                                 self.axis,
-//                                 ix,
-//                                 child_start,
-//                                 visible_bounds.clone(),
-//                             ),
-//                         )
-//                         .on_click(MouseButton::Left, {
-//                             let flexes = self.flexes.clone();
-//                             move |e, v: &mut Workspace, cx| {
-//                                 if e.click_count >= 2 {
-//                                     let mut borrow = flexes.borrow_mut();
-//                                     *borrow = vec![1.; borrow.len()];
-//                                     v.schedule_serialize(cx);
-//                                     cx.notify();
-//                                 }
-//                             }
-//                         });
-//                     cx.scene().push_mouse_region(mouse_region);
-
-//                     cx.scene().pop_stacking_context();
-//                 }
-//             }
-
-//             if overflowing {
-//                 cx.scene().pop_layer();
-//             }
-//         }
-
-//         fn rect_for_text_range(
-//             &self,
-//             range_utf16: Range<usize>,
-//             _: Bounds<Pixels>,
-//             _: Bounds<Pixels>,
-//             _: &Self::LayoutState,
-//             _: &Self::PaintState,
-//             view: &Workspace,
-//             cx: &ViewContext<Workspace>,
-//         ) -> Option<Bounds<Pixels>> {
-//             self.children
-//                 .iter()
-//                 .find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
-//         }
-
-//         fn debug(
-//             &self,
-//             bounds: Bounds<Pixels>,
-//             _: &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>>()
-//             })
-//         }
-//     }
-// }
+mod element {
+
+    use std::{cell::RefCell, iter, rc::Rc, sync::Arc};
+
+    use gpui::{
+        px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, IntoElement,
+        MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Style, WindowContext,
+    };
+    use parking_lot::Mutex;
+    use smallvec::SmallVec;
+
+    use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE};
+
+    pub fn pane_axis(
+        axis: Axis,
+        basis: usize,
+        flexes: Arc<Mutex<Vec<f32>>>,
+        bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
+    ) -> PaneAxisElement {
+        PaneAxisElement {
+            axis,
+            basis,
+            flexes,
+            bounding_boxes,
+            children: SmallVec::new(),
+            active_pane_ix: None,
+        }
+    }
+
+    pub struct PaneAxisElement {
+        axis: Axis,
+        basis: usize,
+        flexes: Arc<Mutex<Vec<f32>>>,
+        bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
+        children: SmallVec<[AnyElement; 2]>,
+        active_pane_ix: Option<usize>,
+    }
+
+    impl PaneAxisElement {
+        pub fn with_active_pane(mut self, active_pane_ix: Option<usize>) -> Self {
+            self.active_pane_ix = active_pane_ix;
+            self
+        }
+
+        fn compute_resize(
+            flexes: &Arc<Mutex<Vec<f32>>>,
+            e: &MouseMoveEvent,
+            ix: usize,
+            axis: Axis,
+            axis_bounds: Bounds<Pixels>,
+            cx: &mut WindowContext,
+        ) {
+            let min_size = match axis {
+                Axis::Horizontal => px(HORIZONTAL_MIN_SIZE),
+                Axis::Vertical => px(VERTICAL_MIN_SIZE),
+            };
+            let mut flexes = flexes.lock();
+            debug_assert!(flex_values_in_bounds(flexes.as_slice()));
+
+            let size = move |ix, flexes: &[f32]| {
+                axis_bounds.size.along(axis) * (flexes[ix] / flexes.len() as f32)
+            };
+
+            // Don't allow resizing to less than the minimum size, if elements are already too small
+            if min_size - px(1.) > size(ix, flexes.as_slice()) {
+                return;
+            }
+
+            let mut proposed_current_pixel_change =
+                (e.position - axis_bounds.origin).along(axis) - size(ix, flexes.as_slice());
+
+            let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
+                let flex_change = pixel_dx / axis_bounds.size.along(axis);
+                let current_target_flex = flexes[target_ix] + flex_change;
+                let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change;
+                (current_target_flex, next_target_flex)
+            };
+
+            let mut successors = iter::from_fn({
+                let forward = proposed_current_pixel_change > px(0.);
+                let mut ix_offset = 0;
+                let len = flexes.len();
+                move || {
+                    let result = if forward {
+                        (ix + 1 + ix_offset < len).then(|| ix + ix_offset)
+                    } else {
+                        (ix as isize - ix_offset as isize >= 0).then(|| ix - ix_offset)
+                    };
+
+                    ix_offset += 1;
+
+                    result
+                }
+            });
+
+            while proposed_current_pixel_change.abs() > px(0.) {
+                let Some(current_ix) = successors.next() else {
+                    break;
+                };
+
+                let next_target_size = Pixels::max(
+                    size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
+                    min_size,
+                );
+
+                let current_target_size = Pixels::max(
+                    size(current_ix, flexes.as_slice()) + size(current_ix + 1, flexes.as_slice())
+                        - next_target_size,
+                    min_size,
+                );
+
+                let current_pixel_change =
+                    current_target_size - size(current_ix, flexes.as_slice());
+
+                let (current_target_flex, next_target_flex) =
+                    flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
+
+                flexes[current_ix] = current_target_flex;
+                flexes[current_ix + 1] = next_target_flex;
+
+                proposed_current_pixel_change -= current_pixel_change;
+            }
+
+            // todo!(reserialize workspace)
+            // workspace.schedule_serialize(cx);
+            cx.notify();
+        }
+
+        fn push_handle(
+            flexes: Arc<Mutex<Vec<f32>>>,
+            dragged_handle: Rc<RefCell<Option<usize>>>,
+            axis: Axis,
+            ix: usize,
+            pane_bounds: Bounds<Pixels>,
+            axis_bounds: Bounds<Pixels>,
+            cx: &mut WindowContext,
+        ) {
+            let handle_bounds = Bounds {
+                origin: pane_bounds.origin.apply_along(axis, |o| {
+                    o + pane_bounds.size.along(axis) - Pixels(HANDLE_HITBOX_SIZE / 2.)
+                }),
+                size: pane_bounds
+                    .size
+                    .apply_along(axis, |_| Pixels(HANDLE_HITBOX_SIZE)),
+            };
+
+            cx.with_z_index(3, |cx| {
+                if handle_bounds.contains(&cx.mouse_position()) {
+                    cx.set_cursor_style(match axis {
+                        Axis::Vertical => CursorStyle::ResizeUpDown,
+                        Axis::Horizontal => CursorStyle::ResizeLeftRight,
+                    })
+                }
+
+                cx.add_opaque_layer(handle_bounds);
+
+                cx.on_mouse_event({
+                    let dragged_handle = dragged_handle.clone();
+                    move |e: &MouseDownEvent, phase, cx| {
+                        if phase.bubble() && handle_bounds.contains(&e.position) {
+                            dragged_handle.replace(Some(ix));
+                        }
+                    }
+                });
+                cx.on_mouse_event(move |e: &MouseMoveEvent, phase, cx| {
+                    let dragged_handle = dragged_handle.borrow();
+                    if *dragged_handle == Some(ix) {
+                        Self::compute_resize(&flexes, e, ix, axis, axis_bounds, cx)
+                    }
+                });
+            });
+        }
+    }
+
+    impl IntoElement for PaneAxisElement {
+        type Element = Self;
+
+        fn element_id(&self) -> Option<ui::prelude::ElementId> {
+            Some(self.basis.into())
+        }
+
+        fn into_element(self) -> Self::Element {
+            self
+        }
+    }
+
+    impl Element for PaneAxisElement {
+        type State = Rc<RefCell<Option<usize>>>;
+
+        fn layout(
+            &mut self,
+            state: Option<Self::State>,
+            cx: &mut ui::prelude::WindowContext,
+        ) -> (gpui::LayoutId, Self::State) {
+            let mut style = Style::default();
+            style.size.width = relative(1.).into();
+            style.size.height = relative(1.).into();
+            let layout_id = cx.request_layout(&style, None);
+            let dragged_pane = state.unwrap_or_else(|| Rc::new(RefCell::new(None)));
+            (layout_id, dragged_pane)
+        }
+
+        fn paint(
+            self,
+            bounds: gpui::Bounds<ui::prelude::Pixels>,
+            state: &mut Self::State,
+            cx: &mut ui::prelude::WindowContext,
+        ) {
+            let flexes = self.flexes.lock().clone();
+            let len = self.children.len();
+            debug_assert!(flexes.len() == len);
+            debug_assert!(flex_values_in_bounds(flexes.as_slice()));
+
+            let mut origin = bounds.origin;
+            let space_per_flex = bounds.size.along(self.axis) / len as f32;
+
+            let mut bounding_boxes = self.bounding_boxes.lock();
+            bounding_boxes.clear();
+
+            for (ix, child) in self.children.into_iter().enumerate() {
+                //todo!(active_pane_magnification)
+                // If usign active pane magnification, need to switch to using
+                // 1 for all non-active panes, and then the magnification for the
+                // active pane.
+                let child_size = bounds
+                    .size
+                    .apply_along(self.axis, |_| space_per_flex * flexes[ix]);
+
+                let child_bounds = Bounds {
+                    origin,
+                    size: child_size,
+                };
+                bounding_boxes.push(Some(child_bounds));
+                cx.with_z_index(0, |cx| {
+                    child.draw(origin, child_size.into(), cx);
+                });
+                cx.with_z_index(1, |cx| {
+                    if ix < len - 1 {
+                        Self::push_handle(
+                            self.flexes.clone(),
+                            state.clone(),
+                            self.axis,
+                            ix,
+                            child_bounds,
+                            bounds,
+                            cx,
+                        );
+                    }
+                });
+
+                origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis));
+            }
+
+            cx.with_z_index(1, |cx| {
+                cx.on_mouse_event({
+                    let state = state.clone();
+                    move |e: &MouseUpEvent, phase, cx| {
+                        if phase.bubble() {
+                            state.replace(None);
+                        }
+                    }
+                });
+            })
+        }
+    }
+
+    impl ParentElement for PaneAxisElement {
+        fn children_mut(&mut self) -> &mut smallvec::SmallVec<[AnyElement; 2]> {
+            &mut self.children
+        }
+    }
+
+    fn flex_values_in_bounds(flexes: &[f32]) -> bool {
+        (flexes.iter().copied().sum::<f32>() - flexes.len() as f32).abs() < 0.001
+    }
+    //     // use std::{cell::RefCell, iter::from_fn, ops::Range, rc::Rc};
+
+    //     // use gpui::{
+    //     //     geometry::{
+    //     //         rect::Bounds<Pixels>,
+    //     //         vector::{vec2f, Vector2F},
+    //     //     },
+    //     //     json::{self, ToJson},
+    //     //     platform::{CursorStyle, MouseButton},
+    //     //     scene::MouseDrag,
+    //     //     AnyElement, Axis, CursorRegion, Element, EventContext, MouseRegion, Bounds<Pixels>Ext,
+    //     //     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>>,
+    //         bounding_boxes: Rc<RefCell<Vec<Option<Bounds<Pixels>>>>>,
+    //     }
+
+    //     impl PaneAxisElement {
+    //         pub fn new(
+    //             axis: Axis,
+    //             basis: usize,
+    //             flexes: Rc<RefCell<Vec<f32>>>,
+    //             bounding_boxes: Rc<RefCell<Vec<Option<Bounds<Pixels>>>>>,
+    //         ) -> Self {
+    //             Self {
+    //                 axis,
+    //                 basis,
+    //                 flexes,
+    //                 bounding_boxes,
+    //                 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 ViewContext<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));
+    //             }
+    //         }
+
+    //         fn handle_resize(
+    //             flexes: Rc<RefCell<Vec<f32>>>,
+    //             axis: Axis,
+    //             preceding_ix: usize,
+    //             child_start: Vector2F,
+    //             drag_bounds: Bounds<Pixels>,
+    //         ) -> impl Fn(MouseDrag, &mut Workspace, &mut EventContext<Workspace>) {
+    //             let size = move |ix, flexes: &[f32]| {
+    //                 drag_bounds.length_along(axis) * (flexes[ix] / flexes.len() as f32)
+    //             };
+
+    //             move |drag, workspace: &mut Workspace, cx| {
+    //                 if drag.end {
+    //                     // TODO: Clear cascading resize state
+    //                     return;
+    //                 }
+    //                 let min_size = match axis {
+    //                     Axis::Horizontal => HORIZONTAL_MIN_SIZE,
+    //                     Axis::Vertical => VERTICAL_MIN_SIZE,
+    //                 };
+    //                 let mut flexes = flexes.borrow_mut();
+
+    //                 // Don't allow resizing to less than the minimum size, if elements are already too small
+    //                 if min_size - 1. > size(preceding_ix, flexes.as_slice()) {
+    //                     return;
+    //                 }
+
+    //                 let mut proposed_current_pixel_change = (drag.position - child_start).along(axis)
+    //                     - size(preceding_ix, flexes.as_slice());
+
+    //                 let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
+    //                     let flex_change = pixel_dx / drag_bounds.length_along(axis);
+    //                     let current_target_flex = flexes[target_ix] + flex_change;
+    //                     let next_target_flex =
+    //                         flexes[(target_ix as isize + next) as usize] - flex_change;
+    //                     (current_target_flex, next_target_flex)
+    //                 };
+
+    //                 let mut successors = from_fn({
+    //                     let forward = proposed_current_pixel_change > 0.;
+    //                     let mut ix_offset = 0;
+    //                     let len = flexes.len();
+    //                     move || {
+    //                         let result = if forward {
+    //                             (preceding_ix + 1 + ix_offset < len).then(|| preceding_ix + ix_offset)
+    //                         } else {
+    //                             (preceding_ix as isize - ix_offset as isize >= 0)
+    //                                 .then(|| preceding_ix - ix_offset)
+    //                         };
+
+    //                         ix_offset += 1;
+
+    //                         result
+    //                     }
+    //                 });
+
+    //                 while proposed_current_pixel_change.abs() > 0. {
+    //                     let Some(current_ix) = successors.next() else {
+    //                         break;
+    //                     };
+
+    //                     let next_target_size = f32::max(
+    //                         size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
+    //                         min_size,
+    //                     );
+
+    //                     let current_target_size = f32::max(
+    //                         size(current_ix, flexes.as_slice())
+    //                             + size(current_ix + 1, flexes.as_slice())
+    //                             - next_target_size,
+    //                         min_size,
+    //                     );
+
+    //                     let current_pixel_change =
+    //                         current_target_size - size(current_ix, flexes.as_slice());
+
+    //                     let (current_target_flex, next_target_flex) =
+    //                         flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
+
+    //                     flexes[current_ix] = current_target_flex;
+    //                     flexes[current_ix + 1] = next_target_flex;
+
+    //                     proposed_current_pixel_change -= current_pixel_change;
+    //                 }
+
+    //                 workspace.schedule_serialize(cx);
+    //                 cx.notify();
+    //             }
+    //         }
+    //     }
+
+    //     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 ViewContext<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,
+    //             bounds: Bounds<Pixels>,
+    //             visible_bounds: Bounds<Pixels>,
+    //             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 {
+    //                 cx.scene().push_layer(Some(visible_bounds));
+    //             }
+
+    //             let mut child_origin = bounds.origin();
+
+    //             let mut bounding_boxes = self.bounding_boxes.borrow_mut();
+    //             bounding_boxes.clear();
+
+    //             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(child_origin, visible_bounds, view, cx);
+
+    //                 bounding_boxes.push(Some(Bounds<Pixels>::new(child_origin, child.size())));
+
+    //                 match self.axis {
+    //                     Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
+    //                     Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
+    //                 }
+
+    //                 if can_resize && children_iter.peek().is_some() {
+    //                     cx.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 => Bounds<Pixels>::new(
+    //                             handle_origin,
+    //                             vec2f(HANDLE_HITBOX_SIZE, visible_bounds.height()),
+    //                         ),
+    //                         Axis::Vertical => Bounds<Pixels>::new(
+    //                             handle_origin,
+    //                             vec2f(visible_bounds.width(), HANDLE_HITBOX_SIZE),
+    //                         ),
+    //                     };
+
+    //                     let style = match self.axis {
+    //                         Axis::Horizontal => CursorStyle::ResizeLeftRight,
+    //                         Axis::Vertical => CursorStyle::ResizeUpDown,
+    //                     };
+
+    //                     cx.scene().push_cursor_region(CursorRegion {
+    //                         bounds: handle_bounds,
+    //                         style,
+    //                     });
+
+    //                     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,
+    //                             Self::handle_resize(
+    //                                 self.flexes.clone(),
+    //                                 self.axis,
+    //                                 ix,
+    //                                 child_start,
+    //                                 visible_bounds.clone(),
+    //                             ),
+    //                         )
+    //                         .on_click(MouseButton::Left, {
+    //                             let flexes = self.flexes.clone();
+    //                             move |e, v: &mut Workspace, cx| {
+    //                                 if e.click_count >= 2 {
+    //                                     let mut borrow = flexes.borrow_mut();
+    //                                     *borrow = vec![1.; borrow.len()];
+    //                                     v.schedule_serialize(cx);
+    //                                     cx.notify();
+    //                                 }
+    //                             }
+    //                         });
+    //                     cx.scene().push_mouse_region(mouse_region);
+
+    //                     cx.scene().pop_stacking_context();
+    //                 }
+    //             }
+
+    //             if overflowing {
+    //                 cx.scene().pop_layer();
+    //             }
+    //         }
+
+    //         fn rect_for_text_range(
+    //             &self,
+    //             range_utf16: Range<usize>,
+    //             _: Bounds<Pixels>,
+    //             _: Bounds<Pixels>,
+    //             _: &Self::LayoutState,
+    //             _: &Self::PaintState,
+    //             view: &Workspace,
+    //             cx: &ViewContext<Workspace>,
+    //         ) -> Option<Bounds<Pixels>> {
+    //             self.children
+    //                 .iter()
+    //                 .find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
+    //         }
+
+    //         fn debug(
+    //             &self,
+    //             bounds: Bounds<Pixels>,
+    //             _: &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/workspace2/src/persistence.rs 🔗

@@ -6,12 +6,12 @@ use std::path::Path;
 
 use anyhow::{anyhow, bail, Context, Result};
 use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
-use gpui::WindowBounds;
+use gpui::{Axis, WindowBounds};
 
 use util::{unzip_option, ResultExt};
 use uuid::Uuid;
 
-use crate::{Axis, WorkspaceId};
+use crate::WorkspaceId;
 
 use model::{
     GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
@@ -403,7 +403,7 @@ impl WorkspaceDb {
         .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))
+                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
                     .transpose()?;
 
                 Ok(SerializedPaneGroup::Group {

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

@@ -1,13 +1,11 @@
-use crate::{
-    item::ItemHandle, Axis, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId,
-};
+use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
 use anyhow::{Context, Result};
 use async_recursion::async_recursion;
 use db::sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
     statement::Statement,
 };
-use gpui::{AsyncWindowContext, Model, Task, View, WeakView, WindowBounds};
+use gpui::{AsyncWindowContext, Axis, Model, Task, View, WeakView, WindowBounds};
 use project::Project;
 use std::{
     path::{Path, PathBuf},

crates/workspace2/src/workspace2.rs 🔗

@@ -29,12 +29,12 @@ use futures::{
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    actions, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView,
+    actions, canvas, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView,
     AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity,
     EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, InteractiveElement,
-    KeyContext, ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Point,
-    PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext,
-    WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
+    KeyContext, ManagedView, Model, ModelContext, MouseMoveEvent, ParentElement, PathPromptOptions,
+    Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext,
+    VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -227,6 +227,9 @@ pub fn init_settings(cx: &mut AppContext) {
 }
 
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
+    cx.default_global::<DockDragState>();
+    cx.default_global::<DockClickReset>();
+
     init_settings(cx);
     notifications::init(cx);
 
@@ -480,8 +483,6 @@ struct FollowerState {
     items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
 }
 
-enum WorkspaceBounds {}
-
 impl Workspace {
     pub fn new(
         workspace_id: WorkspaceId,
@@ -2032,7 +2033,7 @@ impl Workspace {
         };
         let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
         let center = match cursor {
-            Some(cursor) if bounding_box.contains_point(&cursor) => cursor,
+            Some(cursor) if bounding_box.contains(&cursor) => cursor,
             _ => bounding_box.center(),
         };
 
@@ -3571,6 +3572,16 @@ impl FocusableView for Workspace {
     }
 }
 
+struct WorkspaceBounds(Bounds<Pixels>);
+
+//todo!("remove this when better drag APIs are in GPUI2")
+#[derive(Default)]
+struct DockDragState(Option<DockPosition>);
+
+//todo!("remove this when better double APIs are in GPUI2")
+#[derive(Default)]
+struct DockClickReset(Option<Task<()>>);
+
 impl Render for Workspace {
     type Element = Div;
 
@@ -3614,6 +3625,37 @@ impl Render for Workspace {
                     .border_t()
                     .border_b()
                     .border_color(cx.theme().colors().border)
+                    .on_mouse_up(gpui::MouseButton::Left, |_, cx| {
+                        cx.update_global(|drag: &mut DockDragState, cx| {
+                            drag.0 = None;
+                        })
+                    })
+                    .on_mouse_move(cx.listener(|workspace, e: &MouseMoveEvent, cx| {
+                        if let Some(types) = &cx.global::<DockDragState>().0 {
+                            let workspace_bounds = cx.global::<WorkspaceBounds>().0;
+                            match types {
+                                DockPosition::Left => {
+                                    let size = e.position.x;
+                                    workspace.left_dock.update(cx, |left_dock, cx| {
+                                        left_dock.resize_active_panel(Some(size.0), cx);
+                                    });
+                                }
+                                DockPosition::Right => {
+                                    let size = workspace_bounds.size.width - e.position.x;
+                                    workspace.right_dock.update(cx, |right_dock, cx| {
+                                        right_dock.resize_active_panel(Some(size.0), cx);
+                                    });
+                                }
+                                DockPosition::Bottom => {
+                                    let size = workspace_bounds.size.height - e.position.y;
+                                    workspace.bottom_dock.update(cx, |bottom_dock, cx| {
+                                        bottom_dock.resize_active_panel(Some(size.0), cx);
+                                    });
+                                }
+                            }
+                        }
+                    }))
+                    .child(canvas(|bounds, cx| cx.set_global(WorkspaceBounds(bounds))))
                     .child(self.modal_layer.clone())
                     .child(
                         div()