Checkpoint

Nathan Sobo created

Change summary

crates/gpui2/docs/contexts.md     |   0 
crates/gpui2/docs/key_dispatch.md | 101 ++++
crates/gpui2/src/app.rs           |   2 
crates/gpui2/src/elements.rs      |   1 
crates/gpui2/src/elements/div.rs  |   4 
crates/gpui2/src/elements/node.rs | 767 +++++++++++++++++++++++++++++++++
crates/gpui2/src/style.rs         |   2 
crates/gpui2/src/window.rs        |  21 
8 files changed, 894 insertions(+), 4 deletions(-)

Detailed changes

crates/gpui2/docs/key_dispatch.md 🔗

@@ -0,0 +1,101 @@
+# Key Dispatch
+
+GPUI is designed for keyboard-first interactivity.
+
+To expose functionality to the mouse, you render a button with a click handler.
+
+To expose functionality to the keyboard, you bind an *action* in a *key context*.
+
+Actions are similar to framework-level events like `MouseDown`, `KeyDown`, etc, but you can define them yourself:
+
+```rust
+mod menu {
+    #[gpui::action]
+    struct MoveUp;
+
+    #[gpui::action]
+    struct MoveDown;
+}
+```
+
+Actions are frequently unit structs, for which we have a macro. The above could also be written:
+
+```rust
+mod menu {
+    actions!(MoveUp, MoveDown);
+}
+```
+
+Actions can also be more complex types:
+
+```rust
+mod menu {
+    #[gpui::action]
+    struct Move {
+        direction: Direction,
+        select: bool,
+    }
+}
+```
+
+To bind actions, chain `on_action` on to your element:
+
+```rust
+impl Render for Menu {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component {
+        div()
+            .on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext<Menu>| {
+                // ...
+            })
+            .on_action(|this, move: &MoveDown, cx| {
+                // ...
+            })
+            .children(todo!())
+    }
+}
+```
+
+In order to bind keys to actions, you need to declare a *key context* for part of the element tree by calling `key_context`.
+
+```rust
+impl Render for Menu {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component {
+        div()
+            .key_context("menu")
+            .on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext<Menu>| {
+                // ...
+            })
+            .on_action(|this, move: &MoveDown, cx| {
+                // ...
+            })
+            .children(todo!())
+    }
+}
+```
+
+Now you can target your context in the keymap. Note how actions are identified in the keymap by their fully-qualified type name.
+
+```json
+{
+  "context": "menu",
+  "bindings": {
+    "up": "menu::MoveUp",
+    "down": "menu::MoveDown"
+  }
+}
+```
+
+If you had opted for the more complex type definition, you'd provide the serialized representation of the action alongside the name:
+
+```json
+{
+  "context": "menu",
+  "bindings": {
+    "up": ["menu::Move", {direction: "up", select: false}]
+    "down": ["menu::Move", {direction: "down", select: false}]
+    "shift-up": ["menu::Move", {direction: "up", select: true}]
+    "shift-down": ["menu::Move", {direction: "down", select: true}]
+  }
+}
+
+```

crates/gpui2/src/app.rs 🔗

@@ -1068,7 +1068,7 @@ impl<G: 'static> DerefMut for GlobalLease<G> {
 
 /// Contains state associated with an active drag operation, started by dragging an element
 /// within the window or by dragging into the app from the underlying platform.
-pub(crate) struct AnyDrag {
+pub struct AnyDrag {
     pub view: AnyView,
     pub cursor_offset: Point<Pixels>,
 }

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

@@ -252,7 +252,7 @@ where
         cx: &mut ViewContext<V>,
     ) -> LayoutId {
         let style = self.compute_style(Bounds::default(), element_state, cx);
-        style.apply_text_style(cx, |cx| {
+        style.with_text_style(cx, |cx| {
             self.with_element_id(cx, |this, _global_id, cx| {
                 let layout_ids = this
                     .children
@@ -318,7 +318,7 @@ where
                     );
                 });
                 cx.with_z_index(1, |cx| {
-                    style.apply_text_style(cx, |cx| {
+                    style.with_text_style(cx, |cx| {
                         style.apply_overflow(bounds, cx, |cx| {
                             let scroll_offset = element_state.interactive.scroll_offset();
                             cx.with_element_offset(scroll_offset, |cx| {

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

@@ -0,0 +1,767 @@
+use crate::{
+    point, Action, AnyDrag, AnyElement, AnyView, AppContext, BorrowWindow, Bounds, ClickEvent,
+    DispatchPhase, Element, FocusHandle, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId,
+    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Render,
+    ScrollWheelEvent, SharedString, Style, StyleRefinement, Styled, View, ViewContext, Visibility,
+};
+use collections::HashMap;
+use refineable::Refineable;
+use smallvec::SmallVec;
+use std::{
+    any::{Any, TypeId},
+    marker::PhantomData,
+    sync::Arc,
+};
+
+pub struct GroupStyle {
+    pub group: SharedString,
+    pub style: StyleRefinement,
+}
+
+pub trait InteractiveComponent<V: 'static> {
+    fn interactivity(&mut self) -> &mut Interactivity<V>;
+
+    fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().hover_style = f(StyleRefinement::default());
+        self
+    }
+
+    fn group_hover(
+        mut self,
+        group_name: impl Into<SharedString>,
+        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().group_hover_style = Some(GroupStyle {
+            group: group_name.into(),
+            style: f(StyleRefinement::default()),
+        });
+        self
+    }
+
+    fn on_mouse_down(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().mouse_down_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble
+                    && event.button == button
+                    && bounds.contains_point(&event.position)
+                {
+                    handler(view, event, cx)
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_mouse_up(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().mouse_up_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble
+                    && event.button == button
+                    && bounds.contains_point(&event.position)
+                {
+                    handler(view, event, cx)
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_mouse_down_out(
+        mut self,
+        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().mouse_down_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) {
+                    handler(view, event, cx)
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_mouse_up_out(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().mouse_up_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Capture
+                    && event.button == button
+                    && !bounds.contains_point(&event.position)
+                {
+                    handler(view, event, cx);
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_mouse_move(
+        mut self,
+        handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().mouse_move_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    handler(view, event, cx);
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_scroll_wheel(
+        mut self,
+        handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().scroll_wheel_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    handler(view, event, cx);
+                }
+            },
+        ));
+        self
+    }
+
+    /// Capture the given action, fires during the capture phase
+    fn capture_action<A: Action>(
+        mut self,
+        listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().action_listeners.push((
+            TypeId::of::<A>(),
+            Box::new(move |view, action, phase, cx| {
+                let action = action.downcast_ref().unwrap();
+                if phase == DispatchPhase::Capture {
+                    listener(view, action, cx)
+                }
+            }),
+        ));
+        self
+    }
+
+    /// Add a listener for the given action, fires during the bubble event phase
+    fn on_action<A: Action>(
+        mut self,
+        listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().action_listeners.push((
+            TypeId::of::<A>(),
+            Box::new(move |view, action, phase, cx| {
+                let action = action.downcast_ref().unwrap();
+                if phase == DispatchPhase::Bubble {
+                    listener(view, action, cx)
+                }
+            }),
+        ));
+        self
+    }
+
+    fn on_key_down(
+        mut self,
+        listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity()
+            .key_down_listeners
+            .push(Box::new(move |view, event, phase, cx| {
+                listener(view, event, phase, cx)
+            }));
+        self
+    }
+
+    fn on_key_up(
+        mut self,
+        listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity()
+            .key_up_listeners
+            .push(Box::new(move |view, event, phase, cx| {
+                listener(view, event, phase, cx)
+            }));
+        self
+    }
+
+    fn drag_over<S: 'static>(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity()
+            .drag_over_styles
+            .push((TypeId::of::<S>(), f(StyleRefinement::default())));
+        self
+    }
+
+    fn group_drag_over<S: 'static>(
+        mut self,
+        group_name: impl Into<SharedString>,
+        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().group_drag_over_styles.push((
+            TypeId::of::<S>(),
+            GroupStyle {
+                group: group_name.into(),
+                style: f(StyleRefinement::default()),
+            },
+        ));
+        self
+    }
+
+    fn on_drop<W: 'static>(
+        mut self,
+        listener: impl Fn(&mut V, View<W>, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().drop_listeners.push((
+            TypeId::of::<W>(),
+            Box::new(move |view, dragged_view, cx| {
+                listener(view, dragged_view.downcast().unwrap(), cx);
+            }),
+        ));
+        self
+    }
+}
+
+pub trait StatefulInteractiveComponent<V: 'static> {
+    fn interactivity(&mut self) -> &mut StatefulInteractivity<V>;
+
+    fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().active_style = f(StyleRefinement::default());
+        self
+    }
+
+    fn group_active(
+        mut self,
+        group_name: impl Into<SharedString>,
+        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().group_active_style = Some(GroupStyle {
+            group: group_name.into(),
+            style: f(StyleRefinement::default()),
+        });
+        self
+    }
+
+    fn on_click(
+        mut self,
+        listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity()
+            .click_listeners
+            .push(Box::new(move |view, event, cx| listener(view, event, cx)));
+        self
+    }
+
+    fn on_drag<W>(
+        mut self,
+        listener: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+        W: 'static + Render,
+    {
+        debug_assert!(
+            self.interactivity().drag_listener.is_none(),
+            "calling on_drag more than once on the same element is not supported"
+        );
+        self.interactivity().drag_listener =
+            Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag {
+                view: listener(view_state, cx).into(),
+                cursor_offset,
+            }));
+        self
+    }
+
+    fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext<V>)) -> Self
+    where
+        Self: Sized,
+    {
+        debug_assert!(
+            self.interactivity().hover_listener.is_none(),
+            "calling on_hover more than once on the same element is not supported"
+        );
+        self.interactivity().hover_listener = Some(Box::new(listener));
+        self
+    }
+
+    fn tooltip<W>(
+        mut self,
+        build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+        W: 'static + Render,
+    {
+        debug_assert!(
+            self.interactivity().tooltip_builder.is_none(),
+            "calling tooltip more than once on the same element is not supported"
+        );
+        self.interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| {
+            build_tooltip(view_state, cx).into()
+        }));
+
+        self
+    }
+}
+
+pub trait FocusableComponent<V> {
+    fn focusability(&mut self) -> &mut Focusability<V>;
+
+    fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.focusability().focus_style = f(StyleRefinement::default());
+        self
+    }
+
+    fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.focusability().focus_in_style = f(StyleRefinement::default());
+        self
+    }
+
+    fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        // self.focusability(). (f(StyleRefinement::default()));
+        self
+    }
+
+    fn on_focus(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.focusability()
+            .focus_listeners
+            .push(Box::new(move |view, focus_handle, event, cx| {
+                if event.focused.as_ref() == Some(focus_handle) {
+                    listener(view, event, cx)
+                }
+            }));
+        self
+    }
+
+    fn on_blur(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.focusability()
+            .focus_listeners
+            .push(Box::new(move |view, focus_handle, event, cx| {
+                if event.blurred.as_ref() == Some(focus_handle) {
+                    listener(view, event, cx)
+                }
+            }));
+        self
+    }
+
+    fn on_focus_in(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.focusability()
+            .focus_listeners
+            .push(Box::new(move |view, focus_handle, event, cx| {
+                let descendant_blurred = event
+                    .blurred
+                    .as_ref()
+                    .map_or(false, |blurred| focus_handle.contains(blurred, cx));
+                let descendant_focused = event
+                    .focused
+                    .as_ref()
+                    .map_or(false, |focused| focus_handle.contains(focused, cx));
+
+                if !descendant_blurred && descendant_focused {
+                    listener(view, event, cx)
+                }
+            }));
+        self
+    }
+
+    fn on_focus_out(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.focusability()
+            .focus_listeners
+            .push(Box::new(move |view, focus_handle, event, cx| {
+                let descendant_blurred = event
+                    .blurred
+                    .as_ref()
+                    .map_or(false, |blurred| focus_handle.contains(blurred, cx));
+                let descendant_focused = event
+                    .focused
+                    .as_ref()
+                    .map_or(false, |focused| focus_handle.contains(focused, cx));
+                if descendant_blurred && !descendant_focused {
+                    listener(view, event, cx)
+                }
+            }));
+        self
+    }
+}
+
+pub type FocusListeners<V> = SmallVec<[FocusListener<V>; 2]>;
+
+pub type FocusListener<V> =
+    Box<dyn Fn(&mut V, &FocusHandle, &FocusEvent, &mut ViewContext<V>) + 'static>;
+
+pub type MouseDownListener<V> = Box<
+    dyn Fn(&mut V, &MouseDownEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
+>;
+pub type MouseUpListener<V> = Box<
+    dyn Fn(&mut V, &MouseUpEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
+>;
+
+pub type MouseMoveListener<V> = Box<
+    dyn Fn(&mut V, &MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
+>;
+
+pub type ScrollWheelListener<V> = Box<
+    dyn Fn(&mut V, &ScrollWheelEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
+        + 'static,
+>;
+
+pub type ClickListener<V> = Box<dyn Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static>;
+
+pub type DragListener<V> =
+    Box<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + 'static>;
+
+type DropListener<V> = dyn Fn(&mut V, AnyView, &mut ViewContext<V>) + 'static;
+
+pub type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>) + 'static>;
+
+pub type TooltipBuilder<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
+
+pub type KeyDownListener<V> =
+    Box<dyn Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
+
+pub type KeyUpListener<V> =
+    Box<dyn Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
+
+pub type ActionListener<V> =
+    Box<dyn Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext<V>) + 'static>;
+
+pub struct FocusEvent {
+    pub blurred: Option<FocusHandle>,
+    pub focused: Option<FocusHandle>,
+}
+
+pub struct Node<V> {
+    style: StyleRefinement,
+    key_context: KeyContext,
+    interactivity: Interactivity<V>,
+    children: Vec<AnyElement<V>>,
+}
+
+pub struct Interactivity<V> {
+    group: Option<SharedString>,
+    pub dispatch_context: KeyContext,
+    pub mouse_down_listeners: SmallVec<[MouseDownListener<V>; 2]>,
+    pub mouse_up_listeners: SmallVec<[MouseUpListener<V>; 2]>,
+    pub mouse_move_listeners: SmallVec<[MouseMoveListener<V>; 2]>,
+    pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener<V>; 2]>,
+    pub key_down_listeners: SmallVec<[KeyDownListener<V>; 2]>,
+    pub key_up_listeners: SmallVec<[KeyUpListener<V>; 2]>,
+    pub action_listeners: SmallVec<[(TypeId, ActionListener<V>); 8]>,
+    pub hover_style: StyleRefinement,
+    pub group_hover_style: Option<GroupStyle>,
+    drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>,
+    group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>,
+    drop_listeners: SmallVec<[(TypeId, Box<DropListener<V>>); 2]>,
+    scroll_offset: Point<Pixels>,
+}
+
+impl<V> Node<V> {
+    fn compute_style(&self) -> Style {
+        let mut style = Style::default();
+        style.refine(&self.style);
+        style
+    }
+}
+
+impl<V> Styled for Node<V> {
+    fn style(&mut self) -> &mut StyleRefinement {
+        &mut self.style
+    }
+}
+
+impl<V: 'static> InteractiveComponent<V> for Node<V> {
+    fn interactivity(&mut self) -> &mut Interactivity<V> {
+        &mut self.interactivity
+    }
+}
+
+pub struct NodeState {
+    child_layout_ids: SmallVec<[LayoutId; 4]>,
+}
+
+impl<V: 'static> Element<V> for Node<V> {
+    type ElementState = NodeState;
+
+    fn id(&self) -> Option<crate::ElementId> {
+        None
+    }
+
+    fn initialize(
+        &mut self,
+        view_state: &mut V,
+        _: Option<Self::ElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> Self::ElementState {
+        for child in &mut self.children {
+            child.initialize(view_state, cx);
+        }
+        NodeState {
+            child_layout_ids: SmallVec::new(),
+        }
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) -> crate::LayoutId {
+        let style = self.compute_style();
+        style.with_text_style(cx, |cx| {
+            element_state.child_layout_ids = self
+                .children
+                .iter_mut()
+                .map(|child| child.layout(view_state, cx))
+                .collect::<SmallVec<_>>();
+            cx.request_layout(&style, element_state.child_layout_ids.iter().copied())
+        })
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        let style = self.compute_style();
+        if style.visibility == Visibility::Hidden {
+            return;
+        }
+
+        if let Some(mouse_cursor) = style.mouse_cursor {
+            let hovered = bounds.contains_point(&cx.mouse_position());
+            if hovered {
+                cx.set_cursor_style(mouse_cursor);
+            }
+        }
+
+        if let Some(group) = self.interactivity.group.clone() {
+            GroupBounds::push(group, bounds, cx);
+        }
+
+        let z_index = style.z_index.unwrap_or(0);
+
+        let mut child_min = point(Pixels::MAX, Pixels::MAX);
+        let mut child_max = Point::default();
+
+        let content_size = if element_state.child_layout_ids.is_empty() {
+            bounds.size
+        } else {
+            for child_layout_id in &element_state.child_layout_ids {
+                let child_bounds = cx.layout_bounds(*child_layout_id);
+                child_min = child_min.min(&child_bounds.origin);
+                child_max = child_max.max(&child_bounds.lower_right());
+            }
+            (child_max - child_min).into()
+        };
+
+        cx.with_z_index(z_index, |cx| {
+            cx.with_z_index(0, |cx| {
+                style.paint(bounds, cx);
+            });
+            cx.with_z_index(1, |cx| {
+                style.with_text_style(cx, |cx| {
+                    style.apply_overflow(bounds, cx, |cx| {
+                        let scroll_offset = self.interactivity.scroll_offset;
+                        cx.with_element_offset2(scroll_offset, |cx| {
+                            for child in &mut self.children {
+                                child.paint(view_state, cx);
+                            }
+                        });
+                    })
+                })
+            });
+        });
+
+        if let Some(group) = self.interactivity.group.as_ref() {
+            GroupBounds::pop(group, cx);
+        }
+    }
+}
+
+#[derive(Default)]
+pub struct GroupBounds(HashMap<SharedString, SmallVec<[Bounds<Pixels>; 1]>>);
+
+impl GroupBounds {
+    pub fn get(name: &SharedString, cx: &mut AppContext) -> Option<Bounds<Pixels>> {
+        cx.default_global::<Self>()
+            .0
+            .get(name)
+            .and_then(|bounds_stack| bounds_stack.last())
+            .cloned()
+    }
+
+    pub fn push(name: SharedString, bounds: Bounds<Pixels>, cx: &mut AppContext) {
+        cx.default_global::<Self>()
+            .0
+            .entry(name)
+            .or_default()
+            .push(bounds);
+    }
+
+    pub fn pop(name: &SharedString, cx: &mut AppContext) {
+        cx.default_global::<Self>().0.get_mut(name).unwrap().pop();
+    }
+}
+
+pub struct Focusable<V, E> {
+    focusability: Focusability<V>,
+    view_type: PhantomData<V>,
+    element: E,
+}
+
+pub struct Focusability<V> {
+    focus_handle: Option<FocusHandle>,
+    focus_listeners: FocusListeners<V>,
+    focus_style: StyleRefinement,
+    focus_in_style: StyleRefinement,
+    in_focus_style: StyleRefinement,
+}
+
+impl<V, E> FocusableComponent<V> for Focusable<V, E> {
+    fn focusability(&mut self) -> &mut Focusability<V> {
+        &mut self.focusability
+    }
+}
+
+impl<V: 'static, E: InteractiveComponent<V>> InteractiveComponent<V> for Focusable<V, E> {
+    fn interactivity(&mut self) -> &mut Interactivity<V> {
+        self.element.interactivity()
+    }
+}
+
+impl<V: 'static, E: StatefulInteractiveComponent<V>> StatefulInteractiveComponent<V>
+    for Focusable<V, E>
+{
+    fn interactivity(&mut self) -> &mut StatefulInteractivity<V> {
+        self.element.interactivity()
+    }
+}
+
+pub struct Stateful<V, E> {
+    id: SharedString,
+    interactivity: StatefulInteractivity<V>,
+    view_type: PhantomData<V>,
+    element: E,
+}
+
+pub struct StatefulInteractivity<V> {
+    click_listeners: SmallVec<[ClickListener<V>; 2]>,
+    active_style: StyleRefinement,
+    group_active_style: Option<GroupStyle>,
+    drag_listener: Option<DragListener<V>>,
+    hover_listener: Option<HoverListener<V>>,
+    tooltip_builder: Option<TooltipBuilder<V>>,
+}
+
+impl<V: 'static, E> StatefulInteractiveComponent<V> for Stateful<V, E> {
+    fn interactivity(&mut self) -> &mut StatefulInteractivity<V> {
+        &mut self.interactivity
+    }
+}
+
+impl<V: 'static, E: InteractiveComponent<V>> InteractiveComponent<V> for Stateful<V, E> {
+    fn interactivity(&mut self) -> &mut Interactivity<V> {
+        self.element.interactivity()
+    }
+}
+
+impl<V, E: FocusableComponent<V>> FocusableComponent<V> for Stateful<V, E> {
+    fn focusability(&mut self) -> &mut Focusability<V> {
+        self.element.focusability()
+    }
+}

crates/gpui2/src/style.rs 🔗

@@ -228,7 +228,7 @@ impl Style {
         }
     }
 
-    pub fn apply_text_style<C, F, R>(&self, cx: &mut C, f: F) -> R
+    pub fn with_text_style<C, F, R>(&self, cx: &mut C, f: F) -> R
     where
         C: BorrowAppContext,
         F: FnOnce(&mut C) -> R,

crates/gpui2/src/window.rs 🔗

@@ -1587,6 +1587,27 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
         result
     }
 
+    /// Update the global element offset based on the given offset. This is used to implement
+    /// scrolling and position drag handles.
+    fn with_element_offset2<R>(
+        &mut self,
+        offset: Point<Pixels>,
+        f: impl FnOnce(&mut Self) -> R,
+    ) -> R {
+        if offset.is_zero() {
+            return f(self);
+        };
+
+        let offset = self.element_offset() + offset;
+        self.window_mut()
+            .current_frame
+            .element_offset_stack
+            .push(offset);
+        let result = f(self);
+        self.window_mut().current_frame.element_offset_stack.pop();
+        result
+    }
+
     /// Obtain the current element offset.
     fn element_offset(&self) -> Point<Pixels> {
         self.window()