zng_wgt_shortcut/
lib.rs

1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3//!
4//! Shortcut display widget.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12zng_wgt::enable_widget_macros!();
13
14use zng_app::shortcut::{GestureKey, KeyGesture, ModifierGesture, Shortcut};
15use zng_view_api::keyboard::Key;
16use zng_wgt::prelude::*;
17use zng_wgt_wrap::Wrap;
18
19/// Display keyboard shortcuts.
20#[widget($crate::ShortcutText {
21    ($shortcut:expr) => {
22        shortcut = $shortcut;
23    }
24})]
25pub struct ShortcutText(WidgetBase);
26
27impl ShortcutText {
28    fn widget_intrinsic(&mut self) {
29        self.widget_builder().push_build_action(|wgt| {
30            let s = wgt.capture_var_or_default::<Shortcuts>(property_id!(shortcut));
31            wgt.set_child(node(s));
32        });
33    }
34
35    widget_impl! {
36        /// Font size of the shortcut text.
37        pub zng_wgt_text::font_size(size: impl IntoVar<zng_ext_font::FontSize>);
38        /// Font color of the shortcut text.
39        pub zng_wgt_text::font_color(color: impl IntoVar<Rgba>);
40    }
41}
42
43/// Shortcut(s)  that must be displayed
44#[property(CHILD, widget_impl(ShortcutText))]
45pub fn shortcut(wgt: &mut WidgetBuilding, shortcuts: impl IntoVar<Shortcuts>) {
46    let _ = shortcuts;
47    wgt.expect_property_capture();
48}
49
50context_var! {
51    /// Maximum number of shortcuts to display.
52    ///
53    /// Is `1` by default.
54    pub static FIRST_N_VAR: usize = 1;
55
56    /// Widget function that generates the outer panel.
57    pub static PANEL_FN_VAR: WidgetFn<PanelFnArgs> = WidgetFn::new(default_panel_fn);
58
59    /// Widget function that generates the separator between shortcuts.
60    pub static SHORTCUTS_SEPARATOR_FN_VAR: WidgetFn<ShortcutsSeparatorFnArgs> = WidgetFn::new(default_shortcuts_separator_fn);
61
62    /// Widget function that generates a shortcut panel.
63    pub static SHORTCUT_FN_VAR: WidgetFn<ShortcutFnArgs> = WidgetFn::nil();
64
65    /// Widget function that generates the separator between key gestures in chord shortcuts.
66    pub static CHORD_SEPARATOR_FN_VAR: WidgetFn<ChordSeparatorFnArgs> = WidgetFn::new(default_chord_separator_fn);
67
68    /// Widget function that generates the modifier view.
69    pub static MODIFIER_FN_VAR: WidgetFn<ModifierFnArgs> = WidgetFn::new(default_modifier_fn);
70
71    /// Widget function that generates the key gesture panel.
72    pub static KEY_GESTURE_FN_VAR: WidgetFn<KeyGestureFnArgs> = WidgetFn::nil();
73
74    /// Widget function that generates the separators between modifiers and keys in a key gesture.
75    pub static KEY_GESTURE_SEPARATOR_FN_VAR: WidgetFn<KeyGestureSeparatorFnArgs> = WidgetFn::new(default_key_gesture_separator_fn);
76
77    /// Widget function that generates the key view.
78    pub static KEY_FN_VAR: WidgetFn<KeyFnArgs> = WidgetFn::new(default_key_fn);
79
80    /// Widget function that generates content when there is no gesture to display.
81    pub static NONE_FN_VAR: WidgetFn<NoneFnArgs> = WidgetFn::nil();
82}
83
84/// Maximum number of shortcuts to display.
85///
86/// This property sets the [`FIRST_N_VAR`].
87#[property(CONTEXT, default(FIRST_N_VAR), widget_impl(ShortcutText))]
88pub fn first_n(child: impl IntoUiNode, n: impl IntoVar<usize>) -> UiNode {
89    with_context_var(child, FIRST_N_VAR, n)
90}
91
92/// Widget function that converts [`PanelFnArgs`] to a widget.
93///
94/// This property sets the [`PANEL_FN_VAR`].
95#[property(CONTEXT, default(PANEL_FN_VAR), widget_impl(ShortcutText))]
96pub fn panel_fn(child: impl IntoUiNode, panel_fn: impl IntoVar<WidgetFn<PanelFnArgs>>) -> UiNode {
97    with_context_var(child, PANEL_FN_VAR, panel_fn)
98}
99
100/// Widget function that converts [`NoneFnArgs`] to a widget.
101///
102/// This property sets the [`NONE_FN_VAR`].
103#[property(CONTEXT, default(NONE_FN_VAR), widget_impl(ShortcutText))]
104pub fn none_fn(child: impl IntoUiNode, none_fn: impl IntoVar<WidgetFn<NoneFnArgs>>) -> UiNode {
105    with_context_var(child, NONE_FN_VAR, none_fn)
106}
107
108/// Widget function that converts [`ShortcutsSeparatorFnArgs`] to a widget.
109///
110/// This is the separators between shortcuts, when [`first_n`] is more than one and the [`shortcut`] has mode them one shortcut.
111///
112/// Set to nil to not generate a separator.
113///
114/// This property sets the [`SHORTCUTS_SEPARATOR_FN_VAR`].
115///
116/// [`first_n`]: fn@first_n
117/// [`shortcut`]: fn@shortcut
118#[property(CONTEXT, default(SHORTCUTS_SEPARATOR_FN_VAR), widget_impl(ShortcutText))]
119pub fn shortcuts_separator_fn(child: impl IntoUiNode, separator_fn: impl IntoVar<WidgetFn<ShortcutsSeparatorFnArgs>>) -> UiNode {
120    with_context_var(child, SHORTCUTS_SEPARATOR_FN_VAR, separator_fn)
121}
122
123/// Widget function that converts [`ShortcutFnArgs`] to a widget.
124///
125/// Set to [`WidgetFn::nil`] or return the items as a node to pass the items directly to [`panel_fn`].
126///
127/// This property sets the [`SHORTCUT_FN_VAR`].
128///
129/// [`panel_fn`]: fn@panel_fn
130#[property(CONTEXT, default(SHORTCUT_FN_VAR), widget_impl(ShortcutText))]
131pub fn shortcut_fn(child: impl IntoUiNode, panel_fn: impl IntoVar<WidgetFn<ShortcutFnArgs>>) -> UiNode {
132    with_context_var(child, SHORTCUT_FN_VAR, panel_fn)
133}
134
135/// Widget function that converts [`ChordSeparatorFnArgs`] to a widget.
136///
137/// This is the separator between the starter and complement in a [`KeyChord`].
138///
139/// This property sets the [`CHORD_SEPARATOR_FN_VAR`].
140///
141/// [`KeyChord`]: zng_app::shortcut::KeyChord
142#[property(CONTEXT, default(CHORD_SEPARATOR_FN_VAR), widget_impl(ShortcutText))]
143pub fn chord_separator_fn(child: impl IntoUiNode, separator_fn: impl IntoVar<WidgetFn<ChordSeparatorFnArgs>>) -> UiNode {
144    with_context_var(child, CHORD_SEPARATOR_FN_VAR, separator_fn)
145}
146
147/// Widget function that converts [`KeyGestureFnArgs`] to a widget.
148///
149/// Set to [`WidgetFn::nil`] or return the items as a node to pass the items directly to [`shortcut_fn`].
150///
151/// This property sets the [`KEY_GESTURE_FN_VAR`].
152///
153/// [`shortcut_fn`]: fn@shortcut_fn
154#[property(CONTEXT, default(KEY_GESTURE_FN_VAR), widget_impl(ShortcutText))]
155pub fn key_gesture_fn(child: impl IntoUiNode, panel_fn: impl IntoVar<WidgetFn<KeyGestureFnArgs>>) -> UiNode {
156    with_context_var(child, KEY_GESTURE_FN_VAR, panel_fn)
157}
158
159/// Widget function that converts [`KeyGestureSeparatorFnArgs`] to a widget.
160///
161/// This is the separator between the modifiers and key in a [`KeyGesture`].
162///
163/// This property sets the [`KEY_GESTURE_SEPARATOR_FN_VAR`].
164#[property(CONTEXT, default(KEY_GESTURE_SEPARATOR_FN_VAR), widget_impl(ShortcutText))]
165pub fn key_gesture_separator_fn(child: impl IntoUiNode, separator_fn: impl IntoVar<WidgetFn<KeyGestureSeparatorFnArgs>>) -> UiNode {
166    with_context_var(child, KEY_GESTURE_SEPARATOR_FN_VAR, separator_fn)
167}
168
169/// Widget function that converts a [`ModifierFnArgs`] to a widget.
170///
171/// This is used for both the [`Shortcut::Modifier`] standalone and the [`KeyGesture::modifiers`] flags.
172///
173/// This property sets the [`MODIFIER_FN_VAR`].
174#[property(CONTEXT, default(MODIFIER_FN_VAR), widget_impl(ShortcutText))]
175pub fn modifier_fn(child: impl IntoUiNode, modifier_fn: impl IntoVar<WidgetFn<ModifierFnArgs>>) -> UiNode {
176    with_context_var(child, MODIFIER_FN_VAR, modifier_fn)
177}
178
179/// Widget function that converts a [`KeyFnArgs`] to a widget.
180///  
181/// This property sets the [`KEY_FN_VAR`].
182#[property(CONTEXT, default(KEY_FN_VAR), widget_impl(ShortcutText))]
183pub fn key_fn(child: impl IntoUiNode, key_fn: impl IntoVar<WidgetFn<KeyFnArgs>>) -> UiNode {
184    with_context_var(child, KEY_FN_VAR, key_fn)
185}
186
187/// Arguments for [`panel_fn`].
188///
189/// [`panel_fn`]: fn@panel_fn
190#[non_exhaustive]
191pub struct PanelFnArgs {
192    /// Shortcut and shortcut separator items.
193    pub items: UiVec,
194
195    /// If the single item in `items` is the [`none_fn`].
196    ///
197    /// [`none_fn`]: fn@none_fn
198    pub is_none: bool,
199
200    /// The shortcuts that where used to generate the `items`.
201    pub shortcuts: Shortcuts,
202}
203
204/// Arguments for [`none_fn`].
205///
206/// [`none_fn`]: fn@none_fn
207#[non_exhaustive]
208pub struct NoneFnArgs {}
209
210/// Arguments for [`shortcuts_separator_fn`].
211///
212/// [`shortcuts_separator_fn`]: fn@shortcuts_separator_fn
213#[non_exhaustive]
214pub struct ShortcutsSeparatorFnArgs {}
215
216/// Arguments for [`shortcut_fn`].
217///
218/// [`shortcut_fn`]: fn@shortcut_fn
219#[non_exhaustive]
220pub struct ShortcutFnArgs {
221    /// Modifier, key and separator items.
222    pub items: UiVec,
223    /// The shortcut.
224    ///
225    /// The `items` where instantiated from components of this shortcut.
226    pub shortcut: Shortcut,
227}
228
229/// Arguments for [`chord_separator_fn`].
230///
231/// [`chord_separator_fn`]: fn@chord_separator_fn
232#[non_exhaustive]
233pub struct ChordSeparatorFnArgs {}
234
235/// Arguments for [`key_gesture_fn`].
236///
237/// [`key_gesture_fn`]: fn@key_gesture_fn
238#[non_exhaustive]
239pub struct KeyGestureFnArgs {
240    /// Modifier, key and separator items.
241    pub items: UiVec,
242    /// The key gesture.
243    ///
244    /// The `items` where instantiated from components of this gesture.
245    pub gesture: KeyGesture,
246}
247
248/// Arguments for [`modifier_fn`].
249///
250/// [`modifier_fn`]: fn@modifier_fn
251#[non_exhaustive]
252pub struct ModifierFnArgs {
253    /// The modifier.
254    pub modifier: ModifierGesture,
255    /// If is actually the [`Shortcut::Modifier`] press and release gesture.
256    ///
257    /// If `false` is actually a [`ModifiersState`] flag extracted from [`KeyGesture::modifiers`].
258    ///
259    /// [`ModifiersState`]: zng_app::shortcut::ModifiersState
260    pub is_standalone: bool,
261}
262
263/// Arguments for [`key_fn`].
264///
265/// [`key_fn`]: fn@key_fn
266pub struct KeyFnArgs {
267    /// The key.
268    pub key: GestureKey,
269}
270impl KeyFnArgs {
271    /// If the `key` is an invalid value that indicates a editing shortcut.
272    ///
273    /// Widget function should return an invisible, but not collapsed blank space, recommended `Text!(" ")` without any style.
274    pub fn is_editing_blank(&self) -> bool {
275        matches!(&self.key, GestureKey::Key(Key::Unidentified))
276    }
277}
278
279/// Arguments for [`key_gesture_separator_fn`].
280///
281/// [`key_gesture_separator_fn`]: fn@key_gesture_separator_fn
282#[non_exhaustive]
283pub struct KeyGestureSeparatorFnArgs {
284    /// If the separator will be placed between two modifiers.
285    ///
286    /// When this is `false` the separator is placed between a modifier and the key.
287    pub between_modifiers: bool,
288}
289
290/// Default value for [`PANEL_FN_VAR`].
291///
292/// For zero items returns nil, for one item just returns the item, for more returns a `Wrap!`.
293pub fn default_panel_fn(mut args: PanelFnArgs) -> UiNode {
294    match args.items.len() {
295        0 => UiNode::nil(),
296        1 => args.items.remove(0),
297        _ => Wrap!(args.items),
298    }
299}
300
301/// Default value for [`SHORTCUTS_SEPARATOR_FN_VAR`].
302///
303/// Returns `Text!(" or ")`.
304pub fn default_shortcuts_separator_fn(_: ShortcutsSeparatorFnArgs) -> UiNode {
305    zng_wgt_text::Text!(" or ")
306}
307
308/// Default value for [`CHORD_SEPARATOR_FN_VAR`].
309///
310/// Returns `Text!(", ")`.
311pub fn default_chord_separator_fn(_: ChordSeparatorFnArgs) -> UiNode {
312    zng_wgt_text::Text!(", ")
313}
314
315/// Default value for [`KEY_GESTURE_SEPARATOR_FN_VAR`].
316///
317/// Returns `Text!("+")`.
318pub fn default_key_gesture_separator_fn(_: KeyGestureSeparatorFnArgs) -> UiNode {
319    zng_wgt_text::Text!("+")
320}
321
322/// Default value for [`MODIFIER_FN_VAR`].
323///
324/// Returns a [`keycap`] with the [`modifier_txt`].
325pub fn default_modifier_fn(args: ModifierFnArgs) -> UiNode {
326    keycap(modifier_txt(args.modifier), args.is_standalone)
327}
328
329/// Default value for [`KEY_FN_VAR`].
330///
331/// Returns a [`keycap`] with the [`key_txt`].
332pub fn default_key_fn(args: KeyFnArgs) -> UiNode {
333    if args.is_editing_blank() {
334        zng_wgt_text::Text!(" ")
335    } else {
336        keycap(key_txt(args.key), false)
337    }
338}
339
340/// Widget used b the [`default_modifier_fn`] and [`default_key_fn`] to render a `Text!` styled to look like a keycap.
341pub fn keycap(txt: Var<Txt>, is_standalone_modifier: bool) -> UiNode {
342    zng_wgt_text::Text! {
343        txt;
344        font_family = ["Consolas", "Lucida Console", "monospace"];
345        zng_wgt::border = {
346            widths: if is_standalone_modifier { 0.2 } else { 0.08 }.em().max(1.dip()),
347            sides: expr_var! {
348                let base = *#{zng_wgt_text::FONT_COLOR_VAR};
349                let color = match #{zng_color::COLOR_SCHEME_VAR} {
350                    ColorScheme::Dark => colors::BLACK.with_alpha(70.pct()).mix_normal(base),
351                    ColorScheme::Light => colors::WHITE.with_alpha(70.pct()).mix_normal(base),
352                    _ => base.with_alpha(30.pct()),
353                };
354                BorderSides::new_all((
355                    color,
356                    if is_standalone_modifier {
357                        BorderStyle::Double
358                    } else {
359                        BorderStyle::Solid
360                    },
361                ))
362            },
363        };
364        zng_wgt_fill::background_color = zng_color::COLOR_SCHEME_VAR.map(|c| match c {
365            ColorScheme::Dark => colors::BLACK,
366            ColorScheme::Light => colors::WHITE,
367            _ => zng_color::colors::BLACK.with_alpha(100.pct()),
368        });
369        zng_wgt::corner_radius = 0.2.em();
370        txt_align = Align::START;
371        zng_wgt::align = Align::START;
372        zng_wgt_container::padding = (0, 0.20.em(), -0.15.em(), 0.20.em());
373        zng_wgt::margin = (0, 0, -0.10.em(), 0);
374    }
375}
376
377fn node(shortcut: Var<Shortcuts>) -> UiNode {
378    match_node(UiNode::nil(), move |c, op| match op {
379        UiNodeOp::Init => {
380            WIDGET
381                .sub_var(&shortcut)
382                .sub_var(&PANEL_FN_VAR)
383                .sub_var(&SHORTCUTS_SEPARATOR_FN_VAR)
384                .sub_var(&SHORTCUT_FN_VAR)
385                .sub_var(&MODIFIER_FN_VAR)
386                .sub_var(&CHORD_SEPARATOR_FN_VAR)
387                .sub_var(&KEY_FN_VAR)
388                .sub_var(&KEY_GESTURE_SEPARATOR_FN_VAR)
389                .sub_var(&KEY_GESTURE_FN_VAR)
390                .sub_var(&FIRST_N_VAR)
391                .sub_var(&NONE_FN_VAR);
392            *c.node() = generate(shortcut.get());
393            c.init();
394        }
395        UiNodeOp::Deinit => {
396            c.deinit();
397            *c.node() = UiNode::nil();
398        }
399        UiNodeOp::Update { updates } => {
400            if shortcut.is_new()
401                || PANEL_FN_VAR.is_new()
402                || SHORTCUTS_SEPARATOR_FN_VAR.is_new()
403                || SHORTCUT_FN_VAR.is_new()
404                || MODIFIER_FN_VAR.is_new()
405                || CHORD_SEPARATOR_FN_VAR.is_new()
406                || KEY_FN_VAR.is_new()
407                || KEY_GESTURE_SEPARATOR_FN_VAR.is_new()
408                || KEY_GESTURE_FN_VAR.is_new()
409                || FIRST_N_VAR.is_new()
410                || NONE_FN_VAR.is_new()
411            {
412                c.deinit();
413                *c.node() = generate(shortcut.get());
414                c.init();
415            } else {
416                c.update(updates);
417            }
418        }
419        _ => {}
420    })
421}
422
423fn generate(mut shortcut: Shortcuts) -> UiNode {
424    let panel_fn = PANEL_FN_VAR.get();
425    let shortcuts_separator_fn = SHORTCUTS_SEPARATOR_FN_VAR.get();
426    let shortcut_fn = SHORTCUT_FN_VAR.get();
427    let modifier_fn = MODIFIER_FN_VAR.get();
428    let chord_separator_fn = CHORD_SEPARATOR_FN_VAR.get();
429    let separator_fn = KEY_GESTURE_SEPARATOR_FN_VAR.get();
430    let gesture_fn = KEY_GESTURE_FN_VAR.get();
431    let key_fn = KEY_FN_VAR.get();
432    let first_n = FIRST_N_VAR.get();
433
434    shortcut.truncate(first_n);
435
436    let mut items = ui_vec![];
437    for shortcut in shortcut.iter() {
438        if !items.is_empty()
439            && let Some(sep) = shortcuts_separator_fn.call_checked(ShortcutsSeparatorFnArgs {})
440        {
441            items.push(sep);
442        }
443
444        fn gesture(
445            out: &mut UiVec,
446            gesture: KeyGesture,
447            separator_fn: &WidgetFn<KeyGestureSeparatorFnArgs>,
448            modifier_fn: &WidgetFn<ModifierFnArgs>,
449            key_fn: &WidgetFn<KeyFnArgs>,
450            gesture_fn: &WidgetFn<KeyGestureFnArgs>,
451        ) {
452            let mut gesture_items = ui_vec![];
453
454            macro_rules! gen_modifier {
455                ($has:ident, $Variant:ident) => {
456                    if gesture.modifiers.$has()
457                        && let Some(n) = modifier_fn.call_checked(ModifierFnArgs {
458                            modifier: ModifierGesture::$Variant,
459                            is_standalone: false,
460                        })
461                    {
462                        if !gesture_items.is_empty()
463                            && let Some(s) = separator_fn.call_checked(KeyGestureSeparatorFnArgs {
464                                between_modifiers: true,
465                            })
466                        {
467                            gesture_items.push(s)
468                        }
469                        gesture_items.push(n);
470                    }
471                };
472            }
473            gen_modifier!(has_super, Super);
474            gen_modifier!(has_ctrl, Ctrl);
475            gen_modifier!(has_shift, Shift);
476            gen_modifier!(has_alt, Alt);
477
478            if let Some(n) = key_fn.call_checked(KeyFnArgs { key: gesture.key.clone() }) {
479                if !gesture_items.is_empty()
480                    && let Some(s) = separator_fn.call_checked(KeyGestureSeparatorFnArgs { between_modifiers: false })
481                {
482                    gesture_items.push(s);
483                }
484                gesture_items.push(n);
485            }
486
487            if gesture_fn.is_nil() {
488                out.append(&mut gesture_items);
489            } else {
490                let gesture = gesture_fn.call(KeyGestureFnArgs {
491                    items: gesture_items,
492                    gesture,
493                });
494                out.push(gesture);
495            }
496        }
497
498        let mut shortcut_items = ui_vec![];
499        match shortcut {
500            Shortcut::Gesture(g) => gesture(&mut shortcut_items, g.clone(), &separator_fn, &modifier_fn, &key_fn, &gesture_fn),
501            Shortcut::Chord(c) => {
502                gesture(
503                    &mut shortcut_items,
504                    c.starter.clone(),
505                    &separator_fn,
506                    &modifier_fn,
507                    &key_fn,
508                    &gesture_fn,
509                );
510                if !shortcut_items.is_empty()
511                    && let Some(s) = chord_separator_fn.call_checked(ChordSeparatorFnArgs {})
512                {
513                    shortcut_items.push(s);
514                }
515                gesture(
516                    &mut shortcut_items,
517                    c.complement.clone(),
518                    &separator_fn,
519                    &modifier_fn,
520                    &key_fn,
521                    &gesture_fn,
522                );
523            }
524            Shortcut::Modifier(g) => {
525                if let Some(m) = modifier_fn.call_checked(ModifierFnArgs {
526                    modifier: *g,
527                    is_standalone: true,
528                }) {
529                    shortcut_items.push(m);
530                }
531            }
532        }
533        if shortcut_fn.is_nil() {
534            items.append(&mut shortcut_items);
535        } else {
536            let mut s = shortcut_fn.call(ShortcutFnArgs {
537                items: shortcut_items,
538                shortcut: shortcut.clone(),
539            });
540            if let Some(flat) = s.downcast_mut::<UiVec>() {
541                items.append(flat);
542            } else {
543                items.push(s);
544            }
545        }
546    }
547
548    let mut is_none = false;
549    if items.is_empty() {
550        let none_fn = NONE_FN_VAR.get();
551        if let Some(n) = none_fn.call_checked(NoneFnArgs {}) {
552            items.push(n);
553            is_none = true;
554        }
555    }
556
557    panel_fn.call(PanelFnArgs {
558        items,
559        is_none,
560        shortcuts: shortcut,
561    })
562}
563
564mod l10n_helper {
565    use super::*;
566    use zng_ext_l10n::*;
567
568    fn path(file: &'static str) -> LangFilePath {
569        LangFilePath::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION").parse().unwrap(), file)
570    }
571
572    pub fn l10n(file: &'static str, name: &'static str) -> Var<Txt> {
573        let path = path(file);
574        let os_msg = L10N.message(path.clone(), name, std::env::consts::OS, "!FALLBACK").build();
575        let generic_msg = L10N.message(path, name, "", name).build();
576        l10n_expr(os_msg, generic_msg)
577    }
578
579    pub fn os_or(file: &'static str, name: &'static str, generic: Var<Txt>) -> Var<Txt> {
580        let path = path(file);
581        let os_msg = L10N.message(path.clone(), name, std::env::consts::OS, "!FALLBACK").build();
582        l10n_expr(os_msg, generic)
583    }
584
585    fn l10n_expr(os_msg: Var<Txt>, generic_msg: Var<Txt>) -> Var<Txt> {
586        expr_var! {
587            let os_msg = #{os_msg};
588            if os_msg == "!FALLBACK" {
589                #{generic_msg}.clone()
590            } else {
591                os_msg.clone()
592            }
593        }
594    }
595}
596
597/// Gets the localized modifier name.
598pub fn modifier_txt(modifier: ModifierGesture) -> Var<Txt> {
599    // l10n-modifiers-### Modifier key names
600    // l10n-modifiers-###
601    // l10n-modifiers-### * The ID is the `ModifierGesture` variant name. [1]
602    // l10n-modifiers-### * An OS generic text must be provided, optional OS specific text can be set as attributes.
603    // l10n-modifiers-### * OS attribute is a `std::env::consts::OS` value. [2]
604    // l10n-modifiers-###
605    // l10n-modifiers-### [1]: https://zng-ui.github.io/doc/zng/gesture/enum.ModifierGesture.html
606    // l10n-modifiers-### [2]: https://doc.rust-lang.org/std/env/consts/constant.OS.html
607    use zng_ext_l10n::*;
608    match modifier {
609        ModifierGesture::Super => match std::env::consts::OS {
610            "windows" => l10n!("modifiers/Super.windows", "⊞Win"),
611            "macos" => l10n!("modifiers/Super.macos", "⌘Command"),
612            _ => l10n_helper::os_or("modifiers", "Super", l10n!("modifiers/Super", "Super")),
613        },
614        ModifierGesture::Ctrl => match std::env::consts::OS {
615            "macos" => l10n!("modifiers/Ctrl.macos", "^Control"),
616            _ => l10n_helper::os_or("modifiers", "Ctrl", l10n!("modifiers/Ctrl", "Ctrl")),
617        },
618        ModifierGesture::Shift => l10n_helper::os_or("modifiers", "Shift", l10n!("modifiers/Shift", "⇧Shift")),
619        ModifierGesture::Alt => match std::env::consts::OS {
620            "macos" => l10n!("modifiers/Alt.macos", "⌥Option"),
621            _ => l10n_helper::os_or("modifiers", "Alt", l10n!("modifiers/Alt", "Alt")),
622        },
623    }
624}
625
626/// Gets the localized key name.
627pub fn key_txt(key: GestureKey) -> Var<Txt> {
628    // l10n-keys-### Valid gesture key names
629    // l10n-keys-###
630    // l10n-keys-### * The ID is the `Key` variant name. [1]
631    // l10n-keys-### * An OS generic text must be provided, optional OS specific text can be set as attributes.
632    // l10n-keys-### * OS attribute is a `std::env::consts::OS` value. [2]
633    // l10n-keys-### * L10n not include Char, Str, modifiers and composite keys.
634    // l10n-keys-###
635    // l10n-keys-### Note: This file does not include all valid keys, see [1] for a full list.
636    // l10n-keys-###
637    // l10n-keys-### [1]: https://zng-ui.github.io/doc/zng/keyboard/enum.Key.html
638    // l10n-keys-### [2]: https://doc.rust-lang.org/std/env/consts/constant.OS.html
639
640    use zng_ext_l10n::*;
641    if !key.is_valid() {
642        return const_var(Txt::from_static(""));
643    }
644    match key {
645        GestureKey::Key(key) => match key {
646            Key::Char(c) => c.to_uppercase().to_txt().into_var(),
647            Key::Str(s) => s.into_var(),
648            Key::Enter => match std::env::consts::OS {
649                "macos" => l10n!("keys/Enter.macos", "↵Return"),
650                _ => l10n_helper::os_or("keys", "Enter", l10n!("keys/Enter", "↵Enter")),
651            },
652            Key::Backspace => match std::env::consts::OS {
653                "macos" => l10n!("keys/Backspace.macos", "Delete"),
654                _ => l10n_helper::os_or("keys", "Backspace", l10n!("keys/Backspace", "←Backspace")),
655            },
656            Key::Delete => match std::env::consts::OS {
657                "macos" => l10n!("keys/Delete.macos", "Forward Delete"),
658                _ => l10n_helper::os_or("keys", "Delete", l10n!("keys/Delete", "Delete")),
659            },
660            Key::Tab => l10n_helper::os_or("keys", "Tab", l10n!("keys/Tab", "⭾Tab")),
661            Key::ArrowDown => l10n_helper::os_or("keys", "ArrowDown", l10n!("keys/ArrowDown", "↓")),
662            Key::ArrowLeft => l10n_helper::os_or("keys", "ArrowLeft", l10n!("keys/ArrowLeft", "←")),
663            Key::ArrowRight => l10n_helper::os_or("keys", "ArrowRight", l10n!("keys/ArrowRight", "→")),
664            Key::ArrowUp => l10n_helper::os_or("keys", "ArrowUp", l10n!("keys/ArrowUp", "↑")),
665            Key::PageDown => l10n_helper::os_or("keys", "PageDown", l10n!("keys/PageDown", "PgDn")),
666            Key::PageUp => l10n_helper::os_or("keys", "PageUp", l10n!("keys/PageUp", "PgUp")),
667            Key::Cut => l10n_helper::os_or("keys", "Cut", l10n!("keys/Cut", "Cut")),
668            Key::Copy => l10n_helper::os_or("keys", "Copy", l10n!("keys/Copy", "Copy")),
669            Key::Paste => l10n_helper::os_or("keys", "Paste", l10n!("keys/Paste", "Paste")),
670            Key::Undo => l10n_helper::os_or("keys", "Undo", l10n!("keys/Undo", "Undo")),
671            Key::Redo => l10n_helper::os_or("keys", "Redo", l10n!("keys/Redo", "Redo")),
672            Key::ContextMenu => l10n_helper::os_or("keys", "ContextMenu", l10n!("keys/ContextMenu", "≣Context Menu")),
673            Key::Escape => l10n_helper::os_or("keys", "Escape", l10n!("keys/Escape", "Esc")),
674            Key::Find => l10n_helper::os_or("keys", "Find", l10n!("keys/Find", "Find")),
675            Key::Help => l10n_helper::os_or("keys", "Help", l10n!("keys/Help", "?Help")),
676            Key::ZoomIn => l10n_helper::os_or("keys", "ZoomIn", l10n!("keys/ZoomIn", "+Zoom In")),
677            Key::ZoomOut => l10n_helper::os_or("keys", "ZoomOut", l10n!("keys/ZoomOut", "-Zoom Out")),
678            Key::Eject => l10n_helper::os_or("keys", "Eject", l10n!("keys/Eject", "⏏Eject")),
679            Key::PrintScreen => l10n_helper::os_or("keys", "PrintScreen", l10n!("keys/PrintScreen", "PrtSc")),
680            Key::Close => l10n_helper::os_or("keys", "Close", l10n!("keys/Close", "Close")),
681            Key::New => l10n_helper::os_or("keys", "New", l10n!("keys/New", "New")),
682            Key::Open => l10n_helper::os_or("keys", "Open", l10n!("keys/Open", "Open")),
683            Key::Print => l10n_helper::os_or("keys", "Print", l10n!("keys/Open", "Print")),
684            Key::Save => l10n_helper::os_or("keys", "Save", l10n!("keys/Save", "Save")),
685            key => l10n_helper::l10n("keys", key.name()),
686        },
687        GestureKey::Code(key_code) => formatx!("{key_code:?}").into_var(),
688    }
689}