aetna_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 kind: UiEventKind::Hotkey,
56 })
57 }
58
59 /// Build a raw `KeyDown` event routed to the focused target,
60 /// bypassing the library's Tab/Enter/Escape interpretation. Used
61 /// by the runner when the focused node has `capture_keys=true`.
62 /// Returns `None` if no node is focused.
63 pub fn key_down_raw(
64 &self,
65 key: UiKey,
66 modifiers: KeyModifiers,
67 repeat: bool,
68 ) -> Option<UiEvent> {
69 let target = self.focused.clone()?;
70 Some(UiEvent {
71 key: Some(target.key.clone()),
72 target: Some(target),
73 pointer: None,
74 key_press: Some(KeyPress {
75 key,
76 modifiers,
77 repeat,
78 }),
79 text: None,
80 selection: None,
81 modifiers,
82 click_count: 0,
83 path: None,
84 pointer_kind: None,
85 kind: UiEventKind::KeyDown,
86 })
87 }
88
89 pub fn key_down(
90 &mut self,
91 key: UiKey,
92 modifiers: KeyModifiers,
93 repeat: bool,
94 ) -> Option<UiEvent> {
95 if matches!(key, UiKey::Tab) {
96 if modifiers.shift {
97 self.focus_prev();
98 } else {
99 self.focus_next();
100 }
101 self.set_focus_visible(true);
102 return None;
103 }
104
105 // Hotkeys win over focused-Enter activation: a focused button
106 // with no hotkey on Enter still activates, but Ctrl+Enter (if
107 // registered) routes to its hotkey instead. Registration order
108 // is precedence — first match wins.
109 if let Some(event) = self.try_hotkey(&key, modifiers, repeat) {
110 return Some(event);
111 }
112
113 let target = self.focused.clone();
114 // `:focus-visible` rule: raise the ring only when the key is
115 // unambiguous keyboard interaction with the focused widget —
116 // navigation arrows / Home / End / PageUp / PageDown, or
117 // Enter / Space activation. A Ctrl/Cmd/Alt-held key is a
118 // global shortcut; the focused widget is incidental and
119 // shouldn't flash. Character / function / Escape keys also
120 // don't count — they're typing, dismissal, or app actions,
121 // not "I'm steering this widget with the keyboard." Tab
122 // already raised the ring above when it moved focus.
123 if target.is_some() && raises_focus_visible(&key, modifiers) {
124 self.set_focus_visible(true);
125 }
126 let kind = match (&key, target.is_some()) {
127 (UiKey::Enter | UiKey::Space, true) => UiEventKind::Activate,
128 (UiKey::Escape, _) => UiEventKind::Escape,
129 _ => UiEventKind::KeyDown,
130 };
131 Some(UiEvent {
132 key: target.as_ref().map(|t| t.key.clone()),
133 target,
134 pointer: None,
135 key_press: Some(KeyPress {
136 key,
137 modifiers,
138 repeat,
139 }),
140 text: None,
141 selection: None,
142 modifiers,
143 click_count: 0,
144 path: None,
145 pointer_kind: None,
146 kind,
147 })
148 }
149}
150
151/// Whether `key` (with `modifiers` held) should turn on the focus
152/// ring on a pointer-focused widget. Conservative whitelist — see
153/// [`UiState::key_down`] for the rationale.
154fn raises_focus_visible(key: &UiKey, modifiers: KeyModifiers) -> bool {
155 if modifiers.ctrl || modifiers.alt || modifiers.logo {
156 return false;
157 }
158 matches!(
159 key,
160 UiKey::ArrowUp
161 | UiKey::ArrowDown
162 | UiKey::ArrowLeft
163 | UiKey::ArrowRight
164 | UiKey::Home
165 | UiKey::End
166 | UiKey::PageUp
167 | UiKey::PageDown
168 | UiKey::Enter
169 | UiKey::Space
170 )
171}