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}