diff --git a/CHANGELOG.md b/CHANGELOG.md index 143237235..aa110fa2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. Any widget can be wrapped with keyboard shortcut handling by using `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 `ModifiersState` type. Specifically, this trait adds an associated `PRIMARY` constant that resolves to the primary shortcut modifier on the target diff --git a/src/widgets/shortcuts.rs b/src/widgets/shortcuts.rs index aa5c196d0..4d682549e 100644 --- a/src/widgets/shortcuts.rs +++ b/src/widgets/shortcuts.rs @@ -90,7 +90,8 @@ impl ShortcutMap { /// Invokes any associated handlers for `input`. /// /// 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()) { let physical_match = self.0.get(&Shortcut { key: ShortcutKey::Physical(input.physical_key), @@ -102,19 +103,26 @@ impl ShortcutMap { }); 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); + // Prefer an exact physical key match. + if input.state.is_pressed() + && (!input.repeat || physical.repeat) + && physical.callback.invoke(input.clone()).is_break() + { + return HANDLED; } - return HANDLED; + + return if input.state.is_pressed() && (!input.repeat || logical.repeat) { + logical.callback.invoke(input) + } else { + IGNORED + }; } (Some(callback), _) | (_, Some(callback)) => { - if input.state.is_pressed() && (!input.repeat || callback.repeat) { - callback.callback.invoke(input); - } - return HANDLED; + return if input.state.is_pressed() && (!input.repeat || callback.repeat) { + callback.callback.invoke(input) + } else { + IGNORED + }; } _ => {} } diff --git a/src/window.rs b/src/window.rs index 1a3dc996f..edbeba935 100644 --- a/src/window.rs +++ b/src/window.rs @@ -29,7 +29,7 @@ use kludgine::app::winit::event::{ ElementState, Ime, Modifiers, MouseButton, MouseScrollDelta, TouchPhase, }; 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, WindowAttributes, WindowBehavior as _}; @@ -64,6 +64,7 @@ use crate::widget::{ EventHandling, MakeWidget, MountedWidget, OnceCallback, RootBehavior, SharedCallback, WidgetId, WidgetInstance, HANDLED, IGNORED, }; +use crate::widgets::shortcuts::{ShortcutKey, ShortcutMap}; use crate::window::sealed::WindowCommand; use crate::{initialize_tracing, App, ConstraintLimit}; @@ -549,6 +550,7 @@ where modifiers: Option>, enabled_buttons: Option>, fullscreen: Option>>, + shortcuts: Value, } impl Default for Window @@ -644,6 +646,7 @@ where modifiers: None, enabled_buttons: None, fullscreen: None, + shortcuts: Value::default(), } } @@ -1016,6 +1019,47 @@ where self.attributes.app_name = Some(name.into()); 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( + mut self, + key: impl Into, + 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) -> 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( + mut self, + key: impl Into, + 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 Run for Window @@ -1089,6 +1133,7 @@ where .enabled_buttons .unwrap_or(Value::Constant(WindowButtons::all())), fullscreen: this.fullscreen.unwrap_or_default(), + shortcuts: this.shortcuts, }), pending: this.pending, }, @@ -1257,6 +1302,7 @@ struct OpenWindow { enabled_buttons: Tracked>, fullscreen: Tracked>>, modifiers: Dynamic, + shortcuts: Value, } impl OpenWindow @@ -1696,6 +1742,7 @@ where modifiers: settings.modifiers, enabled_buttons: Tracked::from(settings.enabled_buttons).ignoring_first(), fullscreen: Tracked::from(settings.fullscreen).ignoring_first(), + shortcuts: settings.shortcuts, }; this.synchronize_platform_window(&mut window); @@ -2013,6 +2060,14 @@ where { return HANDLED; } + if self + .shortcuts + .map(|shortcuts| shortcuts.input(input.clone())) + .is_break() + { + return HANDLED; + } + drop(target); self.handle_window_keyboard_input(&mut window, kludgine, input) @@ -2790,6 +2845,7 @@ pub(crate) mod sealed { use crate::styles::{FontFamilyList, ThemePair}; use crate::value::{Dynamic, Value}; use crate::widget::{OnceCallback, SharedCallback}; + use crate::widgets::shortcuts::ShortcutMap; use crate::window::{ThemeMode, WindowAttributes}; pub struct Context { @@ -2840,6 +2896,7 @@ pub(crate) mod sealed { pub modifiers: Dynamic, pub enabled_buttons: Value, pub fullscreen: Value>, + pub shortcuts: Value, } pub struct WindowExecute(Box); @@ -3503,6 +3560,7 @@ impl StandaloneWindowBuilder { modifiers: Dynamic::default(), enabled_buttons: Value::dynamic(WindowButtons::all()), fullscreen: Value::default(), + shortcuts: Value::default(), }, );