Skip to main content

zng_wgt_input/
gesture.rs

1//! Gesture events and control, [`on_click`](fn@on_click), [`click_shortcut`](fn@click_shortcut) and more.
2//!
3//! These events aggregate multiple lower-level events to represent a user interaction.
4//! Prefer using these events over the events directly tied to an input device.
5
6use std::{
7    collections::{HashMap, hash_map},
8    mem,
9};
10
11use zng_app::{
12    shortcut::{GestureKey, Shortcuts},
13    widget::info::{TreeFilter, iter::TreeIterator},
14};
15use zng_ext_input::{
16    focus::{FOCUS, FOCUS_CHANGED_EVENT},
17    gesture::{CLICK_EVENT, GESTURES, ShortcutClick},
18};
19use zng_var::AnyVar;
20use zng_view_api::{access::AccessCmdName, keyboard::Key};
21use zng_wgt::{node::bind_state_info, prelude::*};
22
23use zng_ext_input::focus::WidgetInfoFocusExt as _;
24pub use zng_ext_input::gesture::ClickArgs;
25
26event_property! {
27    /// On widget click from any source and of any click count and the widget is enabled.
28    ///
29    /// This is the most general click handler, it raises for all possible sources of the [`CLICK_EVENT`] and any number
30    /// of consecutive clicks. Use [`on_click`](fn@on_click) to handle only primary button clicks or [`on_any_single_click`](fn@on_any_single_click)
31    /// to not include double/triple clicks.
32    ///
33    /// [`CLICK_EVENT`]: zng_ext_input::gesture::CLICK_EVENT
34    #[property(EVENT)]
35    pub fn on_any_click<on_pre_any_click>(child: impl IntoUiNode, handler: Handler<ClickArgs>) -> UiNode {
36        const PRE: bool;
37        let child = EventNodeBuilder::new(CLICK_EVENT)
38            .filter(|| {
39                let id = WIDGET.id();
40                move |args| args.target.contains_enabled(id)
41            })
42            .build::<PRE>(child, handler);
43        access_click(child)
44    }
45
46    /// On widget click from any source and of any click count and the widget is disabled.
47    #[property(EVENT)]
48    pub fn on_disabled_click<on_pre_disabled_click>(child: impl IntoUiNode, handler: Handler<ClickArgs>) -> UiNode {
49        const PRE: bool;
50        let child = EventNodeBuilder::new(CLICK_EVENT)
51            .filter(|| {
52                let id = WIDGET.id();
53                move |args| args.target.contains_disabled(id)
54            })
55            .build::<PRE>(child, handler);
56        access_click(child)
57    }
58
59    /// On widget click from any source but excluding double/triple clicks and the widget is enabled.
60    ///
61    /// This raises for all possible sources of [`CLICK_EVENT`], but only when the click count is one. Use
62    /// [`on_single_click`](fn@on_single_click) to handle only primary button clicks.
63    ///
64    /// [`CLICK_EVENT`]: zng_ext_input::gesture::CLICK_EVENT
65    #[property(EVENT)]
66    pub fn on_any_single_click<on_pre_any_single_click>(child: impl IntoUiNode, handler: Handler<ClickArgs>) -> UiNode {
67        const PRE: bool;
68        let child = EventNodeBuilder::new(CLICK_EVENT)
69            .filter(|| {
70                let id = WIDGET.id();
71                move |args| args.is_single() && args.target.contains_enabled(id)
72            })
73            .build::<PRE>(child, handler);
74        access_click(child)
75    }
76
77    /// On widget double click from any source and the widget is enabled.
78    ///
79    /// This raises for all possible sources of [`CLICK_EVENT`], but only when the click count is two. Use
80    /// [`on_double_click`](fn@on_double_click) to handle only primary button clicks.
81    ///
82    /// [`CLICK_EVENT`]: zng_ext_input::gesture::CLICK_EVENT
83    #[property(EVENT)]
84    pub fn on_any_double_click<on_pre_any_double_click>(child: impl IntoUiNode, handler: Handler<ClickArgs>) -> UiNode {
85        const PRE: bool;
86        EventNodeBuilder::new(CLICK_EVENT)
87            .filter(|| {
88                let id = WIDGET.id();
89                move |args| args.is_double() && args.target.contains_enabled(id)
90            })
91            .build::<PRE>(child, handler)
92    }
93
94    /// On widget triple click from any source and the widget is enabled.
95    ///
96    /// This raises for all possible sources of [`CLICK_EVENT`], but only when the click count is three. Use
97    /// [`on_triple_click`](fn@on_triple_click) to handle only primary button clicks.
98    ///
99    /// [`CLICK_EVENT`]: zng_ext_input::gesture::CLICK_EVENT
100    #[property(EVENT)]
101    pub fn on_any_triple_click<on_pre_any_triple_click>(child: impl IntoUiNode, handler: Handler<ClickArgs>) -> UiNode {
102        const PRE: bool;
103        EventNodeBuilder::new(CLICK_EVENT)
104            .filter(|| {
105                let id = WIDGET.id();
106                move |args| args.is_triple() && args.target.contains_enabled(id)
107            })
108            .build::<PRE>(child, handler)
109    }
110
111    /// On widget click with the primary button and any click count and the widget is enabled.
112    ///
113    /// This raises only if the click [is primary](ClickArgs::is_primary), but raises for any click count (double/triple clicks).
114    /// Use [`on_any_click`](fn@on_any_click) to handle clicks from any button or [`on_single_click`](fn@on_single_click) to not include
115    /// double/triple clicks.
116    #[property(EVENT)]
117    pub fn on_click<on_pre_click>(child: impl IntoUiNode, handler: Handler<ClickArgs>) -> UiNode {
118        const PRE: bool;
119        let child = EventNodeBuilder::new(CLICK_EVENT)
120            .filter(|| {
121                let id = WIDGET.id();
122                move |args| args.is_primary() && args.target.contains_enabled(id)
123            })
124            .build::<PRE>(child, handler);
125        access_click(child)
126    }
127
128    /// On widget click with the primary button, excluding double/triple clicks and the widget is enabled.
129    ///
130    /// This raises only if the click [is primary](ClickArgs::is_primary) and the click count is one. Use
131    /// [`on_any_single_click`](fn@on_any_single_click) to handle single clicks from any button.
132    #[property(EVENT)]
133    pub fn on_single_click<on_pre_single_click>(child: impl IntoUiNode, handler: Handler<ClickArgs>) -> UiNode {
134        const PRE: bool;
135        let child = EventNodeBuilder::new(CLICK_EVENT)
136            .filter(|| {
137                let id = WIDGET.id();
138                move |args| args.is_primary() && args.is_single() && args.target.contains_enabled(id)
139            })
140            .build::<PRE>(child, handler);
141        access_click(child)
142    }
143
144    /// On widget double click with the primary button and the widget is enabled.
145    ///
146    /// This raises only if the click [is primary](ClickArgs::is_primary) and the click count is two. Use
147    /// [`on_any_double_click`](fn@on_any_double_click) to handle double clicks from any button.
148    #[property(EVENT)]
149    pub fn on_double_click<on_pre_double_click>(child: impl IntoUiNode, handler: Handler<ClickArgs>) -> UiNode {
150        const PRE: bool;
151        EventNodeBuilder::new(CLICK_EVENT)
152            .filter(|| {
153                let id = WIDGET.id();
154                move |args| args.is_primary() && args.is_double() && args.target.contains_enabled(id)
155            })
156            .build::<PRE>(child, handler)
157    }
158
159    /// On widget triple click with the primary button and the widget is enabled.
160    ///
161    /// This raises only if the click [is primary](ClickArgs::is_primary) and the click count is three. Use
162    /// [`on_any_double_click`](fn@on_any_double_click) to handle double clicks from any button.
163    #[property(EVENT)]
164    pub fn on_triple_click<on_pre_triple_click>(child: impl IntoUiNode, handler: Handler<ClickArgs>) -> UiNode {
165        const PRE: bool;
166        EventNodeBuilder::new(CLICK_EVENT)
167            .filter(|| {
168                let id = WIDGET.id();
169                move |args| args.is_primary() && args.is_triple() && args.target.contains_enabled(id)
170            })
171            .build::<PRE>(child, handler)
172    }
173
174    /// On widget click with the secondary/context button and the widget is enabled.
175    ///
176    /// This raises only if the click [is context](ClickArgs::is_context).
177    #[property(EVENT)]
178    pub fn on_context_click<on_pre_context_click>(child: impl IntoUiNode, handler: Handler<ClickArgs>) -> UiNode {
179        const PRE: bool;
180        let child = EventNodeBuilder::new(CLICK_EVENT)
181            .filter(|| {
182                let id = WIDGET.id();
183                move |args| args.is_context() && args.target.contains_enabled(id)
184            })
185            .build::<PRE>(child, handler);
186        access_click(child)
187    }
188}
189
190/// Keyboard shortcuts that focus and clicks this widget.
191///
192/// When any of the `shortcuts` is pressed, focus and click this widget.
193#[property(CONTEXT)]
194pub fn click_shortcut(child: impl IntoUiNode, shortcuts: impl IntoVar<Shortcuts>) -> UiNode {
195    click_shortcut_node(child, shortcuts, ShortcutClick::Primary)
196}
197/// Keyboard shortcuts that focus and [context clicks](fn@on_context_click) this widget.
198///
199/// When any of the `shortcuts` is pressed, focus and context clicks this widget.
200#[property(CONTEXT)]
201pub fn context_click_shortcut(child: impl IntoUiNode, shortcuts: impl IntoVar<Shortcuts>) -> UiNode {
202    click_shortcut_node(child, shortcuts, ShortcutClick::Context)
203}
204
205fn click_shortcut_node(child: impl IntoUiNode, shortcuts: impl IntoVar<Shortcuts>, kind: ShortcutClick) -> UiNode {
206    let shortcuts = shortcuts.into_var();
207    let mut _handle = None;
208
209    match_node(child, move |_, op| {
210        let new = match op {
211            UiNodeOp::Init => {
212                WIDGET.sub_var(&shortcuts);
213                Some(shortcuts.get())
214            }
215            UiNodeOp::Deinit => {
216                _handle = None;
217                None
218            }
219            UiNodeOp::Update { .. } => shortcuts.get_new(),
220            _ => None,
221        };
222        if let Some(s) = new {
223            _handle = Some(GESTURES.click_shortcut(s, kind, WIDGET.id()));
224        }
225    })
226}
227
228pub(crate) fn access_click(child: impl IntoUiNode) -> UiNode {
229    access_capable(child, AccessCmdName::Click)
230}
231fn access_capable(child: impl IntoUiNode, cmd: AccessCmdName) -> UiNode {
232    match_node(child, move |_, op| {
233        if let UiNodeOp::Info { info } = op
234            && let Some(mut access) = info.access()
235        {
236            access.push_command(cmd)
237        }
238    })
239}
240
241/// Defines the mnemonic char key that clicks the widget when pressed and focus is within the parent mnemonic scope.
242#[derive(Debug, PartialEq, Hash, Clone)]
243pub enum Mnemonic {
244    /// Scope selects a char using the inner [`mnemonic_txt`] of the widget or descendants.
245    ///
246    /// [`mnemonic_txt`]: fn@mnemonic_txt
247    Auto,
248    /// Explicit alphanumeric char.
249    ///
250    /// The associated char must be a value that can appear in [`Key::Char`] (case indifferent), otherwise it will never match.
251    ///
252    /// In case the same key is set for multiple widgets in a scope the first widget (in tab order) takes it, the others
253    /// do not enable mnemonic shortcut.
254    ///
255    /// [`Key::Char`]: zng_ext_input::keyboard::Key
256    Char(char),
257    /// Explicit alphanumeric key defined in the widget inner text, identified by a `marker` prefix.
258    ///
259    /// After the char is extracted this behaves like `Char`. If the `marker` is not found also fallback to `Auto`.
260    ///
261    /// The `Label!` widget automatically hides the marker (first occurrence before an alphanumeric char).
262    FromTxt {
263        /// Char that is before the key char.
264        ///
265        /// If marker is `'_'` and the text is `"_Cut"` the mnemonic is `'c'`.
266        marker: char,
267        /// If should `Auto` select a char if the marked char is not found or cannot be used.
268        fallback_auto: bool,
269    },
270    /// No mnemonic behavior, disabled.
271    None,
272}
273impl_from_and_into_var! {
274    /// Converts to `Char`
275    fn from(c: char) -> Mnemonic {
276        Mnemonic::Char(c)
277    }
278    /// Converts `true` to `from_txt('_', true)` and `false` to `None`.
279    fn from(from_txt: bool) -> Mnemonic {
280        if from_txt { Mnemonic::from_txt('_', true) } else { Mnemonic::None }
281    }
282}
283impl Mnemonic {
284    /// `FromTxt` with default marker `'_'`.
285    pub fn from_txt(marker: char, fallback_auto: bool) -> Self {
286        Self::FromTxt { marker, fallback_auto }
287    }
288}
289
290/// Defines the mnemonic char key that clicks the widget when pressed and focus is within the parent mnemonic scope.
291///
292/// ```
293/// # macro_rules! example { () => {
294/// Stack! {
295///     mnemonic_scope = true;
296///     alt_focus_scope = true;
297///     children = ui_vec![Button! {
298///         mnemonic = true;
299///         child = Label!("_Open File");
300///     },];
301/// }
302/// # }}
303/// ```
304///
305/// In the example above the `Button!` will be clicked when focus is within the parent `Stack!` and the `O` key is pressed.
306///
307/// Note that `true` converts into [`Mnemonic::FromTxt`] with `_` marker, and if no valid char is defined in the inner [`mnemonic_txt`]
308/// text the behavior falls back to [`Mnemonic::Auto`], so a simple `mnemonic = true` enables the most common use case for this feature.
309///
310/// Note the use of `Label!` instead of `Text!`, the `Label!` widget automatically sets [`mnemonic_txt`], removes the markers
311/// from the rendered text and marks the mnemonic char with an underline.
312///
313/// The `Menu!` and related widgets automatically enables mnemonic for inner buttons, but you still must use `Label!` instead of `Text!`.
314///
315/// Note that the focus event inside the parent [`mnemonic_scope`] must be keyboard [`highlight`], that is, for a `Menu!`, the mnemonics
316/// are only active when when focus enters by pressing `Alt`.
317///
318/// [`mnemonic_scope`]: fn@mnemonic_scope
319/// [`mnemonic_txt`]: fn@mnemonic_txt
320/// [`highlight`]: zng_ext_input::focus::FocusChangedArgs::highlight
321#[property(CONTEXT, default(Mnemonic::None))]
322pub fn mnemonic(child: impl IntoUiNode, mnemonic: impl IntoVar<Mnemonic>) -> UiNode {
323    let mnemonic = mnemonic.into_var();
324    match_node(child, move |_, op| {
325        if let UiNodeOp::Info { info } = op {
326            info.set_meta(*MNEMONIC_ID, mnemonic.clone());
327        }
328    })
329}
330
331/// Defines the inner text of a [`mnemonic`] parent widget.
332///
333/// Note that the `Label!` widget automatically sets this to its own `txt`, this property can override the
334/// inner text. The text is used when the widget or parent mnemonic is [`FromTxt`] or [`Auto`].
335///
336/// [`mnemonic`]: fn@mnemonic
337/// [`FromTxt`]: Mnemonic::FromTxt
338/// [`Auto`]: Mnemonic::Auto
339#[property(CHILD, default(Txt::default()))]
340pub fn mnemonic_txt(child: impl IntoUiNode, txt: impl IntoVar<Txt>) -> UiNode {
341    let txt = txt.into_var();
342    match_node(child, move |_, op| {
343        if let UiNodeOp::Info { info } = op {
344            info.set_meta(*MNEMONIC_TXT_ID, txt.clone());
345        }
346    })
347}
348
349/// Defines a mnemonic shortcut scope.
350///
351/// When focus is within the scope widget and the focus event was caused by key press a
352/// [`GESTURES.click_shortcut`] is set for each [`mnemonic`] descendant.
353///
354/// [`mnemonic`]: fn@mnemonic
355/// [`GESTURES.click_shortcut`]: GESTURES::click_shortcut
356#[property(CONTEXT, default(false))]
357pub fn mnemonic_scope(child: impl IntoUiNode, is_scope: impl IntoVar<bool>) -> UiNode {
358    let is_scope = is_scope.into_var();
359    let mut init = false;
360    let update = var(());
361    let mut var_subs = VarHandles::dummy();
362    let mut shortcut_subs = vec![];
363    let mut is_focus_within = false;
364    let active_mnemonics = var(HashMap::new());
365    let child = with_context_var(child, ACTIVE_MNEMONICS_VAR, active_mnemonics.read_only());
366    match_node(child, move |_, op| match op {
367        UiNodeOp::Init => {
368            WIDGET.sub_var_info(&is_scope).sub_var(&update);
369        }
370        UiNodeOp::Deinit => {
371            init = false;
372            var_subs = VarHandles::dummy();
373            shortcut_subs = vec![];
374            is_focus_within = false;
375            active_mnemonics.set(HashMap::new());
376        }
377        UiNodeOp::Info { info } => {
378            if is_scope.get() {
379                info.flag_meta(*MNEMONIC_SCOPE_ID);
380            }
381            init = true;
382            WIDGET.update();
383        }
384        UiNodeOp::Update { .. } => {
385            let mut set_shortcuts = false;
386            if mem::take(&mut init) {
387                var_subs.clear();
388                shortcut_subs.clear();
389
390                if is_scope.get() {
391                    // sub to is_focus_within
392                    let id = WIDGET.id();
393                    var_subs.push(
394                        FOCUS_CHANGED_EVENT.subscribe_when(UpdateOp::Update, id, move |a| a.is_focus_enter(id) || a.is_focus_leave(id)),
395                    );
396                    is_focus_within = FOCUS.is_highlighting().get() && FOCUS.focused().with(|f| matches!(f, Some(f) if f.contains(id)));
397                    set_shortcuts = is_focus_within;
398
399                    // sub to each descendant mnemonic properties
400                    let mut var_sub = |v: &AnyVar| {
401                        let update_wk = update.downgrade();
402                        var_subs.push(v.hook(move |_| match update_wk.upgrade() {
403                            Some(u) => {
404                                u.update();
405                                true
406                            }
407                            None => false,
408                        }));
409                    };
410                    for d in WIDGET.info().self_and_descendants() {
411                        if let Some(m) = d.meta().get(*MNEMONIC_ID) {
412                            // descendant sets `mnemonic`, subscribe
413                            var_sub(m.as_any());
414                        }
415                        if let Some(t) = d.meta().get(*MNEMONIC_TXT_ID) {
416                            // descendant sets `mnemonic_txt`, subscribe
417                            var_sub(t.as_any());
418                        }
419                    }
420                }
421            } else if is_scope.get() {
422                // else if is inited and enabled, check is_focus_within change
423                let id = WIDGET.id();
424                FOCUS_CHANGED_EVENT.each_update(true, |a| {
425                    let is_within = a.highlight
426                        && match &a.new_focus {
427                            Some(f) => {
428                                // don't activate if is in inner scope
429                                f.contains(id)
430                                    && WINDOW
431                                        .info()
432                                        .get(f.widget_id())
433                                        .unwrap()
434                                        .self_and_ancestors()
435                                        .find(|w| w.is_mnemonic_scope())
436                                        .unwrap()
437                                        .id()
438                                        == id
439                            }
440                            None => false,
441                        };
442                    if is_within != is_focus_within {
443                        if is_within {
444                            is_focus_within = true;
445                            set_shortcuts = true;
446                        } else {
447                            is_focus_within = false;
448                            shortcut_subs.clear();
449                            active_mnemonics.modify(|a| {
450                                if !a.is_empty() {
451                                    a.clear();
452                                }
453                            });
454                        }
455                    }
456                });
457            }
458
459            if is_focus_within && (set_shortcuts || update.is_new()) {
460                // focus entered OR inited and is focus within OR is focus within and descendant state changed
461
462                shortcut_subs.clear();
463
464                let mut chars = HashMap::new();
465                let mut auto = vec![];
466                let info = WIDGET.info();
467                let scope_and_descendants = info.self_and_descendants().tree_filter(|w| {
468                    if w != &info && w.is_mnemonic_scope() {
469                        TreeFilter::SkipAll
470                    } else {
471                        TreeFilter::Include
472                    }
473                });
474                for d in scope_and_descendants {
475                    if let Some(m) = d.mnemonic() {
476                        // descendant sets `mnemonic`
477                        let mut m = m.get();
478
479                        // extract ::Char from inner text
480                        if let Mnemonic::FromTxt { marker, fallback_auto } = m {
481                            // fallback state
482                            m = if fallback_auto { Mnemonic::Auto } else { Mnemonic::None };
483
484                            let mnemonic_and_descendants = d.self_and_descendants().tree_filter(|w| {
485                                if w != &d && (w.is_mnemonic_scope() || w.mnemonic().is_some()) {
486                                    TreeFilter::SkipAll
487                                } else {
488                                    TreeFilter::Include
489                                }
490                            });
491                            for d in mnemonic_and_descendants {
492                                if let Some(txt) = d.mnemonic_txt() {
493                                    let c = txt.with(|txt| {
494                                        let mut return_next = false;
495                                        for c in txt.chars() {
496                                            if return_next {
497                                                return Some(c);
498                                            }
499                                            return_next = c == marker;
500                                        }
501                                        None
502                                    });
503                                    if let Some(c) = c {
504                                        m = Mnemonic::Char(c);
505                                        break;
506                                    }
507                                }
508                            }
509                        }
510
511                        // validate and register ::Char
512                        if let Mnemonic::Char(c) = m {
513                            if c.is_alphanumeric() {
514                                match chars.entry(c.to_lowercase().collect::<Txt>()) {
515                                    hash_map::Entry::Vacant(e) => {
516                                        // valid char
517                                        e.insert((d.id(), c));
518                                        m = Mnemonic::None;
519                                    }
520                                    hash_map::Entry::Occupied(e) => {
521                                        tracing::error!("both {:?} and {:?} set the same mnemonic {:?}", e.get().0, d.id(), c);
522                                        m = Mnemonic::None;
523                                    }
524                                }
525                            } else {
526                                tracing::error!("char `{c:?}` cannot be a mnemonic, not alphanumeric");
527                                m = Mnemonic::None;
528                            }
529                        }
530
531                        // collect ::Auto
532                        if let Mnemonic::Auto = m {
533                            auto.push(d.into_focus_info(true, true));
534                        }
535                    }
536                }
537                // select best char for ::Auto
538                //
539                // - Prefers chars from words that only appear in one label
540                // - Prefers uppercase chars
541                let mut mnemonic_words = HashMap::<Txt, IdSet<WidgetId>>::new();
542                let mut id_words = IdMap::<WidgetId, Vec<Txt>>::new();
543                auto.sort_by_key(|w| w.focus_info().tab_index());
544                for d in &auto {
545                    let d = d.info();
546                    let mut found_txt = false;
547
548                    let mnemonic_and_descendants = d.self_and_descendants().tree_filter(|w| {
549                        if w != d && (w.is_mnemonic_scope() || w.mnemonic().is_some()) {
550                            TreeFilter::SkipAll
551                        } else {
552                            TreeFilter::Include
553                        }
554                    });
555                    for w in mnemonic_and_descendants {
556                        if let Some(txt) = w.mnemonic_txt() {
557                            found_txt = true;
558                            txt.with(|t| {
559                                for word in t.split(' ') {
560                                    let word = word.trim();
561                                    if !word.is_empty() {
562                                        let word = Txt::from_str(word);
563                                        if mnemonic_words.entry(word.clone()).or_default().insert(d.id()) {
564                                            id_words.entry(d.id()).or_default().push(word);
565                                        }
566                                    }
567                                }
568                            })
569                        }
570                    }
571                    if !found_txt {
572                        tracing::warn!(
573                            "no mnemonic selected for {:?}, consider using `Label!` for the inner text or set `mnemonic_txt`",
574                            d.id()
575                        );
576                    }
577                }
578                'select: for d in auto {
579                    let d = d.info();
580                    if let Some(mut words) = id_words.remove(&d.id()) {
581                        words.sort_by_key(|w| mnemonic_words.get(w).unwrap().len());
582
583                        // try uppercase chars first
584                        for w in &words {
585                            for c in w.chars() {
586                                if c.is_alphanumeric()
587                                    && c.is_uppercase()
588                                    && let hash_map::Entry::Vacant(e) = chars.entry(c.to_lowercase().collect::<Txt>())
589                                {
590                                    e.insert((d.id(), c));
591                                    continue 'select;
592                                }
593                            }
594                        }
595                        // try other alphanumeric chars
596                        for w in &words {
597                            for c in w.chars() {
598                                if c.is_alphanumeric()
599                                    && !c.is_uppercase()
600                                    && let hash_map::Entry::Vacant(e) = chars.entry(Txt::from_char(c))
601                                {
602                                    e.insert((d.id(), c));
603                                    continue 'select;
604                                }
605                            }
606                        }
607                    }
608                }
609
610                // register shortcuts
611                for (id, c) in chars.values() {
612                    let h = GESTURES.click_shortcut(GestureKey::Key(Key::Char(*c)), ShortcutClick::Primary, *id);
613                    shortcut_subs.push(h);
614                }
615                active_mnemonics.modify(move |m| {
616                    m.clear();
617                    for (_, (id, c)) in chars {
618                        m.insert(id, c);
619                    }
620                });
621            }
622        }
623        _ => {}
624    })
625}
626
627/// Get the active mnemonic shortcut char for this widget or ancestor.
628///
629/// If this widget or ancestor enables [`mnemonic`] the `state` is set to the selected mnemonic char when focus is within
630/// the parent [`mnemonic_scope`].
631///
632/// [`mnemonic`]: fn@mnemonic
633/// [`mnemonic_scope`]: fn@mnemonic_scope
634#[property(WIDGET_INNER)]
635pub fn get_mnemonic_char(child: impl IntoUiNode, state: impl IntoVar<Option<char>>) -> UiNode {
636    bind_state_info(child, state, move |s| {
637        let info = WIDGET.info();
638        for w in info.self_and_ancestors() {
639            let found_scope = w.is_mnemonic_scope();
640            if found_scope && w != info {
641                break;
642            }
643
644            if w.mnemonic().is_some() {
645                let id = w.id();
646                return ACTIVE_MNEMONICS_VAR.set_bind_map(s, move |m| m.get(&id).copied());
647            }
648
649            if found_scope {
650                break;
651            }
652        }
653        VarHandle::dummy()
654    })
655}
656
657/// Gets the mnemonic mode enabled for this widget or ancestor.
658///
659/// If this widget or ancestor enables [`mnemonic`] the `state` is set to the mnemonic mode.
660///
661/// [`mnemonic`]: fn@mnemonic
662#[property(WIDGET_INNER, default(var(Mnemonic::None)))]
663pub fn get_mnemonic(child: impl IntoUiNode, state: impl IntoVar<Mnemonic>) -> UiNode {
664    bind_state_info(child, state, move |s| {
665        let info = WIDGET.info();
666        for w in info.self_and_ancestors() {
667            let found_scope = w.is_mnemonic_scope();
668            if found_scope && w != info {
669                break;
670            }
671
672            if let Some(m) = w.mnemonic() {
673                return m.set_bind(s);
674            }
675
676            if found_scope {
677                break;
678            }
679        }
680        VarHandle::dummy()
681    })
682}
683
684static_id! {
685    static ref MNEMONIC_SCOPE_ID: StateId<()>;
686    static ref MNEMONIC_ID: StateId<Var<Mnemonic>>;
687    static ref MNEMONIC_TXT_ID: StateId<Var<Txt>>;
688}
689
690context_var! {
691    /// Inside an active [`mnemonic_scope`] this context var is a read-only map of the selected `char` for each descendant of the scope.
692    ///
693    /// [`mnemonic_scope`]: fn@mnemonic_scope
694    pub static ACTIVE_MNEMONICS_VAR: HashMap<WidgetId, char> = HashMap::new();
695}
696
697/// Extension methods for widget info about mnemonic metadata.
698pub trait MnemonicWidgetInfoExt {
699    /// If [`mnemonic_scope`] is enabled in the widget.
700    ///
701    /// [`mnemonic_scope`]: fn@mnemonic_scope
702    fn is_mnemonic_scope(&self) -> bool;
703    /// Reference the [`mnemonic`] set on this widget.
704    ///
705    /// [`mnemonic`]: fn@mnemonic
706    fn mnemonic(&self) -> Option<&Var<Mnemonic>>;
707
708    /// Reference the [`mnemonic_txt`] set on this widget.
709    ///
710    /// [`mnemonic_txt`]: fn@mnemonic_txt
711    fn mnemonic_txt(&self) -> Option<&Var<Txt>>;
712}
713impl MnemonicWidgetInfoExt for WidgetInfo {
714    fn is_mnemonic_scope(&self) -> bool {
715        self.meta().flagged(*MNEMONIC_SCOPE_ID)
716    }
717
718    fn mnemonic(&self) -> Option<&Var<Mnemonic>> {
719        self.meta().get(*MNEMONIC_ID)
720    }
721
722    fn mnemonic_txt(&self) -> Option<&Var<Txt>> {
723        self.meta().get(*MNEMONIC_TXT_ID)
724    }
725}