mirror of
https://github.com/danbulant/cushy
synced 2026-07-06 03:30:38 +00:00
Window shortcuts
Due to complexities in input routing and how that interacts with layers, it seemed prudent to support installing shortcuts directly on the window. This also enables overriding default shortcuts like Primary+W to close the window.
This commit is contained in:
parent
7fc902a5a2
commit
5f832c304f
3 changed files with 81 additions and 12 deletions
|
|
@ -130,6 +130,9 @@ 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`.
|
||||||
|
- `Window` now can have its own `ShortcutMap`, which can be populated using
|
||||||
|
`Window::with_shortcut`/`Window::with_repeating_shortcut`, or provided using
|
||||||
|
`Window::with_shortcuts`.
|
||||||
- `ModifiersStateExt` is a new trait that adds functionality to winit's
|
- `ModifiersStateExt` is a new trait that adds functionality to winit's
|
||||||
`ModifiersState` type. Specifically, this trait adds an associated `PRIMARY`
|
`ModifiersState` type. Specifically, this trait adds an associated `PRIMARY`
|
||||||
constant that resolves to the primary shortcut modifier on the target
|
constant that resolves to the primary shortcut modifier on the target
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,8 @@ impl ShortcutMap {
|
||||||
/// Invokes any associated handlers for `input`.
|
/// Invokes any associated handlers for `input`.
|
||||||
///
|
///
|
||||||
/// Returns whether the event has been handled or not.
|
/// Returns whether the event has been handled or not.
|
||||||
pub fn input(&mut self, input: KeyEvent) -> EventHandling {
|
#[must_use]
|
||||||
|
pub fn input(&self, input: KeyEvent) -> EventHandling {
|
||||||
for modifiers in FuzzyModifiers(input.modifiers.state()) {
|
for modifiers in FuzzyModifiers(input.modifiers.state()) {
|
||||||
let physical_match = self.0.get(&Shortcut {
|
let physical_match = self.0.get(&Shortcut {
|
||||||
key: ShortcutKey::Physical(input.physical_key),
|
key: ShortcutKey::Physical(input.physical_key),
|
||||||
|
|
@ -102,19 +103,26 @@ impl ShortcutMap {
|
||||||
});
|
});
|
||||||
match (physical_match, logical_match) {
|
match (physical_match, logical_match) {
|
||||||
(Some(physical), Some(logical)) if physical.callback != logical.callback => {
|
(Some(physical), Some(logical)) if physical.callback != logical.callback => {
|
||||||
if input.state.is_pressed() && (!input.repeat || physical.repeat) {
|
// Prefer an exact physical key match.
|
||||||
physical.callback.invoke(input.clone());
|
if input.state.is_pressed()
|
||||||
|
&& (!input.repeat || physical.repeat)
|
||||||
|
&& physical.callback.invoke(input.clone()).is_break()
|
||||||
|
{
|
||||||
|
return HANDLED;
|
||||||
}
|
}
|
||||||
if input.state.is_pressed() && (!input.repeat || logical.repeat) {
|
|
||||||
logical.callback.invoke(input);
|
return if input.state.is_pressed() && (!input.repeat || logical.repeat) {
|
||||||
}
|
logical.callback.invoke(input)
|
||||||
return HANDLED;
|
} else {
|
||||||
|
IGNORED
|
||||||
|
};
|
||||||
}
|
}
|
||||||
(Some(callback), _) | (_, Some(callback)) => {
|
(Some(callback), _) | (_, Some(callback)) => {
|
||||||
if input.state.is_pressed() && (!input.repeat || callback.repeat) {
|
return if input.state.is_pressed() && (!input.repeat || callback.repeat) {
|
||||||
callback.callback.invoke(input);
|
callback.callback.invoke(input)
|
||||||
}
|
} else {
|
||||||
return HANDLED;
|
IGNORED
|
||||||
|
};
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ use kludgine::app::winit::event::{
|
||||||
ElementState, Ime, Modifiers, MouseButton, MouseScrollDelta, TouchPhase,
|
ElementState, Ime, Modifiers, MouseButton, MouseScrollDelta, TouchPhase,
|
||||||
};
|
};
|
||||||
use kludgine::app::winit::keyboard::{
|
use kludgine::app::winit::keyboard::{
|
||||||
Key, KeyLocation, NamedKey, NativeKeyCode, PhysicalKey, SmolStr,
|
Key, KeyLocation, ModifiersState, NamedKey, NativeKeyCode, PhysicalKey, SmolStr,
|
||||||
};
|
};
|
||||||
use kludgine::app::winit::window::{self, Cursor, Fullscreen, Icon, WindowButtons, WindowLevel};
|
use kludgine::app::winit::window::{self, Cursor, Fullscreen, Icon, WindowButtons, WindowLevel};
|
||||||
use kludgine::app::{winit, WindowAttributes, WindowBehavior as _};
|
use kludgine::app::{winit, WindowAttributes, WindowBehavior as _};
|
||||||
|
|
@ -64,6 +64,7 @@ use crate::widget::{
|
||||||
EventHandling, MakeWidget, MountedWidget, OnceCallback, RootBehavior, SharedCallback, WidgetId,
|
EventHandling, MakeWidget, MountedWidget, OnceCallback, RootBehavior, SharedCallback, WidgetId,
|
||||||
WidgetInstance, HANDLED, IGNORED,
|
WidgetInstance, HANDLED, IGNORED,
|
||||||
};
|
};
|
||||||
|
use crate::widgets::shortcuts::{ShortcutKey, ShortcutMap};
|
||||||
use crate::window::sealed::WindowCommand;
|
use crate::window::sealed::WindowCommand;
|
||||||
use crate::{initialize_tracing, App, ConstraintLimit};
|
use crate::{initialize_tracing, App, ConstraintLimit};
|
||||||
|
|
||||||
|
|
@ -549,6 +550,7 @@ where
|
||||||
modifiers: Option<Dynamic<Modifiers>>,
|
modifiers: Option<Dynamic<Modifiers>>,
|
||||||
enabled_buttons: Option<Value<WindowButtons>>,
|
enabled_buttons: Option<Value<WindowButtons>>,
|
||||||
fullscreen: Option<Value<Option<Fullscreen>>>,
|
fullscreen: Option<Value<Option<Fullscreen>>>,
|
||||||
|
shortcuts: Value<ShortcutMap>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Behavior> Default for Window<Behavior>
|
impl<Behavior> Default for Window<Behavior>
|
||||||
|
|
@ -644,6 +646,7 @@ where
|
||||||
modifiers: None,
|
modifiers: None,
|
||||||
enabled_buttons: None,
|
enabled_buttons: None,
|
||||||
fullscreen: None,
|
fullscreen: None,
|
||||||
|
shortcuts: Value::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1016,6 +1019,47 @@ where
|
||||||
self.attributes.app_name = Some(name.into());
|
self.attributes.app_name = Some(name.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Invokes `callback` when `key` is pressed while `modifiers` are pressed.
|
||||||
|
///
|
||||||
|
/// Widgets have a chance to handle keyboard input before the Window.
|
||||||
|
pub fn with_shortcut<F>(
|
||||||
|
mut self,
|
||||||
|
key: impl Into<ShortcutKey>,
|
||||||
|
modifiers: ModifiersState,
|
||||||
|
callback: F,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
F: FnMut(KeyEvent) -> EventHandling + Send + 'static,
|
||||||
|
{
|
||||||
|
self.shortcuts
|
||||||
|
.map_mut(|mut shortcuts| shortcuts.insert(key, modifiers, callback));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invokes `shortcuts` when keyboard input is unhandled in this window.
|
||||||
|
pub fn with_shortcuts(mut self, shortcuts: impl IntoValue<ShortcutMap>) -> Self {
|
||||||
|
self.shortcuts = shortcuts.into_value();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invokes `callback` when `key` is pressed while `modifiers` are pressed.
|
||||||
|
/// If the shortcut is held, the callback will be invoked on repeat events.
|
||||||
|
///
|
||||||
|
/// Widgets have a chance to handle keyboard input before the Window.
|
||||||
|
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.shortcuts
|
||||||
|
.map_mut(|mut shortcuts| shortcuts.insert_repeating(key, modifiers, callback));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Behavior> Run for Window<Behavior>
|
impl<Behavior> Run for Window<Behavior>
|
||||||
|
|
@ -1089,6 +1133,7 @@ where
|
||||||
.enabled_buttons
|
.enabled_buttons
|
||||||
.unwrap_or(Value::Constant(WindowButtons::all())),
|
.unwrap_or(Value::Constant(WindowButtons::all())),
|
||||||
fullscreen: this.fullscreen.unwrap_or_default(),
|
fullscreen: this.fullscreen.unwrap_or_default(),
|
||||||
|
shortcuts: this.shortcuts,
|
||||||
}),
|
}),
|
||||||
pending: this.pending,
|
pending: this.pending,
|
||||||
},
|
},
|
||||||
|
|
@ -1257,6 +1302,7 @@ struct OpenWindow<T> {
|
||||||
enabled_buttons: Tracked<Value<WindowButtons>>,
|
enabled_buttons: Tracked<Value<WindowButtons>>,
|
||||||
fullscreen: Tracked<Value<Option<Fullscreen>>>,
|
fullscreen: Tracked<Value<Option<Fullscreen>>>,
|
||||||
modifiers: Dynamic<Modifiers>,
|
modifiers: Dynamic<Modifiers>,
|
||||||
|
shortcuts: Value<ShortcutMap>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> OpenWindow<T>
|
impl<T> OpenWindow<T>
|
||||||
|
|
@ -1696,6 +1742,7 @@ where
|
||||||
modifiers: settings.modifiers,
|
modifiers: settings.modifiers,
|
||||||
enabled_buttons: Tracked::from(settings.enabled_buttons).ignoring_first(),
|
enabled_buttons: Tracked::from(settings.enabled_buttons).ignoring_first(),
|
||||||
fullscreen: Tracked::from(settings.fullscreen).ignoring_first(),
|
fullscreen: Tracked::from(settings.fullscreen).ignoring_first(),
|
||||||
|
shortcuts: settings.shortcuts,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.synchronize_platform_window(&mut window);
|
this.synchronize_platform_window(&mut window);
|
||||||
|
|
@ -2013,6 +2060,14 @@ where
|
||||||
{
|
{
|
||||||
return HANDLED;
|
return HANDLED;
|
||||||
}
|
}
|
||||||
|
if self
|
||||||
|
.shortcuts
|
||||||
|
.map(|shortcuts| shortcuts.input(input.clone()))
|
||||||
|
.is_break()
|
||||||
|
{
|
||||||
|
return HANDLED;
|
||||||
|
}
|
||||||
|
|
||||||
drop(target);
|
drop(target);
|
||||||
|
|
||||||
self.handle_window_keyboard_input(&mut window, kludgine, input)
|
self.handle_window_keyboard_input(&mut window, kludgine, input)
|
||||||
|
|
@ -2790,6 +2845,7 @@ pub(crate) mod sealed {
|
||||||
use crate::styles::{FontFamilyList, ThemePair};
|
use crate::styles::{FontFamilyList, ThemePair};
|
||||||
use crate::value::{Dynamic, Value};
|
use crate::value::{Dynamic, Value};
|
||||||
use crate::widget::{OnceCallback, SharedCallback};
|
use crate::widget::{OnceCallback, SharedCallback};
|
||||||
|
use crate::widgets::shortcuts::ShortcutMap;
|
||||||
use crate::window::{ThemeMode, WindowAttributes};
|
use crate::window::{ThemeMode, WindowAttributes};
|
||||||
|
|
||||||
pub struct Context<C> {
|
pub struct Context<C> {
|
||||||
|
|
@ -2840,6 +2896,7 @@ pub(crate) mod sealed {
|
||||||
pub modifiers: Dynamic<Modifiers>,
|
pub modifiers: Dynamic<Modifiers>,
|
||||||
pub enabled_buttons: Value<WindowButtons>,
|
pub enabled_buttons: Value<WindowButtons>,
|
||||||
pub fullscreen: Value<Option<Fullscreen>>,
|
pub fullscreen: Value<Option<Fullscreen>>,
|
||||||
|
pub shortcuts: Value<ShortcutMap>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WindowExecute(Box<dyn ExecuteFunc>);
|
pub struct WindowExecute(Box<dyn ExecuteFunc>);
|
||||||
|
|
@ -3503,6 +3560,7 @@ impl StandaloneWindowBuilder {
|
||||||
modifiers: Dynamic::default(),
|
modifiers: Dynamic::default(),
|
||||||
enabled_buttons: Value::dynamic(WindowButtons::all()),
|
enabled_buttons: Value::dynamic(WindowButtons::all()),
|
||||||
fullscreen: Value::default(),
|
fullscreen: Value::default(),
|
||||||
|
shortcuts: Value::default(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue