Shortcut fallback, ShortcutMap

This commit is contained in:
Jonathan Johnson 2024-09-10 07:46:34 -07:00
parent aa7e526965
commit 7e69846909
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
4 changed files with 170 additions and 51 deletions

View file

@ -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` is a new widget that simplifies attaching logic to keyboard
shortcuts. Any widget can be wrapped with keyboard shortcut handling by using shortcuts. Any widget can be wrapped with keyboard shortcut handling by using
`MakeWidget::with_shortcut`/`MakeWidget::with_repeating_shortcut`. `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 [139]: https://github.com/khonsulabs/cushy/issues/139

View file

@ -131,7 +131,7 @@ use figures::units::UPx;
use figures::{Fraction, ScreenUnit, Size, Zero}; use figures::{Fraction, ScreenUnit, Size, Zero};
use kludgine::app::winit::error::EventLoopError; use kludgine::app::winit::error::EventLoopError;
pub use names::Name; pub use names::Name;
pub use utils::{Lazy, ModifiersExt, WithClone}; pub use utils::{Lazy, ModifiersExt, ModifiersStateExt, WithClone};
pub use {figures, kludgine}; pub use {figures, kludgine};
pub use self::graphics::Graphics; pub use self::graphics::Graphics;

View file

@ -70,6 +70,24 @@ where
impl_all_tuples!(impl_with_clone); 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`]. /// Helper functions for [`Modifiers`] and [`ModifiersState`].
pub trait ModifiersExt { pub trait ModifiersExt {
/// Returns true if the current state includes the platform's primary /// 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"))] #[cfg(any(target_os = "macos", target_os = "ios"))]
fn only_primary(&self) -> bool { 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")))] #[cfg(not(any(target_os = "macos", target_os = "ios")))]
fn only_primary(&self) -> bool { 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 { fn only_shift(&self) -> bool {
@ -150,7 +168,7 @@ impl ModifiersExt for ModifiersState {
} }
fn only_control(&self) -> bool { 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 { fn only_alt(&self) -> bool {

View file

@ -9,11 +9,150 @@ use crate::widget::{
EventHandling, MakeWidget, SharedCallback, WidgetRef, WrapperWidget, HANDLED, IGNORED, EventHandling, MakeWidget, SharedCallback, WidgetRef, WrapperWidget, HANDLED, IGNORED,
}; };
use crate::window::KeyEvent; use crate::window::KeyEvent;
use crate::{ModifiersExt, ModifiersStateExt};
/// A collection of keyboard shortcut handlers.
#[derive(Default, Debug, Clone)]
pub struct ShortcutMap(AHashMap<Shortcut, ShortcutConfig>);
impl ShortcutMap {
/// Inserts a handler that invokes `callback` once when `key` is pressed
/// with `modifiers`.
#[must_use]
pub fn with_shortcut<F>(
mut self,
key: impl Into<ShortcutKey>,
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<F>(&mut self, key: impl Into<ShortcutKey>, 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<F>(
mut self,
key: impl Into<ShortcutKey>,
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<F>(
&mut self,
key: impl Into<ShortcutKey>,
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<KeyEvent, EventHandling>,
) {
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<Self::Item> {
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. /// A widget that handles keyboard shortcuts.
#[derive(Debug)] #[derive(Debug)]
pub struct Shortcuts { pub struct Shortcuts {
shortcuts: AHashMap<Shortcut, ShortcutConfig>, shortcuts: ShortcutMap,
child: WidgetRef, child: WidgetRef,
} }
@ -22,7 +161,7 @@ impl Shortcuts {
#[must_use] #[must_use]
pub fn new(child: impl MakeWidget) -> Self { pub fn new(child: impl MakeWidget) -> Self {
Self { Self {
shortcuts: AHashMap::new(), shortcuts: ShortcutMap::default(),
child: WidgetRef::new(child), child: WidgetRef::new(child),
} }
} }
@ -41,7 +180,7 @@ impl Shortcuts {
where where
F: FnMut(KeyEvent) -> EventHandling + Send + 'static, F: FnMut(KeyEvent) -> EventHandling + Send + 'static,
{ {
self.insert_shortcut(key.into(), modifiers, false, SharedCallback::new(callback)); self.shortcuts.insert(key, modifiers, callback);
self self
} }
@ -60,26 +199,9 @@ impl Shortcuts {
where where
F: FnMut(KeyEvent) -> EventHandling + Send + 'static, F: FnMut(KeyEvent) -> EventHandling + Send + 'static,
{ {
self.insert_shortcut(key.into(), modifiers, true, SharedCallback::new(callback)); self.shortcuts.insert_repeating(key, modifiers, callback);
self self
} }
fn insert_shortcut(
&mut self,
key: ShortcutKey,
modifiers: ModifiersState,
repeat: bool,
callback: SharedCallback<KeyEvent, EventHandling>,
) {
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)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
@ -194,31 +316,6 @@ impl WrapperWidget for Shortcuts {
_is_synthetic: bool, _is_synthetic: bool,
_context: &mut crate::context::EventContext<'_>, _context: &mut crate::context::EventContext<'_>,
) -> EventHandling { ) -> EventHandling {
let physical_match = self.shortcuts.get(&Shortcut { self.shortcuts.input(input)
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,
}
} }
} }