Skip to main content

gpui_motion_kit/
gesture_native.rs

1//! Native input adapter with a persistent owner and frame-scoped listeners.
2use crate::MotionState;
3use crate::core::{Drag, GestureTargets, InView, Interaction, Motion, Target};
4
5use gpui_kit::{
6    AnyElement, App, Bounds, DispatchPhase, Div, Element, ElementId, EntityId, FocusHandle,
7    GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, KeyDownEvent,
8    KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
9    MouseUpEvent, Pixels, Stateful, Window, prelude::*, px,
10};
11
12use std::{
13    cell::RefCell,
14    rc::Rc,
15    time::{Duration, Instant},
16};
17
18#[derive(Default)]
19struct Data {
20    interaction: Interaction,
21    motion: MotionState,
22    drag: Option<Drag>,
23    focus: Option<FocusHandle>,
24    visibility: Option<InView>,
25    epoch: Option<Instant>,
26    owner: Option<EntityId>,
27    pointer: bool,
28    key: Option<String>,
29    presented: Target,
30}
31
32impl Data {
33    fn now(&self, cx: &App) -> Duration {
34        self.epoch.map_or(Duration::ZERO, |epoch| {
35            cx.background_executor()
36                .now()
37                .saturating_duration_since(epoch)
38        })
39    }
40
41    fn cancel(&mut self, cx: &App) {
42        let now = self.now(cx);
43
44        if let Some(drag) = &mut self.drag {
45            drag.cancel(now);
46        }
47
48        self.pointer = false;
49        self.key = None;
50        self.interaction.pressed = false;
51        self.interaction.dragging = false;
52    }
53
54    fn notify(&self, cx: &mut App) {
55        if let Some(owner) = self.owner {
56            cx.notify(owner);
57        }
58    }
59}
60
61/// Retain one handle per logical control. Clones share input/motion state.
62/// Mouse release outside the hitbox, lost button, Escape, blur and deactivation
63/// clear ownership. Native desktop mouse events are supported, not multi-touch.
64#[derive(Clone, Default)]
65pub struct GestureState(Rc<RefCell<Data>>);
66
67impl GestureState {
68    /// Adds bounded native pointer/arrow-key dragging. Configure once in the view.
69    pub fn with_drag(self, drag: Drag) -> Self {
70        self.0.borrow_mut().drag = Some(drag);
71
72        self
73    }
74
75    /// Enables automatically measured clipped viewport visibility. Configure once.
76    pub fn in_view(self, amount: f64, once: bool) -> Self {
77        self.0.borrow_mut().visibility = Some(InView::new(amount, once));
78
79        self
80    }
81
82    /// Current resolved interaction flags, supplied automatically by the element.
83    pub fn interaction(&self) -> Interaction {
84        self.0.borrow().interaction
85    }
86
87    /// Last presented target, including drag displacement.
88    pub fn presented(&self) -> Target {
89        self.0.borrow().presented
90    }
91
92    /// The stable focus handle, available after the first layout.
93    pub fn focus_handle(&self) -> Option<FocusHandle> {
94        self.0.borrow().focus.clone()
95    }
96
97    /// Cancels pointer/key state and bounded dragging, notifying the owning view.
98    pub fn cancel(&self, cx: &mut App) {
99        let mut data = self.0.borrow_mut();
100
101        data.cancel(cx);
102        data.notify(cx);
103    }
104
105    /// Wraps a styled native control and automatically binds hover/press/focus,
106    /// viewport and optional drag. Supply role, label and click behavior on the
107    /// child. The adapter supplies a stable tab-stop focus handle. Configure
108    /// layout styles before binding; x/y are owned by this adapter, not a second
109    /// `LayoutGroup` on the same control. Native activation remains Kit's behavior.
110    ///
111    /// ```
112    /// use gpui_kit::{div, prelude::*, Role};
113    /// use gpui_motion_kit::{GestureState, core::{GestureTargets, Motion, Target}};
114    /// let state = GestureState::default(); // persistent view field
115    /// let control = state.bind("motion-button", div().id("button").role(Role::Button).child("Press"),
116    ///     Motion::new(Target::default()), GestureTargets::default().while_press(Target::default().opacity(0.5)));
117    /// ```
118    pub fn bind<E: IntoElement + StatefulInteractiveElement + Styled + 'static>(
119        &self,
120        id: impl Into<ElementId>,
121        child: E,
122        motion: Motion,
123        targets: GestureTargets,
124    ) -> GestureElement<E> {
125        GestureElement {
126            id: id.into(),
127            child: Some(child),
128            state: self.clone(),
129            motion,
130            targets,
131            enabled: true,
132        }
133    }
134}
135
136/// Frame-scoped input/motion wrapper produced by `GestureState::bind`.
137pub struct GestureElement<E = Stateful<Div>> {
138    id: ElementId,
139    child: Option<E>,
140    state: GestureState,
141    motion: Motion,
142    targets: GestureTargets,
143    enabled: bool,
144}
145
146impl<E> GestureElement<E> {
147    /// Disables native input bindings and keyboard tab stops. Cancels a held
148    /// gesture on the next render; the owner restores focus if disabling the
149    /// focused control and supplies disabled accessibility metadata on the child
150    /// using its Kit version’s APIs. Visibility observation remains available.
151    pub fn enabled(mut self, enabled: bool) -> Self {
152        self.enabled = enabled;
153
154        self
155    }
156}
157
158impl<E: IntoElement + StatefulInteractiveElement + Styled + 'static> IntoElement
159    for GestureElement<E>
160{
161    type Element = Self;
162
163    fn into_element(self) -> Self {
164        self
165    }
166}
167
168impl<E: IntoElement + StatefulInteractiveElement + Styled + 'static> Element for GestureElement<E> {
169    type RequestLayoutState = AnyElement;
170    type PrepaintState = Hitbox;
171
172    fn id(&self) -> Option<ElementId> {
173        Some(self.id.clone())
174    }
175
176    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
177        None
178    }
179
180    fn request_layout(
181        &mut self,
182        _: Option<&GlobalElementId>,
183        _: Option<&InspectorElementId>,
184        w: &mut Window,
185        cx: &mut App,
186    ) -> (LayoutId, AnyElement) {
187        let mut data = self.state.0.borrow_mut();
188
189        data.owner = Some(w.current_view());
190        data.epoch
191            .get_or_insert_with(|| cx.background_executor().now());
192        let focus = data
193            .focus
194            .get_or_insert_with(|| cx.focus_handle().tab_stop(true))
195            .clone()
196            .tab_stop(self.enabled);
197
198        let focused = self.enabled && focus.is_focused(w);
199
200        if !self.enabled
201            || !w.is_window_active()
202            || (!focused && (data.key.is_some() || data.pointer))
203        {
204            data.cancel(cx);
205        }
206
207        if !self.enabled || !w.is_window_active() {
208            data.interaction.hovered = false;
209        }
210
211        data.interaction.focused = focused;
212        let now = data.now(cx);
213        let target = self.targets.resolve(self.motion.target(), data.interaction);
214
215        let frame = data
216            .motion
217            .sample(self.motion.with_target(target), now, cx.reduce_motion());
218
219        let mut target = frame.target();
220        let mut active = frame.is_animating();
221
222        if let Some(drag) = &mut data.drag {
223            let offset = drag.sample(now, cx.reduce_motion());
224
225            active |= offset.is_animating();
226            let finite = |a: f32, b: f32| {
227                (f64::from(a) + f64::from(b)).clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32
228            };
229
230            target = target
231                .x(finite(target.x_px(), offset.target().x_px()))
232                .y(finite(target.y_px(), offset.target().y_px()));
233        }
234
235        data.presented = target;
236
237        if active {
238            w.request_animation_frame();
239        }
240
241        let child = frame
242            .apply(
243                self.child
244                    .take()
245                    .expect("one layout")
246                    .track_focus(&focus)
247                    .tab_index(0),
248            )
249            .left(px(target.x_px()))
250            .top(px(target.y_px()));
251
252        drop(data);
253        let mut child = child.into_any_element();
254        let layout = child.request_layout(w, cx);
255
256        (layout, child)
257    }
258
259    fn prepaint(
260        &mut self,
261        _: Option<&GlobalElementId>,
262        _: Option<&InspectorElementId>,
263        bounds: Bounds<Pixels>,
264        child: &mut AnyElement,
265        w: &mut Window,
266        cx: &mut App,
267    ) -> Hitbox {
268        let hitbox = w.insert_hitbox(bounds, HitboxBehavior::Normal);
269        let mut data = self.state.0.borrow_mut();
270
271        if let Some(visibility) = &mut data.visibility {
272            let viewport = w.content_mask().bounds;
273
274            let rect = |r: Bounds<Pixels>| {
275                [
276                    f64::from(r.origin.x),
277                    f64::from(r.origin.y),
278                    f64::from(r.size.width),
279                    f64::from(r.size.height),
280                ]
281            };
282
283            if visibility.update(rect(viewport), rect(bounds)) {
284                data.interaction.in_view = visibility.is_visible();
285                let owner = data.owner.expect("owner");
286
287                w.defer(cx, move |_, cx| cx.notify(owner));
288            }
289        }
290
291        drop(data);
292        child.prepaint(w, cx);
293
294        hitbox
295    }
296
297    fn paint(
298        &mut self,
299        _: Option<&GlobalElementId>,
300        _: Option<&InspectorElementId>,
301        _: Bounds<Pixels>,
302        child: &mut AnyElement,
303        hitbox: &mut Hitbox,
304        w: &mut Window,
305        cx: &mut App,
306    ) {
307        if !self.enabled {
308            let region = hitbox.clone();
309
310            w.on_mouse_event(move |_: &MouseDownEvent, phase, w, cx| {
311                if phase == DispatchPhase::Capture && region.is_hovered(w) {
312                    cx.stop_propagation();
313                }
314            });
315            let region = hitbox.clone();
316
317            w.on_mouse_event(move |_: &MouseUpEvent, phase, w, cx| {
318                if phase == DispatchPhase::Capture && region.is_hovered(w) {
319                    cx.stop_propagation();
320                }
321            });
322            let region = hitbox.clone();
323
324            w.on_mouse_event(move |_: &MouseMoveEvent, phase, w, cx| {
325                if phase == DispatchPhase::Capture && region.is_hovered(w) {
326                    cx.stop_propagation();
327                }
328            });
329            let focus = self.state.focus_handle();
330
331            w.on_key_event(move |_: &KeyDownEvent, phase, w, cx| {
332                if phase == DispatchPhase::Capture
333                    && focus.as_ref().is_some_and(|f| f.is_focused(w))
334                {
335                    cx.stop_propagation();
336                }
337            });
338            let focus = self.state.focus_handle();
339
340            w.on_key_event(move |_: &KeyUpEvent, phase, w, cx| {
341                if phase == DispatchPhase::Capture
342                    && focus.as_ref().is_some_and(|f| f.is_focused(w))
343                {
344                    cx.stop_propagation();
345                }
346            });
347            child.paint(w, cx);
348
349            return;
350        }
351
352        let shared = self.state.0.clone();
353        let region = hitbox.clone();
354
355        w.on_mouse_event(move |event: &MouseDownEvent, phase, w, cx| {
356            if phase == DispatchPhase::Bubble
357                && event.button == MouseButton::Left
358                && region.is_hovered(w)
359            {
360                let mut data = shared.borrow_mut();
361
362                if data.pointer {
363                    return;
364                }
365
366                data.pointer = true;
367                data.key = None;
368                data.interaction.pressed = true;
369                let now = data.now(cx);
370
371                if let Some(drag) = &mut data.drag {
372                    drag.begin(
373                        [f64::from(event.position.x), f64::from(event.position.y)],
374                        now,
375                    );
376                    data.interaction.dragging = true;
377                    cx.stop_propagation();
378                }
379
380                if let Some(focus) = &data.focus {
381                    focus.focus(w, cx);
382                }
383
384                data.notify(cx);
385            }
386        });
387        let shared = self.state.0.clone();
388        let region = hitbox.clone();
389
390        w.on_mouse_event(move |event: &MouseMoveEvent, phase, w, cx| {
391            if phase != DispatchPhase::Capture {
392                return;
393            }
394
395            let mut data = shared.borrow_mut();
396            let hover = region.is_hovered(w);
397            let changed = data.interaction.hovered != hover;
398
399            data.interaction.hovered = hover;
400
401            if data.pointer {
402                let now = data.now(cx);
403
404                if event.pressed_button != Some(MouseButton::Left) {
405                    data.cancel(cx);
406                } else if let Some(drag) = &mut data.drag {
407                    drag.move_to(
408                        [f64::from(event.position.x), f64::from(event.position.y)],
409                        now,
410                    );
411                }
412
413                data.notify(cx);
414            } else if changed {
415                data.notify(cx);
416            }
417        });
418        let shared = self.state.0.clone();
419
420        w.on_mouse_event(move |event: &MouseUpEvent, phase, _, cx| {
421            if phase == DispatchPhase::Capture && event.button == MouseButton::Left {
422                let mut data = shared.borrow_mut();
423
424                if !data.pointer {
425                    return;
426                }
427
428                let now = data.now(cx);
429
430                if let Some(drag) = &mut data.drag {
431                    drag.end(now);
432                }
433
434                data.pointer = false;
435                data.interaction.pressed = false;
436                data.interaction.dragging = false;
437                data.notify(cx);
438            }
439        });
440        let shared = self.state.0.clone();
441
442        w.on_mouse_event(move |_: &MouseExitEvent, phase, _, cx| {
443            if phase == DispatchPhase::Capture {
444                let mut data = shared.borrow_mut();
445
446                if data.interaction.hovered {
447                    data.interaction.hovered = false;
448                    data.notify(cx);
449                }
450            }
451        });
452        let shared = self.state.0.clone();
453
454        w.on_key_event(move |event: &KeyDownEvent, phase, w, cx| {
455            if phase != DispatchPhase::Capture {
456                return;
457            }
458
459            let mut data = shared.borrow_mut();
460
461            if !data.focus.as_ref().is_some_and(|f| f.is_focused(w)) {
462                return;
463            }
464
465            let key = event.keystroke.key.as_str();
466            let now = data.now(cx);
467
468            if key == "escape" {
469                data.cancel(cx);
470                data.notify(cx);
471            } else if ["enter", "space"].contains(&key) && !event.is_held && !data.pointer {
472                data.key = Some(key.to_owned());
473                data.interaction.pressed = true;
474                data.notify(cx);
475            } else if let Some(delta) = match key {
476                "left" => Some([-10., 0.]),
477                "right" => Some([10., 0.]),
478                "up" => Some([0., -10.]),
479                "down" => Some([0., 10.]),
480                _ => None,
481            } && let Some(drag) = &mut data.drag
482            {
483                drag.move_by(delta, now);
484                data.notify(cx);
485                cx.stop_propagation();
486            }
487        });
488        let shared = self.state.0.clone();
489
490        w.on_key_event(move |event: &KeyUpEvent, phase, _, cx| {
491            if phase == DispatchPhase::Capture {
492                let mut data = shared.borrow_mut();
493
494                if data.key.as_deref() == Some(event.keystroke.key.as_str()) {
495                    data.key = None;
496                    data.interaction.pressed = data.pointer;
497                    data.notify(cx);
498                }
499            }
500        });
501        // Register key listeners on this wrapper before child paint selects a descendant node.
502        child.paint(w, cx);
503    }
504}