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