Skip to main content

frust_core/event/
key.rs

1//! Keyboard, clipboard-command and editing-state vocabulary for focus-routed input.
2
3use std::fmt;
4
5/// A non-character key the focus path and the shortcut registry route; typed text arrives as
6/// [`Key::Character`]. `Copy`, `Cut` and `Paste` are the hardware keys a shell maps straight onto
7/// [`EditCommand`]; `Insert` exists for the `Shift+Insert`/`Ctrl+Insert` clipboard chords;
8/// `F1`..`F24` are the function keys, which no widget consumes by default.
9///
10/// Variants may be added in a minor release, so a `match` outside `frust-core` needs a wildcard arm.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum NamedKey {
14    Enter,
15    Backspace,
16    Delete,
17    ArrowLeft,
18    ArrowRight,
19    ArrowUp,
20    ArrowDown,
21    Home,
22    End,
23    Escape,
24    Tab,
25    Copy,
26    Cut,
27    Paste,
28    Insert,
29    F1,
30    F2,
31    F3,
32    F4,
33    F5,
34    F6,
35    F7,
36    F8,
37    F9,
38    F10,
39    F11,
40    F12,
41    F13,
42    F14,
43    F15,
44    F16,
45    F17,
46    F18,
47    F19,
48    F20,
49    F21,
50    F22,
51    F23,
52    F24,
53}
54
55impl NamedKey {
56    /// The function-key number (`1..=24`) of `F1`..`F24`, `None` for every other key.
57    pub fn function_number(self) -> Option<u8> {
58        match self {
59            NamedKey::F1 => Some(1),
60            NamedKey::F2 => Some(2),
61            NamedKey::F3 => Some(3),
62            NamedKey::F4 => Some(4),
63            NamedKey::F5 => Some(5),
64            NamedKey::F6 => Some(6),
65            NamedKey::F7 => Some(7),
66            NamedKey::F8 => Some(8),
67            NamedKey::F9 => Some(9),
68            NamedKey::F10 => Some(10),
69            NamedKey::F11 => Some(11),
70            NamedKey::F12 => Some(12),
71            NamedKey::F13 => Some(13),
72            NamedKey::F14 => Some(14),
73            NamedKey::F15 => Some(15),
74            NamedKey::F16 => Some(16),
75            NamedKey::F17 => Some(17),
76            NamedKey::F18 => Some(18),
77            NamedKey::F19 => Some(19),
78            NamedKey::F20 => Some(20),
79            NamedKey::F21 => Some(21),
80            NamedKey::F22 => Some(22),
81            NamedKey::F23 => Some(23),
82            NamedKey::F24 => Some(24),
83            _ => None,
84        }
85    }
86
87    /// The `F<n>` key for `n` in `1..=24`.
88    pub fn function(n: u8) -> Option<NamedKey> {
89        match n {
90            1 => Some(NamedKey::F1),
91            2 => Some(NamedKey::F2),
92            3 => Some(NamedKey::F3),
93            4 => Some(NamedKey::F4),
94            5 => Some(NamedKey::F5),
95            6 => Some(NamedKey::F6),
96            7 => Some(NamedKey::F7),
97            8 => Some(NamedKey::F8),
98            9 => Some(NamedKey::F9),
99            10 => Some(NamedKey::F10),
100            11 => Some(NamedKey::F11),
101            12 => Some(NamedKey::F12),
102            13 => Some(NamedKey::F13),
103            14 => Some(NamedKey::F14),
104            15 => Some(NamedKey::F15),
105            16 => Some(NamedKey::F16),
106            17 => Some(NamedKey::F17),
107            18 => Some(NamedKey::F18),
108            19 => Some(NamedKey::F19),
109            20 => Some(NamedKey::F20),
110            21 => Some(NamedKey::F21),
111            22 => Some(NamedKey::F22),
112            23 => Some(NamedKey::F23),
113            24 => Some(NamedKey::F24),
114            _ => None,
115        }
116    }
117}
118
119/// A logical key: a [`NamedKey`], or the resolved text a key produced (dead keys and IME already
120/// applied), which a widget inserts verbatim.
121#[derive(Clone, Debug, PartialEq, Eq)]
122pub enum Key {
123    Named(NamedKey),
124    Character(String),
125}
126
127/// The modifier chord held when a [`KeyEvent`] fired; `meta` is Command on macOS, Super elsewhere.
128#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
129pub struct Modifiers {
130    pub shift: bool,
131    pub ctrl: bool,
132    pub alt: bool,
133    pub meta: bool,
134}
135
136/// A keyboard key event, delivered down the focus path (never hit-tested).
137#[derive(Clone, Debug, PartialEq, Eq)]
138pub struct KeyEvent {
139    pub key: Key,
140    pub modifiers: Modifiers,
141    /// Whether this is an auto-repeat (key held down), not a fresh press.
142    pub repeat: bool,
143}
144
145impl KeyEvent {
146    /// Whether this is a bare, fresh Space or Enter: what activates a focused button-like control.
147    pub fn is_activation(&self) -> bool {
148        let pressed = match &self.key {
149            Key::Named(named) => *named == NamedKey::Enter,
150            Key::Character(text) => text == " ",
151        };
152        pressed && !self.repeat && self.modifiers == Modifiers::default()
153    }
154}
155
156/// A decoded clipboard/selection verb for the focused editable, so no widget decodes OS chords.
157/// `Copy`/`Cut` carry nothing (the widget answers with `EventCtx::write_clipboard`); `Paste`
158/// carries the text the shell already read. A widget may refuse any verb. Exhaustive on purpose:
159/// a fifth verb must break every `match`.
160#[derive(Clone, PartialEq, Eq)]
161pub enum EditCommand {
162    /// Copy the selection to the host clipboard, leaving the document unchanged.
163    Copy,
164    /// Copy the selection, then delete it.
165    Cut,
166    /// Replace the selection (or insert at the caret) with this text.
167    Paste(String),
168    /// Select the widget's entire content.
169    SelectAll,
170}
171
172impl fmt::Debug for EditCommand {
173    /// Hand-written so pasted text never reaches a log: the payload prints `<redacted>`, no length.
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        match self {
176            EditCommand::Copy => f.write_str("Copy"),
177            EditCommand::Cut => f.write_str("Cut"),
178            EditCommand::Paste(_) => f.debug_tuple("Paste").field(&"<redacted>").finish(),
179            EditCommand::SelectAll => f.write_str("SelectAll"),
180        }
181    }
182}
183
184/// The full editing state of a text field — the value every IME bridge syncs (`-1` = none).
185/// Index boundary rule: at the `AppTree`/shell seam the indices count UTF-16 code units; widgets
186/// and `frust-text` convert to byte offsets, and core interprets none.
187#[derive(Clone, Debug, PartialEq, Eq)]
188pub struct EditingState {
189    pub text: String,
190    pub selection_base: i32,
191    pub selection_extent: i32,
192    pub composing_base: i32,
193    pub composing_extent: i32,
194}
195
196impl Default for EditingState {
197    /// An empty field: the anchors are the `-1` "none" sentinel, not the derived `0`.
198    fn default() -> Self {
199        Self {
200            text: String::new(),
201            selection_base: -1,
202            selection_extent: -1,
203            composing_base: -1,
204            composing_extent: -1,
205        }
206    }
207}