Skip to main content

teksilo_core/
event.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4use teksilo_canvas::{Point, Rect};
5
6use crate::gesture::GestureEvent;
7use crate::pointer::{CancelReason, EventTime, PointerInfo, ScrollPhase};
8
9/// Pointer button identifiers.
10///
11/// `Forward` and `Back` correspond to the auxiliary mouse buttons (mouse
12/// 4 / mouse 5) typically labelled "browser back / forward". Platforms
13/// that don't have those buttons simply never emit them.
14///
15/// `#[non_exhaustive]`: a stylus barrel button and an eraser-end press are
16/// buttons this enum will have to name, and neither exists yet. A downstream
17/// `match` therefore needs a `_` arm.
18#[non_exhaustive]
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum PointerButton {
21    /// Left-click (or main-action button on left-handed mice).
22    Primary,
23    /// Right-click.
24    Secondary,
25    /// Middle / wheel-click.
26    Middle,
27    /// "Back" auxiliary button (mouse 4 on most 5-button mice). Often
28    /// bound to "navigate back" in browsers.
29    Back,
30    /// "Forward" auxiliary button (mouse 5). Often bound to "navigate
31    /// forward".
32    Forward,
33}
34
35/// Set of pointer buttons a gesture recognizer is configured to fire
36/// for. Used by the four click-style recognizers (`TapRecognizer`,
37/// `DoubleTapRecognizer`, `TripleTapRecognizer`, `LongPressRecognizer`)
38/// and the matching widget-level builders (`accept_tap_buttons`, …).
39///
40/// Default for every recognizer is [`ButtonMask::PRIMARY`] — left-click
41/// only — which matches the user's expectation for a "tap" and keeps
42/// right-click free to open a context menu without spuriously
43/// activating the widget. Use [`ButtonMask::ALL`] or a hand-built
44/// `PRIMARY | SECONDARY` etc. to opt into broader button sets.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub struct ButtonMask(u8);
47
48impl ButtonMask {
49    /// Empty mask — no buttons accepted.
50    pub const NONE: Self = Self(0);
51    /// Left-click on most desktop pointing devices.
52    pub const PRIMARY: Self = Self(1 << 0);
53    /// Right-click on most desktop pointing devices.
54    pub const SECONDARY: Self = Self(1 << 1);
55    /// Middle / wheel-click.
56    pub const MIDDLE: Self = Self(1 << 2);
57    /// "Back" auxiliary button (mouse 4).
58    pub const BACK: Self = Self(1 << 3);
59    /// "Forward" auxiliary button (mouse 5).
60    pub const FORWARD: Self = Self(1 << 4);
61    /// All buttons currently representable by [`PointerButton`].
62    pub const ALL: Self = Self(0b0001_1111);
63
64    /// `true` when the mask contains the given button.
65    pub const fn contains(self, button: PointerButton) -> bool {
66        let bit = match button {
67            PointerButton::Primary => 1 << 0,
68            PointerButton::Secondary => 1 << 1,
69            PointerButton::Middle => 1 << 2,
70            PointerButton::Back => 1 << 3,
71            PointerButton::Forward => 1 << 4,
72        };
73        self.0 & bit != 0
74    }
75
76    /// `true` when no buttons are accepted.
77    pub const fn is_empty(self) -> bool {
78        self.0 == 0
79    }
80
81    /// Union — accept any button in either mask.
82    pub const fn union(self, other: Self) -> Self {
83        Self(self.0 | other.0)
84    }
85
86    /// Intersection — accept only buttons present in both masks.
87    pub const fn intersection(self, other: Self) -> Self {
88        Self(self.0 & other.0)
89    }
90}
91
92impl From<PointerButton> for ButtonMask {
93    fn from(button: PointerButton) -> Self {
94        match button {
95            PointerButton::Primary => Self::PRIMARY,
96            PointerButton::Secondary => Self::SECONDARY,
97            PointerButton::Middle => Self::MIDDLE,
98            PointerButton::Back => Self::BACK,
99            PointerButton::Forward => Self::FORWARD,
100        }
101    }
102}
103
104impl<const N: usize> From<[PointerButton; N]> for ButtonMask {
105    fn from(buttons: [PointerButton; N]) -> Self {
106        let mut mask = Self::NONE;
107        let mut i = 0;
108        while i < N {
109            mask = mask.union(ButtonMask::from(buttons[i]));
110            i += 1;
111        }
112        mask
113    }
114}
115
116impl std::ops::BitOr for ButtonMask {
117    type Output = Self;
118    fn bitor(self, rhs: Self) -> Self {
119        self.union(rhs)
120    }
121}
122
123impl std::ops::BitAnd for ButtonMask {
124    type Output = Self;
125    fn bitand(self, rhs: Self) -> Self {
126        self.intersection(rhs)
127    }
128}
129
130impl std::ops::BitOrAssign for ButtonMask {
131    fn bitor_assign(&mut self, rhs: Self) {
132        self.0 |= rhs.0;
133    }
134}
135
136impl std::ops::BitAndAssign for ButtonMask {
137    fn bitand_assign(&mut self, rhs: Self) {
138        self.0 &= rhs.0;
139    }
140}
141
142impl Default for ButtonMask {
143    fn default() -> Self {
144        Self::PRIMARY
145    }
146}
147
148/// Keyboard key identifiers.
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
150pub enum Key {
151    Space,
152    Enter,
153    Escape,
154    Tab,
155    Backspace,
156    Delete,
157    Insert,
158    ArrowUp,
159    ArrowDown,
160    ArrowLeft,
161    ArrowRight,
162    Home,
163    End,
164    PageUp,
165    PageDown,
166    // Letters
167    A,
168    B,
169    C,
170    D,
171    E,
172    F,
173    G,
174    H,
175    I,
176    J,
177    K,
178    L,
179    M,
180    N,
181    O,
182    P,
183    Q,
184    R,
185    S,
186    T,
187    U,
188    V,
189    W,
190    X,
191    Y,
192    Z,
193    // Function keys
194    F1,
195    F2,
196    F3,
197    F4,
198    F5,
199    F6,
200    F7,
201    F8,
202    F9,
203    F10,
204    F11,
205    F12,
206    F13,
207    F14,
208    F15,
209    F16,
210    F17,
211    F18,
212    F19,
213    F20,
214    F21,
215    F22,
216    F23,
217    F24,
218    // Other
219    /// Caps Lock. Delivered as a discrete key press/release (winit's
220    /// `ModifiersState` does not carry lock state), so consumers that
221    /// need the *active* lock state track it themselves on the
222    /// key-down edge. See `WindowState::caps_lock`.
223    CapsLock,
224    /// The dedicated context-menu key: `VK_APPS` on Windows (the key between
225    /// the right Alt and the right Ctrl on most PC layouts), `keysyms::Menu` on
226    /// X11 and Wayland.
227    ///
228    /// **macOS never produces it.** Its keyboards have no such key and
229    /// `winit-0.30.13`'s AppKit backend references the variant zero times, so
230    /// on that platform the only keyboard route to a context menu is a chord.
231    /// See the dispatcher's context-menu handling for the chords Teksilo
232    /// reserves.
233    ContextMenu,
234    Character(char),
235}
236
237impl Key {
238    /// Returns the character this key represents, if any.
239    /// Maps `Key::A`..`Key::Z` to `'a'`..`'z'` (lowercase) and
240    /// `Key::Character(ch)` to `ch`.
241    pub fn to_char(&self) -> Option<char> {
242        match self {
243            Key::A => Some('a'),
244            Key::B => Some('b'),
245            Key::C => Some('c'),
246            Key::D => Some('d'),
247            Key::E => Some('e'),
248            Key::F => Some('f'),
249            Key::G => Some('g'),
250            Key::H => Some('h'),
251            Key::I => Some('i'),
252            Key::J => Some('j'),
253            Key::K => Some('k'),
254            Key::L => Some('l'),
255            Key::M => Some('m'),
256            Key::N => Some('n'),
257            Key::O => Some('o'),
258            Key::P => Some('p'),
259            Key::Q => Some('q'),
260            Key::R => Some('r'),
261            Key::S => Some('s'),
262            Key::T => Some('t'),
263            Key::U => Some('u'),
264            Key::V => Some('v'),
265            Key::W => Some('w'),
266            Key::X => Some('x'),
267            Key::Y => Some('y'),
268            Key::Z => Some('z'),
269            Key::Character(ch) => Some(*ch),
270            _ => None,
271        }
272    }
273
274    /// The text the platform attaches to this key, for the handful of named
275    /// keys that carry any. Mirrors winit's `NamedKey::to_text`, which is
276    /// where these values reach the app from.
277    ///
278    /// Worth knowing because it is surprising: Escape arrives carrying
279    /// U+001B, so a widget that reads `KeyDown::text` sees text on a key
280    /// nobody thinks of as text. A `TextInputField` used to filter that
281    /// control character out, read the empty result as "input rejected" and
282    /// swallow the key — which is how Escape stopped bubbling out of a
283    /// focused field.
284    ///
285    /// Character keys are deliberately absent: `Key::A` is `None` here, and
286    /// the way to simulate typing is `type_text`, which already sends text.
287    /// The gap this closes is only the surprising one.
288    pub fn to_text(&self) -> Option<&'static str> {
289        match self {
290            Key::Enter => Some("\r"),
291            Key::Backspace => Some("\u{8}"),
292            Key::Tab => Some("\t"),
293            Key::Space => Some(" "),
294            Key::Escape => Some("\u{1b}"),
295            _ => None,
296        }
297    }
298}
299
300/// Keyboard modifier state.
301#[derive(
302    Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
303)]
304pub struct Modifiers {
305    bits: u8,
306}
307
308impl Modifiers {
309    pub const NONE: Modifiers = Modifiers { bits: 0 };
310    pub const CTRL: Modifiers = Modifiers { bits: 1 };
311    pub const SHIFT: Modifiers = Modifiers { bits: 2 };
312    pub const ALT: Modifiers = Modifiers { bits: 4 };
313    pub const SUPER: Modifiers = Modifiers { bits: 8 };
314
315    /// The **primary accelerator** modifier for this platform: [`SUPER`]
316    /// (Command, ⌘) on macOS, [`CTRL`] everywhere else.
317    ///
318    /// Desktop platforms disagree about which physical key carries application
319    /// accelerators, and on macOS the disagreement is not cosmetic: Control is
320    /// reserved there for the text system and for the secondary click, while ⌘
321    /// is what a user presses for Save, Copy or Find. Code that hard-codes
322    /// [`CTRL`] to mean "the accelerator" therefore listens to the wrong key on
323    /// one of the three desktop platforms.
324    ///
325    /// Compare against this constant (or call [`Modifiers::command`]) and the
326    /// same code means Ctrl+A on Windows and Linux and ⌘A on macOS. This
327    /// mirrors Qt's `Qt::CTRL`, which likewise resolves to ⌘ on macOS, and the
328    /// convention the native menu bar already applies when it turns a declared
329    /// chord into an `NSMenuItem` key equivalent.
330    ///
331    /// [`SUPER`]: Modifiers::SUPER
332    /// [`CTRL`]: Modifiers::CTRL
333    pub const COMMAND: Modifiers = if cfg!(target_os = "macos") {
334        Self::SUPER
335    } else {
336        Self::CTRL
337    };
338
339    pub fn empty() -> Self {
340        Self::NONE
341    }
342
343    pub fn ctrl(self) -> bool {
344        self.bits & 1 != 0
345    }
346
347    pub fn shift(self) -> bool {
348        self.bits & 2 != 0
349    }
350
351    pub fn alt(self) -> bool {
352        self.bits & 4 != 0
353    }
354
355    pub fn super_key(self) -> bool {
356        self.bits & 8 != 0
357    }
358
359    /// Whether the platform's primary accelerator modifier
360    /// ([`Modifiers::COMMAND`]) is held: Command (⌘) on macOS, Control
361    /// everywhere else.
362    ///
363    /// Use this instead of [`ctrl`](Self::ctrl) wherever the chord means "the
364    /// accelerator" — select-all, the discontiguous-selection click, jump to
365    /// the end of a list. Keep [`ctrl`](Self::ctrl) for the chords that really
366    /// are Control on every platform, macOS included: Ctrl+Tab cycles tabs
367    /// there too (⌘Tab belongs to the application switcher and never reaches
368    /// an app).
369    pub fn command(self) -> bool {
370        self.contains(Self::COMMAND)
371    }
372
373    /// Whether every modifier in `other` is held.
374    pub fn contains(self, other: Modifiers) -> bool {
375        self.bits & other.bits == other.bits
376    }
377
378    /// These modifiers with `other` removed.
379    pub fn without(self, other: Modifiers) -> Modifiers {
380        Modifiers {
381            bits: self.bits & !other.bits,
382        }
383    }
384
385    /// These modifiers with a declared `CTRL` reinterpreted as the platform's
386    /// primary accelerator — see [`Modifiers::COMMAND`] and
387    /// [`KeyStroke::with_command_convention`](crate::shortcut::KeyStroke::with_command_convention),
388    /// which is where this is applied.
389    ///
390    /// A no-op off macOS (where `COMMAND` *is* `CTRL`), and a no-op for a chord
391    /// that already names `SUPER` explicitly: `Ctrl+Super` stays ⌃⌘, a genuine
392    /// two-modifier chord, rather than collapsing to one.
393    pub fn with_command_convention(self) -> Modifiers {
394        self.with_command_convention_using(Self::COMMAND)
395    }
396
397    /// The platform-parameterised core of
398    /// [`with_command_convention`](Self::with_command_convention). Split out so
399    /// the macOS branch is exercised by tests running on any host — the whole
400    /// point of the convention is behaviour a Linux CI cannot otherwise see.
401    ///
402    /// `pub(crate)` rather than private because the same split continues up the
403    /// stack: [`KeyStroke`](crate::shortcut::KeyStroke) and
404    /// [`Shortcut`](crate::shortcut::Shortcut) each carry a `_using` twin that
405    /// bottoms out here, so a shortcut's resolution can be asked "as macOS
406    /// would read it" from a Linux host without restating the rule.
407    pub(crate) fn with_command_convention_using(self, command: Modifiers) -> Modifiers {
408        if self.ctrl() && !self.super_key() {
409            self.without(Self::CTRL) | command
410        } else {
411            self
412        }
413    }
414}
415
416impl std::fmt::Display for Key {
417    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418        match self {
419            Key::Space => f.write_str("Space"),
420            Key::Enter => f.write_str("Enter"),
421            Key::Escape => f.write_str("Esc"),
422            Key::Tab => f.write_str("Tab"),
423            Key::Backspace => f.write_str("Backspace"),
424            Key::Delete => f.write_str("Del"),
425            Key::Insert => f.write_str("Ins"),
426            Key::ArrowUp => f.write_str("Up"),
427            Key::ArrowDown => f.write_str("Down"),
428            Key::ArrowLeft => f.write_str("Left"),
429            Key::ArrowRight => f.write_str("Right"),
430            Key::Home => f.write_str("Home"),
431            Key::End => f.write_str("End"),
432            Key::PageUp => f.write_str("PageUp"),
433            Key::PageDown => f.write_str("PageDown"),
434            Key::A => f.write_str("A"),
435            Key::B => f.write_str("B"),
436            Key::C => f.write_str("C"),
437            Key::D => f.write_str("D"),
438            Key::E => f.write_str("E"),
439            Key::F => f.write_str("F"),
440            Key::G => f.write_str("G"),
441            Key::H => f.write_str("H"),
442            Key::I => f.write_str("I"),
443            Key::J => f.write_str("J"),
444            Key::K => f.write_str("K"),
445            Key::L => f.write_str("L"),
446            Key::M => f.write_str("M"),
447            Key::N => f.write_str("N"),
448            Key::O => f.write_str("O"),
449            Key::P => f.write_str("P"),
450            Key::Q => f.write_str("Q"),
451            Key::R => f.write_str("R"),
452            Key::S => f.write_str("S"),
453            Key::T => f.write_str("T"),
454            Key::U => f.write_str("U"),
455            Key::V => f.write_str("V"),
456            Key::W => f.write_str("W"),
457            Key::X => f.write_str("X"),
458            Key::Y => f.write_str("Y"),
459            Key::Z => f.write_str("Z"),
460            Key::F1 => f.write_str("F1"),
461            Key::F2 => f.write_str("F2"),
462            Key::F3 => f.write_str("F3"),
463            Key::F4 => f.write_str("F4"),
464            Key::F5 => f.write_str("F5"),
465            Key::F6 => f.write_str("F6"),
466            Key::F7 => f.write_str("F7"),
467            Key::F8 => f.write_str("F8"),
468            Key::F9 => f.write_str("F9"),
469            Key::F10 => f.write_str("F10"),
470            Key::F11 => f.write_str("F11"),
471            Key::F12 => f.write_str("F12"),
472            Key::F13 => f.write_str("F13"),
473            Key::F14 => f.write_str("F14"),
474            Key::F15 => f.write_str("F15"),
475            Key::F16 => f.write_str("F16"),
476            Key::F17 => f.write_str("F17"),
477            Key::F18 => f.write_str("F18"),
478            Key::F19 => f.write_str("F19"),
479            Key::F20 => f.write_str("F20"),
480            Key::F21 => f.write_str("F21"),
481            Key::F22 => f.write_str("F22"),
482            Key::F23 => f.write_str("F23"),
483            Key::F24 => f.write_str("F24"),
484            Key::CapsLock => f.write_str("CapsLock"),
485            Key::ContextMenu => f.write_str("Menu"),
486            Key::Character(c) => write!(f, "{}", c.to_uppercase()),
487        }
488    }
489}
490
491impl std::fmt::Display for Modifiers {
492    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
493        if self.ctrl() {
494            f.write_str("Ctrl+")?;
495        }
496        if self.alt() {
497            f.write_str("Alt+")?;
498        }
499        if self.shift() {
500            f.write_str("Shift+")?;
501        }
502        if self.super_key() {
503            // Named for the key the user is looking at. This string reaches
504            // assistive tech through the accessibility tree's
505            // `keyboard_shortcut`, and a Mac screen-reader user announced
506            // "Super+S" for ⌘S has been told the wrong key.
507            f.write_str(if cfg!(target_os = "macos") {
508                "Cmd+"
509            } else {
510                "Super+"
511            })?;
512        }
513        Ok(())
514    }
515}
516
517impl std::ops::BitOr for Modifiers {
518    type Output = Self;
519    fn bitor(self, rhs: Self) -> Self {
520        Modifiers {
521            bits: self.bits | rhs.bits,
522        }
523    }
524}
525
526/// Scroll delta from mouse wheel or trackpad.
527#[derive(Debug, Clone, Copy, PartialEq)]
528pub enum ScrollDelta {
529    /// Line-based scrolling (mouse wheel).
530    Lines { x: f32, y: f32 },
531    /// Pixel-based scrolling (trackpad).
532    Pixels { x: f32, y: f32 },
533}
534
535/// Where a [`WidgetEvent::ScrollIntoView`] target should come to rest on the
536/// scroll container's vertical axis.
537///
538/// The horizontal axis is always revealed minimally — a fraction only has an
539/// obvious meaning for the axis the request is *about*, and pinning a caret
540/// vertically must not yank a horizontally-scrolled view sideways.
541#[derive(Debug, Clone, Copy, PartialEq)]
542pub enum ScrollAlign {
543    /// Scroll the least amount that makes the target fully visible, and not at
544    /// all when it already is. This is what focus-driven reveals and
545    /// [`EventContext::ensure_visible`](crate::widget::EventContext::ensure_visible)
546    /// use, and it is the behaviour every scroll container had before
547    /// alignment existed.
548    Minimal,
549    /// Pin the target at `f` of the way down the viewport — `0.0` flush with
550    /// the top, `0.5` centred, `1.0` flush with the bottom — **whether or not
551    /// it is already visible**. Being unconditional is the whole point: a
552    /// typewriter-scrolling caret that only moved the view when it fell off
553    /// the edge would not be pinned at all.
554    ///
555    /// The container still clamps to its scroll range, so a target near the
556    /// start or end of the content comes to rest as close to `f` as the range
557    /// allows. See [`ScrollArea::scroll_past_end`] for buying range past the
558    /// end of the content so the last line can still reach the pin.
559    ///
560    /// [`ScrollArea::scroll_past_end`]: https://docs.rs/teksilo-widgets
561    Fraction(f32),
562}
563
564/// Whether a [`WidgetEvent::ScrollIntoView`] should jump or glide.
565///
566/// Split out from the container's own `smooth_scrolling` setting because the
567/// right answer depends on the *request*, not the container: a caret pinned on
568/// every keystroke must snap (animating it is what produces the "screen
569/// bouncing" typewriter-mode users complain about in other editors), while the
570/// same container gliding for a page-down or a search hit reads as polish.
571#[derive(Debug, Clone, Copy, PartialEq, Eq)]
572pub enum ScrollMotion {
573    /// Jump straight to the target offset.
574    Instant,
575    /// Animate to the target offset, if the container has smooth scrolling
576    /// enabled. Containers with `smooth_scrolling(false)` still jump.
577    Smooth,
578}
579
580/// Events dispatched to widgets.
581#[derive(Debug, Clone)]
582pub enum WidgetEvent {
583    /// A button went down.
584    PointerDown {
585        /// Where, in the receiving widget's own coordinate space (the router
586        /// localises it on delivery — see
587        /// [`WidgetTree::dispatch_pointer`](crate::WidgetTree::dispatch_pointer)).
588        position: Point,
589        /// Which button. A direct pointer reports
590        /// [`PointerButton::Primary`] for a contact.
591        button: PointerButton,
592        /// Modifier keys held when the press landed.
593        modifiers: Modifiers,
594        /// Who pressed — identity, kind, buttons, axes and timestamp.
595        ///
596        /// Read it through [`EventContext::pointer`](crate::widget::EventContext::pointer)
597        /// or its `pointer_kind()` shorthand rather than by destructuring, so a
598        /// widget that only needs "was this a finger?" does not have to name
599        /// the whole struct. Defaults to
600        /// [`PointerInfo::mouse`] at the epoch for every legacy construction
601        /// site and for [`pointer_down`](Self::pointer_down), so a site that
602        /// says nothing about pointers keeps meaning what it meant before the
603        /// touch programme.
604        pointer: PointerInfo,
605    },
606    /// A button came up.
607    PointerUp {
608        /// Where, in the receiving widget's own coordinate space.
609        position: Point,
610        /// Which button was released.
611        button: PointerButton,
612        /// Modifier keys held when the release landed.
613        modifiers: Modifiers,
614        /// Who released. See [`PointerDown::pointer`](Self::PointerDown).
615        pointer: PointerInfo,
616    },
617    /// A pointer moved. Sent whether or not a button is held; a contact only
618    /// ever moves with its button held, since a finger cannot hover.
619    PointerMove {
620        /// Where, in the receiving widget's own coordinate space.
621        position: Point,
622        /// Modifier keys held during the move.
623        ///
624        /// A drag decides what it means from the modifiers at the *move*, not
625        /// at the press — Shift extends a selection and Ctrl makes a marquee
626        /// additive from the moment the key goes down, mid-drag included.
627        /// Defaults to [`Modifiers::NONE`] for
628        /// [`pointer_move`](Self::pointer_move) and for a producer that tracks
629        /// no modifier state.
630        modifiers: Modifiers,
631        /// Who moved. See [`PointerDown::pointer`](Self::PointerDown).
632        pointer: PointerInfo,
633    },
634    /// The hover owner came onto this widget. Never sent for a contact: a
635    /// finger writes no hover (see
636    /// [`PointerTable::hover_owner`](crate::pointer::table::PointerTable::hover_owner)).
637    PointerEnter {
638        /// Who entered — a mouse, or a pen in proximity.
639        pointer: PointerInfo,
640    },
641    /// The hover owner left this widget.
642    PointerLeave {
643        /// Who left. See [`PointerEnter`](Self::PointerEnter).
644        pointer: PointerInfo,
645    },
646    Scroll {
647        delta: ScrollDelta,
648        /// Modifier keys held at the time of the scroll event.
649        /// Defaults to `Modifiers::NONE` for synthesized events
650        /// (tests, keyboard-driven scroll requests). Real-platform
651        /// scroll events populate this from the platform's tracked
652        /// modifier state — apps detect Ctrl-wheel-to-zoom by
653        /// inspecting `modifiers.ctrl()`.
654        modifiers: Modifiers,
655        /// Where the pointer was when the scroll happened, in **window**-logical
656        /// coordinates, or `None` when the producer has no position for it.
657        ///
658        /// The frame is in the name because it is the one positional field a
659        /// handler receives that is *not* localised to the receiving widget
660        /// (`localize_event` deliberately has no `Scroll` arm), and a widget
661        /// that reads it as a local point silently lands a cell or a row out.
662        /// Convert with the receiver's own bounds before using it as content
663        /// coordinates.
664        ///
665        /// It is window-space because both of its frame-sensitive uses need it
666        /// to be. The router **routes** by it — `Some` hit-tests, `None` falls
667        /// back to the hovered (else focused) widget — and hit-testing is
668        /// necessarily window-space. And `common/scrollable.rs`'s
669        /// `handle_scroll_event` feeds it to `pan_step` →
670        /// [`KineticScroller::pan`](crate::kinetic::KineticScroller::pan),
671        /// whose tracker follows the *pointer*: localisation resolves against
672        /// the captor's **current** bounds on every event, so a localised
673        /// position would feed that tracker samples polluted by the motion of
674        /// the very widget being measured.
675        ///
676        /// A mouse wheel has always been positionless and stays so — hover is
677        /// under the cursor, so hit-testing would find the same widget anyway.
678        /// A pan synthesised from a direct pointer *must* carry one, because a
679        /// contact never writes hover and a positionless pan would route
680        /// nowhere.
681        window_position: Option<Point>,
682        /// Where in a continuous scroll gesture this sample sits.
683        /// [`ScrollPhase::Discrete`] — a self-contained wheel notch — for
684        /// everything Teksilo produced before the touch programme.
685        phase: ScrollPhase,
686        /// Who scrolled. Defaults to
687        /// [`PointerInfo::mouse`](crate::pointer::PointerInfo::mouse) at the
688        /// epoch for every legacy construction site; a real sample carries the
689        /// pointer's identity, kind and timestamp.
690        pointer: PointerInfo,
691    },
692    /// A pointer interaction was revoked by the system rather than completed by
693    /// the user — see [`CancelReason`].
694    ///
695    /// Distinct from [`PointerUp`](Self::PointerUp) on purpose: an Up means the
696    /// user finished, so a drag drops and a tap fires; a cancel means the
697    /// interaction is being taken away, so state must be unwound and nothing
698    /// may activate.
699    ///
700    /// **Terminal**: no `PointerUp` follows for that pointer, and one that
701    /// arrives anyway is swallowed. Delivered by the cancel funnel,
702    /// [`WidgetTree::cancel_pointer`](crate::WidgetTree::cancel_pointer), to
703    /// the widget holding the pointer — or, failing that, to the last one that
704    /// accepted an event from it. A widget receives it through
705    /// `.on_pointer_cancel(..)` or through its raw `on_pointer_event` hook.
706    PointerCancel {
707        /// Where the pointer was last seen, in **window**-logical coordinates,
708        /// when the revoking path knows. A platform cancel usually carries no
709        /// position at all.
710        ///
711        /// Window-space, and named for it, for the same reason as
712        /// [`Scroll::window_position`](Self::Scroll) — but kept there by a
713        /// different mechanism, worth knowing before "fixing" either. The cancel
714        /// funnel delivers through the router's **non**-localising route
715        /// (`dispatch_to_widget_direct`), so what puts this value in window space
716        /// is simply that the funnel records the pointer table's own position
717        /// verbatim; `localize_event` having no `PointerCancel` arm is true but
718        /// would not matter on this path. A widget whose
719        /// `PointerDown`/`Move`/`Up` handling works in local coordinates must
720        /// convert before feeding this to the same sink.
721        window_position: Option<Point>,
722        /// Why the interaction was revoked.
723        reason: CancelReason,
724        /// Which pointer was revoked.
725        pointer: PointerInfo,
726    },
727    KeyDown {
728        key: Key,
729        modifiers: Modifiers,
730        text: Option<String>,
731    },
732    KeyUp {
733        key: Key,
734        modifiers: Modifiers,
735    },
736    ImeComposition {
737        text: String,
738        cursor: Option<std::ops::Range<usize>>,
739    },
740    ImeCommit {
741        text: String,
742    },
743    FocusGained {
744        origin: crate::focus::FocusOrigin,
745    },
746    FocusLost,
747    AccessAction {
748        action: accesskit::Action,
749        target: Option<crate::widget_id::WidgetId>,
750        /// Raw AccessKit NodeId from the original `ActionRequest`.
751        /// May be a synthetic (widget-emitted child) NodeId — use
752        /// `crate::accessibility::is_synthetic` to distinguish it
753        /// from a widget-derived NodeId. The widget that registered
754        /// the parent (retrieved via `tree.widget_for_synthetic`)
755        /// is the one set in `target`.
756        target_node: accesskit::NodeId,
757        /// Payload carried by the `ActionRequest`. For
758        /// `Action::SetTextSelection` this is
759        /// `ActionData::SetTextSelection(TextSelection)`, for
760        /// `Action::SetValue` it's `ActionData::Value(Box<str>)`,
761        /// for scroll actions it carries scroll offsets, etc.
762        /// Widgets that declare these actions must read the payload
763        /// to honour screen-reader-initiated requests.
764        data: Option<accesskit::ActionData>,
765    },
766    /// Dispatched by the framework to a clipping ancestor when a child
767    /// gains focus but is outside the viewport. The scroll area adjusts
768    /// its offset to make the target bounds visible, with an optional
769    /// margin around the target.
770    ScrollIntoView {
771        target_bounds: Rect,
772        /// Extra margin (in logical pixels) to keep around the target
773        /// when scrolling it into view. Defaults to 0.0.
774        margin: f32,
775        /// Where the target should end up on the scroll container's
776        /// **vertical** axis. [`ScrollAlign::Minimal`] (the default, and what
777        /// every focus-driven reveal uses) only scrolls when the target is not
778        /// already fully visible; [`ScrollAlign::Fraction`] *pins* it to a
779        /// fixed height in the viewport whether or not it was already visible.
780        align: ScrollAlign,
781        /// Whether the container should jump to the new offset or glide to it.
782        /// See [`ScrollMotion`].
783        motion: ScrollMotion,
784        /// Optional back-channel for the handling scroll container to report
785        /// how far it actually scrolled (`(dx, dy)` in content pixels). When
786        /// several nested scroll containers must each reveal the same target,
787        /// the ancestor walk (`scroll_rect_into_view`) reads this after
788        /// dispatching to an inner container and shifts `target_bounds` by the
789        /// negated delta before asking the next (outer) one — so the outer sees
790        /// where the target will land once the inner's (deferred) scroll
791        /// applies, not its pre-scroll position. `None` disables reporting (the
792        /// nested-reveal refinement is unavailable). A handler that ignores it
793        /// still works for the common single-container case.
794        ///
795        /// `Arc<Mutex<..>>` (not `Rc<Cell<..>>`) so `WidgetEvent` stays `Send`
796        /// — some events are posted across threads. This one is only ever
797        /// touched on the dispatch thread, so the lock is always uncontended.
798        applied_scroll: Option<std::sync::Arc<std::sync::Mutex<teksilo_canvas::Point>>>,
799    },
800    /// A recognized gesture event, routed through the same preview/bubble system.
801    Gesture {
802        gesture: GestureEvent,
803    },
804}
805
806impl WidgetEvent {
807    /// A wheel notch with no position — routed by the hovered (else focused)
808    /// widget, exactly as every scroll in Teksilo was before the touch
809    /// programme.
810    ///
811    /// It exists so that the three fields [`Scroll`](Self::Scroll) gained cost
812    /// each of its construction sites one line rather than five. The pointer
813    /// defaults to
814    /// [`PointerInfo::mouse`] at [`EventTime::ZERO`]: a free constructor has no
815    /// tree and therefore no clock, and nothing reads the timestamp of a
816    /// legacy-constructed event. A sample that has a real time enters through
817    /// [`WidgetTree::dispatch_scroll`](crate::WidgetTree::dispatch_scroll)
818    /// instead.
819    pub fn scroll(delta: ScrollDelta, modifiers: Modifiers) -> Self {
820        Self::Scroll {
821            delta,
822            modifiers,
823            window_position: None,
824            phase: ScrollPhase::Discrete,
825            pointer: PointerInfo::mouse(EventTime::ZERO),
826        }
827    }
828
829    /// A wheel notch routed by hit test at `position` rather than by hover.
830    ///
831    /// Use this where the producer genuinely knows where the pointer was; a
832    /// mouse-wheel translator should keep using [`scroll`](Self::scroll), whose
833    /// hover routing is what it has always had.
834    pub fn scroll_at(delta: ScrollDelta, modifiers: Modifiers, position: Point) -> Self {
835        Self::Scroll {
836            delta,
837            modifiers,
838            window_position: Some(position),
839            phase: ScrollPhase::Discrete,
840            pointer: PointerInfo::mouse(EventTime::ZERO),
841        }
842    }
843
844    /// A mouse press: [`PointerInfo::mouse`] at the epoch.
845    ///
846    /// This and its siblings are why adding `pointer` to the five `Pointer*`
847    /// variants was a one-line-per-site sweep rather than a rewrite. Use them
848    /// wherever the producer genuinely describes a mouse — every test that is
849    /// pinning mouse behaviour, and every synthesizer that has no pointer of
850    /// its own. A producer that *does* know which pointer it speaks for must
851    /// write the variant out and thread the real [`PointerInfo`], or
852    /// `ctx.pointer_kind()` reads `Mouse` for a finger and every direct-pointer
853    /// branch in the framework silently takes the indirect path.
854    pub fn pointer_down(position: Point, button: PointerButton, modifiers: Modifiers) -> Self {
855        Self::PointerDown {
856            position,
857            button,
858            modifiers,
859            pointer: PointerInfo::mouse(EventTime::ZERO),
860        }
861    }
862
863    /// A mouse release. See [`pointer_down`](Self::pointer_down).
864    pub fn pointer_up(position: Point, button: PointerButton, modifiers: Modifiers) -> Self {
865        Self::PointerUp {
866            position,
867            button,
868            modifiers,
869            pointer: PointerInfo::mouse(EventTime::ZERO),
870        }
871    }
872
873    /// A mouse move with no modifiers held. See
874    /// [`pointer_down`](Self::pointer_down); use
875    /// [`pointer_move_with`](Self::pointer_move_with) where the producer tracks
876    /// modifier state, since a drag reads Shift and Ctrl from the *move*.
877    pub fn pointer_move(position: Point) -> Self {
878        Self::pointer_move_with(position, Modifiers::NONE)
879    }
880
881    /// A mouse move carrying tracked modifier state. See
882    /// [`pointer_down`](Self::pointer_down).
883    pub fn pointer_move_with(position: Point, modifiers: Modifiers) -> Self {
884        Self::PointerMove {
885            position,
886            modifiers,
887            pointer: PointerInfo::mouse(EventTime::ZERO),
888        }
889    }
890
891    /// The mouse entered a widget. See [`pointer_down`](Self::pointer_down).
892    pub fn pointer_enter() -> Self {
893        Self::PointerEnter {
894            pointer: PointerInfo::mouse(EventTime::ZERO),
895        }
896    }
897
898    /// The mouse left a widget. See [`pointer_down`](Self::pointer_down).
899    pub fn pointer_leave() -> Self {
900        Self::PointerLeave {
901            pointer: PointerInfo::mouse(EventTime::ZERO),
902        }
903    }
904}
905
906/// The result of handling an event.
907#[derive(Debug, Clone, Copy, PartialEq, Eq)]
908pub enum EventResponse {
909    /// The event was handled; stop propagation.
910    Handled,
911    /// The event was not handled; let it bubble.
912    Ignored,
913}
914
915#[cfg(test)]
916mod modifier_tests {
917    use super::*;
918
919    // The convention itself, exercised on both platform settings from any host.
920    // `Modifiers::COMMAND` resolves at compile time, so a Linux CI would
921    // otherwise only ever see half of what this rule does — and the half it
922    // cannot see is the one the rule exists for.
923
924    #[test]
925    fn command_convention_rewrites_a_bare_ctrl_on_macos() {
926        let mac = Modifiers::CTRL.with_command_convention_using(Modifiers::SUPER);
927        assert_eq!(mac, Modifiers::SUPER);
928
929        let mac =
930            (Modifiers::CTRL | Modifiers::SHIFT).with_command_convention_using(Modifiers::SUPER);
931        assert_eq!(mac, Modifiers::SUPER | Modifiers::SHIFT);
932    }
933
934    #[test]
935    fn command_convention_is_a_no_op_where_command_is_ctrl() {
936        for m in [
937            Modifiers::CTRL,
938            Modifiers::CTRL | Modifiers::SHIFT,
939            Modifiers::ALT,
940            Modifiers::NONE,
941            Modifiers::SUPER,
942        ] {
943            assert_eq!(m.with_command_convention_using(Modifiers::CTRL), m);
944        }
945    }
946
947    #[test]
948    fn command_convention_leaves_an_explicit_super_alone() {
949        // A chord that already names Super is a deliberate ⌘ chord, and
950        // `Ctrl+Super` is a genuine two-modifier chord — neither collapses.
951        assert_eq!(
952            Modifiers::SUPER.with_command_convention_using(Modifiers::SUPER),
953            Modifiers::SUPER
954        );
955        let both = Modifiers::CTRL | Modifiers::SUPER;
956        assert_eq!(both.with_command_convention_using(Modifiers::SUPER), both);
957    }
958
959    #[test]
960    fn command_convention_is_idempotent() {
961        for command in [Modifiers::CTRL, Modifiers::SUPER] {
962            for m in [
963                Modifiers::CTRL,
964                Modifiers::CTRL | Modifiers::SHIFT | Modifiers::ALT,
965                Modifiers::SUPER,
966                Modifiers::NONE,
967            ] {
968                let once = m.with_command_convention_using(command);
969                assert_eq!(once.with_command_convention_using(command), once);
970            }
971        }
972    }
973
974    #[test]
975    fn command_predicate_follows_the_platform() {
976        // Whichever platform this runs on, `COMMAND` is one of the two, and
977        // `command()` tracks exactly it.
978        assert!(Modifiers::COMMAND.command());
979        assert!(!Modifiers::ALT.command());
980        assert!((Modifiers::COMMAND | Modifiers::SHIFT).command());
981
982        if cfg!(target_os = "macos") {
983            assert_eq!(Modifiers::COMMAND, Modifiers::SUPER);
984            assert!(!Modifiers::CTRL.command());
985        } else {
986            assert_eq!(Modifiers::COMMAND, Modifiers::CTRL);
987            assert!(!Modifiers::SUPER.command());
988        }
989    }
990
991    #[test]
992    fn contains_requires_every_named_modifier() {
993        let cs = Modifiers::CTRL | Modifiers::SHIFT;
994        assert!(cs.contains(Modifiers::CTRL));
995        assert!(cs.contains(cs));
996        assert!(!cs.contains(Modifiers::CTRL | Modifiers::ALT));
997        assert!(cs.contains(Modifiers::NONE));
998    }
999
1000    #[test]
1001    fn without_clears_only_the_named_modifiers() {
1002        let all = Modifiers::CTRL | Modifiers::SHIFT | Modifiers::SUPER;
1003        assert_eq!(
1004            all.without(Modifiers::SUPER),
1005            Modifiers::CTRL | Modifiers::SHIFT
1006        );
1007        assert_eq!(all.without(Modifiers::ALT), all);
1008    }
1009}