Skip to main content

oxiui_core/
events.rs

1//! Input event types: mouse buttons, keyboard keys, and modifier state.
2//!
3//! These types back the richer [`UiEvent`](crate::UiEvent) variants
4//! (`MouseDown`, `KeyDown`, …) added on top of the original immediate-mode
5//! event set. The original variants (`Resize`, `Mouse`, `KeyPress`, IME …)
6//! are retained for backward compatibility with existing adapters.
7
8/// A mouse button.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub enum MouseButton {
12    /// Primary (usually left) button.
13    Left,
14    /// Secondary (usually right) button.
15    Right,
16    /// Middle / wheel button.
17    Middle,
18    /// Any other button, identified by platform index.
19    Other(u16),
20}
21
22/// Keyboard modifier-key state.
23///
24/// `meta` is the Command key on macOS and the Super/Windows key elsewhere.
25#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub struct Modifiers {
28    /// Control key held.
29    pub ctrl: bool,
30    /// Alt / Option key held.
31    pub alt: bool,
32    /// Shift key held.
33    pub shift: bool,
34    /// Meta (Command / Super / Windows) key held.
35    pub meta: bool,
36}
37
38impl Modifiers {
39    /// No modifiers held.
40    pub const NONE: Modifiers = Modifiers {
41        ctrl: false,
42        alt: false,
43        shift: false,
44        meta: false,
45    };
46
47    /// Returns `true` if no modifier keys are held.
48    pub fn is_empty(self) -> bool {
49        !self.ctrl && !self.alt && !self.shift && !self.meta
50    }
51
52    /// Returns `true` if the platform "command" modifier is held: `meta` on
53    /// macOS-style platforms, `ctrl` elsewhere. Callers that don't track the
54    /// platform can treat either as the accelerator modifier.
55    pub fn command(self) -> bool {
56        self.ctrl || self.meta
57    }
58}
59
60/// A logical key, abstracted away from physical scan codes.
61///
62/// `Character` carries the produced text for printable keys; named variants
63/// cover the common non-printable keys needed for navigation and editing.
64///
65/// The enum is `#[non_exhaustive]`: new variants added in future releases will
66/// deserialise as a serde unknown-field error rather than silently mismatching.
67#[derive(Clone, Debug, PartialEq, Eq)]
68#[non_exhaustive]
69#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
70pub enum Key {
71    /// A printable character (already mapped through the active layout/IME).
72    Character(String),
73    /// Enter / Return.
74    Enter,
75    /// Tab.
76    Tab,
77    /// Space bar.
78    Space,
79    /// Backspace.
80    Backspace,
81    /// Delete (forward delete).
82    Delete,
83    /// Escape.
84    Escape,
85    /// Left arrow.
86    ArrowLeft,
87    /// Right arrow.
88    ArrowRight,
89    /// Up arrow.
90    ArrowUp,
91    /// Down arrow.
92    ArrowDown,
93    /// Home.
94    Home,
95    /// End.
96    End,
97    /// Page Up.
98    PageUp,
99    /// Page Down.
100    PageDown,
101    /// A function key `F1`–`F24` (the `u8` is the number, 1-based).
102    Function(u8),
103    /// Any other named key, identified by string (forward-compatible escape).
104    Named(String),
105}
106
107impl Key {
108    /// If this key produces text, return it; otherwise `None`.
109    pub fn as_text(&self) -> Option<&str> {
110        match self {
111            Key::Character(s) => Some(s.as_str()),
112            Key::Space => Some(" "),
113            _ => None,
114        }
115    }
116}
117
118/// Mouse scroll-wheel delta.
119///
120/// Positive `y` scrolls content up (wheel away from the user) by convention.
121#[derive(Clone, Copy, Debug, PartialEq)]
122#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
123pub enum ScrollDelta {
124    /// Discrete line-based scrolling (mouse wheel notches).
125    Lines {
126        /// Horizontal lines.
127        x: f32,
128        /// Vertical lines.
129        y: f32,
130    },
131    /// Smooth pixel-precise scrolling (trackpads).
132    Pixels {
133        /// Horizontal pixels.
134        x: f32,
135        /// Vertical pixels.
136        y: f32,
137    },
138}
139
140// ── Rich pointer / keyboard / touch events (dispatch layer) ─────────────────
141//
142// The variants below feed the [`EventDispatcher`](crate::dispatch::EventDispatcher)
143// capture/bubble pipeline. They are richer than the immediate-mode
144// [`UiEvent`](crate::UiEvent) set (which adapters emit) and carry geometry in
145// [`Point`](crate::geometry::Point) form for hit testing.
146
147use crate::geometry::Point;
148
149/// A pointer (mouse / trackpad) event in tree-local coordinates.
150#[derive(Clone, Debug, PartialEq)]
151pub enum MouseEvent {
152    /// A button was pressed.
153    Down {
154        /// Position in tree-local logical pixels.
155        pos: Point,
156        /// Which button.
157        button: MouseButton,
158        /// Modifier state.
159        modifiers: Modifiers,
160    },
161    /// A button was released.
162    Up {
163        /// Position in tree-local logical pixels.
164        pos: Point,
165        /// Which button.
166        button: MouseButton,
167        /// Modifier state.
168        modifiers: Modifiers,
169    },
170    /// The pointer moved without a press/release.
171    Move {
172        /// New position.
173        pos: Point,
174        /// Modifier state.
175        modifiers: Modifiers,
176    },
177    /// The pointer entered a widget's bounds.
178    Enter {
179        /// Position at entry.
180        pos: Point,
181    },
182    /// The pointer left a widget's bounds.
183    Leave {
184        /// Position at exit (last known).
185        pos: Point,
186    },
187    /// A double click (two `Down`s within the platform double-click window).
188    DoubleClick {
189        /// Position.
190        pos: Point,
191        /// Which button.
192        button: MouseButton,
193        /// Modifier state.
194        modifiers: Modifiers,
195    },
196    /// A triple click (three rapid `Down`s; selects a paragraph by convention).
197    TripleClick {
198        /// Position.
199        pos: Point,
200        /// Which button.
201        button: MouseButton,
202        /// Modifier state.
203        modifiers: Modifiers,
204    },
205    /// A scroll-wheel / trackpad scroll, discrete or smooth (see [`ScrollDelta`]).
206    Scroll {
207        /// Position of the pointer when the scroll occurred.
208        pos: Point,
209        /// Scroll delta.
210        delta: ScrollDelta,
211        /// Modifier state.
212        modifiers: Modifiers,
213    },
214    /// A drag gesture began (button held and moved past the start threshold).
215    DragStart {
216        /// Position where the drag started.
217        pos: Point,
218        /// Which button initiated the drag.
219        button: MouseButton,
220    },
221    /// The pointer moved while dragging.
222    DragMove {
223        /// Current position.
224        pos: Point,
225        /// Movement since the previous `DragMove`/`DragStart`.
226        delta: Point,
227    },
228    /// A drag gesture ended (button released).
229    DragEnd {
230        /// Position where the drag ended.
231        pos: Point,
232        /// Which button was released.
233        button: MouseButton,
234    },
235}
236
237impl MouseEvent {
238    /// The pointer position carried by this event.
239    pub fn position(&self) -> Point {
240        match self {
241            MouseEvent::Down { pos, .. }
242            | MouseEvent::Up { pos, .. }
243            | MouseEvent::Move { pos, .. }
244            | MouseEvent::Enter { pos }
245            | MouseEvent::Leave { pos }
246            | MouseEvent::DoubleClick { pos, .. }
247            | MouseEvent::TripleClick { pos, .. }
248            | MouseEvent::Scroll { pos, .. }
249            | MouseEvent::DragStart { pos, .. }
250            | MouseEvent::DragMove { pos, .. }
251            | MouseEvent::DragEnd { pos, .. } => *pos,
252        }
253    }
254}
255
256/// A physical key location, independent of the active keyboard layout.
257///
258/// Identified by a USB-HID-style code name (e.g. `"KeyA"`, `"Digit1"`). This
259/// distinguishes the *position* pressed from the *character produced* (which
260/// lives in the logical [`Key`]); a French AZERTY `Q` key and a US QWERTY `A`
261/// key share the same physical code `"KeyA"`.
262#[derive(Clone, Debug, PartialEq, Eq, Hash)]
263pub struct PhysicalKey(pub String);
264
265impl PhysicalKey {
266    /// Construct from a code name.
267    pub fn new(code: impl Into<String>) -> Self {
268        Self(code.into())
269    }
270
271    /// Borrow the code name.
272    pub fn code(&self) -> &str {
273        &self.0
274    }
275}
276
277/// A keyboard event carrying both logical and physical key information.
278#[derive(Clone, Debug, PartialEq)]
279pub enum KeyboardEvent {
280    /// A key was pressed (or auto-repeated; see `repeat`).
281    Down {
282        /// The logical key (layout/IME mapped).
283        key: Key,
284        /// The physical key location, if known.
285        physical: Option<PhysicalKey>,
286        /// Modifier state.
287        modifiers: Modifiers,
288        /// `true` if this is an OS auto-repeat of a held key.
289        repeat: bool,
290    },
291    /// A key was released.
292    Up {
293        /// The logical key.
294        key: Key,
295        /// The physical key location, if known.
296        physical: Option<PhysicalKey>,
297        /// Modifier state.
298        modifiers: Modifiers,
299    },
300    /// Committed text input (post-IME). Distinct from `Down` so a text field can
301    /// ignore navigation keys while still receiving typed characters.
302    CharInput {
303        /// The committed text (may be multiple code points).
304        text: String,
305    },
306}
307
308impl KeyboardEvent {
309    /// Returns `true` if this is an auto-repeat `Down` event.
310    pub fn is_repeat(&self) -> bool {
311        matches!(self, KeyboardEvent::Down { repeat: true, .. })
312    }
313}
314
315/// A recognised multi-touch gesture.
316#[derive(Clone, Copy, Debug, PartialEq)]
317pub enum GestureKind {
318    /// A two-finger pinch; `scale` is the relative scale factor (`1.0` = none).
319    Pinch {
320        /// Relative scale since gesture start (`> 1` zoom in, `< 1` zoom out).
321        scale: f32,
322    },
323    /// A two-finger rotation; `radians` is the signed angle delta.
324    Rotate {
325        /// Rotation delta in radians (positive = counter-clockwise).
326        radians: f32,
327    },
328    /// A swipe; `delta` is the translation vector of the swipe.
329    Swipe {
330        /// Swipe translation in logical pixels.
331        delta: Point,
332    },
333}
334
335/// A touch event from a single contact point, identified by a stable touch id.
336#[derive(Clone, Copy, Debug, PartialEq)]
337pub enum TouchEvent {
338    /// A finger touched down.
339    Start {
340        /// Stable identifier for this contact for its lifetime.
341        id: u64,
342        /// Contact position.
343        pos: Point,
344    },
345    /// A touched finger moved.
346    Move {
347        /// Contact identifier.
348        id: u64,
349        /// New position.
350        pos: Point,
351    },
352    /// A finger lifted off.
353    End {
354        /// Contact identifier.
355        id: u64,
356        /// Position at release.
357        pos: Point,
358    },
359    /// The contact was cancelled by the system (e.g. palm rejection).
360    Cancel {
361        /// Contact identifier.
362        id: u64,
363    },
364    /// A recognised gesture spanning one or more contacts.
365    Gesture {
366        /// The recognised gesture.
367        kind: GestureKind,
368        /// The gesture centroid.
369        center: Point,
370    },
371}
372
373impl TouchEvent {
374    /// The stable touch id, or `None` for centroid-only gesture events.
375    pub fn touch_id(&self) -> Option<u64> {
376        match self {
377            TouchEvent::Start { id, .. }
378            | TouchEvent::Move { id, .. }
379            | TouchEvent::End { id, .. }
380            | TouchEvent::Cancel { id } => Some(*id),
381            TouchEvent::Gesture { .. } => None,
382        }
383    }
384}
385
386/// Propagation control flags an event handler can set to influence dispatch.
387///
388/// Defaults are `false`/`false`: the event continues through the
389/// capture/bubble phases and the default action is allowed.
390#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
391pub struct Propagation {
392    /// Stop the event travelling to further nodes in the current and
393    /// subsequent phases (capture/bubble).
394    pub stop_propagation: bool,
395    /// Suppress the framework's default action for this event.
396    pub prevent_default: bool,
397}
398
399impl Propagation {
400    /// No control set — continue propagation, allow default.
401    pub const CONTINUE: Propagation = Propagation {
402        stop_propagation: false,
403        prevent_default: false,
404    };
405
406    /// A [`Propagation`] that stops further propagation.
407    pub const fn stop() -> Self {
408        Self {
409            stop_propagation: true,
410            prevent_default: false,
411        }
412    }
413
414    /// A [`Propagation`] that prevents the default action.
415    pub const fn prevent() -> Self {
416        Self {
417            stop_propagation: false,
418            prevent_default: true,
419        }
420    }
421
422    /// Merge two propagation results, OR-ing each flag (sticky once set).
423    pub fn merge(self, other: Propagation) -> Propagation {
424        Propagation {
425            stop_propagation: self.stop_propagation || other.stop_propagation,
426            prevent_default: self.prevent_default || other.prevent_default,
427        }
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    #[test]
436    fn modifiers_command() {
437        assert!(Modifiers::NONE.is_empty());
438        let ctrl = Modifiers {
439            ctrl: true,
440            ..Modifiers::NONE
441        };
442        assert!(ctrl.command());
443        let meta = Modifiers {
444            meta: true,
445            ..Modifiers::NONE
446        };
447        assert!(meta.command());
448        assert!(!Modifiers::NONE.command());
449    }
450
451    #[test]
452    fn key_as_text() {
453        assert_eq!(Key::Character("ä".into()).as_text(), Some("ä"));
454        assert_eq!(Key::Space.as_text(), Some(" "));
455        assert_eq!(Key::Enter.as_text(), None);
456        assert_eq!(Key::Function(5), Key::Function(5));
457    }
458
459    #[test]
460    fn mouse_button_eq() {
461        assert_eq!(MouseButton::Left, MouseButton::Left);
462        assert_ne!(MouseButton::Left, MouseButton::Right);
463        assert_eq!(MouseButton::Other(7), MouseButton::Other(7));
464    }
465
466    #[test]
467    fn mouse_event_position() {
468        let e = MouseEvent::Down {
469            pos: Point::new(3.0, 4.0),
470            button: MouseButton::Left,
471            modifiers: Modifiers::NONE,
472        };
473        assert_eq!(e.position(), Point::new(3.0, 4.0));
474        let drag = MouseEvent::DragMove {
475            pos: Point::new(9.0, 9.0),
476            delta: Point::new(1.0, 1.0),
477        };
478        assert_eq!(drag.position(), Point::new(9.0, 9.0));
479    }
480
481    #[test]
482    fn keyboard_event_repeat_and_char() {
483        let down = KeyboardEvent::Down {
484            key: Key::Character("a".into()),
485            physical: Some(PhysicalKey::new("KeyA")),
486            modifiers: Modifiers::NONE,
487            repeat: true,
488        };
489        assert!(down.is_repeat());
490        let ci = KeyboardEvent::CharInput { text: "x".into() };
491        assert!(!ci.is_repeat());
492        if let KeyboardEvent::Down {
493            physical: Some(p), ..
494        } = &down
495        {
496            assert_eq!(p.code(), "KeyA");
497        } else {
498            panic!("expected Down with physical key");
499        }
500    }
501
502    #[test]
503    fn touch_event_id_and_gesture() {
504        assert_eq!(
505            TouchEvent::Start {
506                id: 7,
507                pos: Point::ZERO
508            }
509            .touch_id(),
510            Some(7)
511        );
512        let g = TouchEvent::Gesture {
513            kind: GestureKind::Pinch { scale: 1.5 },
514            center: Point::new(10.0, 10.0),
515        };
516        assert_eq!(g.touch_id(), None);
517        assert_eq!(g, g);
518    }
519
520    #[test]
521    fn propagation_merge_is_sticky() {
522        let a = Propagation::stop();
523        let b = Propagation::prevent();
524        let m = a.merge(b);
525        assert!(m.stop_propagation);
526        assert!(m.prevent_default);
527        // CONTINUE merged with stop still stops.
528        assert!(
529            Propagation::CONTINUE
530                .merge(Propagation::stop())
531                .stop_propagation
532        );
533        assert_eq!(Propagation::default(), Propagation::CONTINUE);
534    }
535}