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>,
120}
121
122impl TextFieldFocusState {
123 pub(crate) fn new() -> Self {
124 Self {
125 focused_field: RefCell::new(None),
126 focused_handler: RefCell::new(None),
127 focused_modal_depth: Cell::new(0),
128 }
129 }
130
131 fn request_focus(
132 &self,
133 is_focused: Rc<RefCell<bool>>,
134 handler: Rc<dyn FocusedTextFieldHandler>,
135 modal_depth: usize,
136 ) {
137 let mut current = self.focused_field.borrow_mut();
138
139 if let Some(ref weak) = *current
140 && let Some(old_focused) = weak.upgrade()
141 {
142 *old_focused.borrow_mut() = false;
143 }
144
145 *is_focused.borrow_mut() = true;
146 *current = Some(Rc::downgrade(&is_focused));
147 *self.focused_handler.borrow_mut() = Some(handler);
148 self.focused_modal_depth.set(modal_depth);
149 }
150
151 fn clear_focus(&self) {
152 let mut current = self.focused_field.borrow_mut();
153
154 if let Some(ref weak) = *current
155 && let Some(focused) = weak.upgrade()
156 {
157 *focused.borrow_mut() = false;
158 }
159
160 *current = None;
161 *self.focused_handler.borrow_mut() = None;
162 self.focused_modal_depth.set(0);
163 }
164
165 fn focused_at_depth(&self, depth: usize) -> bool {
167 self.has_focused_field() && self.focused_modal_depth.get() == depth
168 }
169
170 fn has_focused_field(&self) -> bool {
171 if self.focused_field_is_live() {
172 return true;
173 }
174 self.clear_stale_focus();
175 false
176 }
177
178 fn focused_field_is_live(&self) -> bool {
180 self.focused_field
181 .borrow()
182 .as_ref()
183 .is_some_and(|weak| weak.upgrade().is_some())
184 }
185
186 fn clear_stale_focus(&self) {
190 let stale_node = self
191 .focused_handler
192 .borrow()
193 .as_ref()
194 .and_then(|handler| handler.node_id());
195 let had_entry = self.focused_field.borrow_mut().take().is_some();
196 if !had_entry {
197 return;
198 }
199 self.focused_handler.borrow_mut().take();
200 if let Some(node_id) = stale_node {
201 crate::schedule_draw_repass(node_id);
202 }
203 crate::cursor_animation::stop_cursor_blink();
204 }
205
206 fn focused_handler(&self) -> Option<Rc<dyn FocusedTextFieldHandler>> {
207 if !self.has_focused_field() {
208 return None;
209 }
210 self.focused_handler.borrow().as_ref().cloned()
211 }
212
213 fn dispatch_key_event(&self, event: &KeyEvent) -> bool {
214 if let Some(handler) = self.focused_handler() {
215 handler.handle_key(event)
216 } else {
217 false
218 }
219 }
220
221 fn dispatch_paste(&self, text: &str) -> bool {
222 if let Some(handler) = self.focused_handler() {
223 handler.insert_text(text);
224 true
225 } else {
226 false
227 }
228 }
229
230 fn dispatch_delete_surrounding(&self, before_bytes: usize, after_bytes: usize) -> bool {
231 if let Some(handler) = self.focused_handler() {
232 handler.delete_surrounding(before_bytes, after_bytes);
233 true
234 } else {
235 false
236 }
237 }
238
239 fn dispatch_copy(&self) -> Option<String> {
240 self.focused_handler()
241 .and_then(|handler| handler.copy_selection())
242 }
243
244 fn dispatch_cut(&self) -> Option<String> {
245 self.focused_handler()
246 .and_then(|handler| handler.cut_selection())
247 }
248
249 fn dispatch_select_all(&self) -> bool {
250 if let Some(handler) = self.focused_handler() {
251 handler.select_all();
252 true
253 } else {
254 false
255 }
256 }
257
258 fn dispatch_ime_preedit(&self, text: &str, cursor: Option<(usize, usize)>) -> bool {
259 if let Some(handler) = self.focused_handler() {
260 handler.set_composition(text, cursor);
261 true
262 } else {
263 false
264 }
265 }
266
267 fn dispatch_ime_finish_composing(&self) -> bool {
268 if let Some(handler) = self.focused_handler() {
269 handler.finish_composition();
270 true
271 } else {
272 false
273 }
274 }
275
276 fn dispatch_ime_set_composing_region(&self, start_bytes: usize, end_bytes: usize) -> bool {
277 if let Some(handler) = self.focused_handler() {
278 handler.set_composing_region(start_bytes, end_bytes);
279 true
280 } else {
281 false
282 }
283 }
284
285 fn dispatch_ime_set_selection(&self, start_bytes: usize, end_bytes: usize) -> bool {
286 if let Some(handler) = self.focused_handler() {
287 handler.set_selection(start_bytes, end_bytes);
288 true
289 } else {
290 false
291 }
292 }
293
294 fn focused_editor_state(&self) -> Option<ImeEditorState> {
295 self.focused_handler()
296 .and_then(|handler| handler.editor_state())
297 }
298
299 fn focused_caret_geometry(&self) -> Option<ImeCaretGeometry> {
300 self.focused_handler()
301 .and_then(|handler| handler.caret_geometry())
302 }
303}
304
305pub fn request_focus(
318 is_focused: Rc<RefCell<bool>>,
319 handler: Rc<dyn FocusedTextFieldHandler>,
320 modal_depth: usize,
321) {
322 if modal_depth < crate::modal::current_modal_depth() {
323 return;
324 }
325
326 let previous_field = focused_field_node();
331 let gaining_field = handler.node_id();
332
333 crate::render_state::with_text_field_focus(|state| {
334 state.request_focus(is_focused, handler, modal_depth)
335 });
336
337 for node_id in [previous_field, gaining_field].into_iter().flatten() {
338 crate::schedule_draw_repass(node_id);
339 }
340
341 crate::cursor_animation::start_cursor_blink();
343
344 crate::text_input_session::notify_text_input_focus_gained();
347
348 crate::request_render_invalidation();
351}
352
353pub(crate) fn clear_focus_for_closed_modal(depth: usize) {
359 let owns_focus =
360 crate::render_state::with_text_field_focus(|state| state.focused_at_depth(depth));
361 if owns_focus {
362 clear_focus();
363 }
364}
365
366pub fn clear_focus() {
368 if let Some(node_id) = focused_field_node() {
371 crate::schedule_draw_repass(node_id);
372 }
373 crate::render_state::with_text_field_focus(|state| state.clear_focus());
374
375 crate::cursor_animation::stop_cursor_blink();
377
378 crate::text_input_session::notify_text_input_focus_lost();
380
381 crate::request_render_invalidation();
382}
383
384pub fn focused_field_node() -> Option<cranpose_core::NodeId> {
387 crate::render_state::with_text_field_focus(|state| {
388 state
389 .focused_handler()
390 .and_then(|handler| handler.node_id())
391 })
392}
393
394pub fn has_focused_field() -> bool {
397 let has_focus = crate::render_state::with_text_field_focus(|state| state.has_focused_field());
398 if !has_focus {
399 crate::text_input_session::notify_text_input_focus_lost();
403 }
404 has_focus
405}
406
407pub fn dispatch_key_event(event: &KeyEvent) -> bool {
414 crate::render_state::with_text_field_focus(|state| state.dispatch_key_event(event))
415}
416
417pub fn dispatch_paste(text: &str) -> bool {
420 crate::render_state::with_text_field_focus(|state| state.dispatch_paste(text))
421}
422
423pub fn dispatch_delete_surrounding(before_bytes: usize, after_bytes: usize) -> bool {
426 crate::render_state::with_text_field_focus(|state| {
427 state.dispatch_delete_surrounding(before_bytes, after_bytes)
428 })
429}
430
431pub fn dispatch_copy() -> Option<String> {
434 crate::render_state::with_text_field_focus(|state| state.dispatch_copy())
435}
436
437pub fn dispatch_cut() -> Option<String> {
440 crate::render_state::with_text_field_focus(|state| state.dispatch_cut())
441}
442
443pub fn dispatch_select_all() -> bool {
446 crate::render_state::with_text_field_focus(|state| state.dispatch_select_all())
447}
448
449pub fn dispatch_ime_preedit(text: &str, cursor: Option<(usize, usize)>) -> bool {
453 crate::render_state::with_text_field_focus(|state| state.dispatch_ime_preedit(text, cursor))
454}
455
456pub fn dispatch_ime_finish_composing() -> bool {
460 crate::render_state::with_text_field_focus(|state| state.dispatch_ime_finish_composing())
461}
462
463pub fn dispatch_ime_set_composing_region(start_bytes: usize, end_bytes: usize) -> bool {
467 crate::render_state::with_text_field_focus(|state| {
468 state.dispatch_ime_set_composing_region(start_bytes, end_bytes)
469 })
470}
471
472pub fn dispatch_ime_set_selection(start_bytes: usize, end_bytes: usize) -> bool {
477 crate::render_state::with_text_field_focus(|state| {
478 state.dispatch_ime_set_selection(start_bytes, end_bytes)
479 })
480}
481
482pub fn focused_editor_state() -> Option<ImeEditorState> {
486 crate::render_state::with_text_field_focus(|state| state.focused_editor_state())
487}
488
489pub fn focused_caret_geometry() -> Option<ImeCaretGeometry> {
492 crate::render_state::with_text_field_focus(|state| state.focused_caret_geometry())
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 struct MockHandler;
501 impl FocusedTextFieldHandler for MockHandler {
502 fn handle_key(&self, _: &KeyEvent) -> bool {
503 false
504 }
505 fn insert_text(&self, _: &str) {}
506 fn delete_surrounding(&self, _: usize, _: usize) {}
507 fn copy_selection(&self) -> Option<String> {
508 None
509 }
510 fn cut_selection(&self) -> Option<String> {
511 None
512 }
513 fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {}
514 }
515
516 fn mock_handler() -> Rc<dyn FocusedTextFieldHandler> {
517 Rc::new(MockHandler)
518 }
519
520 struct NodeBackedHandler(cranpose_core::NodeId);
523 impl FocusedTextFieldHandler for NodeBackedHandler {
524 fn node_id(&self) -> Option<cranpose_core::NodeId> {
525 Some(self.0)
526 }
527 fn handle_key(&self, _: &KeyEvent) -> bool {
528 false
529 }
530 fn insert_text(&self, _: &str) {}
531 fn delete_surrounding(&self, _: usize, _: usize) {}
532 fn copy_selection(&self) -> Option<String> {
533 None
534 }
535 fn cut_selection(&self) -> Option<String> {
536 None
537 }
538 fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {}
539 }
540
541 #[test]
546 fn focus_transitions_schedule_scoped_draw_repasses_on_both_fields() {
547 let _app_context = crate::render_state::app_context_test_scope();
548 let _ = crate::render_state::take_draw_repass_nodes();
549
550 let first = Rc::new(RefCell::new(false));
551 request_focus(first.clone(), Rc::new(NodeBackedHandler(7)), 0);
552 assert!(
553 crate::render_state::take_draw_repass_nodes().contains(&7),
554 "gaining focus must re-record the gaining field's draws"
555 );
556
557 let second = Rc::new(RefCell::new(false));
558 request_focus(second.clone(), Rc::new(NodeBackedHandler(9)), 0);
559 let repasses = crate::render_state::take_draw_repass_nodes();
560 assert!(
561 repasses.contains(&7) && repasses.contains(&9),
562 "a focus hand-off must re-record both fields, got {repasses:?}"
563 );
564
565 clear_focus();
566 assert!(
567 crate::render_state::take_draw_repass_nodes().contains(&9),
568 "losing focus must re-record the field that had the caret"
569 );
570 }
571
572 #[test]
576 fn a_blink_transition_schedules_a_scoped_repass_on_the_focused_field() {
577 let _app_context = crate::render_state::app_context_test_scope();
578 let focus = Rc::new(RefCell::new(false));
579 request_focus(focus.clone(), Rc::new(NodeBackedHandler(21)), 0);
580 let _ = crate::render_state::take_draw_repass_nodes();
581
582 let past_interval = web_time::Instant::now()
583 + crate::cursor_animation::CursorAnimationState::BLINK_INTERVAL
584 + std::time::Duration::from_millis(1);
585 assert!(
586 crate::cursor_animation::tick_cursor_blink_at(past_interval),
587 "the tick past the interval must flip visibility"
588 );
589 assert!(
590 crate::render_state::take_draw_repass_nodes().contains(&21),
591 "the flip must re-record the focused field's draws"
592 );
593 clear_focus();
594 }
595
596 #[test]
597 fn request_focus_sets_flag() {
598 let _app_context = crate::render_state::app_context_test_scope();
599 let focus = Rc::new(RefCell::new(false));
600 request_focus(focus.clone(), mock_handler(), 0);
601 assert!(*focus.borrow());
602 clear_focus();
603 }
604
605 #[test]
606 fn request_focus_clears_previous() {
607 let _app_context = crate::render_state::app_context_test_scope();
608 let focus1 = Rc::new(RefCell::new(false));
609 let focus2 = Rc::new(RefCell::new(false));
610
611 request_focus(focus1.clone(), mock_handler(), 0);
612 assert!(*focus1.borrow());
613
614 request_focus(focus2.clone(), mock_handler(), 0);
615 assert!(!*focus1.borrow()); assert!(*focus2.borrow()); clear_focus();
618 }
619
620 #[test]
621 fn clear_focus_unfocuses_current() {
622 let _app_context = crate::render_state::app_context_test_scope();
623 let focus = Rc::new(RefCell::new(false));
624 request_focus(focus.clone(), mock_handler(), 0);
625 assert!(*focus.borrow());
626
627 clear_focus();
628 assert!(!*focus.borrow());
629 }
630
631 #[derive(Default)]
632 struct DispatchRecordingHandler {
633 key_count: Cell<usize>,
634 insert_count: Cell<usize>,
635 delete_count: Cell<usize>,
636 copy_count: Cell<usize>,
637 cut_count: Cell<usize>,
638 preedit_count: Cell<usize>,
639 last_delete: Cell<Option<(usize, usize)>>,
640 }
641
642 impl DispatchRecordingHandler {
643 fn bump(cell: &Cell<usize>) {
644 cell.set(cell.get() + 1);
645 }
646
647 fn total_calls(&self) -> usize {
648 self.key_count.get()
649 + self.insert_count.get()
650 + self.delete_count.get()
651 + self.copy_count.get()
652 + self.cut_count.get()
653 + self.preedit_count.get()
654 }
655 }
656
657 impl FocusedTextFieldHandler for DispatchRecordingHandler {
658 fn handle_key(&self, _: &KeyEvent) -> bool {
659 Self::bump(&self.key_count);
660 true
661 }
662
663 fn insert_text(&self, _: &str) {
664 Self::bump(&self.insert_count);
665 }
666
667 fn delete_surrounding(&self, before_bytes: usize, after_bytes: usize) {
668 Self::bump(&self.delete_count);
669 self.last_delete.set(Some((before_bytes, after_bytes)));
670 }
671
672 fn copy_selection(&self) -> Option<String> {
673 Self::bump(&self.copy_count);
674 Some("copy".to_string())
675 }
676
677 fn cut_selection(&self) -> Option<String> {
678 Self::bump(&self.cut_count);
679 Some("cut".to_string())
680 }
681
682 fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {
683 Self::bump(&self.preedit_count);
684 }
685 }
686
687 #[test]
688 fn dispatch_delete_surrounding_calls_handler() {
689 let _app_context = crate::render_state::app_context_test_scope();
690 let focus = Rc::new(RefCell::new(false));
691 let handler = Rc::new(DispatchRecordingHandler::default());
692
693 request_focus(Rc::clone(&focus), handler.clone(), 0);
694 assert!(dispatch_delete_surrounding(3, 1));
695 assert_eq!(handler.last_delete.get(), Some((3, 1)));
696
697 clear_focus();
698 }
699
700 #[test]
701 fn dispatch_clears_stale_focus_owner_before_invoking_handler() {
702 let _app_context = crate::render_state::app_context_test_scope();
703 let handler = Rc::new(DispatchRecordingHandler::default());
704
705 {
706 let focus = Rc::new(RefCell::new(false));
707 request_focus(Rc::clone(&focus), handler.clone(), 0);
708 assert!(has_focused_field());
709 }
710
711 let key_event = KeyEvent::key_down(crate::key_event::KeyCode::A, "a");
712
713 assert!(!dispatch_key_event(&key_event));
714 assert!(!dispatch_paste("stale paste"));
715 assert!(!dispatch_delete_surrounding(2, 1));
716 assert_eq!(dispatch_copy(), None);
717 assert_eq!(dispatch_cut(), None);
718 assert!(!dispatch_ime_preedit("preedit", Some((1, 1))));
719 assert!(!has_focused_field());
720 assert_eq!(
721 handler.total_calls(),
722 0,
723 "stale focused-field handlers must not receive input"
724 );
725 }
726
727 #[test]
728 fn stale_focus_cleanup_after_hidden_blink_does_not_reenter_the_focus_registry_borrow() {
729 let _app_context = crate::render_state::app_context_test_scope();
730
731 let focus = Rc::new(RefCell::new(false));
732 request_focus(focus.clone(), Rc::new(NodeBackedHandler(3)), 0);
733
734 let past_interval = web_time::Instant::now()
735 + crate::cursor_animation::CursorAnimationState::BLINK_INTERVAL
736 + std::time::Duration::from_millis(1);
737 assert!(
738 crate::cursor_animation::tick_cursor_blink_at(past_interval),
739 "the blink must already have toggled to hidden, matching the real \
740 timing where the bug's stop_cursor_blink() call is a visibility \
741 change and therefore reaches invalidate_focused_caret()"
742 );
743
744 drop(focus);
745
746 assert!(
747 !has_focused_field(),
748 "a field dropped without clear_focus must read back as unfocused \
749 instead of panicking on a reentrant borrow of focused_field"
750 );
751 }
752
753 #[test]
754 fn stale_focus_cleanup_repasses_the_node_that_lost_its_caret() {
755 let _app_context = crate::render_state::app_context_test_scope();
756 let _ = crate::render_state::take_draw_repass_nodes();
757
758 let focus = Rc::new(RefCell::new(false));
759 request_focus(focus.clone(), Rc::new(NodeBackedHandler(11)), 0);
760 let _ = crate::render_state::take_draw_repass_nodes();
761
762 drop(focus);
763
764 assert!(!has_focused_field());
765 assert!(
766 crate::render_state::take_draw_repass_nodes().contains(&11),
767 "discovering a stale field lazily must repass its node just like \
768 an explicit clear_focus does, or its caret is left stale on screen"
769 );
770 }
771
772 #[derive(Default)]
773 struct KeyboardProbe {
774 calls: RefCell<Vec<&'static str>>,
775 }
776
777 impl crate::text_input_session::PlatformTextInputHandler for KeyboardProbe {
778 fn show_keyboard(&self) {
779 self.calls.borrow_mut().push("show");
780 }
781
782 fn hide_keyboard(&self) {
783 self.calls.borrow_mut().push("hide");
784 }
785 }
786
787 #[test]
788 fn focus_transitions_drive_platform_keyboard() {
789 let _app_context = crate::render_state::app_context_test_scope();
790 let keyboard = Rc::new(KeyboardProbe::default());
791 crate::text_input_session::set_platform_text_input_handler(keyboard.clone());
792
793 let focus = Rc::new(RefCell::new(false));
794 request_focus(focus.clone(), mock_handler(), 0);
795 assert_eq!(*keyboard.calls.borrow(), vec!["show"]);
796
797 request_focus(focus, mock_handler(), 0);
800 assert_eq!(*keyboard.calls.borrow(), vec!["show", "show"]);
801
802 clear_focus();
803 assert_eq!(*keyboard.calls.borrow(), vec!["show", "show", "hide"]);
804 }
805
806 #[test]
807 fn stale_focus_detection_hides_platform_keyboard() {
808 let _app_context = crate::render_state::app_context_test_scope();
809 let keyboard = Rc::new(KeyboardProbe::default());
810 crate::text_input_session::set_platform_text_input_handler(keyboard.clone());
811
812 {
813 let focus = Rc::new(RefCell::new(false));
814 request_focus(focus, mock_handler(), 0);
815 }
818
819 assert!(!has_focused_field());
820 assert_eq!(*keyboard.calls.borrow(), vec!["show", "hide"]);
821
822 assert!(!has_focused_field());
824 assert_eq!(*keyboard.calls.borrow(), vec!["show", "hide"]);
825 }
826
827 #[test]
828 fn text_field_focus_is_scoped_by_app_context() {
829 let _app_context = crate::render_state::app_context_test_scope();
830 let first = crate::render_state::AppContext::new_with_density(1.0);
831 let second = crate::render_state::AppContext::new_with_density(1.0);
832 let first_focus = Rc::new(RefCell::new(false));
833 let second_focus = Rc::new(RefCell::new(false));
834
835 first.enter(|| {
836 request_focus(first_focus.clone(), mock_handler(), 0);
837 assert!(has_focused_field());
838 assert!(*first_focus.borrow());
839 });
840
841 second.enter(|| {
842 assert!(!has_focused_field());
843 request_focus(second_focus.clone(), mock_handler(), 0);
844 assert!(has_focused_field());
845 assert!(*second_focus.borrow());
846 });
847
848 first.enter(|| {
849 assert!(has_focused_field());
850 assert!(*first_focus.borrow());
851 clear_focus();
852 assert!(!has_focused_field());
853 assert!(!*first_focus.borrow());
854 });
855
856 second.enter(|| {
857 assert!(has_focused_field());
858 assert!(*second_focus.borrow());
859 clear_focus();
860 });
861 }
862}