mirror of
https://github.com/danbulant/cushy
synced 2026-06-19 06:21:15 +00:00
Shortcut fallback, ShortcutMap
This commit is contained in:
parent
aa7e526965
commit
7e69846909
4 changed files with 170 additions and 51 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
24
src/utils.rs
24
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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue