mirror of
https://github.com/danbulant/cushy
synced 2026-07-06 11:40:37 +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
|
- `#[cushy::main]` is a new attribute proc-macro that simplifies initializing
|
||||||
and running multi-window applications.
|
and running multi-window applications.
|
||||||
- `Window::on_open` executes a callback when the window is initially opened.
|
- `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
|
[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 figures::{IntoSigned, IntoUnsigned, Point, Rect, Size, Zero};
|
||||||
use intentional::Assert;
|
use intentional::Assert;
|
||||||
use kludgine::app::winit::event::{Ime, MouseButton, MouseScrollDelta, TouchPhase};
|
use kludgine::app::winit::event::{Ime, MouseButton, MouseScrollDelta, TouchPhase};
|
||||||
|
use kludgine::app::winit::keyboard::ModifiersState;
|
||||||
use kludgine::app::winit::window::CursorIcon;
|
use kludgine::app::winit::window::CursorIcon;
|
||||||
use kludgine::Color;
|
use kludgine::Color;
|
||||||
use parking_lot::{Mutex, MutexGuard};
|
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::checkbox::{Checkable, CheckboxState};
|
||||||
use crate::widgets::layers::{OverlayLayer, Tooltipped};
|
use crate::widgets::layers::{OverlayLayer, Tooltipped};
|
||||||
use crate::widgets::list::List;
|
use crate::widgets::list::List;
|
||||||
|
use crate::widgets::shortcuts::{ShortcutKey, Shortcuts};
|
||||||
use crate::widgets::{
|
use crate::widgets::{
|
||||||
Align, Button, Checkbox, Collapse, Container, Disclose, Expand, Layers, Resize, Scroll, Space,
|
Align, Button, Checkbox, Collapse, Container, Disclose, Expand, Layers, Resize, Scroll, Space,
|
||||||
Stack, Style, Themed, ThemedMode, Validated, Wrap,
|
Stack, Style, Themed, ThemedMode, Validated, Wrap,
|
||||||
|
|
@ -954,6 +956,43 @@ pub trait MakeWidget: Sized {
|
||||||
Style::new(Styles::new().with_dynamic(name, dynamic), self)
|
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.
|
/// Styles `self` with the largest of 6 heading styles.
|
||||||
fn h1(self) -> Style {
|
fn h1(self) -> Style {
|
||||||
self.xxxx_large()
|
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`.
|
/// 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.
|
/// This type is used by widgets to signal an event that can happen only onceq.
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ pub mod radio;
|
||||||
mod resize;
|
mod resize;
|
||||||
pub mod scroll;
|
pub mod scroll;
|
||||||
pub mod select;
|
pub mod select;
|
||||||
|
pub mod shortcuts;
|
||||||
pub mod slider;
|
pub mod slider;
|
||||||
mod space;
|
mod space;
|
||||||
pub mod stack;
|
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