1#![warn(missing_docs)]
8
9use crate::cursor::MouseCursorInner;
10use crate::item_tree::ItemTreeRc;
11use crate::item_tree::{ItemRc, ItemWeak, VisitChildrenResult};
12use crate::items::{
13 AllowedDragActions, BuiltInMouseCursor, DropEvent, ItemRef, OperatingSystemType,
14 TextCursorDirection,
15};
16pub use crate::items::{FocusReason, KeyEvent, KeyboardModifiers, PointerEventButton};
17use crate::lengths::{ItemTransform, LogicalPoint, LogicalVector};
18use crate::window::{WindowAdapter, WindowInner};
19use crate::{Coord, Property, SharedString};
20use alloc::rc::Rc;
21use alloc::vec::Vec;
22use const_field_offset::FieldOffsets;
23use core::cell::Cell;
24use core::fmt::Display;
25use core::pin::Pin;
26use core::time::Duration;
27
28#[repr(C)]
33#[derive(Debug, Clone, PartialEq)]
34pub enum MouseEvent {
35 Pressed {
37 position: LogicalPoint,
39 button: PointerEventButton,
41 click_count: u8,
43 touch_finger_id: i32,
45 },
46 Released {
48 position: LogicalPoint,
50 button: PointerEventButton,
52 click_count: u8,
54 touch_finger_id: i32,
56 },
57 Moved {
59 position: LogicalPoint,
61 touch_finger_id: i32,
63 },
64 Wheel {
66 position: LogicalPoint,
68 delta_x: Coord,
70 delta_y: Coord,
72 phase: TouchPhase,
74 },
75 DragMove {
79 event: DropEvent,
81 allowed: AllowedDragActions,
83 },
84 Drop {
86 event: DropEvent,
88 allowed: AllowedDragActions,
90 },
91 PinchGesture {
93 position: LogicalPoint,
95 delta: f32,
97 phase: TouchPhase,
99 },
100 RotationGesture {
102 position: LogicalPoint,
104 delta: f32,
106 phase: TouchPhase,
108 },
109 Exit,
111}
112
113impl MouseEvent {
114 pub fn touch_finger_id(&self) -> i32 {
116 match self {
117 MouseEvent::Pressed { touch_finger_id, .. } => *touch_finger_id,
118 MouseEvent::Released { touch_finger_id, .. } => *touch_finger_id,
119 MouseEvent::Moved { touch_finger_id, .. } => *touch_finger_id,
120 _ => 0,
121 }
122 }
123
124 pub fn is_from_touch(&self) -> bool {
126 self.touch_finger_id() != 0
128 }
129
130 pub fn position(&self) -> Option<LogicalPoint> {
132 match self {
133 MouseEvent::Pressed { position, .. } => Some(*position),
134 MouseEvent::Released { position, .. } => Some(*position),
135 MouseEvent::Moved { position, .. } => Some(*position),
136 MouseEvent::Wheel { position, .. } => Some(*position),
137 MouseEvent::PinchGesture { position, .. } => Some(*position),
138 MouseEvent::RotationGesture { position, .. } => Some(*position),
139 MouseEvent::DragMove { event: e, .. } | MouseEvent::Drop { event: e, .. } => {
140 Some(crate::lengths::logical_point_from_api(e.position))
141 }
142 MouseEvent::Exit => None,
143 }
144 }
145
146 pub fn translate(&mut self, vec: LogicalVector) {
148 let pos = match self {
149 MouseEvent::Pressed { position, .. } => Some(position),
150 MouseEvent::Released { position, .. } => Some(position),
151 MouseEvent::Moved { position, .. } => Some(position),
152 MouseEvent::Wheel { position, .. } => Some(position),
153 MouseEvent::PinchGesture { position, .. } => Some(position),
154 MouseEvent::RotationGesture { position, .. } => Some(position),
155 MouseEvent::DragMove { event: e, .. } | MouseEvent::Drop { event: e, .. } => {
156 e.position = crate::api::LogicalPosition::from_euclid(
157 crate::lengths::logical_point_from_api(e.position) + vec,
158 );
159 None
160 }
161 MouseEvent::Exit => None,
162 };
163 if let Some(pos) = pos {
164 *pos += vec;
165 }
166 }
167
168 pub fn transform(&mut self, transform: ItemTransform) {
170 let pos = match self {
171 MouseEvent::Pressed { position, .. } => Some(position),
172 MouseEvent::Released { position, .. } => Some(position),
173 MouseEvent::Moved { position, .. } => Some(position),
174 MouseEvent::Wheel { position, .. } => Some(position),
175 MouseEvent::PinchGesture { position, .. } => Some(position),
176 MouseEvent::RotationGesture { position, .. } => Some(position),
177 MouseEvent::DragMove { event: e, .. } | MouseEvent::Drop { event: e, .. } => {
178 e.position = crate::api::LogicalPosition::from_euclid(
179 transform
180 .transform_point(crate::lengths::logical_point_from_api(e.position).cast())
181 .cast(),
182 );
183 None
184 }
185 MouseEvent::Exit => None,
186 };
187 if let Some(pos) = pos {
188 *pos = transform.transform_point(pos.cast()).cast();
189 }
190 }
191
192 fn set_click_count(&mut self, count: u8) {
194 match self {
195 MouseEvent::Pressed { click_count, .. } | MouseEvent::Released { click_count, .. } => {
196 *click_count = count
197 }
198 _ => (),
199 }
200 }
201}
202
203#[allow(missing_docs)]
205#[repr(C)]
206#[derive(Debug, Clone, Copy, PartialEq)]
207pub enum BackendMouseEvent {
208 Pressed {
210 position: LogicalPoint,
211 button: PointerEventButton,
212 click_count: u8,
213 touch_finger_id: i32,
214 },
215 Released {
217 position: LogicalPoint,
218 button: PointerEventButton,
219 click_count: u8,
220 touch_finger_id: i32,
221 },
222 Moved { position: LogicalPoint, touch_finger_id: i32 },
224 Wheel { position: LogicalPoint, delta_x: Coord, delta_y: Coord, phase: TouchPhase },
226 PinchGesture { position: LogicalPoint, delta: f32, phase: TouchPhase },
228 RotationGesture { position: LogicalPoint, delta: f32, phase: TouchPhase },
230 Exit,
232}
233
234impl From<BackendMouseEvent> for MouseEvent {
235 fn from(event: BackendMouseEvent) -> Self {
236 match event {
237 BackendMouseEvent::Pressed { position, button, click_count, touch_finger_id } => {
238 Self::Pressed { position, button, click_count, touch_finger_id }
239 }
240 BackendMouseEvent::Released { position, button, click_count, touch_finger_id } => {
241 Self::Released { position, button, click_count, touch_finger_id }
242 }
243 BackendMouseEvent::Moved { position, touch_finger_id } => {
244 Self::Moved { position, touch_finger_id }
245 }
246 BackendMouseEvent::Wheel { position, delta_x, delta_y, phase } => {
247 Self::Wheel { position, delta_x, delta_y, phase }
248 }
249 BackendMouseEvent::PinchGesture { position, delta, phase } => {
250 Self::PinchGesture { position, delta, phase }
251 }
252 BackendMouseEvent::RotationGesture { position, delta, phase } => {
253 Self::RotationGesture { position, delta, phase }
254 }
255 BackendMouseEvent::Exit => Self::Exit,
256 }
257 }
258}
259
260#[allow(missing_docs)]
262#[derive(Debug, Clone, PartialEq)]
263pub enum BackendDragEvent {
264 Move { event: DropEvent, allowed: AllowedDragActions },
266 Drop { event: DropEvent, allowed: AllowedDragActions },
268 Leave,
270}
271
272impl From<BackendDragEvent> for MouseEvent {
273 fn from(event: BackendDragEvent) -> Self {
274 match event {
275 BackendDragEvent::Move { event, allowed } => Self::DragMove { event, allowed },
276 BackendDragEvent::Drop { event, allowed } => Self::Drop { event, allowed },
277 BackendDragEvent::Leave => Self::Exit,
279 }
280 }
281}
282
283#[repr(u8)]
287#[derive(Debug, Clone, Copy, PartialEq)]
288pub enum TouchPhase {
289 Started,
291 Moved,
293 Ended,
295 Cancelled,
297}
298
299#[repr(u8)]
304#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
305pub enum InputEventResult {
306 EventAccepted,
309 #[default]
311 EventIgnored,
312 GrabMouse,
314 StartDrag,
316}
317
318#[repr(C)]
322#[derive(Debug, Copy, Clone, PartialEq, Default)]
323pub enum InputEventFilterResult {
324 #[default]
327 ForwardEvent,
328 ForwardAndIgnore,
331 ForwardAndInterceptGrab,
334 Intercept,
337 DelayForwarding(u64),
345 ForwardAndObserve,
348}
349
350#[allow(missing_docs, non_upper_case_globals)]
352pub mod key_codes {
353 macro_rules! declare_consts_for_special_keys {
354 ($($char:literal # $name:ident # $($shifted:ident)? $(=> $($_muda:ident)? # $($_qt:ident)|* # $($_winit:ident $(($_pos:ident))?)|* # $($_xkb:ident)|* )? ;)*) => {
355 $(pub const $name : char = $char;)*
356
357 #[allow(missing_docs)]
358 #[derive(Debug, Copy, Clone, PartialEq)]
359 #[non_exhaustive]
360 pub enum Key {
375 $($name,)*
376 }
377
378 impl From<Key> for char {
379 fn from(k: Key) -> Self {
380 match k {
381 $(Key::$name => $name,)*
382 }
383 }
384 }
385
386 impl From<Key> for crate::SharedString {
387 fn from(k: Key) -> Self {
388 char::from(k).into()
389 }
390 }
391 };
392 }
393
394 i_slint_common::for_each_keys!(declare_consts_for_special_keys);
395}
396
397#[derive(Clone, Copy, Default, Debug)]
400pub(crate) struct InternalKeyboardModifierState {
401 left_alt: bool,
402 right_alt: bool,
403 altgr: bool,
404 left_control: bool,
405 right_control: bool,
406 left_meta: bool,
407 right_meta: bool,
408 left_shift: bool,
409 right_shift: bool,
410}
411
412impl InternalKeyboardModifierState {
413 pub(crate) fn state_update(mut self, pressed: bool, text: &SharedString) -> Option<Self> {
416 if let Some(key_code) = text.chars().next() {
417 match key_code {
418 key_codes::Alt => self.left_alt = pressed,
419 key_codes::AltGr => self.altgr = pressed,
420 key_codes::Control => self.left_control = pressed,
421 key_codes::ControlR => self.right_control = pressed,
422 key_codes::Shift => self.left_shift = pressed,
423 key_codes::ShiftR => self.right_shift = pressed,
424 key_codes::Meta => self.left_meta = pressed,
425 key_codes::MetaR => self.right_meta = pressed,
426 _ => return None,
427 };
428
429 debug_assert_eq!(key_code.len_utf8(), text.len());
433 }
434
435 Some(self)
436 }
437
438 pub fn shift(&self) -> bool {
439 self.right_shift || self.left_shift
440 }
441 pub fn alt(&self) -> bool {
442 self.right_alt || self.left_alt
443 }
444 pub fn meta(&self) -> bool {
445 self.right_meta || self.left_meta
446 }
447 pub fn control(&self) -> bool {
448 self.right_control || self.left_control
449 }
450
451 pub fn modifiers_for(&self, _event: &InternalKeyEvent) -> KeyboardModifiers {
452 #[allow(unused_mut)]
453 let mut alt = self.alt();
454 #[allow(unused_mut)]
455 let mut control = self.control();
456
457 #[cfg(target_os = "windows")]
476 {
477 if !self.altgr && self.control() && self.alt() {
479 let implies_altgr = if _event.text_without_modifiers.is_empty() {
486 _event.key_event.text.chars().any(|c| !c.is_ascii_alphanumeric())
487 } else {
488 _event.text_without_modifiers.to_lowercase()
489 != _event.key_event.text.to_lowercase()
490 };
491 if implies_altgr {
492 alt = false;
493 control = false;
494 }
495 }
496 }
497 #[cfg(target_family = "wasm")]
498 if crate::detect_operating_system() == OperatingSystemType::Windows {
499 let is_altgr = self.altgr
503 || (self.control()
504 && self.alt()
505 && _event.key_event.text.chars().any(|c| !c.is_ascii_alphanumeric()));
506 if is_altgr {
507 alt = false;
508 control = false;
509 }
510 }
511
512 KeyboardModifiers { alt, control, meta: self.meta(), shift: self.shift() }
513 }
514}
515
516impl From<InternalKeyboardModifierState> for KeyboardModifiers {
517 fn from(internal_state: InternalKeyboardModifierState) -> Self {
518 Self {
519 alt: internal_state.alt(),
520 control: internal_state.control(),
521 meta: internal_state.meta(),
522 shift: internal_state.shift(),
523 }
524 }
525}
526
527#[i_slint_core_macros::slint_doc]
528#[derive(Clone, Eq, PartialEq, Default)]
561#[repr(C)]
562pub struct Keys {
563 inner: KeysInner,
564}
565
566#[derive(Debug, Clone, PartialEq, Eq)]
568enum KeysParseErrorInner {
569 NoKey,
571 MultipleKeys,
573 MultipleGraphemeClusters(SharedString),
576 NotLowercase(SharedString),
579 IncompatibleModifiers(SharedString),
582}
583
584#[derive(Debug, Clone, PartialEq, Eq)]
589pub struct KeysParseError(KeysParseErrorInner);
590
591impl core::fmt::Display for KeysParseError {
592 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
593 match &self.0 {
594 KeysParseErrorInner::NoKey => write!(f, "no key found (only modifiers)"),
595 KeysParseErrorInner::MultipleKeys => {
596 write!(f, "multiple non-modifier keys found")
597 }
598 KeysParseErrorInner::MultipleGraphemeClusters(s) => {
599 write!(f, "key string must be a single grapheme cluster, got: {s}")
600 }
601 KeysParseErrorInner::NotLowercase(s) => {
602 let lower = s.to_lowercase();
603 write!(f, "key string must be lowercase, use \"{lower}\" instead")
604 }
605 KeysParseErrorInner::IncompatibleModifiers(msg) => write!(f, "{msg}"),
606 }
607 }
608}
609
610impl core::error::Error for KeysParseError {}
611
612use i_slint_common::key_codes::{ShiftBehavior, lookup_key_name};
613
614pub fn make_keys(
616 key: SharedString,
617 modifiers: KeyboardModifiers,
618 ignore_shift: bool,
619 ignore_alt: bool,
620) -> Keys {
621 Keys {
622 inner: KeysInner { key: key.to_lowercase().into(), modifiers, ignore_shift, ignore_alt },
623 }
624}
625
626#[cfg(feature = "ffi")]
627#[allow(unsafe_code)]
628pub(crate) mod ffi {
629 use crate::api::ToSharedString as _;
630
631 use super::*;
632
633 #[unsafe(no_mangle)]
634 pub unsafe extern "C" fn slint_keys(
635 key: &SharedString,
636 alt: bool,
637 control: bool,
638 shift: bool,
639 meta: bool,
640 ignore_shift: bool,
641 ignore_alt: bool,
642 out: &mut Keys,
643 ) {
644 *out = make_keys(
645 key.clone(),
646 KeyboardModifiers { alt, control, shift, meta },
647 ignore_shift,
648 ignore_alt,
649 );
650 }
651
652 #[unsafe(no_mangle)]
653 pub unsafe extern "C" fn slint_keys_debug_string(shortcut: &Keys, out: &mut SharedString) {
654 *out = crate::format!("{shortcut:?}");
655 }
656
657 #[unsafe(no_mangle)]
658 pub unsafe extern "C" fn slint_keys_to_string(shortcut: &Keys, out: &mut SharedString) {
659 *out = shortcut.to_shared_string();
660 }
661
662 #[unsafe(no_mangle)]
663 pub unsafe extern "C" fn slint_keys_from_parts(
664 parts: crate::slice::Slice<'_, SharedString>,
665 out: &mut Keys,
666 ) -> bool {
667 match keys_from_parts(parts.as_slice().iter().map(|s| s.as_str())) {
668 Ok(keys) => {
669 *out = keys;
670 true
671 }
672 Err(_) => false,
673 }
674 }
675
676 #[unsafe(no_mangle)]
677 pub unsafe extern "C" fn slint_keys_to_parts(
678 keys: &Keys,
679 out: &mut crate::SharedVector<SharedString>,
680 ) {
681 *out = keys.to_parts().map(SharedString::from).collect();
682 }
683}
684
685fn normalize_key(key: &str) -> SharedString {
687 let lowered = key.to_lowercase();
688 cfg_if::cfg_if! {
689 if #[cfg(feature = "shared-parley")] {
690 let normalizer = icu_normalizer::ComposingNormalizer::new_nfc();
691 let normalized = normalizer.normalize(&lowered);
692 SharedString::from(normalized.as_ref())
693 } else {
694 SharedString::from(lowered.as_str())
695 }
696 }
697}
698
699fn keys_from_parts<'a>(parts: impl Iterator<Item = &'a str>) -> Result<Keys, KeysParseError> {
700 keys_from_parts_inner(parts).map_err(KeysParseError)
701}
702
703fn keys_from_parts_inner<'a>(
704 parts: impl Iterator<Item = &'a str>,
705) -> Result<Keys, KeysParseErrorInner> {
706 use unicode_segmentation::UnicodeSegmentation;
707
708 let mut modifiers = KeyboardModifiers::default();
709 let mut ignore_shift = false;
710 let mut ignore_alt = false;
711 let mut key_part: Option<&str> = None;
712
713 for part in parts {
714 if part.is_empty() {
721 continue;
722 }
723 match part {
724 "Control" => modifiers.control = true,
725 "Alt" => {
726 if ignore_alt {
727 return Err(KeysParseErrorInner::IncompatibleModifiers(
728 "Alt and Alt? cannot be combined".into(),
729 ));
730 }
731 modifiers.alt = true;
732 }
733 "Shift" => {
734 if ignore_shift {
735 return Err(KeysParseErrorInner::IncompatibleModifiers(
736 "Shift and Shift? cannot be combined".into(),
737 ));
738 }
739 modifiers.shift = true;
740 }
741 "Meta" => modifiers.meta = true,
742 "Shift?" => {
743 if modifiers.shift {
744 return Err(KeysParseErrorInner::IncompatibleModifiers(
745 "Shift and Shift? cannot be combined".into(),
746 ));
747 }
748 ignore_shift = true;
749 }
750 "Alt?" => {
751 if modifiers.alt {
752 return Err(KeysParseErrorInner::IncompatibleModifiers(
753 "Alt and Alt? cannot be combined".into(),
754 ));
755 }
756 ignore_alt = true;
757 }
758 _ => {
759 if key_part.is_some() {
760 return Err(KeysParseErrorInner::MultipleKeys);
761 }
762 key_part = Some(part);
763 }
764 }
765 }
766
767 let key_name = match key_part {
768 Some(k) => k,
769 None if modifiers == KeyboardModifiers::default() && !ignore_shift && !ignore_alt => {
770 return Ok(Keys::default());
772 }
773 None => return Err(KeysParseErrorInner::NoKey),
774 };
775
776 if let Some((key_char, shift_behavior)) = lookup_key_name(key_name) {
778 if matches!(shift_behavior, ShiftBehavior::LocalizedShiftable { .. }) {
780 if modifiers.shift {
781 return Err(KeysParseErrorInner::IncompatibleModifiers(
782 alloc::format!(
783 "Key bindings involving {key_name} ignore Shift to support different keyboard layouts; remove Shift"
784 ).into(),
785 ));
786 }
787 ignore_shift = true;
788 }
789 let key: SharedString = key_char.to_lowercase().collect::<alloc::string::String>().into();
791 return Ok(Keys { inner: KeysInner { key, modifiers, ignore_shift, ignore_alt } });
792 }
793
794 let grapheme_count = key_name.graphemes(true).count();
797 if grapheme_count > 1 {
798 return Err(KeysParseErrorInner::MultipleGraphemeClusters(key_name.into()));
799 }
800
801 let lowered = key_name.to_lowercase();
803 if lowered != key_name {
804 return Err(KeysParseErrorInner::NotLowercase(key_name.into()));
805 }
806
807 let key = normalize_key(key_name);
808 Ok(Keys { inner: KeysInner { key, modifiers, ignore_shift, ignore_alt } })
809}
810
811#[derive(PartialEq, Eq, Clone, Default)]
814#[repr(C)]
815pub struct KeysInner {
816 pub key: SharedString,
820 pub modifiers: KeyboardModifiers,
822 pub ignore_shift: bool,
824 pub ignore_alt: bool,
826}
827
828impl KeysInner {
829 pub fn from_pub(keys: &Keys) -> &Self {
831 &keys.inner
832 }
833}
834
835impl Keys {
836 #[i_slint_core_macros::slint_doc]
837 pub fn from_parts<'a>(
856 parts: impl IntoIterator<Item = &'a str>,
857 ) -> Result<Keys, KeysParseError> {
858 keys_from_parts(parts.into_iter())
859 }
860
861 #[i_slint_core_macros::slint_doc]
862 pub fn to_parts(&self) -> impl Iterator<Item = &str> {
889 let inner = &self.inner;
890 let has_key = !inner.key.is_empty();
891 [
901 (has_key && inner.modifiers.meta).then_some("Meta"),
902 (has_key && inner.modifiers.control).then_some("Control"),
903 (has_key && inner.modifiers.alt).then_some("Alt"),
904 (has_key && !inner.modifiers.alt && inner.ignore_alt).then_some("Alt?"),
905 (has_key && inner.modifiers.shift).then_some("Shift"),
906 (has_key && !inner.modifiers.shift && inner.ignore_shift).then_some("Shift?"),
907 has_key.then(|| inner.key.as_str()),
908 ]
909 .into_iter()
910 .flatten()
911 }
912
913 pub(crate) fn matches(&self, key_event: &KeyEvent) -> bool {
915 let inner = &self.inner;
916 if inner.key.is_empty() {
918 return false;
919 }
920
921 let mut expected_modifiers = inner.modifiers;
923 if inner.ignore_shift {
924 expected_modifiers.shift = key_event.modifiers.shift;
925 }
926 if inner.ignore_alt {
927 expected_modifiers.alt = key_event.modifiers.alt;
928 }
929 let event_text = key_event.text.chars().flat_map(|character| character.to_lowercase());
937
938 event_text.eq(inner.key.chars()) && key_event.modifiers == expected_modifiers
939 }
940
941 fn format_key_for_display(&self) -> crate::SharedString {
942 let key_str = self.inner.key.as_str();
943 let first_char = key_str.chars().next();
944
945 if let Some(first_char) = first_char {
946 macro_rules! check_special_key {
947 ($($char:literal # $name:ident # $($shifted:ident)? $(=> $($_muda:ident)? # $($qt:ident)|* # $($winit:ident $(($_pos:ident))?)|* # $($xkb:ident)|*)? ;)*) => {
948 match first_char {
949 $($(
950 $char => {
952 let _ = stringify!($($qt)|*); return stringify!($name).into();
954 }
955 )?)*
956 _ => ()
957 }
958 };
959 }
960 i_slint_common::for_each_keys!(check_special_key);
961 }
962
963 if key_str.chars().count() == 1 {
964 return key_str.to_uppercase().into();
965 }
966
967 key_str.into()
968 }
969}
970
971impl Display for Keys {
972 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
988 let inner = &self.inner;
989 if inner.key.is_empty() {
990 return Ok(());
991 }
992
993 if crate::is_apple_platform() {
994 if inner.modifiers.meta {
1001 f.write_str("⌃")?;
1002 }
1003 if !inner.ignore_alt && inner.modifiers.alt {
1004 f.write_str("⌥")?;
1005 }
1006 if !inner.ignore_shift && inner.modifiers.shift {
1007 f.write_str("⇧")?;
1008 }
1009 if inner.modifiers.control {
1010 f.write_str("⌘")?;
1011 }
1012 } else {
1013 let separator = "+";
1014
1015 let (ctrl_str, alt_str, shift_str, meta_str) =
1018 if crate::detect_operating_system() == OperatingSystemType::Windows {
1019 ("Ctrl", "Alt", "Shift", "Win")
1020 } else {
1021 ("Ctrl", "Alt", "Shift", "Super")
1022 };
1023
1024 if inner.modifiers.meta {
1025 f.write_str(meta_str)?;
1026 f.write_str(separator)?;
1027 }
1028 if inner.modifiers.control {
1029 f.write_str(ctrl_str)?;
1030 f.write_str(separator)?;
1031 }
1032 if !inner.ignore_alt && inner.modifiers.alt {
1033 f.write_str(alt_str)?;
1034 f.write_str(separator)?;
1035 }
1036 if !inner.ignore_shift && inner.modifiers.shift {
1037 f.write_str(shift_str)?;
1038 f.write_str(separator)?;
1039 }
1040 }
1041 f.write_str(&self.format_key_for_display())
1042 }
1043}
1044
1045impl core::fmt::Debug for Keys {
1046 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1048 let inner = &self.inner;
1049 if inner.key.is_empty() {
1051 write!(f, "")
1052 } else {
1053 let alt = inner
1054 .ignore_alt
1055 .then_some("Alt?+")
1056 .or(inner.modifiers.alt.then_some("Alt+"))
1057 .unwrap_or_default();
1058 let ctrl = if inner.modifiers.control { "Control+" } else { "" };
1059 let meta = if inner.modifiers.meta { "Meta+" } else { "" };
1060 let shift = inner
1061 .ignore_shift
1062 .then_some("Shift?+")
1063 .or(inner.modifiers.shift.then_some("Shift+"))
1064 .unwrap_or_default();
1065 let keycode: SharedString = inner
1066 .key
1067 .chars()
1068 .flat_map(|character| {
1069 let mut escaped = alloc::vec![];
1070 if character.is_control() {
1071 escaped.extend(character.escape_unicode());
1072 } else {
1073 escaped.push(character);
1074 }
1075 escaped
1076 })
1077 .collect();
1078 write!(f, "{meta}{ctrl}{alt}{shift}\"{keycode}\"")
1079 }
1080 }
1081}
1082
1083#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
1085#[repr(u8)]
1086pub enum KeyEventType {
1087 #[default]
1089 KeyPressed = 0,
1090 KeyReleased = 1,
1092 UpdateComposition = 2,
1095 CommitComposition = 3,
1097}
1098
1099#[derive(Default, Debug, Clone, PartialEq)]
1100pub struct InternalKeyEvent {
1102 pub key_event: KeyEvent,
1104 pub event_type: KeyEventType,
1106 #[cfg(target_os = "windows")]
1112 pub text_without_modifiers: SharedString,
1113 pub replacement_range: Option<core::ops::Range<i32>>,
1117 pub preedit_text: SharedString,
1119 pub preedit_selection: Option<core::ops::Range<i32>>,
1121 pub cursor_position: Option<i32>,
1123 pub anchor_position: Option<i32>,
1125}
1126
1127impl InternalKeyEvent {
1128 pub fn shortcut(&self) -> Option<StandardShortcut> {
1131 if self.key_event.modifiers.control && !self.key_event.modifiers.shift {
1132 match self.key_event.text.as_str() {
1133 #[cfg(not(target_arch = "wasm32"))]
1134 "c" => Some(StandardShortcut::Copy),
1135 #[cfg(not(target_arch = "wasm32"))]
1136 "x" => Some(StandardShortcut::Cut),
1137 #[cfg(not(target_arch = "wasm32"))]
1138 "v" => Some(StandardShortcut::Paste),
1139 "a" => Some(StandardShortcut::SelectAll),
1140 "f" => Some(StandardShortcut::Find),
1141 "s" => Some(StandardShortcut::Save),
1142 "p" => Some(StandardShortcut::Print),
1143 "z" => Some(StandardShortcut::Undo),
1144 #[cfg(target_os = "windows")]
1145 "y" => Some(StandardShortcut::Redo),
1146 "r" => Some(StandardShortcut::Refresh),
1147 _ => None,
1148 }
1149 } else if self.key_event.modifiers.control && self.key_event.modifiers.shift {
1150 match self.key_event.text.as_str() {
1151 #[cfg(not(target_os = "windows"))]
1152 "z" | "Z" => Some(StandardShortcut::Redo),
1153 _ => None,
1154 }
1155 } else {
1156 None
1157 }
1158 }
1159
1160 pub fn text_shortcut(&self) -> Option<TextShortcut> {
1163 let ke = &self.key_event;
1164 let keycode = ke.text.chars().next()?;
1165
1166 let is_apple = crate::is_apple_platform();
1167
1168 let move_mod = if is_apple {
1169 ke.modifiers.alt && !ke.modifiers.control && !ke.modifiers.meta
1170 } else {
1171 ke.modifiers.control && !ke.modifiers.alt && !ke.modifiers.meta
1172 };
1173
1174 if move_mod {
1175 match keycode {
1176 key_codes::LeftArrow => {
1177 return Some(TextShortcut::Move(TextCursorDirection::BackwardByWord));
1178 }
1179 key_codes::RightArrow => {
1180 return Some(TextShortcut::Move(TextCursorDirection::ForwardByWord));
1181 }
1182 key_codes::UpArrow => {
1183 return Some(TextShortcut::Move(TextCursorDirection::StartOfParagraph));
1184 }
1185 key_codes::DownArrow => {
1186 return Some(TextShortcut::Move(TextCursorDirection::EndOfParagraph));
1187 }
1188 key_codes::Backspace => {
1189 return Some(TextShortcut::DeleteWordBackward);
1190 }
1191 key_codes::Delete => {
1192 return Some(TextShortcut::DeleteWordForward);
1193 }
1194 _ => (),
1195 };
1196 }
1197
1198 #[cfg(not(target_os = "macos"))]
1199 {
1200 if ke.modifiers.control && !ke.modifiers.alt && !ke.modifiers.meta {
1201 match keycode {
1202 key_codes::Home => {
1203 return Some(TextShortcut::Move(TextCursorDirection::StartOfText));
1204 }
1205 key_codes::End => {
1206 return Some(TextShortcut::Move(TextCursorDirection::EndOfText));
1207 }
1208 _ => (),
1209 };
1210 }
1211 }
1212
1213 if is_apple && ke.modifiers.control {
1214 match keycode {
1215 key_codes::LeftArrow => {
1216 return Some(TextShortcut::Move(TextCursorDirection::StartOfLine));
1217 }
1218 key_codes::RightArrow => {
1219 return Some(TextShortcut::Move(TextCursorDirection::EndOfLine));
1220 }
1221 key_codes::UpArrow => {
1222 return Some(TextShortcut::Move(TextCursorDirection::StartOfText));
1223 }
1224 key_codes::DownArrow => {
1225 return Some(TextShortcut::Move(TextCursorDirection::EndOfText));
1226 }
1227 key_codes::Backspace => {
1228 return Some(TextShortcut::DeleteToStartOfLine);
1229 }
1230 _ => (),
1231 };
1232 }
1233
1234 if let Ok(direction) = TextCursorDirection::try_from(keycode) {
1235 Some(TextShortcut::Move(direction))
1236 } else {
1237 match keycode {
1238 key_codes::Backspace => Some(TextShortcut::DeleteBackward),
1239 key_codes::Delete => Some(TextShortcut::DeleteForward),
1240 _ => None,
1241 }
1242 }
1243 }
1244}
1245
1246pub enum StandardShortcut {
1248 Copy,
1250 Cut,
1252 Paste,
1254 SelectAll,
1256 Find,
1258 Save,
1260 Print,
1262 Undo,
1264 Redo,
1266 Refresh,
1268}
1269
1270pub enum TextShortcut {
1272 Move(TextCursorDirection),
1274 DeleteForward,
1276 DeleteBackward,
1278 DeleteWordForward,
1280 DeleteWordBackward,
1282 DeleteToStartOfLine,
1284}
1285
1286#[repr(u8)]
1289#[derive(Debug, Clone, Copy, PartialEq, Default)]
1290pub enum KeyEventResult {
1291 EventAccepted,
1293 #[default]
1295 EventIgnored,
1296}
1297
1298#[repr(u8)]
1301#[derive(Debug, Clone, Copy, PartialEq, Default)]
1302pub enum FocusEventResult {
1303 FocusAccepted,
1305 #[default]
1307 FocusIgnored,
1308}
1309
1310#[derive(Debug, Clone, Copy, PartialEq)]
1313#[repr(u8)]
1314pub enum FocusEvent {
1315 FocusIn(FocusReason),
1317 FocusOut(FocusReason),
1319}
1320
1321#[derive(Default)]
1323pub struct ClickState {
1324 click_count_time_stamp: Cell<Option<crate::animations::Instant>>,
1325 click_count: Cell<u8>,
1326 click_position: Cell<LogicalPoint>,
1327 click_button: Cell<PointerEventButton>,
1328}
1329
1330impl ClickState {
1331 fn restart(
1333 &self,
1334 position: LogicalPoint,
1335 button: PointerEventButton,
1336 now: crate::animations::Instant,
1337 ) {
1338 self.click_count.set(0);
1339 self.click_count_time_stamp.set(Some(now));
1340 self.click_position.set(position);
1341 self.click_button.set(button);
1342 }
1343
1344 pub fn reset(&self) {
1346 self.click_count.set(0);
1347 self.click_count_time_stamp.replace(None);
1348 }
1349
1350 pub fn check_repeat(&self, mouse_event: MouseEvent, ctx: &crate::SlintContext) -> MouseEvent {
1354 let click_interval = ctx.platform().click_interval();
1355 match mouse_event {
1356 MouseEvent::Pressed { position, button, touch_finger_id, .. } => {
1357 let instant_now = crate::animations::Instant::now(ctx);
1358
1359 if let Some(click_count_time_stamp) = self.click_count_time_stamp.get() {
1360 if instant_now - click_count_time_stamp < click_interval
1361 && button == self.click_button.get()
1362 && (position - self.click_position.get()).square_length() < 100 as _
1363 {
1364 self.click_count.set(self.click_count.get().wrapping_add(1));
1365 self.click_count_time_stamp.set(Some(instant_now));
1366 } else {
1367 self.restart(position, button, instant_now);
1368 }
1369 } else {
1370 self.restart(position, button, instant_now);
1371 }
1372
1373 return MouseEvent::Pressed {
1374 position,
1375 button,
1376 click_count: self.click_count.get(),
1377 touch_finger_id,
1378 };
1379 }
1380 MouseEvent::Released { position, button, touch_finger_id, .. } => {
1381 return MouseEvent::Released {
1382 position,
1383 button,
1384 click_count: self.click_count.get(),
1385 touch_finger_id,
1386 };
1387 }
1388 _ => {}
1389 };
1390
1391 mouse_event
1392 }
1393}
1394
1395#[derive(Clone)]
1397pub(crate) struct DragData {
1398 pub(crate) event: DropEvent,
1401 pub(crate) allowed: AllowedDragActions,
1403}
1404
1405#[derive(Default)]
1407pub struct MouseInputState {
1408 item_stack: Vec<(ItemWeak, InputEventFilterResult)>,
1411 observers: Vec<ItemWeak>,
1416 pub(crate) offset: LogicalPoint,
1418 grabbed: bool,
1420 pub(crate) drag_data: Option<DragData>,
1423 pub(crate) drag_source: Option<ItemWeak>,
1426 pub(crate) drop_target: Option<ItemWeak>,
1430 delayed: Option<(crate::timers::Timer, MouseEvent)>,
1431 delayed_exit_items: Vec<ItemWeak>,
1432 pub(crate) cursor: MouseCursorInner,
1433}
1434
1435impl MouseInputState {
1436 fn top_item(&self) -> Option<ItemRc> {
1438 self.item_stack.last().and_then(|x| x.0.upgrade())
1439 }
1440
1441 pub(crate) fn arm_in_window_drag(
1444 &mut self,
1445 drag_area: core::pin::Pin<&crate::items::DragArea>,
1446 source: ItemWeak,
1447 seed_position: crate::api::LogicalPosition,
1448 ) {
1449 let (mut drop_event, allowed) = drag_area.initial_drop_event();
1450 drop_event.position = seed_position;
1451 self.drag_data = Some(DragData { event: drop_event, allowed });
1452 self.drag_source = Some(source);
1453 drag_area.dragging.set(true);
1454 }
1455
1456 pub fn top_item_including_delayed(&self) -> Option<ItemRc> {
1458 self.delayed_exit_items.last().and_then(|x| x.upgrade()).or_else(|| self.top_item())
1459 }
1460
1461 pub fn has_delayed_event(&self) -> bool {
1463 self.delayed.is_some()
1464 }
1465
1466 pub fn drop_target_action(&self) -> Option<crate::items::DragAction> {
1469 let action = self
1470 .drop_target
1471 .as_ref()
1472 .and_then(|t| t.upgrade())
1473 .and_then(|i| i.downcast::<crate::items::DropArea>())
1474 .map(|d| d.as_pin_ref().current_action())?;
1475 (action != crate::items::DragAction::None).then_some(action)
1476 }
1477}
1478
1479pub(crate) struct MouseGrabResult {
1480 pub event: Option<MouseEvent>,
1483 pub accepted: bool,
1486}
1487
1488fn offer_native_drag(
1491 window_adapter: &Rc<dyn WindowAdapter>,
1492 drag_area: core::pin::Pin<&crate::items::DragArea>,
1493 source: ItemWeak,
1494 seed_position: crate::api::LogicalPosition,
1495 state: &mut MouseInputState,
1496) {
1497 let data = drag_area.data();
1498 if data.has_plain_text() || data.has_image() {
1500 let request = crate::window::DragRequest {
1501 data: data.clone(),
1502 allowed: drag_area.allowed_actions(),
1503 drag_image: drag_area.drag_image(),
1504 drag_image_offset: euclid::vec2(
1505 drag_area.drag_image_offset_x(),
1506 drag_area.drag_image_offset_y(),
1507 ),
1508 };
1509 if window_adapter.internal(crate::InternalToken).is_some_and(|i| i.start_drag(&request)) {
1510 let drag = crate::window::NativePendingDrag { request, source, seed_position };
1513 crate::window::WindowInner::from_pub(window_adapter.window())
1514 .set_native_drag(Some(drag));
1515 drag_area.dragging.set(true);
1516 return;
1517 }
1518 }
1519 state.arm_in_window_drag(drag_area, source, seed_position);
1521}
1522
1523pub(crate) fn handle_mouse_grab(
1525 mouse_event: &MouseEvent,
1526 window_adapter: &Rc<dyn WindowAdapter>,
1527 mouse_input_state: &mut MouseInputState,
1528) -> MouseGrabResult {
1529 if !mouse_input_state.grabbed || mouse_input_state.item_stack.is_empty() {
1530 return MouseGrabResult { event: Some(mouse_event.clone()), accepted: false };
1531 };
1532
1533 let mut event = mouse_event.clone();
1534 let mut intercept = false;
1535 let mut invalid = false;
1536
1537 event.translate(-mouse_input_state.offset.to_vector());
1538
1539 mouse_input_state.item_stack.retain(|it| {
1540 if invalid {
1541 return false;
1542 }
1543 let item = if let Some(item) = it.0.upgrade() {
1544 item
1545 } else {
1546 invalid = true;
1547 return false;
1548 };
1549 if intercept {
1550 item.borrow().as_ref().input_event(
1551 &MouseEvent::Exit,
1552 window_adapter,
1553 &item,
1554 &mut mouse_input_state.cursor,
1555 );
1556 return false;
1557 }
1558 let g = item.geometry();
1559 event.translate(-g.origin.to_vector());
1560 if window_adapter.renderer().supports_transformations()
1561 && let Some(inverse_transform) = item.inverse_children_transform()
1562 {
1563 event.transform(inverse_transform);
1564 }
1565
1566 let interested = matches!(
1567 it.1,
1568 InputEventFilterResult::ForwardAndInterceptGrab
1569 | InputEventFilterResult::DelayForwarding(_)
1570 );
1571
1572 if interested
1573 && item.borrow().as_ref().input_event_filter_before_children(
1574 &event,
1575 window_adapter,
1576 &item,
1577 &mut mouse_input_state.cursor,
1578 ) == InputEventFilterResult::Intercept
1579 {
1580 intercept = true;
1581 }
1582 true
1583 });
1584 if invalid {
1585 return MouseGrabResult { event: Some(mouse_event.clone()), accepted: false };
1586 }
1587
1588 let grabber = mouse_input_state.top_item().unwrap();
1589 let input_result = grabber.borrow().as_ref().input_event(
1590 &event,
1591 window_adapter,
1592 &grabber,
1593 &mut mouse_input_state.cursor,
1594 );
1595 match input_result {
1596 InputEventResult::GrabMouse => MouseGrabResult { event: None, accepted: true },
1597 InputEventResult::StartDrag => {
1598 mouse_input_state.grabbed = false;
1599 let drag_area_item = grabber.downcast::<crate::items::DragArea>().unwrap();
1600 let drag_area = drag_area_item.as_pin_ref();
1601 let seed_position = mouse_event
1604 .position()
1605 .map(crate::lengths::logical_position_to_api)
1606 .unwrap_or_default();
1607 offer_native_drag(
1608 window_adapter,
1609 drag_area,
1610 grabber.downgrade(),
1611 seed_position,
1612 mouse_input_state,
1613 );
1614 MouseGrabResult { event: None, accepted: true }
1615 }
1616 InputEventResult::EventAccepted | InputEventResult::EventIgnored => {
1617 mouse_input_state.grabbed = false;
1618 MouseGrabResult {
1620 event: Some(mouse_event.position().map_or(MouseEvent::Exit, |position| {
1621 MouseEvent::Moved { position, touch_finger_id: mouse_event.touch_finger_id() }
1622 })),
1623 accepted: input_result == InputEventResult::EventAccepted,
1624 }
1625 }
1626 }
1627}
1628
1629pub(crate) fn send_exit_events(
1630 old_input_state: &MouseInputState,
1631 new_input_state: &mut MouseInputState,
1632 mut pos: Option<LogicalPoint>,
1633 window_adapter: &Rc<dyn WindowAdapter>,
1634) {
1635 let cursor = &mut MouseCursorInner::BuiltIn(BuiltInMouseCursor::Default);
1637
1638 for it in core::mem::take(&mut new_input_state.delayed_exit_items) {
1639 let Some(item) = it.upgrade() else { continue };
1640 item.borrow().as_ref().input_event(&MouseEvent::Exit, window_adapter, &item, cursor);
1641 }
1642
1643 let mut clipped = false;
1644 for (idx, it) in old_input_state.item_stack.iter().enumerate() {
1645 let Some(item) = it.0.upgrade() else { break };
1646 let g = item.geometry();
1647 let contains = pos.is_some_and(|p| g.contains(p));
1648 if let Some(p) = pos.as_mut() {
1649 *p -= g.origin.to_vector();
1650 if window_adapter.renderer().supports_transformations()
1651 && let Some(inverse_transform) = item.inverse_children_transform()
1652 {
1653 *p = inverse_transform.transform_point(p.cast()).cast();
1654 }
1655 }
1656 if !contains || clipped {
1657 if item.borrow().as_ref().clips_children() {
1658 clipped = true;
1659 }
1660 item.borrow().as_ref().input_event(&MouseEvent::Exit, window_adapter, &item, cursor);
1661 } else if new_input_state.item_stack.get(idx).is_none_or(|(x, _)| *x != it.0) {
1662 if new_input_state.delayed.is_some() {
1664 new_input_state.delayed_exit_items.push(it.0.clone());
1665 } else {
1666 item.borrow().as_ref().input_event(
1667 &MouseEvent::Exit,
1668 window_adapter,
1669 &item,
1670 cursor,
1671 );
1672 }
1673 }
1674 }
1675
1676 for obs in &old_input_state.observers {
1682 if new_input_state.observers.iter().any(|x| x == obs)
1683 || new_input_state.item_stack.iter().any(|(x, _)| x == obs)
1684 {
1685 continue;
1686 }
1687 let Some(item) = obs.upgrade() else { continue };
1688 item.borrow().as_ref().input_event(&MouseEvent::Exit, window_adapter, &item, cursor);
1689 }
1690}
1691
1692pub struct MouseInputResult {
1694 pub state: MouseInputState,
1696 pub accepted: bool,
1699}
1700
1701pub fn process_mouse_input(
1705 root: ItemRc,
1706 mouse_event: &MouseEvent,
1707 window_adapter: &Rc<dyn WindowAdapter>,
1708 mut mouse_input_state: MouseInputState,
1709) -> MouseInputResult {
1710 let mut result = MouseInputState {
1711 drag_data: mouse_input_state.drag_data.clone(),
1712 drag_source: mouse_input_state.drag_source.clone(),
1713 drop_target: mouse_input_state.drop_target.clone(),
1714 cursor: mouse_input_state.cursor.clone(),
1715 ..Default::default()
1716 };
1717 let r = send_mouse_event_to_item(
1718 mouse_event,
1719 root.clone(),
1720 window_adapter,
1721 &mut result,
1722 mouse_input_state.top_item().as_ref(),
1723 false,
1724 );
1725 let accepted = r.has_aborted();
1726 if matches!(mouse_event, MouseEvent::DragMove { .. }) {
1727 result.drop_target =
1730 accepted.then(|| result.item_stack.last().map(|(w, _)| w.clone())).flatten();
1731 }
1732 if mouse_input_state.delayed.is_some()
1733 && (!accepted
1734 || Option::zip(result.item_stack.last(), mouse_input_state.item_stack.last())
1735 .is_none_or(|(a, b)| a.0 != b.0))
1736 {
1737 mouse_input_state.cursor = result.cursor;
1739 return MouseInputResult { state: mouse_input_state, accepted };
1740 }
1741 send_exit_events(&mouse_input_state, &mut result, mouse_event.position(), window_adapter);
1742
1743 if let MouseEvent::Wheel { position, .. } = mouse_event
1744 && accepted
1745 {
1746 let moved = process_mouse_input(
1750 root,
1751 &MouseEvent::Moved { position: *position, touch_finger_id: 0 },
1752 window_adapter,
1753 result,
1754 );
1755 return MouseInputResult { state: moved.state, accepted: true };
1756 }
1757
1758 MouseInputResult { state: result, accepted }
1759}
1760
1761pub(crate) fn process_delayed_event(
1762 window_adapter: &Rc<dyn WindowAdapter>,
1763 mut mouse_input_state: MouseInputState,
1764) -> MouseInputState {
1765 let event = match mouse_input_state.delayed.take() {
1767 Some(e) => e.1,
1768 None => return mouse_input_state,
1769 };
1770
1771 let top_item = match mouse_input_state.top_item() {
1772 Some(i) => i,
1773 None => return MouseInputState::default(),
1774 };
1775
1776 let prev_target = mouse_input_state.delayed_exit_items.last().and_then(|x| x.upgrade());
1778 let last_top_item = prev_target.as_ref().unwrap_or(&top_item);
1779
1780 let mut actual_visitor =
1781 |component: &ItemTreeRc, index: u32, _: Pin<ItemRef>| -> VisitChildrenResult {
1782 send_mouse_event_to_item(
1783 &event,
1784 ItemRc::new(component.clone(), index),
1785 window_adapter,
1786 &mut mouse_input_state,
1787 Some(last_top_item),
1788 true,
1789 )
1790 };
1791 vtable::new_vref!(let mut actual_visitor : VRefMut<crate::item_tree::ItemVisitorVTable> for crate::item_tree::ItemVisitor = &mut actual_visitor);
1792 vtable::VRc::borrow_pin(top_item.item_tree()).as_ref().visit_children_item(
1793 top_item.index() as isize,
1794 crate::item_tree::TraversalOrder::FrontToBack,
1795 actual_visitor,
1796 );
1797 mouse_input_state
1798}
1799
1800fn send_mouse_event_to_item(
1801 mouse_event: &MouseEvent,
1802 item_rc: ItemRc,
1803 window_adapter: &Rc<dyn WindowAdapter>,
1804 result: &mut MouseInputState,
1805 last_top_item: Option<&ItemRc>,
1806 ignore_delays: bool,
1807) -> VisitChildrenResult {
1808 let item = item_rc.borrow();
1809 let geom = item_rc.geometry();
1810 let mut event_for_children = mouse_event.clone();
1812 event_for_children.translate(-geom.origin.to_vector());
1814 if window_adapter.renderer().supports_transformations() {
1815 if let Some(inverse_transform) = item_rc.inverse_children_transform() {
1817 event_for_children.transform(inverse_transform);
1818 }
1819 }
1820
1821 let filter_result = if mouse_event.position().is_some_and(|p| geom.contains(p))
1822 || item.as_ref().clips_children()
1823 {
1824 item.as_ref().input_event_filter_before_children(
1825 &event_for_children,
1826 window_adapter,
1827 &item_rc,
1828 &mut result.cursor,
1829 )
1830 } else {
1831 InputEventFilterResult::ForwardAndIgnore
1832 };
1833
1834 let (forward_to_children, ignore) = match filter_result {
1835 InputEventFilterResult::ForwardEvent => (true, false),
1836 InputEventFilterResult::ForwardAndIgnore => (true, true),
1837 InputEventFilterResult::ForwardAndInterceptGrab => (true, false),
1838 InputEventFilterResult::Intercept => (false, false),
1839 InputEventFilterResult::DelayForwarding(_) if ignore_delays => (true, false),
1840 InputEventFilterResult::DelayForwarding(duration) => {
1841 let timer = WindowInner::from_pub(window_adapter.window()).context().new_timer();
1842 let w = Rc::downgrade(window_adapter);
1843 timer.start(
1844 crate::timers::TimerMode::SingleShot,
1845 Duration::from_millis(duration),
1846 move || {
1847 if let Some(w) = w.upgrade() {
1848 WindowInner::from_pub(w.window()).process_delayed_event();
1849 }
1850 },
1851 );
1852 result.delayed = Some((timer, event_for_children));
1853 result
1854 .item_stack
1855 .push((item_rc.downgrade(), InputEventFilterResult::DelayForwarding(duration)));
1856 return VisitChildrenResult::abort(item_rc.index(), 0);
1857 }
1858 InputEventFilterResult::ForwardAndObserve => (true, true),
1862 };
1863
1864 result.item_stack.push((item_rc.downgrade(), filter_result));
1865 if forward_to_children {
1866 let mut actual_visitor =
1867 |component: &ItemTreeRc, index: u32, _: Pin<ItemRef>| -> VisitChildrenResult {
1868 send_mouse_event_to_item(
1869 &event_for_children,
1870 ItemRc::new(component.clone(), index),
1871 window_adapter,
1872 result,
1873 last_top_item,
1874 ignore_delays,
1875 )
1876 };
1877 vtable::new_vref!(let mut actual_visitor : VRefMut<crate::item_tree::ItemVisitorVTable> for crate::item_tree::ItemVisitor = &mut actual_visitor);
1878 let r = vtable::VRc::borrow_pin(item_rc.item_tree()).as_ref().visit_children_item(
1879 item_rc.index() as isize,
1880 crate::item_tree::TraversalOrder::FrontToBack,
1881 actual_visitor,
1882 );
1883 if r.has_aborted() {
1884 return r;
1885 }
1886 };
1887
1888 let r = if ignore {
1889 InputEventResult::EventIgnored
1890 } else {
1891 let mut event = mouse_event.clone();
1892 event.translate(-geom.origin.to_vector());
1893 if last_top_item.is_none_or(|x| *x != item_rc) {
1894 event.set_click_count(0);
1895 }
1896 item.as_ref().input_event(&event, window_adapter, &item_rc, &mut result.cursor)
1897 };
1898 match r {
1899 InputEventResult::EventAccepted => VisitChildrenResult::abort(item_rc.index(), 0),
1900 InputEventResult::EventIgnored => {
1901 let popped = result.item_stack.pop();
1902 debug_assert_eq!(
1903 popped.as_ref().map(|x| (x.0.upgrade().unwrap().index(), x.1)).unwrap(),
1904 (item_rc.index(), filter_result)
1905 );
1906 if filter_result == InputEventFilterResult::ForwardAndObserve
1909 && let Some((weak, _)) = popped
1910 && !result.observers.contains(&weak)
1911 {
1912 result.observers.push(weak);
1913 }
1914 VisitChildrenResult::CONTINUE
1915 }
1916 InputEventResult::GrabMouse => {
1917 result.item_stack.last_mut().unwrap().1 =
1918 InputEventFilterResult::ForwardAndInterceptGrab;
1919 result.grabbed = true;
1920 VisitChildrenResult::abort(item_rc.index(), 0)
1921 }
1922 InputEventResult::StartDrag => {
1923 result.item_stack.last_mut().unwrap().1 =
1924 InputEventFilterResult::ForwardAndInterceptGrab;
1925 result.grabbed = false;
1926 let drag_area_item = item_rc.downcast::<crate::items::DragArea>().unwrap();
1927 let drag_area = drag_area_item.as_pin_ref();
1928 let seed_position = mouse_event
1932 .position()
1933 .map(|p| p - geom.origin.to_vector())
1934 .map(|p| item_rc.map_to_window(p))
1935 .map(crate::lengths::logical_position_to_api)
1936 .unwrap_or_default();
1937 offer_native_drag(
1938 window_adapter,
1939 drag_area,
1940 item_rc.downgrade(),
1941 seed_position,
1942 result,
1943 );
1944 VisitChildrenResult::abort(item_rc.index(), 0)
1945 }
1946 }
1947}
1948
1949#[derive(FieldOffsets)]
1956#[repr(C)]
1957#[pin]
1958pub(crate) struct TextCursorBlinker {
1959 cursor_visible: Property<bool>,
1960 cursor_blink_timer: crate::timers::Timer,
1961}
1962
1963impl TextCursorBlinker {
1964 pub fn new() -> Pin<Rc<Self>> {
1967 Rc::pin(Self {
1968 cursor_visible: Property::new(true),
1969 cursor_blink_timer: Default::default(),
1970 })
1971 }
1972
1973 pub fn set_binding(
1976 instance: Pin<Rc<TextCursorBlinker>>,
1977 prop: &Property<bool>,
1978 ctx: &crate::SlintContext,
1979 cycle_duration: Duration,
1980 ) {
1981 instance.as_ref().cursor_visible.set(true);
1982 Self::start(&instance, ctx, cycle_duration);
1984 prop.set_binding(move || {
1985 TextCursorBlinker::FIELD_OFFSETS.cursor_visible().apply_pin(instance.as_ref()).get()
1986 });
1987 }
1988
1989 pub fn start(self: &Pin<Rc<Self>>, ctx: &crate::SlintContext, cycle_duration: Duration) {
1992 if self.cursor_blink_timer.running() {
1993 self.cursor_blink_timer.restart();
1994 } else {
1995 let toggle_cursor = {
1996 let weak_blinker = pin_weak::rc::PinWeak::downgrade(self.clone());
1997 move || {
1998 if let Some(blinker) = weak_blinker.upgrade() {
1999 let visible = TextCursorBlinker::FIELD_OFFSETS
2000 .cursor_visible()
2001 .apply_pin(blinker.as_ref())
2002 .get();
2003 blinker.cursor_visible.set(!visible);
2004 }
2005 }
2006 };
2007 if !cycle_duration.is_zero() {
2008 self.cursor_blink_timer.start_on(
2009 ctx,
2010 crate::timers::TimerMode::Repeated,
2011 cycle_duration / 2,
2012 toggle_cursor,
2013 );
2014 }
2015 }
2016 }
2017
2018 pub fn stop(&self) {
2021 self.cursor_blink_timer.stop()
2022 }
2023}
2024
2025#[derive(Clone, Copy, Default)]
2027struct TouchPoint {
2028 id: i32,
2029 position: LogicalPoint,
2030}
2031
2032const MAX_TRACKED_TOUCHES: usize = 5;
2038
2039#[derive(Clone)]
2040struct TouchMap {
2041 entries: [TouchPoint; MAX_TRACKED_TOUCHES],
2042 len: usize,
2043}
2044
2045impl Default for TouchMap {
2046 fn default() -> Self {
2047 Self { entries: [TouchPoint::default(); MAX_TRACKED_TOUCHES], len: 0 }
2048 }
2049}
2050
2051impl TouchMap {
2052 fn get(&self, id: i32) -> Option<&TouchPoint> {
2053 self.entries[..self.len].iter().find(|tp| tp.id == id)
2054 }
2055
2056 fn get_mut(&mut self, id: i32) -> Option<&mut TouchPoint> {
2057 self.entries[..self.len].iter_mut().find(|tp| tp.id == id)
2058 }
2059
2060 fn insert(&mut self, point: TouchPoint) {
2061 if let Some(existing) = self.entries[..self.len].iter_mut().find(|tp| tp.id == point.id) {
2062 *existing = point;
2063 } else if self.len < MAX_TRACKED_TOUCHES {
2064 self.entries[self.len] = point;
2065 self.len += 1;
2066 }
2067 }
2068
2069 fn remove(&mut self, id: i32) {
2070 if let Some(idx) = self.entries[..self.len].iter().position(|tp| tp.id == id) {
2071 self.len -= 1;
2072 self.entries[idx] = self.entries[self.len];
2073 }
2074 }
2075
2076 fn len(&self) -> usize {
2077 self.len
2078 }
2079
2080 fn first_two_ids(&self) -> Option<(i32, i32)> {
2082 if self.len >= 2 { Some((self.entries[0].id, self.entries[1].id)) } else { None }
2083 }
2084
2085 fn first(&self) -> Option<&TouchPoint> {
2087 if self.len > 0 { Some(&self.entries[0]) } else { None }
2088 }
2089}
2090
2091const MAX_TOUCH_EVENTS: usize = 4;
2097
2098#[derive(Clone)]
2099pub(crate) struct TouchEventBuffer {
2100 events: [Option<MouseEvent>; MAX_TOUCH_EVENTS],
2101 len: usize,
2102}
2103
2104impl TouchEventBuffer {
2105 fn new() -> Self {
2106 Self { events: [None, None, None, None], len: 0 }
2107 }
2108
2109 fn push(&mut self, event: MouseEvent) {
2110 debug_assert!(self.len < MAX_TOUCH_EVENTS, "TouchEventBuffer overflow");
2111 if self.len < MAX_TOUCH_EVENTS {
2112 self.events[self.len] = Some(event);
2113 self.len += 1;
2114 }
2115 }
2116
2117 pub(crate) fn into_iter(self) -> impl Iterator<Item = MouseEvent> {
2119 let len = self.len;
2120 self.events.into_iter().take(len).flatten()
2121 }
2122}
2123
2124#[derive(Default, Debug, Clone, Copy)]
2126enum GestureRecognitionState {
2127 #[default]
2129 Idle,
2130 TwoFingersDown { finger_ids: (i32, i32), initial_distance: f32, last_angle: euclid::Angle<f32> },
2132 Pinching {
2134 finger_ids: (i32, i32),
2135 initial_distance: f32,
2136 last_scale: f32,
2137 last_angle: euclid::Angle<f32>,
2138 },
2139}
2140
2141pub(crate) struct TouchState {
2148 active_touches: TouchMap,
2149 primary_touch_id: Option<i32>,
2151 gesture_state: GestureRecognitionState,
2152}
2153
2154impl Default for TouchState {
2155 fn default() -> Self {
2156 Self {
2157 active_touches: TouchMap::default(),
2158 primary_touch_id: None,
2159 gesture_state: GestureRecognitionState::Idle,
2160 }
2161 }
2162}
2163
2164impl TouchState {
2165 const PINCH_THRESHOLD: f32 = 8.0;
2167
2168 const ROTATION_THRESHOLD: f32 = 5.0;
2170
2171 fn gesture_finger_ids(&self) -> Option<(i32, i32)> {
2173 match self.gesture_state {
2174 GestureRecognitionState::TwoFingersDown { finger_ids, .. }
2175 | GestureRecognitionState::Pinching { finger_ids, .. } => Some(finger_ids),
2176 GestureRecognitionState::Idle => None,
2177 }
2178 }
2179
2180 fn geometry_for(&self, (id_a, id_b): (i32, i32)) -> Option<(f32, euclid::Angle<f32>)> {
2182 let a = self.active_touches.get(id_a)?;
2183 let b = self.active_touches.get(id_b)?;
2184 let delta = (b.position - a.position).cast::<f32>();
2185 Some((delta.length(), delta.angle_from_x_axis()))
2186 }
2187
2188 fn gesture_finger_positions(&self) -> Option<(&TouchPoint, &TouchPoint)> {
2190 let (id_a, id_b) = self.gesture_finger_ids()?;
2191 let a = self.active_touches.get(id_a)?;
2192 let b = self.active_touches.get(id_b)?;
2193 Some((a, b))
2194 }
2195
2196 fn gesture_midpoint(&self) -> Option<LogicalPoint> {
2198 let (a, b) = self.gesture_finger_positions()?;
2199 let mid = a.position.cast::<f32>().lerp(b.position.cast::<f32>(), 0.5);
2200 Some(mid.cast())
2201 }
2202
2203 fn gesture_geometry(&self) -> Option<(f32, euclid::Angle<f32>)> {
2205 let (a, b) = self.gesture_finger_positions()?;
2206 let delta = (b.position - a.position).cast::<f32>();
2207 Some((delta.length(), delta.angle_from_x_axis()))
2208 }
2209
2210 fn is_gesture_finger(&self, id: i32) -> bool {
2212 self.gesture_finger_ids().is_some_and(|(a, b)| id == a || id == b)
2213 }
2214
2215 pub(crate) fn process(
2222 &mut self,
2223 id: i32,
2224 position: LogicalPoint,
2225 phase: TouchPhase,
2226 ) -> TouchEventBuffer {
2227 let mut events = TouchEventBuffer::new();
2228 match phase {
2229 TouchPhase::Started => self.process_started(id, position, &mut events),
2230 TouchPhase::Moved => self.process_moved(id, position, &mut events),
2231 TouchPhase::Ended => self.process_ended(id, position, false, &mut events),
2232 TouchPhase::Cancelled => self.process_ended(id, position, true, &mut events),
2233 }
2234 events
2235 }
2236
2237 fn process_started(&mut self, id: i32, position: LogicalPoint, events: &mut TouchEventBuffer) {
2238 self.active_touches.insert(TouchPoint { id, position });
2239
2240 let total = self.active_touches.len();
2241 if total == 1 {
2242 self.primary_touch_id = Some(id);
2244 self.gesture_state = GestureRecognitionState::Idle;
2245 events.push(MouseEvent::Pressed {
2246 position,
2247 button: PointerEventButton::Left,
2248 click_count: 0,
2249 touch_finger_id: id + 1,
2250 });
2251 } else if total == 2 {
2252 let finger_ids = self.active_touches.first_two_ids().unwrap_or((0, 0));
2254
2255 let primary_pos = self
2258 .primary_touch_id
2259 .and_then(|pid| self.active_touches.get(pid))
2260 .map(|tp| tp.position)
2261 .unwrap_or(position);
2262
2263 let (initial_distance, last_angle) =
2265 self.geometry_for(finger_ids).unwrap_or((0.0, euclid::Angle::zero()));
2266 self.gesture_state = GestureRecognitionState::TwoFingersDown {
2267 finger_ids,
2268 initial_distance,
2269 last_angle,
2270 };
2271
2272 events.push(MouseEvent::Released {
2273 position: primary_pos,
2274 button: PointerEventButton::Left,
2275 click_count: 0,
2276 touch_finger_id: id + 1,
2277 });
2278 }
2279 }
2281
2282 #[allow(clippy::collapsible_match)]
2283 fn process_moved(&mut self, id: i32, position: LogicalPoint, events: &mut TouchEventBuffer) {
2284 if let Some(tp) = self.active_touches.get_mut(id) {
2285 tp.position = position;
2286 }
2287
2288 let is_gesture_finger = self.is_gesture_finger(id);
2289
2290 match self.gesture_state {
2291 GestureRecognitionState::Idle => {
2292 if self.primary_touch_id == Some(id) {
2293 events.push(MouseEvent::Moved { position, touch_finger_id: id + 1 });
2294 }
2295 }
2296 GestureRecognitionState::TwoFingersDown {
2297 finger_ids,
2298 initial_distance,
2299 last_angle,
2300 } if is_gesture_finger => {
2301 if let Some((dist, angle)) = self.gesture_geometry() {
2302 let delta_dist = (dist - initial_distance).abs();
2303 let delta_angle = (angle - last_angle).signed().to_degrees().abs();
2304 if delta_dist > Self::PINCH_THRESHOLD || delta_angle > Self::ROTATION_THRESHOLD
2305 {
2306 self.gesture_state = GestureRecognitionState::Pinching {
2310 finger_ids,
2311 initial_distance: dist,
2312 last_scale: 1.0,
2313 last_angle: angle,
2314 };
2315
2316 let midpoint = self.gesture_midpoint().unwrap_or(position);
2317
2318 events.push(MouseEvent::PinchGesture {
2319 position: midpoint,
2320 delta: 0.0,
2321 phase: TouchPhase::Started,
2322 });
2323 events.push(MouseEvent::RotationGesture {
2324 position: midpoint,
2325 delta: 0.0,
2326 phase: TouchPhase::Started,
2327 });
2328 }
2329 }
2330 }
2331 GestureRecognitionState::Pinching {
2332 initial_distance, last_scale, last_angle, ..
2333 } if is_gesture_finger => {
2334 if let Some((dist, angle)) = self.gesture_geometry() {
2335 let midpoint = self.gesture_midpoint().unwrap_or(position);
2336
2337 let current_scale =
2338 if initial_distance > 0.0 { dist / initial_distance } else { 1.0 };
2339 let scale_delta = current_scale - last_scale;
2340
2341 let rotation_delta = (angle - last_angle).signed().to_degrees();
2344
2345 if let GestureRecognitionState::Pinching {
2347 last_scale: ref mut ls,
2348 last_angle: ref mut la,
2349 ..
2350 } = self.gesture_state
2351 {
2352 *ls = current_scale;
2353 *la = angle;
2354 }
2355
2356 events.push(MouseEvent::PinchGesture {
2357 position: midpoint,
2358 delta: scale_delta,
2359 phase: TouchPhase::Moved,
2360 });
2361 events.push(MouseEvent::RotationGesture {
2362 position: midpoint,
2363 delta: rotation_delta,
2364 phase: TouchPhase::Moved,
2365 });
2366 }
2367 }
2368 _ => {}
2369 }
2370 }
2371
2372 #[allow(clippy::collapsible_match)]
2373 fn process_ended(
2374 &mut self,
2375 id: i32,
2376 position: LogicalPoint,
2377 is_cancelled: bool,
2378 events: &mut TouchEventBuffer,
2379 ) {
2380 let is_gesture_finger = self.is_gesture_finger(id);
2382 let midpoint = self.gesture_midpoint().unwrap_or(position);
2383 self.active_touches.remove(id);
2384
2385 match self.gesture_state {
2386 GestureRecognitionState::Idle => {
2387 if self.primary_touch_id == Some(id) {
2388 self.primary_touch_id = None;
2389 events.push(MouseEvent::Released {
2390 position,
2391 button: PointerEventButton::Left,
2392 click_count: 0,
2393 touch_finger_id: id + 1,
2394 });
2395 events.push(MouseEvent::Exit);
2396 }
2397 }
2398 GestureRecognitionState::TwoFingersDown { .. } if is_gesture_finger => {
2399 self.gesture_state = GestureRecognitionState::Idle;
2400 if !is_cancelled {
2401 if let Some(remaining) = self.active_touches.first() {
2402 let remaining_pos = remaining.position;
2403 self.primary_touch_id = Some(remaining.id);
2404 events.push(MouseEvent::Pressed {
2405 position: remaining_pos,
2406 button: PointerEventButton::Left,
2407 click_count: 0,
2408 touch_finger_id: remaining.id + 1,
2409 });
2410 } else {
2411 self.primary_touch_id = None;
2412 events.push(MouseEvent::Exit);
2413 }
2414 } else {
2415 self.primary_touch_id = None;
2416 events.push(MouseEvent::Exit);
2417 }
2418 }
2419 GestureRecognitionState::Pinching { .. } if is_gesture_finger => {
2420 self.gesture_state = GestureRecognitionState::Idle;
2421
2422 let gesture_phase =
2423 if is_cancelled { TouchPhase::Cancelled } else { TouchPhase::Ended };
2424
2425 let remaining = if !is_cancelled {
2426 self.active_touches.first().map(|tp| (tp.id, tp.position))
2427 } else {
2428 None
2429 };
2430 if let Some((rid, _)) = remaining {
2431 self.primary_touch_id = Some(rid);
2432 } else {
2433 self.primary_touch_id = None;
2434 }
2435
2436 events.push(MouseEvent::PinchGesture {
2437 position: midpoint,
2438 delta: 0.0,
2439 phase: gesture_phase,
2440 });
2441 events.push(MouseEvent::RotationGesture {
2442 position: midpoint,
2443 delta: 0.0,
2444 phase: gesture_phase,
2445 });
2446
2447 if let Some((rid, rpos)) = remaining {
2448 events.push(MouseEvent::Pressed {
2449 position: rpos,
2450 button: PointerEventButton::Left,
2451 click_count: 0,
2452 touch_finger_id: rid + 1,
2453 });
2454 } else {
2455 events.push(MouseEvent::Exit);
2456 }
2457 }
2458 _ => {}
2459 }
2460 }
2461}
2462
2463#[cfg(test)]
2464mod touch_tests {
2465 extern crate alloc;
2466 use alloc::vec;
2467 use alloc::vec::Vec;
2468
2469 use super::*;
2470 use crate::lengths::LogicalPoint;
2471
2472 fn pt(x: f32, y: f32) -> LogicalPoint {
2473 euclid::point2(x, y)
2474 }
2475
2476 #[test]
2481 fn touch_map_insert_and_get() {
2482 let mut map = TouchMap::default();
2483 assert_eq!(map.len(), 0);
2484 map.insert(TouchPoint { id: 1, position: pt(10.0, 20.0) });
2485 assert_eq!(map.len(), 1);
2486 assert!(map.get(1).is_some());
2487 assert!((map.get(1).unwrap().position.x - 10.0).abs() < f32::EPSILON);
2488 assert!(map.get(2).is_none());
2489 }
2490
2491 #[test]
2492 fn touch_map_update_existing() {
2493 let mut map = TouchMap::default();
2494 map.insert(TouchPoint { id: 1, position: pt(10.0, 20.0) });
2495 map.insert(TouchPoint { id: 1, position: pt(30.0, 40.0) });
2496 assert_eq!(map.len(), 1);
2497 assert!((map.get(1).unwrap().position.x - 30.0).abs() < f32::EPSILON);
2498 }
2499
2500 #[test]
2501 fn touch_map_remove() {
2502 let mut map = TouchMap::default();
2503 map.insert(TouchPoint { id: 1, position: pt(10.0, 20.0) });
2504 map.insert(TouchPoint { id: 2, position: pt(30.0, 40.0) });
2505 assert_eq!(map.len(), 2);
2506 map.remove(1);
2507 assert_eq!(map.len(), 1);
2508 assert!(map.get(1).is_none());
2509 assert!(map.get(2).is_some());
2510 }
2511
2512 #[test]
2513 fn touch_map_remove_nonexistent() {
2514 let mut map = TouchMap::default();
2515 map.insert(TouchPoint { id: 1, position: pt(10.0, 20.0) });
2516 map.remove(99);
2517 assert_eq!(map.len(), 1);
2518 }
2519
2520 #[test]
2521 fn touch_map_capacity() {
2522 let mut map = TouchMap::default();
2523 for i in 0..MAX_TRACKED_TOUCHES {
2524 map.insert(TouchPoint { id: i as i32, position: pt(i as f32, 0.0) });
2525 }
2526 assert_eq!(map.len(), MAX_TRACKED_TOUCHES);
2527 map.insert(TouchPoint { id: 99, position: pt(99.0, 0.0) });
2529 assert_eq!(map.len(), MAX_TRACKED_TOUCHES);
2530 assert!(map.get(99).is_none());
2531 }
2532
2533 #[test]
2534 fn touch_map_first_two_ids() {
2535 let mut map = TouchMap::default();
2536 assert!(map.first_two_ids().is_none());
2537 map.insert(TouchPoint { id: 5, position: pt(0.0, 0.0) });
2538 assert!(map.first_two_ids().is_none());
2539 map.insert(TouchPoint { id: 10, position: pt(0.0, 0.0) });
2540 assert_eq!(map.first_two_ids(), Some((5, 10)));
2541 }
2542
2543 #[test]
2544 fn touch_map_first() {
2545 let mut map = TouchMap::default();
2546 assert!(map.first().is_none());
2547 map.insert(TouchPoint { id: 7, position: pt(1.0, 2.0) });
2548 let tp = map.first().unwrap();
2549 assert_eq!(tp.id, 7);
2550 assert!((tp.position.x - 1.0).abs() < f32::EPSILON);
2551 }
2552
2553 #[test]
2554 fn touch_map_get_mut() {
2555 let mut map = TouchMap::default();
2556 map.insert(TouchPoint { id: 1, position: pt(0.0, 0.0) });
2557 map.get_mut(1).unwrap().position = pt(5.0, 6.0);
2558 assert!((map.get(1).unwrap().position.x - 5.0).abs() < f32::EPSILON);
2559 }
2560
2561 #[derive(Debug, PartialEq)]
2566 enum Ev {
2567 Pressed(f32, f32),
2568 Released(f32, f32),
2569 Moved(f32, f32),
2570 Exit,
2571 PinchStarted,
2572 PinchMoved(f32),
2573 PinchEnded,
2574 PinchCancelled,
2575 RotationStarted,
2576 RotationMoved(f32),
2577 RotationEnded,
2578 RotationCancelled,
2579 }
2580
2581 fn classify(events: &TouchEventBuffer) -> Vec<Ev> {
2582 events
2583 .clone()
2584 .into_iter()
2585 .map(|e| match e {
2586 MouseEvent::Pressed { position, .. } => Ev::Pressed(position.x, position.y),
2587 MouseEvent::Released { position, .. } => Ev::Released(position.x, position.y),
2588 MouseEvent::Moved { position, .. } => Ev::Moved(position.x, position.y),
2589 MouseEvent::Exit => Ev::Exit,
2590 MouseEvent::PinchGesture { delta, phase, .. } => match phase {
2591 TouchPhase::Started => Ev::PinchStarted,
2592 TouchPhase::Moved => Ev::PinchMoved(delta),
2593 TouchPhase::Ended => Ev::PinchEnded,
2594 TouchPhase::Cancelled => Ev::PinchCancelled,
2595 },
2596 MouseEvent::RotationGesture { delta, phase, .. } => match phase {
2597 TouchPhase::Started => Ev::RotationStarted,
2598 TouchPhase::Moved => Ev::RotationMoved(delta),
2599 TouchPhase::Ended => Ev::RotationEnded,
2600 TouchPhase::Cancelled => Ev::RotationCancelled,
2601 },
2602 _ => panic!("unexpected event: {:?}", e),
2603 })
2604 .collect()
2605 }
2606
2607 #[test]
2612 fn single_finger_press_move_release() {
2613 let mut state = TouchState::default();
2614
2615 let evs = state.process(1, pt(100.0, 200.0), TouchPhase::Started);
2616 assert_eq!(classify(&evs), vec![Ev::Pressed(100.0, 200.0)]);
2617
2618 let evs = state.process(1, pt(110.0, 200.0), TouchPhase::Moved);
2619 assert_eq!(classify(&evs), vec![Ev::Moved(110.0, 200.0)]);
2620
2621 let evs = state.process(1, pt(110.0, 200.0), TouchPhase::Ended);
2622 assert_eq!(classify(&evs), vec![Ev::Released(110.0, 200.0), Ev::Exit]);
2623 }
2624
2625 #[test]
2626 fn single_finger_cancel() {
2627 let mut state = TouchState::default();
2628
2629 state.process(1, pt(100.0, 200.0), TouchPhase::Started);
2630
2631 let evs = state.process(1, pt(100.0, 200.0), TouchPhase::Cancelled);
2632 assert_eq!(classify(&evs), vec![Ev::Released(100.0, 200.0), Ev::Exit]);
2633 }
2634
2635 #[test]
2636 fn non_primary_move_ignored() {
2637 let mut state = TouchState::default();
2638 state.process(1, pt(100.0, 200.0), TouchPhase::Started);
2640
2641 let evs = state.process(99, pt(50.0, 50.0), TouchPhase::Moved);
2643 assert!(classify(&evs).is_empty());
2644 }
2645
2646 #[test]
2651 fn two_fingers_synthesize_release_then_gesture() {
2652 let mut state = TouchState::default();
2653
2654 let evs = state.process(1, pt(100.0, 200.0), TouchPhase::Started);
2656 assert_eq!(classify(&evs), vec![Ev::Pressed(100.0, 200.0)]);
2657
2658 let evs = state.process(2, pt(200.0, 200.0), TouchPhase::Started);
2660 assert_eq!(classify(&evs), vec![Ev::Released(100.0, 200.0)]);
2661 assert!(matches!(state.gesture_state, GestureRecognitionState::TwoFingersDown { .. }));
2662
2663 let evs = state.process(2, pt(220.0, 200.0), TouchPhase::Moved);
2665 assert_eq!(classify(&evs), vec![Ev::PinchStarted, Ev::RotationStarted]);
2666 assert!(matches!(state.gesture_state, GestureRecognitionState::Pinching { .. }));
2667 }
2668
2669 #[test]
2670 fn two_fingers_below_threshold_no_gesture() {
2671 let mut state = TouchState::default();
2672
2673 state.process(1, pt(100.0, 200.0), TouchPhase::Started);
2674 state.process(2, pt(200.0, 200.0), TouchPhase::Started);
2675
2676 let evs = state.process(2, pt(202.0, 200.0), TouchPhase::Moved);
2678 assert!(classify(&evs).is_empty());
2679 assert!(matches!(state.gesture_state, GestureRecognitionState::TwoFingersDown { .. }));
2680 }
2681
2682 #[test]
2683 fn pinch_produces_scale_deltas() {
2684 let mut state = TouchState::default();
2685
2686 state.process(1, pt(0.0, 0.0), TouchPhase::Started);
2688 state.process(2, pt(100.0, 0.0), TouchPhase::Started);
2689
2690 state.process(2, pt(120.0, 0.0), TouchPhase::Moved);
2692 assert!(matches!(state.gesture_state, GestureRecognitionState::Pinching { .. }));
2693
2694 let evs = state.process(2, pt(180.0, 0.0), TouchPhase::Moved);
2698 let classified = classify(&evs);
2699 assert_eq!(classified.len(), 2);
2700 if let Ev::PinchMoved(delta) = classified[0] {
2701 assert!((delta - 0.5).abs() < 0.01, "expected ~0.5, got {}", delta);
2702 } else {
2703 panic!("expected PinchMoved, got {:?}", classified[0]);
2704 }
2705 }
2706
2707 #[test]
2708 fn rotation_produces_correct_deltas() {
2709 let mut state = TouchState::default();
2710
2711 state.process(1, pt(0.0, 0.0), TouchPhase::Started);
2714 state.process(2, pt(100.0, 0.0), TouchPhase::Started);
2715
2716 state.process(2, pt(120.0, 0.0), TouchPhase::Moved);
2718 assert!(matches!(state.gesture_state, GestureRecognitionState::Pinching { .. }));
2719
2720 let evs = state.process(2, pt(70.7, 70.7), TouchPhase::Moved);
2725 let classified = classify(&evs);
2726 assert_eq!(classified.len(), 2);
2727 if let Ev::RotationMoved(delta) = classified[1] {
2728 assert!((delta - 45.0).abs() < 1.0, "expected ~45.0 (clockwise), got {}", delta);
2729 } else {
2730 panic!("expected RotationMoved, got {:?}", classified[1]);
2731 }
2732 }
2733
2734 #[test]
2735 fn rotation_across_180_degree_boundary() {
2736 let mut state = TouchState::default();
2737
2738 state.process(1, pt(0.0, 0.0), TouchPhase::Started);
2741 state.process(2, pt(-100.0, -10.0), TouchPhase::Started);
2742
2743 state.process(2, pt(-120.0, -10.0), TouchPhase::Moved);
2745 assert!(matches!(state.gesture_state, GestureRecognitionState::Pinching { .. }));
2746
2747 let evs = state.process(2, pt(-100.0, 10.0), TouchPhase::Moved);
2752 let classified = classify(&evs);
2753 if let Ev::RotationMoved(delta) = classified[1] {
2754 assert!(
2755 delta.abs() < 20.0,
2756 "rotation should be a small delta (~11°), got {} (discontinuity!)",
2757 delta
2758 );
2759 } else {
2760 panic!("expected RotationMoved, got {:?}", classified[1]);
2761 }
2762 }
2763
2764 #[test]
2769 fn pinch_end_with_remaining_finger() {
2770 let mut state = TouchState::default();
2771
2772 state.process(1, pt(0.0, 0.0), TouchPhase::Started);
2773 state.process(2, pt(100.0, 0.0), TouchPhase::Started);
2774 state.process(2, pt(120.0, 0.0), TouchPhase::Moved);
2776
2777 let evs = state.process(2, pt(120.0, 0.0), TouchPhase::Ended);
2779 let classified = classify(&evs);
2780 assert_eq!(classified, vec![Ev::PinchEnded, Ev::RotationEnded, Ev::Pressed(0.0, 0.0)]);
2781 assert!(matches!(state.gesture_state, GestureRecognitionState::Idle));
2782 assert_eq!(state.primary_touch_id, Some(1));
2783 }
2784
2785 #[test]
2786 fn pinch_cancel_emits_cancelled_and_exit() {
2787 let mut state = TouchState::default();
2788
2789 state.process(1, pt(0.0, 0.0), TouchPhase::Started);
2790 state.process(2, pt(100.0, 0.0), TouchPhase::Started);
2791 state.process(2, pt(120.0, 0.0), TouchPhase::Moved);
2792
2793 let evs = state.process(2, pt(120.0, 0.0), TouchPhase::Cancelled);
2795 let classified = classify(&evs);
2796 assert_eq!(classified, vec![Ev::PinchCancelled, Ev::RotationCancelled, Ev::Exit]);
2797 assert!(state.primary_touch_id.is_none());
2798 }
2799
2800 #[test]
2801 fn two_fingers_down_lift_before_threshold_returns_to_idle() {
2802 let mut state = TouchState::default();
2803
2804 state.process(1, pt(100.0, 200.0), TouchPhase::Started);
2805 state.process(2, pt(200.0, 200.0), TouchPhase::Started);
2806 assert!(matches!(state.gesture_state, GestureRecognitionState::TwoFingersDown { .. }));
2807
2808 let evs = state.process(2, pt(200.0, 200.0), TouchPhase::Ended);
2810 let classified = classify(&evs);
2811 assert_eq!(classified, vec![Ev::Pressed(100.0, 200.0)]);
2813 assert!(matches!(state.gesture_state, GestureRecognitionState::Idle));
2814 assert_eq!(state.primary_touch_id, Some(1));
2815 }
2816
2817 #[test]
2818 fn two_fingers_down_cancel_both_emits_exit() {
2819 let mut state = TouchState::default();
2820
2821 state.process(1, pt(100.0, 200.0), TouchPhase::Started);
2822 state.process(2, pt(200.0, 200.0), TouchPhase::Started);
2823
2824 let evs = state.process(2, pt(200.0, 200.0), TouchPhase::Cancelled);
2826 assert_eq!(classify(&evs), vec![Ev::Exit]);
2827
2828 let evs = state.process(1, pt(100.0, 200.0), TouchPhase::Cancelled);
2830 assert!(classify(&evs).is_empty());
2831 }
2832
2833 #[test]
2838 fn third_finger_ignored_for_gesture() {
2839 let mut state = TouchState::default();
2840
2841 state.process(1, pt(0.0, 0.0), TouchPhase::Started);
2842 state.process(2, pt(100.0, 0.0), TouchPhase::Started);
2843
2844 let evs = state.process(3, pt(50.0, 50.0), TouchPhase::Started);
2846 assert!(classify(&evs).is_empty());
2847 assert_eq!(state.active_touches.len(), 3);
2848 }
2849
2850 #[test]
2855 fn euclid_angle_signed_wrapping() {
2856 use euclid::Angle;
2857 let wrap = |deg: f32| Angle::degrees(deg).signed().to_degrees();
2858 assert!(wrap(0.0).abs() < f32::EPSILON);
2859 assert!((wrap(180.0) - 180.0).abs() < 0.01);
2860 assert!((wrap(181.0) - (-179.0)).abs() < 0.01);
2861 assert!((wrap(-181.0) - 179.0).abs() < 0.01);
2862 assert!(wrap(360.0).abs() < 0.01);
2863 }
2864
2865 #[test]
2866 fn zero_distance_fingers_no_division_by_zero() {
2867 let mut state = TouchState::default();
2868
2869 state.process(1, pt(100.0, 100.0), TouchPhase::Started);
2871 state.process(2, pt(100.0, 100.0), TouchPhase::Started);
2872 assert!(matches!(state.gesture_state, GestureRecognitionState::TwoFingersDown { .. }));
2873
2874 let evs = state.process(2, pt(120.0, 100.0), TouchPhase::Moved);
2876 assert!(matches!(state.gesture_state, GestureRecognitionState::Pinching { .. }));
2877 let classified = classify(&evs);
2878 assert_eq!(classified.len(), 2);
2879 assert_eq!(classified[0], Ev::PinchStarted);
2880
2881 let evs = state.process(2, pt(140.0, 100.0), TouchPhase::Moved);
2884 let classified = classify(&evs);
2885 if let Ev::PinchMoved(delta) = classified[0] {
2886 assert!(delta.is_finite(), "scale delta should be finite, got {}", delta);
2887 } else {
2888 panic!("expected PinchMoved, got {:?}", classified[0]);
2889 }
2890 }
2891}
2892
2893#[cfg(test)]
2894mod tests {
2895 use super::*;
2896 extern crate alloc;
2897
2898 #[test]
2899 fn test_to_string() {
2900 let test_cases = [
2901 (
2902 "a",
2903 KeyboardModifiers { alt: false, control: true, shift: false, meta: false },
2904 false,
2905 false,
2906 "⌘A",
2907 "Ctrl+A",
2908 "Ctrl+A",
2909 ),
2910 (
2911 "a",
2912 KeyboardModifiers { alt: true, control: true, shift: true, meta: true },
2913 false,
2914 false,
2915 "⌃⌥⇧⌘A",
2916 "Win+Ctrl+Alt+Shift+A",
2917 "Super+Ctrl+Alt+Shift+A",
2918 ),
2919 (
2920 "\u{001b}",
2921 KeyboardModifiers { alt: false, control: true, shift: true, meta: false },
2922 false,
2923 false,
2924 "⇧⌘Escape",
2925 "Ctrl+Shift+Escape",
2926 "Ctrl+Shift+Escape",
2927 ),
2928 (
2929 "+",
2930 KeyboardModifiers { alt: false, control: true, shift: false, meta: false },
2931 true,
2932 false,
2933 "⌘+",
2934 "Ctrl++",
2935 "Ctrl++",
2936 ),
2937 (
2938 "a",
2939 KeyboardModifiers { alt: true, control: true, shift: false, meta: false },
2940 false,
2941 true,
2942 "⌘A",
2943 "Ctrl+A",
2944 "Ctrl+A",
2945 ),
2946 (
2947 "",
2948 KeyboardModifiers { alt: false, control: true, shift: false, meta: false },
2949 false,
2950 false,
2951 "",
2952 "",
2953 "",
2954 ),
2955 (
2956 "\u{000a}",
2957 KeyboardModifiers { alt: false, control: false, shift: false, meta: false },
2958 false,
2959 false,
2960 "Return",
2961 "Return",
2962 "Return",
2963 ),
2964 (
2965 "\u{0009}",
2966 KeyboardModifiers { alt: false, control: false, shift: false, meta: false },
2967 false,
2968 false,
2969 "Tab",
2970 "Tab",
2971 "Tab",
2972 ),
2973 (
2974 "\u{0020}",
2975 KeyboardModifiers { alt: false, control: false, shift: false, meta: false },
2976 false,
2977 false,
2978 "Space",
2979 "Space",
2980 "Space",
2981 ),
2982 (
2983 "\u{0008}",
2984 KeyboardModifiers { alt: false, control: false, shift: false, meta: false },
2985 false,
2986 false,
2987 "Backspace",
2988 "Backspace",
2989 "Backspace",
2990 ),
2991 ];
2992
2993 for (
2994 key,
2995 modifiers,
2996 ignore_shift,
2997 ignore_alt,
2998 _expected_macos,
2999 _expected_windows,
3000 _expected_linux,
3001 ) in test_cases
3002 {
3003 let shortcut = make_keys(key.into(), modifiers, ignore_shift, ignore_alt);
3004
3005 use crate::alloc::string::ToString;
3006 let result = shortcut.to_string();
3007
3008 #[cfg(target_os = "macos")]
3009 assert_eq!(result.as_str(), _expected_macos, "Failed for key: {:?}", key);
3010
3011 #[cfg(target_os = "windows")]
3012 assert_eq!(result.as_str(), _expected_windows, "Failed for key: {:?}", key);
3013
3014 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
3015 assert_eq!(result.as_str(), _expected_linux, "Failed for key: {:?}", key);
3016 }
3017 }
3018
3019 #[test]
3020 fn test_from_parts_valid() {
3021 let f5_key = alloc::string::String::from(char::from(key_codes::Key::F5));
3022 let ret_key = alloc::string::String::from(char::from(key_codes::Key::Return));
3023 let pause_key = alloc::string::String::from(char::from(key_codes::Key::Pause));
3024
3025 let cases: &[(&str, &[&str], &str, KeyboardModifiers, bool, bool)] = &[
3027 (
3028 "Control+A",
3029 &["Control", "A"],
3030 "a",
3031 KeyboardModifiers { control: true, ..Default::default() },
3032 false,
3033 false,
3034 ),
3035 (
3036 "Control+Shift+A",
3037 &["Control", "Shift", "A"],
3038 "a",
3039 KeyboardModifiers { control: true, shift: true, ..Default::default() },
3040 false,
3041 false,
3042 ),
3043 (
3044 "Control+Shift?+Z (explicit ignore_shift)",
3045 &["Control", "Shift?", "Z"],
3046 "z",
3047 KeyboardModifiers { control: true, ..Default::default() },
3048 true,
3049 false,
3050 ),
3051 (
3052 "Control+Alt?+A (ignore_alt)",
3053 &["Control", "Alt?", "A"],
3054 "a",
3055 KeyboardModifiers { control: true, ..Default::default() },
3056 false,
3057 true,
3058 ),
3059 (
3060 "F5 alone (special key)",
3061 &["F5"],
3062 &f5_key,
3063 KeyboardModifiers::default(),
3064 false,
3065 false,
3066 ),
3067 ("Return key", &["Return"], &ret_key, KeyboardModifiers::default(), false, false),
3068 (
3069 "Control+Plus (LocalizedShiftable → auto ignore_shift)",
3070 &["Control", "Plus"],
3071 "+",
3072 KeyboardModifiers { control: true, ..Default::default() },
3073 true,
3074 false,
3075 ),
3076 (
3077 "Control+'+' (literal, no auto ignore_shift)",
3078 &["Control", "+"],
3079 "+",
3080 KeyboardModifiers { control: true, ..Default::default() },
3081 false,
3082 false,
3083 ),
3084 (
3085 "Control+Shift+Alt+A (all modifiers)",
3086 &["Control", "Shift", "Alt", "A"],
3087 "a",
3088 KeyboardModifiers { control: true, shift: true, alt: true, ..Default::default() },
3089 false,
3090 false,
3091 ),
3092 ("empty input → Keys::default()", &[], "", KeyboardModifiers::default(), false, false),
3093 (
3094 "Control+€ (unicode literal)",
3095 &["Control", "€"],
3096 "€",
3097 KeyboardModifiers { control: true, ..Default::default() },
3098 false,
3099 false,
3100 ),
3101 (
3102 "Control+é (lowercase literal)",
3103 &["Control", "é"],
3104 "é",
3105 KeyboardModifiers { control: true, ..Default::default() },
3106 false,
3107 false,
3108 ),
3109 ("A alone (named key)", &["A"], "a", KeyboardModifiers::default(), false, false),
3110 (
3114 "F5 codepoint literal",
3115 &[&f5_key],
3116 &f5_key,
3117 KeyboardModifiers::default(),
3118 false,
3119 false,
3120 ),
3121 (
3122 "Pause codepoint literal",
3123 &[&pause_key],
3124 &pause_key,
3125 KeyboardModifiers::default(),
3126 false,
3127 false,
3128 ),
3129 (
3130 "Control + F5 codepoint literal",
3131 &["Control", &f5_key],
3132 &f5_key,
3133 KeyboardModifiers { control: true, ..Default::default() },
3134 false,
3135 false,
3136 ),
3137 ("\" \" literal → Space", &[" "], " ", KeyboardModifiers::default(), false, false),
3140 ("Space named", &["Space"], " ", KeyboardModifiers::default(), false, false),
3141 ("\"\\t\" literal → Tab", &["\t"], "\t", KeyboardModifiers::default(), false, false),
3142 ("Tab named", &["Tab"], "\t", KeyboardModifiers::default(), false, false),
3143 (
3144 "\"\\n\" literal → Return",
3145 &["\n"],
3146 &ret_key,
3147 KeyboardModifiers::default(),
3148 false,
3149 false,
3150 ),
3151 (
3152 "Control+\" \" (literal space with a modifier)",
3153 &["Control", " "],
3154 " ",
3155 KeyboardModifiers { control: true, ..Default::default() },
3156 false,
3157 false,
3158 ),
3159 (
3160 "empty part is skipped → Keys::default()",
3161 &[""],
3162 "",
3163 KeyboardModifiers::default(),
3164 false,
3165 false,
3166 ),
3167 (
3168 "a alone (literal fallback, same result as named A)",
3169 &["a"],
3170 "a",
3171 KeyboardModifiers::default(),
3172 false,
3173 false,
3174 ),
3175 ];
3176
3177 for (desc, parts, expected_key, mods, is, ia) in cases {
3178 let result =
3179 Keys::from_parts(parts.iter().copied()).unwrap_or_else(|e| panic!("{desc}: {e}"));
3180 assert_eq!(result, make_keys((*expected_key).into(), *mods, *is, *ia), "{desc}");
3181 }
3182 }
3183
3184 #[test]
3185 fn test_from_parts_invalid() {
3186 use super::KeysParseErrorInner;
3187 let cases: &[(&str, &[&str], KeysParseError)] = &[
3188 ("lowercase 'control'", &["control", "A"], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3190 ("uppercase 'CONTROL'", &["CONTROL", "A"], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3191 ("'Ctrl' alias", &["Ctrl", "A"], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3192 ("'ctrl' alias", &["ctrl", "A"], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3193 ("'Win' alias", &["Win", "A"], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3195 ("'Super' alias", &["Super", "A"], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3196 ("modifiers only", &["Control", "Shift"], KeysParseError(KeysParseErrorInner::NoKey)),
3198 ("two keys", &["A", "B"], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3200 (
3202 "multi-char unknown",
3203 &["Control", "Foobar"],
3204 KeysParseError(KeysParseErrorInner::MultipleGraphemeClusters("Foobar".into())),
3205 ),
3206 (
3207 "two-char literal",
3208 &["Control", "ab"],
3209 KeysParseError(KeysParseErrorInner::MultipleGraphemeClusters("ab".into())),
3210 ),
3211 (
3214 "padded modifier ' Control ' alone",
3215 &[" Control "],
3216 KeysParseError(KeysParseErrorInner::MultipleGraphemeClusters(" Control ".into())),
3217 ),
3218 (
3219 "padded modifier ' Control ' is a key, so 'A' is a second key",
3220 &[" Control ", "A"],
3221 KeysParseError(KeysParseErrorInner::MultipleKeys),
3222 ),
3223 (
3224 "padded key ' A '",
3225 &["Control", " A "],
3226 KeysParseError(KeysParseErrorInner::MultipleGraphemeClusters(" A ".into())),
3227 ),
3228 ("two space literals", &[" ", " "], KeysParseError(KeysParseErrorInner::MultipleKeys)),
3230 (
3231 "lowercase 'return' (not a named key)",
3232 &["return"],
3233 KeysParseError(KeysParseErrorInner::MultipleGraphemeClusters("return".into())),
3234 ),
3235 (
3237 "uppercase literal É",
3238 &["Control", "É"],
3239 KeysParseError(KeysParseErrorInner::NotLowercase("É".into())),
3240 ),
3241 (
3243 "Shift + Shift?",
3244 &["Shift", "Shift?", "A"],
3245 KeysParseError(KeysParseErrorInner::IncompatibleModifiers("Shift and Shift? cannot be combined".into())),
3246 ),
3247 (
3248 "Alt + Alt?",
3249 &["Alt", "Alt?", "A"],
3250 KeysParseError(KeysParseErrorInner::IncompatibleModifiers("Alt and Alt? cannot be combined".into())),
3251 ),
3252 (
3253 "Shift + LocalizedShiftable key (Plus)",
3254 &["Control", "Shift", "Plus"],
3255 KeysParseError(KeysParseErrorInner::IncompatibleModifiers(
3256 "Key bindings involving Plus ignore Shift to support different keyboard layouts; remove Shift".into(),
3257 )),
3258 ),
3259 ];
3260
3261 for (desc, parts, expected_err) in cases {
3262 let result = Keys::from_parts(parts.iter().copied());
3263 assert!(result.is_err(), "{desc}: expected error, got {result:?}");
3264 assert_eq!(&result.unwrap_err(), expected_err, "{desc}");
3265 }
3266 }
3267
3268 #[test]
3269 fn test_to_parts_roundtrip() {
3270 let inputs: &[&[&str]] = &[
3272 &[],
3273 &["A"],
3274 &["Control", "A"],
3275 &["Control", "Shift", "A"],
3276 &["Control", "Shift?", "Z"],
3277 &["Control", "Alt?", "A"],
3278 &["Control", "Alt", "Shift", "Meta", "A"],
3279 &["Meta", "Control", "Alt", "Shift", "A"],
3280 &["F5"],
3281 &["Return"],
3282 &["Space"],
3283 &[" "], &["\t"], &["\n"], &["Control", " "],
3287 &["Control", "Plus"], &["Control", "+"], &["Control", "Digit0"],
3290 &["Control", "€"],
3291 &["Control", "é"],
3292 ];
3293 for parts in inputs {
3294 let k = Keys::from_parts(parts.iter().copied()).unwrap();
3295 let out_strs: alloc::vec::Vec<&str> = k.to_parts().collect();
3296 let k2 = Keys::from_parts(out_strs.iter().copied()).unwrap();
3297 assert_eq!(k, k2, "round-trip mismatch for {parts:?} → {out_strs:?}");
3298 }
3299 }
3300
3301 #[test]
3302 fn test_to_parts_canonical_form() {
3303 let f5 = alloc::string::String::from(char::from(key_codes::Key::F5));
3306 let ret = alloc::string::String::from(char::from(key_codes::Key::Return));
3307 let pause = alloc::string::String::from(char::from(key_codes::Key::Pause));
3308 let cases: &[(&[&str], &[&str])] = &[
3309 (&[], &[]),
3310 (&["A"], &["a"]), (&["a"], &["a"]),
3312 (&["Control", "S"], &["Control", "s"]),
3313 (&["Control", "Shift?", "Z"], &["Control", "Shift?", "z"]),
3314 (&["Control", "Alt?", "A"], &["Control", "Alt?", "a"]),
3315 (&["F5"], &[&f5]),
3316 (&[&f5], &[&f5]), (&["Pause"], &[&pause]),
3318 (&[&pause], &[&pause]),
3319 (&[" "], &[" "]),
3323 (&["Space"], &[" "]),
3324 (&["\t"], &["\t"]),
3325 (&["Tab"], &["\t"]),
3326 (&["\n"], &[&ret]),
3327 (&["Return"], &[&ret]),
3328 (&["Control", " "], &["Control", " "]),
3329 (&["Control", "Plus"], &["Control", "Shift?", "+"]),
3332 (&["Control", "+"], &["Control", "+"]), (&["Control", "€"], &["Control", "€"]),
3334 (&["Meta", "Control", "Alt", "Shift", "A"], &["Meta", "Control", "Alt", "Shift", "a"]),
3335 ];
3336 for (input, expected) in cases {
3337 let k = Keys::from_parts(input.iter().copied()).unwrap();
3338 let out_strs: alloc::vec::Vec<&str> = k.to_parts().collect();
3339 assert_eq!(&out_strs.as_slice(), expected, "for input {input:?}");
3340 }
3341 }
3342
3343 #[test]
3344 fn test_from_parts_matching() {
3345 let cases: &[(&str, &[&str], &str, KeyboardModifiers, bool)] = &[
3347 (
3348 "Control+A matches",
3349 &["Control", "A"],
3350 "a",
3351 KeyboardModifiers { control: true, ..Default::default() },
3352 true,
3353 ),
3354 (
3355 "Control+A wrong key",
3356 &["Control", "A"],
3357 "b",
3358 KeyboardModifiers { control: true, ..Default::default() },
3359 false,
3360 ),
3361 (
3362 "Control+A wrong modifier",
3363 &["Control", "A"],
3364 "a",
3365 KeyboardModifiers { alt: true, ..Default::default() },
3366 false,
3367 ),
3368 (
3369 "Shift? matches with shift",
3370 &["Control", "Shift?", "Z"],
3371 "z",
3372 KeyboardModifiers { control: true, shift: true, ..Default::default() },
3373 true,
3374 ),
3375 (
3376 "Shift? matches without shift",
3377 &["Control", "Shift?", "Z"],
3378 "z",
3379 KeyboardModifiers { control: true, ..Default::default() },
3380 true,
3381 ),
3382 ];
3383
3384 for (desc, parts, text, mods, expected) in cases {
3385 let k =
3386 Keys::from_parts(parts.iter().copied()).unwrap_or_else(|e| panic!("{desc}: {e}"));
3387 let event = KeyEvent { text: (*text).into(), modifiers: *mods, ..Default::default() };
3388 assert_eq!(k.matches(&event), *expected, "{desc}");
3389 }
3390
3391 let return_char: char = key_codes::Key::Return.into();
3393 let k = Keys::from_parts(["Return"]).unwrap();
3394 let event = KeyEvent {
3395 text: SharedString::from(alloc::string::String::from(return_char)),
3396 modifiers: KeyboardModifiers::default(),
3397 ..Default::default()
3398 };
3399 assert!(k.matches(&event), "Return key should match Return event");
3400 }
3401}