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
9pub(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
67pub 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 subscriptions: RefCell<Vec<Rc<dyn IDisposable>>>,
77 is_composing: Cell<bool>,
78 focus_is_leaving_view: Cell<bool>,
80}
81
82impl BrowserTextInputMethod {
83 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 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 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 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 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 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 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 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 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 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 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 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}