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.rs
1use super::{
2 BindingLens, Context, CustomTag, EditableBinding, EditableBindingLens, FixedBinding, Keymap,
3 Keystroke, Trigger,
4};
5use crate::{actions::StandardAction, Action, EntityId};
6use itertools::Either;
7use std::{collections::HashMap, sync::Arc};
8 
9#[derive(Default)]
10pub struct Matcher {
11 pending: HashMap<EntityId, Pending>,
12 keymap: Keymap,
13 /// Default binding validator that should run on every binding (irrespective of the [`Context`]
14 /// the binding was registered against).
15 default_binding_validator: Option<BindingValidatorFn>,
16 /// List of validators to be used during binding validation. Each binding validator validates
17 /// all of the bindings that match the [`Context`] it is paired with.
18 binding_validators: Vec<(Context, BindingValidatorFn)>,
19 /// Function to convert bindings that have a [`CustomTag`] trigger to one that has a
20 /// [`Keystroke`]-based trigger instead. If `None`, bindings are not converted.
21 custom_trigger_to_keystroke_fn: Option<Box<dyn Fn(CustomTag) -> Option<Keystroke> + 'static>>,
22 /// Function to lookup the default keystroke for a given custom action. Used when converting
23 /// custom actions to key events during keybinding editing.
24 default_keystroke_trigger_for_custom_action:
25 Option<Box<dyn Fn(CustomTag) -> Option<Keystroke> + 'static>>,
26}
27 
28#[derive(Default)]
29struct Pending {
30 keystrokes: Vec<Keystroke>,
31 context: Option<Context>,
32}
33 
34type BindingValidatorFn = Box<dyn Fn(BindingLens) -> IsBindingValid>;
35 
36/// Enum indicating the results of validating a binding.
37#[derive(Debug, PartialEq)]
38pub enum IsBindingValid {
39 /// The binding is valid.
40 Yes,
41 /// The binding is invalid.
42 No,
43}
44 
45pub enum MatchResult {
46 None,
47 Pending,
48 Action(Arc<dyn Action>),
49}
50 
51impl Matcher {
52 pub fn new(keymap: Keymap) -> Self {
53 Self {
54 pending: HashMap::new(),
55 keymap,
56 default_binding_validator: None,
57 binding_validators: vec![],
58 custom_trigger_to_keystroke_fn: None,
59 default_keystroke_trigger_for_custom_action: None,
60 }
61 }
62 
63 pub fn set_keymap(&mut self, keymap: Keymap) {
64 self.pending.clear();
65 self.keymap = keymap;
66 }
67 
68 /// Helper function to that returns [`Trigger`] with any [`Trigger::Custom`]s replaced by a
69 /// [`Trigger::Keystrokes`].
70 fn convert_custom_trigger_to_keystroke_trigger(
71 trigger: Trigger,
72 custom_tag_to_keystroke: &dyn Fn(CustomTag) -> Option<Keystroke>,
73 ) -> Trigger {
74 let Trigger::Custom(custom_tag) = trigger else {
75 return trigger;
76 };
77 
78 let Some(new_keystroke) = custom_tag_to_keystroke(custom_tag) else {
79 return trigger;
80 };
81 
82 Trigger::Keystrokes(vec![new_keystroke])
83 }
84 
85 pub fn register_fixed_bindings<T: IntoIterator<Item = FixedBinding>>(&mut self, bindings: T) {
86 self.pending.clear();
87 
88 let bindings = match &self.custom_trigger_to_keystroke_fn {
89 None => Either::Left(bindings),
90 Some(custom_tag_to_keystroke) => {
91 let bindings = bindings.into_iter().map(|mut fixed_binding| {
92 fixed_binding.trigger = Self::convert_custom_trigger_to_keystroke_trigger(
93 fixed_binding.trigger,
94 custom_tag_to_keystroke,
95 );
96 fixed_binding
97 });
98 Either::Right(bindings)
99 }
100 };
101 self.keymap.register_fixed_bindings(bindings.into_iter());
102 }
103 
104 /// Register new actions with the key matcher
105 ///
106 /// Editable Bindings have a name identifier which can be used to override their key bindings
107 /// via the `set_custom_trigger` method.
108 pub fn register_editable_bindings<A: IntoIterator<Item = EditableBinding>>(
109 &mut self,
110 actions: A,
111 ) {
112 self.pending.clear();
113 
114 let actions = match &self.custom_trigger_to_keystroke_fn {
115 None => Either::Left(actions),
116 Some(custom_tag_to_keystroke) => {
117 let bindings = actions.into_iter().map(|mut editable_binding| {
118 editable_binding.trigger = Self::convert_custom_trigger_to_keystroke_trigger(
119 editable_binding.trigger,
120 custom_tag_to_keystroke,
121 );
122 editable_binding
123 });
124 Either::Right(bindings)
125 }
126 };
127 self.keymap.register_editable_bindings(actions.into_iter());
128 }
129 
130 /// Set a custom trigger for a given editable binding name.
131 ///
132 /// This will override the default trigger for that action.
133 pub fn set_custom_trigger(&mut self, name: String, trigger: Trigger) {
134 self.pending.clear();
135 self.keymap
136 .update_custom_trigger(name.as_str(), Some(trigger));
137 }
138 
139 /// Remove any custom trigger associated with a given action.
140 ///
141 /// This will return the trigger to its default state.
142 pub fn remove_custom_trigger<N>(&mut self, name: N)
143 where
144 N: AsRef<str>,
145 {
146 self.pending.clear();
147 self.keymap.update_custom_trigger(name.as_ref(), None);
148 }
149 
150 /// Registers a validator that validates every binding that matches the given view's default
151 /// [`Context`].
152 /// After the app is initialized, the provided `binding_validator` function is called for every
153 /// binding that matches the View's default context. If the binding is invalid (indicated by
154 /// [`IsBindingValid::No`]), the app will panic if `debug_assertions` are enabled.
155 #[cfg(debug_assertions)]
156 pub(crate) fn register_binding_validator<F: Fn(BindingLens) -> IsBindingValid + 'static>(
157 &mut self,
158 context: Context,
159 binding_validator: F,
160 ) {
161 self.binding_validators
162 .push((context, Box::new(binding_validator)));
163 }
164 
165 /// Sets a default binding validator that runs on _every_ binding that is registered by the
166 /// application.
167 #[cfg(debug_assertions)]
168 pub(crate) fn set_default_binding_validator<F: Fn(BindingLens) -> IsBindingValid + 'static>(
169 &mut self,
170 binding_validator: F,
171 ) {
172 self.default_binding_validator = Some(Box::new(binding_validator));
173 }
174 
175 /// Runs through each registered binding validator, asserting that each matching binding is
176 /// valid.
177 #[cfg(debug_assertions)]
178 pub(crate) fn validate_bindings(&mut self) {
179 let mut all_failed_bindings = vec![];
180 for (context, validator) in &self.binding_validators {
181 for binding in self.bindings_for_context(context.clone()) {
182 if let IsBindingValid::No = validator(binding) {
183 all_failed_bindings.push(binding);
184 }
185 }
186 }
187 
188 if let Some(default_validator) = &self.default_binding_validator {
189 for binding in self.get_bindings() {
190 if let IsBindingValid::No = default_validator(binding) {
191 all_failed_bindings.push(binding);
192 }
193 }
194 }
195 
196 if !all_failed_bindings.is_empty() {
197 panic!("Bindings failed validation {all_failed_bindings:#?}");
198 }
199 }
200 
201 /// Overrides any registered binding that has a [`Trigger::Custom`] to one that is keystroke
202 /// based ([`Trigger::Keystrokes`]) using the provided `custom_to_keystroke` fn.
203 pub(crate) fn convert_custom_triggers_to_keystroke_triggers(
204 &mut self,
205 custom_to_keystroke: impl Fn(CustomTag) -> Option<Keystroke> + 'static,
206 ) {
207 self.custom_trigger_to_keystroke_fn = Some(Box::new(custom_to_keystroke));
208 }
209 
210 /// Registers a lookup function that returns the default keystroke for a given custom action.
211 /// Used when converting custom actions to key events during keybinding editing.
212 pub(crate) fn register_default_keystroke_triggers_for_custom_actions(
213 &mut self,
214 custom_to_keystroke: impl Fn(CustomTag) -> Option<Keystroke> + 'static,
215 ) {
216 self.default_keystroke_trigger_for_custom_action = Some(Box::new(custom_to_keystroke));
217 }
218 
219 pub(crate) fn custom_action_bindings(&self) -> impl Iterator<Item = BindingLens<'_>> {
220 self.keymap.custom_action_bindings()
221 }
222 
223 /// Returns the first matching binding for the given custom action (not taking)
224 /// into account the current context
225 pub fn default_binding_for_custom_action(
226 &self,
227 custom_tag: CustomTag,
228 ) -> Option<BindingLens<'_>> {
229 self.keymap
230 .bindings()
231 // Filter out just the matching custom binding or action.
232 // We look for matches against either the current or original trigger.
233 .find(|binding| {
234 matches!(
235 (binding.trigger, binding.original_trigger),
236 (Trigger::Custom(tag), _) | (_, Some(Trigger::Custom(tag))) if *tag == custom_tag
237 )
238 })
239 }
240 
241 /// Returns any matching binding for the given custom tag and context
242 pub fn binding_for_custom_action_in_context(
243 &self,
244 custom_tag: CustomTag,
245 context: &Context,
246 ) -> Option<BindingLens<'_>> {
247 self.keymap
248 .custom_action_bindings()
249 // First filter out just the matching custom binding or action
250 // We look for matches against either the current or original trigger.
251 .filter(|binding| {
252 matches!(
253 (binding.trigger, binding.original_trigger),
254 (Trigger::Custom(tag), _) | (_, Some(Trigger::Custom(tag))) if *tag == custom_tag
255 )
256 })
257 // And then filter against the current context and return the first match
258 .find(move |binding| binding.context_predicate.eval(context))
259 }
260 
261 pub fn default_keystroke_trigger_for_custom_action(
262 &self,
263 custom_tag: CustomTag,
264 ) -> Option<Keystroke> {
265 self.default_keystroke_trigger_for_custom_action
266 .as_ref()
267 .and_then(|f| f(custom_tag))
268 }
269 
270 pub fn get_binding_by_name(&self, name: &str) -> Option<BindingLens<'_>> {
271 self.keymap.get_binding_by_name(name)
272 }
273 
274 /// Returns an iterator of lenses to key bindings that apply to the given context.
275 ///
276 /// Key bindings are returned in precedence order, so the highest precedence key binding is
277 /// returned first.
278 pub fn bindings_for_context(&self, context: Context) -> impl Iterator<Item = BindingLens<'_>> {
279 self.keymap
280 .bindings()
281 .filter(move |binding| binding.context_predicate.eval(&context))
282 }
283 
284 /// Fetch an iterator of editable bindings
285 ///
286 /// The triggers for those actions will be overwritten by any custom triggers
287 ///
288 /// Items will be returned in the reverse order they were registered, the most recently
289 /// registered editable binding will have the highest precedence
290 pub fn editable_bindings(&self) -> impl Iterator<Item = EditableBindingLens<'_>> {
291 self.keymap.editable_bindings()
292 }
293 
294 /// Fetch an iterator of `BindingLens` objects, with the editable key bindings
295 /// modified by the custom bindings, where appropriate.
296 ///
297 /// Editable bindings will be returned first, followed by any fixed bindings in the reverse
298 /// order they were added.
299 pub fn get_bindings(&self) -> impl Iterator<Item = BindingLens<'_>> {
300 self.keymap.bindings()
301 }
302 
303 pub fn push_keystroke(
304 &mut self,
305 keystroke: Keystroke,
306 view_id: EntityId,
307 ctx: &Context,
308 ) -> MatchResult {
309 let pending = self.pending.entry(view_id).or_default();
310 
311 if let Some(pending_ctx) = pending.context.as_ref() {
312 if pending_ctx != ctx {
313 pending.keystrokes.clear();
314 }
315 }
316 
317 pending.keystrokes.push(keystroke);
318 
319 let mut retain_pending = false;
320 for binding in self.keymap.bindings() {
321 if let Trigger::Keystrokes(keystrokes) = &binding.trigger {
322 if keystrokes.starts_with(&pending.keystrokes)
323 && binding.context_predicate.eval(ctx)
324 {
325 if keystrokes.len() == pending.keystrokes.len() {
326 self.pending.remove(&view_id);
327 return MatchResult::Action(binding.action.clone());
328 } else {
329 retain_pending = true;
330 pending.context = Some(ctx.clone());
331 }
332 }
333 }
334 }
335 
336 if retain_pending {
337 MatchResult::Pending
338 } else {
339 self.pending.remove(&view_id);
340 MatchResult::None
341 }
342 }
343 
344 // Attempt to match with a StandardAction.
345 // This returns None or Action, never Pending.
346 pub fn match_standard(&self, action: StandardAction, ctx: &Context) -> MatchResult {
347 for binding in self.keymap.bindings() {
348 if let Trigger::Standard(triggeract) = binding.trigger {
349 if *triggeract == action && binding.context_predicate.eval(ctx) {
350 return MatchResult::Action(binding.action.clone());
351 }
352 }
353 }
354 MatchResult::None
355 }
356 
357 // Attempt to match with a CustomAction.
358 // This returns None or Action, never Pending.
359 pub fn match_custom(&self, action: CustomTag, ctx: &Context) -> MatchResult {
360 for binding in self.keymap.bindings() {
361 if let Trigger::Custom(tag) = binding.trigger {
362 if *tag == action && binding.context_predicate.eval(ctx) {
363 return MatchResult::Action(binding.action.clone());
364 }
365 }
366 if let Some(Trigger::Custom(tag)) = binding.original_trigger {
367 if *tag == action && binding.context_predicate.eval(ctx) {
368 return MatchResult::Action(binding.action.clone());
369 }
370 }
371 }
372 MatchResult::None
373 }
374}
375 
376#[cfg(test)]
377#[path = "matcher_test.rs"]
378mod tests;
379