Skip to main content

guise/input/
keys.rs

1//! Keyboard handling for single-line text fields.
2//!
3//! Shared by [`TextInput`](super::TextInput) and by hosts that drive a
4//! [`TextEdit`](super::TextEdit) directly — inline fields that render their own
5//! chrome (a search bar, a palette) rather than embedding the full component.
6//! macOS/Linux conventions: Option = word-wise, Cmd = line-wise, plus the
7//! Emacs-style Ctrl+A / Ctrl+E / Ctrl+K.
8
9use gpui::Keystroke;
10
11use super::edit::TextEdit;
12
13/// What a keystroke did to a single-line field, so the host can react.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum KeyOutcome {
16    /// Enter — the host should commit.
17    Submit,
18    /// Escape — the host should dismiss.
19    Cancel,
20    /// The field changed; redraw.
21    Edited,
22    /// Not handled here; the host may act on it (e.g. Tab, Cmd+W).
23    Pass,
24}
25
26/// Apply `ks` to `edit`, returning what the host should do. `platform` is Cmd
27/// on macOS; `alt` is Option.
28///
29/// This is the whole keyboard, including typing the printable character. The
30/// components use [`apply_nav`] instead and leave text entry to the platform's
31/// input handler, which is what makes IME and dead keys work; this entry point
32/// stays for hosts that drive a [`TextEdit`] themselves and only get raw key
33/// events.
34pub fn apply_key(edit: &mut TextEdit, ks: &Keystroke) -> KeyOutcome {
35    let outcome = apply_nav(edit, ks);
36    if outcome != KeyOutcome::Pass {
37        return outcome;
38    }
39    // Printable input: never on Cmd/Ctrl chords (those are shortcuts);
40    // Option+key is allowed so composed glyphs land. Control characters are
41    // filtered out — the platform reports a `\t` for Tab and a `\n` for
42    // Enter, and a text field must not type either.
43    if !ks.modifiers.platform && !ks.modifiers.control {
44        if let Some(text) = ks.key_char.as_deref() {
45            let text: String = text.chars().filter(|c| !c.is_control()).collect();
46            if !text.is_empty() {
47                edit.insert(&text);
48                return KeyOutcome::Edited;
49            }
50        }
51    }
52    KeyOutcome::Pass
53}
54
55/// Navigation, selection, and deletion only — everything a text field does
56/// with a key that isn't typing a character. Returns [`KeyOutcome::Pass`] for
57/// anything it doesn't recognise, including every printable key.
58pub fn apply_nav(edit: &mut TextEdit, ks: &Keystroke) -> KeyOutcome {
59    let m = &ks.modifiers;
60    match ks.key.as_str() {
61        "enter" => return KeyOutcome::Submit,
62        "escape" => return KeyOutcome::Cancel,
63        // Cmd/Super+A selects the whole field (Ctrl+A stays Emacs line-start).
64        "a" if m.platform => {
65            edit.select_all();
66            return KeyOutcome::Edited;
67        }
68        "left" => {
69            if !m.shift && !m.platform && !m.alt && edit.collapse_selection_start() {
70                return KeyOutcome::Edited;
71            }
72            edit.pre_move(m.shift);
73            if m.platform {
74                edit.home();
75            } else if m.alt {
76                edit.word_left();
77            } else {
78                edit.left();
79            }
80            return KeyOutcome::Edited;
81        }
82        "right" => {
83            if !m.shift && !m.platform && !m.alt && edit.collapse_selection_end() {
84                return KeyOutcome::Edited;
85            }
86            edit.pre_move(m.shift);
87            if m.platform {
88                edit.end();
89            } else if m.alt {
90                edit.word_right();
91            } else {
92                edit.right();
93            }
94            return KeyOutcome::Edited;
95        }
96        // Single-line: vertical keys collapse to the line edges.
97        "up" | "home" => {
98            edit.pre_move(m.shift);
99            edit.home();
100            return KeyOutcome::Edited;
101        }
102        "down" | "end" => {
103            edit.pre_move(m.shift);
104            edit.end();
105            return KeyOutcome::Edited;
106        }
107        "backspace" => {
108            if m.platform {
109                edit.delete_to_start();
110            } else if m.alt {
111                edit.delete_word_back();
112            } else {
113                edit.backspace();
114            }
115            return KeyOutcome::Edited;
116        }
117        "delete" => {
118            if m.platform {
119                edit.delete_to_end();
120            } else if m.alt {
121                edit.delete_word_forward();
122            } else {
123                edit.delete();
124            }
125            return KeyOutcome::Edited;
126        }
127        "k" if m.control => {
128            edit.delete_to_end();
129            return KeyOutcome::Edited;
130        }
131        "a" if m.control => {
132            edit.home();
133            return KeyOutcome::Edited;
134        }
135        "e" if m.control => {
136            edit.end();
137            return KeyOutcome::Edited;
138        }
139        _ => {}
140    }
141    KeyOutcome::Pass
142}