zng_ext_input/
gesture.rs

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