From 7e69846909604d38288c02ed995be6a243e29b6d Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Tue, 10 Sep 2024 07:46:34 -0700 Subject: [PATCH] Shortcut fallback, ShortcutMap --- CHANGELOG.md | 4 + src/lib.rs | 2 +- src/utils.rs | 24 ++++- src/widgets/shortcuts.rs | 191 +++++++++++++++++++++++++++++---------- 4 files changed, 170 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bde546..224ff54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,6 +126,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Shortcuts` is a new widget that simplifies attaching logic to keyboard shortcuts. Any widget can be wrapped with keyboard shortcut handling by using `MakeWidget::with_shortcut`/`MakeWidget::with_repeating_shortcut`. +- `ModifiersStateExt` is a new trait that adds functionality to winit's + `ModifiersState` type. Specifically, this trait adds an associated `PRIMARY` + constant that resolves to the primary shortcut modifier on the target + platform. [139]: https://github.com/khonsulabs/cushy/issues/139 diff --git a/src/lib.rs b/src/lib.rs index 46280dc..02e121d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,7 +131,7 @@ use figures::units::UPx; use figures::{Fraction, ScreenUnit, Size, Zero}; use kludgine::app::winit::error::EventLoopError; pub use names::Name; -pub use utils::{Lazy, ModifiersExt, WithClone}; +pub use utils::{Lazy, ModifiersExt, ModifiersStateExt, WithClone}; pub use {figures, kludgine}; pub use self::graphics::Graphics; diff --git a/src/utils.rs b/src/utils.rs index b244c0c..36ce831 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -70,6 +70,24 @@ where impl_all_tuples!(impl_with_clone); +/// Helper constants for [`ModifiersState`] +pub trait ModifiersStateExt { + /// The modifier key used for shortcuts. + /// + /// For Apple based platforms, this is [`ModifierState::SUPER`]. This + /// corresponds to the Apple/Command key. + /// + /// For all other platforms, this is [`ModifiersState::CONTROL`]. + const PRIMARY: Self; +} + +impl ModifiersStateExt for ModifiersState { + #[cfg(any(target_os = "macos", target_os = "ios"))] + const PRIMARY: Self = Self::SUPER; + #[cfg(not(any(target_os = "macos", target_os = "ios")))] + const PRIMARY: Self = Self::CONTROL; +} + /// Helper functions for [`Modifiers`] and [`ModifiersState`]. pub trait ModifiersExt { /// Returns true if the current state includes the platform's primary @@ -137,12 +155,12 @@ impl ModifiersExt for ModifiersState { #[cfg(any(target_os = "macos", target_os = "ios"))] fn only_primary(&self) -> bool { - self.super_key() && !self.shift_key() && !self.control_key() && !self.alt_key() + self.super_key() && !self.control_key() && !self.shift_key() && !self.alt_key() } #[cfg(not(any(target_os = "macos", target_os = "ios")))] fn only_primary(&self) -> bool { - self.control_key() && !self.shift_key() && !self.super_key() && !self.alt_key() + self.control_key() && !self.super_key() && !self.shift_key() && !self.alt_key() } fn only_shift(&self) -> bool { @@ -150,7 +168,7 @@ impl ModifiersExt for ModifiersState { } fn only_control(&self) -> bool { - self.control_key() && !self.shift_key() && !self.super_key() && !self.alt_key() + self.control_key() && !self.super_key() && !self.shift_key() && !self.alt_key() } fn only_alt(&self) -> bool { diff --git a/src/widgets/shortcuts.rs b/src/widgets/shortcuts.rs index b431f8f..aa5c196 100644 --- a/src/widgets/shortcuts.rs +++ b/src/widgets/shortcuts.rs @@ -9,11 +9,150 @@ use crate::widget::{ EventHandling, MakeWidget, SharedCallback, WidgetRef, WrapperWidget, HANDLED, IGNORED, }; use crate::window::KeyEvent; +use crate::{ModifiersExt, ModifiersStateExt}; + +/// A collection of keyboard shortcut handlers. +#[derive(Default, Debug, Clone)] +pub struct ShortcutMap(AHashMap); + +impl ShortcutMap { + /// Inserts a handler that invokes `callback` once when `key` is pressed + /// with `modifiers`. + #[must_use] + pub fn with_shortcut( + mut self, + key: impl Into, + modifiers: ModifiersState, + callback: F, + ) -> Self + where + F: FnMut(KeyEvent) -> EventHandling + Send + 'static, + { + self.insert(key.into(), modifiers, callback); + self + } + + /// Inserts a handler that invokes `callback` once when `key` is pressed + /// with `modifiers`. + pub fn insert(&mut self, key: impl Into, modifiers: ModifiersState, callback: F) + where + F: FnMut(KeyEvent) -> EventHandling + Send + 'static, + { + self.insert_shortcut_inner(key.into(), modifiers, false, SharedCallback::new(callback)); + } + + /// Inserts a handler that invokes `callback` when `key` is pressed with + /// `modifiers`. This callback will be invoked for repeated key events. + #[must_use] + pub fn with_repeating_shortcut( + mut self, + key: impl Into, + modifiers: ModifiersState, + callback: F, + ) -> Self + where + F: FnMut(KeyEvent) -> EventHandling + Send + 'static, + { + self.insert_repeating(key.into(), modifiers, callback); + self + } + + /// Inserts a handler that invokes `callback` when `key` is pressed with + /// `modifiers`. This callback will be invoked for repeated key events. + pub fn insert_repeating( + &mut self, + key: impl Into, + modifiers: ModifiersState, + callback: F, + ) where + F: FnMut(KeyEvent) -> EventHandling + Send + 'static, + { + self.insert_shortcut_inner(key.into(), modifiers, true, SharedCallback::new(callback)); + } + + fn insert_shortcut_inner( + &mut self, + key: ShortcutKey, + modifiers: ModifiersState, + repeat: bool, + callback: SharedCallback, + ) { + let (first, second) = Shortcut { key, modifiers }.into_variations(); + let config = ShortcutConfig { repeat, callback }; + + if let Some(second) = second { + self.0.insert(second, config.clone()); + } + + self.0.insert(first, config); + } + + /// Invokes any associated handlers for `input`. + /// + /// Returns whether the event has been handled or not. + pub fn input(&mut self, input: KeyEvent) -> EventHandling { + for modifiers in FuzzyModifiers(input.modifiers.state()) { + let physical_match = self.0.get(&Shortcut { + key: ShortcutKey::Physical(input.physical_key), + modifiers, + }); + let logical_match = self.0.get(&Shortcut { + key: ShortcutKey::Logical(input.logical_key.clone()), + modifiers, + }); + match (physical_match, logical_match) { + (Some(physical), Some(logical)) if physical.callback != logical.callback => { + if input.state.is_pressed() && (!input.repeat || physical.repeat) { + physical.callback.invoke(input.clone()); + } + if input.state.is_pressed() && (!input.repeat || logical.repeat) { + logical.callback.invoke(input); + } + return HANDLED; + } + (Some(callback), _) | (_, Some(callback)) => { + if input.state.is_pressed() && (!input.repeat || callback.repeat) { + callback.callback.invoke(input); + } + return HANDLED; + } + _ => {} + } + } + + IGNORED + } +} + +/// An iterator that attempts one fallback towards a common shortcut modifier. +/// +/// The precedence for the fallback is: Primary, Control, Super. +struct FuzzyModifiers(ModifiersState); + +impl Iterator for FuzzyModifiers { + type Item = ModifiersState; + + fn next(&mut self) -> Option { + let modifiers = self.0; + if modifiers.is_empty() { + return None; + } else if modifiers.primary() && !modifiers.only_primary() { + self.0 = ModifiersState::PRIMARY; + } else if modifiers.control_key() && !modifiers.only_control() { + self.0 = ModifiersState::CONTROL; + } else if modifiers.super_key() && !modifiers.only_super() { + self.0 = ModifiersState::SUPER; + } else { + self.0 = ModifiersState::empty(); + } + Some(modifiers) + } +} /// A widget that handles keyboard shortcuts. #[derive(Debug)] pub struct Shortcuts { - shortcuts: AHashMap, + shortcuts: ShortcutMap, child: WidgetRef, } @@ -22,7 +161,7 @@ impl Shortcuts { #[must_use] pub fn new(child: impl MakeWidget) -> Self { Self { - shortcuts: AHashMap::new(), + shortcuts: ShortcutMap::default(), child: WidgetRef::new(child), } } @@ -41,7 +180,7 @@ impl Shortcuts { where F: FnMut(KeyEvent) -> EventHandling + Send + 'static, { - self.insert_shortcut(key.into(), modifiers, false, SharedCallback::new(callback)); + self.shortcuts.insert(key, modifiers, callback); self } @@ -60,26 +199,9 @@ impl Shortcuts { where F: FnMut(KeyEvent) -> EventHandling + Send + 'static, { - self.insert_shortcut(key.into(), modifiers, true, SharedCallback::new(callback)); + self.shortcuts.insert_repeating(key, modifiers, callback); self } - - fn insert_shortcut( - &mut self, - key: ShortcutKey, - modifiers: ModifiersState, - repeat: bool, - callback: SharedCallback, - ) { - let (first, second) = Shortcut { key, modifiers }.into_variations(); - let config = ShortcutConfig { repeat, callback }; - - if let Some(second) = second { - self.shortcuts.insert(second, config.clone()); - } - - self.shortcuts.insert(first, config); - } } #[derive(Debug, Clone, Eq, PartialEq, Hash)] @@ -194,31 +316,6 @@ impl WrapperWidget for Shortcuts { _is_synthetic: bool, _context: &mut crate::context::EventContext<'_>, ) -> EventHandling { - let physical_match = self.shortcuts.get(&Shortcut { - key: ShortcutKey::Physical(input.physical_key), - modifiers: input.modifiers.state(), - }); - let logical_match = self.shortcuts.get(&Shortcut { - key: ShortcutKey::Logical(input.logical_key.clone()), - modifiers: input.modifiers.state(), - }); - match (physical_match, logical_match) { - (Some(physical), Some(logical)) if physical.callback != logical.callback => { - if input.state.is_pressed() && (!input.repeat || physical.repeat) { - physical.callback.invoke(input.clone()); - } - if input.state.is_pressed() && (!input.repeat || logical.repeat) { - logical.callback.invoke(input); - } - HANDLED - } - (Some(callback), _) | (_, Some(callback)) => { - if input.state.is_pressed() && (!input.repeat || callback.repeat) { - callback.callback.invoke(input); - } - HANDLED - } - _ => IGNORED, - } + self.shortcuts.input(input) } }