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. 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

View file

@ -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;

View file

@ -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 {

View file

@ -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<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.
#[derive(Debug)]
pub struct Shortcuts {
shortcuts: AHashMap<Shortcut, ShortcutConfig>,
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<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)]
@ -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)
}
}