git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/text_input.rs (3.6K)

 1 //! "A text field is being edited": the announcement behind text input.
 2 //!
 3 //! The compositor needs to know when a field is open for typing, because that
 4 //! is what raises the on-screen keyboard after a touch (cce-compositor's
 5 //! `osk.rs`) and what activates an input method. A widget says so by calling
 6 //! [`claim`] from its paint, every frame it is editing; a widget that stops
 7 //! painting has stopped claiming, so there is no enable/disable pair to close
 8 //! on every path out of editing (Enter, Escape, a click elsewhere, a focus
 9 //! change, its page dropped).
10 //!
11 //! A claim is the caret the input method sees: it is [`crate::ime::report_caret`]
12 //! by its older name, and the shells read it through [`crate::ime::caret`]
13 //! once the frame is built (Wayland's `text-input-v3` in
14 //! `backend::text_input`, the browser's keyboard sink, the AppKit shell's
15 //! `NSTextInputClient`). A widget that knows its caret claims the caret; one
16 //! that does not claims its field. The last claim of a frame wins, so a
17 //! widget may claim its field first and its caret once it has drawn it.
18 //!
19 //! `TextBox`, `Spinbox`, `Slider`'s readout, `ColorSelector`, the params
20 //! pane's code rows and a focused `DocEditor` claim on their own. An app
21 //! drawing its own text (a `LineEdit`, a terminal, an editor) claims from its
22 //! `display_list` while it has a caret.
23 //!
24 //! An app that paints its widgets only on some frames and replays the cached
25 //! result on the rest (a `rebuild_layout` flattening the tree) would claim on
26 //! the rebuilds alone, and every replayed frame would read as the field
27 //! closing. It runs the painting under [`capture`] and claims the captured
28 //! rectangle again on every frame.
29 
30 /// A field is editing this frame, its caret (or the field, when the caret is
31 /// not known) at `x, y, w, h` in the window's logical px. The last claim of a
32 /// frame wins.
33 pub fn claim(x: f32, y: f32, w: f32, h: f32) {
34     crate::ime::report_caret(x, y, w, h);
35 }
36 
37 /// Run `paint` and return what it claimed, alongside its result. A claim
38 /// made earlier this frame stands if `paint` makes none, so wrapping a
39 /// painting changes nothing about the frame's claim — it only reports it.
40 pub fn capture<R>(paint: impl FnOnce() -> R) -> (R, Option<[f32; 4]>) {
41     let before = crate::ime::swap_reported(None);
42     let out = paint();
43     let made = crate::ime::swap_reported(None);
44     crate::ime::swap_reported(made.or(before));
45     (out, made)
46 }
47 
48 #[cfg(test)]
49 mod tests {
50     use super::*;
51 
52     #[test]
53     fn a_claim_lasts_one_frame() {
54         crate::ime::begin_frame();
55         claim(1.0, 2.0, 3.0, 4.0);
56         crate::ime::end_frame();
57         assert_eq!(crate::ime::caret(), Some([1.0, 2.0, 3.0, 4.0]));
58         crate::ime::begin_frame();
59         crate::ime::end_frame();
60         assert_eq!(crate::ime::caret(), None, "a frame nobody claimed disables the text input");
61     }
62 
63     #[test]
64     fn capture_reports_a_painting_claim_and_keeps_the_frame_whole() {
65         crate::ime::begin_frame();
66         claim(1.0, 1.0, 1.0, 1.0);
67         let ((), made) = capture(|| {});
68         assert_eq!(made, None, "a painting that claims nothing reports nothing");
69         crate::ime::end_frame();
70         assert_eq!(crate::ime::caret(), Some([1.0, 1.0, 1.0, 1.0]), "and the earlier claim stands");
71         crate::ime::begin_frame();
72         let (n, made) = capture(|| {
73             claim(5.0, 6.0, 7.0, 8.0);
74             3
75         });
76         assert_eq!((n, made), (3, Some([5.0, 6.0, 7.0, 8.0])));
77         crate::ime::end_frame();
78         assert_eq!(crate::ime::caret(), Some([5.0, 6.0, 7.0, 8.0]), "the frame keeps what was captured");
79     }
80 }