Checkpoint

Nathan Sobo created

Change summary

crates/editor2/src/element.rs     |   4 
crates/gpui2/src/element.rs       |   2 
crates/gpui2/src/elements/div.rs  |   6 
crates/gpui2/src/elements/node.rs | 582 +++++++++++++++++++++++++++-----
crates/gpui2/src/gpui2.rs         |  16 
crates/gpui2/src/style.rs         |  44 ++
crates/gpui2/src/window.rs        |  47 -
7 files changed, 548 insertions(+), 153 deletions(-)

Detailed changes

crates/editor2/src/element.rs 🔗

@@ -616,7 +616,7 @@ impl EditorElement {
         let line_end_overshoot = 0.15 * layout.position_map.line_height;
         let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces;
 
-        cx.with_content_mask(ContentMask { bounds }, |cx| {
+        cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
             // todo!("cursor region")
             // cx.scene().push_cursor_region(CursorRegion {
             //     bounds,
@@ -2659,7 +2659,7 @@ impl Element<Editor> for EditorElement {
 
         // We call with_z_index to establish a new stacking context.
         cx.with_z_index(0, |cx| {
-            cx.with_content_mask(ContentMask { bounds }, |cx| {
+            cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
                 self.paint_mouse_listeners(
                     bounds,
                     gutter_bounds,

crates/gpui2/src/element.rs 🔗

@@ -255,7 +255,7 @@ where
         // Ignore the element offset when drawing this element, as the origin is already specified
         // in absolute terms.
         origin -= cx.element_offset();
-        cx.with_element_offset(Some(origin), |cx| self.paint(view_state, cx))
+        cx.with_element_offset(origin, |cx| self.paint(view_state, cx))
     }
 }
 

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.with_text_style(cx, |cx| {
+        style.apply_text_style(cx, |cx| {
             self.with_element_id(cx, |this, _global_id, cx| {
                 let layout_ids = this
                     .children
@@ -318,10 +318,10 @@ where
                     );
                 });
                 cx.with_z_index(1, |cx| {
-                    style.with_text_style(cx, |cx| {
+                    style.apply_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| {
+                            cx.with_element_offset(scroll_offset.unwrap_or_default(), |cx| {
                                 for child in &mut this.children {
                                     child.paint(view_state, cx);
                                 }

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

@@ -1,10 +1,12 @@
 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,
+    point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext,
+    BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, FocusHandle, KeyContext,
+    KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
+    Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled,
+    Task, View, ViewContext, Visibility,
 };
 use collections::HashMap;
+use parking_lot::Mutex;
 use refineable::Refineable;
 use smallvec::SmallVec;
 use std::{
@@ -12,14 +14,20 @@ use std::{
     marker::PhantomData,
     mem,
     sync::Arc,
+    time::Duration,
 };
+use taffy::style::Overflow;
+
+const DRAG_THRESHOLD: f64 = 2.;
+const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
+const TOOLTIP_OFFSET: Point<Pixels> = Point::new(px(10.0), px(8.0));
 
 pub struct GroupStyle {
     pub group: SharedString,
     pub style: StyleRefinement,
 }
 
-pub trait InteractiveComponent<V: 'static> {
+pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
     fn interactivity(&mut self) -> &mut Interactivity<V>;
 
     fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
@@ -274,7 +282,7 @@ pub trait InteractiveComponent<V: 'static> {
     }
 }
 
-pub trait StatefulInteractiveComponent<V: 'static>: InteractiveComponent<V> {
+pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveComponent<V> {
     fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
     where
         Self: Sized,
@@ -541,6 +549,13 @@ impl<V: 'static> InteractiveComponent<V> for Node<V> {
 
 pub struct NodeState {
     child_layout_ids: SmallVec<[LayoutId; 4]>,
+    interactive_state: InteractiveElementState,
+}
+
+impl AsMut<InteractiveElementState> for InteractiveElementState {
+    fn as_mut(&mut self) -> &mut InteractiveElementState {
+        self
+    }
 }
 
 impl<V: 'static> Element<V> for Node<V> {
@@ -553,7 +568,7 @@ impl<V: 'static> Element<V> for Node<V> {
     fn initialize(
         &mut self,
         view_state: &mut V,
-        _: Option<Self::ElementState>,
+        previous_element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
     ) -> Self::ElementState {
         for child in &mut self.children {
@@ -561,6 +576,9 @@ impl<V: 'static> Element<V> for Node<V> {
         }
         NodeState {
             child_layout_ids: SmallVec::new(),
+            interactive_state: previous_element_state
+                .map(|s| s.interactive_state)
+                .unwrap_or_default(),
         }
     }
 
@@ -570,15 +588,20 @@ impl<V: 'static> Element<V> for Node<V> {
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) -> crate::LayoutId {
-        let style = self.interactivity().compute_style(None, cx);
-        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())
-        })
+        let mut interactivity = mem::take(&mut self.interactivity);
+        let layout_id =
+            interactivity.layout(&mut element_state.interactive_state, cx, |style, cx| {
+                cx.with_text_style(style.text_style().cloned(), |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())
+                })
+            });
+        self.interactivity = interactivity;
+        layout_id
     }
 
     fn paint(
@@ -588,27 +611,10 @@ impl<V: 'static> Element<V> for Node<V> {
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) {
-        let style = self.interactivity.compute_style(Some(bounds), cx);
-        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 interactivity = mem::take(&mut self.interactivity);
 
         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 {
@@ -620,35 +626,41 @@ impl<V: 'static> Element<V> for Node<V> {
             (child_max - child_min).into()
         };
 
-        let mut interactivity = mem::take(&mut self.interactivity);
-        interactivity.paint(bounds, cx, |cx| {
-            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);
-                                }
-                            });
+        interactivity.paint(
+            bounds,
+            content_size,
+            &mut element_state.interactive_state,
+            cx,
+            |style, scroll_offset, cx| {
+                if style.visibility == Visibility::Hidden {
+                    return;
+                }
+
+                let z_index = style.z_index.unwrap_or(0);
+
+                cx.with_z_index(z_index, |cx| {
+                    cx.with_z_index(0, |cx| {
+                        style.paint(bounds, cx);
+                    });
+                    cx.with_z_index(1, |cx| {
+                        cx.with_text_style(style.text_style().cloned(), |cx| {
+                            cx.with_content_mask(style.overflow_mask(bounds), |cx| {
+                                cx.with_element_offset(scroll_offset, |cx| {
+                                    for child in &mut self.children {
+                                        child.paint(view_state, cx);
+                                    }
+                                })
+                            })
                         })
                     })
-                });
-            });
-        });
+                })
+            },
+        );
         self.interactivity = interactivity;
-
-        if let Some(group) = self.interactivity.group.as_ref() {
-            GroupBounds::pop(group, cx);
-        }
     }
 }
 
-pub enum FocusState {
+pub enum FocusStatus {
     /// The current element is not focused, and does not contain or descend from the focused element.
     None,
     /// The current element is focused.
@@ -660,56 +672,87 @@ pub enum FocusState {
 }
 
 pub struct Interactivity<V> {
-    pub active: bool,
-    pub group_active: bool,
-    pub hovered: bool,
-    pub group_hovered: bool,
-    pub focus: FocusState,
-    pub key_context: KeyContext,
-    pub focus_handle: Option<FocusHandle>,
-    pub scroll_offset: Point<Pixels>,
-    pub base_style: StyleRefinement,
-    pub focus_style: StyleRefinement,
-    pub focus_in_style: StyleRefinement,
-    pub in_focus_style: StyleRefinement,
-    pub hover_style: StyleRefinement,
-    pub group_hover_style: Option<GroupStyle>,
-    pub active_style: StyleRefinement,
-    pub group_active_style: Option<GroupStyle>,
-    pub drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>,
-    pub group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>,
-    pub 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 drop_listeners: SmallVec<[(TypeId, Box<DropListener<V>>); 2]>,
-    pub click_listeners: SmallVec<[ClickListener<V>; 2]>,
-    pub drag_listener: Option<DragListener<V>>,
-    pub hover_listener: Option<HoverListener<V>>,
-    pub tooltip_builder: Option<TooltipBuilder<V>>,
+    active: Option<MouseDownEvent>,
+    group_active: bool,
+    hovered: bool,
+    group_hovered: bool,
+    focus_status: FocusStatus,
+    key_context: KeyContext,
+    focus_handle: Option<FocusHandle>,
+    scroll_offset: Point<Pixels>,
+    base_style: StyleRefinement,
+    focus_style: StyleRefinement,
+    focus_in_style: StyleRefinement,
+    in_focus_style: StyleRefinement,
+    hover_style: StyleRefinement,
+    group_hover_style: Option<GroupStyle>,
+    active_style: StyleRefinement,
+    group_active_style: Option<GroupStyle>,
+    drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>,
+    group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>,
+    group: Option<SharedString>,
+    dispatch_context: KeyContext,
+    mouse_down_listeners: SmallVec<[MouseDownListener<V>; 2]>,
+    mouse_up_listeners: SmallVec<[MouseUpListener<V>; 2]>,
+    mouse_move_listeners: SmallVec<[MouseMoveListener<V>; 2]>,
+    scroll_wheel_listeners: SmallVec<[ScrollWheelListener<V>; 2]>,
+    key_down_listeners: SmallVec<[KeyDownListener<V>; 2]>,
+    key_up_listeners: SmallVec<[KeyUpListener<V>; 2]>,
+    action_listeners: SmallVec<[(TypeId, ActionListener<V>); 8]>,
+    drop_listeners: SmallVec<[(TypeId, Box<DropListener<V>>); 2]>,
+    click_listeners: SmallVec<[ClickListener<V>; 2]>,
+    drag_listener: Option<DragListener<V>>,
+    hover_listener: Option<HoverListener<V>>,
+    tooltip_builder: Option<TooltipBuilder<V>>,
+}
+
+#[derive(Default)]
+pub struct InteractiveElementState {
+    clicked_state: Arc<Mutex<ElementClickedState>>,
+    hover_state: Arc<Mutex<bool>>,
+    pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
+    scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
+    active_tooltip: Arc<Mutex<Option<ActiveTooltip>>>,
+}
+
+struct ActiveTooltip {
+    #[allow(unused)] // used to drop the task
+    waiting: Option<Task<()>>,
+    tooltip: Option<AnyTooltip>,
 }
 
-impl<V: 'static> Interactivity<V> {
+/// Whether or not the element or a group that contains it is clicked by the mouse.
+#[derive(Copy, Clone, Default, Eq, PartialEq)]
+struct ElementClickedState {
+    pub group: bool,
+    pub element: bool,
+}
+
+impl ElementClickedState {
+    fn is_clicked(&self) -> bool {
+        self.group || self.element
+    }
+}
+
+impl<V> Interactivity<V>
+where
+    V: 'static,
+{
     fn compute_style(&self, bounds: Option<Bounds<Pixels>>, cx: &mut ViewContext<V>) -> Style {
         let mut style = Style::default();
         style.refine(&self.base_style);
 
-        match self.focus {
-            FocusState::None => {}
-            FocusState::Focus => {
+        match self.focus_status {
+            FocusStatus::None => {}
+            FocusStatus::Focus => {
                 style.refine(&self.focus_style);
                 style.refine(&self.focus_in_style);
                 style.refine(&self.in_focus_style);
             }
-            FocusState::FocusIn => {
+            FocusStatus::FocusIn => {
                 style.refine(&self.focus_in_style);
             }
-            FocusState::InFocus => {
+            FocusStatus::InFocus => {
                 style.refine(&self.in_focus_style);
             }
         }
@@ -756,19 +799,66 @@ impl<V: 'static> Interactivity<V> {
             }
         }
 
-        if self.active {
+        if self.active.is_some() {
             style.refine(&self.active_style)
         }
 
         style
     }
 
+    fn layout(
+        &mut self,
+        element_state: &mut InteractiveElementState,
+        cx: &mut ViewContext<V>,
+        f: impl FnOnce(Style, &mut ViewContext<V>) -> LayoutId,
+    ) -> LayoutId {
+        let mut style = Style::default();
+        style.refine(&self.base_style);
+
+        if let Some(focus_handle) = self.focus_handle.as_ref() {
+            if focus_handle.contains_focused(cx) {
+                style.refine(&self.focus_in_style);
+            }
+
+            if focus_handle.within_focused(cx) {
+                style.refine(&self.in_focus_style);
+            }
+
+            if focus_handle.is_focused(cx) {
+                style.refine(&self.focus_style);
+            }
+        }
+
+        let clicked_state = element_state.clicked_state.lock();
+        if clicked_state.group {
+            if let Some(group_style) = self.group_active_style.as_ref() {
+                style.refine(&group_style.style);
+            }
+        }
+        if clicked_state.element {
+            style.refine(&self.active_style);
+        }
+
+        f(style, cx)
+    }
+
     fn paint(
         &mut self,
         bounds: Bounds<Pixels>,
+        content_size: Size<Pixels>,
+        element_state: &mut InteractiveElementState,
         cx: &mut ViewContext<V>,
-        f: impl FnOnce(&mut ViewContext<V>),
+        f: impl FnOnce(Style, Point<Pixels>, &mut ViewContext<V>),
     ) {
+        let style = self.compute_style(Some(bounds), cx);
+
+        if let Some(mouse_cursor) = style.mouse_cursor {
+            let hovered = bounds.contains_point(&cx.mouse_position());
+            if hovered {
+                cx.set_cursor_style(mouse_cursor);
+            }
+        }
+
         for listener in self.mouse_down_listeners.drain(..) {
             cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| {
                 listener(state, event, &bounds, phase, cx);
@@ -845,22 +935,212 @@ impl<V: 'static> Interactivity<V> {
             });
         }
 
+        let mut element_state: &mut InteractiveElementState = element_state.as_mut();
+
+        let click_listeners = mem::take(&mut self.click_listeners);
+        let drag_listener = mem::take(&mut self.drag_listener);
+
+        if !click_listeners.is_empty() || drag_listener.is_some() {
+            let pending_mouse_down = element_state.pending_mouse_down.clone();
+            let mouse_down = pending_mouse_down.lock().clone();
+            if let Some(mouse_down) = mouse_down {
+                if let Some(drag_listener) = drag_listener {
+                    let active_state = element_state.clicked_state.clone();
+
+                    cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
+                        if cx.active_drag.is_some() {
+                            if phase == DispatchPhase::Capture {
+                                cx.notify();
+                            }
+                        } else if phase == DispatchPhase::Bubble
+                            && bounds.contains_point(&event.position)
+                            && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD
+                        {
+                            *active_state.lock() = ElementClickedState::default();
+                            let cursor_offset = event.position - bounds.origin;
+                            let drag = drag_listener(view_state, cursor_offset, cx);
+                            cx.active_drag = Some(drag);
+                            cx.notify();
+                            cx.stop_propagation();
+                        }
+                    });
+                }
+
+                cx.on_mouse_event(move |view_state, event: &MouseUpEvent, phase, cx| {
+                    if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                        let mouse_click = ClickEvent {
+                            down: mouse_down.clone(),
+                            up: event.clone(),
+                        };
+                        for listener in &click_listeners {
+                            listener(view_state, &mouse_click, cx);
+                        }
+                    }
+                    *pending_mouse_down.lock() = None;
+                });
+            } else {
+                cx.on_mouse_event(move |_state, event: &MouseDownEvent, phase, _cx| {
+                    if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                        *pending_mouse_down.lock() = Some(event.clone());
+                    }
+                });
+            }
+        }
+
+        if let Some(hover_listener) = self.hover_listener.take() {
+            let was_hovered = element_state.hover_state.clone();
+            let has_mouse_down = element_state.pending_mouse_down.clone();
+
+            cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
+                if phase != DispatchPhase::Bubble {
+                    return;
+                }
+                let is_hovered =
+                    bounds.contains_point(&event.position) && has_mouse_down.lock().is_none();
+                let mut was_hovered = was_hovered.lock();
+
+                if is_hovered != was_hovered.clone() {
+                    *was_hovered = is_hovered;
+                    drop(was_hovered);
+
+                    hover_listener(view_state, is_hovered, cx);
+                }
+            });
+        }
+
+        if let Some(tooltip_builder) = self.tooltip_builder.take() {
+            let active_tooltip = element_state.active_tooltip.clone();
+            let pending_mouse_down = element_state.pending_mouse_down.clone();
+
+            cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+                if phase != DispatchPhase::Bubble {
+                    return;
+                }
+
+                let is_hovered =
+                    bounds.contains_point(&event.position) && pending_mouse_down.lock().is_none();
+                if !is_hovered {
+                    active_tooltip.lock().take();
+                    return;
+                }
+
+                if active_tooltip.lock().is_none() {
+                    let task = cx.spawn({
+                        let active_tooltip = active_tooltip.clone();
+                        let tooltip_builder = tooltip_builder.clone();
+
+                        move |view, mut cx| async move {
+                            cx.background_executor().timer(TOOLTIP_DELAY).await;
+                            view.update(&mut cx, move |view_state, cx| {
+                                active_tooltip.lock().replace(ActiveTooltip {
+                                    waiting: None,
+                                    tooltip: Some(AnyTooltip {
+                                        view: tooltip_builder(view_state, cx),
+                                        cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET,
+                                    }),
+                                });
+                                cx.notify();
+                            })
+                            .ok();
+                        }
+                    });
+                    active_tooltip.lock().replace(ActiveTooltip {
+                        waiting: Some(task),
+                        tooltip: None,
+                    });
+                }
+            });
+
+            if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() {
+                if active_tooltip.tooltip.is_some() {
+                    cx.active_tooltip = active_tooltip.tooltip.clone()
+                }
+            }
+        }
+
+        let active_state = element_state.clicked_state.clone();
+        if !active_state.lock().is_clicked() {
+            cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| {
+                if phase == DispatchPhase::Capture {
+                    *active_state.lock() = ElementClickedState::default();
+                    cx.notify();
+                }
+            });
+        } else {
+            let active_group_bounds = self
+                .group_active_style
+                .as_ref()
+                .and_then(|group_active| GroupBounds::get(&group_active.group, cx));
+            cx.on_mouse_event(move |_view, down: &MouseDownEvent, phase, cx| {
+                if phase == DispatchPhase::Bubble {
+                    let group = active_group_bounds
+                        .map_or(false, |bounds| bounds.contains_point(&down.position));
+                    let element = bounds.contains_point(&down.position);
+                    if group || element {
+                        *active_state.lock() = ElementClickedState { group, element };
+                        cx.notify();
+                    }
+                }
+            });
+        }
+
+        let overflow = style.overflow;
+        if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll {
+            let scroll_offset = element_state
+                .scroll_offset
+                .get_or_insert_with(Arc::default)
+                .clone();
+            let line_height = cx.line_height();
+            let scroll_max = (content_size - bounds.size).max(&Size::default());
+
+            cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    let mut scroll_offset = scroll_offset.lock();
+                    let old_scroll_offset = *scroll_offset;
+                    let delta = event.delta.pixel_delta(line_height);
+
+                    if overflow.x == Overflow::Scroll {
+                        scroll_offset.x =
+                            (scroll_offset.x + delta.x).clamp(-scroll_max.width, px(0.));
+                    }
+
+                    if overflow.y == Overflow::Scroll {
+                        scroll_offset.y =
+                            (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.));
+                    }
+
+                    if *scroll_offset != old_scroll_offset {
+                        cx.notify();
+                        cx.stop_propagation();
+                    }
+                }
+            });
+        }
+
+        if let Some(group) = self.group.clone() {
+            GroupBounds::push(group, bounds, cx);
+        }
+
         cx.with_key_dispatch(
             self.key_context.clone(),
             self.focus_handle.clone(),
-            |_, cx| f(cx),
+            |_, cx| f(style, self.scroll_offset, cx),
         );
+
+        if let Some(group) = self.group.as_ref() {
+            GroupBounds::pop(group, cx);
+        }
     }
 }
 
 impl<V: 'static> Default for Interactivity<V> {
     fn default() -> Self {
         Self {
-            active: false,
+            active: None,
             group_active: false,
             hovered: false,
             group_hovered: false,
-            focus: FocusState::None,
+            focus_status: FocusStatus::None,
             key_context: KeyContext::default(),
             focus_handle: None,
             scroll_offset: Point::default(),
@@ -937,26 +1217,80 @@ impl<V, E> FocusableComponent<V> for Focusable<V, E> {
     }
 }
 
-impl<V: 'static, E: InteractiveComponent<V>> InteractiveComponent<V> for Focusable<V, E> {
+impl<V, E> InteractiveComponent<V> for Focusable<V, E>
+where
+    V: 'static,
+    E: InteractiveComponent<V>,
+{
     fn interactivity(&mut self) -> &mut Interactivity<V> {
         self.element.interactivity()
     }
 }
 
-impl<V: 'static, E: StatefulInteractiveComponent<V>> StatefulInteractiveComponent<V>
+impl<V: 'static, E: StatefulInteractiveComponent<V, E>> StatefulInteractiveComponent<V, E>
     for Focusable<V, E>
 {
 }
 
+impl<V, E> Element<V> for Focusable<V, E>
+where
+    V: 'static,
+    E: Element<V>,
+{
+    type ElementState = E::ElementState;
+
+    fn id(&self) -> Option<crate::ElementId> {
+        todo!()
+    }
+
+    fn initialize(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> Self::ElementState {
+        todo!()
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) -> LayoutId {
+        todo!()
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        todo!()
+    }
+}
+
 pub struct Stateful<V, E> {
     id: SharedString,
     view_type: PhantomData<V>,
     element: E,
 }
 
-impl<V: 'static, E: InteractiveComponent<V>> StatefulInteractiveComponent<V> for Stateful<V, E> {}
+impl<V, E> StatefulInteractiveComponent<V, E> for Stateful<V, E>
+where
+    V: 'static,
+    E: Element<V>,
+    Self: InteractiveComponent<V>,
+{
+}
 
-impl<V: 'static, E: InteractiveComponent<V>> InteractiveComponent<V> for Stateful<V, E> {
+impl<V, E> InteractiveComponent<V> for Stateful<V, E>
+where
+    V: 'static,
+    E: InteractiveComponent<V>,
+{
     fn interactivity(&mut self) -> &mut Interactivity<V> {
         self.element.interactivity()
     }
@@ -967,3 +1301,43 @@ impl<V, E: FocusableComponent<V>> FocusableComponent<V> for Stateful<V, E> {
         self.element.focusability()
     }
 }
+
+impl<V, E> Element<V> for Stateful<V, E>
+where
+    V: 'static,
+    E: Element<V>,
+{
+    type ElementState = InteractiveElementState;
+
+    fn id(&self) -> Option<crate::ElementId> {
+        todo!()
+    }
+
+    fn initialize(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> Self::ElementState {
+        todo!()
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) -> LayoutId {
+        todo!()
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        todo!()
+    }
+}

crates/gpui2/src/gpui2.rs 🔗

@@ -148,7 +148,7 @@ pub enum GlobalKey {
 }
 
 pub trait BorrowAppContext {
-    fn with_text_style<F, R>(&mut self, style: TextStyleRefinement, f: F) -> R
+    fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R
     where
         F: FnOnce(&mut Self) -> R;
 
@@ -159,14 +159,18 @@ impl<C> BorrowAppContext for C
 where
     C: BorrowMut<AppContext>,
 {
-    fn with_text_style<F, R>(&mut self, style: TextStyleRefinement, f: F) -> R
+    fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R
     where
         F: FnOnce(&mut Self) -> R,
     {
-        self.borrow_mut().push_text_style(style);
-        let result = f(self);
-        self.borrow_mut().pop_text_style();
-        result
+        if let Some(style) = style {
+            self.borrow_mut().push_text_style(style);
+            let result = f(self);
+            self.borrow_mut().pop_text_style();
+            result
+        } else {
+            f(self)
+        }
     }
 
     fn set_global<G: 'static>(&mut self, global: G) {

crates/gpui2/src/style.rs 🔗

@@ -2,7 +2,7 @@ use crate::{
     black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
     Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
     FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Result,
-    Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, WindowContext,
+    Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext,
 };
 use refineable::{Cascade, Refineable};
 use smallvec::SmallVec;
@@ -220,7 +220,7 @@ pub struct HighlightStyle {
 impl Eq for HighlightStyle {}
 
 impl Style {
-    pub fn text_style(&self, _cx: &WindowContext) -> Option<&TextStyleRefinement> {
+    pub fn text_style(&self) -> Option<&TextStyleRefinement> {
         if self.text.is_some() {
             Some(&self.text)
         } else {
@@ -228,13 +228,47 @@ impl Style {
         }
     }
 
-    pub fn with_text_style<C, F, R>(&self, cx: &mut C, f: F) -> R
+    pub fn overflow_mask(&self, bounds: Bounds<Pixels>) -> Option<ContentMask<Pixels>> {
+        match self.overflow {
+            Point {
+                x: Overflow::Visible,
+                y: Overflow::Visible,
+            } => None,
+            _ => {
+                let current_mask = bounds;
+                let min = current_mask.origin;
+                let max = current_mask.lower_right();
+                let bounds = match (
+                    self.overflow.x == Overflow::Visible,
+                    self.overflow.y == Overflow::Visible,
+                ) {
+                    // x and y both visible
+                    (true, true) => return None,
+                    // x visible, y hidden
+                    (true, false) => Bounds::from_corners(
+                        point(min.x, bounds.origin.y),
+                        point(max.x, bounds.lower_right().y),
+                    ),
+                    // x hidden, y visible
+                    (false, true) => Bounds::from_corners(
+                        point(bounds.origin.x, min.y),
+                        point(bounds.lower_right().x, max.y),
+                    ),
+                    // both hidden
+                    (false, false) => bounds,
+                };
+                Some(ContentMask { bounds })
+            }
+        }
+    }
+
+    pub fn apply_text_style<C, F, R>(&self, cx: &mut C, f: F) -> R
     where
         C: BorrowAppContext,
         F: FnOnce(&mut C) -> R,
     {
         if self.text.is_some() {
-            cx.with_text_style(self.text.clone(), f)
+            cx.with_text_style(Some(self.text.clone()), f)
         } else {
             f(cx)
         }
@@ -274,7 +308,7 @@ impl Style {
             bounds: mask_bounds,
         };
 
-        cx.with_content_mask(mask, f)
+        cx.with_content_mask(Some(mask), f)
     }
 
     /// Paints the background of an element styled with this style.

crates/gpui2/src/window.rs 🔗

@@ -1067,7 +1067,7 @@ impl<'a> WindowContext<'a> {
         if let Some(active_drag) = self.app.active_drag.take() {
             self.with_z_index(1, |cx| {
                 let offset = cx.mouse_position() - active_drag.cursor_offset;
-                cx.with_element_offset(Some(offset), |cx| {
+                cx.with_element_offset(offset, |cx| {
                     let available_space =
                         size(AvailableSpace::MinContent, AvailableSpace::MinContent);
                     active_drag.view.draw(available_space, cx);
@@ -1076,7 +1076,7 @@ impl<'a> WindowContext<'a> {
             });
         } else if let Some(active_tooltip) = self.app.active_tooltip.take() {
             self.with_z_index(1, |cx| {
-                cx.with_element_offset(Some(active_tooltip.cursor_offset), |cx| {
+                cx.with_element_offset(active_tooltip.cursor_offset, |cx| {
                     let available_space =
                         size(AvailableSpace::MinContent, AvailableSpace::MinContent);
                     active_tooltip.view.draw(available_space, cx);
@@ -1553,43 +1553,26 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
     /// with the current mask.
     fn with_content_mask<R>(
         &mut self,
-        mask: ContentMask<Pixels>,
+        mask: Option<ContentMask<Pixels>>,
         f: impl FnOnce(&mut Self) -> R,
     ) -> R {
-        let mask = mask.intersect(&self.content_mask());
-        self.window_mut()
-            .current_frame
-            .content_mask_stack
-            .push(mask);
-        let result = f(self);
-        self.window_mut().current_frame.content_mask_stack.pop();
-        result
+        if let Some(mask) = mask {
+            let mask = mask.intersect(&self.content_mask());
+            self.window_mut()
+                .current_frame
+                .content_mask_stack
+                .push(mask);
+            let result = f(self);
+            self.window_mut().current_frame.content_mask_stack.pop();
+            result
+        } else {
+            f(self)
+        }
     }
 
     /// Update the global element offset based on the given offset. This is used to implement
     /// scrolling and position drag handles.
     fn with_element_offset<R>(
-        &mut self,
-        offset: Option<Point<Pixels>>,
-        f: impl FnOnce(&mut Self) -> R,
-    ) -> R {
-        let Some(offset) = offset else {
-            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
-    }
-
-    /// 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,