Add a new `Hooks` element to invoke a callback before layout

Antonio Scandurra created

This is useful to cap the width of sidebars when dragging the
resize handles beyond the maximum bounds of the sidebar.

Change summary

gpui/src/elements.rs         |  2 
gpui/src/elements/hooks.rs   | 79 ++++++++++++++++++++++++++++++++++++++
zed/src/workspace/sidebar.rs | 15 +++++-
3 files changed, 93 insertions(+), 3 deletions(-)

Detailed changes

gpui/src/elements.rs 🔗

@@ -5,6 +5,7 @@ mod container;
 mod empty;
 mod event_handler;
 mod flex;
+mod hooks;
 mod label;
 mod line_box;
 mod list;
@@ -23,6 +24,7 @@ pub use container::*;
 pub use empty::*;
 pub use event_handler::*;
 pub use flex::*;
+pub use hooks::*;
 pub use label::*;
 pub use line_box::*;
 pub use list::*;

gpui/src/elements/hooks.rs 🔗

@@ -0,0 +1,79 @@
+use crate::{
+    geometry::{rect::RectF, vector::Vector2F},
+    json::json,
+    DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
+    SizeConstraint,
+};
+
+pub struct Hooks {
+    child: ElementBox,
+    before_layout: Option<Box<dyn FnMut(SizeConstraint, &mut LayoutContext)>>,
+}
+
+impl Hooks {
+    pub fn new(child: ElementBox) -> Self {
+        Self {
+            child,
+            before_layout: None,
+        }
+    }
+
+    pub fn on_before_layout(
+        mut self,
+        f: impl 'static + FnMut(SizeConstraint, &mut LayoutContext),
+    ) -> Self {
+        self.before_layout = Some(Box::new(f));
+        self
+    }
+}
+
+impl Element for Hooks {
+    type LayoutState = ();
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: SizeConstraint,
+        cx: &mut LayoutContext,
+    ) -> (Vector2F, Self::LayoutState) {
+        if let Some(handler) = self.before_layout.as_mut() {
+            handler(constraint, cx);
+        }
+        let size = self.child.layout(constraint, cx);
+        (size, ())
+    }
+
+    fn paint(
+        &mut self,
+        bounds: RectF,
+        visible_bounds: RectF,
+        _: &mut Self::LayoutState,
+        cx: &mut PaintContext,
+    ) {
+        self.child.paint(bounds.origin(), visible_bounds, cx);
+    }
+
+    fn dispatch_event(
+        &mut self,
+        event: &Event,
+        _: RectF,
+        _: &mut Self::LayoutState,
+        _: &mut Self::PaintState,
+        cx: &mut EventContext,
+    ) -> bool {
+        self.child.dispatch_event(event, cx)
+    }
+
+    fn debug(
+        &self,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &DebugContext,
+    ) -> serde_json::Value {
+        json!({
+            "type": "Hooks",
+            "child": self.child.debug(cx),
+        })
+    }
+}

zed/src/workspace/sidebar.rs 🔗

@@ -112,12 +112,21 @@ impl Sidebar {
             if matches!(self.side, Side::Right) {
                 container.add_child(self.render_resize_handle(settings, cx));
             }
+
+            let width = self.width.clone();
             container.add_child(
                 Flexible::new(
                     1.,
-                    ConstrainedBox::new(ChildView::new(active_item.id()).boxed())
-                        .with_max_width(*self.width.borrow())
-                        .boxed(),
+                    Hooks::new(
+                        ConstrainedBox::new(ChildView::new(active_item.id()).boxed())
+                            .with_max_width(*self.width.borrow())
+                            .boxed(),
+                    )
+                    .on_before_layout(move |constraint, _| {
+                        let mut width = width.borrow_mut();
+                        *width = width.min(constraint.max.x());
+                    })
+                    .boxed(),
                 )
                 .boxed(),
             );