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