StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use super::{ |
| 2 | BindingLens, Context, CustomTag, EditableBinding, EditableBindingLens, FixedBinding, Keymap, |
| 3 | Keystroke, Trigger, |
| 4 | }; |
| 5 | use crate::{actions::StandardAction, Action, EntityId}; |
| 6 | use itertools::Either; |
| 7 | use std::{collections::HashMap, sync::Arc}; |
| 8 | |
| 9 | #[derive(Default)] |
| 10 | pub 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)] |
| 29 | struct Pending { |
| 30 | keystrokes: Vec<Keystroke>, |
| 31 | context: Option<Context>, |
| 32 | } |
| 33 | |
| 34 | type BindingValidatorFn = Box<dyn Fn(BindingLens) -> IsBindingValid>; |
| 35 | |
| 36 | /// Enum indicating the results of validating a binding. |
| 37 | #[derive(Debug, PartialEq)] |
| 38 | pub enum IsBindingValid { |
| 39 | /// The binding is valid. |
| 40 | Yes, |
| 41 | /// The binding is invalid. |
| 42 | No, |
| 43 | } |
| 44 | |
| 45 | pub enum MatchResult { |
| 46 | None, |
| 47 | Pending, |
| 48 | Action(Arc<dyn Action>), |
| 49 | } |
| 50 | |
| 51 | impl 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"] |
| 378 | mod tests; |
| 379 |