Skip to main content

ferroui_browser/
browser_text_input_method.rs

1use crate::browser_input_handler::BrowserInputHandler;
2use crate::interop::{input_helper, JsObject};
3use ferroui_base::input::text_input::{ITextInputMethodImpl, TextInputMethodClient, TextInputOptions, TextSelection};
4use ferroui_base::reactive::IDisposable;
5use ferroui_base::Rect;
6use std::cell::{Cell, RefCell};
7use std::rc::{Rc, Weak};
8
9/// The calls the text input method makes into the page: the hidden input
10/// element of the view and the element of the view itself. Replaced by a
11/// recorder in the tests.
12pub(crate) trait ITextInputPage {
13    fn hide_input_element(&self);
14    fn show_input_element(&self);
15    fn focus_input_element(&self);
16    fn focus_container_element(&self);
17    fn clear_input_element(&self);
18    fn set_surrounding_text(&self, text: &str, start: i32, end: i32);
19    fn set_bounds(&self, x: i32, y: i32, width: i32, height: i32, caret: i32);
20    fn selection_start(&self) -> i32;
21    fn selection_end(&self) -> i32;
22}
23
24struct TextInputPage {
25    input_element: JsObject,
26    container_element: JsObject,
27}
28
29impl ITextInputPage for TextInputPage {
30    fn hide_input_element(&self) {
31        input_helper::hide_element(&self.input_element);
32    }
33
34    fn show_input_element(&self) {
35        input_helper::show_element(&self.input_element);
36    }
37
38    fn focus_input_element(&self) {
39        input_helper::focus_element(&self.input_element);
40    }
41
42    fn focus_container_element(&self) {
43        input_helper::focus_element(&self.container_element);
44    }
45
46    fn clear_input_element(&self) {
47        input_helper::clear_input_element(&self.input_element);
48    }
49
50    fn set_surrounding_text(&self, text: &str, start: i32, end: i32) {
51        input_helper::set_surrounding_text(&self.input_element, text, start, end);
52    }
53
54    fn set_bounds(&self, x: i32, y: i32, width: i32, height: i32, caret: i32) {
55        input_helper::set_bounds(&self.input_element, x, y, width, height, caret);
56    }
57
58    fn selection_start(&self) -> i32 {
59        input_helper::get_selection_start(&self.input_element)
60    }
61
62    fn selection_end(&self) -> i32 {
63        input_helper::get_selection_end(&self.input_element)
64    }
65}
66
67/// The text input method of a view: the hidden input element of the view
68/// stands in for the text the client edits, so that the input method of
69/// the browser has an element to compose into.
70pub struct BrowserTextInputMethod {
71    weak_self: Weak<BrowserTextInputMethod>,
72    page: Box<dyn ITextInputPage>,
73    input_handler: Weak<BrowserInputHandler>,
74    client: RefCell<Option<Rc<dyn TextInputMethodClient>>>,
75    /// The subscriptions to the events of the current client.
76    subscriptions: RefCell<Vec<Rc<dyn IDisposable>>>,
77    is_composing: Cell<bool>,
78    /// Set while the page reports that the keyboard focus left the view.
79    focus_is_leaving_view: Cell<bool>,
80}
81
82impl BrowserTextInputMethod {
83    /// Creates the text input method of the view in `container_element`,
84    /// whose hidden input element is `input_element`.
85    ///
86    /// # Panics
87    /// Panics when one of the elements is null or undefined.
88    pub(crate) fn new(
89        input_handler: Weak<BrowserInputHandler>,
90        container_element: JsObject,
91        input_element: JsObject,
92    ) -> Rc<Self> {
93        if input_element.is_null() || input_element.is_undefined() {
94            panic!("Value cannot be null. (Parameter 'inputElement')");
95        }
96        if container_element.is_null() || container_element.is_undefined() {
97            panic!("Value cannot be null. (Parameter 'containerElement')");
98        }
99
100        Self::with_page(input_handler, Box::new(TextInputPage { input_element, container_element }))
101    }
102
103    pub(crate) fn with_page(input_handler: Weak<BrowserInputHandler>, page: Box<dyn ITextInputPage>) -> Rc<Self> {
104        Rc::new_cyclic(|weak_self| Self {
105            weak_self: weak_self.clone(),
106            page,
107            input_handler,
108            client: RefCell::new(None),
109            subscriptions: RefCell::new(Vec::new()),
110            is_composing: Cell::new(false),
111            focus_is_leaving_view: Cell::new(false),
112        })
113    }
114
115    /// Whether the input method of the browser is composing text.
116    pub fn is_composing(&self) -> bool {
117        self.is_composing.get()
118    }
119
120    fn client(&self) -> Option<Rc<dyn TextInputMethodClient>> {
121        self.client.borrow().clone()
122    }
123
124    fn handler(&self, action: fn(&BrowserTextInputMethod)) -> Rc<dyn Fn()> {
125        let this = self.weak_self.clone();
126        Rc::new(move || {
127            if let Some(this) = this.upgrade() {
128                action(&this);
129            }
130        })
131    }
132
133    /// Runs `action` as the reaction to the keyboard focus of the page
134    /// having left the view: a client that goes away meanwhile does not
135    /// bring the focus back.
136    pub(crate) fn while_focus_leaves_view(&self, action: impl FnOnce()) {
137        let previous = self.focus_is_leaving_view.replace(true);
138        action();
139        self.focus_is_leaving_view.set(previous);
140    }
141
142    /// Gives the focus of the page to the hidden input element, unless the
143    /// focus is just leaving the view.
144    // Differs from the original, which focuses unconditionally: a client that updates its
145    // cursor while it loses the focus would take the focus back from the element of the page
146    // the user went to.
147    fn focus_input_element(&self) {
148        if !self.focus_is_leaving_view.get() {
149            self.page.focus_input_element();
150        }
151    }
152
153    fn hide_ime(&self) {
154        self.page.hide_input_element();
155        // Differs from the original, which always gives the focus back to the element of the
156        // view. There the framework never learns that the page moved the focus elsewhere; here
157        // it does, drops its focused element and with it the client, and focusing the view then
158        // would take the focus back from the element of the page the user just went to.
159        if !self.focus_is_leaving_view.get() {
160            self.page.focus_container_element();
161        }
162    }
163
164    fn input_pane_activation_requested(&self) {
165        if self.client().is_some() {
166            self.show_ime();
167        }
168    }
169
170    fn show_ime(&self) {
171        self.page.show_input_element();
172        self.focus_input_element();
173    }
174
175    fn surrounding_text_changed(&self) {
176        if let Some(client) = self.client() {
177            let surrounding_text = client.surrounding_text();
178            let selection = client.selection();
179
180            self.page.set_surrounding_text(&surrounding_text, selection.start, selection.end);
181        }
182    }
183
184    /// The hidden input element is about to change: `input_type` is the
185    /// kind of the change and `start` and `end` the range it targets.
186    pub fn on_before_input(&self, input_type: &str, mut start: i32, mut end: i32) {
187        if input_type != "deleteByComposition" {
188            if input_type == "deleteContentBackward" {
189                start = self.page.selection_start();
190                end = self.page.selection_end();
191            } else {
192                start = -1;
193                end = -1;
194            }
195        }
196
197        if start != -1 && end != -1 {
198            if let Some(client) = self.client() {
199                client.set_selection(TextSelection::new(start, end));
200            }
201        }
202    }
203
204    /// The input method of the browser started composing.
205    pub fn on_composition_start(&self) {
206        let Some(client) = self.client() else {
207            return;
208        };
209
210        client.set_preedit_text(None);
211        self.is_composing.set(true);
212    }
213
214    /// The text being composed changed.
215    pub fn on_composition_update(&self, data: Option<&str>) {
216        let Some(client) = self.client() else {
217            return;
218        };
219
220        client.set_preedit_text(data);
221    }
222
223    /// The input method of the browser finished composing; `data` is the
224    /// text it committed.
225    pub fn on_composition_end(&self, data: Option<&str>) {
226        let Some(client) = self.client() else {
227            return;
228        };
229
230        self.is_composing.set(false);
231
232        client.set_preedit_text(None);
233
234        if let Some(data) = data {
235            if let Some(input_handler) = self.input_handler.upgrade() {
236                input_handler.raw_text_event(data);
237            }
238        }
239    }
240}
241
242impl ITextInputMethodImpl for BrowserTextInputMethod {
243    fn set_client(&self, client: Option<Rc<dyn TextInputMethodClient>>) {
244        if self.client().is_some() {
245            let subscriptions = std::mem::take(&mut *self.subscriptions.borrow_mut());
246            for subscription in subscriptions {
247                subscription.dispose();
248            }
249        }
250
251        if let Some(client) = &client {
252            let subscriptions = vec![
253                client.surrounding_text_changed(self.handler(Self::surrounding_text_changed)),
254                client.input_pane_activation_requested(self.handler(Self::input_pane_activation_requested)),
255            ];
256            *self.subscriptions.borrow_mut() = subscriptions;
257        }
258
259        self.page.clear_input_element();
260
261        let old_client = self.client.replace(client);
262        drop(old_client);
263
264        if let Some(client) = self.client() {
265            self.show_ime();
266
267            let surrounding_text = client.surrounding_text();
268            let selection = client.selection();
269
270            self.page.set_surrounding_text(&surrounding_text, selection.start, selection.end);
271        } else {
272            self.hide_ime();
273        }
274    }
275
276    fn set_cursor_rect(&self, rect: Rect) {
277        self.focus_input_element();
278        let caret = self.client().map_or(0, |client| client.selection().end);
279        self.page.set_bounds(rect.x as i32, rect.y as i32, rect.width as i32, rect.height as i32, caret);
280        self.focus_input_element();
281    }
282
283    fn set_options(&self, _options: &TextInputOptions) {}
284
285    fn reset(&self) {
286        self.page.clear_input_element();
287        self.page.set_surrounding_text("", 0, 0);
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use ferroui_base::input::text_input::TextInputMethodClientEvents;
295    use ferroui_base::{Ref, Visual};
296
297    type Calls = Rc<RefCell<Vec<String>>>;
298
299    struct RecordingPage {
300        calls: Calls,
301        selection: Rc<Cell<(i32, i32)>>,
302    }
303
304    impl RecordingPage {
305        fn log(&self, call: impl Into<String>) {
306            self.calls.borrow_mut().push(call.into());
307        }
308    }
309
310    impl ITextInputPage for RecordingPage {
311        fn hide_input_element(&self) {
312            self.log("hide");
313        }
314
315        fn show_input_element(&self) {
316            self.log("show");
317        }
318
319        fn focus_input_element(&self) {
320            self.log("focus input");
321        }
322
323        fn focus_container_element(&self) {
324            self.log("focus container");
325        }
326
327        fn clear_input_element(&self) {
328            self.log("clear");
329        }
330
331        fn set_surrounding_text(&self, text: &str, start: i32, end: i32) {
332            self.log(format!("text '{text}' {start} {end}"));
333        }
334
335        fn set_bounds(&self, x: i32, y: i32, width: i32, height: i32, caret: i32) {
336            self.log(format!("bounds {x} {y} {width} {height} caret {caret}"));
337        }
338
339        fn selection_start(&self) -> i32 {
340            self.selection.get().0
341        }
342
343        fn selection_end(&self) -> i32 {
344            self.selection.get().1
345        }
346    }
347
348    struct TestClient {
349        events: TextInputMethodClientEvents,
350        text: RefCell<String>,
351        selection: Cell<TextSelection>,
352        preedit: RefCell<Vec<Option<String>>>,
353    }
354
355    impl TestClient {
356        fn new(text: &str, start: i32, end: i32) -> Rc<Self> {
357            Rc::new(Self {
358                events: TextInputMethodClientEvents::new(),
359                text: RefCell::new(text.to_string()),
360                selection: Cell::new(TextSelection::new(start, end)),
361                preedit: RefCell::new(Vec::new()),
362            })
363        }
364    }
365
366    impl TextInputMethodClient for TestClient {
367        fn events(&self) -> &TextInputMethodClientEvents {
368            &self.events
369        }
370
371        fn text_view_visual(&self) -> Option<Ref<Visual>> {
372            None
373        }
374
375        fn supports_preedit(&self) -> bool {
376            true
377        }
378
379        fn supports_surrounding_text(&self) -> bool {
380            true
381        }
382
383        fn surrounding_text(&self) -> String {
384            self.text.borrow().clone()
385        }
386
387        fn cursor_rectangle(&self) -> Rect {
388            Rect::default()
389        }
390
391        fn selection(&self) -> TextSelection {
392            self.selection.get()
393        }
394
395        fn set_selection(&self, value: TextSelection) {
396            self.selection.set(value);
397        }
398
399        fn set_preedit_text(&self, preedit_text: Option<&str>) {
400            self.preedit.borrow_mut().push(preedit_text.map(str::to_string));
401        }
402    }
403
404    struct Fixture {
405        method: Rc<BrowserTextInputMethod>,
406        calls: Calls,
407        selection: Rc<Cell<(i32, i32)>>,
408    }
409
410    impl Fixture {
411        fn new() -> Self {
412            let calls: Calls = Rc::new(RefCell::new(Vec::new()));
413            let selection = Rc::new(Cell::new((0, 0)));
414            let page = RecordingPage { calls: calls.clone(), selection: selection.clone() };
415            let method = BrowserTextInputMethod::with_page(Weak::new(), Box::new(page));
416            Self { method, calls, selection }
417        }
418
419        fn take_calls(&self) -> Vec<String> {
420            std::mem::take(&mut *self.calls.borrow_mut())
421        }
422
423        fn set_client(&self, client: &Rc<TestClient>) {
424            let client: Rc<dyn TextInputMethodClient> = client.clone();
425            self.method.set_client(Some(client));
426        }
427    }
428
429    #[test]
430    fn a_client_shows_the_input_element_and_mirrors_its_text() {
431        let fixture = Fixture::new();
432        let client = TestClient::new("hello", 1, 3);
433
434        fixture.set_client(&client);
435
436        assert_eq!(vec!["clear", "show", "focus input", "text 'hello' 1 3"], fixture.take_calls());
437    }
438
439    #[test]
440    fn without_a_client_the_input_element_is_hidden_and_the_view_focused() {
441        let fixture = Fixture::new();
442        fixture.set_client(&TestClient::new("hello", 1, 3));
443        fixture.take_calls();
444
445        fixture.method.set_client(None);
446
447        assert_eq!(vec!["clear", "hide", "focus container"], fixture.take_calls());
448    }
449
450    #[test]
451    fn a_client_that_goes_away_because_the_focus_left_the_view_does_not_take_the_focus_back() {
452        let fixture = Fixture::new();
453        fixture.set_client(&TestClient::new("hello", 1, 3));
454        fixture.take_calls();
455
456        fixture.method.while_focus_leaves_view(|| {
457            // The client moves its cursor as it loses the focus, and then goes away.
458            fixture.method.set_cursor_rect(Rect::new(1.0, 2.0, 3.0, 4.0));
459            fixture.method.set_client(None);
460        });
461        let calls = fixture.take_calls();
462        assert!(!calls.iter().any(|call| call.starts_with("focus")), "{calls:?}");
463        assert_eq!(Some(&"hide".to_string()), calls.last());
464
465        // Afterwards the usual behaviour is back.
466        fixture.set_client(&TestClient::new("hello", 1, 3));
467        fixture.take_calls();
468        fixture.method.set_client(None);
469        assert_eq!(vec!["clear", "hide", "focus container"], fixture.take_calls());
470    }
471
472    #[test]
473    fn a_change_of_the_surrounding_text_of_the_client_is_mirrored() {
474        let fixture = Fixture::new();
475        let client = TestClient::new("hello", 1, 3);
476        fixture.set_client(&client);
477        fixture.take_calls();
478
479        *client.text.borrow_mut() = "hello world".to_string();
480        client.selection.set(TextSelection::new(11, 11));
481        client.raise_surrounding_text_changed();
482
483        assert_eq!(vec!["text 'hello world' 11 11"], fixture.take_calls());
484    }
485
486    #[test]
487    fn a_request_for_the_input_pane_shows_the_input_element_again() {
488        let fixture = Fixture::new();
489        let client = TestClient::new("", 0, 0);
490        fixture.set_client(&client);
491        fixture.take_calls();
492
493        client.raise_input_pane_activation_requested();
494
495        assert_eq!(vec!["show", "focus input"], fixture.take_calls());
496    }
497
498    #[test]
499    fn the_events_of_a_replaced_client_are_no_longer_followed() {
500        let fixture = Fixture::new();
501        let first = TestClient::new("first", 0, 0);
502        let second = TestClient::new("second", 2, 2);
503        fixture.set_client(&first);
504        fixture.set_client(&second);
505        fixture.take_calls();
506
507        first.raise_surrounding_text_changed();
508        first.raise_input_pane_activation_requested();
509        assert!(fixture.take_calls().is_empty());
510
511        second.raise_surrounding_text_changed();
512        assert_eq!(vec!["text 'second' 2 2"], fixture.take_calls());
513
514        fixture.method.set_client(None);
515        fixture.take_calls();
516        second.raise_surrounding_text_changed();
517        second.raise_input_pane_activation_requested();
518        assert!(fixture.take_calls().is_empty());
519    }
520
521    #[test]
522    fn the_cursor_rectangle_moves_the_input_element_to_the_caret_of_the_client() {
523        let fixture = Fixture::new();
524
525        fixture.method.set_cursor_rect(Rect::new(10.9, 20.2, 1.0, 16.7));
526        assert_eq!(vec!["focus input", "bounds 10 20 1 16 caret 0", "focus input"], fixture.take_calls());
527
528        fixture.set_client(&TestClient::new("hello", 1, 3));
529        fixture.take_calls();
530        fixture.method.set_cursor_rect(Rect::new(10.9, 20.2, 1.0, 16.7));
531        assert_eq!(vec!["focus input", "bounds 10 20 1 16 caret 3", "focus input"], fixture.take_calls());
532    }
533
534    #[test]
535    fn reset_empties_the_input_element() {
536        let fixture = Fixture::new();
537
538        fixture.method.reset();
539
540        assert_eq!(vec!["clear", "text '' 0 0"], fixture.take_calls());
541    }
542
543    #[test]
544    fn a_deletion_by_composition_selects_the_range_it_targets() {
545        let fixture = Fixture::new();
546        let client = TestClient::new("hello", 5, 5);
547        fixture.set_client(&client);
548
549        fixture.method.on_before_input("deleteByComposition", 2, 4);
550
551        assert_eq!(TextSelection::new(2, 4), client.selection());
552    }
553
554    #[test]
555    fn a_deletion_by_composition_without_a_range_leaves_the_selection() {
556        let fixture = Fixture::new();
557        let client = TestClient::new("hello", 5, 5);
558        fixture.set_client(&client);
559
560        fixture.method.on_before_input("deleteByComposition", -1, -1);
561        fixture.method.on_before_input("deleteByComposition", 2, -1);
562
563        assert_eq!(TextSelection::new(5, 5), client.selection());
564    }
565
566    #[test]
567    fn a_backward_deletion_selects_the_selection_of_the_input_element() {
568        let fixture = Fixture::new();
569        let client = TestClient::new("hello", 5, 5);
570        fixture.set_client(&client);
571        fixture.selection.set((3, 4));
572
573        fixture.method.on_before_input("deleteContentBackward", 0, 1);
574
575        assert_eq!(TextSelection::new(3, 4), client.selection());
576    }
577
578    #[test]
579    fn other_changes_of_the_input_element_leave_the_selection() {
580        let fixture = Fixture::new();
581        let client = TestClient::new("hello", 5, 5);
582        fixture.set_client(&client);
583        fixture.selection.set((3, 4));
584
585        fixture.method.on_before_input("insertText", 0, 1);
586        fixture.method.on_before_input("insertCompositionText", 2, 4);
587
588        assert_eq!(TextSelection::new(5, 5), client.selection());
589    }
590
591    #[test]
592    fn a_change_of_the_input_element_without_a_client_is_ignored() {
593        let fixture = Fixture::new();
594
595        fixture.method.on_before_input("deleteByComposition", 2, 4);
596        fixture.method.on_before_input("deleteContentBackward", 0, 1);
597
598        assert!(fixture.take_calls().is_empty());
599    }
600
601    #[test]
602    fn a_composition_sets_the_preedit_text_and_clears_it_at_its_end() {
603        let fixture = Fixture::new();
604        let client = TestClient::new("", 0, 0);
605        fixture.set_client(&client);
606        assert!(!fixture.method.is_composing());
607
608        fixture.method.on_composition_start();
609        assert!(fixture.method.is_composing());
610
611        fixture.method.on_composition_update(Some("k"));
612        fixture.method.on_composition_update(Some("ka"));
613        fixture.method.on_composition_update(None);
614        assert!(fixture.method.is_composing());
615
616        fixture.method.on_composition_end(Some("\u{304b}"));
617        assert!(!fixture.method.is_composing());
618
619        assert_eq!(
620            vec![None, Some("k".to_string()), Some("ka".to_string()), None, None],
621            *client.preedit.borrow()
622        );
623    }
624
625    #[test]
626    fn a_composition_without_a_client_changes_nothing() {
627        let fixture = Fixture::new();
628
629        fixture.method.on_composition_start();
630        assert!(!fixture.method.is_composing());
631
632        fixture.method.on_composition_update(Some("k"));
633        fixture.method.on_composition_end(Some("k"));
634
635        assert!(!fixture.method.is_composing());
636        assert!(fixture.take_calls().is_empty());
637    }
638
639    #[test]
640    fn a_composition_that_loses_its_client_stays_composing_until_a_client_ends_one() {
641        // As in the original: the end of a composition is only seen with a client.
642        let fixture = Fixture::new();
643        let client = TestClient::new("", 0, 0);
644        fixture.set_client(&client);
645        fixture.method.on_composition_start();
646
647        fixture.method.set_client(None);
648        fixture.method.on_composition_end(None);
649        assert!(fixture.method.is_composing());
650
651        fixture.set_client(&client);
652        fixture.method.on_composition_end(None);
653        assert!(!fixture.method.is_composing());
654    }
655}