diff --git a/crates/gpui2/docs/contexts.md b/crates/gpui2/docs/contexts.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/crates/gpui2/docs/key_dispatch.md b/crates/gpui2/docs/key_dispatch.md new file mode 100644 index 0000000000000000000000000000000000000000..339eb430281b5863b68bda2bd3f71117963adf8e --- /dev/null +++ b/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) -> impl Component { + div() + .on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext| { + // ... + }) + .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) -> impl Component { + div() + .key_context("menu") + .on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext| { + // ... + }) + .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}] + } +} + +``` diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 61c6195d903095105b741caed25e408a49519cf1..1a3d95e761f154966256a1d8b9df92c8450be578 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -1068,7 +1068,7 @@ impl DerefMut for GlobalLease { /// 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, } diff --git a/crates/gpui2/src/elements.rs b/crates/gpui2/src/elements.rs index eb061f7d34c8e77c1a366123818b253d33620c7a..e0e155fb0390c15faf34c28af0d41d6130c55512 100644 --- a/crates/gpui2/src/elements.rs +++ b/crates/gpui2/src/elements.rs @@ -1,5 +1,6 @@ mod div; mod img; +mod node; mod svg; mod text; mod uniform_list; diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 95c44038ed115ead27a00d2dcd7a10e1fae429dc..080cdc2c4de47ad8f75886670b457fb8d46209b2 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -252,7 +252,7 @@ where cx: &mut ViewContext, ) -> 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| { diff --git a/crates/gpui2/src/elements/node.rs b/crates/gpui2/src/elements/node.rs new file mode 100644 index 0000000000000000000000000000000000000000..ec73af872112983a3766d0eea7efba26c1cd50e5 --- /dev/null +++ b/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 { + fn interactivity(&mut self) -> &mut Interactivity; + + 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, + 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) + '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) + '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) + '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) + '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) + '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) + '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( + mut self, + listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().action_listeners.push(( + TypeId::of::(), + 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( + mut self, + listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().action_listeners.push(( + TypeId::of::(), + 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) + '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) + '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(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity() + .drag_over_styles + .push((TypeId::of::(), f(StyleRefinement::default()))); + self + } + + fn group_drag_over( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self + where + Self: Sized, + { + self.interactivity().group_drag_over_styles.push(( + TypeId::of::(), + GroupStyle { + group: group_name.into(), + style: f(StyleRefinement::default()), + }, + )); + self + } + + fn on_drop( + mut self, + listener: impl Fn(&mut V, View, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().drop_listeners.push(( + TypeId::of::(), + Box::new(move |view, dragged_view, cx| { + listener(view, dragged_view.downcast().unwrap(), cx); + }), + )); + self + } +} + +pub trait StatefulInteractiveComponent { + fn interactivity(&mut self) -> &mut StatefulInteractivity; + + 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, + 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) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity() + .click_listeners + .push(Box::new(move |view, event, cx| listener(view, event, cx))); + self + } + + fn on_drag( + mut self, + listener: impl Fn(&mut V, &mut ViewContext) -> View + '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)) -> 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( + mut self, + build_tooltip: impl Fn(&mut V, &mut ViewContext) -> View + '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 { + fn focusability(&mut self) -> &mut Focusability; + + 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) + '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) + '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) + '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) + '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 = SmallVec<[FocusListener; 2]>; + +pub type FocusListener = + Box) + 'static>; + +pub type MouseDownListener = Box< + dyn Fn(&mut V, &MouseDownEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, +>; +pub type MouseUpListener = Box< + dyn Fn(&mut V, &MouseUpEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, +>; + +pub type MouseMoveListener = Box< + dyn Fn(&mut V, &MouseMoveEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, +>; + +pub type ScrollWheelListener = Box< + dyn Fn(&mut V, &ScrollWheelEvent, &Bounds, DispatchPhase, &mut ViewContext) + + 'static, +>; + +pub type ClickListener = Box) + 'static>; + +pub type DragListener = + Box, &mut ViewContext) -> AnyDrag + 'static>; + +type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; + +pub type HoverListener = Box) + 'static>; + +pub type TooltipBuilder = Arc) -> AnyView + 'static>; + +pub type KeyDownListener = + Box) + 'static>; + +pub type KeyUpListener = + Box) + 'static>; + +pub type ActionListener = + Box) + 'static>; + +pub struct FocusEvent { + pub blurred: Option, + pub focused: Option, +} + +pub struct Node { + style: StyleRefinement, + key_context: KeyContext, + interactivity: Interactivity, + children: Vec>, +} + +pub struct Interactivity { + group: Option, + pub dispatch_context: KeyContext, + pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, + pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, + pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, + pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>, + pub key_down_listeners: SmallVec<[KeyDownListener; 2]>, + pub key_up_listeners: SmallVec<[KeyUpListener; 2]>, + pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, + pub hover_style: StyleRefinement, + pub group_hover_style: Option, + drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, + group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>, + drop_listeners: SmallVec<[(TypeId, Box>); 2]>, + scroll_offset: Point, +} + +impl Node { + fn compute_style(&self) -> Style { + let mut style = Style::default(); + style.refine(&self.style); + style + } +} + +impl Styled for Node { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +impl InteractiveComponent for Node { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } +} + +pub struct NodeState { + child_layout_ids: SmallVec<[LayoutId; 4]>, +} + +impl Element for Node { + type ElementState = NodeState; + + fn id(&self) -> Option { + None + } + + fn initialize( + &mut self, + view_state: &mut V, + _: Option, + cx: &mut ViewContext, + ) -> 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, + ) -> 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::>(); + cx.request_layout(&style, element_state.child_layout_ids.iter().copied()) + }) + } + + fn paint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + 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; 1]>>); + +impl GroupBounds { + pub fn get(name: &SharedString, cx: &mut AppContext) -> Option> { + cx.default_global::() + .0 + .get(name) + .and_then(|bounds_stack| bounds_stack.last()) + .cloned() + } + + pub fn push(name: SharedString, bounds: Bounds, cx: &mut AppContext) { + cx.default_global::() + .0 + .entry(name) + .or_default() + .push(bounds); + } + + pub fn pop(name: &SharedString, cx: &mut AppContext) { + cx.default_global::().0.get_mut(name).unwrap().pop(); + } +} + +pub struct Focusable { + focusability: Focusability, + view_type: PhantomData, + element: E, +} + +pub struct Focusability { + focus_handle: Option, + focus_listeners: FocusListeners, + focus_style: StyleRefinement, + focus_in_style: StyleRefinement, + in_focus_style: StyleRefinement, +} + +impl FocusableComponent for Focusable { + fn focusability(&mut self) -> &mut Focusability { + &mut self.focusability + } +} + +impl> InteractiveComponent for Focusable { + fn interactivity(&mut self) -> &mut Interactivity { + self.element.interactivity() + } +} + +impl> StatefulInteractiveComponent + for Focusable +{ + fn interactivity(&mut self) -> &mut StatefulInteractivity { + self.element.interactivity() + } +} + +pub struct Stateful { + id: SharedString, + interactivity: StatefulInteractivity, + view_type: PhantomData, + element: E, +} + +pub struct StatefulInteractivity { + click_listeners: SmallVec<[ClickListener; 2]>, + active_style: StyleRefinement, + group_active_style: Option, + drag_listener: Option>, + hover_listener: Option>, + tooltip_builder: Option>, +} + +impl StatefulInteractiveComponent for Stateful { + fn interactivity(&mut self) -> &mut StatefulInteractivity { + &mut self.interactivity + } +} + +impl> InteractiveComponent for Stateful { + fn interactivity(&mut self) -> &mut Interactivity { + self.element.interactivity() + } +} + +impl> FocusableComponent for Stateful { + fn focusability(&mut self) -> &mut Focusability { + self.element.focusability() + } +} diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 664cc61f8a1bf100678bc13d2cf796019380f9e5..18b92c0b8b7ae8f0bfe83a7f45d56e5b92fe5e51 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -228,7 +228,7 @@ impl Style { } } - pub fn apply_text_style(&self, cx: &mut C, f: F) -> R + pub fn with_text_style(&self, cx: &mut C, f: F) -> R where C: BorrowAppContext, F: FnOnce(&mut C) -> R, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 4a7241a5c52f0238f50679b5d25005d35cda35c0..52c5464e4fe77b5a76a0de922208744e06c5be00 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1587,6 +1587,27 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { 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( + &mut self, + offset: Point, + 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 { self.window()