Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/keymap/matcher_test.rs
StratoSDK / crates / strato-ui-core / src / keymap / matcher_test.rs
1use crate::keymap::macros::*;
2 
3use super::*;
4 
5#[test]
6fn test_matcher() -> anyhow::Result<()> {
7 #[derive(Debug, PartialEq)]
8 enum Action {
9 A(String),
10 B,
11 AB,
12 }
13 
14 let keymap = Keymap::new(vec![
15 FixedBinding::new("a", Action::A("b".into()), id!("a")),
16 FixedBinding::new("b", Action::B, id!("a")),
17 FixedBinding::new("a b", Action::AB, id!("a") | id!("b")),
18 ]);
19 
20 let mut ctx_a = Context::default();
21 ctx_a.set.insert("a");
22 
23 let mut ctx_b = Context::default();
24 ctx_b.set.insert("b");
25 
26 let mut matcher = Matcher::new(keymap);
27 
28 let view_id = EntityId::new();
29 
30 // Basic match
31 assert_eq!(
32 matcher
33 .test_keystroke("a", view_id, &ctx_a)
34 .unwrap()
35 .as_action::<Action>(),
36 &Action::A("b".into())
37 );
38 
39 // Multi-keystroke match
40 assert!(matcher.test_keystroke("a", view_id, &ctx_b).is_none());
41 assert_eq!(
42 matcher
43 .test_keystroke("b", view_id, &ctx_b)
44 .unwrap()
45 .as_action::<Action>(),
46 &Action::AB
47 );
48 
49 // Failed matches don't interfere with matching subsequent keys
50 assert!(matcher.test_keystroke("x", view_id, &ctx_a).is_none());
51 assert_eq!(
52 matcher
53 .test_keystroke("a", view_id, &ctx_a)
54 .unwrap()
55 .as_action::<Action>(),
56 &Action::A("b".into())
57 );
58 
59 // Pending keystrokes are cleared when the context changes
60 assert!(matcher.test_keystroke("a", view_id, &ctx_b).is_none());
61 assert_eq!(
62 matcher
63 .test_keystroke("b", view_id, &ctx_a)
64 .unwrap()
65 .as_action::<Action>(),
66 &Action::B
67 );
68 
69 let mut ctx_c = Context::default();
70 ctx_c.set.insert("c");
71 
72 // Pending keystrokes are maintained per-view
73 let view_id1 = EntityId::new();
74 let view_id2 = EntityId::new();
75 assert_ne!(view_id1, view_id2);
76 assert!(matcher.test_keystroke("a", view_id1, &ctx_b).is_none());
77 assert!(matcher.test_keystroke("a", view_id2, &ctx_c).is_none());
78 assert_eq!(
79 matcher
80 .test_keystroke("b", view_id1, &ctx_b)
81 .unwrap()
82 .as_action::<Action>(),
83 &Action::AB
84 );
85 
86 Ok(())
87}
88 
89#[test]
90fn test_editable_binding_matching() {
91 #[derive(Debug, PartialEq)]
92 enum Action {
93 A(&'static str),
94 B,
95 AOrB,
96 }
97 
98 let mut keymap = Keymap::default();
99 use crate::keymap::macros::*;
100 keymap.register_editable_bindings([
101 EditableBinding::new("a", "Action for A", Action::A("b"))
102 .with_key_binding("a")
103 .with_context_predicate(id!("a")),
104 EditableBinding::new("b", "Action for B", Action::B)
105 .with_key_binding("b")
106 .with_context_predicate(id!("a")),
107 EditableBinding::new("a_or_b", "Action for A or B", Action::AOrB)
108 .with_key_binding("a b")
109 .with_context_predicate(id!("a") | id!("b")),
110 ]);
111 
112 let mut ctx_a = Context::default();
113 ctx_a.set.insert("a");
114 
115 let mut ctx_b = Context::default();
116 ctx_b.set.insert("b");
117 
118 let mut matcher = Matcher::new(keymap);
119 
120 let view_id = EntityId::new();
121 
122 // Basic match
123 assert_eq!(
124 matcher
125 .test_keystroke("a", view_id, &ctx_a)
126 .unwrap()
127 .as_action::<Action>(),
128 &Action::A("b"),
129 );
130 
131 // Multi-keystroke match
132 assert!(matcher.test_keystroke("a", view_id, &ctx_b).is_none());
133 assert_eq!(
134 matcher
135 .test_keystroke("b", view_id, &ctx_b)
136 .unwrap()
137 .as_action::<Action>(),
138 &Action::AOrB
139 );
140 
141 // Failed matches don't interfere with matching subsequent keys
142 assert!(matcher.test_keystroke("x", view_id, &ctx_a).is_none());
143 assert_eq!(
144 matcher
145 .test_keystroke("a", view_id, &ctx_a)
146 .unwrap()
147 .as_action::<Action>(),
148 &Action::A("b")
149 );
150 
151 // Pending keystrokes are cleared when the context changes
152 assert!(matcher.test_keystroke("a", view_id, &ctx_b).is_none());
153 assert_eq!(
154 matcher
155 .test_keystroke("b", view_id, &ctx_a)
156 .unwrap()
157 .as_action::<Action>(),
158 &Action::B
159 );
160 
161 let mut ctx_c = Context::default();
162 ctx_c.set.insert("c");
163 
164 // Pending keystrokes are maintained per-view
165 let view_id1 = EntityId::new();
166 let view_id2 = EntityId::new();
167 assert_ne!(view_id1, view_id2);
168 assert!(matcher.test_keystroke("a", view_id1, &ctx_b).is_none());
169 assert!(matcher.test_keystroke("a", view_id2, &ctx_c).is_none());
170 assert_eq!(
171 matcher
172 .test_keystroke("b", view_id1, &ctx_b)
173 .unwrap()
174 .as_action::<Action>(),
175 &Action::AOrB
176 );
177}
178 
179#[test]
180fn test_bindings_for_context() {
181 #[derive(Debug)]
182 enum Action {
183 A,
184 B,
185 C,
186 }
187 let keymap = Keymap::new(vec![
188 FixedBinding::new("a", Action::A, id!("a")),
189 FixedBinding::new("b", Action::B, id!("b")),
190 FixedBinding::new("c", Action::C, id!("b")),
191 ]);
192 let matcher = Matcher::new(keymap);
193 
194 let mut ctx_a = Context::default();
195 ctx_a.set.insert("a");
196 
197 let mut ctx_b = Context::default();
198 ctx_b.set.insert("b");
199 
200 // Getting bindings for the 'a' context returns a single result
201 let ctx_a_bindings = matcher
202 .bindings_for_context(ctx_a)
203 .filter_map(|bind| match bind.trigger {
204 Trigger::Keystrokes(keys) => {
205 assert_eq!(keys.len(), 1);
206 Some(keys[0].normalized())
207 }
208 _ => None,
209 })
210 .collect::<Vec<_>>();
211 
212 assert_eq!(ctx_a_bindings.len(), 1);
213 assert_eq!(ctx_a_bindings, vec!["a"]);
214 
215 // Getting bindings for the 'b' context returns two results, in the reverse order they
216 // added, so the "c" binding first followed by the "b" binding
217 let ctx_b_bindings = matcher
218 .bindings_for_context(ctx_b)
219 .filter_map(|bind| match bind.trigger {
220 Trigger::Keystrokes(keys) => {
221 assert_eq!(keys.len(), 1);
222 Some(keys[0].normalized())
223 }
224 _ => None,
225 })
226 .collect::<Vec<_>>();
227 assert_eq!(ctx_b_bindings, vec!["c", "b"]);
228}
229 
230impl Matcher {
231 fn test_keystroke(
232 &mut self,
233 keystroke: &str,
234 view_id: EntityId,
235 ctx: &Context,
236 ) -> Option<Arc<dyn Action>> {
237 match self.push_keystroke(Keystroke::parse(keystroke).unwrap(), view_id, ctx) {
238 MatchResult::Action(action) => Some(action),
239 _ => None,
240 }
241 }
242}
243 
244trait AsAction {
245 fn as_action<A: Action>(&self) -> &A;
246}
247 
248impl AsAction for Arc<dyn Action> {
249 fn as_action<A: Action>(&self) -> &A {
250 self.as_ref().as_any().downcast_ref::<A>().unwrap()
251 }
252}
253