Center dock resize handle hitboxes (#9225)

Antonio Scandurra , Julia , and Nathan created

Also, add a `deferred` function which takes an element to paint after
the current element tree.

Release Notes:

- Improved the size and position of the hitbox for resizing left, right,
and bottom panels.
([#8855](https://github.com/zed-industries/zed/issues/8855))

Co-authored-by: Julia <julia@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>

Change summary

crates/gpui/src/elements/deferred.rs | 63 ++++++++++++++++++++++++++++++
crates/gpui/src/elements/mod.rs      |  2 
crates/gpui/src/window/element_cx.rs |  3 
crates/workspace/src/dock.rs         | 45 ++++++++++----------
4 files changed, 88 insertions(+), 25 deletions(-)

Detailed changes

crates/gpui/src/elements/deferred.rs 🔗

@@ -0,0 +1,63 @@
+use crate::{AnyElement, Bounds, Element, ElementContext, IntoElement, LayoutId, Pixels};
+
+/// Builds a `Deferred` element, which delays the layout and paint of its child.
+pub fn deferred(child: impl IntoElement) -> Deferred {
+    Deferred {
+        child: Some(child.into_any_element()),
+        priority: 0,
+    }
+}
+
+/// An element which delays the painting of its child until after all of
+/// its ancestors, while keeping its layout as part of the current element tree.
+pub struct Deferred {
+    child: Option<AnyElement>,
+    priority: usize,
+}
+
+impl Element for Deferred {
+    type BeforeLayout = ();
+    type AfterLayout = ();
+
+    fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, ()) {
+        let layout_id = self.child.as_mut().unwrap().before_layout(cx);
+        (layout_id, ())
+    }
+
+    fn after_layout(
+        &mut self,
+        _bounds: Bounds<Pixels>,
+        _before_layout: &mut Self::BeforeLayout,
+        cx: &mut ElementContext,
+    ) {
+        let child = self.child.take().unwrap();
+        let element_offset = cx.element_offset();
+        cx.defer_draw(child, element_offset, self.priority)
+    }
+
+    fn paint(
+        &mut self,
+        _bounds: Bounds<Pixels>,
+        _before_layout: &mut Self::BeforeLayout,
+        _after_layout: &mut Self::AfterLayout,
+        _cx: &mut ElementContext,
+    ) {
+    }
+}
+
+impl IntoElement for Deferred {
+    type Element = Self;
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}
+
+impl Deferred {
+    /// Sets a priority for the element. A higher priority conceptually means painting the element
+    /// on top of deferred draws with a lower priority (i.e. closer to the viewer).
+    pub fn priority(mut self, priority: usize) -> Self {
+        self.priority = priority;
+        self
+    }
+}

crates/gpui/src/elements/mod.rs 🔗

@@ -1,4 +1,5 @@
 mod canvas;
+mod deferred;
 mod div;
 mod img;
 mod list;
@@ -8,6 +9,7 @@ mod text;
 mod uniform_list;
 
 pub use canvas::*;
+pub use deferred::*;
 pub use div::*;
 pub use img::*;
 pub use list::*;

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

@@ -381,8 +381,7 @@ impl<'a> ElementContext<'a> {
 
         let mut sorted_deferred_draws =
             (0..self.window.next_frame.deferred_draws.len()).collect::<SmallVec<[_; 8]>>();
-        sorted_deferred_draws
-            .sort_unstable_by_key(|ix| self.window.next_frame.deferred_draws[*ix].priority);
+        sorted_deferred_draws.sort_by_key(|ix| self.window.next_frame.deferred_draws[*ix].priority);
         self.layout_deferred_draws(&sorted_deferred_draws);
 
         self.window.mouse_hit_test = self.window.next_frame.hit_test(self.window.mouse_position);

crates/workspace/src/dock.rs 🔗

@@ -2,10 +2,10 @@ use crate::persistence::model::DockData;
 use crate::DraggedDock;
 use crate::{status_bar::StatusItemView, Workspace};
 use gpui::{
-    div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Entity, EntityId,
-    EventEmitter, FocusHandle, FocusableView, IntoElement, KeyContext, MouseButton, ParentElement,
-    Render, SharedString, StyleRefinement, Styled, Subscription, View, ViewContext, VisualContext,
-    WeakView, WindowContext,
+    deferred, div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Entity,
+    EntityId, EventEmitter, FocusHandle, FocusableView, IntoElement, KeyContext, MouseButton,
+    ParentElement, Render, SharedString, StyleRefinement, Styled, Subscription, View, ViewContext,
+    VisualContext, WeakView, WindowContext,
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -551,7 +551,7 @@ impl Render for Dock {
             let size = entry.panel.size(cx);
 
             let position = self.position;
-            let mut handle = div()
+            let handle = div()
                 .id("resize-handle")
                 .on_drag(DraggedDock(position), |dock, cx| {
                     cx.stop_propagation();
@@ -564,36 +564,35 @@ impl Render for Dock {
                     }
                 }))
                 .occlude();
-
-            match self.position() {
-                DockPosition::Left => {
-                    handle = handle
+            let handle = match self.position() {
+                DockPosition::Left => deferred(
+                    handle
                         .absolute()
-                        .right(px(0.))
+                        .right(-RESIZE_HANDLE_SIZE / 2.)
                         .top(px(0.))
                         .h_full()
                         .w(RESIZE_HANDLE_SIZE)
-                        .cursor_col_resize();
-                }
-                DockPosition::Bottom => {
-                    handle = handle
+                        .cursor_col_resize(),
+                ),
+                DockPosition::Bottom => deferred(
+                    handle
                         .absolute()
-                        .top(px(0.))
+                        .top(-RESIZE_HANDLE_SIZE / 2.)
                         .left(px(0.))
                         .w_full()
                         .h(RESIZE_HANDLE_SIZE)
-                        .cursor_row_resize();
-                }
-                DockPosition::Right => {
-                    handle = handle
+                        .cursor_row_resize(),
+                ),
+                DockPosition::Right => deferred(
+                    handle
                         .absolute()
                         .top(px(0.))
-                        .left(px(0.))
+                        .left(-RESIZE_HANDLE_SIZE / 2.)
                         .h_full()
                         .w(RESIZE_HANDLE_SIZE)
-                        .cursor_col_resize();
-                }
-            }
+                        .cursor_col_resize(),
+                ),
+            };
 
             div()
                 .key_context(dispatch_context)