zng_ext_input/
gesture.rs

1//! Aggregate events.
2
3use parking_lot::Mutex;
4use std::{
5    collections::{HashMap, HashSet},
6    convert::TryFrom,
7    num::NonZeroU32,
8    sync::Arc,
9    time::Duration,
10};
11use zng_app::{
12    AppExtension, DInstant, HeadlessApp,
13    access::{ACCESS_CLICK_EVENT, AccessClickArgs},
14    event::{AnyEventArgs, Command, CommandScope, EVENTS, EventPropagationHandle, event, event_args},
15    shortcut::{
16        CommandShortcutExt, GestureKey, KeyChord, KeyGesture, ModifierGesture, ModifiersState, Shortcut, ShortcutFilter, Shortcuts,
17        shortcut,
18    },
19    update::EventUpdate,
20    view_process::raw_device_events::DeviceId,
21    widget::{
22        WidgetId,
23        info::{HitTestInfo, InteractionPath, WidgetPath},
24    },
25    window::WindowId,
26};
27use zng_app_context::app_local;
28use zng_ext_window::WINDOWS;
29use zng_handle::{Handle, HandleOwner, WeakHandle};
30use zng_layout::unit::DipPoint;
31use zng_var::{ArcVar, Var, var};
32use zng_view_api::{
33    keyboard::{Key, KeyCode, KeyLocation, KeyState, NativeKeyCode},
34    mouse::MouseButton,
35};
36
37use crate::{
38    focus::{FOCUS, FocusRequest, FocusTarget},
39    keyboard::{HeadlessAppKeyboardExt, KEY_INPUT_EVENT, KeyInputArgs},
40    mouse::{MOUSE_CLICK_EVENT, MouseClickArgs},
41    touch::{TOUCH_LONG_PRESS_EVENT, TOUCH_TAP_EVENT, TouchLongPressArgs, TouchTapArgs},
42};
43
44/// Specific information from the source of a [`ClickArgs`].
45#[derive(Debug, Clone)]
46pub enum ClickArgsSource {
47    /// Click event was generated by the [mouse click event](MOUSE_CLICK_EVENT).
48    Mouse {
49        /// Which mouse button generated the event.
50        button: MouseButton,
51
52        /// Position of the mouse in the window.
53        position: DipPoint,
54
55        /// Hit-test result for the mouse point in the window, at the moment the click event
56        /// was generated.
57        hits: HitTestInfo,
58    },
59
60    /// Click event was generated by the [touch tap event](TOUCH_TAP_EVENT) or [touch long press](TOUCH_LONG_PRESS_EVENT).
61    Touch {
62        /// Position of the touch point in the window.
63        position: DipPoint,
64
65        /// Hit-test result for the touch point in the window, at the moment the tap event
66        /// was generated.
67        hits: HitTestInfo,
68
69        /// Is `true` if the source event was a tab, is `false` if the source event was long press.
70        is_tap: bool,
71    },
72
73    /// Click event was generated by the [shortcut event](SHORTCUT_EVENT).
74    Shortcut {
75        /// The shortcut.
76        shortcut: Shortcut,
77        /// Kind of click represented by the `shortcut`.
78        kind: ShortcutClick,
79    },
80
81    /// Click event was generated by [accessibility event](ACCESS_CLICK_EVENT).
82    ///
83    /// This is handled the same way as a `Shortcut` event.
84    Access {
85        /// Is `true` if the requested primary action, is `false` is requested context action.
86        is_primary: bool,
87    },
88}
89
90/// What kind of click a shortcut represents in a [`ClickArgsSource::Shortcut`].
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum ShortcutClick {
93    /// The shortcut represents a primary click on the focused widget.
94    Primary,
95    /// The shortcut represents a context click on the focused widget.
96    Context,
97}
98
99event_args! {
100    /// [`CLICK_EVENT`] arguments.
101    ///
102    /// [`CLICK_EVENT`]: crate::gesture::CLICK_EVENT
103    pub struct ClickArgs {
104        /// Id of window that received the event.
105        pub window_id: WindowId,
106
107        /// Id of device that generated the event.
108        ///
109        /// Is `None` if the event was generated programmatically.
110        pub device_id: Option<DeviceId>,
111
112        /// Specific info from the source of this event.
113        pub source: ClickArgsSource,
114
115        /// Sequential click count. Number `1` is single click, `2` is double click, etc.
116        ///
117        /// Mouse clicks are translated directly, keyboard clicks are the key repeat count plus one.
118        pub click_count: NonZeroU32,
119
120        /// If the event was auto-generated by holding the key or button pressed.
121        pub is_repeat: bool,
122
123        /// What modifier keys where pressed when this event happened.
124        pub modifiers: ModifiersState,
125
126        /// The mouse input top-most hit or the focused element at the time of the key input.
127        pub target: InteractionPath,
128
129        ..
130
131        /// The [`target`].
132        ///
133        /// [`target`]: Self::target
134        fn delivery_list(&self, list: &mut UpdateDeliveryList) {
135            list.insert_wgt(&self.target)
136        }
137    }
138
139    /// [`SHORTCUT_EVENT`] arguments.
140    pub struct ShortcutArgs {
141        /// Id of window that received the event.
142        pub window_id: WindowId,
143
144        /// Id of device that generated the event.
145        ///
146        /// Is `None` if the event was generated programmatically.
147        pub device_id: Option<DeviceId>,
148
149        /// The shortcut.
150        pub shortcut: Shortcut,
151
152        /// Number of repeats generated by holding the key pressed.
153        ///
154        /// This is zero for the first key press, increments by one for each event while the key is held pressed.
155        pub repeat_count: u32,
156
157        /// Actions that will run if this event propagation is not stopped.
158        pub actions: ShortcutActions,
159
160        ..
161
162        /// No target, only app extensions.
163        fn delivery_list(&self, _list: &mut UpdateDeliveryList) {}
164    }
165}
166impl From<MouseClickArgs> for ClickArgs {
167    fn from(args: MouseClickArgs) -> Self {
168        ClickArgs::new(
169            args.timestamp,
170            args.propagation().clone(),
171            args.window_id,
172            args.device_id,
173            ClickArgsSource::Mouse {
174                button: args.button,
175                position: args.position,
176                hits: args.hits,
177            },
178            args.click_count,
179            args.is_repeat,
180            args.modifiers,
181            args.target,
182        )
183    }
184}
185impl From<TouchTapArgs> for ClickArgs {
186    fn from(args: TouchTapArgs) -> Self {
187        ClickArgs::new(
188            args.timestamp,
189            args.propagation().clone(),
190            args.window_id,
191            args.device_id,
192            ClickArgsSource::Touch {
193                position: args.position,
194                hits: args.hits,
195                is_tap: true,
196            },
197            args.tap_count,
198            false,
199            args.modifiers,
200            args.target,
201        )
202    }
203}
204impl From<TouchLongPressArgs> for ClickArgs {
205    fn from(args: TouchLongPressArgs) -> Self {
206        ClickArgs::new(
207            args.timestamp,
208            args.propagation().clone(),
209            args.window_id,
210            args.device_id,
211            ClickArgsSource::Touch {
212                position: args.position,
213                hits: args.hits,
214                is_tap: false,
215            },
216            NonZeroU32::new(1).unwrap(),
217            false,
218            args.modifiers,
219            args.target,
220        )
221    }
222}
223impl ClickArgs {
224    /// Returns `true` if the widget is enabled in [`target`].
225    ///
226    /// [`target`]: Self::target
227    pub fn is_enabled(&self, widget_id: WidgetId) -> bool {
228        self.target.interactivity_of(widget_id).map(|i| i.is_enabled()).unwrap_or(false)
229    }
230
231    /// Returns `true` if the widget is disabled in [`target`].
232    ///
233    /// [`target`]: Self::target
234    pub fn is_disabled(&self, widget_id: WidgetId) -> bool {
235        self.target.interactivity_of(widget_id).map(|i| i.is_disabled()).unwrap_or(false)
236    }
237
238    /// If the event counts as *primary* click.
239    ///
240    /// A primary click causes the default widget function interaction.
241    ///
242    /// Returns `true` if the click source is a left mouse button click or a
243    /// [primary click shortcut](GESTURES::click_focused) or a touch tap.
244    pub fn is_primary(&self) -> bool {
245        match &self.source {
246            ClickArgsSource::Mouse { button, .. } => *button == MouseButton::Left,
247            ClickArgsSource::Touch { is_tap, .. } => *is_tap,
248            ClickArgsSource::Shortcut { kind, .. } => *kind == ShortcutClick::Primary,
249            ClickArgsSource::Access { is_primary } => *is_primary,
250        }
251    }
252
253    /// If the event counts as a *context menu* request.
254    ///
255    /// Returns `true` if the [`click_count`](Self::click_count) is `1` and the
256    /// click source is a right mouse button click or a [context click shortcut](GESTURES::context_click_focused)
257    /// or a touch long press.
258    pub fn is_context(&self) -> bool {
259        self.click_count.get() == 1
260            && match &self.source {
261                ClickArgsSource::Mouse { button, .. } => *button == MouseButton::Right,
262                ClickArgsSource::Touch { is_tap, .. } => !*is_tap,
263                ClickArgsSource::Shortcut { kind, .. } => *kind == ShortcutClick::Context,
264                ClickArgsSource::Access { is_primary } => !*is_primary,
265            }
266    }
267
268    /// If the event was caused by a press of `mouse_button`.
269    pub fn is_mouse_btn(&self, mouse_button: MouseButton) -> bool {
270        match &self.source {
271            ClickArgsSource::Mouse { button, .. } => *button == mouse_button,
272            _ => false,
273        }
274    }
275
276    /// The shortcut the generated this event.
277    pub fn shortcut(&self) -> Option<Shortcut> {
278        match &self.source {
279            ClickArgsSource::Shortcut { shortcut, .. } => Some(shortcut.clone()),
280            _ => None,
281        }
282    }
283
284    /// If the [`click_count`](Self::click_count) is `1`.
285    pub fn is_single(&self) -> bool {
286        self.click_count.get() == 1
287    }
288
289    /// If the [`click_count`](Self::click_count) is `2`.
290    pub fn is_double(&self) -> bool {
291        self.click_count.get() == 2
292    }
293
294    /// If the [`click_count`](Self::click_count) is `3`.
295    pub fn is_triple(&self) -> bool {
296        self.click_count.get() == 3
297    }
298
299    /// If this event was generated by a mouse device.
300    pub fn is_from_mouse(&self) -> bool {
301        matches!(&self.source, ClickArgsSource::Mouse { .. })
302    }
303
304    /// If this event was generated by a touch device.
305    pub fn is_from_touch(&self) -> bool {
306        matches!(&self.source, ClickArgsSource::Touch { .. })
307    }
308
309    /// If this event was generated by a keyboard device.
310    pub fn is_from_keyboard(&self) -> bool {
311        matches!(&self.source, ClickArgsSource::Shortcut { .. })
312    }
313
314    /// If this event was generated by accessibility automation event.
315    ///
316    /// Note that accessibility assistants can also simulate mouse click events, these events
317    /// are not classified as accessibility sourced.
318    pub fn is_from_access(&self) -> bool {
319        matches!(&self.source, ClickArgsSource::Access { .. })
320    }
321
322    /// Gets the click position, if the click was generated by a device with position.
323    ///
324    /// The position is in the coordinates of [`target`](ClickArgs::target).
325    pub fn position(&self) -> Option<DipPoint> {
326        match &self.source {
327            ClickArgsSource::Mouse { position, .. } => Some(*position),
328            ClickArgsSource::Touch { position, .. } => Some(*position),
329            ClickArgsSource::Shortcut { .. } | ClickArgsSource::Access { .. } => None,
330        }
331    }
332}
333
334event! {
335    /// Aggregate click event.
336    ///
337    /// Can be a mouse click, a shortcut press or a touch tap.
338    pub static CLICK_EVENT: ClickArgs;
339
340    /// Shortcut input event.
341    ///
342    /// Event happens every time a full [`Shortcut`] is completed by key press.
343    ///
344    /// This event is not send to any widget, use the [`GESTURES`] service to setup widget targets for shortcuts.
345    ///
346    /// [`Shortcut`]: zng_app::shortcut::Shortcut
347    pub static SHORTCUT_EVENT: ShortcutArgs;
348}
349
350/// Application extension that provides aggregate events.
351///
352/// Events this extension provides.
353///
354/// * [`CLICK_EVENT`]
355/// * [`SHORTCUT_EVENT`]
356///
357/// Services this extension provides.
358///
359/// * [`GESTURES`]
360#[derive(Default)]
361pub struct GestureManager {}
362impl AppExtension for GestureManager {
363    fn init(&mut self) {
364        // touch gesture event, only notifies if has hooks or subscribers.
365        TOUCH_TAP_EVENT.as_any().hook(|_| true).perm();
366        TOUCH_LONG_PRESS_EVENT.as_any().hook(|_| true).perm();
367    }
368
369    fn event(&mut self, update: &mut EventUpdate) {
370        if let Some(args) = MOUSE_CLICK_EVENT.on_unhandled(update) {
371            // Generate click events from mouse clicks.
372            CLICK_EVENT.notify(args.clone().into());
373        } else if let Some(args) = KEY_INPUT_EVENT.on_unhandled(update) {
374            // Generate shortcut events from keyboard input.
375            GESTURES_SV.write().on_key_input(args);
376        } else if let Some(args) = TOUCH_TAP_EVENT.on_unhandled(update) {
377            // Generate click events from taps.
378            CLICK_EVENT.notify(args.clone().into());
379        } else if let Some(args) = TOUCH_LONG_PRESS_EVENT.on_unhandled(update) {
380            // Generate click events from touch long press.
381            if !args.propagation().is_stopped() {
382                CLICK_EVENT.notify(args.clone().into());
383            }
384        } else if let Some(args) = SHORTCUT_EVENT.on_unhandled(update) {
385            // Run shortcut actions.
386            GESTURES_SV.write().on_shortcut(args);
387        } else if let Some(args) = ACCESS_CLICK_EVENT.on_unhandled(update) {
388            // Run access click.
389            GESTURES_SV.write().on_access(args);
390        }
391    }
392}
393
394app_local! {
395    static GESTURES_SV: GesturesService = GesturesService::new();
396}
397
398struct GesturesService {
399    click_focused: ArcVar<Shortcuts>,
400    context_click_focused: ArcVar<Shortcuts>,
401    shortcut_pressed_duration: ArcVar<Duration>,
402
403    pressed_modifier: Option<(WindowId, ModifierGesture)>,
404    primed_starter: Option<KeyGesture>,
405    chords: HashMap<KeyGesture, HashSet<KeyGesture>>,
406
407    primary_clicks: Vec<(Shortcut, Arc<ShortcutTarget>)>,
408    context_clicks: Vec<(Shortcut, Arc<ShortcutTarget>)>,
409    focus: Vec<(Shortcut, Arc<ShortcutTarget>)>,
410}
411impl GesturesService {
412    fn new() -> Self {
413        Self {
414            click_focused: var([shortcut!(Enter), shortcut!(Space)].into()),
415            context_click_focused: var([shortcut!(ContextMenu)].into()),
416            shortcut_pressed_duration: var(Duration::from_millis(50)),
417
418            pressed_modifier: None,
419            primed_starter: None,
420            chords: HashMap::default(),
421
422            primary_clicks: vec![],
423            context_clicks: vec![],
424            focus: vec![],
425        }
426    }
427
428    fn register_target(&mut self, shortcuts: Shortcuts, kind: Option<ShortcutClick>, target: WidgetId) -> ShortcutsHandle {
429        if shortcuts.is_empty() {
430            return ShortcutsHandle::dummy();
431        }
432
433        let (owner, handle) = ShortcutsHandle::new();
434        let target = Arc::new(ShortcutTarget {
435            widget_id: target,
436            last_found: Mutex::new(None),
437            handle: owner,
438        });
439
440        let collection = match kind {
441            Some(ShortcutClick::Primary) => &mut self.primary_clicks,
442            Some(ShortcutClick::Context) => &mut self.context_clicks,
443            None => &mut self.focus,
444        };
445
446        if collection.len() > 500 {
447            collection.retain(|(_, e)| !e.handle.is_dropped());
448        }
449
450        for s in shortcuts.0 {
451            if let Shortcut::Chord(c) = &s {
452                self.chords.entry(c.starter.clone()).or_default().insert(c.complement.clone());
453            }
454
455            collection.push((s, target.clone()));
456        }
457
458        handle
459    }
460
461    fn on_key_input(&mut self, args: &KeyInputArgs) {
462        let key = args.shortcut_key();
463        if !args.propagation().is_stopped() && !matches!(key, Key::Unidentified) {
464            match args.state {
465                KeyState::Pressed => {
466                    if let Ok(gesture_key) = GestureKey::try_from(key.clone()) {
467                        self.on_shortcut_pressed(Shortcut::Gesture(KeyGesture::new(args.modifiers, gesture_key)), args);
468                        self.pressed_modifier = None;
469                    } else if let Ok(mod_gesture) = ModifierGesture::try_from(key) {
470                        if args.repeat_count == 0 {
471                            self.pressed_modifier = Some((args.target.window_id(), mod_gesture));
472                        }
473                    } else {
474                        self.pressed_modifier = None;
475                        self.primed_starter = None;
476                    }
477                }
478                KeyState::Released => {
479                    if let Ok(mod_gesture) = ModifierGesture::try_from(key) {
480                        if let (Some((window_id, gesture)), true) = (self.pressed_modifier.take(), args.modifiers.is_empty()) {
481                            if window_id == args.target.window_id() && mod_gesture == gesture {
482                                self.on_shortcut_pressed(Shortcut::Modifier(mod_gesture), args);
483                            }
484                        }
485                    }
486                }
487            }
488        } else {
489            // Scancode only or already handled.
490            self.primed_starter = None;
491            self.pressed_modifier = None;
492        }
493    }
494    fn on_shortcut_pressed(&mut self, mut shortcut: Shortcut, key_args: &KeyInputArgs) {
495        if let Some(starter) = self.primed_starter.take() {
496            if let Shortcut::Gesture(g) = &shortcut {
497                if let Some(complements) = self.chords.get(&starter) {
498                    if complements.contains(g) {
499                        shortcut = Shortcut::Chord(KeyChord {
500                            starter,
501                            complement: g.clone(),
502                        });
503                    }
504                }
505            }
506        }
507
508        let actions = ShortcutActions::new(self, shortcut.clone());
509
510        SHORTCUT_EVENT.notify(ShortcutArgs::new(
511            key_args.timestamp,
512            key_args.propagation().clone(),
513            key_args.window_id,
514            key_args.device_id,
515            shortcut,
516            key_args.repeat_count,
517            actions,
518        ));
519    }
520
521    fn on_shortcut(&mut self, args: &ShortcutArgs) {
522        if args.actions.has_actions() {
523            args.actions
524                .run(args.timestamp, args.propagation(), args.device_id, args.repeat_count);
525        } else if let Shortcut::Gesture(k) = &args.shortcut {
526            if self.chords.contains_key(k) {
527                self.primed_starter = Some(k.clone());
528            }
529        }
530    }
531
532    fn on_access(&mut self, args: &AccessClickArgs) {
533        if let Ok(tree) = WINDOWS.widget_tree(args.window_id) {
534            if let Some(wgt) = tree.get(args.widget_id) {
535                let path = wgt.interaction_path();
536                if !path.interactivity().is_blocked() {
537                    let args = ClickArgs::now(
538                        args.window_id,
539                        None,
540                        ClickArgsSource::Access {
541                            is_primary: args.is_primary,
542                        },
543                        NonZeroU32::new(1).unwrap(),
544                        false,
545                        ModifiersState::empty(),
546                        path,
547                    );
548                    CLICK_EVENT.notify(args);
549                }
550            }
551        }
552    }
553
554    fn cleanup(&mut self) {
555        self.primary_clicks.retain(|(_, e)| !e.handle.is_dropped());
556        self.context_clicks.retain(|(_, e)| !e.handle.is_dropped());
557        self.focus.retain(|(_, e)| !e.handle.is_dropped());
558    }
559}
560
561/// Gesture events config service.
562///
563/// This service is provided by [`GestureManager`].
564///
565/// # Shortcuts
566///
567/// This service coordinates shortcut associations with widgets and commands. To define a command's shortcut use
568/// the [`CommandShortcutExt`] methods. To define the shortcut that *focus* and *clicks* a widget use [`click_shortcut`].
569/// To define the shortcut that only focuses on a widget use [`focus_shortcut`]. To define a custom handle for a shortcut
570/// use [`on_pre_event`] or [`on_event`] with the [`SHORTCUT_EVENT`].
571///
572/// ## Event Order
573///
574/// The same shortcut can end-up registered for multiple targets, activation of a shortcut causes these effects in this order:
575///
576/// 0. The gestures manager receives a [`KEY_INPUT_EVENT`] in the [`event`] track, if a shortcut is completed it gets combined with
577///    any primed chords and become the shortcut that will cause the following actions:
578///
579/// 1. The click, command and focus shortcuts are resolved in this order:
580///
581///    **First exclusively**:
582///    
583///    * Primary [`click_shortcut`] targeting a widget that is enabled and focused.
584///    * Command scoped in a widget that is enabled and focused.
585///    * Contextual [`click_shortcut`] targeting a widget that is enabled and focused.
586///    * Primary, command scoped or contextual targeting a widget that is enabled and closest to the focused widget,
587///      child first (of the focused).
588///    * Primary, command scoped or contextual targeting a widget that is enabled in the focused window.
589///    * Primary, command scoped or contextual targeting a widget that is enabled.
590///    * [`focus_shortcut`] targeting a widget that is in the focused window.
591///    * [`focus_shortcut`] targeting a widget that is enabled.
592///    * The [`click_focused`] and [`context_click_focused`].
593///    * *Same as the above, but for disabled widgets*
594///
595///     **And then:**
596///
597///    a. All enabled commands targeting the focused window.
598///
599///    b. All enabled commands targeting the app.
600///
601/// 2. The app level [`SHORTCUT_EVENT`] is notified, with the list of actions that will run, app extensions can handle it before [`event`]
602///    to stop the resolved actions.
603///
604/// 3. The gestures manager receives the shortcut in [`event`], if propagation is not stopped and it contains any actions they are run,
605///    the click and command events are linked by the same propagation. If the shortcut contains no action and it
606///
607/// 3. If the shortcut is a [`KeyChord::starter`] for one of the registered shortcuts, and was not claimed by
608///    any of the above, the chord starter is primed for the next shortcut press.
609///
610/// The event propagation flag of shortcut, click and command events are linked, so stopping [`propagation`] in one signal
611/// all others.
612///
613/// [`click_focused`]: Self::click_focused
614/// [`context_click_focused`]: Self::context_click_focused
615/// [`click_shortcut`]: Self::click_shortcut
616/// [`focus_shortcut`]: Self::focus_shortcut
617/// [`on_pre_event`]: zng_app::event::Event::on_pre_event
618/// [`on_event`]: zng_app::event::Event::on_event
619/// [`BLOCKED`]: zng_app::widget::info::Interactivity::BLOCKED
620/// [`propagation`]: AnyEventArgs::propagation
621/// [`event_preview`]: AppExtension::event_preview
622/// [`event_ui`]: AppExtension::event_ui
623/// [`event`]: AppExtension::event
624/// [`propagation`]: EventArgs::propagation
625/// [`KeyChord::starter`]: zng_app::shortcut::KeyChord::starter
626/// [`CommandShortcutExt`]: zng_app::shortcut::CommandShortcutExt
627pub struct GESTURES;
628struct ShortcutTarget {
629    widget_id: WidgetId,
630    last_found: Mutex<Option<WidgetPath>>,
631    handle: HandleOwner<()>,
632}
633impl ShortcutTarget {
634    fn resolve_path(&self) -> Option<InteractionPath> {
635        let mut found = self.last_found.lock();
636        if let Some(found) = &mut *found {
637            if let Ok(tree) = WINDOWS.widget_tree(found.window_id()) {
638                if let Some(w) = tree.get(found.widget_id()) {
639                    let path = w.interaction_path();
640                    *found = path.as_path().clone();
641
642                    return path.unblocked();
643                }
644            }
645        }
646
647        if let Some(w) = WINDOWS.widget_info(self.widget_id) {
648            let path = w.interaction_path();
649            *found = Some(path.as_path().clone());
650
651            return path.unblocked();
652        }
653
654        None
655    }
656}
657impl GESTURES {
658    /// Shortcuts that generate a primary [`CLICK_EVENT`] for the focused widget.
659    /// The shortcut only works if no widget or command claims it.
660    ///
661    /// Clicks generated by this shortcut count as [primary](ClickArgs::is_primary).
662    ///
663    /// Initial shortcuts are [`Enter`](Key::Enter) and [`Space`](Key::Space).
664    pub fn click_focused(&self) -> ArcVar<Shortcuts> {
665        GESTURES_SV.read().click_focused.clone()
666    }
667
668    /// Shortcuts that generate a context [`CLICK_EVENT`] for the focused widget.
669    /// The shortcut only works if no widget or command claims it.
670    ///
671    /// Clicks generated by this shortcut count as [context](ClickArgs::is_context).
672    ///
673    /// Initial shortcut is [`ContextMenu`](Key::ContextMenu).
674    pub fn context_click_focused(&self) -> ArcVar<Shortcuts> {
675        GESTURES_SV.read().context_click_focused.clone()
676    }
677
678    /// When a shortcut or access primary click happens, targeted widgets can indicate that
679    /// they are pressed for this duration.
680    ///
681    /// Initial value is `50ms`, set to `0` to deactivate this type of indication.
682    pub fn shortcut_pressed_duration(&self) -> ArcVar<Duration> {
683        GESTURES_SV.read().shortcut_pressed_duration.clone()
684    }
685
686    /// Register a widget to receive shortcut clicks when any of the `shortcuts` are pressed.
687    pub fn click_shortcut(&self, shortcuts: impl Into<Shortcuts>, kind: ShortcutClick, target: WidgetId) -> ShortcutsHandle {
688        GESTURES_SV.write().register_target(shortcuts.into(), Some(kind), target)
689    }
690
691    /// Register a widget to receive keyboard focus when any of the `shortcuts` are pressed.
692    ///
693    /// If the widget is not focusable the focus moves to the first focusable descendant or the first focusable ancestor.
694    pub fn focus_shortcut(&self, shortcuts: impl Into<Shortcuts>, target: WidgetId) -> ShortcutsHandle {
695        GESTURES_SV.write().register_target(shortcuts.into(), None, target)
696    }
697
698    /// Gets all the event notifications that are send if the `shortcut` was pressed at this moment.
699    ///
700    /// See the [struct] level docs for details of how shortcut targets are resolved.
701    ///
702    /// [struct]: Self
703    pub fn shortcut_actions(&self, shortcut: Shortcut) -> ShortcutActions {
704        ShortcutActions::new(&mut GESTURES_SV.write(), shortcut)
705    }
706}
707
708/// Represents the resolved targets for a shortcut at a time.
709///
710/// You can use the [`GESTURES.shortcut_actions`] method to get a value of this.
711///
712/// [`GESTURES.shortcut_actions`]: GESTURES::shortcut_actions
713#[derive(Debug, Clone)]
714pub struct ShortcutActions {
715    shortcut: Shortcut,
716
717    focus: Option<WidgetId>,
718    click: Option<(InteractionPath, ShortcutClick)>,
719    commands: Vec<Command>,
720}
721impl ShortcutActions {
722    fn new(gestures: &mut GesturesService, shortcut: Shortcut) -> ShortcutActions {
723        //    **First exclusively**:
724        //
725        //    * Primary [`click_shortcut`] targeting a widget that is enabled and focused.
726        //    * Command scoped in a widget that is enabled and focused.
727        //    * Contextual [`click_shortcut`] targeting a widget that is enabled and focused.
728        //    * Primary, command scoped or contextual targeting a widget that is enabled and closest to the focused widget,
729        //      child first (of the focused).
730        //    * Primary, command scoped or contextual targeting a widget that is enabled in the focused window.
731        //    * Primary, command scoped or contextual targeting a widget that is enabled.
732        //    * [`focus_shortcut`] targeting a widget that is in the focused window.
733        //    * [`focus_shortcut`] targeting a widget that is enabled.
734        //    * The [`click_focused`] and [`context_click_focused`].
735        //    * *Same as the above, but for disabled widgets*
736        //
737        //     **And then:**
738        //
739        //    a. All enabled commands targeting the focused window.
740        //
741        //    b. All enabled commands targeting the app.
742
743        let focused = FOCUS.focused().get();
744
745        enum Kind {
746            Click(InteractionPath, ShortcutClick),
747            Command(InteractionPath, Command),
748            Focus(InteractionPath),
749        }
750        impl Kind {
751            fn kind_key(&self) -> u8 {
752                match self {
753                    Kind::Click(p, s) => {
754                        if p.interactivity().is_enabled() {
755                            match s {
756                                ShortcutClick::Primary => 0,
757                                ShortcutClick::Context => 2,
758                            }
759                        } else {
760                            match s {
761                                ShortcutClick::Primary => 10,
762                                ShortcutClick::Context => 12,
763                            }
764                        }
765                    }
766                    Kind::Command(p, _) => {
767                        if p.interactivity().is_enabled() {
768                            1
769                        } else {
770                            11
771                        }
772                    }
773                    Kind::Focus(p) => {
774                        if p.interactivity().is_enabled() {
775                            4
776                        } else {
777                            14
778                        }
779                    }
780                }
781            }
782        }
783
784        fn distance_key(focused: &Option<InteractionPath>, p: &InteractionPath) -> u32 {
785            let mut key = u32::MAX - 1;
786            if let Some(focused) = focused {
787                if p.window_id() == focused.window_id() {
788                    key -= 1;
789                    if let Some(i) = p.widgets_path().iter().position(|&id| id == focused.widget_id()) {
790                        // is descendant of focused (or it)
791                        key = (p.widgets_path().len() - i) as u32;
792                    } else if let Some(i) = focused.widgets_path().iter().position(|&id| id == p.widget_id()) {
793                        // is ancestor of focused
794                        key = key / 2 + (focused.widgets_path().len() - i) as u32;
795                    }
796                }
797            }
798            key
799        }
800
801        let mut some_primary_dropped = false;
802        let primary_click_matches = gestures.primary_clicks.iter().filter_map(|(s, entry)| {
803            if entry.handle.is_dropped() {
804                some_primary_dropped = true;
805                return None;
806            }
807            if *s != shortcut {
808                return None;
809            }
810
811            let p = entry.resolve_path()?;
812            Some((distance_key(&focused, &p), Kind::Click(p, ShortcutClick::Primary)))
813        });
814
815        let mut some_ctx_dropped = false;
816        let context_click_matches = gestures.context_clicks.iter().filter_map(|(s, entry)| {
817            if entry.handle.is_dropped() {
818                some_ctx_dropped = true;
819                return None;
820            }
821            if *s != shortcut {
822                return None;
823            }
824
825            let p = entry.resolve_path()?;
826            Some((distance_key(&focused, &p), Kind::Click(p, ShortcutClick::Context)))
827        });
828
829        let mut some_focus_dropped = false;
830        let focus_matches = gestures.focus.iter().filter_map(|(s, entry)| {
831            if entry.handle.is_dropped() {
832                some_focus_dropped = true;
833                return None;
834            }
835            if *s != shortcut {
836                return None;
837            }
838
839            let p = entry.resolve_path()?;
840            Some((distance_key(&focused, &p), Kind::Focus(p)))
841        });
842
843        let mut cmd_window = vec![];
844        let mut cmd_app = vec![];
845        let cmd_matches = EVENTS.commands().into_iter().filter_map(|cmd| {
846            if !cmd.shortcut_matches(&shortcut) {
847                return None;
848            }
849
850            match cmd.scope() {
851                CommandScope::Window(w) => {
852                    if let Some(f) = &focused {
853                        if f.window_id() == w {
854                            cmd_window.push(cmd);
855                        }
856                    }
857                }
858                CommandScope::Widget(id) => {
859                    if let Some(info) = WINDOWS.widget_info(id) {
860                        let p = info.interaction_path();
861                        return Some((distance_key(&focused, &p), Kind::Command(p, cmd)));
862                    }
863                }
864                CommandScope::App => cmd_app.push(cmd),
865            }
866
867            None
868        });
869
870        let mut best_kind = u8::MAX;
871        let mut best_distance = u32::MAX;
872        let mut best = None;
873
874        for (distance_key, choice) in primary_click_matches
875            .chain(cmd_matches)
876            .chain(context_click_matches)
877            .chain(focus_matches)
878        {
879            let kind_key = choice.kind_key();
880            match kind_key.cmp(&best_kind) {
881                std::cmp::Ordering::Less => {
882                    best_kind = kind_key;
883                    best_distance = distance_key;
884                    best = Some(choice);
885                }
886                std::cmp::Ordering::Equal => {
887                    if distance_key < best_distance {
888                        best_distance = distance_key;
889                        best = Some(choice);
890                    }
891                }
892                std::cmp::Ordering::Greater => {}
893            }
894        }
895
896        let mut click = None;
897        let mut focus = None;
898        let mut commands = vec![];
899
900        match best {
901            Some(k) => match k {
902                Kind::Click(p, s) => click = Some((p, s)),
903                Kind::Command(_, cmd) => commands.push(cmd),
904                Kind::Focus(p) => focus = Some(p.widget_id()),
905            },
906            None => {
907                if let Some(p) = focused {
908                    click = if gestures.click_focused.with(|c| c.contains(&shortcut)) {
909                        Some((p, ShortcutClick::Primary))
910                    } else if gestures.context_click_focused.with(|c| c.contains(&shortcut)) {
911                        Some((p, ShortcutClick::Context))
912                    } else {
913                        None
914                    };
915                }
916            }
917        }
918
919        commands.append(&mut cmd_window);
920        commands.append(&mut cmd_app);
921
922        if some_primary_dropped || some_ctx_dropped || some_focus_dropped {
923            gestures.cleanup();
924        }
925
926        Self {
927            shortcut,
928            focus,
929            click,
930            commands,
931        }
932    }
933
934    /// The shortcut.
935    pub fn shortcut(&self) -> &Shortcut {
936        &self.shortcut
937    }
938
939    /// Focus target.
940    ///
941    /// If `click` is some, this is a direct focus request to it, or if the first command is scoped on a widget this
942    /// is a direct focus request on the scope, or if this is some it is a direct or related request to the focus shortcut target.
943    pub fn focus(&self) -> Option<FocusTarget> {
944        if let Some((p, _)) = &self.click {
945            return Some(FocusTarget::Direct { target: p.widget_id() });
946        } else if let Some(c) = self.commands.first() {
947            if let CommandScope::Widget(w) = c.scope() {
948                if FOCUS.focused().with(|f| f.as_ref().map(|p| !p.contains(w)).unwrap_or(true)) {
949                    return Some(FocusTarget::Direct { target: w });
950                }
951            }
952        }
953        self.focus.map(|target| FocusTarget::DirectOrRelated {
954            target,
955            navigation_origin: true,
956        })
957    }
958
959    /// Click target and kind.
960    pub fn click(&self) -> Option<(&InteractionPath, ShortcutClick)> {
961        self.click.as_ref().map(|(p, k)| (p, *k))
962    }
963
964    /// Commands.
965    ///
966    /// Only the first command may be scoped in a widget, others are scoped on the focused window or app.
967    pub fn commands(&self) -> &[Command] {
968        &self.commands
969    }
970
971    /// If any action was found for the shortcut.
972    pub fn has_actions(&self) -> bool {
973        self.click.is_some() || self.focus.is_some() || !self.commands.is_empty()
974    }
975
976    /// Send all events and focus request.
977    fn run(&self, timestamp: DInstant, propagation: &EventPropagationHandle, device_id: Option<DeviceId>, repeat_count: u32) {
978        if let Some(target) = self.focus() {
979            FOCUS.focus(FocusRequest::new(target, true));
980        }
981
982        if let Some((target, kind)) = &self.click {
983            let args = ClickArgs::new(
984                timestamp,
985                propagation.clone(),
986                target.window_id(),
987                device_id,
988                ClickArgsSource::Shortcut {
989                    shortcut: self.shortcut.clone(),
990                    kind: *kind,
991                },
992                NonZeroU32::new(repeat_count.saturating_add(1)).unwrap(),
993                repeat_count > 0,
994                self.shortcut.modifiers_state(),
995                target.clone(),
996            );
997            CLICK_EVENT.notify(args);
998        }
999        for command in &self.commands {
1000            command.notify_linked(propagation.clone(), None);
1001        }
1002    }
1003}
1004
1005/// Represents shortcuts claim in [`click_shortcut`] or [`focus_shortcut`].
1006///
1007/// [`click_shortcut`]: GESTURES::click_shortcut
1008/// [`focus_shortcut`]: GESTURES::focus_shortcut
1009#[derive(Clone, PartialEq, Eq, Hash, Debug)]
1010#[repr(transparent)]
1011#[must_use = "the shortcuts claim is removed if the handle is dropped"]
1012pub struct ShortcutsHandle(Handle<()>);
1013impl ShortcutsHandle {
1014    pub(super) fn new() -> (HandleOwner<()>, Self) {
1015        let (owner, handle) = Handle::new(());
1016        (owner, ShortcutsHandle(handle))
1017    }
1018
1019    /// Create dummy handle that is always in the *released* state.
1020    ///
1021    /// Note that `Option<ShortcutsHandle>` takes up the same space as `ShortcutsHandle` and avoids an allocation.
1022    pub fn dummy() -> Self {
1023        ShortcutsHandle(Handle::dummy(()))
1024    }
1025
1026    /// Drops the handle but does **not** release.
1027    ///
1028    /// The claim stays registered for the duration of the app or until another handle calls [`release`](Self::release).
1029    /// Note that shortcut claims only work if the target widget is found and is not [`BLOCKED`].
1030    ///
1031    /// [`BLOCKED`]: zng_app::widget::info::Interactivity::BLOCKED
1032    pub fn perm(self) {
1033        self.0.perm();
1034    }
1035
1036    /// If another handle has called [`perm`](Self::perm).
1037    ///
1038    /// If `true` the claim will stay active until the app exits, unless [`release`](Self::release) is called.
1039    pub fn is_permanent(&self) -> bool {
1040        self.0.is_permanent()
1041    }
1042
1043    /// Drops the handle and releases the claim
1044    pub fn release(self) {
1045        self.0.force_drop();
1046    }
1047
1048    /// If another handle has called [`release`](Self::release).
1049    ///
1050    /// The claim is already dropped or will be dropped in the next app update, this is irreversible.
1051    pub fn is_released(&self) -> bool {
1052        self.0.is_dropped()
1053    }
1054
1055    /// Create a weak handle.
1056    pub fn downgrade(&self) -> WeakShortcutsHandle {
1057        WeakShortcutsHandle(self.0.downgrade())
1058    }
1059}
1060
1061/// Weak [`ShortcutsHandle`].
1062#[derive(Clone, PartialEq, Eq, Hash, Default, Debug)]
1063pub struct WeakShortcutsHandle(pub(super) WeakHandle<()>);
1064impl WeakShortcutsHandle {
1065    /// New weak handle that does not upgrade.
1066    pub fn new() -> Self {
1067        Self(WeakHandle::new())
1068    }
1069
1070    /// Get the shortcuts handle if it is still installed.
1071    pub fn upgrade(&self) -> Option<ShortcutsHandle> {
1072        self.0.upgrade().map(ShortcutsHandle)
1073    }
1074}
1075
1076/// Extension trait that adds gesture simulation methods to [`HeadlessApp`].
1077///
1078/// [`HeadlessApp`]: zng_app::HeadlessApp
1079pub trait HeadlessAppGestureExt {
1080    /// Generates key press events to mimic the shortcut and updates.
1081    fn press_shortcut(&mut self, window_id: WindowId, shortcut: impl Into<Shortcut>);
1082}
1083impl HeadlessAppGestureExt for HeadlessApp {
1084    fn press_shortcut(&mut self, window_id: WindowId, shortcut: impl Into<Shortcut>) {
1085        let shortcut = shortcut.into();
1086        match shortcut {
1087            Shortcut::Modifier(m) => {
1088                let (code, key) = m.left_key();
1089                self.press_key(window_id, code, KeyLocation::Standard, key);
1090            }
1091            Shortcut::Gesture(g) => match g.key {
1092                GestureKey::Key(k) => self.press_modified_key(
1093                    window_id,
1094                    g.modifiers,
1095                    KeyCode::Unidentified(NativeKeyCode::Unidentified),
1096                    KeyLocation::Standard,
1097                    k,
1098                ),
1099                GestureKey::Code(c) => self.press_modified_key(window_id, g.modifiers, c, KeyLocation::Standard, Key::Unidentified),
1100            },
1101            Shortcut::Chord(c) => {
1102                self.press_shortcut(window_id, c.starter);
1103                self.press_shortcut(window_id, c.complement);
1104            }
1105        }
1106    }
1107}
1108
1109/// Adds the `shortcut_matches` method to commands.
1110pub trait CommandShortcutMatchesExt: CommandShortcutExt {
1111    /// Returns `true` if the command has handlers, enabled or disabled, and the shortcut if one of the command shortcuts.
1112    fn shortcut_matches(self, shortcut: &Shortcut) -> bool;
1113}
1114impl CommandShortcutMatchesExt for Command {
1115    fn shortcut_matches(self, shortcut: &Shortcut) -> bool {
1116        if !self.has_handlers().get() {
1117            return false;
1118        }
1119
1120        let s = self.shortcut();
1121        if s.with(|s| !s.contains(shortcut)) {
1122            return false;
1123        }
1124
1125        let filter = self.shortcut_filter().get();
1126        if filter.is_empty() {
1127            return true;
1128        }
1129        if filter.contains(ShortcutFilter::CMD_ENABLED) && !self.is_enabled_value() {
1130            return false;
1131        }
1132
1133        match self.scope() {
1134            CommandScope::App => filter == ShortcutFilter::CMD_ENABLED,
1135            CommandScope::Window(id) => {
1136                if filter.contains(ShortcutFilter::FOCUSED) {
1137                    FOCUS.focused().with(|p| {
1138                        let p = match p {
1139                            Some(p) => p,
1140                            None => return false,
1141                        };
1142                        if p.window_id() != id {
1143                            return false;
1144                        }
1145                        !filter.contains(ShortcutFilter::ENABLED) || p.interaction_path().next().map(|i| i.is_enabled()).unwrap_or(false)
1146                    })
1147                } else if filter.contains(ShortcutFilter::ENABLED) {
1148                    let tree = match WINDOWS.widget_tree(id) {
1149                        Ok(t) => t,
1150                        Err(_) => return false,
1151                    };
1152
1153                    tree.root().interactivity().is_enabled()
1154                } else {
1155                    true
1156                }
1157            }
1158            CommandScope::Widget(id) => {
1159                if filter.contains(ShortcutFilter::FOCUSED) {
1160                    FOCUS.focused().with(|p| {
1161                        let p = match p {
1162                            Some(p) => p,
1163                            None => return false,
1164                        };
1165                        if !p.contains(id) {
1166                            return false;
1167                        }
1168                        !filter.contains(ShortcutFilter::ENABLED) || p.interactivity_of(id).map(|i| i.is_enabled()).unwrap_or(false)
1169                    })
1170                } else if filter.contains(ShortcutFilter::ENABLED) {
1171                    if let Some(w) = WINDOWS.widget_info(id) {
1172                        return w.interactivity().is_enabled();
1173                    }
1174
1175                    false
1176                } else {
1177                    true
1178                }
1179            }
1180        }
1181    }
1182}