1use std::{
11 cell::{Cell, RefCell},
12 rc::{Rc, Weak},
13};
14
15use crate::key_event::KeyEvent;
16
17#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct ImeEditorState {
24 pub text: String,
26 pub selection_start: usize,
28 pub selection_end: usize,
30 pub composition: Option<(usize, usize)>,
32 pub single_line: bool,
35}
36
37#[derive(Clone, Debug, PartialEq)]
42pub struct ImeCaretGeometry {
43 pub caret_xs: Vec<f32>,
46 pub top: f32,
48 pub line_height: f32,
50}
51
52pub trait FocusedTextFieldHandler {
55 fn node_id(&self) -> Option<cranpose_core::NodeId> {
61 None
62 }
63 fn handle_key(&self, event: &KeyEvent) -> bool;
65 fn insert_text(&self, text: &str);
67 fn delete_surrounding(&self, before_bytes: usize, after_bytes: usize);
69 fn copy_selection(&self) -> Option<String>;
71 fn cut_selection(&self) -> Option<String>;
73 fn select_all(&self) {}
75 fn set_composition(&self, text: &str, cursor: Option<(usize, usize)>);
80 fn finish_composition(&self) {}
84 fn set_composing_region(&self, start_bytes: usize, end_bytes: usize) {
89 let _ = (start_bytes, end_bytes);
90 }
91 fn set_selection(&self, start_bytes: usize, end_bytes: usize) {
97 let _ = (start_bytes, end_bytes);
98 }
99 fn editor_state(&self) -> Option<ImeEditorState> {
103 None
104 }
105 fn caret_geometry(&self) -> Option<ImeCaretGeometry> {
109 None
110 }
111}
112
113pub(crate) struct TextFieldFocusState {
114 focused_field: RefCell<Option<Weak<RefCell<bool>>>>,
115 focused_handler: RefCell<Option<Rc<dyn FocusedTextFieldHandler>>>,
116 focused_modal_depth: Cell<usize>,
117}
118
119impl TextFieldFocusState {
120 pub(crate) fn new() -> Self {
121 Self {
122 focused_field: RefCell::new(None),
123 focused_handler: RefCell::new(None),
124 focused_modal_depth: Cell::new(0),
125 }
126 }
127
128 fn request_focus(
129 &self,
130 is_focused: Rc<RefCell<bool>>,
131 handler: Rc<dyn FocusedTextFieldHandler>,
132 modal_depth: usize,
133 ) {
134 let mut current = self.focused_field.borrow_mut();
135
136 if let Some(ref weak) = *current
137 && let Some(old_focused) = weak.upgrade()
138 {
139 *old_focused.borrow_mut() = false;
140 }
141
142 *is_focused.borrow_mut() = true;
143 *current = Some(Rc::downgrade(&is_focused));
144 *self.focused_handler.borrow_mut() = Some(handler);
145 self.focused_modal_depth.set(modal_depth);
146 }
147
148 fn clear_focus(&self) {
149 let mut current = self.focused_field.borrow_mut();
150
151 if let Some(ref weak) = *current
152 && let Some(focused) = weak.upgrade()
153 {
154 *focused.borrow_mut() = false;
155 }
156
157 *current = None;
158 *self.focused_handler.borrow_mut() = None;
159 self.focused_modal_depth.set(0);
160 }
161
162 fn focused_at_depth(&self, depth: usize) -> bool {
163 self.has_focused_field() && self.focused_modal_depth.get() == depth
164 }
165
166 fn has_focused_field(&self) -> bool {
167 if self.focused_field_is_live() {
168 return true;
169 }
170 self.clear_stale_focus();
171 false
172 }
173
174 fn focused_field_is_live(&self) -> bool {
175 self.focused_field
176 .borrow()
177 .as_ref()
178 .is_some_and(|weak| weak.upgrade().is_some())
179 }
180
181 fn clear_stale_focus(&self) {
182 let stale_node = self
183 .focused_handler
184 .borrow()
185 .as_ref()
186 .and_then(|handler| handler.node_id());
187 let had_entry = self.focused_field.borrow_mut().take().is_some();
188 if !had_entry {
189 return;
190 }
191 self.focused_handler.borrow_mut().take();
192 if let Some(node_id) = stale_node {
193 crate::schedule_draw_repass(node_id);
194 }
195 crate::cursor_animation::stop_cursor_blink();
196 }
197
198 fn focused_handler(&self) -> Option<Rc<dyn FocusedTextFieldHandler>> {
199 if !self.has_focused_field() {
200 return None;
201 }
202 self.focused_handler.borrow().as_ref().cloned()
203 }
204
205 fn dispatch_key_event(&self, event: &KeyEvent) -> bool {
206 if let Some(handler) = self.focused_handler() {
207 handler.handle_key(event)
208 } else {
209 false
210 }
211 }
212
213 fn dispatch_paste(&self, text: &str) -> bool {
214 if let Some(handler) = self.focused_handler() {
215 handler.insert_text(text);
216 true
217 } else {
218 false
219 }
220 }
221
222 fn dispatch_delete_surrounding(&self, before_bytes: usize, after_bytes: usize) -> bool {
223 if let Some(handler) = self.focused_handler() {
224 handler.delete_surrounding(before_bytes, after_bytes);
225 true
226 } else {
227 false
228 }
229 }
230
231 fn dispatch_copy(&self) -> Option<String> {
232 self.focused_handler()
233 .and_then(|handler| handler.copy_selection())
234 }
235
236 fn dispatch_cut(&self) -> Option<String> {
237 self.focused_handler()
238 .and_then(|handler| handler.cut_selection())
239 }
240
241 fn dispatch_select_all(&self) -> bool {
242 if let Some(handler) = self.focused_handler() {
243 handler.select_all();
244 true
245 } else {
246 false
247 }
248 }
249
250 fn dispatch_ime_preedit(&self, text: &str, cursor: Option<(usize, usize)>) -> bool {
251 if let Some(handler) = self.focused_handler() {
252 handler.set_composition(text, cursor);
253 true
254 } else {
255 false
256 }
257 }
258
259 fn dispatch_ime_finish_composing(&self) -> bool {
260 if let Some(handler) = self.focused_handler() {
261 handler.finish_composition();
262 true
263 } else {
264 false
265 }
266 }
267
268 fn dispatch_ime_set_composing_region(&self, start_bytes: usize, end_bytes: usize) -> bool {
269 if let Some(handler) = self.focused_handler() {
270 handler.set_composing_region(start_bytes, end_bytes);
271 true
272 } else {
273 false
274 }
275 }
276
277 fn dispatch_ime_set_selection(&self, start_bytes: usize, end_bytes: usize) -> bool {
278 if let Some(handler) = self.focused_handler() {
279 handler.set_selection(start_bytes, end_bytes);
280 true
281 } else {
282 false
283 }
284 }
285
286 fn focused_editor_state(&self) -> Option<ImeEditorState> {
287 self.focused_handler()
288 .and_then(|handler| handler.editor_state())
289 }
290
291 fn focused_caret_geometry(&self) -> Option<ImeCaretGeometry> {
292 self.focused_handler()
293 .and_then(|handler| handler.caret_geometry())
294 }
295}
296
297pub fn request_focus(
310 is_focused: Rc<RefCell<bool>>,
311 handler: Rc<dyn FocusedTextFieldHandler>,
312 modal_depth: usize,
313) {
314 if modal_depth < crate::modal::current_modal_depth() {
315 return;
316 }
317
318 let previous_field = focused_field_node();
319 let gaining_field = handler.node_id();
320
321 crate::render_state::with_text_field_focus(|state| {
322 state.request_focus(is_focused, handler, modal_depth);
323 });
324
325 for node_id in [previous_field, gaining_field].into_iter().flatten() {
326 crate::schedule_draw_repass(node_id);
327 }
328
329 crate::cursor_animation::start_cursor_blink();
330
331 crate::text_input_session::notify_text_input_focus_gained();
332
333 crate::request_render_invalidation();
334}
335
336pub(crate) fn clear_focus_for_closed_modal(depth: usize) {
337 let owns_focus =
338 crate::render_state::with_text_field_focus(|state| state.focused_at_depth(depth));
339 if owns_focus {
340 clear_focus();
341 }
342}
343
344pub fn clear_focus() {
346 let previous = focused_field_node();
347 if let Some(node_id) = previous {
348 crate::schedule_draw_repass(node_id);
349 }
350 crate::render_state::with_text_field_focus(TextFieldFocusState::clear_focus);
351 if previous.is_some() && crate::focus_dispatch::active_focus_target() == previous {
352 crate::focus_dispatch::clear_active_focus();
353 }
354
355 crate::cursor_animation::stop_cursor_blink();
356
357 crate::text_input_session::notify_text_input_focus_lost();
358
359 crate::request_render_invalidation();
360}
361
362pub fn focused_field_node() -> Option<cranpose_core::NodeId> {
365 crate::render_state::with_text_field_focus(|state| {
366 state
367 .focused_handler()
368 .and_then(|handler| handler.node_id())
369 })
370}
371
372pub fn has_focused_field() -> bool {
375 let has_focus =
376 crate::render_state::with_text_field_focus(TextFieldFocusState::has_focused_field);
377 if !has_focus {
378 crate::text_input_session::notify_text_input_focus_lost();
379 }
380 has_focus
381}
382
383pub fn dispatch_key_event(event: &KeyEvent) -> bool {
386 crate::render_state::with_text_field_focus(|state| state.dispatch_key_event(event))
387}
388
389pub fn dispatch_paste(text: &str) -> bool {
392 crate::render_state::with_text_field_focus(|state| state.dispatch_paste(text))
393}
394
395pub fn dispatch_delete_surrounding(before_bytes: usize, after_bytes: usize) -> bool {
398 crate::render_state::with_text_field_focus(|state| {
399 state.dispatch_delete_surrounding(before_bytes, after_bytes)
400 })
401}
402
403pub fn dispatch_copy() -> Option<String> {
406 crate::render_state::with_text_field_focus(TextFieldFocusState::dispatch_copy)
407}
408
409pub fn dispatch_cut() -> Option<String> {
412 crate::render_state::with_text_field_focus(TextFieldFocusState::dispatch_cut)
413}
414
415pub fn dispatch_select_all() -> bool {
418 crate::render_state::with_text_field_focus(TextFieldFocusState::dispatch_select_all)
419}
420
421pub fn dispatch_ime_preedit(text: &str, cursor: Option<(usize, usize)>) -> bool {
425 crate::render_state::with_text_field_focus(|state| state.dispatch_ime_preedit(text, cursor))
426}
427
428pub fn dispatch_ime_finish_composing() -> bool {
432 crate::render_state::with_text_field_focus(TextFieldFocusState::dispatch_ime_finish_composing)
433}
434
435pub fn dispatch_ime_set_composing_region(start_bytes: usize, end_bytes: usize) -> bool {
439 crate::render_state::with_text_field_focus(|state| {
440 state.dispatch_ime_set_composing_region(start_bytes, end_bytes)
441 })
442}
443
444pub fn dispatch_ime_set_selection(start_bytes: usize, end_bytes: usize) -> bool {
449 crate::render_state::with_text_field_focus(|state| {
450 state.dispatch_ime_set_selection(start_bytes, end_bytes)
451 })
452}
453
454pub fn focused_editor_state() -> Option<ImeEditorState> {
458 crate::render_state::with_text_field_focus(TextFieldFocusState::focused_editor_state)
459}
460
461pub fn focused_caret_geometry() -> Option<ImeCaretGeometry> {
464 crate::render_state::with_text_field_focus(TextFieldFocusState::focused_caret_geometry)
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 struct MockHandler;
472 impl FocusedTextFieldHandler for MockHandler {
473 fn handle_key(&self, _: &KeyEvent) -> bool {
474 false
475 }
476 fn insert_text(&self, _: &str) {}
477 fn delete_surrounding(&self, _: usize, _: usize) {}
478 fn copy_selection(&self) -> Option<String> {
479 None
480 }
481 fn cut_selection(&self) -> Option<String> {
482 None
483 }
484 fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {}
485 }
486
487 fn mock_handler() -> Rc<dyn FocusedTextFieldHandler> {
488 Rc::new(MockHandler)
489 }
490
491 struct NodeBackedHandler(cranpose_core::NodeId);
492 impl FocusedTextFieldHandler for NodeBackedHandler {
493 fn node_id(&self) -> Option<cranpose_core::NodeId> {
494 Some(self.0)
495 }
496 fn handle_key(&self, _: &KeyEvent) -> bool {
497 false
498 }
499 fn insert_text(&self, _: &str) {}
500 fn delete_surrounding(&self, _: usize, _: usize) {}
501 fn copy_selection(&self) -> Option<String> {
502 None
503 }
504 fn cut_selection(&self) -> Option<String> {
505 None
506 }
507 fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {}
508 }
509
510 #[test]
511 fn focus_transitions_schedule_scoped_draw_repasses_on_both_fields() {
512 let _app_context = crate::render_state::app_context_test_scope();
513 let _ = crate::render_state::take_draw_repass_nodes();
514
515 let first = Rc::new(RefCell::new(false));
516 request_focus(first, Rc::new(NodeBackedHandler(7)), 0);
517 assert!(
518 crate::render_state::take_draw_repass_nodes().contains(&7),
519 "gaining focus must re-record the gaining field's draws"
520 );
521
522 let second = Rc::new(RefCell::new(false));
523 request_focus(second, Rc::new(NodeBackedHandler(9)), 0);
524 let repasses = crate::render_state::take_draw_repass_nodes();
525 assert!(
526 repasses.contains(&7) && repasses.contains(&9),
527 "a focus hand-off must re-record both fields, got {repasses:?}"
528 );
529
530 clear_focus();
531 assert!(
532 crate::render_state::take_draw_repass_nodes().contains(&9),
533 "losing focus must re-record the field that had the caret"
534 );
535 }
536
537 #[test]
538 fn a_blink_transition_schedules_a_scoped_repass_on_the_focused_field() {
539 let _app_context = crate::render_state::app_context_test_scope();
540 let focus = Rc::new(RefCell::new(false));
541 request_focus(focus, Rc::new(NodeBackedHandler(21)), 0);
542 let _ = crate::render_state::take_draw_repass_nodes();
543
544 let past_interval = web_time::Instant::now()
545 + crate::cursor_animation::CursorAnimationState::BLINK_INTERVAL
546 + std::time::Duration::from_millis(1);
547 assert!(
548 crate::cursor_animation::tick_cursor_blink_at(past_interval),
549 "the tick past the interval must flip visibility"
550 );
551 assert!(
552 crate::render_state::take_draw_repass_nodes().contains(&21),
553 "the flip must re-record the focused field's draws"
554 );
555 clear_focus();
556 }
557
558 #[test]
559 fn request_focus_sets_flag() {
560 let _app_context = crate::render_state::app_context_test_scope();
561 let focus = Rc::new(RefCell::new(false));
562 request_focus(focus.clone(), mock_handler(), 0);
563 assert!(*focus.borrow());
564 clear_focus();
565 }
566
567 #[test]
568 fn request_focus_clears_previous() {
569 let _app_context = crate::render_state::app_context_test_scope();
570 let focus1 = Rc::new(RefCell::new(false));
571 let focus2 = Rc::new(RefCell::new(false));
572
573 request_focus(focus1.clone(), mock_handler(), 0);
574 assert!(*focus1.borrow());
575
576 request_focus(focus2.clone(), mock_handler(), 0);
577 assert!(!*focus1.borrow());
578 assert!(*focus2.borrow());
579 clear_focus();
580 }
581
582 #[test]
583 fn clear_focus_unfocuses_current() {
584 let _app_context = crate::render_state::app_context_test_scope();
585 let focus = Rc::new(RefCell::new(false));
586 request_focus(focus.clone(), mock_handler(), 0);
587 assert!(*focus.borrow());
588
589 clear_focus();
590 assert!(!*focus.borrow());
591 }
592
593 #[derive(Default)]
594 struct DispatchRecordingHandler {
595 key_count: Cell<usize>,
596 insert_count: Cell<usize>,
597 delete_count: Cell<usize>,
598 copy_count: Cell<usize>,
599 cut_count: Cell<usize>,
600 preedit_count: Cell<usize>,
601 last_delete: Cell<Option<(usize, usize)>>,
602 }
603
604 impl DispatchRecordingHandler {
605 fn bump(cell: &Cell<usize>) {
606 cell.set(cell.get() + 1);
607 }
608
609 fn total_calls(&self) -> usize {
610 self.key_count.get()
611 + self.insert_count.get()
612 + self.delete_count.get()
613 + self.copy_count.get()
614 + self.cut_count.get()
615 + self.preedit_count.get()
616 }
617 }
618
619 impl FocusedTextFieldHandler for DispatchRecordingHandler {
620 fn handle_key(&self, _: &KeyEvent) -> bool {
621 Self::bump(&self.key_count);
622 true
623 }
624
625 fn insert_text(&self, _: &str) {
626 Self::bump(&self.insert_count);
627 }
628
629 fn delete_surrounding(&self, before_bytes: usize, after_bytes: usize) {
630 Self::bump(&self.delete_count);
631 self.last_delete.set(Some((before_bytes, after_bytes)));
632 }
633
634 fn copy_selection(&self) -> Option<String> {
635 Self::bump(&self.copy_count);
636 Some("copy".to_string())
637 }
638
639 fn cut_selection(&self) -> Option<String> {
640 Self::bump(&self.cut_count);
641 Some("cut".to_string())
642 }
643
644 fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {
645 Self::bump(&self.preedit_count);
646 }
647 }
648
649 #[test]
650 fn dispatch_delete_surrounding_calls_handler() {
651 let _app_context = crate::render_state::app_context_test_scope();
652 let focus = Rc::new(RefCell::new(false));
653 let handler = Rc::new(DispatchRecordingHandler::default());
654
655 request_focus(Rc::clone(&focus), handler.clone(), 0);
656 assert!(dispatch_delete_surrounding(3, 1));
657 assert_eq!(handler.last_delete.get(), Some((3, 1)));
658
659 clear_focus();
660 }
661
662 #[test]
663 fn dispatch_clears_stale_focus_owner_before_invoking_handler() {
664 let _app_context = crate::render_state::app_context_test_scope();
665 let handler = Rc::new(DispatchRecordingHandler::default());
666
667 {
668 let focus = Rc::new(RefCell::new(false));
669 request_focus(Rc::clone(&focus), handler.clone(), 0);
670 assert!(has_focused_field());
671 }
672
673 let key_event = KeyEvent::key_down(crate::key_event::KeyCode::A, "a");
674
675 assert!(!dispatch_key_event(&key_event));
676 assert!(!dispatch_paste("stale paste"));
677 assert!(!dispatch_delete_surrounding(2, 1));
678 assert_eq!(dispatch_copy(), None);
679 assert_eq!(dispatch_cut(), None);
680 assert!(!dispatch_ime_preedit("preedit", Some((1, 1))));
681 assert!(!has_focused_field());
682 assert_eq!(
683 handler.total_calls(),
684 0,
685 "stale focused-field handlers must not receive input"
686 );
687 }
688
689 #[test]
690 fn stale_focus_cleanup_after_hidden_blink_does_not_reenter_the_focus_registry_borrow() {
691 let _app_context = crate::render_state::app_context_test_scope();
692
693 let focus = Rc::new(RefCell::new(false));
694 request_focus(focus.clone(), Rc::new(NodeBackedHandler(3)), 0);
695
696 let past_interval = web_time::Instant::now()
697 + crate::cursor_animation::CursorAnimationState::BLINK_INTERVAL
698 + std::time::Duration::from_millis(1);
699 assert!(
700 crate::cursor_animation::tick_cursor_blink_at(past_interval),
701 "the blink must already have toggled to hidden, matching the real \
702 timing where the bug's stop_cursor_blink() call is a visibility \
703 change and therefore reaches invalidate_focused_caret()"
704 );
705
706 drop(focus);
707
708 assert!(
709 !has_focused_field(),
710 "a field dropped without clear_focus must read back as unfocused \
711 instead of panicking on a reentrant borrow of focused_field"
712 );
713 }
714
715 #[test]
716 fn stale_focus_cleanup_repasses_the_node_that_lost_its_caret() {
717 let _app_context = crate::render_state::app_context_test_scope();
718 let _ = crate::render_state::take_draw_repass_nodes();
719
720 let focus = Rc::new(RefCell::new(false));
721 request_focus(focus.clone(), Rc::new(NodeBackedHandler(11)), 0);
722 let _ = crate::render_state::take_draw_repass_nodes();
723
724 drop(focus);
725
726 assert!(!has_focused_field());
727 assert!(
728 crate::render_state::take_draw_repass_nodes().contains(&11),
729 "discovering a stale field lazily must repass its node just like \
730 an explicit clear_focus does, or its caret is left stale on screen"
731 );
732 }
733
734 #[derive(Default)]
735 struct KeyboardProbe {
736 calls: RefCell<Vec<&'static str>>,
737 }
738
739 impl crate::text_input_session::PlatformTextInputHandler for KeyboardProbe {
740 fn show_keyboard(&self) {
741 self.calls.borrow_mut().push("show");
742 }
743
744 fn hide_keyboard(&self) {
745 self.calls.borrow_mut().push("hide");
746 }
747 }
748
749 #[test]
750 fn focus_transitions_drive_platform_keyboard() {
751 let _app_context = crate::render_state::app_context_test_scope();
752 let keyboard = Rc::new(KeyboardProbe::default());
753 crate::text_input_session::set_platform_text_input_handler(keyboard.clone());
754
755 let focus = Rc::new(RefCell::new(false));
756 request_focus(focus.clone(), mock_handler(), 0);
757 assert_eq!(*keyboard.calls.borrow(), vec!["show"]);
758
759 request_focus(focus, mock_handler(), 0);
760 assert_eq!(*keyboard.calls.borrow(), vec!["show", "show"]);
761
762 clear_focus();
763 assert_eq!(*keyboard.calls.borrow(), vec!["show", "show", "hide"]);
764 }
765
766 #[test]
767 fn stale_focus_detection_hides_platform_keyboard() {
768 let _app_context = crate::render_state::app_context_test_scope();
769 let keyboard = Rc::new(KeyboardProbe::default());
770 crate::text_input_session::set_platform_text_input_handler(keyboard.clone());
771
772 {
773 let focus = Rc::new(RefCell::new(false));
774 request_focus(focus, mock_handler(), 0);
775 }
776
777 assert!(!has_focused_field());
778 assert_eq!(*keyboard.calls.borrow(), vec!["show", "hide"]);
779
780 assert!(!has_focused_field());
781 assert_eq!(*keyboard.calls.borrow(), vec!["show", "hide"]);
782 }
783
784 #[test]
785 fn text_field_focus_is_scoped_by_app_context() {
786 let _app_context = crate::render_state::app_context_test_scope();
787 let first = crate::render_state::AppContext::new_with_density(1.0);
788 let second = crate::render_state::AppContext::new_with_density(1.0);
789 let first_focus = Rc::new(RefCell::new(false));
790 let second_focus = Rc::new(RefCell::new(false));
791
792 first.enter(|| {
793 request_focus(first_focus.clone(), mock_handler(), 0);
794 assert!(has_focused_field());
795 assert!(*first_focus.borrow());
796 });
797
798 second.enter(|| {
799 assert!(!has_focused_field());
800 request_focus(second_focus.clone(), mock_handler(), 0);
801 assert!(has_focused_field());
802 assert!(*second_focus.borrow());
803 });
804
805 first.enter(|| {
806 assert!(has_focused_field());
807 assert!(*first_focus.borrow());
808 clear_focus();
809 assert!(!has_focused_field());
810 assert!(!*first_focus.borrow());
811 });
812
813 second.enter(|| {
814 assert!(has_focused_field());
815 assert!(*second_focus.borrow());
816 clear_focus();
817 });
818 }
819}