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}