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