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