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}