Skip to main content

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}