Skip to main content

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}