Skip to main content

i_slint_core/
input.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4// cSpell: ignore altgr rpos Unapply
5/*! Module handling mouse events
6*/
7#![warn(missing_docs)]
8
9use crate::cursor::MouseCursorInner;
10use crate::item_tree::ItemTreeRc;
11use crate::item_tree::{ItemRc, ItemWeak, VisitChildrenResult};
12use crate::items::{
13    AllowedDragActions, BuiltInMouseCursor, DropEvent, ItemRef, OperatingSystemType,
14    TextCursorDirection,
15};
16pub use crate::items::{FocusReason, KeyEvent, KeyboardModifiers, PointerEventButton};
17use crate::lengths::{ItemTransform, LogicalPoint, LogicalVector};
18use crate::window::{WindowAdapter, WindowInner};
19use crate::{Coord, Property, SharedString};
20use alloc::rc::Rc;
21use alloc::vec::Vec;
22use const_field_offset::FieldOffsets;
23use core::cell::Cell;
24use core::fmt::Display;
25use core::pin::Pin;
26use core::time::Duration;
27
28/// A mouse or touch event
29///
30/// The only difference with [`crate::platform::WindowEvent`] is that it uses untyped `Point`
31/// TODO: merge with platform::WindowEvent
32#[repr(C)]
33#[derive(Debug, Clone, PartialEq)]
34pub enum MouseEvent {
35    /// The mouse or finger was pressed
36    Pressed {
37        /// The position of the pointer when the event happened.
38        position: LogicalPoint,
39        /// The button that was pressed.
40        button: PointerEventButton,
41        /// The current click count reported for this press.
42        click_count: u8,
43        /// The touch ID if the event originated from touch input.
44        touch_finger_id: i32,
45    },
46    /// The mouse or finger was released
47    Released {
48        /// The position of the pointer when the event happened.
49        position: LogicalPoint,
50        /// The button that was released.
51        button: PointerEventButton,
52        /// The current click count reported for this release.
53        click_count: u8,
54        /// The touch ID if the event originated from touch input.
55        touch_finger_id: i32,
56    },
57    /// The position of the pointer has changed
58    Moved {
59        /// The new position of the pointer.
60        position: LogicalPoint,
61        /// The touch ID if the event originated from touch input.
62        touch_finger_id: i32,
63    },
64    /// Wheel was operated.
65    Wheel {
66        /// The position of the pointer when the event happened.
67        position: LogicalPoint,
68        /// The horizontal scroll delta in logical pixels.
69        delta_x: Coord,
70        /// The vertical scroll delta in logical pixels.
71        delta_y: Coord,
72        /// The gesture phase reported for the wheel event.
73        phase: TouchPhase,
74    },
75    /// The mouse is being dragged over this item.
76    /// [`InputEventResult::EventIgnored`] means that the item does not handle the drag operation
77    /// and [`InputEventResult::EventAccepted`] means that the item can accept it.
78    DragMove {
79        /// The dragged payload and its current position/proposed action.
80        event: DropEvent,
81        /// The actions the drag source permits.
82        allowed: AllowedDragActions,
83    },
84    /// The mouse is released while dragging over this item.
85    Drop {
86        /// The dragged payload and its current position/proposed action.
87        event: DropEvent,
88        /// The actions the drag source permits.
89        allowed: AllowedDragActions,
90    },
91    /// A platform-recognized pinch gesture (macOS/iOS trackpad, Qt).
92    PinchGesture {
93        /// The focal position of the gesture.
94        position: LogicalPoint,
95        /// The incremental scale delta for this gesture update.
96        delta: f32,
97        /// The gesture phase reported by the platform.
98        phase: TouchPhase,
99    },
100    /// A platform-recognized rotation gesture (macOS/iOS trackpad, Qt).
101    RotationGesture {
102        /// The focal position of the gesture.
103        position: LogicalPoint,
104        /// The incremental rotation in degrees, where positive means clockwise.
105        delta: f32,
106        /// The gesture phase reported by the platform.
107        phase: TouchPhase,
108    },
109    /// The mouse exited the item or component
110    Exit,
111}
112
113impl MouseEvent {
114    /// The touch ID if the event originated from touch input.
115    pub fn touch_finger_id(&self) -> i32 {
116        match self {
117            MouseEvent::Pressed { touch_finger_id, .. } => *touch_finger_id,
118            MouseEvent::Released { touch_finger_id, .. } => *touch_finger_id,
119            MouseEvent::Moved { touch_finger_id, .. } => *touch_finger_id,
120            _ => 0,
121        }
122    }
123
124    /// Whether the event originates from a touch screen rather than a mouse.
125    pub fn is_from_touch(&self) -> bool {
126        // touch events carry the finger id + 1, events from a mouse carry 0
127        self.touch_finger_id() != 0
128    }
129
130    /// The position of the cursor for this event, if any
131    pub fn position(&self) -> Option<LogicalPoint> {
132        match self {
133            MouseEvent::Pressed { position, .. } => Some(*position),
134            MouseEvent::Released { position, .. } => Some(*position),
135            MouseEvent::Moved { position, .. } => Some(*position),
136            MouseEvent::Wheel { position, .. } => Some(*position),
137            MouseEvent::PinchGesture { position, .. } => Some(*position),
138            MouseEvent::RotationGesture { position, .. } => Some(*position),
139            MouseEvent::DragMove { event: e, .. } | MouseEvent::Drop { event: e, .. } => {
140                Some(crate::lengths::logical_point_from_api(e.position))
141            }
142            MouseEvent::Exit => None,
143        }
144    }
145
146    /// Translate the position by the given value
147    pub fn translate(&mut self, vec: LogicalVector) {
148        let pos = match self {
149            MouseEvent::Pressed { position, .. } => Some(position),
150            MouseEvent::Released { position, .. } => Some(position),
151            MouseEvent::Moved { position, .. } => Some(position),
152            MouseEvent::Wheel { position, .. } => Some(position),
153            MouseEvent::PinchGesture { position, .. } => Some(position),
154            MouseEvent::RotationGesture { position, .. } => Some(position),
155            MouseEvent::DragMove { event: e, .. } | MouseEvent::Drop { event: e, .. } => {
156                e.position = crate::api::LogicalPosition::from_euclid(
157                    crate::lengths::logical_point_from_api(e.position) + vec,
158                );
159                None
160            }
161            MouseEvent::Exit => None,
162        };
163        if let Some(pos) = pos {
164            *pos += vec;
165        }
166    }
167
168    /// Transform the position by the given item transform.
169    pub fn transform(&mut self, transform: ItemTransform) {
170        let pos = match self {
171            MouseEvent::Pressed { position, .. } => Some(position),
172            MouseEvent::Released { position, .. } => Some(position),
173            MouseEvent::Moved { position, .. } => Some(position),
174            MouseEvent::Wheel { position, .. } => Some(position),
175            MouseEvent::PinchGesture { position, .. } => Some(position),
176            MouseEvent::RotationGesture { position, .. } => Some(position),
177            MouseEvent::DragMove { event: e, .. } | MouseEvent::Drop { event: e, .. } => {
178                e.position = crate::api::LogicalPosition::from_euclid(
179                    transform
180                        .transform_point(crate::lengths::logical_point_from_api(e.position).cast())
181                        .cast(),
182                );
183                None
184            }
185            MouseEvent::Exit => None,
186        };
187        if let Some(pos) = pos {
188            *pos = transform.transform_point(pos.cast()).cast();
189        }
190    }
191
192    /// Set the click count of the pressed or released event
193    fn set_click_count(&mut self, count: u8) {
194        match self {
195            MouseEvent::Pressed { click_count, .. } | MouseEvent::Released { click_count, .. } => {
196                *click_count = count
197            }
198            _ => (),
199        }
200    }
201}
202
203/// The mouse events a backend can deliver to the runtime.
204#[allow(missing_docs)]
205#[repr(C)]
206#[derive(Debug, Clone, Copy, PartialEq)]
207pub enum BackendMouseEvent {
208    /// The mouse or finger was pressed
209    Pressed {
210        position: LogicalPoint,
211        button: PointerEventButton,
212        click_count: u8,
213        touch_finger_id: i32,
214    },
215    /// The mouse or finger was released
216    Released {
217        position: LogicalPoint,
218        button: PointerEventButton,
219        click_count: u8,
220        touch_finger_id: i32,
221    },
222    /// The position of the pointer has changed
223    Moved { position: LogicalPoint, touch_finger_id: i32 },
224    /// Wheel was operated.
225    Wheel { position: LogicalPoint, delta_x: Coord, delta_y: Coord, phase: TouchPhase },
226    /// A platform-recognized pinch gesture (macOS/iOS trackpad, Qt).
227    PinchGesture { position: LogicalPoint, delta: f32, phase: TouchPhase },
228    /// A platform-recognized rotation gesture (macOS/iOS trackpad, Qt).
229    RotationGesture { position: LogicalPoint, delta: f32, phase: TouchPhase },
230    /// The mouse exited the item or component
231    Exit,
232}
233
234impl From<BackendMouseEvent> for MouseEvent {
235    fn from(event: BackendMouseEvent) -> Self {
236        match event {
237            BackendMouseEvent::Pressed { position, button, click_count, touch_finger_id } => {
238                Self::Pressed { position, button, click_count, touch_finger_id }
239            }
240            BackendMouseEvent::Released { position, button, click_count, touch_finger_id } => {
241                Self::Released { position, button, click_count, touch_finger_id }
242            }
243            BackendMouseEvent::Moved { position, touch_finger_id } => {
244                Self::Moved { position, touch_finger_id }
245            }
246            BackendMouseEvent::Wheel { position, delta_x, delta_y, phase } => {
247                Self::Wheel { position, delta_x, delta_y, phase }
248            }
249            BackendMouseEvent::PinchGesture { position, delta, phase } => {
250                Self::PinchGesture { position, delta, phase }
251            }
252            BackendMouseEvent::RotationGesture { position, delta, phase } => {
253                Self::RotationGesture { position, delta, phase }
254            }
255            BackendMouseEvent::Exit => Self::Exit,
256        }
257    }
258}
259
260/// The drag and drop events a backend can deliver, through [`WindowInner::process_drag_event`].
261#[allow(missing_docs)]
262#[derive(Debug, Clone, PartialEq)]
263pub enum BackendDragEvent {
264    /// A drag is hovering over the window.
265    Move { event: DropEvent, allowed: AllowedDragActions },
266    /// A drag was released over the window.
267    Drop { event: DropEvent, allowed: AllowedDragActions },
268    /// A drag left the window, or was cancelled while hovering over it.
269    Leave,
270}
271
272impl From<BackendDragEvent> for MouseEvent {
273    fn from(event: BackendDragEvent) -> Self {
274        match event {
275            BackendDragEvent::Move { event, allowed } => Self::DragMove { event, allowed },
276            BackendDragEvent::Drop { event, allowed } => Self::Drop { event, allowed },
277            // A drag leaving tears down the hover state the same way the pointer leaving does.
278            BackendDragEvent::Leave => Self::Exit,
279        }
280    }
281}
282
283/// Phase of a touch, gesture event or wheel event.
284/// A touchpad is recognized as wheel event and therefore
285/// we need to find out when the touch event starts and ends
286#[repr(u8)]
287#[derive(Debug, Clone, Copy, PartialEq)]
288pub enum TouchPhase {
289    /// The gesture began (e.g., first finger touched or platform gesture started).
290    Started,
291    /// The gesture is ongoing (e.g., fingers moved or platform gesture updated).
292    Moved,
293    /// The gesture completed normally.
294    Ended,
295    /// The gesture was cancelled (e.g., interrupted by the system) or the mouse wheel was used
296    Cancelled,
297}
298
299/// This value is returned by the `input_event` function of an Item
300/// to notify the run-time about how the event was handled and
301/// what the next steps are.
302/// See [`crate::items::ItemVTable::input_event`].
303#[repr(u8)]
304#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
305pub enum InputEventResult {
306    /// The event was accepted. This may result in additional events, for example
307    /// accepting a mouse move will result in a MouseExit event later.
308    EventAccepted,
309    /// The event was ignored.
310    #[default]
311    EventIgnored,
312    /// All further mouse events need to be sent to this item or component
313    GrabMouse,
314    /// Will start a drag operation. Can only be returned from a [`crate::items::DragArea`] item.
315    StartDrag,
316}
317
318/// This value is returned by the `input_event_filter_before_children` function, which
319/// can specify how to further process the event.
320/// See [`crate::items::ItemVTable::input_event_filter_before_children`].
321#[repr(C)]
322#[derive(Debug, Copy, Clone, PartialEq, Default)]
323pub enum InputEventFilterResult {
324    /// The event is going to be forwarded to children, then the [`crate::items::ItemVTable::input_event`]
325    /// function is called
326    #[default]
327    ForwardEvent,
328    /// The event will be forwarded to the children, but the [`crate::items::ItemVTable::input_event`] is not
329    /// going to be called for this item
330    ForwardAndIgnore,
331    /// Just like `ForwardEvent`, but even in the case that children grabs the mouse, this function
332    /// will still be called for further events
333    ForwardAndInterceptGrab,
334    /// The event will not be forwarded to children, if a child already had the grab, the
335    /// grab will be cancelled with a [`MouseEvent::Exit`] event
336    Intercept,
337    /// The event will be forwarded to the children with a delay (in milliseconds), unless it is
338    /// being intercepted.
339    /// This is what happens when the flickable wants to delay the event.
340    /// This should only be used for Press event, and the event will be sent after the delay, or
341    /// if a release event is seen before that delay
342    /// If any other component is handling the event it will be not handled by the component returned this result
343    //(Can't use core::time::Duration because it is not repr(c))
344    DelayForwarding(u64),
345    /// Like `ForwardAndIgnore`, but the item still receives a [`MouseEvent::Exit`]
346    /// when the pointer leaves, even if a sibling handles the event in between.
347    ForwardAndObserve,
348}
349
350/// This module contains the constant character code used to represent the keys.
351#[allow(missing_docs, non_upper_case_globals)]
352pub mod key_codes {
353    macro_rules! declare_consts_for_special_keys {
354       ($($char:literal # $name:ident # $($shifted:ident)? $(=> $($_muda:ident)? # $($_qt:ident)|* # $($_winit:ident $(($_pos:ident))?)|*    # $($_xkb:ident)|* )? ;)*) => {
355            $(pub const $name : char = $char;)*
356
357            #[allow(missing_docs)]
358            #[derive(Debug, Copy, Clone, PartialEq)]
359            #[non_exhaustive]
360            /// The `Key` enum is used to map a specific key by name e.g. `Key::Control` to an
361            /// internal used unicode representation. The enum is convertible to [`std::char`] and [`slint::SharedString`](`crate::SharedString`).
362            /// Use this with [`slint::platform::WindowEvent`](`crate::platform::WindowEvent`) to supply key events to Slint's platform abstraction.
363            ///
364            /// # Example
365            ///
366            /// Send an tab key press event to a window
367            ///
368            /// ```
369            /// use slint::platform::{WindowEvent, Key};
370            /// fn send_tab_pressed(window: &slint::Window) {
371            ///     window.dispatch_event(WindowEvent::KeyPressed { text: Key::Tab.into() });
372            /// }
373            /// ```
374            pub enum Key {
375                $($name,)*
376            }
377
378            impl From<Key> for char {
379                fn from(k: Key) -> Self {
380                    match k {
381                        $(Key::$name => $name,)*
382                    }
383                }
384            }
385
386            impl From<Key> for crate::SharedString {
387                fn from(k: Key) -> Self {
388                    char::from(k).into()
389                }
390            }
391        };
392    }
393
394    i_slint_common::for_each_keys!(declare_consts_for_special_keys);
395}
396
397/// Internal struct to maintain the pressed/released state of the keys that
398/// map to keyboard modifiers.
399#[derive(Clone, Copy, Default, Debug)]
400pub(crate) struct InternalKeyboardModifierState {
401    left_alt: bool,
402    right_alt: bool,
403    altgr: bool,
404    left_control: bool,
405    right_control: bool,
406    left_meta: bool,
407    right_meta: bool,
408    left_shift: bool,
409    right_shift: bool,
410}
411
412impl InternalKeyboardModifierState {
413    /// Updates a flag of the modifiers if the key of the given text is pressed.
414    /// Returns an updated modifier if detected; None otherwise;
415    pub(crate) fn state_update(mut self, pressed: bool, text: &SharedString) -> Option<Self> {
416        if let Some(key_code) = text.chars().next() {
417            match key_code {
418                key_codes::Alt => self.left_alt = pressed,
419                key_codes::AltGr => self.altgr = pressed,
420                key_codes::Control => self.left_control = pressed,
421                key_codes::ControlR => self.right_control = pressed,
422                key_codes::Shift => self.left_shift = pressed,
423                key_codes::ShiftR => self.right_shift = pressed,
424                key_codes::Meta => self.left_meta = pressed,
425                key_codes::MetaR => self.right_meta = pressed,
426                _ => return None,
427            };
428
429            // Encoded keyboard modifiers must appear as individual key events. This could
430            // be relaxed by implementing a string split, but right now WindowEvent::KeyPressed
431            // holds only a single char.
432            debug_assert_eq!(key_code.len_utf8(), text.len());
433        }
434
435        Some(self)
436    }
437
438    pub fn shift(&self) -> bool {
439        self.right_shift || self.left_shift
440    }
441    pub fn alt(&self) -> bool {
442        self.right_alt || self.left_alt
443    }
444    pub fn meta(&self) -> bool {
445        self.right_meta || self.left_meta
446    }
447    pub fn control(&self) -> bool {
448        self.right_control || self.left_control
449    }
450
451    pub fn modifiers_for(&self, _event: &InternalKeyEvent) -> KeyboardModifiers {
452        #[allow(unused_mut)]
453        let mut alt = self.alt();
454        #[allow(unused_mut)]
455        let mut control = self.control();
456
457        // Windows treats Ctrl+Alt as implying AltGr, but not vice-versa
458        // Unfortunately, our different backends produce different key combinations here.
459        //
460        // ## Qt
461        // Qt always sends Ctrl + Alt instead of AltGr, and does not tell us whether this
462        // was interpreted as AltGr or not. So with Qt we have no way of telling whether
463        // AltGr is pressed, and we have to assume that it is pressed whenever Ctrl + Alt is pressed.
464        // In that case the `text_without_modifiers` is also not set.
465        //
466        // ## Winit
467        // Winit sends the actual Ctrl/Alt/AltGr keypress correctly.
468        // With winit we can detect whether ctrl+alt actually caused a AltGr conversion or not,
469        // by checking whether the text_without_modifiers is different from the event text.
470        //
471        // ## Wasm
472        // Winit on the web for some reasons sends first a Ctrl and then AltGr event when only AltGr
473        // is pressed.
474        // So there we need to get rid of the additional Ctrl event whenever AltGr is pressed.
475        #[cfg(target_os = "windows")]
476        {
477            // Non-web windows (Usually winit or Qt)
478            if !self.altgr && self.control() && self.alt() {
479                // AltGr is not pressed, but Ctrl+Alt is pressed.
480                // Try to detect if an AltGr conversion occurred.
481                // If so, disable Ctrl and Alt
482                //
483                // On platforms that don't provide text_without_modifiers, fall back to a simple
484                // heuristic that assumes A-Z & 0-9 are not produced with AltGr, but all other keys are.
485                let implies_altgr = if _event.text_without_modifiers.is_empty() {
486                    _event.key_event.text.chars().any(|c| !c.is_ascii_alphanumeric())
487                } else {
488                    _event.text_without_modifiers.to_lowercase()
489                        != _event.key_event.text.to_lowercase()
490                };
491                if implies_altgr {
492                    alt = false;
493                    control = false;
494                }
495            }
496        }
497        #[cfg(target_family = "wasm")]
498        if crate::detect_operating_system() == OperatingSystemType::Windows {
499            // Non-native windows (e.g. Winit on the web)
500            // This currently injects additional Ctrl events, so remove those if AltGr is
501            // pressed.
502            let is_altgr = self.altgr
503                || (self.control()
504                    && self.alt()
505                    && _event.key_event.text.chars().any(|c| !c.is_ascii_alphanumeric()));
506            if is_altgr {
507                alt = false;
508                control = false;
509            }
510        }
511
512        KeyboardModifiers { alt, control, meta: self.meta(), shift: self.shift() }
513    }
514}
515
516impl From<InternalKeyboardModifierState> for KeyboardModifiers {
517    fn from(internal_state: InternalKeyboardModifierState) -> Self {
518        Self {
519            alt: internal_state.alt(),
520            control: internal_state.control(),
521            meta: internal_state.meta(),
522            shift: internal_state.shift(),
523        }
524    }
525}
526
527#[i_slint_core_macros::slint_doc]
528/// The `Keys` type is the Rust representation of Slint's `keys` primitive type.
529///
530/// It can be created with the `@keys` macro in Slint and defines which key event(s) activate a KeyBinding.
531///
532/// See also the Slint documentation on [Key Bindings](slint:KeyBindingOverview).
533///
534/// In `.slint` files, `Keys` values are typically created via the `@keys(...)` macro.
535/// From backend code, they can be created from a list of string parts with the similar
536/// syntax as the macro:
537///
538/// ```rust
539/// use slint::Keys;
540///
541/// let save = Keys::from_parts(["Control", "S"])?;
542/// let undo = Keys::from_parts(["Control", "Shift?", "Z"])?;
543/// let f5 = Keys::from_parts(["F5"])?;
544/// let zoom_in = Keys::from_parts(["Control", "Plus"])?;
545/// let euro = Keys::from_parts(["Control", "€"])?;
546/// let empty = Keys::from_parts([])?;  // same as Keys::default()
547/// # Ok::<(), i_slint_core::input::KeysParseError>(())
548/// ```
549/// ## Parts format
550///
551/// Each element is either a modifier or a key (case-sensitive, matching the `@keys` macro):
552/// - **Modifiers** (optional): `Control`, `Alt`, `Shift`, `Meta`
553/// - **Optional modifiers**: `Shift?`, `Alt?` (match regardless of that modifier's state)
554/// - **Named key** (required, exactly one): A named key (`Return`, `Tab`, `F1`, `Plus`, `Space`, `A`–`Z`, etc.)
555/// - **String literal fallback**: If no named key matches, the part is treated as a string
556///   literal — it must be a single lowercase grapheme cluster (e.g., `"€"`, `"é"`)
557///
558/// Keys with layout-dependent shifted variants (digits `Digit0`–`Digit9`, symbols like
559/// `Plus`, `Comma`, etc.) automatically get `Shift?` behavior, just like the `@keys` macro.
560#[derive(Clone, Eq, PartialEq, Default)]
561#[repr(C)]
562pub struct Keys {
563    inner: KeysInner,
564}
565
566/// Internal representation of key-parse errors. Variants are not part of the public API.
567#[derive(Debug, Clone, PartialEq, Eq)]
568enum KeysParseErrorInner {
569    /// No key was found (only modifiers were specified).
570    NoKey,
571    /// More than one non-modifier key was found.
572    MultipleKeys,
573    /// A string literal contains more than one grapheme cluster.
574    /// The contained string is the offending key part (e.g. `"ab"` or `"return"`).
575    MultipleGraphemeClusters(SharedString),
576    /// A string literal is not lowercase.
577    /// The contained string is the offending key part (e.g. `"É"`).
578    NotLowercase(SharedString),
579    /// Incompatible modifiers were specified (e.g. both `Shift` and `Shift?`).
580    /// The contained string is a human-readable description of the conflict.
581    IncompatibleModifiers(SharedString),
582}
583
584/// Error type returned when constructing a [`Keys`] from string parts.
585///
586/// This is an opaque error type. Use its [`Display`] implementation
587/// to obtain a human-readable description of the problem.
588#[derive(Debug, Clone, PartialEq, Eq)]
589pub struct KeysParseError(KeysParseErrorInner);
590
591impl core::fmt::Display for KeysParseError {
592    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
593        match &self.0 {
594            KeysParseErrorInner::NoKey => write!(f, "no key found (only modifiers)"),
595            KeysParseErrorInner::MultipleKeys => {
596                write!(f, "multiple non-modifier keys found")
597            }
598            KeysParseErrorInner::MultipleGraphemeClusters(s) => {
599                write!(f, "key string must be a single grapheme cluster, got: {s}")
600            }
601            KeysParseErrorInner::NotLowercase(s) => {
602                let lower = s.to_lowercase();
603                write!(f, "key string must be lowercase, use \"{lower}\" instead")
604            }
605            KeysParseErrorInner::IncompatibleModifiers(msg) => write!(f, "{msg}"),
606        }
607    }
608}
609
610impl core::error::Error for KeysParseError {}
611
612use i_slint_common::key_codes::{ShiftBehavior, lookup_key_name};
613
614/// Re-exported in private_unstable_api to create a Keys struct.
615pub fn make_keys(
616    key: SharedString,
617    modifiers: KeyboardModifiers,
618    ignore_shift: bool,
619    ignore_alt: bool,
620) -> Keys {
621    Keys {
622        inner: KeysInner { key: key.to_lowercase().into(), modifiers, ignore_shift, ignore_alt },
623    }
624}
625
626#[cfg(feature = "ffi")]
627#[allow(unsafe_code)]
628pub(crate) mod ffi {
629    use crate::api::ToSharedString as _;
630
631    use super::*;
632
633    #[unsafe(no_mangle)]
634    pub unsafe extern "C" fn slint_keys(
635        key: &SharedString,
636        alt: bool,
637        control: bool,
638        shift: bool,
639        meta: bool,
640        ignore_shift: bool,
641        ignore_alt: bool,
642        out: &mut Keys,
643    ) {
644        *out = make_keys(
645            key.clone(),
646            KeyboardModifiers { alt, control, shift, meta },
647            ignore_shift,
648            ignore_alt,
649        );
650    }
651
652    #[unsafe(no_mangle)]
653    pub unsafe extern "C" fn slint_keys_debug_string(shortcut: &Keys, out: &mut SharedString) {
654        *out = crate::format!("{shortcut:?}");
655    }
656
657    #[unsafe(no_mangle)]
658    pub unsafe extern "C" fn slint_keys_to_string(shortcut: &Keys, out: &mut SharedString) {
659        *out = shortcut.to_shared_string();
660    }
661
662    #[unsafe(no_mangle)]
663    pub unsafe extern "C" fn slint_keys_from_parts(
664        parts: crate::slice::Slice<'_, SharedString>,
665        out: &mut Keys,
666    ) -> bool {
667        match keys_from_parts(parts.as_slice().iter().map(|s| s.as_str())) {
668            Ok(keys) => {
669                *out = keys;
670                true
671            }
672            Err(_) => false,
673        }
674    }
675
676    #[unsafe(no_mangle)]
677    pub unsafe extern "C" fn slint_keys_to_parts(
678        keys: &Keys,
679        out: &mut crate::SharedVector<SharedString>,
680    ) {
681        *out = keys.to_parts().map(SharedString::from).collect();
682    }
683}
684
685/// Normalize a key string: lowercase and NFC-normalize.
686fn normalize_key(key: &str) -> SharedString {
687    let lowered = key.to_lowercase();
688    cfg_if::cfg_if! {
689        if #[cfg(feature = "shared-parley")] {
690            let normalizer = icu_normalizer::ComposingNormalizer::new_nfc();
691            let normalized = normalizer.normalize(&lowered);
692            SharedString::from(normalized.as_ref())
693        } else {
694            SharedString::from(lowered.as_str())
695        }
696    }
697}
698
699fn keys_from_parts<'a>(parts: impl Iterator<Item = &'a str>) -> Result<Keys, KeysParseError> {
700    keys_from_parts_inner(parts).map_err(KeysParseError)
701}
702
703fn keys_from_parts_inner<'a>(
704    parts: impl Iterator<Item = &'a str>,
705) -> Result<Keys, KeysParseErrorInner> {
706    use unicode_segmentation::UnicodeSegmentation;
707
708    let mut modifiers = KeyboardModifiers::default();
709    let mut ignore_shift = false;
710    let mut ignore_alt = false;
711    let mut key_part: Option<&str> = None;
712
713    for part in parts {
714        // Parts are *not* trimmed: whitespace is significant, so `" "`, `"\t"` and
715        // `"\n"` are valid literal spellings of the Space, Tab and Return keys, the
716        // same way `@keys(" ")` is valid in Slint. Trimming would silently swallow
717        // them and make those keys unreachable through a literal. Empty parts carry
718        // no information and are skipped, which keeps `from_parts([""])` equivalent
719        // to `from_parts([])`.
720        if part.is_empty() {
721            continue;
722        }
723        match part {
724            "Control" => modifiers.control = true,
725            "Alt" => {
726                if ignore_alt {
727                    return Err(KeysParseErrorInner::IncompatibleModifiers(
728                        "Alt and Alt? cannot be combined".into(),
729                    ));
730                }
731                modifiers.alt = true;
732            }
733            "Shift" => {
734                if ignore_shift {
735                    return Err(KeysParseErrorInner::IncompatibleModifiers(
736                        "Shift and Shift? cannot be combined".into(),
737                    ));
738                }
739                modifiers.shift = true;
740            }
741            "Meta" => modifiers.meta = true,
742            "Shift?" => {
743                if modifiers.shift {
744                    return Err(KeysParseErrorInner::IncompatibleModifiers(
745                        "Shift and Shift? cannot be combined".into(),
746                    ));
747                }
748                ignore_shift = true;
749            }
750            "Alt?" => {
751                if modifiers.alt {
752                    return Err(KeysParseErrorInner::IncompatibleModifiers(
753                        "Alt and Alt? cannot be combined".into(),
754                    ));
755                }
756                ignore_alt = true;
757            }
758            _ => {
759                if key_part.is_some() {
760                    return Err(KeysParseErrorInner::MultipleKeys);
761                }
762                key_part = Some(part);
763            }
764        }
765    }
766
767    let key_name = match key_part {
768        Some(k) => k,
769        None if modifiers == KeyboardModifiers::default() && !ignore_shift && !ignore_alt => {
770            // Empty input (or only empty parts) → Keys::default(), same as @keys()
771            return Ok(Keys::default());
772        }
773        None => return Err(KeysParseErrorInner::NoKey),
774    };
775
776    // First: try named-key lookup (case-sensitive, like the @keys macro)
777    if let Some((key_char, shift_behavior)) = lookup_key_name(key_name) {
778        // Auto-set ignore_shift for keys with localized shifted variants
779        if matches!(shift_behavior, ShiftBehavior::LocalizedShiftable { .. }) {
780            if modifiers.shift {
781                return Err(KeysParseErrorInner::IncompatibleModifiers(
782                    alloc::format!(
783                        "Key bindings involving {key_name} ignore Shift to support different keyboard layouts; remove Shift"
784                    ).into(),
785                ));
786            }
787            ignore_shift = true;
788        }
789        // Key code literals in key_codes.rs are already NFC-normalized, just lowercase.
790        let key: SharedString = key_char.to_lowercase().collect::<alloc::string::String>().into();
791        return Ok(Keys { inner: KeysInner { key, modifiers, ignore_shift, ignore_alt } });
792    }
793
794    // Fallback: treat as a string literal (like @keys("€"))
795    // Must be a single grapheme cluster
796    let grapheme_count = key_name.graphemes(true).count();
797    if grapheme_count > 1 {
798        return Err(KeysParseErrorInner::MultipleGraphemeClusters(key_name.into()));
799    }
800
801    // Must be lowercase
802    let lowered = key_name.to_lowercase();
803    if lowered != key_name {
804        return Err(KeysParseErrorInner::NotLowercase(key_name.into()));
805    }
806
807    let key = normalize_key(key_name);
808    Ok(Keys { inner: KeysInner { key, modifiers, ignore_shift, ignore_alt } })
809}
810
811/// Internal representation of the `Keys` type.
812/// This is semver exempt and is only used to set up the native menu in the backends.
813#[derive(PartialEq, Eq, Clone, Default)]
814#[repr(C)]
815pub struct KeysInner {
816    /// The `key` used to trigger the shortcut
817    ///
818    /// Note: This is currently converted to lowercase when the shortcut is created!
819    pub key: SharedString,
820    /// `KeyboardModifier`s that need to be pressed for the shortcut to fire
821    pub modifiers: KeyboardModifiers,
822    /// Whether to ignore shift state when matching the shortcut
823    pub ignore_shift: bool,
824    /// Whether to ignore alt state when matching the shortcut
825    pub ignore_alt: bool,
826}
827
828impl KeysInner {
829    /// Private access to the KeysInner for a given Keys value.
830    pub fn from_pub(keys: &Keys) -> &Self {
831        &keys.inner
832    }
833}
834
835impl Keys {
836    #[i_slint_core_macros::slint_doc]
837    /// Create a `Keys` from an iterator of string parts (matching `@keys` macro syntax).
838    ///
839    /// Each element is either a modifier (`Control`, `Shift`, `Alt`, `Meta`, `Shift?`, `Alt?`)
840    /// or a key. Keys are first looked up by name (case-sensitive) in the Key namespace;
841    /// if not found, treated as a string literal (must be a single lowercase grapheme cluster).
842    /// Exactly one non-modifier key must be present.
843    ///
844    /// Parts are taken verbatim — they are not trimmed — so whitespace is significant:
845    /// `" "`, `"\t"` and `"\n"` are literal spellings of the `Space`, `Tab` and `Return`
846    /// keys, just as `@keys(" ")` is valid in Slint. A part must therefore match a
847    /// modifier or key exactly; `" Control "` is not the `Control` modifier.
848    ///
849    /// An empty iterator returns `Keys::default()` (same as `@keys()`). Empty parts are
850    /// skipped, so `from_parts([""])` is also `Keys::default()`.
851    ///
852    /// See also the Slint documentation on [Key Bindings](slint:KeyBindingOverview).
853    ///
854    /// Note: This currently only supports a **single shortcut** (one key + modifiers).
855    pub fn from_parts<'a>(
856        parts: impl IntoIterator<Item = &'a str>,
857    ) -> Result<Keys, KeysParseError> {
858        keys_from_parts(parts.into_iter())
859    }
860
861    #[i_slint_core_macros::slint_doc]
862    /// Decompose this `Keys` value into the string parts that
863    /// [`Keys::from_parts`] accepts.
864    ///
865    /// See also the Slint documentation on [Key Bindings](slint:KeyBindingOverview).
866    ///
867    /// A `Keys` value that is converted into parts and then re-created from those parts
868    /// with [`Keys::from_parts`] will be equal to the input `Keys` value:
869    ///
870    /// ```
871    /// use slint::Keys;
872    /// let k = Keys::from_parts(["Control", "Shift?", "Z"])?;
873    /// let k_from_parts = Keys::from_parts(k.to_parts())?;
874    /// assert_eq!(k_from_parts, k);
875    /// # Ok::<(), i_slint_core::input::KeysParseError>(())
876    /// ```
877    ///
878    /// Note that while a round-trip guarantees that the resulting `Keys` instances will
879    /// be equal, the parts returned by `to_parts` can be different from the parts used
880    /// to construct the `Keys` instance with `from_parts`.
881    ///
882    /// A part is not necessarily printable, so a text format storing parts has to quote
883    /// or escape them. The
884    /// [`runtime_key_bindings`](https://github.com/slint-ui/slint/tree/master/examples/runtime_key_bindings)
885    /// example shows one way to persist a user-configured shortcut and restore it.
886    ///
887    /// An empty `Keys` (i.e. [`Keys::default()`]) returns an empty iterator.
888    pub fn to_parts(&self) -> impl Iterator<Item = &str> {
889        let inner = &self.inner;
890        let has_key = !inner.key.is_empty();
891        // Order matches the `@keys` macro / Debug impl: Meta, Control, Alt, Shift.
892        //
893        // The key itself is always emitted as the stored character, never as the
894        // name it may have been created from. Names are not reversed back: a
895        // `LocalizedShiftable` name auto-applies `ignore_shift` on re-parse, so
896        // emitting one would break the round-trip of a literal such as
897        // `["Control", "+"]` (which has `ignore_shift = false`). Emitting the raw
898        // character lets `ignore_shift` be carried explicitly by `Shift?`, so
899        // `@keys(Control + Plus)` comes back as `["Control", "Shift?", "+"]`.
900        [
901            (has_key && inner.modifiers.meta).then_some("Meta"),
902            (has_key && inner.modifiers.control).then_some("Control"),
903            (has_key && inner.modifiers.alt).then_some("Alt"),
904            (has_key && !inner.modifiers.alt && inner.ignore_alt).then_some("Alt?"),
905            (has_key && inner.modifiers.shift).then_some("Shift"),
906            (has_key && !inner.modifiers.shift && inner.ignore_shift).then_some("Shift?"),
907            has_key.then(|| inner.key.as_str()),
908        ]
909        .into_iter()
910        .flatten()
911    }
912
913    /// Check whether a `Keys` can be triggered by the given `KeyEvent`
914    pub(crate) fn matches(&self, key_event: &KeyEvent) -> bool {
915        let inner = &self.inner;
916        // An empty Keys is never triggered, even if the modifiers match.
917        if inner.key.is_empty() {
918            return false;
919        }
920
921        // TODO: Should this check the event_type and only match on KeyReleased?
922        let mut expected_modifiers = inner.modifiers;
923        if inner.ignore_shift {
924            expected_modifiers.shift = key_event.modifiers.shift;
925        }
926        if inner.ignore_alt {
927            expected_modifiers.alt = key_event.modifiers.alt;
928        }
929        // Note: The shortcut's key is already in lowercase and NFC-normalized
930        // (by the compiler and backends respectively), so we only need to
931        // lowercase the event text. Backends are expected to NFC-normalize
932        // key event text before dispatching.
933        //
934        // This improves our handling of CapsLock and Shift, as the event text will be in uppercase
935        // if caps lock is active, even if shift is not pressed.
936        let event_text = key_event.text.chars().flat_map(|character| character.to_lowercase());
937
938        event_text.eq(inner.key.chars()) && key_event.modifiers == expected_modifiers
939    }
940
941    fn format_key_for_display(&self) -> crate::SharedString {
942        let key_str = self.inner.key.as_str();
943        let first_char = key_str.chars().next();
944
945        if let Some(first_char) = first_char {
946            macro_rules! check_special_key {
947                ($($char:literal # $name:ident # $($shifted:ident)? $(=> $($_muda:ident)? # $($qt:ident)|* # $($winit:ident $(($_pos:ident))?)|* # $($xkb:ident)|*)? ;)*) => {
948                    match first_char {
949                    $($(
950                        // Use $qt as a marker - if it exists, generate the check
951                        $char => {
952                            let _ = stringify!($($qt)|*); // Use $qt to enable this branch
953                            return stringify!($name).into();
954                        }
955                    )?)*
956                        _ => ()
957                    }
958                };
959            }
960            i_slint_common::for_each_keys!(check_special_key);
961        }
962
963        if key_str.chars().count() == 1 {
964            return key_str.to_uppercase().into();
965        }
966
967        key_str.into()
968    }
969}
970
971impl Display for Keys {
972    /// Converts the [`Keys`] to a string that looks native on the current platform.
973    ///
974    /// For example, the shortcut created with `@keys(Meta + Control + A)`
975    /// will be converted like this:
976    /// - **macOS**: `⌃⌘A`
977    /// - **Windows**: `Win+Ctrl+A`
978    /// - **Linux**: `Super+Ctrl+A`
979    ///
980    /// Note that this functions output is best-effort and may be adjusted/improved at any time,
981    /// do not rely on this output to be stable!
982    //
983    // References for implementation
984    // - macOS: <https://developer.apple.com/design/human-interface-guidelines/keyboards>
985    // - Windows: <https://learn.microsoft.com/en-us/windows/apps/design/input/keyboard-accelerators>
986    // - Linux: <https://developer.gnome.org/hig/guidelines/keyboard.html>
987    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
988        let inner = &self.inner;
989        if inner.key.is_empty() {
990            return Ok(());
991        }
992
993        if crate::is_apple_platform() {
994            // Slint remaps modifiers on macOS: control → Command, meta → Control
995            // From Apple's documentation:
996            //
997            // List modifier keys in the correct order.
998            // If you use more than one modifier key in a custom shortcut, always list them in this order:
999            //  Control, Option, Shift, Command
1000            if inner.modifiers.meta {
1001                f.write_str("⌃")?;
1002            }
1003            if !inner.ignore_alt && inner.modifiers.alt {
1004                f.write_str("⌥")?;
1005            }
1006            if !inner.ignore_shift && inner.modifiers.shift {
1007                f.write_str("⇧")?;
1008            }
1009            if inner.modifiers.control {
1010                f.write_str("⌘")?;
1011            }
1012        } else {
1013            let separator = "+";
1014
1015            // TODO: These should probably be translated, but better to have at least
1016            // platform-local names than nothing.
1017            let (ctrl_str, alt_str, shift_str, meta_str) =
1018                if crate::detect_operating_system() == OperatingSystemType::Windows {
1019                    ("Ctrl", "Alt", "Shift", "Win")
1020                } else {
1021                    ("Ctrl", "Alt", "Shift", "Super")
1022                };
1023
1024            if inner.modifiers.meta {
1025                f.write_str(meta_str)?;
1026                f.write_str(separator)?;
1027            }
1028            if inner.modifiers.control {
1029                f.write_str(ctrl_str)?;
1030                f.write_str(separator)?;
1031            }
1032            if !inner.ignore_alt && inner.modifiers.alt {
1033                f.write_str(alt_str)?;
1034                f.write_str(separator)?;
1035            }
1036            if !inner.ignore_shift && inner.modifiers.shift {
1037                f.write_str(shift_str)?;
1038                f.write_str(separator)?;
1039            }
1040        }
1041        f.write_str(&self.format_key_for_display())
1042    }
1043}
1044
1045impl core::fmt::Debug for Keys {
1046    /// Formats the keyboard shortcut so that the output would be accepted by the @keys macro in Slint.
1047    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1048        let inner = &self.inner;
1049        // Make sure to keep this in sync with the implementation in compiler/langtype.rs
1050        if inner.key.is_empty() {
1051            write!(f, "")
1052        } else {
1053            let alt = inner
1054                .ignore_alt
1055                .then_some("Alt?+")
1056                .or(inner.modifiers.alt.then_some("Alt+"))
1057                .unwrap_or_default();
1058            let ctrl = if inner.modifiers.control { "Control+" } else { "" };
1059            let meta = if inner.modifiers.meta { "Meta+" } else { "" };
1060            let shift = inner
1061                .ignore_shift
1062                .then_some("Shift?+")
1063                .or(inner.modifiers.shift.then_some("Shift+"))
1064                .unwrap_or_default();
1065            let keycode: SharedString = inner
1066                .key
1067                .chars()
1068                .flat_map(|character| {
1069                    let mut escaped = alloc::vec![];
1070                    if character.is_control() {
1071                        escaped.extend(character.escape_unicode());
1072                    } else {
1073                        escaped.push(character);
1074                    }
1075                    escaped
1076                })
1077                .collect();
1078            write!(f, "{meta}{ctrl}{alt}{shift}\"{keycode}\"")
1079        }
1080    }
1081}
1082
1083/// This enum defines the different kinds of key events that can happen.
1084#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
1085#[repr(u8)]
1086pub enum KeyEventType {
1087    /// A key on a keyboard was pressed.
1088    #[default]
1089    KeyPressed = 0,
1090    /// A key on a keyboard was released.
1091    KeyReleased = 1,
1092    /// The input method updates the currently composed text. The KeyEvent's text field is the pre-edit text and
1093    /// composition_selection specifies the placement of the cursor within the pre-edit text.
1094    UpdateComposition = 2,
1095    /// The input method replaces the currently composed text with the final result of the composition.
1096    CommitComposition = 3,
1097}
1098
1099#[derive(Default, Debug, Clone, PartialEq)]
1100/// This struct is used to pass key events to the runtime.
1101pub struct InternalKeyEvent {
1102    /// That's the public type with only public fields
1103    pub key_event: KeyEvent,
1104    /// Indicates whether the key was pressed or released
1105    pub event_type: KeyEventType,
1106    /// The key without any modifiers held
1107    /// Important on Windows, to distinguish between key presses when Ctrl+Alt was pressed
1108    /// vs. AltGr.
1109    /// This is optional, and we will fall back to a heuristic for Ctrl+Alt on Windows if this
1110    /// isn't provided.
1111    #[cfg(target_os = "windows")]
1112    pub text_without_modifiers: SharedString,
1113    /// If the event type is KeyEventType::UpdateComposition or KeyEventType::CommitComposition,
1114    /// then this field specifies what part of the current text to replace.
1115    /// Relative to the offset of the pre-edit text within the text input element's text.
1116    pub replacement_range: Option<core::ops::Range<i32>>,
1117    /// If the event type is KeyEventType::UpdateComposition, this is the new pre-edit text
1118    pub preedit_text: SharedString,
1119    /// The selection within the preedit_text
1120    pub preedit_selection: Option<core::ops::Range<i32>>,
1121    /// The new cursor position, when None, the cursor is put after the text that was just inserted
1122    pub cursor_position: Option<i32>,
1123    /// The anchor position, when None, the cursor is put after the text that was just inserted
1124    pub anchor_position: Option<i32>,
1125}
1126
1127impl InternalKeyEvent {
1128    /// If a shortcut was pressed, this function returns `Some(StandardShortcut)`.
1129    /// Otherwise it returns None.
1130    pub fn shortcut(&self) -> Option<StandardShortcut> {
1131        if self.key_event.modifiers.control && !self.key_event.modifiers.shift {
1132            match self.key_event.text.as_str() {
1133                #[cfg(not(target_arch = "wasm32"))]
1134                "c" => Some(StandardShortcut::Copy),
1135                #[cfg(not(target_arch = "wasm32"))]
1136                "x" => Some(StandardShortcut::Cut),
1137                #[cfg(not(target_arch = "wasm32"))]
1138                "v" => Some(StandardShortcut::Paste),
1139                "a" => Some(StandardShortcut::SelectAll),
1140                "f" => Some(StandardShortcut::Find),
1141                "s" => Some(StandardShortcut::Save),
1142                "p" => Some(StandardShortcut::Print),
1143                "z" => Some(StandardShortcut::Undo),
1144                #[cfg(target_os = "windows")]
1145                "y" => Some(StandardShortcut::Redo),
1146                "r" => Some(StandardShortcut::Refresh),
1147                _ => None,
1148            }
1149        } else if self.key_event.modifiers.control && self.key_event.modifiers.shift {
1150            match self.key_event.text.as_str() {
1151                #[cfg(not(target_os = "windows"))]
1152                "z" | "Z" => Some(StandardShortcut::Redo),
1153                _ => None,
1154            }
1155        } else {
1156            None
1157        }
1158    }
1159
1160    /// If a shortcut concerning text editing was pressed, this function
1161    /// returns `Some(TextShortcut)`. Otherwise it returns None.
1162    pub fn text_shortcut(&self) -> Option<TextShortcut> {
1163        let ke = &self.key_event;
1164        let keycode = ke.text.chars().next()?;
1165
1166        let is_apple = crate::is_apple_platform();
1167
1168        let move_mod = if is_apple {
1169            ke.modifiers.alt && !ke.modifiers.control && !ke.modifiers.meta
1170        } else {
1171            ke.modifiers.control && !ke.modifiers.alt && !ke.modifiers.meta
1172        };
1173
1174        if move_mod {
1175            match keycode {
1176                key_codes::LeftArrow => {
1177                    return Some(TextShortcut::Move(TextCursorDirection::BackwardByWord));
1178                }
1179                key_codes::RightArrow => {
1180                    return Some(TextShortcut::Move(TextCursorDirection::ForwardByWord));
1181                }
1182                key_codes::UpArrow => {
1183                    return Some(TextShortcut::Move(TextCursorDirection::StartOfParagraph));
1184                }
1185                key_codes::DownArrow => {
1186                    return Some(TextShortcut::Move(TextCursorDirection::EndOfParagraph));
1187                }
1188                key_codes::Backspace => {
1189                    return Some(TextShortcut::DeleteWordBackward);
1190                }
1191                key_codes::Delete => {
1192                    return Some(TextShortcut::DeleteWordForward);
1193                }
1194                _ => (),
1195            };
1196        }
1197
1198        #[cfg(not(target_os = "macos"))]
1199        {
1200            if ke.modifiers.control && !ke.modifiers.alt && !ke.modifiers.meta {
1201                match keycode {
1202                    key_codes::Home => {
1203                        return Some(TextShortcut::Move(TextCursorDirection::StartOfText));
1204                    }
1205                    key_codes::End => {
1206                        return Some(TextShortcut::Move(TextCursorDirection::EndOfText));
1207                    }
1208                    _ => (),
1209                };
1210            }
1211        }
1212
1213        if is_apple && ke.modifiers.control {
1214            match keycode {
1215                key_codes::LeftArrow => {
1216                    return Some(TextShortcut::Move(TextCursorDirection::StartOfLine));
1217                }
1218                key_codes::RightArrow => {
1219                    return Some(TextShortcut::Move(TextCursorDirection::EndOfLine));
1220                }
1221                key_codes::UpArrow => {
1222                    return Some(TextShortcut::Move(TextCursorDirection::StartOfText));
1223                }
1224                key_codes::DownArrow => {
1225                    return Some(TextShortcut::Move(TextCursorDirection::EndOfText));
1226                }
1227                key_codes::Backspace => {
1228                    return Some(TextShortcut::DeleteToStartOfLine);
1229                }
1230                _ => (),
1231            };
1232        }
1233
1234        if let Ok(direction) = TextCursorDirection::try_from(keycode) {
1235            Some(TextShortcut::Move(direction))
1236        } else {
1237            match keycode {
1238                key_codes::Backspace => Some(TextShortcut::DeleteBackward),
1239                key_codes::Delete => Some(TextShortcut::DeleteForward),
1240                _ => None,
1241            }
1242        }
1243    }
1244}
1245
1246/// Represents a non context specific shortcut.
1247pub enum StandardShortcut {
1248    /// Copy Something
1249    Copy,
1250    /// Cut Something
1251    Cut,
1252    /// Paste Something
1253    Paste,
1254    /// Select All
1255    SelectAll,
1256    /// Find/Search Something
1257    Find,
1258    /// Save Something
1259    Save,
1260    /// Print Something
1261    Print,
1262    /// Undo the last action
1263    Undo,
1264    /// Redo the last undone action
1265    Redo,
1266    /// Refresh
1267    Refresh,
1268}
1269
1270/// Shortcuts that are used when editing text
1271pub enum TextShortcut {
1272    /// Move the cursor
1273    Move(TextCursorDirection),
1274    /// Delete the Character to the right of the cursor
1275    DeleteForward,
1276    /// Delete the Character to the left of the cursor (aka Backspace).
1277    DeleteBackward,
1278    /// Delete the word to the right of the cursor
1279    DeleteWordForward,
1280    /// Delete the word to the left of the cursor (aka Ctrl + Backspace).
1281    DeleteWordBackward,
1282    /// Delete to the left of the cursor until the start of the line
1283    DeleteToStartOfLine,
1284}
1285
1286/// Represents how an item's key_event handler dealt with a key event.
1287/// An accepted event results in no further event propagation.
1288#[repr(u8)]
1289#[derive(Debug, Clone, Copy, PartialEq, Default)]
1290pub enum KeyEventResult {
1291    /// The event was handled.
1292    EventAccepted,
1293    /// The event was not handled and should be sent to other items.
1294    #[default]
1295    EventIgnored,
1296}
1297
1298/// Represents how an item's focus_event handler dealt with a focus event.
1299/// An accepted event results in no further event propagation.
1300#[repr(u8)]
1301#[derive(Debug, Clone, Copy, PartialEq, Default)]
1302pub enum FocusEventResult {
1303    /// The event was handled.
1304    FocusAccepted,
1305    /// The event was not handled and should be sent to other items.
1306    #[default]
1307    FocusIgnored,
1308}
1309
1310/// This event is sent to a component and items when they receive or lose
1311/// the keyboard focus.
1312#[derive(Debug, Clone, Copy, PartialEq)]
1313#[repr(u8)]
1314pub enum FocusEvent {
1315    /// This event is sent when an item receives the focus.
1316    FocusIn(FocusReason),
1317    /// This event is sent when an item loses the focus.
1318    FocusOut(FocusReason),
1319}
1320
1321/// This state is used to count the clicks separated by [`crate::platform::Platform::click_interval`]
1322#[derive(Default)]
1323pub struct ClickState {
1324    click_count_time_stamp: Cell<Option<crate::animations::Instant>>,
1325    click_count: Cell<u8>,
1326    click_position: Cell<LogicalPoint>,
1327    click_button: Cell<PointerEventButton>,
1328}
1329
1330impl ClickState {
1331    /// Resets the timer and count.
1332    fn restart(
1333        &self,
1334        position: LogicalPoint,
1335        button: PointerEventButton,
1336        now: crate::animations::Instant,
1337    ) {
1338        self.click_count.set(0);
1339        self.click_count_time_stamp.set(Some(now));
1340        self.click_position.set(position);
1341        self.click_button.set(button);
1342    }
1343
1344    /// Reset to an invalid state
1345    pub fn reset(&self) {
1346        self.click_count.set(0);
1347        self.click_count_time_stamp.replace(None);
1348    }
1349
1350    /// Check if the click is repeated.
1351    /// Takes the context rather than just the interval: the timestamps it compares have to
1352    /// come from the same clock, which only the context can name.
1353    pub fn check_repeat(&self, mouse_event: MouseEvent, ctx: &crate::SlintContext) -> MouseEvent {
1354        let click_interval = ctx.platform().click_interval();
1355        match mouse_event {
1356            MouseEvent::Pressed { position, button, touch_finger_id, .. } => {
1357                let instant_now = crate::animations::Instant::now(ctx);
1358
1359                if let Some(click_count_time_stamp) = self.click_count_time_stamp.get() {
1360                    if instant_now - click_count_time_stamp < click_interval
1361                        && button == self.click_button.get()
1362                        && (position - self.click_position.get()).square_length() < 100 as _
1363                    {
1364                        self.click_count.set(self.click_count.get().wrapping_add(1));
1365                        self.click_count_time_stamp.set(Some(instant_now));
1366                    } else {
1367                        self.restart(position, button, instant_now);
1368                    }
1369                } else {
1370                    self.restart(position, button, instant_now);
1371                }
1372
1373                return MouseEvent::Pressed {
1374                    position,
1375                    button,
1376                    click_count: self.click_count.get(),
1377                    touch_finger_id,
1378                };
1379            }
1380            MouseEvent::Released { position, button, touch_finger_id, .. } => {
1381                return MouseEvent::Released {
1382                    position,
1383                    button,
1384                    click_count: self.click_count.get(),
1385                    touch_finger_id,
1386                };
1387            }
1388            _ => {}
1389        };
1390
1391        mouse_event
1392    }
1393}
1394
1395/// The data for an in-flight drag-and-drop operation, held while a drag is active.
1396#[derive(Clone)]
1397pub(crate) struct DragData {
1398    /// The dragged payload together with its current position and proposed action.
1399    /// The `position` is updated on every move.
1400    pub(crate) event: DropEvent,
1401    /// The actions the drag source permits, captured at drag start.
1402    pub(crate) allowed: AllowedDragActions,
1403}
1404
1405/// The state which a window should hold for the mouse input
1406#[derive(Default)]
1407pub struct MouseInputState {
1408    /// The stack of item which contain the mouse cursor (or grab),
1409    /// along with the last result from the input function
1410    item_stack: Vec<(ItemWeak, InputEventFilterResult)>,
1411    /// Passive trackers that saw the last event without claiming it (see
1412    /// [`InputEventResult::ObserveEvent`]). Held outside `item_stack` so the stack
1413    /// stays a single root-to-leaf path; entries here receive a synthesized
1414    /// [`MouseEvent::Exit`] when they no longer appear after a new event.
1415    observers: Vec<ItemWeak>,
1416    /// Offset to apply to the first item of the stack (used if there is a popup)
1417    pub(crate) offset: LogicalPoint,
1418    /// true if the top item of the stack has the mouse grab
1419    grabbed: bool,
1420    /// When this is Some, it means we are in the middle of a drag-drop operation and it contains the dragged data.
1421    /// The `position` field has no signification
1422    pub(crate) drag_data: Option<DragData>,
1423    /// The `DragArea` that initiated the in-flight drag.
1424    /// `None` for drags coming from outside (native cross-window/cross-process DnD).
1425    pub(crate) drag_source: Option<ItemWeak>,
1426    /// The DropArea that accepted the most recent DragMove, if any. On release we use
1427    /// this to decide whether to deliver a Drop — matching OS DnD pipelines, where a
1428    /// target that didn't previously accept never receives a drop.
1429    pub(crate) drop_target: Option<ItemWeak>,
1430    delayed: Option<(crate::timers::Timer, MouseEvent)>,
1431    delayed_exit_items: Vec<ItemWeak>,
1432    pub(crate) cursor: MouseCursorInner,
1433}
1434
1435impl MouseInputState {
1436    /// Return the item in the top of the stack
1437    fn top_item(&self) -> Option<ItemRc> {
1438        self.item_stack.last().and_then(|x| x.0.upgrade())
1439    }
1440
1441    /// Arm the in-window drag: seed `drag_data`/`drag_source` from `drag_area` at `seed_position`
1442    /// and mark it dragging.
1443    pub(crate) fn arm_in_window_drag(
1444        &mut self,
1445        drag_area: core::pin::Pin<&crate::items::DragArea>,
1446        source: ItemWeak,
1447        seed_position: crate::api::LogicalPosition,
1448    ) {
1449        let (mut drop_event, allowed) = drag_area.initial_drop_event();
1450        drop_event.position = seed_position;
1451        self.drag_data = Some(DragData { event: drop_event, allowed });
1452        self.drag_source = Some(source);
1453        drag_area.dragging.set(true);
1454    }
1455
1456    /// Returns the item in the top of the stack, if there is a delayed event, this would be the top of the delayed stack
1457    pub fn top_item_including_delayed(&self) -> Option<ItemRc> {
1458        self.delayed_exit_items.last().and_then(|x| x.upgrade()).or_else(|| self.top_item())
1459    }
1460
1461    /// Returns true if there is a pending delayed event (e.g. from a Flickable)
1462    pub fn has_delayed_event(&self) -> bool {
1463        self.delayed.is_some()
1464    }
1465
1466    /// The action negotiated with the `DropArea` that accepted the most recent
1467    /// `DragMove`/`Drop`, or `None` if none accepted.
1468    pub fn drop_target_action(&self) -> Option<crate::items::DragAction> {
1469        let action = self
1470            .drop_target
1471            .as_ref()
1472            .and_then(|t| t.upgrade())
1473            .and_then(|i| i.downcast::<crate::items::DropArea>())
1474            .map(|d| d.as_pin_ref().current_action())?;
1475        (action != crate::items::DragAction::None).then_some(action)
1476    }
1477}
1478
1479pub(crate) struct MouseGrabResult {
1480    /// The event that still needs normal hit-test dispatch. `None` means the grabber
1481    /// fully handled the original event.
1482    pub event: Option<MouseEvent>,
1483    /// Whether the grabber consumed the original event before any follow-up event was
1484    /// synthesized for hover/grab refresh.
1485    pub accepted: bool,
1486}
1487
1488/// Start a drag from `drag_area`, preferring a native (OS-level) drag and falling back to the
1489/// in-window drag (armed on `state`) when no backend takes over.
1490fn offer_native_drag(
1491    window_adapter: &Rc<dyn WindowAdapter>,
1492    drag_area: core::pin::Pin<&crate::items::DragArea>,
1493    source: ItemWeak,
1494    seed_position: crate::api::LogicalPosition,
1495    state: &mut MouseInputState,
1496) {
1497    let data = drag_area.data();
1498    // A native drag only carries serializable data, so offer it only when there's some.
1499    if data.has_plain_text() || data.has_image() {
1500        let request = crate::window::DragRequest {
1501            data: data.clone(),
1502            allowed: drag_area.allowed_actions(),
1503            drag_image: drag_area.drag_image(),
1504            drag_image_offset: euclid::vec2(
1505                drag_area.drag_image_offset_x(),
1506                drag_area.drag_image_offset_y(),
1507            ),
1508        };
1509        if window_adapter.internal(crate::InternalToken).is_some_and(|i| i.start_drag(&request)) {
1510            // The backend took over (and defers the actual drag). Stash it so it can report
1511            // completion or fall back, and so a drop back onto this window restores the data.
1512            let drag = crate::window::NativePendingDrag { request, source, seed_position };
1513            crate::window::WindowInner::from_pub(window_adapter.window())
1514                .set_native_drag(Some(drag));
1515            drag_area.dragging.set(true);
1516            return;
1517        }
1518    }
1519    // No backend took over: fall back to the in-window drag.
1520    state.arm_in_window_drag(drag_area, source, seed_position);
1521}
1522
1523/// Try to handle the mouse grabber.
1524pub(crate) fn handle_mouse_grab(
1525    mouse_event: &MouseEvent,
1526    window_adapter: &Rc<dyn WindowAdapter>,
1527    mouse_input_state: &mut MouseInputState,
1528) -> MouseGrabResult {
1529    if !mouse_input_state.grabbed || mouse_input_state.item_stack.is_empty() {
1530        return MouseGrabResult { event: Some(mouse_event.clone()), accepted: false };
1531    };
1532
1533    let mut event = mouse_event.clone();
1534    let mut intercept = false;
1535    let mut invalid = false;
1536
1537    event.translate(-mouse_input_state.offset.to_vector());
1538
1539    mouse_input_state.item_stack.retain(|it| {
1540        if invalid {
1541            return false;
1542        }
1543        let item = if let Some(item) = it.0.upgrade() {
1544            item
1545        } else {
1546            invalid = true;
1547            return false;
1548        };
1549        if intercept {
1550            item.borrow().as_ref().input_event(
1551                &MouseEvent::Exit,
1552                window_adapter,
1553                &item,
1554                &mut mouse_input_state.cursor,
1555            );
1556            return false;
1557        }
1558        let g = item.geometry();
1559        event.translate(-g.origin.to_vector());
1560        if window_adapter.renderer().supports_transformations()
1561            && let Some(inverse_transform) = item.inverse_children_transform()
1562        {
1563            event.transform(inverse_transform);
1564        }
1565
1566        let interested = matches!(
1567            it.1,
1568            InputEventFilterResult::ForwardAndInterceptGrab
1569                | InputEventFilterResult::DelayForwarding(_)
1570        );
1571
1572        if interested
1573            && item.borrow().as_ref().input_event_filter_before_children(
1574                &event,
1575                window_adapter,
1576                &item,
1577                &mut mouse_input_state.cursor,
1578            ) == InputEventFilterResult::Intercept
1579        {
1580            intercept = true;
1581        }
1582        true
1583    });
1584    if invalid {
1585        return MouseGrabResult { event: Some(mouse_event.clone()), accepted: false };
1586    }
1587
1588    let grabber = mouse_input_state.top_item().unwrap();
1589    let input_result = grabber.borrow().as_ref().input_event(
1590        &event,
1591        window_adapter,
1592        &grabber,
1593        &mut mouse_input_state.cursor,
1594    );
1595    match input_result {
1596        InputEventResult::GrabMouse => MouseGrabResult { event: None, accepted: true },
1597        InputEventResult::StartDrag => {
1598            mouse_input_state.grabbed = false;
1599            let drag_area_item = grabber.downcast::<crate::items::DragArea>().unwrap();
1600            let drag_area = drag_area_item.as_pin_ref();
1601            // Seed the drag position from the event that crossed the drag threshold so
1602            // the renderer can place the drag-image overlay before the first DragMove.
1603            let seed_position = mouse_event
1604                .position()
1605                .map(crate::lengths::logical_position_to_api)
1606                .unwrap_or_default();
1607            offer_native_drag(
1608                window_adapter,
1609                drag_area,
1610                grabber.downgrade(),
1611                seed_position,
1612                mouse_input_state,
1613            );
1614            MouseGrabResult { event: None, accepted: true }
1615        }
1616        InputEventResult::EventAccepted | InputEventResult::EventIgnored => {
1617            mouse_input_state.grabbed = false;
1618            // Return a move event so that the new position can be registered properly
1619            MouseGrabResult {
1620                event: Some(mouse_event.position().map_or(MouseEvent::Exit, |position| {
1621                    MouseEvent::Moved { position, touch_finger_id: mouse_event.touch_finger_id() }
1622                })),
1623                accepted: input_result == InputEventResult::EventAccepted,
1624            }
1625        }
1626    }
1627}
1628
1629pub(crate) fn send_exit_events(
1630    old_input_state: &MouseInputState,
1631    new_input_state: &mut MouseInputState,
1632    mut pos: Option<LogicalPoint>,
1633    window_adapter: &Rc<dyn WindowAdapter>,
1634) {
1635    // Note that exit events can't actually change the cursor from default so we'll ignore the result
1636    let cursor = &mut MouseCursorInner::BuiltIn(BuiltInMouseCursor::Default);
1637
1638    for it in core::mem::take(&mut new_input_state.delayed_exit_items) {
1639        let Some(item) = it.upgrade() else { continue };
1640        item.borrow().as_ref().input_event(&MouseEvent::Exit, window_adapter, &item, cursor);
1641    }
1642
1643    let mut clipped = false;
1644    for (idx, it) in old_input_state.item_stack.iter().enumerate() {
1645        let Some(item) = it.0.upgrade() else { break };
1646        let g = item.geometry();
1647        let contains = pos.is_some_and(|p| g.contains(p));
1648        if let Some(p) = pos.as_mut() {
1649            *p -= g.origin.to_vector();
1650            if window_adapter.renderer().supports_transformations()
1651                && let Some(inverse_transform) = item.inverse_children_transform()
1652            {
1653                *p = inverse_transform.transform_point(p.cast()).cast();
1654            }
1655        }
1656        if !contains || clipped {
1657            if item.borrow().as_ref().clips_children() {
1658                clipped = true;
1659            }
1660            item.borrow().as_ref().input_event(&MouseEvent::Exit, window_adapter, &item, cursor);
1661        } else if new_input_state.item_stack.get(idx).is_none_or(|(x, _)| *x != it.0) {
1662            // The item is still under the mouse, but no longer in the item stack. We should also sent the exit event, unless we delay it
1663            if new_input_state.delayed.is_some() {
1664                new_input_state.delayed_exit_items.push(it.0.clone());
1665            } else {
1666                item.borrow().as_ref().input_event(
1667                    &MouseEvent::Exit,
1668                    window_adapter,
1669                    &item,
1670                    cursor,
1671                );
1672            }
1673        }
1674    }
1675
1676    // Observers live outside the path-stack and are tracked by identity. Exit fires
1677    // only when the item is missing from BOTH the new observer set and the new path
1678    // stack: an item whose ForwardAndObserve filter never ran (because a child aborted
1679    // before reaching it) is still on the path stack with another filter result, and
1680    // should not receive Exit.
1681    for obs in &old_input_state.observers {
1682        if new_input_state.observers.iter().any(|x| x == obs)
1683            || new_input_state.item_stack.iter().any(|(x, _)| x == obs)
1684        {
1685            continue;
1686        }
1687        let Some(item) = obs.upgrade() else { continue };
1688        item.borrow().as_ref().input_event(&MouseEvent::Exit, window_adapter, &item, cursor);
1689    }
1690}
1691
1692/// Outcome of [`process_mouse_input`].
1693pub struct MouseInputResult {
1694    /// The new dispatch state to install in place of the one passed in.
1695    pub state: MouseInputState,
1696    /// `true` when an item consumed the event (`EventAccepted`, `GrabMouse`,
1697    /// `StartDrag`, or a `DropArea` taking a `DragMove`/`Drop`).
1698    pub accepted: bool,
1699}
1700
1701/// Process the `mouse_event` on the `component`. The `mouse_input_state` is the previous
1702/// dispatch state (grab stack, cursor, in-flight drag); the returned [`MouseInputResult`]
1703/// carries the state that replaces it and whether the event was consumed.
1704pub fn process_mouse_input(
1705    root: ItemRc,
1706    mouse_event: &MouseEvent,
1707    window_adapter: &Rc<dyn WindowAdapter>,
1708    mut mouse_input_state: MouseInputState,
1709) -> MouseInputResult {
1710    let mut result = MouseInputState {
1711        drag_data: mouse_input_state.drag_data.clone(),
1712        drag_source: mouse_input_state.drag_source.clone(),
1713        drop_target: mouse_input_state.drop_target.clone(),
1714        cursor: mouse_input_state.cursor.clone(),
1715        ..Default::default()
1716    };
1717    let r = send_mouse_event_to_item(
1718        mouse_event,
1719        root.clone(),
1720        window_adapter,
1721        &mut result,
1722        mouse_input_state.top_item().as_ref(),
1723        false,
1724    );
1725    let accepted = r.has_aborted();
1726    if matches!(mouse_event, MouseEvent::DragMove { .. }) {
1727        // Remember the accepting DropArea (or forget if none did) so the subsequent
1728        // Release knows whether to deliver a Drop.
1729        result.drop_target =
1730            accepted.then(|| result.item_stack.last().map(|(w, _)| w.clone())).flatten();
1731    }
1732    if mouse_input_state.delayed.is_some()
1733        && (!accepted
1734            || Option::zip(result.item_stack.last(), mouse_input_state.item_stack.last())
1735                .is_none_or(|(a, b)| a.0 != b.0))
1736    {
1737        // Keep the delayed event but transfer the just-attempted dispatch's cursor.
1738        mouse_input_state.cursor = result.cursor;
1739        return MouseInputResult { state: mouse_input_state, accepted };
1740    }
1741    send_exit_events(&mouse_input_state, &mut result, mouse_event.position(), window_adapter);
1742
1743    if let MouseEvent::Wheel { position, .. } = mouse_event
1744        && accepted
1745    {
1746        // An accepted wheel event might have moved things. Send a synthetic Moved to refresh
1747        // has-hover. The original wheel's `accepted` (always `true` in this branch) is the
1748        // outcome the caller sees — the synthetic Moved is an internal implementation detail.
1749        let moved = process_mouse_input(
1750            root,
1751            &MouseEvent::Moved { position: *position, touch_finger_id: 0 },
1752            window_adapter,
1753            result,
1754        );
1755        return MouseInputResult { state: moved.state, accepted: true };
1756    }
1757
1758    MouseInputResult { state: result, accepted }
1759}
1760
1761pub(crate) fn process_delayed_event(
1762    window_adapter: &Rc<dyn WindowAdapter>,
1763    mut mouse_input_state: MouseInputState,
1764) -> MouseInputState {
1765    // the take bellow will also destroy the Timer
1766    let event = match mouse_input_state.delayed.take() {
1767        Some(e) => e.1,
1768        None => return mouse_input_state,
1769    };
1770
1771    let top_item = match mouse_input_state.top_item() {
1772        Some(i) => i,
1773        None => return MouseInputState::default(),
1774    };
1775
1776    // Recover the real previous click target so click_count is preserved across delayed events
1777    let prev_target = mouse_input_state.delayed_exit_items.last().and_then(|x| x.upgrade());
1778    let last_top_item = prev_target.as_ref().unwrap_or(&top_item);
1779
1780    let mut actual_visitor =
1781        |component: &ItemTreeRc, index: u32, _: Pin<ItemRef>| -> VisitChildrenResult {
1782            send_mouse_event_to_item(
1783                &event,
1784                ItemRc::new(component.clone(), index),
1785                window_adapter,
1786                &mut mouse_input_state,
1787                Some(last_top_item),
1788                true,
1789            )
1790        };
1791    vtable::new_vref!(let mut actual_visitor : VRefMut<crate::item_tree::ItemVisitorVTable> for crate::item_tree::ItemVisitor = &mut actual_visitor);
1792    vtable::VRc::borrow_pin(top_item.item_tree()).as_ref().visit_children_item(
1793        top_item.index() as isize,
1794        crate::item_tree::TraversalOrder::FrontToBack,
1795        actual_visitor,
1796    );
1797    mouse_input_state
1798}
1799
1800fn send_mouse_event_to_item(
1801    mouse_event: &MouseEvent,
1802    item_rc: ItemRc,
1803    window_adapter: &Rc<dyn WindowAdapter>,
1804    result: &mut MouseInputState,
1805    last_top_item: Option<&ItemRc>,
1806    ignore_delays: bool,
1807) -> VisitChildrenResult {
1808    let item = item_rc.borrow();
1809    let geom = item_rc.geometry();
1810    // translated in our coordinate
1811    let mut event_for_children = mouse_event.clone();
1812    // Unapply the translation to go from 'world' space to local space
1813    event_for_children.translate(-geom.origin.to_vector());
1814    if window_adapter.renderer().supports_transformations() {
1815        // Unapply other transforms.
1816        if let Some(inverse_transform) = item_rc.inverse_children_transform() {
1817            event_for_children.transform(inverse_transform);
1818        }
1819    }
1820
1821    let filter_result = if mouse_event.position().is_some_and(|p| geom.contains(p))
1822        || item.as_ref().clips_children()
1823    {
1824        item.as_ref().input_event_filter_before_children(
1825            &event_for_children,
1826            window_adapter,
1827            &item_rc,
1828            &mut result.cursor,
1829        )
1830    } else {
1831        InputEventFilterResult::ForwardAndIgnore
1832    };
1833
1834    let (forward_to_children, ignore) = match filter_result {
1835        InputEventFilterResult::ForwardEvent => (true, false),
1836        InputEventFilterResult::ForwardAndIgnore => (true, true),
1837        InputEventFilterResult::ForwardAndInterceptGrab => (true, false),
1838        InputEventFilterResult::Intercept => (false, false),
1839        InputEventFilterResult::DelayForwarding(_) if ignore_delays => (true, false),
1840        InputEventFilterResult::DelayForwarding(duration) => {
1841            let timer = WindowInner::from_pub(window_adapter.window()).context().new_timer();
1842            let w = Rc::downgrade(window_adapter);
1843            timer.start(
1844                crate::timers::TimerMode::SingleShot,
1845                Duration::from_millis(duration),
1846                move || {
1847                    if let Some(w) = w.upgrade() {
1848                        WindowInner::from_pub(w.window()).process_delayed_event();
1849                    }
1850                },
1851            );
1852            result.delayed = Some((timer, event_for_children));
1853            result
1854                .item_stack
1855                .push((item_rc.downgrade(), InputEventFilterResult::DelayForwarding(duration)));
1856            return VisitChildrenResult::abort(item_rc.index(), 0);
1857        }
1858        // Like ForwardAndIgnore: forward to children, skip input_event. The
1859        // EventIgnored arm below moves our entry from the path stack to the observers
1860        // side list instead of dropping it.
1861        InputEventFilterResult::ForwardAndObserve => (true, true),
1862    };
1863
1864    result.item_stack.push((item_rc.downgrade(), filter_result));
1865    if forward_to_children {
1866        let mut actual_visitor =
1867            |component: &ItemTreeRc, index: u32, _: Pin<ItemRef>| -> VisitChildrenResult {
1868                send_mouse_event_to_item(
1869                    &event_for_children,
1870                    ItemRc::new(component.clone(), index),
1871                    window_adapter,
1872                    result,
1873                    last_top_item,
1874                    ignore_delays,
1875                )
1876            };
1877        vtable::new_vref!(let mut actual_visitor : VRefMut<crate::item_tree::ItemVisitorVTable> for crate::item_tree::ItemVisitor = &mut actual_visitor);
1878        let r = vtable::VRc::borrow_pin(item_rc.item_tree()).as_ref().visit_children_item(
1879            item_rc.index() as isize,
1880            crate::item_tree::TraversalOrder::FrontToBack,
1881            actual_visitor,
1882        );
1883        if r.has_aborted() {
1884            return r;
1885        }
1886    };
1887
1888    let r = if ignore {
1889        InputEventResult::EventIgnored
1890    } else {
1891        let mut event = mouse_event.clone();
1892        event.translate(-geom.origin.to_vector());
1893        if last_top_item.is_none_or(|x| *x != item_rc) {
1894            event.set_click_count(0);
1895        }
1896        item.as_ref().input_event(&event, window_adapter, &item_rc, &mut result.cursor)
1897    };
1898    match r {
1899        InputEventResult::EventAccepted => VisitChildrenResult::abort(item_rc.index(), 0),
1900        InputEventResult::EventIgnored => {
1901            let popped = result.item_stack.pop();
1902            debug_assert_eq!(
1903                popped.as_ref().map(|x| (x.0.upgrade().unwrap().index(), x.1)).unwrap(),
1904                (item_rc.index(), filter_result)
1905            );
1906            // For ForwardAndObserve, migrate the entry to the observers side list (dedup)
1907            // so a later Exit can still reach it.
1908            if filter_result == InputEventFilterResult::ForwardAndObserve
1909                && let Some((weak, _)) = popped
1910                && !result.observers.contains(&weak)
1911            {
1912                result.observers.push(weak);
1913            }
1914            VisitChildrenResult::CONTINUE
1915        }
1916        InputEventResult::GrabMouse => {
1917            result.item_stack.last_mut().unwrap().1 =
1918                InputEventFilterResult::ForwardAndInterceptGrab;
1919            result.grabbed = true;
1920            VisitChildrenResult::abort(item_rc.index(), 0)
1921        }
1922        InputEventResult::StartDrag => {
1923            result.item_stack.last_mut().unwrap().1 =
1924                InputEventFilterResult::ForwardAndInterceptGrab;
1925            result.grabbed = false;
1926            let drag_area_item = item_rc.downcast::<crate::items::DragArea>().unwrap();
1927            let drag_area = drag_area_item.as_pin_ref();
1928            // `mouse_event` here is in the parent item's coords (this function is called
1929            // recursively); translate into the DragArea's local coords, then map back to
1930            // window coords so the drag-image overlay places at the right spot from the start.
1931            let seed_position = mouse_event
1932                .position()
1933                .map(|p| p - geom.origin.to_vector())
1934                .map(|p| item_rc.map_to_window(p))
1935                .map(crate::lengths::logical_position_to_api)
1936                .unwrap_or_default();
1937            offer_native_drag(
1938                window_adapter,
1939                drag_area,
1940                item_rc.downgrade(),
1941                seed_position,
1942                result,
1943            );
1944            VisitChildrenResult::abort(item_rc.index(), 0)
1945        }
1946    }
1947}
1948
1949/// The TextCursorBlinker takes care of providing a toggled boolean property
1950/// that can be used to animate a blinking cursor. It's typically stored in the
1951/// Window using a Weak and set_binding() can be used to set up a binding on a given
1952/// property that'll keep it up-to-date. That binding keeps a strong reference to the
1953/// blinker. If the underlying item that uses it goes away, the binding goes away and
1954/// so does the blinker.
1955#[derive(FieldOffsets)]
1956#[repr(C)]
1957#[pin]
1958pub(crate) struct TextCursorBlinker {
1959    cursor_visible: Property<bool>,
1960    cursor_blink_timer: crate::timers::Timer,
1961}
1962
1963impl TextCursorBlinker {
1964    /// Creates a new instance, wrapped in a Pin<Rc<_>> because the boolean property
1965    /// the blinker properties uses the property system that requires pinning.
1966    pub fn new() -> Pin<Rc<Self>> {
1967        Rc::pin(Self {
1968            cursor_visible: Property::new(true),
1969            cursor_blink_timer: Default::default(),
1970        })
1971    }
1972
1973    /// Sets a binding on the provided property that will ensure that the property value
1974    /// is true when the cursor should be shown and false if not.
1975    pub fn set_binding(
1976        instance: Pin<Rc<TextCursorBlinker>>,
1977        prop: &Property<bool>,
1978        ctx: &crate::SlintContext,
1979        cycle_duration: Duration,
1980    ) {
1981        instance.as_ref().cursor_visible.set(true);
1982        // Re-start timer, in case.
1983        Self::start(&instance, ctx, cycle_duration);
1984        prop.set_binding(move || {
1985            TextCursorBlinker::FIELD_OFFSETS.cursor_visible().apply_pin(instance.as_ref()).get()
1986        });
1987    }
1988
1989    /// Starts the blinking cursor timer that will toggle the cursor and update all bindings that
1990    /// were installed on properties with set_binding call.
1991    pub fn start(self: &Pin<Rc<Self>>, ctx: &crate::SlintContext, cycle_duration: Duration) {
1992        if self.cursor_blink_timer.running() {
1993            self.cursor_blink_timer.restart();
1994        } else {
1995            let toggle_cursor = {
1996                let weak_blinker = pin_weak::rc::PinWeak::downgrade(self.clone());
1997                move || {
1998                    if let Some(blinker) = weak_blinker.upgrade() {
1999                        let visible = TextCursorBlinker::FIELD_OFFSETS
2000                            .cursor_visible()
2001                            .apply_pin(blinker.as_ref())
2002                            .get();
2003                        blinker.cursor_visible.set(!visible);
2004                    }
2005                }
2006            };
2007            if !cycle_duration.is_zero() {
2008                self.cursor_blink_timer.start_on(
2009                    ctx,
2010                    crate::timers::TimerMode::Repeated,
2011                    cycle_duration / 2,
2012                    toggle_cursor,
2013                );
2014            }
2015        }
2016    }
2017
2018    /// Stops the blinking cursor timer. This is usually used for example when the window that contains
2019    /// text editable elements looses the focus or is hidden.
2020    pub fn stop(&self) {
2021        self.cursor_blink_timer.stop()
2022    }
2023}
2024
2025/// A single active touch point.
2026#[derive(Clone, Copy, Default)]
2027struct TouchPoint {
2028    id: i32,
2029    position: LogicalPoint,
2030}
2031
2032/// Fixed-capacity map of touch IDs to touch points.
2033///
2034/// Touchscreens rarely report more than 5 simultaneous contacts, and gesture
2035/// recognition only uses the first two. A linear-scan array avoids the heap
2036/// allocation and pointer-chasing overhead of `BTreeMap` for this tiny collection.
2037const MAX_TRACKED_TOUCHES: usize = 5;
2038
2039#[derive(Clone)]
2040struct TouchMap {
2041    entries: [TouchPoint; MAX_TRACKED_TOUCHES],
2042    len: usize,
2043}
2044
2045impl Default for TouchMap {
2046    fn default() -> Self {
2047        Self { entries: [TouchPoint::default(); MAX_TRACKED_TOUCHES], len: 0 }
2048    }
2049}
2050
2051impl TouchMap {
2052    fn get(&self, id: i32) -> Option<&TouchPoint> {
2053        self.entries[..self.len].iter().find(|tp| tp.id == id)
2054    }
2055
2056    fn get_mut(&mut self, id: i32) -> Option<&mut TouchPoint> {
2057        self.entries[..self.len].iter_mut().find(|tp| tp.id == id)
2058    }
2059
2060    fn insert(&mut self, point: TouchPoint) {
2061        if let Some(existing) = self.entries[..self.len].iter_mut().find(|tp| tp.id == point.id) {
2062            *existing = point;
2063        } else if self.len < MAX_TRACKED_TOUCHES {
2064            self.entries[self.len] = point;
2065            self.len += 1;
2066        }
2067    }
2068
2069    fn remove(&mut self, id: i32) {
2070        if let Some(idx) = self.entries[..self.len].iter().position(|tp| tp.id == id) {
2071            self.len -= 1;
2072            self.entries[idx] = self.entries[self.len];
2073        }
2074    }
2075
2076    fn len(&self) -> usize {
2077        self.len
2078    }
2079
2080    /// Returns the first two distinct IDs, or `None` if fewer than 2 entries.
2081    fn first_two_ids(&self) -> Option<(i32, i32)> {
2082        if self.len >= 2 { Some((self.entries[0].id, self.entries[1].id)) } else { None }
2083    }
2084
2085    /// Returns the first entry, if any.
2086    fn first(&self) -> Option<&TouchPoint> {
2087        if self.len > 0 { Some(&self.entries[0]) } else { None }
2088    }
2089}
2090
2091/// Fixed-capacity buffer for [`MouseEvent`]s produced by the touch state machine.
2092///
2093/// No branch in [`TouchState::process`] emits more than 3 events (gesture end
2094/// produces PinchEnded + RotationEnded + Pressed/Exit). Capacity 4 provides a
2095/// margin without heap allocation.
2096const MAX_TOUCH_EVENTS: usize = 4;
2097
2098#[derive(Clone)]
2099pub(crate) struct TouchEventBuffer {
2100    events: [Option<MouseEvent>; MAX_TOUCH_EVENTS],
2101    len: usize,
2102}
2103
2104impl TouchEventBuffer {
2105    fn new() -> Self {
2106        Self { events: [None, None, None, None], len: 0 }
2107    }
2108
2109    fn push(&mut self, event: MouseEvent) {
2110        debug_assert!(self.len < MAX_TOUCH_EVENTS, "TouchEventBuffer overflow");
2111        if self.len < MAX_TOUCH_EVENTS {
2112            self.events[self.len] = Some(event);
2113            self.len += 1;
2114        }
2115    }
2116
2117    /// Returns an iterator over the buffered events.
2118    pub(crate) fn into_iter(self) -> impl Iterator<Item = MouseEvent> {
2119        let len = self.len;
2120        self.events.into_iter().take(len).flatten()
2121    }
2122}
2123
2124/// State of the multi-touch gesture recognizer.
2125#[derive(Default, Debug, Clone, Copy)]
2126enum GestureRecognitionState {
2127    /// 0-1 fingers; forwarding as mouse events.
2128    #[default]
2129    Idle,
2130    /// 2 fingers down, waiting for movement to exceed threshold.
2131    TwoFingersDown { finger_ids: (i32, i32), initial_distance: f32, last_angle: euclid::Angle<f32> },
2132    /// Actively synthesizing PinchGesture/RotationGesture events.
2133    Pinching {
2134        finger_ids: (i32, i32),
2135        initial_distance: f32,
2136        last_scale: f32,
2137        last_angle: euclid::Angle<f32>,
2138    },
2139}
2140
2141/// Tracks all active touch points and recognizes pinch/rotation gestures.
2142///
2143/// When only one finger is down, touch events are forwarded as mouse events.
2144/// When two fingers are down and move beyond a threshold, synthesized
2145/// `PinchGesture` and `RotationGesture` events are emitted — the same events
2146/// that platform gesture recognition (e.g. macOS trackpad) produces.
2147pub(crate) struct TouchState {
2148    active_touches: TouchMap,
2149    /// The finger forwarded as mouse events during single-touch.
2150    primary_touch_id: Option<i32>,
2151    gesture_state: GestureRecognitionState,
2152}
2153
2154impl Default for TouchState {
2155    fn default() -> Self {
2156        Self {
2157            active_touches: TouchMap::default(),
2158            primary_touch_id: None,
2159            gesture_state: GestureRecognitionState::Idle,
2160        }
2161    }
2162}
2163
2164impl TouchState {
2165    /// Minimum movement (in logical pixels) before two fingers are recognized as a pinch.
2166    const PINCH_THRESHOLD: f32 = 8.0;
2167
2168    /// Minimum angular change (in degrees) before two fingers are recognized as a rotation.
2169    const ROTATION_THRESHOLD: f32 = 5.0;
2170
2171    /// Returns the finger IDs from the current gesture state, if any.
2172    fn gesture_finger_ids(&self) -> Option<(i32, i32)> {
2173        match self.gesture_state {
2174            GestureRecognitionState::TwoFingersDown { finger_ids, .. }
2175            | GestureRecognitionState::Pinching { finger_ids, .. } => Some(finger_ids),
2176            GestureRecognitionState::Idle => None,
2177        }
2178    }
2179
2180    /// Returns (distance, angle) between two specific touch points.
2181    fn geometry_for(&self, (id_a, id_b): (i32, i32)) -> Option<(f32, euclid::Angle<f32>)> {
2182        let a = self.active_touches.get(id_a)?;
2183        let b = self.active_touches.get(id_b)?;
2184        let delta = (b.position - a.position).cast::<f32>();
2185        Some((delta.length(), delta.angle_from_x_axis()))
2186    }
2187
2188    /// Returns the positions of the two gesture fingers, or `None` if not available.
2189    fn gesture_finger_positions(&self) -> Option<(&TouchPoint, &TouchPoint)> {
2190        let (id_a, id_b) = self.gesture_finger_ids()?;
2191        let a = self.active_touches.get(id_a)?;
2192        let b = self.active_touches.get(id_b)?;
2193        Some((a, b))
2194    }
2195
2196    /// Returns the midpoint between the two gesture fingers, or `None`.
2197    fn gesture_midpoint(&self) -> Option<LogicalPoint> {
2198        let (a, b) = self.gesture_finger_positions()?;
2199        let mid = a.position.cast::<f32>().lerp(b.position.cast::<f32>(), 0.5);
2200        Some(mid.cast())
2201    }
2202
2203    /// Returns (distance, angle) between the two gesture fingers.
2204    fn gesture_geometry(&self) -> Option<(f32, euclid::Angle<f32>)> {
2205        let (a, b) = self.gesture_finger_positions()?;
2206        let delta = (b.position - a.position).cast::<f32>();
2207        Some((delta.length(), delta.angle_from_x_axis()))
2208    }
2209
2210    /// Returns true if the given touch ID is one of the two gesture fingers.
2211    fn is_gesture_finger(&self, id: i32) -> bool {
2212        self.gesture_finger_ids().is_some_and(|(a, b)| id == a || id == b)
2213    }
2214
2215    /// Run the touch state machine for a single event and return the
2216    /// [`MouseEvent`]s to dispatch.
2217    ///
2218    /// This is intentionally separated from [`crate::window::WindowInner::process_touch_input`]
2219    /// so that the `RefCell` borrow can be dropped *once* before dispatching,
2220    /// rather than requiring a manual `drop` at every branch.
2221    pub(crate) fn process(
2222        &mut self,
2223        id: i32,
2224        position: LogicalPoint,
2225        phase: TouchPhase,
2226    ) -> TouchEventBuffer {
2227        let mut events = TouchEventBuffer::new();
2228        match phase {
2229            TouchPhase::Started => self.process_started(id, position, &mut events),
2230            TouchPhase::Moved => self.process_moved(id, position, &mut events),
2231            TouchPhase::Ended => self.process_ended(id, position, false, &mut events),
2232            TouchPhase::Cancelled => self.process_ended(id, position, true, &mut events),
2233        }
2234        events
2235    }
2236
2237    fn process_started(&mut self, id: i32, position: LogicalPoint, events: &mut TouchEventBuffer) {
2238        self.active_touches.insert(TouchPoint { id, position });
2239
2240        let total = self.active_touches.len();
2241        if total == 1 {
2242            // First finger: become primary, forward as mouse press.
2243            self.primary_touch_id = Some(id);
2244            self.gesture_state = GestureRecognitionState::Idle;
2245            events.push(MouseEvent::Pressed {
2246                position,
2247                button: PointerEventButton::Left,
2248                click_count: 0,
2249                touch_finger_id: id + 1,
2250            });
2251        } else if total == 2 {
2252            // Second finger: transition Idle → TwoFingersDown.
2253            let finger_ids = self.active_touches.first_two_ids().unwrap_or((0, 0));
2254
2255            // Synthesize a Release for the primary finger to clear any
2256            // Flickable grab / delay state.
2257            let primary_pos = self
2258                .primary_touch_id
2259                .and_then(|pid| self.active_touches.get(pid))
2260                .map(|tp| tp.position)
2261                .unwrap_or(position);
2262
2263            // Compute initial geometry for threshold detection.
2264            let (initial_distance, last_angle) =
2265                self.geometry_for(finger_ids).unwrap_or((0.0, euclid::Angle::zero()));
2266            self.gesture_state = GestureRecognitionState::TwoFingersDown {
2267                finger_ids,
2268                initial_distance,
2269                last_angle,
2270            };
2271
2272            events.push(MouseEvent::Released {
2273                position: primary_pos,
2274                button: PointerEventButton::Left,
2275                click_count: 0,
2276                touch_finger_id: id + 1,
2277            });
2278        }
2279        // 3+ fingers: tracked in active_touches but ignored for gesture.
2280    }
2281
2282    #[allow(clippy::collapsible_match)]
2283    fn process_moved(&mut self, id: i32, position: LogicalPoint, events: &mut TouchEventBuffer) {
2284        if let Some(tp) = self.active_touches.get_mut(id) {
2285            tp.position = position;
2286        }
2287
2288        let is_gesture_finger = self.is_gesture_finger(id);
2289
2290        match self.gesture_state {
2291            GestureRecognitionState::Idle => {
2292                if self.primary_touch_id == Some(id) {
2293                    events.push(MouseEvent::Moved { position, touch_finger_id: id + 1 });
2294                }
2295            }
2296            GestureRecognitionState::TwoFingersDown {
2297                finger_ids,
2298                initial_distance,
2299                last_angle,
2300            } if is_gesture_finger => {
2301                if let Some((dist, angle)) = self.gesture_geometry() {
2302                    let delta_dist = (dist - initial_distance).abs();
2303                    let delta_angle = (angle - last_angle).signed().to_degrees().abs();
2304                    if delta_dist > Self::PINCH_THRESHOLD || delta_angle > Self::ROTATION_THRESHOLD
2305                    {
2306                        // Re-snapshot so the first gesture event starts from
2307                        // the current geometry rather than accumulating the
2308                        // threshold movement.
2309                        self.gesture_state = GestureRecognitionState::Pinching {
2310                            finger_ids,
2311                            initial_distance: dist,
2312                            last_scale: 1.0,
2313                            last_angle: angle,
2314                        };
2315
2316                        let midpoint = self.gesture_midpoint().unwrap_or(position);
2317
2318                        events.push(MouseEvent::PinchGesture {
2319                            position: midpoint,
2320                            delta: 0.0,
2321                            phase: TouchPhase::Started,
2322                        });
2323                        events.push(MouseEvent::RotationGesture {
2324                            position: midpoint,
2325                            delta: 0.0,
2326                            phase: TouchPhase::Started,
2327                        });
2328                    }
2329                }
2330            }
2331            GestureRecognitionState::Pinching {
2332                initial_distance, last_scale, last_angle, ..
2333            } if is_gesture_finger => {
2334                if let Some((dist, angle)) = self.gesture_geometry() {
2335                    let midpoint = self.gesture_midpoint().unwrap_or(position);
2336
2337                    let current_scale =
2338                        if initial_distance > 0.0 { dist / initial_distance } else { 1.0 };
2339                    let scale_delta = current_scale - last_scale;
2340
2341                    // `.signed()` wraps to [-pi, pi] so crossing the ±180°
2342                    // atan2 boundary doesn't produce a full-revolution jump.
2343                    let rotation_delta = (angle - last_angle).signed().to_degrees();
2344
2345                    // Update the mutable state for next frame.
2346                    if let GestureRecognitionState::Pinching {
2347                        last_scale: ref mut ls,
2348                        last_angle: ref mut la,
2349                        ..
2350                    } = self.gesture_state
2351                    {
2352                        *ls = current_scale;
2353                        *la = angle;
2354                    }
2355
2356                    events.push(MouseEvent::PinchGesture {
2357                        position: midpoint,
2358                        delta: scale_delta,
2359                        phase: TouchPhase::Moved,
2360                    });
2361                    events.push(MouseEvent::RotationGesture {
2362                        position: midpoint,
2363                        delta: rotation_delta,
2364                        phase: TouchPhase::Moved,
2365                    });
2366                }
2367            }
2368            _ => {}
2369        }
2370    }
2371
2372    #[allow(clippy::collapsible_match)]
2373    fn process_ended(
2374        &mut self,
2375        id: i32,
2376        position: LogicalPoint,
2377        is_cancelled: bool,
2378        events: &mut TouchEventBuffer,
2379    ) {
2380        // Check gesture membership *before* removing from the map.
2381        let is_gesture_finger = self.is_gesture_finger(id);
2382        let midpoint = self.gesture_midpoint().unwrap_or(position);
2383        self.active_touches.remove(id);
2384
2385        match self.gesture_state {
2386            GestureRecognitionState::Idle => {
2387                if self.primary_touch_id == Some(id) {
2388                    self.primary_touch_id = None;
2389                    events.push(MouseEvent::Released {
2390                        position,
2391                        button: PointerEventButton::Left,
2392                        click_count: 0,
2393                        touch_finger_id: id + 1,
2394                    });
2395                    events.push(MouseEvent::Exit);
2396                }
2397            }
2398            GestureRecognitionState::TwoFingersDown { .. } if is_gesture_finger => {
2399                self.gesture_state = GestureRecognitionState::Idle;
2400                if !is_cancelled {
2401                    if let Some(remaining) = self.active_touches.first() {
2402                        let remaining_pos = remaining.position;
2403                        self.primary_touch_id = Some(remaining.id);
2404                        events.push(MouseEvent::Pressed {
2405                            position: remaining_pos,
2406                            button: PointerEventButton::Left,
2407                            click_count: 0,
2408                            touch_finger_id: remaining.id + 1,
2409                        });
2410                    } else {
2411                        self.primary_touch_id = None;
2412                        events.push(MouseEvent::Exit);
2413                    }
2414                } else {
2415                    self.primary_touch_id = None;
2416                    events.push(MouseEvent::Exit);
2417                }
2418            }
2419            GestureRecognitionState::Pinching { .. } if is_gesture_finger => {
2420                self.gesture_state = GestureRecognitionState::Idle;
2421
2422                let gesture_phase =
2423                    if is_cancelled { TouchPhase::Cancelled } else { TouchPhase::Ended };
2424
2425                let remaining = if !is_cancelled {
2426                    self.active_touches.first().map(|tp| (tp.id, tp.position))
2427                } else {
2428                    None
2429                };
2430                if let Some((rid, _)) = remaining {
2431                    self.primary_touch_id = Some(rid);
2432                } else {
2433                    self.primary_touch_id = None;
2434                }
2435
2436                events.push(MouseEvent::PinchGesture {
2437                    position: midpoint,
2438                    delta: 0.0,
2439                    phase: gesture_phase,
2440                });
2441                events.push(MouseEvent::RotationGesture {
2442                    position: midpoint,
2443                    delta: 0.0,
2444                    phase: gesture_phase,
2445                });
2446
2447                if let Some((rid, rpos)) = remaining {
2448                    events.push(MouseEvent::Pressed {
2449                        position: rpos,
2450                        button: PointerEventButton::Left,
2451                        click_count: 0,
2452                        touch_finger_id: rid + 1,
2453                    });
2454                } else {
2455                    events.push(MouseEvent::Exit);
2456                }
2457            }
2458            _ => {}
2459        }
2460    }
2461}
2462
2463#[cfg(test)]
2464mod touch_tests {
2465    extern crate alloc;
2466    use alloc::vec;
2467    use alloc::vec::Vec;
2468
2469    use super::*;
2470    use crate::lengths::LogicalPoint;
2471
2472    fn pt(x: f32, y: f32) -> LogicalPoint {
2473        euclid::point2(x, y)
2474    }
2475
2476    // -----------------------------------------------------------------------
2477    // TouchMap tests
2478    // -----------------------------------------------------------------------
2479
2480    #[test]
2481    fn touch_map_insert_and_get() {
2482        let mut map = TouchMap::default();
2483        assert_eq!(map.len(), 0);
2484        map.insert(TouchPoint { id: 1, position: pt(10.0, 20.0) });
2485        assert_eq!(map.len(), 1);
2486        assert!(map.get(1).is_some());
2487        assert!((map.get(1).unwrap().position.x - 10.0).abs() < f32::EPSILON);
2488        assert!(map.get(2).is_none());
2489    }
2490
2491    #[test]
2492    fn touch_map_update_existing() {
2493        let mut map = TouchMap::default();
2494        map.insert(TouchPoint { id: 1, position: pt(10.0, 20.0) });
2495        map.insert(TouchPoint { id: 1, position: pt(30.0, 40.0) });
2496        assert_eq!(map.len(), 1);
2497        assert!((map.get(1).unwrap().position.x - 30.0).abs() < f32::EPSILON);
2498    }
2499
2500    #[test]
2501    fn touch_map_remove() {
2502        let mut map = TouchMap::default();
2503        map.insert(TouchPoint { id: 1, position: pt(10.0, 20.0) });
2504        map.insert(TouchPoint { id: 2, position: pt(30.0, 40.0) });
2505        assert_eq!(map.len(), 2);
2506        map.remove(1);
2507        assert_eq!(map.len(), 1);
2508        assert!(map.get(1).is_none());
2509        assert!(map.get(2).is_some());
2510    }
2511
2512    #[test]
2513    fn touch_map_remove_nonexistent() {
2514        let mut map = TouchMap::default();
2515        map.insert(TouchPoint { id: 1, position: pt(10.0, 20.0) });
2516        map.remove(99);
2517        assert_eq!(map.len(), 1);
2518    }
2519
2520    #[test]
2521    fn touch_map_capacity() {
2522        let mut map = TouchMap::default();
2523        for i in 0..MAX_TRACKED_TOUCHES {
2524            map.insert(TouchPoint { id: i as i32, position: pt(i as f32, 0.0) });
2525        }
2526        assert_eq!(map.len(), MAX_TRACKED_TOUCHES);
2527        // Inserting beyond capacity is silently ignored.
2528        map.insert(TouchPoint { id: 99, position: pt(99.0, 0.0) });
2529        assert_eq!(map.len(), MAX_TRACKED_TOUCHES);
2530        assert!(map.get(99).is_none());
2531    }
2532
2533    #[test]
2534    fn touch_map_first_two_ids() {
2535        let mut map = TouchMap::default();
2536        assert!(map.first_two_ids().is_none());
2537        map.insert(TouchPoint { id: 5, position: pt(0.0, 0.0) });
2538        assert!(map.first_two_ids().is_none());
2539        map.insert(TouchPoint { id: 10, position: pt(0.0, 0.0) });
2540        assert_eq!(map.first_two_ids(), Some((5, 10)));
2541    }
2542
2543    #[test]
2544    fn touch_map_first() {
2545        let mut map = TouchMap::default();
2546        assert!(map.first().is_none());
2547        map.insert(TouchPoint { id: 7, position: pt(1.0, 2.0) });
2548        let tp = map.first().unwrap();
2549        assert_eq!(tp.id, 7);
2550        assert!((tp.position.x - 1.0).abs() < f32::EPSILON);
2551    }
2552
2553    #[test]
2554    fn touch_map_get_mut() {
2555        let mut map = TouchMap::default();
2556        map.insert(TouchPoint { id: 1, position: pt(0.0, 0.0) });
2557        map.get_mut(1).unwrap().position = pt(5.0, 6.0);
2558        assert!((map.get(1).unwrap().position.x - 5.0).abs() < f32::EPSILON);
2559    }
2560
2561    // -----------------------------------------------------------------------
2562    // Helper: extract event types for readable assertions
2563    // -----------------------------------------------------------------------
2564
2565    #[derive(Debug, PartialEq)]
2566    enum Ev {
2567        Pressed(f32, f32),
2568        Released(f32, f32),
2569        Moved(f32, f32),
2570        Exit,
2571        PinchStarted,
2572        PinchMoved(f32),
2573        PinchEnded,
2574        PinchCancelled,
2575        RotationStarted,
2576        RotationMoved(f32),
2577        RotationEnded,
2578        RotationCancelled,
2579    }
2580
2581    fn classify(events: &TouchEventBuffer) -> Vec<Ev> {
2582        events
2583            .clone()
2584            .into_iter()
2585            .map(|e| match e {
2586                MouseEvent::Pressed { position, .. } => Ev::Pressed(position.x, position.y),
2587                MouseEvent::Released { position, .. } => Ev::Released(position.x, position.y),
2588                MouseEvent::Moved { position, .. } => Ev::Moved(position.x, position.y),
2589                MouseEvent::Exit => Ev::Exit,
2590                MouseEvent::PinchGesture { delta, phase, .. } => match phase {
2591                    TouchPhase::Started => Ev::PinchStarted,
2592                    TouchPhase::Moved => Ev::PinchMoved(delta),
2593                    TouchPhase::Ended => Ev::PinchEnded,
2594                    TouchPhase::Cancelled => Ev::PinchCancelled,
2595                },
2596                MouseEvent::RotationGesture { delta, phase, .. } => match phase {
2597                    TouchPhase::Started => Ev::RotationStarted,
2598                    TouchPhase::Moved => Ev::RotationMoved(delta),
2599                    TouchPhase::Ended => Ev::RotationEnded,
2600                    TouchPhase::Cancelled => Ev::RotationCancelled,
2601                },
2602                _ => panic!("unexpected event: {:?}", e),
2603            })
2604            .collect()
2605    }
2606
2607    // -----------------------------------------------------------------------
2608    // TouchState: single-finger forwarding
2609    // -----------------------------------------------------------------------
2610
2611    #[test]
2612    fn single_finger_press_move_release() {
2613        let mut state = TouchState::default();
2614
2615        let evs = state.process(1, pt(100.0, 200.0), TouchPhase::Started);
2616        assert_eq!(classify(&evs), vec![Ev::Pressed(100.0, 200.0)]);
2617
2618        let evs = state.process(1, pt(110.0, 200.0), TouchPhase::Moved);
2619        assert_eq!(classify(&evs), vec![Ev::Moved(110.0, 200.0)]);
2620
2621        let evs = state.process(1, pt(110.0, 200.0), TouchPhase::Ended);
2622        assert_eq!(classify(&evs), vec![Ev::Released(110.0, 200.0), Ev::Exit]);
2623    }
2624
2625    #[test]
2626    fn single_finger_cancel() {
2627        let mut state = TouchState::default();
2628
2629        state.process(1, pt(100.0, 200.0), TouchPhase::Started);
2630
2631        let evs = state.process(1, pt(100.0, 200.0), TouchPhase::Cancelled);
2632        assert_eq!(classify(&evs), vec![Ev::Released(100.0, 200.0), Ev::Exit]);
2633    }
2634
2635    #[test]
2636    fn non_primary_move_ignored() {
2637        let mut state = TouchState::default();
2638        // Touch 1 is primary.
2639        state.process(1, pt(100.0, 200.0), TouchPhase::Started);
2640
2641        // Move for a different ID that was never started (edge case).
2642        let evs = state.process(99, pt(50.0, 50.0), TouchPhase::Moved);
2643        assert!(classify(&evs).is_empty());
2644    }
2645
2646    // -----------------------------------------------------------------------
2647    // TouchState: two-finger → gesture transition
2648    // -----------------------------------------------------------------------
2649
2650    #[test]
2651    fn two_fingers_synthesize_release_then_gesture() {
2652        let mut state = TouchState::default();
2653
2654        // Finger 1 down.
2655        let evs = state.process(1, pt(100.0, 200.0), TouchPhase::Started);
2656        assert_eq!(classify(&evs), vec![Ev::Pressed(100.0, 200.0)]);
2657
2658        // Finger 2 down → synthesized release for finger 1.
2659        let evs = state.process(2, pt(200.0, 200.0), TouchPhase::Started);
2660        assert_eq!(classify(&evs), vec![Ev::Released(100.0, 200.0)]);
2661        assert!(matches!(state.gesture_state, GestureRecognitionState::TwoFingersDown { .. }));
2662
2663        // Move finger 2 far enough to trigger pinch (> 8px threshold).
2664        let evs = state.process(2, pt(220.0, 200.0), TouchPhase::Moved);
2665        assert_eq!(classify(&evs), vec![Ev::PinchStarted, Ev::RotationStarted]);
2666        assert!(matches!(state.gesture_state, GestureRecognitionState::Pinching { .. }));
2667    }
2668
2669    #[test]
2670    fn two_fingers_below_threshold_no_gesture() {
2671        let mut state = TouchState::default();
2672
2673        state.process(1, pt(100.0, 200.0), TouchPhase::Started);
2674        state.process(2, pt(200.0, 200.0), TouchPhase::Started);
2675
2676        // Small movement within threshold.
2677        let evs = state.process(2, pt(202.0, 200.0), TouchPhase::Moved);
2678        assert!(classify(&evs).is_empty());
2679        assert!(matches!(state.gesture_state, GestureRecognitionState::TwoFingersDown { .. }));
2680    }
2681
2682    #[test]
2683    fn pinch_produces_scale_deltas() {
2684        let mut state = TouchState::default();
2685
2686        // Set up: finger 1 at (0, 0), finger 2 at (100, 0) → distance = 100.
2687        state.process(1, pt(0.0, 0.0), TouchPhase::Started);
2688        state.process(2, pt(100.0, 0.0), TouchPhase::Started);
2689
2690        // Move finger 2 to (120, 0) to exceed threshold and start pinching.
2691        state.process(2, pt(120.0, 0.0), TouchPhase::Moved);
2692        assert!(matches!(state.gesture_state, GestureRecognitionState::Pinching { .. }));
2693
2694        // Now move finger 2 further to (180, 0).
2695        // New distance = 180, initial distance (re-snapshotted) = 120.
2696        // Scale = 180/120 = 1.5, delta = 1.5 - 1.0 = 0.5.
2697        let evs = state.process(2, pt(180.0, 0.0), TouchPhase::Moved);
2698        let classified = classify(&evs);
2699        assert_eq!(classified.len(), 2);
2700        if let Ev::PinchMoved(delta) = classified[0] {
2701            assert!((delta - 0.5).abs() < 0.01, "expected ~0.5, got {}", delta);
2702        } else {
2703            panic!("expected PinchMoved, got {:?}", classified[0]);
2704        }
2705    }
2706
2707    #[test]
2708    fn rotation_produces_correct_deltas() {
2709        let mut state = TouchState::default();
2710
2711        // Finger 1 at origin, finger 2 on the X axis at (100, 0).
2712        // Initial angle = atan2(0, 100) = 0°.
2713        state.process(1, pt(0.0, 0.0), TouchPhase::Started);
2714        state.process(2, pt(100.0, 0.0), TouchPhase::Started);
2715
2716        // Move finger 2 far enough to trigger gesture.
2717        state.process(2, pt(120.0, 0.0), TouchPhase::Moved);
2718        assert!(matches!(state.gesture_state, GestureRecognitionState::Pinching { .. }));
2719
2720        // Rotate ~45° clockwise: move finger 2 from (120, 0) to roughly
2721        // (70.7, 70.7) which is at 45° from origin.
2722        // atan2(70.7, 70.7) ≈ 45°. Delta from re-snapshotted 0° = +45°.
2723        // Slint convention: positive = clockwise → delta ≈ +45°.
2724        let evs = state.process(2, pt(70.7, 70.7), TouchPhase::Moved);
2725        let classified = classify(&evs);
2726        assert_eq!(classified.len(), 2);
2727        if let Ev::RotationMoved(delta) = classified[1] {
2728            assert!((delta - 45.0).abs() < 1.0, "expected ~45.0 (clockwise), got {}", delta);
2729        } else {
2730            panic!("expected RotationMoved, got {:?}", classified[1]);
2731        }
2732    }
2733
2734    #[test]
2735    fn rotation_across_180_degree_boundary() {
2736        let mut state = TouchState::default();
2737
2738        // Finger 1 at origin, finger 2 at (-100, -10).
2739        // angle = atan2(-10, -100) ≈ -174.3°.
2740        state.process(1, pt(0.0, 0.0), TouchPhase::Started);
2741        state.process(2, pt(-100.0, -10.0), TouchPhase::Started);
2742
2743        // Trigger gesture by moving far enough.
2744        state.process(2, pt(-120.0, -10.0), TouchPhase::Moved);
2745        assert!(matches!(state.gesture_state, GestureRecognitionState::Pinching { .. }));
2746
2747        // Rotate across the ±180° boundary: move finger 2 to (-100, 10).
2748        // New angle = atan2(10, -100) ≈ 174.3°.
2749        // Raw angular change crosses ±180°, but per-frame delta should be
2750        // small (~11.4° which is 2 * 5.7°), NOT a ~349° jump.
2751        let evs = state.process(2, pt(-100.0, 10.0), TouchPhase::Moved);
2752        let classified = classify(&evs);
2753        if let Ev::RotationMoved(delta) = classified[1] {
2754            assert!(
2755                delta.abs() < 20.0,
2756                "rotation should be a small delta (~11°), got {} (discontinuity!)",
2757                delta
2758            );
2759        } else {
2760            panic!("expected RotationMoved, got {:?}", classified[1]);
2761        }
2762    }
2763
2764    // -----------------------------------------------------------------------
2765    // TouchState: gesture end transitions
2766    // -----------------------------------------------------------------------
2767
2768    #[test]
2769    fn pinch_end_with_remaining_finger() {
2770        let mut state = TouchState::default();
2771
2772        state.process(1, pt(0.0, 0.0), TouchPhase::Started);
2773        state.process(2, pt(100.0, 0.0), TouchPhase::Started);
2774        // Trigger pinch.
2775        state.process(2, pt(120.0, 0.0), TouchPhase::Moved);
2776
2777        // Lift finger 2 → gesture ends, finger 1 gets re-pressed.
2778        let evs = state.process(2, pt(120.0, 0.0), TouchPhase::Ended);
2779        let classified = classify(&evs);
2780        assert_eq!(classified, vec![Ev::PinchEnded, Ev::RotationEnded, Ev::Pressed(0.0, 0.0)]);
2781        assert!(matches!(state.gesture_state, GestureRecognitionState::Idle));
2782        assert_eq!(state.primary_touch_id, Some(1));
2783    }
2784
2785    #[test]
2786    fn pinch_cancel_emits_cancelled_and_exit() {
2787        let mut state = TouchState::default();
2788
2789        state.process(1, pt(0.0, 0.0), TouchPhase::Started);
2790        state.process(2, pt(100.0, 0.0), TouchPhase::Started);
2791        state.process(2, pt(120.0, 0.0), TouchPhase::Moved);
2792
2793        // Cancel finger 2.
2794        let evs = state.process(2, pt(120.0, 0.0), TouchPhase::Cancelled);
2795        let classified = classify(&evs);
2796        assert_eq!(classified, vec![Ev::PinchCancelled, Ev::RotationCancelled, Ev::Exit]);
2797        assert!(state.primary_touch_id.is_none());
2798    }
2799
2800    #[test]
2801    fn two_fingers_down_lift_before_threshold_returns_to_idle() {
2802        let mut state = TouchState::default();
2803
2804        state.process(1, pt(100.0, 200.0), TouchPhase::Started);
2805        state.process(2, pt(200.0, 200.0), TouchPhase::Started);
2806        assert!(matches!(state.gesture_state, GestureRecognitionState::TwoFingersDown { .. }));
2807
2808        // Lift finger 2 without exceeding movement threshold.
2809        let evs = state.process(2, pt(200.0, 200.0), TouchPhase::Ended);
2810        let classified = classify(&evs);
2811        // Remaining finger 1 gets re-pressed.
2812        assert_eq!(classified, vec![Ev::Pressed(100.0, 200.0)]);
2813        assert!(matches!(state.gesture_state, GestureRecognitionState::Idle));
2814        assert_eq!(state.primary_touch_id, Some(1));
2815    }
2816
2817    #[test]
2818    fn two_fingers_down_cancel_both_emits_exit() {
2819        let mut state = TouchState::default();
2820
2821        state.process(1, pt(100.0, 200.0), TouchPhase::Started);
2822        state.process(2, pt(200.0, 200.0), TouchPhase::Started);
2823
2824        // Cancel finger 2 (gesture finger, no remaining → Exit).
2825        let evs = state.process(2, pt(200.0, 200.0), TouchPhase::Cancelled);
2826        assert_eq!(classify(&evs), vec![Ev::Exit]);
2827
2828        // Cancel finger 1 (now in Idle, but not primary since cancel cleared it).
2829        let evs = state.process(1, pt(100.0, 200.0), TouchPhase::Cancelled);
2830        assert!(classify(&evs).is_empty());
2831    }
2832
2833    // -----------------------------------------------------------------------
2834    // TouchState: 3+ fingers
2835    // -----------------------------------------------------------------------
2836
2837    #[test]
2838    fn third_finger_ignored_for_gesture() {
2839        let mut state = TouchState::default();
2840
2841        state.process(1, pt(0.0, 0.0), TouchPhase::Started);
2842        state.process(2, pt(100.0, 0.0), TouchPhase::Started);
2843
2844        // Third finger: no additional events.
2845        let evs = state.process(3, pt(50.0, 50.0), TouchPhase::Started);
2846        assert!(classify(&evs).is_empty());
2847        assert_eq!(state.active_touches.len(), 3);
2848    }
2849
2850    // -----------------------------------------------------------------------
2851    // Angle wrapping via Euclid
2852    // -----------------------------------------------------------------------
2853
2854    #[test]
2855    fn euclid_angle_signed_wrapping() {
2856        use euclid::Angle;
2857        let wrap = |deg: f32| Angle::degrees(deg).signed().to_degrees();
2858        assert!(wrap(0.0).abs() < f32::EPSILON);
2859        assert!((wrap(180.0) - 180.0).abs() < 0.01);
2860        assert!((wrap(181.0) - (-179.0)).abs() < 0.01);
2861        assert!((wrap(-181.0) - 179.0).abs() < 0.01);
2862        assert!(wrap(360.0).abs() < 0.01);
2863    }
2864
2865    #[test]
2866    fn zero_distance_fingers_no_division_by_zero() {
2867        let mut state = TouchState::default();
2868
2869        // Two fingers at the exact same position → distance = 0.
2870        state.process(1, pt(100.0, 100.0), TouchPhase::Started);
2871        state.process(2, pt(100.0, 100.0), TouchPhase::Started);
2872        assert!(matches!(state.gesture_state, GestureRecognitionState::TwoFingersDown { .. }));
2873
2874        // Move one finger far enough to trigger gesture.
2875        let evs = state.process(2, pt(120.0, 100.0), TouchPhase::Moved);
2876        assert!(matches!(state.gesture_state, GestureRecognitionState::Pinching { .. }));
2877        let classified = classify(&evs);
2878        assert_eq!(classified.len(), 2);
2879        assert_eq!(classified[0], Ev::PinchStarted);
2880
2881        // Move further — scale should not be inf/NaN despite initial_distance
2882        // having been 0 (re-snapshotted to 20.0 at threshold crossing).
2883        let evs = state.process(2, pt(140.0, 100.0), TouchPhase::Moved);
2884        let classified = classify(&evs);
2885        if let Ev::PinchMoved(delta) = classified[0] {
2886            assert!(delta.is_finite(), "scale delta should be finite, got {}", delta);
2887        } else {
2888            panic!("expected PinchMoved, got {:?}", classified[0]);
2889        }
2890    }
2891}
2892
2893#[cfg(test)]
2894mod tests {
2895    use super::*;
2896    extern crate alloc;
2897
2898    #[test]
2899    fn test_to_string() {
2900        let test_cases = [
2901            (
2902                "a",
2903                KeyboardModifiers { alt: false, control: true, shift: false, meta: false },
2904                false,
2905                false,
2906                "⌘A",
2907                "Ctrl+A",
2908                "Ctrl+A",
2909            ),
2910            (
2911                "a",
2912                KeyboardModifiers { alt: true, control: true, shift: true, meta: true },
2913                false,
2914                false,
2915                "⌃⌥⇧⌘A",
2916                "Win+Ctrl+Alt+Shift+A",
2917                "Super+Ctrl+Alt+Shift+A",
2918            ),
2919            (
2920                "\u{001b}",
2921                KeyboardModifiers { alt: false, control: true, shift: true, meta: false },
2922                false,
2923                false,
2924                "⇧⌘Escape",
2925                "Ctrl+Shift+Escape",
2926                "Ctrl+Shift+Escape",
2927            ),
2928            (
2929                "+",
2930                KeyboardModifiers { alt: false, control: true, shift: false, meta: false },
2931                true,
2932                false,
2933                "⌘+",
2934                "Ctrl++",
2935                "Ctrl++",
2936            ),
2937            (
2938                "a",
2939                KeyboardModifiers { alt: true, control: true, shift: false, meta: false },
2940                false,
2941                true,
2942                "⌘A",
2943                "Ctrl+A",
2944                "Ctrl+A",
2945            ),
2946            (
2947                "",
2948                KeyboardModifiers { alt: false, control: true, shift: false, meta: false },
2949                false,
2950                false,
2951                "",
2952                "",
2953                "",
2954            ),
2955            (
2956                "\u{000a}",
2957                KeyboardModifiers { alt: false, control: false, shift: false, meta: false },
2958                false,
2959                false,
2960                "Return",
2961                "Return",
2962                "Return",
2963            ),
2964            (
2965                "\u{0009}",
2966                KeyboardModifiers { alt: false, control: false, shift: false, meta: false },
2967                false,
2968                false,
2969                "Tab",
2970                "Tab",
2971                "Tab",
2972            ),
2973            (
2974                "\u{0020}",
2975                KeyboardModifiers { alt: false, control: false, shift: false, meta: false },
2976                false,
2977                false,
2978                "Space",
2979                "Space",
2980                "Space",
2981            ),
2982            (
2983                "\u{0008}",
2984                KeyboardModifiers { alt: false, control: false, shift: false, meta: false },
2985                false,
2986                false,
2987                "Backspace",
2988                "Backspace",
2989                "Backspace",
2990            ),
2991        ];
2992
2993        for (
2994            key,
2995            modifiers,
2996            ignore_shift,
2997            ignore_alt,
2998            _expected_macos,
2999            _expected_windows,
3000            _expected_linux,
3001        ) in test_cases
3002        {
3003            let shortcut = make_keys(key.into(), modifiers, ignore_shift, ignore_alt);
3004
3005            use crate::alloc::string::ToString;
3006            let result = shortcut.to_string();
3007
3008            #[cfg(target_os = "macos")]
3009            assert_eq!(result.as_str(), _expected_macos, "Failed for key: {:?}", key);
3010
3011            #[cfg(target_os = "windows")]
3012            assert_eq!(result.as_str(), _expected_windows, "Failed for key: {:?}", key);
3013
3014            #[cfg(not(any(target_os = "macos", target_os = "windows")))]
3015            assert_eq!(result.as_str(), _expected_linux, "Failed for key: {:?}", key);
3016        }
3017    }
3018
3019    #[test]
3020    fn test_from_parts_valid() {
3021        let f5_key = alloc::string::String::from(char::from(key_codes::Key::F5));
3022        let ret_key = alloc::string::String::from(char::from(key_codes::Key::Return));
3023        let pause_key = alloc::string::String::from(char::from(key_codes::Key::Pause));
3024
3025        // (description, input parts, expected key, modifiers, ignore_shift, ignore_alt)
3026        let cases: &[(&str, &[&str], &str, KeyboardModifiers, bool, bool)] = &[
3027            (
3028                "Control+A",
3029                &["Control", "A"],
3030                "a",
3031                KeyboardModifiers { control: true, ..Default::default() },
3032                false,
3033                false,
3034            ),
3035            (
3036                "Control+Shift+A",
3037                &["Control", "Shift", "A"],
3038                "a",
3039                KeyboardModifiers { control: true, shift: true, ..Default::default() },
3040                false,
3041                false,
3042            ),
3043            (
3044                "Control+Shift?+Z (explicit ignore_shift)",
3045                &["Control", "Shift?", "Z"],
3046                "z",
3047                KeyboardModifiers { control: true, ..Default::default() },
3048                true,
3049                false,
3050            ),
3051            (
3052                "Control+Alt?+A (ignore_alt)",
3053                &["Control", "Alt?", "A"],
3054                "a",
3055                KeyboardModifiers { control: true, ..Default::default() },
3056                false,
3057                true,
3058            ),
3059            (
3060                "F5 alone (special key)",
3061                &["F5"],
3062                &f5_key,
3063                KeyboardModifiers::default(),
3064                false,
3065                false,
3066            ),
3067            ("Return key", &["Return"], &ret_key, KeyboardModifiers::default(), false, false),
3068            (
3069                "Control+Plus (LocalizedShiftable → auto ignore_shift)",
3070                &["Control", "Plus"],
3071                "+",
3072                KeyboardModifiers { control: true, ..Default::default() },
3073                true,
3074                false,
3075            ),
3076            (
3077                "Control+'+' (literal, no auto ignore_shift)",
3078                &["Control", "+"],
3079                "+",
3080                KeyboardModifiers { control: true, ..Default::default() },
3081                false,
3082                false,
3083            ),
3084            (
3085                "Control+Shift+Alt+A (all modifiers)",
3086                &["Control", "Shift", "Alt", "A"],
3087                "a",
3088                KeyboardModifiers { control: true, shift: true, alt: true, ..Default::default() },
3089                false,
3090                false,
3091            ),
3092            ("empty input → Keys::default()", &[], "", KeyboardModifiers::default(), false, false),
3093            (
3094                "Control+€ (unicode literal)",
3095                &["Control", "€"],
3096                "€",
3097                KeyboardModifiers { control: true, ..Default::default() },
3098                false,
3099                false,
3100            ),
3101            (
3102                "Control+é (lowercase literal)",
3103                &["Control", "é"],
3104                "é",
3105                KeyboardModifiers { control: true, ..Default::default() },
3106                false,
3107                false,
3108            ),
3109            ("A alone (named key)", &["A"], "a", KeyboardModifiers::default(), false, false),
3110            // The special keys are represented by reserved unicode codepoints. Passing
3111            // one of those characters as a literal must produce the same `Keys` as its
3112            // name, so `to_parts` output stays acceptable to `from_parts`.
3113            (
3114                "F5 codepoint literal",
3115                &[&f5_key],
3116                &f5_key,
3117                KeyboardModifiers::default(),
3118                false,
3119                false,
3120            ),
3121            (
3122                "Pause codepoint literal",
3123                &[&pause_key],
3124                &pause_key,
3125                KeyboardModifiers::default(),
3126                false,
3127                false,
3128            ),
3129            (
3130                "Control + F5 codepoint literal",
3131                &["Control", &f5_key],
3132                &f5_key,
3133                KeyboardModifiers { control: true, ..Default::default() },
3134                false,
3135                false,
3136            ),
3137            // Whitespace is significant: these literals name the Space/Tab/Return keys
3138            // and must agree with their named spellings (parts are not trimmed).
3139            ("\" \" literal → Space", &[" "], " ", KeyboardModifiers::default(), false, false),
3140            ("Space named", &["Space"], " ", KeyboardModifiers::default(), false, false),
3141            ("\"\\t\" literal → Tab", &["\t"], "\t", KeyboardModifiers::default(), false, false),
3142            ("Tab named", &["Tab"], "\t", KeyboardModifiers::default(), false, false),
3143            (
3144                "\"\\n\" literal → Return",
3145                &["\n"],
3146                &ret_key,
3147                KeyboardModifiers::default(),
3148                false,
3149                false,
3150            ),
3151            (
3152                "Control+\" \" (literal space with a modifier)",
3153                &["Control", " "],
3154                " ",
3155                KeyboardModifiers { control: true, ..Default::default() },
3156                false,
3157                false,
3158            ),
3159            (
3160                "empty part is skipped → Keys::default()",
3161                &[""],
3162                "",
3163                KeyboardModifiers::default(),
3164                false,
3165                false,
3166            ),
3167            (
3168                "a alone (literal fallback, same result as named A)",
3169                &["a"],
3170                "a",
3171                KeyboardModifiers::default(),
3172                false,
3173                false,
3174            ),
3175        ];
3176
3177        for (desc, parts, expected_key, mods, is, ia) in cases {
3178            let result =
3179                Keys::from_parts(parts.iter().copied()).unwrap_or_else(|e| panic!("{desc}: {e}"));
3180            assert_eq!(result, make_keys((*expected_key).into(), *mods, *is, *ia), "{desc}");
3181        }
3182    }
3183
3184    #[test]
3185    fn test_from_parts_invalid() {
3186        use super::KeysParseErrorInner;
3187        let cases: &[(&str, &[&str], KeysParseError)] = &[
3188            // Case-sensitive modifiers: unrecognized modifier parses as a second key
3189            ("lowercase 'control'", &["control", "A"], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3190            ("uppercase 'CONTROL'", &["CONTROL", "A"], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3191            ("'Ctrl' alias", &["Ctrl", "A"], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3192            ("'ctrl' alias", &["ctrl", "A"], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3193            // Meta aliases not accepted
3194            ("'Win' alias", &["Win", "A"], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3195            ("'Super' alias", &["Super", "A"], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3196            // No key
3197            ("modifiers only", &["Control", "Shift"], KeysParseError(KeysParseErrorInner::NoKey)),
3198            // Multiple keys
3199            ("two keys", &["A", "B"], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3200            // Multi-grapheme cluster
3201            (
3202                "multi-char unknown",
3203                &["Control", "Foobar"],
3204                KeysParseError(KeysParseErrorInner::MultipleGraphemeClusters("Foobar".into())),
3205            ),
3206            (
3207                "two-char literal",
3208                &["Control", "ab"],
3209                KeysParseError(KeysParseErrorInner::MultipleGraphemeClusters("ab".into())),
3210            ),
3211            // Parts are not trimmed, so a padded modifier is no longer a modifier: it
3212            // falls through to the key branch and fails as a multi-grapheme literal.
3213            (
3214                "padded modifier ' Control ' alone",
3215                &[" Control "],
3216                KeysParseError(KeysParseErrorInner::MultipleGraphemeClusters(" Control ".into())),
3217            ),
3218            (
3219                "padded modifier ' Control ' is a key, so 'A' is a second key",
3220                &[" Control ", "A"],
3221                KeysParseError(KeysParseErrorInner::MultipleKeys),
3222            ),
3223            (
3224                "padded key ' A '",
3225                &["Control", " A "],
3226                KeysParseError(KeysParseErrorInner::MultipleGraphemeClusters(" A ".into())),
3227            ),
3228            // Two whitespace literals are two keys, not one trimmed-away key.
3229            ("two space literals", &[" ", " "], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3230            (
3231                "lowercase 'return' (not a named key)",
3232                &["return"],
3233                KeysParseError(KeysParseErrorInner::MultipleGraphemeClusters("return".into())),
3234            ),
3235            // Not lowercase
3236            (
3237                "uppercase literal É",
3238                &["Control", "É"],
3239                KeysParseError(KeysParseErrorInner::NotLowercase("É".into())),
3240            ),
3241            // Incompatible modifiers
3242            (
3243                "Shift + Shift?",
3244                &["Shift", "Shift?", "A"],
3245                KeysParseError(KeysParseErrorInner::IncompatibleModifiers("Shift and Shift? cannot be combined".into())),
3246            ),
3247            (
3248                "Alt + Alt?",
3249                &["Alt", "Alt?", "A"],
3250                KeysParseError(KeysParseErrorInner::IncompatibleModifiers("Alt and Alt? cannot be combined".into())),
3251            ),
3252            (
3253                "Shift + LocalizedShiftable key (Plus)",
3254                &["Control", "Shift", "Plus"],
3255                KeysParseError(KeysParseErrorInner::IncompatibleModifiers(
3256                    "Key bindings involving Plus ignore Shift to support different keyboard layouts; remove Shift".into(),
3257                )),
3258            ),
3259        ];
3260
3261        for (desc, parts, expected_err) in cases {
3262            let result = Keys::from_parts(parts.iter().copied());
3263            assert!(result.is_err(), "{desc}: expected error, got {result:?}");
3264            assert_eq!(&result.unwrap_err(), expected_err, "{desc}");
3265        }
3266    }
3267
3268    #[test]
3269    fn test_to_parts_roundtrip() {
3270        // Inputs that should round-trip identically through from_parts → to_parts.
3271        let inputs: &[&[&str]] = &[
3272            &[],
3273            &["A"],
3274            &["Control", "A"],
3275            &["Control", "Shift", "A"],
3276            &["Control", "Shift?", "Z"],
3277            &["Control", "Alt?", "A"],
3278            &["Control", "Alt", "Shift", "Meta", "A"],
3279            &["Meta", "Control", "Alt", "Shift", "A"],
3280            &["F5"],
3281            &["Return"],
3282            &["Space"],
3283            &[" "],  // literal space: to_parts emits the "Space" name
3284            &["\t"], // literal tab: to_parts emits the "Tab" name
3285            &["\n"], // literal newline: to_parts emits the "Return" name
3286            &["Control", " "],
3287            &["Control", "Plus"], // LocalizedShiftable: desugars to ["Control", "Shift?", "+"]
3288            &["Control", "+"],    // literal '+': stays ["Control", "+"] (no auto ignore_shift)
3289            &["Control", "Digit0"],
3290            &["Control", "€"],
3291            &["Control", "é"],
3292        ];
3293        for parts in inputs {
3294            let k = Keys::from_parts(parts.iter().copied()).unwrap();
3295            let out_strs: alloc::vec::Vec<&str> = k.to_parts().collect();
3296            let k2 = Keys::from_parts(out_strs.iter().copied()).unwrap();
3297            assert_eq!(k, k2, "round-trip mismatch for {parts:?} → {out_strs:?}");
3298        }
3299    }
3300
3301    #[test]
3302    fn test_to_parts_canonical_form() {
3303        // Spot-check the exact strings to lock in the canonical output. Modifiers keep
3304        // their names; the key is always the stored character, never a key name.
3305        let f5 = alloc::string::String::from(char::from(key_codes::Key::F5));
3306        let ret = alloc::string::String::from(char::from(key_codes::Key::Return));
3307        let pause = alloc::string::String::from(char::from(key_codes::Key::Pause));
3308        let cases: &[(&[&str], &[&str])] = &[
3309            (&[], &[]),
3310            (&["A"], &["a"]), // named key → its stored character
3311            (&["a"], &["a"]),
3312            (&["Control", "S"], &["Control", "s"]),
3313            (&["Control", "Shift?", "Z"], &["Control", "Shift?", "z"]),
3314            (&["Control", "Alt?", "A"], &["Control", "Alt?", "a"]),
3315            (&["F5"], &[&f5]),
3316            (&[&f5], &[&f5]), // reserved codepoint as a literal
3317            (&["Pause"], &[&pause]),
3318            (&[&pause], &[&pause]),
3319            // Whitespace and control characters are emitted raw, which is why a text
3320            // format storing these has to escape them (see the runtime_key_bindings
3321            // example). They still round-trip, because from_parts does not trim.
3322            (&[" "], &[" "]),
3323            (&["Space"], &[" "]),
3324            (&["\t"], &["\t"]),
3325            (&["Tab"], &["\t"]),
3326            (&["\n"], &[&ret]),
3327            (&["Return"], &[&ret]),
3328            (&["Control", " "], &["Control", " "]),
3329            // LocalizedShiftable: the auto ignore_shift is surfaced as Shift? and the
3330            // raw character is emitted, so the "Plus" name is not reproduced.
3331            (&["Control", "Plus"], &["Control", "Shift?", "+"]),
3332            (&["Control", "+"], &["Control", "+"]), // literal '+': no auto ignore_shift
3333            (&["Control", "€"], &["Control", "€"]),
3334            (&["Meta", "Control", "Alt", "Shift", "A"], &["Meta", "Control", "Alt", "Shift", "a"]),
3335        ];
3336        for (input, expected) in cases {
3337            let k = Keys::from_parts(input.iter().copied()).unwrap();
3338            let out_strs: alloc::vec::Vec<&str> = k.to_parts().collect();
3339            assert_eq!(&out_strs.as_slice(), expected, "for input {input:?}");
3340        }
3341    }
3342
3343    #[test]
3344    fn test_from_parts_matching() {
3345        // (description, input parts, event text, event modifiers, should_match)
3346        let cases: &[(&str, &[&str], &str, KeyboardModifiers, bool)] = &[
3347            (
3348                "Control+A matches",
3349                &["Control", "A"],
3350                "a",
3351                KeyboardModifiers { control: true, ..Default::default() },
3352                true,
3353            ),
3354            (
3355                "Control+A wrong key",
3356                &["Control", "A"],
3357                "b",
3358                KeyboardModifiers { control: true, ..Default::default() },
3359                false,
3360            ),
3361            (
3362                "Control+A wrong modifier",
3363                &["Control", "A"],
3364                "a",
3365                KeyboardModifiers { alt: true, ..Default::default() },
3366                false,
3367            ),
3368            (
3369                "Shift? matches with shift",
3370                &["Control", "Shift?", "Z"],
3371                "z",
3372                KeyboardModifiers { control: true, shift: true, ..Default::default() },
3373                true,
3374            ),
3375            (
3376                "Shift? matches without shift",
3377                &["Control", "Shift?", "Z"],
3378                "z",
3379                KeyboardModifiers { control: true, ..Default::default() },
3380                true,
3381            ),
3382        ];
3383
3384        for (desc, parts, text, mods, expected) in cases {
3385            let k =
3386                Keys::from_parts(parts.iter().copied()).unwrap_or_else(|e| panic!("{desc}: {e}"));
3387            let event = KeyEvent { text: (*text).into(), modifiers: *mods, ..Default::default() };
3388            assert_eq!(k.matches(&event), *expected, "{desc}");
3389        }
3390
3391        // Special key matching: Return
3392        let return_char: char = key_codes::Key::Return.into();
3393        let k = Keys::from_parts(["Return"]).unwrap();
3394        let event = KeyEvent {
3395            text: SharedString::from(alloc::string::String::from(return_char)),
3396            modifiers: KeyboardModifiers::default(),
3397            ..Default::default()
3398        };
3399        assert!(k.matches(&event), "Return key should match Return event");
3400    }
3401}