incular-gestures 0.1.1

Platform-neutral gesture recognition and input interaction primitives for Incular
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
use std::rc::Rc;

use incular_core::time::Instant;

use incular_core::{
    DragStartBehavior, KeyboardEvent, Offset, PointerPhase, PointerSampleMetadata, TrackpadGesture,
};

use crate::focus::{FocusBehavior, FocusNode};
use crate::keyboard::{
    ActionInvocationSubscription, ActionListenerSubscription, ErasedActionScope,
    ErasedShortcutScope,
};

use crate::scale::{ScaleEndDetails, ScaleStartDetails, ScaleUpdateDetails};

/// A platform-neutral pointer event.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PointerEvent {
    pub pointer: u64,
    pub position: Offset,
    pub phase: PointerPhase,
    pub time: Instant,
}

/// A pointer event carrying the metadata needed by raw input widgets.
///
/// [`PointerEvent`] predates device-aware routing and remains intentionally
/// small for source compatibility. Raw widgets use this value so a retained
/// route can preserve the pointer stream, physical device, pressed buttons,
/// phase, and event timestamp without coupling the widget crate to a native
/// event type.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RawPointerEvent {
    /// Stable for the duration of one pointer sequence.
    pub pointer: u64,
    /// Platform device identifier. Zero means that the source did not supply
    /// a distinct device identifier.
    pub device: u64,
    /// Physical category of the device producing the event.
    pub kind: PointerDeviceKind,
    /// Flutter-compatible button bit mask. Zero is used for hover and for
    /// adapters that do not expose button state.
    pub buttons: u32,
    /// Button whose state changed for a Down/Up event, when the source can
    /// identify it independently from the complete pressed-button chord.
    pub button: Option<u32>,
    pub sample: PointerSampleMetadata,
    pub position: Offset,
    pub phase: PointerPhase,
    pub time: Instant,
    /// Flutter `delta`: movement since this pointer's previous event. The
    /// retained tree fills it in; zero for a press.
    pub delta: Offset,
    /// Flutter `localPosition`: [`Self::position`] in the receiving
    /// widget's coordinates. The retained tree sets it per listener; it
    /// equals the position until then.
    pub local_position: Offset,
    /// Flutter `localDelta`: [`Self::delta`] in the receiving widget's
    /// coordinates.
    pub local_delta: Offset,
    /// Flutter `down`: whether the pointer is in contact (a press, or a move
    /// with buttons held). False for hovers, releases and cancellations.
    pub down: bool,
    /// Flutter `synthesized`: the platform did not report this event; the
    /// framework derived it from others.
    pub synthesized: bool,
    /// Flutter `PointerScrollEvent.scrollDelta`: the wheel movement of a
    /// pointer signal, positive when content scrolls down or right. Zero for
    /// other events.
    pub scroll_delta: Offset,
}

impl RawPointerEvent {
    #[must_use]
    pub const fn new(
        pointer: u64,
        device: u64,
        kind: PointerDeviceKind,
        buttons: u32,
        position: Offset,
        phase: PointerPhase,
        time: Instant,
    ) -> Self {
        Self {
            pointer,
            device,
            kind,
            buttons,
            button: None,
            sample: PointerSampleMetadata::EMPTY,
            position,
            phase,
            time,
            delta: Offset::ZERO,
            local_position: position,
            local_delta: Offset::ZERO,
            down: pointer_is_down(phase, buttons),
            synthesized: false,
            scroll_delta: Offset::ZERO,
        }
    }

    /// Records the button whose state changed for a Down/Up transition.
    #[must_use]
    pub const fn with_button(mut self, button: Option<u32>) -> Self {
        self.button = button;
        self
    }

    /// A pointer signal (Flutter `PointerScrollEvent`) at `position`.
    #[must_use]
    pub fn scroll(
        pointer: u64,
        device: u64,
        kind: PointerDeviceKind,
        position: Offset,
        scroll_delta: Offset,
        time: Instant,
    ) -> Self {
        Self {
            scroll_delta,
            ..Self::new(pointer, device, kind, 0, position, PointerPhase::Move, time)
        }
    }

    #[must_use]
    pub const fn with_sample(mut self, sample: PointerSampleMetadata) -> Self {
        self.sample = sample;
        self
    }

    /// Converts to the legacy gesture event while retaining the raw event as
    /// the source of truth for listeners and tap regions.
    #[must_use]
    pub const fn legacy(self) -> PointerEvent {
        PointerEvent {
            pointer: self.pointer,
            position: self.position,
            phase: self.phase,
            time: self.time,
        }
    }
}

impl From<PointerEvent> for RawPointerEvent {
    fn from(event: PointerEvent) -> Self {
        Self::new(
            event.pointer,
            0,
            PointerDeviceKind::Mouse,
            0,
            event.position,
            event.phase,
            event.time,
        )
    }
}

/// Flutter's `down` for an event: presses, and moves with a button held.
const fn pointer_is_down(phase: PointerPhase, buttons: u32) -> bool {
    match phase {
        PointerPhase::Down => true,
        PointerPhase::Move => buttons != 0,
        PointerPhase::Up | PointerPhase::Cancel | PointerPhase::Enter | PointerPhase::Exit => false,
    }
}

/// A renderer-independent cursor preference consumed by higher-level mouse-region widgets.
///
/// `Defer` asks the retained hit-test route to continue behind the current
/// region. Concrete platform adapters map the remaining values to native
/// cursors; keeping the vocabulary here avoids putting platform handles in
/// the widget or painting crates.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum MouseCursor {
    #[default]
    Defer,
    Basic,
    Click,
    Text,
    Crosshair,
    Grab,
    Grabbing,
    NotAllowed,
    ResizeHorizontal,
    ResizeVertical,
    /// North-west / south-east diagonal resize.
    ResizeUpLeftDownRight,
    /// North-east / south-west diagonal resize.
    ResizeUpRightDownLeft,
}

pub use incular_core::PointerDeviceKind;

/// Physical pointer velocity in logical pixels per second.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Velocity {
    pub pixels_per_second: Offset,
}

impl Velocity {
    pub const ZERO: Self = Self {
        pixels_per_second: Offset::ZERO,
    };

    #[must_use]
    pub const fn new(pixels_per_second: Offset) -> Self {
        Self { pixels_per_second }
    }
}

/// Position and timing metadata for pointer tap-down events.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TapDownDetails {
    pub global_position: Offset,
    pub local_position: Offset,
    pub kind: PointerDeviceKind,
}

/// Position and timing metadata for pointer tap-up events.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TapUpDetails {
    pub global_position: Offset,
    pub local_position: Offset,
    pub kind: PointerDeviceKind,
}

/// Metadata for drag start events (Flutter `DragStartDetails`).
///
/// With Flutter's default `DragStartBehavior.start` the position is where the
/// drag was accepted, not where the pointer first went down.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DragStartDetails {
    /// Timestamp of the event that started the drag (`sourceTimeStamp`).
    pub source_time: Option<Instant>,
    pub global_position: Offset,
    pub local_position: Offset,
    /// The kind of the device that started the drag.
    pub kind: PointerDeviceKind,
}

/// Metadata for drag down events.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DragDownDetails {
    pub global_position: Offset,
    pub local_position: Offset,
}

/// Metadata for long-press start events.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LongPressStartDetails {
    pub global_position: Offset,
    pub local_position: Offset,
}

/// Metadata for long-press movement update events.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LongPressMoveUpdateDetails {
    pub global_position: Offset,
    pub local_position: Offset,
    pub offset_from_origin: Offset,
    pub local_offset_from_origin: Offset,
}

/// Metadata for long-press end events.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LongPressEndDetails {
    pub global_position: Offset,
    pub local_position: Offset,
    pub velocity: Velocity,
}

/// Callbacks understood by Incular's retained pointer gesture recognizer.
#[derive(Clone, Default)]
pub struct GestureCallbacks {
    pub on_tap: Option<Rc<dyn Fn()>>,
    pub on_tap_down: Option<Rc<dyn Fn(TapDownDetails)>>,
    pub on_tap_up: Option<Rc<dyn Fn(TapUpDetails)>>,
    pub on_tap_cancel: Option<Rc<dyn Fn()>>,
    pub on_double_tap: Option<Rc<dyn Fn()>>,
    pub on_double_tap_down: Option<Rc<dyn Fn(TapDownDetails)>>,
    pub on_double_tap_cancel: Option<Rc<dyn Fn()>>,
    pub on_long_press: Option<Rc<dyn Fn()>>,
    pub on_long_press_start: Option<Rc<dyn Fn(LongPressStartDetails)>>,
    pub on_long_press_move_update: Option<Rc<dyn Fn(LongPressMoveUpdateDetails)>>,
    pub on_long_press_up: Option<Rc<dyn Fn()>>,
    pub on_long_press_end: Option<Rc<dyn Fn(LongPressEndDetails)>>,
    pub on_pan_down: Option<Rc<dyn Fn(DragDownDetails)>>,
    pub on_pan_start: Option<Rc<dyn Fn(DragStartDetails)>>,
    pub on_pan_update: Option<Rc<dyn Fn(DragUpdateDetails)>>,
    /// Called once when a pan ends normally with the release velocity.
    pub on_pan_end: Option<Rc<dyn Fn(DragEndDetails)>>,
    pub on_pan_cancel: Option<Rc<dyn Fn()>>,
    pub on_horizontal_drag_down: Option<Rc<dyn Fn(DragDownDetails)>>,
    pub on_horizontal_drag_start: Option<Rc<dyn Fn(DragStartDetails)>>,
    /// Receives a pan whose first slop-exceeding movement was horizontal.
    /// It competes with vertical drags in a retained `GestureDetector`.
    pub on_horizontal_drag_update: Option<Rc<dyn Fn(DragUpdateDetails)>>,
    /// Called once when a horizontal drag ends normally.
    pub on_horizontal_drag_end: Option<Rc<dyn Fn(DragEndDetails)>>,
    pub on_horizontal_drag_cancel: Option<Rc<dyn Fn()>>,
    pub on_vertical_drag_down: Option<Rc<dyn Fn(DragDownDetails)>>,
    pub on_vertical_drag_start: Option<Rc<dyn Fn(DragStartDetails)>>,
    /// Receives a pan whose first slop-exceeding movement was vertical.
    /// It competes with horizontal drags in a retained `GestureDetector`.
    pub on_vertical_drag_update: Option<Rc<dyn Fn(DragUpdateDetails)>>,
    /// Called once when a vertical drag ends normally.
    pub on_vertical_drag_end: Option<Rc<dyn Fn(DragEndDetails)>>,
    pub on_vertical_drag_cancel: Option<Rc<dyn Fn()>>,
    pub on_scale_start: Option<Rc<dyn Fn(ScaleStartDetails)>>,
    /// Receives the active focal point and relative distance for a retained
    /// multi-pointer region. Single-pointer recognizers ignore this callback.
    pub on_scale_update: Option<Rc<dyn Fn(ScaleUpdateDetails)>>,
    pub on_scale_end: Option<Rc<dyn Fn(ScaleEndDetails)>>,
    /// Aggregate native trackpad gestures are delivered without manufacturing
    /// touch contacts. They use their own gesture-arena stream.
    pub on_trackpad_gesture: Option<Rc<dyn Fn(TrackpadGesture)>>,
    /// Called once for a retained pointer sequence when this region loses its
    /// arena claim or the platform cancels the sequence.
    pub on_cancel: Option<Rc<dyn Fn()>>,
    /// Receives a keyboard event after the retained runtime resolves the
    /// focused element. Returning `true` consumes the event and prevents
    /// outer keyboard scopes and text editing from seeing it.
    pub on_key: Option<Rc<dyn Fn(KeyboardEvent) -> bool>>,
    /// Receives key-down events that were not consumed by [`Self::on_key`].
    pub on_key_down: Option<Rc<dyn Fn(KeyboardEvent)>>,
    /// Receives repeated key-down events in addition to [`Self::on_key_down`].
    pub on_key_repeat: Option<Rc<dyn Fn(KeyboardEvent)>>,
    /// Receives key-up events that were not consumed by [`Self::on_key`].
    pub on_key_up: Option<Rc<dyn Fn(KeyboardEvent)>>,
    /// An optional typed shortcut dispatcher. Widgets owns the closure so
    /// this crate remains independent from the retained runtime.
    pub on_shortcut: Option<Rc<dyn Fn(KeyboardEvent) -> bool>>,
    /// A typed shortcut scope retained separately from the callback above so
    /// the widget tree can resolve an intent against action ancestors.
    pub shortcut_scope: Option<Rc<ErasedShortcutScope>>,
    /// A typed action scope used by descendant shortcut scopes.
    pub action_scope: Option<Rc<ErasedActionScope>>,
    /// Lifetime token for a higher-level action-listener registration.
    pub action_listener: Option<Rc<ActionListenerSubscription>>,
    /// Lifetime token for an action invocation listener installed by a
    /// retained `ActionListener` widget.
    pub action_invocation_listener: Option<Rc<ActionInvocationSubscription>>,
    /// Focus metadata associated with a keyboard listener. Keeping this on the
    /// platform-neutral callback value lets the retained tree route keyboard
    /// input without introducing a runtime dependency into gestures.
    pub focus_node: Option<FocusNode>,
    /// Combined retained focus, hover, and focus-highlight behavior.
    pub focus_behavior: Option<Rc<FocusBehavior>>,
    pub autofocus: bool,
    pub include_semantics: bool,
    /// Renderer-independent cursor preference for hover-capable widgets.
    pub mouse_cursor: MouseCursor,
    /// Flutter `GestureDetector.dragStartBehavior` for the drag and scale
    /// recognizers: [`DragStartBehavior::Start`] (the default) starts at the
    /// accepted position; [`DragStartBehavior::Down`] starts at the press and
    /// reports the movement that crossed the slop as the first update.
    pub drag_start_behavior: DragStartBehavior,
}

/// The callback action selected after a recognizer has claimed its arena.
/// This is public so retained adapters can defer callbacks until arbitration
/// has completed while the higher-level gesture detector remains source-compatible.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum GestureAction {
    Tap,
    DoubleTap,
    LongPress,
    /// The long-press deadline (`kLongPressTimeout`) elapsed with the pointer
    /// still down: the recognizer won the arena and `onLongPressStart` fires.
    LongPressStart(LongPressStartDetails),
    /// The pan recognizer won the arena (Flutter `onStart`).
    PanStart(DragStartDetails),
    Pan(DragUpdateDetails),
    PanEnd(DragEndDetails),
    /// The horizontal drag recognizer won the arena.
    HorizontalDragStart(DragStartDetails),
    HorizontalDrag(DragUpdateDetails),
    HorizontalDragEnd(DragEndDetails),
    /// The vertical drag recognizer won the arena.
    VerticalDragStart(DragStartDetails),
    VerticalDrag(DragUpdateDetails),
    VerticalDragEnd(DragEndDetails),
    /// The arena accepted an [`crate::ArenaGestureRecognizer`] for
    /// `pointer`; its recognizer's `acceptGesture` follows.
    ArenaAccepted {
        pointer: u64,
    },
}

/// A recognizer's claim for one pointer event.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum GestureDecision {
    Pending,
    Accept(GestureAction),
    Reject,
    Cancelled,
}

/// Details of one drag movement (Flutter `DragUpdateDetails`).
///
/// `delta` is the movement since the previous event, not the displacement
/// from the pointer-down position. Accepting a drag does not itself produce an
/// update: the first update reports the movement after acceptance.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DragUpdateDetails {
    /// Timestamp of the pointer event (`sourceTimeStamp`).
    pub source_time: Option<Instant>,
    /// Movement since the previous event. Horizontal and vertical drags only
    /// report their own axis; the other component is zero.
    pub delta: Offset,
    /// The movement along the drag axis. `None` for a pan, which has no axis.
    pub primary_delta: Option<f32>,
    pub global_position: Offset,
    pub local_position: Offset,
    /// The kind of the device that is dragging.
    pub kind: PointerDeviceKind,
}

/// Details of the end of a drag (Flutter `DragEndDetails`).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DragEndDetails {
    /// Velocity of the pointer when it stopped, zero for a slow release.
    pub velocity: Velocity,
    /// The velocity along the drag axis. `None` for a pan, which has no axis.
    pub primary_velocity: Option<f32>,
    pub global_position: Offset,
    pub local_position: Offset,
}

/// Callbacks for [`DragGestureDetector`](crate::DragGestureDetector).
#[derive(Clone, Default)]
pub struct DragCallbacks {
    pub on_start: Option<Rc<dyn Fn(DragStartDetails)>>,
    pub on_update: Option<Rc<dyn Fn(DragUpdateDetails)>>,
    pub on_end: Option<Rc<dyn Fn(DragEndDetails)>>,
}