damascene_core/state/keyboard.rs
1//! Keyboard modifiers, hotkeys, and focused-key dispatch.
2
3use crate::event::{KeyChord, KeyModifiers, KeyPress, UiEvent, UiEventKind, UiKey};
4
5use super::UiState;
6
7impl UiState {
8 /// Replace the hotkey registry. Called by the host runner from
9 /// `App::hotkeys()` once per build cycle.
10 pub fn set_hotkeys(&mut self, hotkeys: Vec<(KeyChord, String)>) {
11 self.hotkeys.registry = hotkeys;
12 }
13
14 /// Update the tracked modifier mask. Hosts call this from their
15 /// platform's "modifiers changed" hook (e.g. winit's
16 /// `WindowEvent::ModifiersChanged`); the value is stamped into
17 /// `UiEvent.modifiers` for every subsequent pointer event so
18 /// widgets can detect Shift+click / Ctrl+drag without needing a
19 /// per-call modifier parameter.
20 pub fn set_modifiers(&mut self, modifiers: KeyModifiers) {
21 self.modifiers = modifiers;
22 }
23
24 /// Match `key + modifiers` against the registered hotkey chords.
25 /// Returns a `Hotkey` event if any registered chord matches; the
26 /// `event.key` is the chord's registered name. Used by both the
27 /// library-default path and the capture-keys path (hotkeys always
28 /// win over a widget's raw key capture).
29 pub fn try_hotkey(
30 &self,
31 key: &UiKey,
32 modifiers: KeyModifiers,
33 repeat: bool,
34 ) -> Option<UiEvent> {
35 let (_, name) = self
36 .hotkeys
37 .registry
38 .iter()
39 .find(|(chord, _)| chord.matches(key, modifiers))?;
40 Some(UiEvent {
41 key: Some(name.clone()),
42 target: None,
43 pointer: None,
44 key_press: Some(KeyPress {
45 key: key.clone(),
46 modifiers,
47 repeat,
48 }),
49 text: None,
50 selection: None,
51 modifiers,
52 click_count: 0,
53 path: None,
54 pointer_kind: None,
55 wheel_delta: None,
56 kind: UiEventKind::Hotkey,
57 })
58 }
59
60 /// Build a raw `KeyDown` event routed to the focused target,
61 /// bypassing the library's Tab/Enter/Escape interpretation. Used
62 /// by the runner when the focused node has `capture_keys=true`.
63 /// Returns `None` if no node is focused.
64 pub fn key_down_raw(
65 &self,
66 key: UiKey,
67 modifiers: KeyModifiers,
68 repeat: bool,
69 ) -> Option<UiEvent> {
70 let target = self.focused.clone()?;
71 Some(UiEvent {
72 key: Some(target.key.clone()),
73 target: Some(target),
74 pointer: None,
75 key_press: Some(KeyPress {
76 key,
77 modifiers,
78 repeat,
79 }),
80 text: None,
81 selection: None,
82 modifiers,
83 click_count: 0,
84 path: None,
85 pointer_kind: None,
86 wheel_delta: None,
87 kind: UiEventKind::KeyDown,
88 })
89 }
90
91 pub fn key_down(
92 &mut self,
93 key: UiKey,
94 modifiers: KeyModifiers,
95 repeat: bool,
96 ) -> Option<UiEvent> {
97 if matches!(key, UiKey::Tab) {
98 if modifiers.shift {
99 self.focus_prev();
100 } else {
101 self.focus_next();
102 }
103 self.set_focus_visible(true);
104 return None;
105 }
106
107 // Hotkeys win over focused-Enter activation: a focused button
108 // with no hotkey on Enter still activates, but Ctrl+Enter (if
109 // registered) routes to its hotkey instead. Registration order
110 // is precedence — first match wins.
111 if let Some(event) = self.try_hotkey(&key, modifiers, repeat) {
112 return Some(event);
113 }
114
115 let target = self.focused.clone();
116 // `:focus-visible` rule: raise the ring only when the key is
117 // unambiguous keyboard interaction with the focused widget —
118 // navigation arrows / Home / End / PageUp / PageDown, or
119 // Enter / Space activation. A Ctrl/Cmd/Alt-held key is a
120 // global shortcut; the focused widget is incidental and
121 // shouldn't flash. Character / function / Escape keys also
122 // don't count — they're typing, dismissal, or app actions,
123 // not "I'm steering this widget with the keyboard." Tab
124 // already raised the ring above when it moved focus.
125 if target.is_some() && raises_focus_visible(&key, modifiers) {
126 self.set_focus_visible(true);
127 }
128 let kind = match (&key, target.is_some()) {
129 (UiKey::Enter | UiKey::Space, true) => UiEventKind::Activate,
130 (UiKey::Escape, _) => UiEventKind::Escape,
131 _ => UiEventKind::KeyDown,
132 };
133 Some(UiEvent {
134 key: target.as_ref().map(|t| t.key.clone()),
135 target,
136 pointer: None,
137 key_press: Some(KeyPress {
138 key,
139 modifiers,
140 repeat,
141 }),
142 text: None,
143 selection: None,
144 modifiers,
145 click_count: 0,
146 path: None,
147 pointer_kind: None,
148 wheel_delta: None,
149 kind,
150 })
151 }
152}
153
154/// Whether `key` (with `modifiers` held) should turn on the focus
155/// ring on a pointer-focused widget. Conservative whitelist — see
156/// [`UiState::key_down`] for the rationale.
157fn raises_focus_visible(key: &UiKey, modifiers: KeyModifiers) -> bool {
158 if modifiers.ctrl || modifiers.alt || modifiers.logo {
159 return false;
160 }
161 matches!(
162 key,
163 UiKey::ArrowUp
164 | UiKey::ArrowDown
165 | UiKey::ArrowLeft
166 | UiKey::ArrowRight
167 | UiKey::Home
168 | UiKey::End
169 | UiKey::PageUp
170 | UiKey::PageDown
171 | UiKey::Enter
172 | UiKey::Space
173 )
174}