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}