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#![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#[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 pub zng_wgt_text::font_size(size: impl IntoVar<zng_ext_font::FontSize>);
38 pub zng_wgt_text::font_color(color: impl IntoVar<Rgba>);
40 }
41}
42
43#[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 pub static FIRST_N_VAR: usize = 1;
55
56 pub static PANEL_FN_VAR: WidgetFn<PanelFnArgs> = WidgetFn::new(default_panel_fn);
58
59 pub static SHORTCUTS_SEPARATOR_FN_VAR: WidgetFn<ShortcutsSeparatorFnArgs> = WidgetFn::new(default_shortcuts_separator_fn);
61
62 pub static SHORTCUT_FN_VAR: WidgetFn<ShortcutFnArgs> = WidgetFn::nil();
64
65 pub static CHORD_SEPARATOR_FN_VAR: WidgetFn<ChordSeparatorFnArgs> = WidgetFn::new(default_chord_separator_fn);
67
68 pub static MODIFIER_FN_VAR: WidgetFn<ModifierFnArgs> = WidgetFn::new(default_modifier_fn);
70
71 pub static KEY_GESTURE_FN_VAR: WidgetFn<KeyGestureFnArgs> = WidgetFn::nil();
73
74 pub static KEY_GESTURE_SEPARATOR_FN_VAR: WidgetFn<KeyGestureSeparatorFnArgs> = WidgetFn::new(default_key_gesture_separator_fn);
76
77 pub static KEY_FN_VAR: WidgetFn<KeyFnArgs> = WidgetFn::new(default_key_fn);
79
80 pub static NONE_FN_VAR: WidgetFn<NoneFnArgs> = WidgetFn::nil();
82}
83
84#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[non_exhaustive]
191pub struct PanelFnArgs {
192 pub items: UiVec,
194
195 pub is_none: bool,
199
200 pub shortcuts: Shortcuts,
202}
203
204#[non_exhaustive]
208pub struct NoneFnArgs {}
209
210#[non_exhaustive]
214pub struct ShortcutsSeparatorFnArgs {}
215
216#[non_exhaustive]
220pub struct ShortcutFnArgs {
221 pub items: UiVec,
223 pub shortcut: Shortcut,
227}
228
229#[non_exhaustive]
233pub struct ChordSeparatorFnArgs {}
234
235#[non_exhaustive]
239pub struct KeyGestureFnArgs {
240 pub items: UiVec,
242 pub gesture: KeyGesture,
246}
247
248#[non_exhaustive]
252pub struct ModifierFnArgs {
253 pub modifier: ModifierGesture,
255 pub is_standalone: bool,
261}
262
263pub struct KeyFnArgs {
267 pub key: GestureKey,
269}
270impl KeyFnArgs {
271 pub fn is_editing_blank(&self) -> bool {
275 matches!(&self.key, GestureKey::Key(Key::Unidentified))
276 }
277}
278
279#[non_exhaustive]
283pub struct KeyGestureSeparatorFnArgs {
284 pub between_modifiers: bool,
288}
289
290pub 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
301pub fn default_shortcuts_separator_fn(_: ShortcutsSeparatorFnArgs) -> UiNode {
305 zng_wgt_text::Text!(" or ")
306}
307
308pub fn default_chord_separator_fn(_: ChordSeparatorFnArgs) -> UiNode {
312 zng_wgt_text::Text!(", ")
313}
314
315pub fn default_key_gesture_separator_fn(_: KeyGestureSeparatorFnArgs) -> UiNode {
319 zng_wgt_text::Text!("+")
320}
321
322pub fn default_modifier_fn(args: ModifierFnArgs) -> UiNode {
326 keycap(modifier_txt(args.modifier), args.is_standalone)
327}
328
329pub 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
340pub 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
597pub fn modifier_txt(modifier: ModifierGesture) -> Var<Txt> {
599 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
626pub fn key_txt(key: GestureKey) -> Var<Txt> {
628 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}