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}