GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/backend/text_input.rs (11.1K)
1 //! `text-input-unstable-v3`: the Wayland shell's half of input-method
2 //! composition (`crate::ime`), as the hidden textarea is the browser's and
3 //! `NSTextInputClient` the AppKit shell's.
4 //!
5 //! The compositor relays between this object and the input method (an
6 //! `input-method-v2` client: fcitx5, IBus through its Wayland frontend, …):
7 //!
8 //! - **What we tell it.** While a widget is editing text (`ime::caret` is
9 //! set) and the seat's text-input focus is on our surface (`enter`), the
10 //! text input is ENABLED, with a normal content type and the caret as the
11 //! cursor rectangle (surface px), re-sent when the caret moves; otherwise
12 //! it is disabled. A composition a widget dropped (`ime::take_reset`) is
13 //! cancelled by disabling and enabling again, which resets the input
14 //! method's state. Every state change is one `commit`, counted.
15 //! - **What it tells us.** `preedit_string`, `commit_string` and
16 //! `delete_surrounding_text` are double-buffered and applied on `done`, in
17 //! the protocol's order: the old composition out, the commit typed
18 //! (`Driver::commit_text`), the new composition in (`Driver::preedit`).
19 //! A batch with no `preedit_string` ends the composition. We send no
20 //! surrounding text, so a deletion has nothing to count its bytes in and
21 //! is not applied. A `done` whose serial is behind our commits still
22 //! applies its text — the protocol asks only that it not change our state.
23 //! - `enter` applies the last frame's caret at once (an idle window builds
24 //! no frame to do it); `leave` drops any composition, as the protocol
25 //! asks, and an enabled text input is disabled there and then: wlroots
26 //! keeps a text input's enabled state across a leave, and a stale
27 //! "enabled" turns the next enable into a plain commit the compositor
28 //! ignores.
29 //!
30 //! - A press re-announces a field that survives it (`ime::note_press`):
31 //! the next plan that still has a caret commits it again, unchanged, as a
32 //! `Move` to the same rectangle. The compositor raises its on-screen
33 //! keyboard on an enable or commit that follows a touch, and a field that
34 //! was already open (a focused terminal, a text box still editing after the
35 //! board was dismissed) would otherwise send nothing when tapped again. A
36 //! press that ends the editing disables instead, so tapping away never
37 //! flashes the board; a mouse click re-commits too, and the compositor
38 //! ignores it, since no finger armed the board.
39 //!
40 //! The pure part — what a batch does, and what state to send — is
41 //! [`Batch::apply_order`] and [`TextInput::plan`], tested with no
42 //! compositor.
43
44 use crate::ime::Preedit;
45
46 /// The double-buffered text a `done` applies.
47 #[derive(Debug, Default, Clone, PartialEq, Eq)]
48 pub struct Batch {
49 pub preedit: Option<Preedit>,
50 pub commit: Option<String>,
51 pub delete: Option<(u32, u32)>,
52 }
53
54 /// One step of applying a batch.
55 #[derive(Debug, Clone, PartialEq, Eq)]
56 pub enum Apply {
57 Preedit(Option<Preedit>),
58 Commit(String),
59 }
60
61 impl Batch {
62 /// A `preedit_string` event: the protocol's cursor (bytes, -1 for
63 /// hidden) as `Preedit`'s.
64 pub fn set_preedit(&mut self, text: Option<String>, begin: i32, end: i32) {
65 let text = text.unwrap_or_default();
66 let cursor = (begin >= 0 && end >= 0).then_some((begin as usize, end as usize));
67 self.preedit = (!text.is_empty()).then(|| Preedit::new(text, cursor));
68 }
69
70 /// The batch as the protocol's `done` orders it: the old composition
71 /// out, the commit typed, the new composition in.
72 pub fn apply_order(self) -> Vec<Apply> {
73 let mut steps = Vec::new();
74 if let Some(text) = self.commit.filter(|t| !t.is_empty()) {
75 steps.push(Apply::Preedit(None));
76 steps.push(Apply::Commit(text));
77 }
78 steps.push(Apply::Preedit(self.preedit));
79 steps
80 }
81 }
82
83 /// What to send the compositor this turn.
84 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
85 pub enum Send {
86 /// Nothing changed.
87 Nothing,
88 /// `enable` (after a `disable` if `reset`), the content type, the
89 /// cursor rectangle, `commit`.
90 Enable { rect: [i32; 4], reset: bool },
91 /// The cursor rectangle moved: it, then `commit`.
92 Move { rect: [i32; 4] },
93 /// `disable`, `commit`.
94 Disable,
95 }
96
97 /// The text input's state on our side.
98 #[derive(Debug, Default)]
99 pub struct TextInput {
100 /// The seat's text-input focus is on our surface.
101 pub entered: bool,
102 /// We have committed an `enable` (and no `disable` since).
103 pub enabled: bool,
104 /// The cursor rectangle last committed.
105 pub sent_rect: Option<[i32; 4]>,
106 /// `commit` requests issued: what a current `done` carries as its serial.
107 pub commits: u32,
108 /// The batch since the last `done`.
109 pub pending: Batch,
110 }
111
112 impl TextInput {
113 /// What the state should become, given the editing widget's caret
114 /// (`ime::caret`, in the app's logical px), the surface's px per
115 /// logical px, and whether a widget asked for its composition to be
116 /// cancelled, and whether a press landed since the last plan (an open
117 /// field is then committed again). Records the new state as sent.
118 pub fn plan(&mut self, caret: Option<[f32; 4]>, surface_scale: f32, reset: bool, pressed: bool) -> Send {
119 let rect = caret.map(|[x, y, w, h]| {
120 let s = surface_scale;
121 [(x * s).round() as i32, (y * s).round() as i32, ((w * s).round() as i32).max(1), ((h * s).round() as i32).max(1)]
122 });
123 let send = match (self.entered, rect) {
124 (true, Some(rect)) if !self.enabled || reset => Send::Enable { rect, reset: self.enabled && reset },
125 (true, Some(rect)) if self.sent_rect != Some(rect) || pressed => Send::Move { rect },
126 (true, Some(_)) => Send::Nothing,
127 (true, None) if self.enabled => Send::Disable,
128 _ => Send::Nothing,
129 };
130 match send {
131 Send::Enable { rect, reset } => {
132 self.enabled = true;
133 self.sent_rect = Some(rect);
134 // A reset commits its disable before the enable.
135 self.commits += if reset { 2 } else { 1 };
136 }
137 Send::Move { rect } => {
138 self.sent_rect = Some(rect);
139 self.commits += 1;
140 }
141 Send::Disable => {
142 self.enabled = false;
143 self.sent_rect = None;
144 self.commits += 1;
145 }
146 Send::Nothing => {}
147 }
148 send
149 }
150
151 /// `enter`: our surface has the text-input focus; the next plan enables
152 /// if a widget is editing.
153 pub fn enter(&mut self) {
154 self.entered = true;
155 self.enabled = false;
156 self.sent_rect = None;
157 }
158
159 /// `leave`: the focus went. True when we were enabled, and so owe a
160 /// `disable` and `commit` (counted here): wlroots keeps the enabled
161 /// state across a leave, and the next `enter`'s enable would otherwise
162 /// be a plain commit it ignores.
163 pub fn leave(&mut self) -> bool {
164 let owed = self.enabled;
165 if owed {
166 self.commits += 1;
167 }
168 self.entered = false;
169 self.enabled = false;
170 self.sent_rect = None;
171 self.pending = Batch::default();
172 owed
173 }
174
175 /// `done`: the batch to apply, the pending state back to initial.
176 pub fn done(&mut self) -> Batch {
177 std::mem::take(&mut self.pending)
178 }
179 }
180
181 #[cfg(test)]
182 mod tests {
183 use super::*;
184
185 #[test]
186 fn a_done_ends_the_composition_types_the_commit_and_starts_the_next() {
187 let mut b = Batch { commit: Some("日本".into()), ..Batch::default() };
188 b.set_preedit(Some("ご".into()), 3, 3);
189 assert_eq!(
190 b.apply_order(),
191 vec![
192 Apply::Preedit(None),
193 Apply::Commit("日本".into()),
194 Apply::Preedit(Some(Preedit::new("ご", Some((3, 3))))),
195 ]
196 );
197 // A batch with no preedit_string ends the composition; a hidden
198 // cursor is none.
199 assert_eq!(Batch::default().apply_order(), vec![Apply::Preedit(None)]);
200 let mut b = Batch::default();
201 b.set_preedit(Some("か".into()), -1, -1);
202 assert_eq!(b.apply_order(), vec![Apply::Preedit(Some(Preedit::new("か", None)))]);
203 }
204
205 #[test]
206 fn enabled_only_while_focused_and_editing_and_every_change_one_commit() {
207 let mut ti = TextInput::default();
208 let caret = Some([10.0, 20.0, 1.5, 16.0]);
209 // Editing, but not yet entered: nothing to say.
210 assert_eq!(ti.plan(caret, 1.0, false, false), Send::Nothing);
211 ti.enter();
212 assert_eq!(ti.plan(caret, 1.0, false, false), Send::Enable { rect: [10, 20, 2, 16], reset: false });
213 assert_eq!(ti.plan(caret, 1.0, false, false), Send::Nothing);
214 // The caret moves; at a forced scale of 2 the surface is twice the app.
215 assert_eq!(ti.plan(Some([30.0, 20.0, 1.5, 16.0]), 2.0, false, false), Send::Move { rect: [60, 40, 3, 32] });
216 // A widget dropped its composition: disable and enable again.
217 assert_eq!(ti.plan(Some([30.0, 20.0, 1.5, 16.0]), 2.0, true, false), Send::Enable { rect: [60, 40, 3, 32], reset: true });
218 // Nothing editing.
219 assert_eq!(ti.plan(None, 1.0, false, false), Send::Disable);
220 assert_eq!(ti.plan(None, 1.0, false, false), Send::Nothing);
221 assert_eq!(ti.commits, 5, "enable, move, disable + enable, disable");
222 // The focus leaves; editing again enables nothing until it is back.
223 assert!(!ti.leave(), "nothing enabled, nothing owed");
224 assert_eq!(ti.plan(caret, 1.0, false, false), Send::Nothing);
225 ti.enter();
226 assert!(matches!(ti.plan(caret, 1.0, false, false), Send::Enable { reset: false, .. }));
227 }
228
229 #[test]
230 fn a_leave_while_enabled_owes_a_disable_and_enter_enables_again() {
231 let mut ti = TextInput::default();
232 let caret = Some([10.0, 20.0, 1.5, 16.0]);
233 ti.enter();
234 ti.plan(caret, 1.0, false, false);
235 assert!(ti.leave(), "wlroots keeps the enabled state across a leave");
236 assert_eq!(ti.commits, 2, "the enable, then the disable");
237 ti.enter();
238 assert_eq!(ti.plan(caret, 1.0, false, false), Send::Enable { rect: [10, 20, 2, 16], reset: false });
239 }
240
241 #[test]
242 fn a_press_on_an_open_field_commits_it_again() {
243 let mut ti = TextInput::default();
244 let caret = Some([10.0, 20.0, 1.5, 16.0]);
245 ti.enter();
246 ti.plan(caret, 1.0, false, false);
247 assert_eq!(ti.plan(caret, 1.0, false, true), Send::Move { rect: [10, 20, 2, 16] }, "unchanged, announced again");
248 assert_eq!(ti.plan(caret, 1.0, false, false), Send::Nothing);
249 // A press that ends the editing only disables.
250 assert_eq!(ti.plan(None, 1.0, false, true), Send::Disable);
251 // A press with nothing editing says nothing.
252 assert_eq!(ti.plan(None, 1.0, false, true), Send::Nothing);
253 }
254
255 #[test]
256 fn a_reset_with_nothing_enabled_is_an_ordinary_enable() {
257 let mut ti = TextInput::default();
258 ti.enter();
259 assert_eq!(ti.plan(Some([0.0, 0.0, 1.0, 10.0]), 1.0, true, false), Send::Enable { rect: [0, 0, 1, 10], reset: false });
260 }
261 }