mirror of
https://github.com/danbulant/cushy
synced 2026-06-19 22:41:10 +00:00
Keyboard shortcut handling
This commit is contained in:
parent
448482e7bf
commit
c2d07344d9
4 changed files with 310 additions and 0 deletions
|
|
@ -123,6 +123,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- `#[cushy::main]` is a new attribute proc-macro that simplifies initializing
|
||||
and running multi-window applications.
|
||||
- `Window::on_open` executes a callback when the window is initially opened.
|
||||
- `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`.
|
||||
|
||||
|
||||
[139]: https://github.com/khonsulabs/cushy/issues/139
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use figures::units::{Px, UPx};
|
|||
use figures::{IntoSigned, IntoUnsigned, Point, Rect, Size, Zero};
|
||||
use intentional::Assert;
|
||||
use kludgine::app::winit::event::{Ime, MouseButton, MouseScrollDelta, TouchPhase};
|
||||
use kludgine::app::winit::keyboard::ModifiersState;
|
||||
use kludgine::app::winit::window::CursorIcon;
|
||||
use kludgine::Color;
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
|
|
@ -40,6 +41,7 @@ use crate::value::{Dynamic, Generation, IntoDynamic, IntoValue, Validation, Valu
|
|||
use crate::widgets::checkbox::{Checkable, CheckboxState};
|
||||
use crate::widgets::layers::{OverlayLayer, Tooltipped};
|
||||
use crate::widgets::list::List;
|
||||
use crate::widgets::shortcuts::{ShortcutKey, Shortcuts};
|
||||
use crate::widgets::{
|
||||
Align, Button, Checkbox, Collapse, Container, Disclose, Expand, Layers, Resize, Scroll, Space,
|
||||
Stack, Style, Themed, ThemedMode, Validated, Wrap,
|
||||
|
|
@ -954,6 +956,43 @@ pub trait MakeWidget: Sized {
|
|||
Style::new(Styles::new().with_dynamic(name, dynamic), self)
|
||||
}
|
||||
|
||||
/// Invokes `callback` when `key` is pressed while `modifiers` are pressed.
|
||||
///
|
||||
/// This shortcut will only be invoked if focus is within `self` or a child
|
||||
/// of `self`, or if the returned widget becomes the root widget of a
|
||||
/// window.
|
||||
#[must_use]
|
||||
fn with_shortcut<F>(
|
||||
self,
|
||||
key: impl Into<ShortcutKey>,
|
||||
modifiers: ModifiersState,
|
||||
callback: F,
|
||||
) -> Shortcuts
|
||||
where
|
||||
F: FnMut(KeyEvent) -> EventHandling + Send + 'static,
|
||||
{
|
||||
Shortcuts::new(self).with_shortcut(key, modifiers, callback)
|
||||
}
|
||||
|
||||
/// Invokes `callback` when `key` is pressed while `modifiers` are pressed.
|
||||
/// If the shortcut is held, the callback will be invoked on repeat events.
|
||||
///
|
||||
/// This shortcut will only be invoked if focus is within `self` or a child
|
||||
/// of `self`, or if the returned widget becomes the root widget of a
|
||||
/// window.
|
||||
#[must_use]
|
||||
fn with_repeating_shortcut<F>(
|
||||
self,
|
||||
key: impl Into<ShortcutKey>,
|
||||
modifiers: ModifiersState,
|
||||
callback: F,
|
||||
) -> Shortcuts
|
||||
where
|
||||
F: FnMut(KeyEvent) -> EventHandling + Send + 'static,
|
||||
{
|
||||
Shortcuts::new(self).with_repeating_shortcut(key, modifiers, callback)
|
||||
}
|
||||
|
||||
/// Styles `self` with the largest of 6 heading styles.
|
||||
fn h1(self) -> Style {
|
||||
self.xxxx_large()
|
||||
|
|
@ -1677,6 +1716,49 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// A [`Callback`] that can be cloned.
|
||||
///
|
||||
/// Only one thread can be invoking a shared callback at any given time.
|
||||
pub struct SharedCallback<T = (), R = ()>(Arc<Mutex<Callback<T, R>>>);
|
||||
|
||||
impl<T, R> SharedCallback<T, R> {
|
||||
/// Returns a new instance that calls `function` each time the callback is
|
||||
/// invoked.
|
||||
pub fn new<F>(function: F) -> Self
|
||||
where
|
||||
F: FnMut(T) -> R + Send + 'static,
|
||||
{
|
||||
Self(Arc::new(Mutex::new(Callback::new(function))))
|
||||
}
|
||||
|
||||
/// Invokes the wrapped function and returns the produced value.
|
||||
pub fn invoke(&self, value: T) -> R {
|
||||
self.0.lock().invoke(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R> Debug for SharedCallback<T, R> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("SharedCallback")
|
||||
.field(&Arc::as_ptr(&self.0))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R> Eq for SharedCallback<T, R> {}
|
||||
|
||||
impl<T, R> PartialEq for SharedCallback<T, R> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.0, &other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R> Clone for SharedCallback<T, R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// A function that can be invoked once with a parameter (`T`) and returns `R`.
|
||||
///
|
||||
/// This type is used by widgets to signal an event that can happen only onceq.
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ pub mod radio;
|
|||
mod resize;
|
||||
pub mod scroll;
|
||||
pub mod select;
|
||||
pub mod shortcuts;
|
||||
pub mod slider;
|
||||
mod space;
|
||||
pub mod stack;
|
||||
|
|
|
|||
224
src/widgets/shortcuts.rs
Normal file
224
src/widgets/shortcuts.rs
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
//! A keyboard shortcut handling widget.
|
||||
|
||||
use ahash::AHashMap;
|
||||
use kludgine::app::winit::keyboard::{
|
||||
Key, KeyCode, ModifiersState, NamedKey, NativeKey, NativeKeyCode, PhysicalKey, SmolStr,
|
||||
};
|
||||
|
||||
use crate::widget::{
|
||||
EventHandling, MakeWidget, SharedCallback, WidgetRef, WrapperWidget, HANDLED, IGNORED,
|
||||
};
|
||||
use crate::window::KeyEvent;
|
||||
|
||||
/// A widget that handles keyboard shortcuts.
|
||||
#[derive(Debug)]
|
||||
pub struct Shortcuts {
|
||||
shortcuts: AHashMap<Shortcut, ShortcutConfig>,
|
||||
child: WidgetRef,
|
||||
}
|
||||
|
||||
impl Shortcuts {
|
||||
/// Wraps `child` with keyboard shortcut handling.
|
||||
#[must_use]
|
||||
pub fn new(child: impl MakeWidget) -> Self {
|
||||
Self {
|
||||
shortcuts: AHashMap::new(),
|
||||
child: WidgetRef::new(child),
|
||||
}
|
||||
}
|
||||
|
||||
/// Invokes `callback` when `key` is pressed while `modifiers` are pressed.
|
||||
///
|
||||
/// This shortcut will only be invoked if focus is within a child of this
|
||||
/// widget, or if this widget becomes the root widget of a window.
|
||||
#[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_shortcut(key.into(), modifiers, false, SharedCallback::new(callback));
|
||||
self
|
||||
}
|
||||
|
||||
/// Invokes `callback` when `key` is pressed while `modifiers` are pressed.
|
||||
/// If the shortcut is held, the callback will be invoked on repeat events.
|
||||
///
|
||||
/// This shortcut will only be invoked if focus is within a child of this
|
||||
/// widget, or if this widget becomes the root widget of a window.
|
||||
#[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_shortcut(key.into(), modifiers, true, SharedCallback::new(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)]
|
||||
struct Shortcut {
|
||||
pub key: ShortcutKey,
|
||||
pub modifiers: ModifiersState,
|
||||
}
|
||||
|
||||
impl Shortcut {
|
||||
fn into_variations(self) -> (Shortcut, Option<Shortcut>) {
|
||||
let modifiers = self.modifiers;
|
||||
let extra = match &self.key {
|
||||
ShortcutKey::Logical(Key::Character(c)) => {
|
||||
let lowercase = SmolStr::new(c.to_lowercase());
|
||||
let uppercase = SmolStr::new(c.to_uppercase());
|
||||
if c == &lowercase {
|
||||
Some(Shortcut {
|
||||
key: uppercase.into(),
|
||||
modifiers,
|
||||
})
|
||||
} else {
|
||||
Some(Shortcut {
|
||||
key: lowercase.into(),
|
||||
modifiers,
|
||||
})
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
(self, extra)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PhysicalKey> for ShortcutKey {
|
||||
fn from(key: PhysicalKey) -> Self {
|
||||
ShortcutKey::Physical(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Key> for ShortcutKey {
|
||||
fn from(key: Key) -> Self {
|
||||
ShortcutKey::Logical(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NamedKey> for ShortcutKey {
|
||||
fn from(key: NamedKey) -> Self {
|
||||
Self::from(Key::from(key))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NativeKey> for ShortcutKey {
|
||||
fn from(key: NativeKey) -> Self {
|
||||
Self::from(Key::from(key))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SmolStr> for ShortcutKey {
|
||||
fn from(key: SmolStr) -> Self {
|
||||
Self::from(Key::Character(key))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'_ str> for ShortcutKey {
|
||||
fn from(key: &'_ str) -> Self {
|
||||
Self::from(SmolStr::new(key))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeyCode> for ShortcutKey {
|
||||
fn from(key: KeyCode) -> Self {
|
||||
Self::from(PhysicalKey::from(key))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NativeKeyCode> for ShortcutKey {
|
||||
fn from(key: NativeKeyCode) -> Self {
|
||||
Self::from(PhysicalKey::from(key))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ShortcutConfig {
|
||||
repeat: bool,
|
||||
callback: SharedCallback<KeyEvent, EventHandling>,
|
||||
}
|
||||
|
||||
/// A key used in a [`Shortcuts`] widget.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum ShortcutKey {
|
||||
/// A logical key.
|
||||
///
|
||||
/// Logical keys are mapped using the operating system configuration.
|
||||
Logical(Key),
|
||||
|
||||
/// A physical key.
|
||||
///
|
||||
/// Physical keys represent a physical keyboard location and may be
|
||||
/// different logical keys depending on operating system configurations.
|
||||
Physical(PhysicalKey),
|
||||
}
|
||||
|
||||
impl WrapperWidget for Shortcuts {
|
||||
fn child_mut(&mut self) -> &mut crate::widget::WidgetRef {
|
||||
&mut self.child
|
||||
}
|
||||
|
||||
fn keyboard_input(
|
||||
&mut self,
|
||||
_device_id: crate::window::DeviceId,
|
||||
input: KeyEvent,
|
||||
_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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue