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(|state| state.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 = crate::render_state::with_text_field_focus(|state| state.has_focused_field());
376 if !has_focus {
377 crate::text_input_session::notify_text_input_focus_lost();
378 }
379 has_focus
380}
381
382pub fn dispatch_key_event(event: &KeyEvent) -> bool {
385 crate::render_state::with_text_field_focus(|state| state.dispatch_key_event(event))
386}
387
388pub fn dispatch_paste(text: &str) -> bool {
391 crate::render_state::with_text_field_focus(|state| state.dispatch_paste(text))
392}
393
394pub fn dispatch_delete_surrounding(before_bytes: usize, after_bytes: usize) -> bool {
397 crate::render_state::with_text_field_focus(|state| {
398 state.dispatch_delete_surrounding(before_bytes, after_bytes)
399 })
400}
401
402pub fn dispatch_copy() -> Option<String> {
405 crate::render_state::with_text_field_focus(|state| state.dispatch_copy())
406}
407
408pub fn dispatch_cut() -> Option<String> {
411 crate::render_state::with_text_field_focus(|state| state.dispatch_cut())
412}
413
414pub fn dispatch_select_all() -> bool {
417 crate::render_state::with_text_field_focus(|state| state.dispatch_select_all())
418}
419
420pub fn dispatch_ime_preedit(text: &str, cursor: Option<(usize, usize)>) -> bool {
424 crate::render_state::with_text_field_focus(|state| state.dispatch_ime_preedit(text, cursor))
425}
426
427pub fn dispatch_ime_finish_composing() -> bool {
431 crate::render_state::with_text_field_focus(|state| state.dispatch_ime_finish_composing())
432}
433
434pub fn dispatch_ime_set_composing_region(start_bytes: usize, end_bytes: usize) -> bool {
438 crate::render_state::with_text_field_focus(|state| {
439 state.dispatch_ime_set_composing_region(start_bytes, end_bytes)
440 })
441}
442
443pub fn dispatch_ime_set_selection(start_bytes: usize, end_bytes: usize) -> bool {
448 crate::render_state::with_text_field_focus(|state| {
449 state.dispatch_ime_set_selection(start_bytes, end_bytes)
450 })
451}
452
453pub fn focused_editor_state() -> Option<ImeEditorState> {
457 crate::render_state::with_text_field_focus(|state| state.focused_editor_state())
458}
459
460pub fn focused_caret_geometry() -> Option<ImeCaretGeometry> {
463 crate::render_state::with_text_field_focus(|state| state.focused_caret_geometry())
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469
470 struct MockHandler;
471 impl FocusedTextFieldHandler for MockHandler {
472 fn handle_key(&self, _: &KeyEvent) -> bool {
473 false
474 }
475 fn insert_text(&self, _: &str) {}
476 fn delete_surrounding(&self, _: usize, _: usize) {}
477 fn copy_selection(&self) -> Option<String> {
478 None
479 }
480 fn cut_selection(&self) -> Option<String> {
481 None
482 }
483 fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {}
484 }
485
486 fn mock_handler() -> Rc<dyn FocusedTextFieldHandler> {
487 Rc::new(MockHandler)
488 }
489
490 struct NodeBackedHandler(cranpose_core::NodeId);
491 impl FocusedTextFieldHandler for NodeBackedHandler {
492 fn node_id(&self) -> Option<cranpose_core::NodeId> {
493 Some(self.0)
494 }
495 fn handle_key(&self, _: &KeyEvent) -> bool {
496 false
497 }
498 fn insert_text(&self, _: &str) {}
499 fn delete_surrounding(&self, _: usize, _: usize) {}
500 fn copy_selection(&self) -> Option<String> {
501 None
502 }
503 fn cut_selection(&self) -> Option<String> {
504 None
505 }
506 fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {}
507 }
508
509 #[test]
510 fn focus_transitions_schedule_scoped_draw_repasses_on_both_fields() {
511 let _app_context = crate::render_state::app_context_test_scope();
512 let _ = crate::render_state::take_draw_repass_nodes();
513
514 let first = Rc::new(RefCell::new(false));
515 request_focus(first.clone(), Rc::new(NodeBackedHandler(7)), 0);
516 assert!(
517 crate::render_state::take_draw_repass_nodes().contains(&7),
518 "gaining focus must re-record the gaining field's draws"
519 );
520
521 let second = Rc::new(RefCell::new(false));
522 request_focus(second.clone(), Rc::new(NodeBackedHandler(9)), 0);
523 let repasses = crate::render_state::take_draw_repass_nodes();
524 assert!(
525 repasses.contains(&7) && repasses.contains(&9),
526 "a focus hand-off must re-record both fields, got {repasses:?}"
527 );
528
529 clear_focus();
530 assert!(
531 crate::render_state::take_draw_repass_nodes().contains(&9),
532 "losing focus must re-record the field that had the caret"
533 );
534 }
535
536 #[test]
537 fn a_blink_transition_schedules_a_scoped_repass_on_the_focused_field() {
538 let _app_context = crate::render_state::app_context_test_scope();
539 let focus = Rc::new(RefCell::new(false));
540 request_focus(focus.clone(), Rc::new(NodeBackedHandler(21)), 0);
541 let _ = crate::render_state::take_draw_repass_nodes();
542
543 let past_interval = web_time::Instant::now()
544 + crate::cursor_animation::CursorAnimationState::BLINK_INTERVAL
545 + std::time::Duration::from_millis(1);
546 assert!(
547 crate::cursor_animation::tick_cursor_blink_at(past_interval),
548 "the tick past the interval must flip visibility"
549 );
550 assert!(
551 crate::render_state::take_draw_repass_nodes().contains(&21),
552 "the flip must re-record the focused field's draws"
553 );
554 clear_focus();
555 }
556
557 #[test]
558 fn request_focus_sets_flag() {
559 let _app_context = crate::render_state::app_context_test_scope();
560 let focus = Rc::new(RefCell::new(false));
561 request_focus(focus.clone(), mock_handler(), 0);
562 assert!(*focus.borrow());
563 clear_focus();
564 }
565
566 #[test]
567 fn request_focus_clears_previous() {
568 let _app_context = crate::render_state::app_context_test_scope();
569 let focus1 = Rc::new(RefCell::new(false));
570 let focus2 = Rc::new(RefCell::new(false));
571
572 request_focus(focus1.clone(), mock_handler(), 0);
573 assert!(*focus1.borrow());
574
575 request_focus(focus2.clone(), mock_handler(), 0);
576 assert!(!*focus1.borrow());
577 assert!(*focus2.borrow());
578 clear_focus();
579 }
580
581 #[test]
582 fn clear_focus_unfocuses_current() {
583 let _app_context = crate::render_state::app_context_test_scope();
584 let focus = Rc::new(RefCell::new(false));
585 request_focus(focus.clone(), mock_handler(), 0);
586 assert!(*focus.borrow());
587
588 clear_focus();
589 assert!(!*focus.borrow());
590 }
591
592 #[derive(Default)]
593 struct DispatchRecordingHandler {
594 key_count: Cell<usize>,
595 insert_count: Cell<usize>,
596 delete_count: Cell<usize>,
597 copy_count: Cell<usize>,
598 cut_count: Cell<usize>,
599 preedit_count: Cell<usize>,
600 last_delete: Cell<Option<(usize, usize)>>,
601 }
602
603 impl DispatchRecordingHandler {
604 fn bump(cell: &Cell<usize>) {
605 cell.set(cell.get() + 1);
606 }
607
608 fn total_calls(&self) -> usize {
609 self.key_count.get()
610 + self.insert_count.get()
611 + self.delete_count.get()
612 + self.copy_count.get()
613 + self.cut_count.get()
614 + self.preedit_count.get()
615 }
616 }
617
618 impl FocusedTextFieldHandler for DispatchRecordingHandler {
619 fn handle_key(&self, _: &KeyEvent) -> bool {
620 Self::bump(&self.key_count);
621 true
622 }
623
624 fn insert_text(&self, _: &str) {
625 Self::bump(&self.insert_count);
626 }
627
628 fn delete_surrounding(&self, before_bytes: usize, after_bytes: usize) {
629 Self::bump(&self.delete_count);
630 self.last_delete.set(Some((before_bytes, after_bytes)));
631 }
632
633 fn copy_selection(&self) -> Option<String> {
634 Self::bump(&self.copy_count);
635 Some("copy".to_string())
636 }
637
638 fn cut_selection(&self) -> Option<String> {
639 Self::bump(&self.cut_count);
640 Some("cut".to_string())
641 }
642
643 fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {
644 Self::bump(&self.preedit_count);
645 }
646 }
647
648 #[test]
649 fn dispatch_delete_surrounding_calls_handler() {
650 let _app_context = crate::render_state::app_context_test_scope();
651 let focus = Rc::new(RefCell::new(false));
652 let handler = Rc::new(DispatchRecordingHandler::default());
653
654 request_focus(Rc::clone(&focus), handler.clone(), 0);
655 assert!(dispatch_delete_surrounding(3, 1));
656 assert_eq!(handler.last_delete.get(), Some((3, 1)));
657
658 clear_focus();
659 }
660
661 #[test]
662 fn dispatch_clears_stale_focus_owner_before_invoking_handler() {
663 let _app_context = crate::render_state::app_context_test_scope();
664 let handler = Rc::new(DispatchRecordingHandler::default());
665
666 {
667 let focus = Rc::new(RefCell::new(false));
668 request_focus(Rc::clone(&focus), handler.clone(), 0);
669 assert!(has_focused_field());
670 }
671
672 let key_event = KeyEvent::key_down(crate::key_event::KeyCode::A, "a");
673
674 assert!(!dispatch_key_event(&key_event));
675 assert!(!dispatch_paste("stale paste"));
676 assert!(!dispatch_delete_surrounding(2, 1));
677 assert_eq!(dispatch_copy(), None);
678 assert_eq!(dispatch_cut(), None);
679 assert!(!dispatch_ime_preedit("preedit", Some((1, 1))));
680 assert!(!has_focused_field());
681 assert_eq!(
682 handler.total_calls(),
683 0,
684 "stale focused-field handlers must not receive input"
685 );
686 }
687
688 #[test]
689 fn stale_focus_cleanup_after_hidden_blink_does_not_reenter_the_focus_registry_borrow() {
690 let _app_context = crate::render_state::app_context_test_scope();
691
692 let focus = Rc::new(RefCell::new(false));
693 request_focus(focus.clone(), Rc::new(NodeBackedHandler(3)), 0);
694
695 let past_interval = web_time::Instant::now()
696 + crate::cursor_animation::CursorAnimationState::BLINK_INTERVAL
697 + std::time::Duration::from_millis(1);
698 assert!(
699 crate::cursor_animation::tick_cursor_blink_at(past_interval),
700 "the blink must already have toggled to hidden, matching the real \
701 timing where the bug's stop_cursor_blink() call is a visibility \
702 change and therefore reaches invalidate_focused_caret()"
703 );
704
705 drop(focus);
706
707 assert!(
708 !has_focused_field(),
709 "a field dropped without clear_focus must read back as unfocused \
710 instead of panicking on a reentrant borrow of focused_field"
711 );
712 }
713
714 #[test]
715 fn stale_focus_cleanup_repasses_the_node_that_lost_its_caret() {
716 let _app_context = crate::render_state::app_context_test_scope();
717 let _ = crate::render_state::take_draw_repass_nodes();
718
719 let focus = Rc::new(RefCell::new(false));
720 request_focus(focus.clone(), Rc::new(NodeBackedHandler(11)), 0);
721 let _ = crate::render_state::take_draw_repass_nodes();
722
723 drop(focus);
724
725 assert!(!has_focused_field());
726 assert!(
727 crate::render_state::take_draw_repass_nodes().contains(&11),
728 "discovering a stale field lazily must repass its node just like \
729 an explicit clear_focus does, or its caret is left stale on screen"
730 );
731 }
732
733 #[derive(Default)]
734 struct KeyboardProbe {
735 calls: RefCell<Vec<&'static str>>,
736 }
737
738 impl crate::text_input_session::PlatformTextInputHandler for KeyboardProbe {
739 fn show_keyboard(&self) {
740 self.calls.borrow_mut().push("show");
741 }
742
743 fn hide_keyboard(&self) {
744 self.calls.borrow_mut().push("hide");
745 }
746 }
747
748 #[test]
749 fn focus_transitions_drive_platform_keyboard() {
750 let _app_context = crate::render_state::app_context_test_scope();
751 let keyboard = Rc::new(KeyboardProbe::default());
752 crate::text_input_session::set_platform_text_input_handler(keyboard.clone());
753
754 let focus = Rc::new(RefCell::new(false));
755 request_focus(focus.clone(), mock_handler(), 0);
756 assert_eq!(*keyboard.calls.borrow(), vec!["show"]);
757
758 request_focus(focus, mock_handler(), 0);
759 assert_eq!(*keyboard.calls.borrow(), vec!["show", "show"]);
760
761 clear_focus();
762 assert_eq!(*keyboard.calls.borrow(), vec!["show", "show", "hide"]);
763 }
764
765 #[test]
766 fn stale_focus_detection_hides_platform_keyboard() {
767 let _app_context = crate::render_state::app_context_test_scope();
768 let keyboard = Rc::new(KeyboardProbe::default());
769 crate::text_input_session::set_platform_text_input_handler(keyboard.clone());
770
771 {
772 let focus = Rc::new(RefCell::new(false));
773 request_focus(focus, mock_handler(), 0);
774 }
775
776 assert!(!has_focused_field());
777 assert_eq!(*keyboard.calls.borrow(), vec!["show", "hide"]);
778
779 assert!(!has_focused_field());
780 assert_eq!(*keyboard.calls.borrow(), vec!["show", "hide"]);
781 }
782
783 #[test]
784 fn text_field_focus_is_scoped_by_app_context() {
785 let _app_context = crate::render_state::app_context_test_scope();
786 let first = crate::render_state::AppContext::new_with_density(1.0);
787 let second = crate::render_state::AppContext::new_with_density(1.0);
788 let first_focus = Rc::new(RefCell::new(false));
789 let second_focus = Rc::new(RefCell::new(false));
790
791 first.enter(|| {
792 request_focus(first_focus.clone(), mock_handler(), 0);
793 assert!(has_focused_field());
794 assert!(*first_focus.borrow());
795 });
796
797 second.enter(|| {
798 assert!(!has_focused_field());
799 request_focus(second_focus.clone(), mock_handler(), 0);
800 assert!(has_focused_field());
801 assert!(*second_focus.borrow());
802 });
803
804 first.enter(|| {
805 assert!(has_focused_field());
806 assert!(*first_focus.borrow());
807 clear_focus();
808 assert!(!has_focused_field());
809 assert!(!*first_focus.borrow());
810 });
811
812 second.enter(|| {
813 assert!(has_focused_field());
814 assert!(*second_focus.borrow());
815 clear_focus();
816 });
817 }
818}