1use blockworx_paint::{Chord, Key, Modifiers};
11use blockworx_tools::commands::{CommandId, chords};
12use web_sys::Window;
13
14#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
16pub enum Token {
17 Command,
19 #[default]
20 Control,
21}
22
23impl Token {
24 #[must_use]
27 pub fn of(window: &Window) -> Self {
28 let agent = window.navigator().user_agent().unwrap_or_default();
29 if agent.contains("Mac") || agent.contains("iPhone") || agent.contains("iPad") {
30 Self::Command
31 } else {
32 Self::Control
33 }
34 }
35
36 fn held(self) -> &'static str {
37 match self {
38 Token::Command => "\u{2318}",
39 Token::Control => "Ctrl+",
40 }
41 }
42
43 fn shifted(self) -> &'static str {
44 match self {
45 Token::Command => "\u{2318}\u{21e7}",
46 Token::Control => "Ctrl+Shift+",
47 }
48 }
49}
50
51#[must_use]
53pub fn spelled(token: Token, chord: Chord) -> String {
54 let held = match chord.modifiers {
55 Modifiers::None => "",
56 Modifiers::Command => token.held(),
57 Modifiers::CommandShift => token.shifted(),
58 };
59 format!("{held}{}", pressed(chord.key))
60}
61
62#[must_use]
65pub fn hinted(token: Token, says: &str, id: CommandId) -> String {
66 let keys: Vec<String> = chords(id).map(|chord| spelled(token, *chord)).collect();
67 if keys.is_empty() {
68 says.to_owned()
69 } else {
70 format!("{says} ({})", keys.join(", "))
71 }
72}
73
74fn pressed(key: Key) -> &'static str {
75 match key {
76 Key::Num0 => "0",
77 Key::Num1 => "1",
78 Key::Num2 => "2",
79 Key::Num3 => "3",
80 Key::Num4 => "4",
81 Key::Num5 => "5",
82 Key::Num6 => "6",
83 Key::Num7 => "7",
84 Key::Num8 => "8",
85 Key::B => "B",
86 Key::E => "E",
87 Key::I => "I",
88 Key::K => "K",
89 Key::M => "M",
90 Key::N => "N",
91 Key::P => "P",
92 Key::R => "R",
93 Key::T => "T",
94 Key::Y => "Y",
95 Key::Z => "Z",
96 Key::Equals => "=",
97 Key::Plus => "+",
98 Key::Minus => "-",
99 Key::ArrowLeft => "\u{2190}",
100 Key::ArrowRight => "\u{2192}",
101 Key::ArrowUp => "\u{2191}",
102 Key::ArrowDown => "\u{2193}",
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use blockworx_tools::names::ToolName;
109
110 use super::*;
111
112 fn chord(modifiers: Modifiers, key: Key) -> Chord {
113 Chord { modifiers, key }
114 }
115
116 #[test]
117 fn a_bare_key_is_written_as_itself() {
118 assert_eq!(
119 spelled(Token::Control, chord(Modifiers::None, Key::Num3)),
120 "3",
121 );
122 }
123
124 #[test]
126 fn the_modifier_is_written_the_way_the_platform_writes_it() {
127 let undo = chord(Modifiers::Command, Key::Z);
128 assert_eq!(spelled(Token::Control, undo), "Ctrl+Z");
129 assert_eq!(spelled(Token::Command, undo), "\u{2318}Z");
130 let redo = chord(Modifiers::CommandShift, Key::Z);
131 assert_eq!(spelled(Token::Control, redo), "Ctrl+Shift+Z");
132 assert_eq!(spelled(Token::Command, redo), "\u{2318}\u{21e7}Z");
133 }
134
135 #[test]
138 fn a_hint_names_every_key_the_table_binds() {
139 let hint = hinted(
140 Token::Control,
141 "New Block",
142 CommandId::Arm(ToolName::NewBlock),
143 );
144 assert_eq!(hint, "New Block (2, Ctrl+B)");
145 assert_eq!(
146 hinted(Token::Control, "Redo", CommandId::Redo),
147 "Redo (Ctrl+Shift+Z, Ctrl+Y)",
148 );
149 }
150
151 #[test]
154 fn an_unbound_command_is_named_without_a_key() {
155 assert_eq!(
156 hinted(Token::Control, "Import\u{2026}", CommandId::Import),
157 "Import\u{2026}",
158 );
159 }
160}