GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/ime.rs (8.3K)
1 //! Input-method composition, between the text widget the user types into and
2 //! the shell whose window system runs the input method (an IME: Japanese,
3 //! Chinese, Korean, an accent picker, an emoji panel).
4 //!
5 //! Three things cross, and only one of them is new to the toolkit:
6 //!
7 //! - **The commit** — text the input method has finished composing — is
8 //! delivered as TYPED text (`Driver::commit_text`: a key press carrying it,
9 //! then its release), so every widget that inserts what a key types takes
10 //! it with no change: `TextBox`, `LineEdit`, the `DocEditor`, an app's own
11 //! field.
12 //! - **The composition** (the preedit: text still being composed, with the
13 //! input method's cursor in it) is shared here: a shell sets it
14 //! ([`set_preedit`], through `Driver::preedit`), and the widget that is
15 //! editing shows it at its caret — `TextBox` splices it into its buffer as
16 //! a provisional run, underlined, which is never committed, recorded in
17 //! its history or reported as a change.
18 //! - **The caret** goes the other way: a widget that is editing reports its
19 //! caret's rect each frame it paints ([`report_caret`]), so the shell can
20 //! place the input method's candidate window under it, and knows text
21 //! input is wanted at all ([`caret`] is `None` when nothing is editing;
22 //! the macOS shell then keeps keys away from the input method, so an IME
23 //! left on does not swallow an app's single-key shortcuts).
24 //!
25 //! A widget that drops a composition it was showing — it stopped editing
26 //! mid-composition — asks the shell to cancel it in the input method too
27 //! ([`request_reset`], drained by the shell with [`take_reset`]).
28 //!
29 //! The shells that feed it: the Wayland shell through `text-input-v3`
30 //! (`backend::text_input`), the browser's through a hidden textarea, the
31 //! AppKit shell as an `NSTextInputClient`.
32 //!
33 //! Per thread, as the context menu is: a window's widgets, its shell and its
34 //! frame are one thread's.
35
36 use std::cell::RefCell;
37
38 /// Text the input method is still composing.
39 #[derive(Debug, Clone, PartialEq, Eq, Default)]
40 pub struct Preedit {
41 pub text: String,
42 /// The input method's cursor (or selected clause) in `text`, as a byte
43 /// range; `None` to show no caret in it.
44 pub cursor: Option<(usize, usize)>,
45 }
46
47 impl Preedit {
48 pub fn new(text: impl Into<String>, cursor: Option<(usize, usize)>) -> Self {
49 Self { text: text.into(), cursor }
50 }
51
52 /// The caret's position in `text`, in chars: the end of the input
53 /// method's cursor range, or the end of the text when it has none. A
54 /// byte offset off a char boundary or past the end is clamped back.
55 pub fn caret_chars(&self) -> usize {
56 let end = self.cursor.map_or(self.text.len(), |(_, e)| e).min(self.text.len());
57 let mut end = end;
58 while !self.text.is_char_boundary(end) {
59 end -= 1;
60 }
61 self.text[..end].chars().count()
62 }
63 }
64
65 #[derive(Default)]
66 pub(crate) struct State {
67 preedit: Option<Preedit>,
68 /// Moves on every change of `preedit`, so a widget can tell it has
69 /// applied the current one.
70 generation: u64,
71 /// The caret the last built frame reported, and the one this frame has
72 /// so far.
73 caret: Option<[f32; 4]>,
74 reported: Option<[f32; 4]>,
75 reset: bool,
76 /// A press landed since the shell last asked ([`note_press`]).
77 pressed: bool,
78 }
79
80 /// The current window's composition and caret (`crate::window_state`).
81 fn state<R>(f: impl FnOnce(&RefCell<State>) -> R) -> R {
82 crate::window_state::with(|w| f(&w.ime))
83 }
84
85 /// The composition changed: `None` (or empty text) when there is none. A
86 /// shell's, through `Driver::preedit`.
87 pub fn set_preedit(preedit: Option<Preedit>) {
88 let preedit = preedit.filter(|p| !p.text.is_empty());
89 state(|s| {
90 let mut s = s.borrow_mut();
91 if s.preedit != preedit {
92 s.preedit = preedit;
93 s.generation += 1;
94 }
95 });
96 }
97
98 /// The composition, if the input method is composing.
99 pub fn preedit() -> Option<Preedit> {
100 state(|s| s.borrow().preedit.clone())
101 }
102
103 /// Moves whenever the composition does.
104 pub fn generation() -> u64 {
105 state(|s| s.borrow().generation)
106 }
107
108 /// A frame is being built: carets are reported afresh.
109 pub fn begin_frame() {
110 state(|s| s.borrow_mut().reported = None);
111 }
112
113 /// The frame is built: what was reported is the caret.
114 pub fn end_frame() {
115 state(|s| {
116 let mut s = s.borrow_mut();
117 s.caret = s.reported;
118 });
119 }
120
121 /// A widget editing text has its caret at `x, y` (`w` x `h`), in the
122 /// window's logical px. Called as it paints.
123 pub fn report_caret(x: f32, y: f32, w: f32, h: f32) {
124 state(|s| s.borrow_mut().reported = Some([x, y, w, h]));
125 }
126
127 /// Where the editing widget's caret was in the last built frame, or `None`
128 /// when no widget is editing text.
129 pub fn caret() -> Option<[f32; 4]> {
130 state(|s| s.borrow().caret)
131 }
132
133 /// A widget dropped a composition it was showing: the input method should
134 /// cancel it too. Clears the composition.
135 pub fn request_reset() {
136 set_preedit(None);
137 state(|s| s.borrow_mut().reset = true);
138 }
139
140 /// What this frame has reported so far, replaced by `caret`. For
141 /// `text_input::capture`, which reads what one painting reported.
142 pub(crate) fn swap_reported(caret: Option<[f32; 4]>) -> Option<[f32; 4]> {
143 state(|s| std::mem::replace(&mut s.borrow_mut().reported, caret))
144 }
145
146 /// A pointer or touch press reached the window. A field that is still
147 /// editing in the next frame is announced to the shell again
148 /// ([`take_press`]): the compositor raises its on-screen keyboard on an
149 /// announcement that follows a touch, and a field that was already open —
150 /// a focused terminal, a text box still editing — would otherwise say
151 /// nothing new when tapped. True when a field is editing, so the caller
152 /// builds that frame.
153 pub fn note_press() -> bool {
154 state(|s| {
155 let mut s = s.borrow_mut();
156 s.pressed = true;
157 s.caret.is_some()
158 })
159 }
160
161 /// Whether a press landed since the last call. A shell's.
162 pub fn take_press() -> bool {
163 state(|s| std::mem::replace(&mut s.borrow_mut().pressed, false))
164 }
165
166 /// Whether a reset was asked for since the last call. A shell's.
167 pub fn take_reset() -> bool {
168 state(|s| std::mem::replace(&mut s.borrow_mut().reset, false))
169 }
170
171 #[cfg(test)]
172 mod tests {
173 use super::*;
174
175 #[test]
176 fn a_composition_moves_the_generation_only_when_it_changes() {
177 let g0 = generation();
178 set_preedit(Some(Preedit::new("に", None)));
179 let g1 = generation();
180 assert!(g1 > g0);
181 set_preedit(Some(Preedit::new("に", None)));
182 assert_eq!(generation(), g1);
183 // Empty text is no composition.
184 set_preedit(Some(Preedit::new("", None)));
185 assert_eq!(preedit(), None);
186 assert!(generation() > g1);
187 }
188
189 #[test]
190 fn the_caret_is_the_end_of_the_cursor_range_in_chars() {
191 assert_eq!(Preedit::new("にほん", None).caret_chars(), 3);
192 // "に" is three bytes.
193 assert_eq!(Preedit::new("にほん", Some((0, 3))).caret_chars(), 1);
194 // Off a boundary, clamped back.
195 assert_eq!(Preedit::new("にほん", Some((0, 4))).caret_chars(), 1);
196 assert_eq!(Preedit::new("ab", Some((0, 9))).caret_chars(), 2);
197 }
198
199 #[test]
200 fn the_caret_is_what_the_last_built_frame_reported() {
201 begin_frame();
202 report_caret(10.0, 20.0, 1.5, 16.0);
203 assert_eq!(caret(), None, "not until the frame is built");
204 end_frame();
205 assert_eq!(caret(), Some([10.0, 20.0, 1.5, 16.0]));
206 begin_frame();
207 end_frame();
208 assert_eq!(caret(), None, "a frame with nothing editing");
209 }
210
211 #[test]
212 fn a_press_wants_a_frame_only_while_a_field_is_editing() {
213 begin_frame();
214 end_frame();
215 assert!(!note_press(), "nothing editing: no frame owed");
216 assert!(take_press(), "but the press is still noted");
217 assert!(!take_press(), "once");
218 begin_frame();
219 report_caret(1.0, 2.0, 3.0, 4.0);
220 end_frame();
221 assert!(note_press());
222 assert!(take_press());
223 }
224
225 #[test]
226 fn a_reset_clears_the_composition_and_is_taken_once() {
227 set_preedit(Some(Preedit::new("ka", None)));
228 request_reset();
229 assert_eq!(preedit(), None);
230 assert!(take_reset());
231 assert!(!take_reset());
232 }
233 }