git.lucas.co / cce-ui
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 }