Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 7.2 kB View raw
1use core::fmt; 2use core::num::NonZeroU32; 3use std::collections::BTreeMap; 4 5use serde::{Deserialize, Serialize}; 6 7use crate::input::{KeyCode, KeyEvent, ModifierMask}; 8 9#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 10pub struct KeyChord { 11 pub key: KeyCode, 12 pub modifiers: ModifierMask, 13} 14 15impl KeyChord { 16 #[must_use] 17 pub const fn new(key: KeyCode, modifiers: ModifierMask) -> Self { 18 Self { key, modifiers } 19 } 20} 21 22impl From<KeyEvent> for KeyChord { 23 fn from(event: KeyEvent) -> Self { 24 Self { 25 key: event.code, 26 modifiers: event.modifiers, 27 } 28 } 29} 30 31impl fmt::Display for KeyChord { 32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 33 fmt::Display::fmt(&self.modifiers, f)?; 34 fmt::Display::fmt(&self.key, f) 35 } 36} 37 38#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 39pub enum HotkeyScope { 40 Global, 41 Viewport, 42 FeatureTree, 43 Sketch, 44 Extrude, 45 Modal, 46 TextInput, 47} 48 49#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 50#[serde(transparent)] 51pub struct ActionId(NonZeroU32); 52 53impl ActionId { 54 #[must_use] 55 pub const fn new(id: NonZeroU32) -> Self { 56 Self(id) 57 } 58 59 #[must_use] 60 pub const fn get(self) -> NonZeroU32 { 61 self.0 62 } 63} 64 65#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 66pub struct HotkeyBinding { 67 pub chord: KeyChord, 68 pub scope: HotkeyScope, 69 pub action: ActionId, 70} 71 72impl HotkeyBinding { 73 #[must_use] 74 pub const fn new(chord: KeyChord, scope: HotkeyScope, action: ActionId) -> Self { 75 Self { 76 chord, 77 scope, 78 action, 79 } 80 } 81} 82 83#[derive(Debug, thiserror::Error, PartialEq, Eq)] 84pub enum HotkeyTableError { 85 #[error("hotkey conflict: chord already bound in this scope")] 86 Conflict { 87 chord: KeyChord, 88 scope: HotkeyScope, 89 existing: ActionId, 90 attempted: ActionId, 91 }, 92} 93 94#[derive(Clone, Debug, Default, PartialEq)] 95pub struct HotkeyTable { 96 bindings: BTreeMap<(HotkeyScope, KeyChord), ActionId>, 97} 98 99impl HotkeyTable { 100 #[must_use] 101 pub fn new() -> Self { 102 Self::default() 103 } 104 105 pub fn try_from_bindings(bindings: Vec<HotkeyBinding>) -> Result<Self, HotkeyTableError> { 106 bindings 107 .into_iter() 108 .try_fold(Self::new(), |mut table, binding| { 109 table.try_register(binding)?; 110 Ok(table) 111 }) 112 } 113 114 pub fn try_register(&mut self, binding: HotkeyBinding) -> Result<(), HotkeyTableError> { 115 let key = (binding.scope, binding.chord); 116 if let Some(&existing) = self.bindings.get(&key) { 117 return Err(HotkeyTableError::Conflict { 118 chord: binding.chord, 119 scope: binding.scope, 120 existing, 121 attempted: binding.action, 122 }); 123 } 124 self.bindings.insert(key, binding.action); 125 Ok(()) 126 } 127 128 #[must_use] 129 pub fn dispatch(&self, chord: KeyChord, scopes: &HotkeyScopes) -> Option<ActionId> { 130 scopes 131 .innermost_first() 132 .find_map(|scope| self.bindings.get(&(*scope, chord)).copied()) 133 } 134} 135 136#[derive(Clone, Debug, Default, PartialEq, Eq)] 137pub struct HotkeyScopes { 138 stack: Vec<HotkeyScope>, 139} 140 141impl HotkeyScopes { 142 #[must_use] 143 pub fn from_outer_to_inner<I: IntoIterator<Item = HotkeyScope>>(scopes: I) -> Self { 144 Self { 145 stack: scopes.into_iter().collect(), 146 } 147 } 148 149 pub fn push(&mut self, scope: HotkeyScope) { 150 self.stack.push(scope); 151 } 152 153 pub fn pop(&mut self) -> Option<HotkeyScope> { 154 self.stack.pop() 155 } 156 157 #[must_use] 158 pub fn innermost_first(&self) -> impl DoubleEndedIterator<Item = &HotkeyScope> { 159 self.stack.iter().rev() 160 } 161} 162 163#[cfg(test)] 164mod tests { 165 use core::num::NonZeroU32; 166 167 use super::{ 168 ActionId, HotkeyBinding, HotkeyScope, HotkeyScopes, HotkeyTable, HotkeyTableError, KeyChord, 169 }; 170 use crate::input::{KeyChar, KeyCode, ModifierMask, NamedKey}; 171 172 fn scopes(s: &[HotkeyScope]) -> HotkeyScopes { 173 HotkeyScopes::from_outer_to_inner(s.iter().copied()) 174 } 175 176 fn action(n: u32) -> ActionId { 177 let Some(nz) = NonZeroU32::new(n) else { 178 panic!("test action id must be non-zero"); 179 }; 180 ActionId::new(nz) 181 } 182 183 fn ctrl_s() -> KeyChord { 184 KeyChord::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL) 185 } 186 187 fn esc() -> KeyChord { 188 KeyChord::new(KeyCode::Named(NamedKey::Escape), ModifierMask::NONE) 189 } 190 191 #[test] 192 fn duplicate_chord_in_same_scope_conflicts() { 193 let result = HotkeyTable::try_from_bindings(vec![ 194 HotkeyBinding::new(ctrl_s(), HotkeyScope::Global, action(1)), 195 HotkeyBinding::new(ctrl_s(), HotkeyScope::Global, action(2)), 196 ]); 197 match result { 198 Err(HotkeyTableError::Conflict { 199 existing, 200 attempted, 201 .. 202 }) => { 203 assert_eq!(existing, action(1)); 204 assert_eq!(attempted, action(2)); 205 } 206 _ => panic!("expected conflict"), 207 } 208 } 209 210 #[test] 211 fn same_chord_different_scopes_does_not_conflict() { 212 let table = HotkeyTable::try_from_bindings(vec![ 213 HotkeyBinding::new(esc(), HotkeyScope::Global, action(1)), 214 HotkeyBinding::new(esc(), HotkeyScope::Modal, action(2)), 215 ]); 216 assert!(table.is_ok()); 217 } 218 219 #[test] 220 fn dispatch_innermost_scope_first() { 221 let Ok(table) = HotkeyTable::try_from_bindings(vec![ 222 HotkeyBinding::new(esc(), HotkeyScope::Global, action(1)), 223 HotkeyBinding::new(esc(), HotkeyScope::Modal, action(99)), 224 ]) else { 225 panic!("registration must succeed"); 226 }; 227 let with_modal = scopes(&[HotkeyScope::Global, HotkeyScope::Modal]); 228 let no_modal = scopes(&[HotkeyScope::Global]); 229 assert_eq!(table.dispatch(esc(), &with_modal), Some(action(99))); 230 assert_eq!(table.dispatch(esc(), &no_modal), Some(action(1))); 231 } 232 233 #[test] 234 fn dispatch_misses_unmatched_chord() { 235 let Ok(table) = HotkeyTable::try_from_bindings(vec![HotkeyBinding::new( 236 ctrl_s(), 237 HotkeyScope::Global, 238 action(1), 239 )]) else { 240 panic!("registration must succeed"); 241 }; 242 assert_eq!(table.dispatch(esc(), &scopes(&[HotkeyScope::Global])), None); 243 } 244 245 #[test] 246 fn dispatch_ignores_inactive_scope() { 247 let Ok(table) = HotkeyTable::try_from_bindings(vec![HotkeyBinding::new( 248 esc(), 249 HotkeyScope::Modal, 250 action(7), 251 )]) else { 252 panic!("registration must succeed"); 253 }; 254 assert_eq!(table.dispatch(esc(), &scopes(&[HotkeyScope::Global])), None); 255 assert_eq!( 256 table.dispatch(esc(), &scopes(&[HotkeyScope::Modal])), 257 Some(action(7)) 258 ); 259 } 260}