Skip to main content

cranpose_ui/
text_field_focus.rs

1//! Focus manager for text fields.
2//!
3//! This module tracks which text field currently has focus, ensuring only one
4//! text field is focused at a time. When a new field requests focus, the
5//! previously focused field is automatically unfocused.
6//!
7//! O(1) key dispatch: The focused field's handler is stored for direct invocation,
8//! avoiding O(N) tree scans on every keystroke.
9
10use std::{
11    cell::{Cell, RefCell},
12    rc::{Rc, Weak},
13};
14
15use crate::key_event::KeyEvent;
16
17/// Snapshot of the focused text field's editable state for platform IMEs.
18///
19/// All offsets are UTF-8 byte offsets into `text`. Platform layers that talk
20/// to UTF-16 based IMEs (Android's `InputConnection`, web composition events)
21/// convert on their side of the boundary.
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct ImeEditorState {
24    /// Full text content of the field.
25    pub text: String,
26    /// Selection start (min) in bytes. Equal to `selection_end` for a caret.
27    pub selection_start: usize,
28    /// Selection end (max) in bytes.
29    pub selection_end: usize,
30    /// Active composing (preedit) region in bytes, if any.
31    pub composition: Option<(usize, usize)>,
32    /// Whether the field is single-line (platforms use this to pick the
33    /// keyboard action, e.g. Android `IME_ACTION_DONE` vs newline).
34    pub single_line: bool,
35}
36
37/// Window-space caret geometry for the focused field, used by platforms whose
38/// native text input positions the caret by coordinates rather than key events
39/// (iOS `UITextInput`: spacebar-trackpad cursor movement and tap-to-position).
40/// All values are logical pixels in window space.
41#[derive(Clone, Debug, PartialEq)]
42pub struct ImeCaretGeometry {
43    /// Caret x for each character-boundary offset, in text order: entry `k` is
44    /// the caret after `k` characters, so `caret_xs.len() == chars + 1`.
45    pub caret_xs: Vec<f32>,
46    /// Top y of the (single) caret line.
47    pub top: f32,
48    /// Height of one line (the caret's height).
49    pub line_height: f32,
50}
51
52/// Handler trait for focused text field operations.
53/// Stored in focus module for O(1) key/clipboard dispatch.
54pub trait FocusedTextFieldHandler {
55    /// Handle a key event. Returns true if consumed.
56    fn handle_key(&self, event: &KeyEvent) -> bool;
57    /// Insert pasted text.
58    fn insert_text(&self, text: &str);
59    /// Delete text surrounding the cursor or selection.
60    fn delete_surrounding(&self, before_bytes: usize, after_bytes: usize);
61    /// Copy current selection. Returns None if nothing selected.
62    fn copy_selection(&self) -> Option<String>;
63    /// Cut current selection (copy + delete). Returns None if nothing selected.
64    fn cut_selection(&self) -> Option<String>;
65    /// Selects all the field's text (contextual-menu "Select all").
66    fn select_all(&self) {}
67    /// Set IME composition (preedit) state.
68    /// - `text`: The composition text being typed (empty string to clear,
69    ///   which *deletes* the preedit text)
70    /// - `cursor`: Optional cursor position within composition (start, end)
71    fn set_composition(&self, text: &str, cursor: Option<(usize, usize)>);
72    /// Finish the active composition, keeping the composed text as regular
73    /// committed text (Android `finishComposingText` semantics). No-op when
74    /// no composition is active.
75    fn finish_composition(&self) {}
76    /// Mark existing text as the composing region without changing it
77    /// (Android `setComposingRegion` semantics, used by autocorrect to
78    /// re-compose an already committed word). Offsets are UTF-8 bytes;
79    /// implementations clamp them to valid character boundaries.
80    fn set_composing_region(&self, start_bytes: usize, end_bytes: usize) {
81        let _ = (start_bytes, end_bytes);
82    }
83    /// Move the selection/caret to `[start_bytes, end_bytes)` without editing
84    /// text (Android `InputConnection.setSelection` semantics). Used by
85    /// Gboard's spacebar-swipe cursor control, which scrubs the caret by
86    /// repeatedly setting the selection. Offsets are UTF-8 bytes; implementations
87    /// clamp them to valid character boundaries.
88    fn set_selection(&self, start_bytes: usize, end_bytes: usize) {
89        let _ = (start_bytes, end_bytes);
90    }
91    /// Snapshot of the current editable state for platform IMEs
92    /// (`InputConnection` text queries, session seeding). `None` when the
93    /// handler cannot expose its state.
94    fn editor_state(&self) -> Option<ImeEditorState> {
95        None
96    }
97    /// Window-space caret geometry (see [`ImeCaretGeometry`]) for coordinate-based
98    /// platform text input. `None` when the handler cannot expose it (e.g. the
99    /// field has not been laid out yet).
100    fn caret_geometry(&self) -> Option<ImeCaretGeometry> {
101        None
102    }
103}
104
105pub(crate) struct TextFieldFocusState {
106    focused_field: RefCell<Option<Weak<RefCell<bool>>>>,
107    focused_handler: RefCell<Option<Rc<dyn FocusedTextFieldHandler>>>,
108    /// The [`crate::modal::local_modal_depth`] the currently focused field was
109    /// composed at, so a closing modal can tell whether the field it guarded
110    /// is the one that just went away (see [`Self::focused_at_depth`]).
111    focused_modal_depth: Cell<usize>,
112}
113
114impl TextFieldFocusState {
115    pub(crate) fn new() -> Self {
116        Self {
117            focused_field: RefCell::new(None),
118            focused_handler: RefCell::new(None),
119            focused_modal_depth: Cell::new(0),
120        }
121    }
122
123    fn request_focus(
124        &self,
125        is_focused: Rc<RefCell<bool>>,
126        handler: Rc<dyn FocusedTextFieldHandler>,
127        modal_depth: usize,
128    ) {
129        let mut current = self.focused_field.borrow_mut();
130
131        if let Some(ref weak) = *current
132            && let Some(old_focused) = weak.upgrade()
133        {
134            *old_focused.borrow_mut() = false;
135        }
136
137        *is_focused.borrow_mut() = true;
138        *current = Some(Rc::downgrade(&is_focused));
139        *self.focused_handler.borrow_mut() = Some(handler);
140        self.focused_modal_depth.set(modal_depth);
141    }
142
143    fn clear_focus(&self) {
144        let mut current = self.focused_field.borrow_mut();
145
146        if let Some(ref weak) = *current
147            && let Some(focused) = weak.upgrade()
148        {
149            *focused.borrow_mut() = false;
150        }
151
152        *current = None;
153        *self.focused_handler.borrow_mut() = None;
154        self.focused_modal_depth.set(0);
155    }
156
157    /// Whether a live field is focused and was composed at exactly `depth`.
158    fn focused_at_depth(&self, depth: usize) -> bool {
159        self.has_focused_field() && self.focused_modal_depth.get() == depth
160    }
161
162    fn has_focused_field(&self) -> bool {
163        let mut current = self.focused_field.borrow_mut();
164        if let Some(ref weak) = *current {
165            if weak.upgrade().is_some() {
166                return true;
167            }
168            *current = None;
169            *self.focused_handler.borrow_mut() = None;
170            crate::cursor_animation::stop_cursor_blink();
171        }
172        false
173    }
174
175    fn focused_handler(&self) -> Option<Rc<dyn FocusedTextFieldHandler>> {
176        if !self.has_focused_field() {
177            return None;
178        }
179        self.focused_handler.borrow().as_ref().cloned()
180    }
181
182    fn dispatch_key_event(&self, event: &KeyEvent) -> bool {
183        if let Some(handler) = self.focused_handler() {
184            handler.handle_key(event)
185        } else {
186            false
187        }
188    }
189
190    fn dispatch_paste(&self, text: &str) -> bool {
191        if let Some(handler) = self.focused_handler() {
192            handler.insert_text(text);
193            true
194        } else {
195            false
196        }
197    }
198
199    fn dispatch_delete_surrounding(&self, before_bytes: usize, after_bytes: usize) -> bool {
200        if let Some(handler) = self.focused_handler() {
201            handler.delete_surrounding(before_bytes, after_bytes);
202            true
203        } else {
204            false
205        }
206    }
207
208    fn dispatch_copy(&self) -> Option<String> {
209        self.focused_handler()
210            .and_then(|handler| handler.copy_selection())
211    }
212
213    fn dispatch_cut(&self) -> Option<String> {
214        self.focused_handler()
215            .and_then(|handler| handler.cut_selection())
216    }
217
218    fn dispatch_select_all(&self) -> bool {
219        if let Some(handler) = self.focused_handler() {
220            handler.select_all();
221            true
222        } else {
223            false
224        }
225    }
226
227    fn dispatch_ime_preedit(&self, text: &str, cursor: Option<(usize, usize)>) -> bool {
228        if let Some(handler) = self.focused_handler() {
229            handler.set_composition(text, cursor);
230            true
231        } else {
232            false
233        }
234    }
235
236    fn dispatch_ime_finish_composing(&self) -> bool {
237        if let Some(handler) = self.focused_handler() {
238            handler.finish_composition();
239            true
240        } else {
241            false
242        }
243    }
244
245    fn dispatch_ime_set_composing_region(&self, start_bytes: usize, end_bytes: usize) -> bool {
246        if let Some(handler) = self.focused_handler() {
247            handler.set_composing_region(start_bytes, end_bytes);
248            true
249        } else {
250            false
251        }
252    }
253
254    fn dispatch_ime_set_selection(&self, start_bytes: usize, end_bytes: usize) -> bool {
255        if let Some(handler) = self.focused_handler() {
256            handler.set_selection(start_bytes, end_bytes);
257            true
258        } else {
259            false
260        }
261    }
262
263    fn focused_editor_state(&self) -> Option<ImeEditorState> {
264        self.focused_handler()
265            .and_then(|handler| handler.editor_state())
266    }
267
268    fn focused_caret_geometry(&self) -> Option<ImeCaretGeometry> {
269        self.focused_handler()
270            .and_then(|handler| handler.caret_geometry())
271    }
272}
273
274/// Requests focus for a text field composed at [`crate::modal::local_modal_depth`]
275/// `modal_depth`.
276///
277/// Refused (a no-op) when `modal_depth` is shallower than the modal depth
278/// that is open right now — a field behind an open dialog must not be
279/// able to steal focus from it. A field inside the innermost dialog (or
280/// outside any dialog, while none is open) has `modal_depth` equal to the
281/// current modal depth and is granted focus normally.
282///
283/// If another text field was previously focused, it will be unfocused first.
284/// The provided `is_focused` handle should be the field's focus state.
285/// The handler is stored for O(1) key dispatch.
286pub fn request_focus(
287    is_focused: Rc<RefCell<bool>>,
288    handler: Rc<dyn FocusedTextFieldHandler>,
289    modal_depth: usize,
290) {
291    if modal_depth < crate::modal::current_modal_depth() {
292        return;
293    }
294
295    crate::render_state::with_text_field_focus(|state| {
296        state.request_focus(is_focused, handler, modal_depth)
297    });
298
299    // Start cursor blink animation (timer-based, not continuous redraw)
300    crate::cursor_animation::start_cursor_blink();
301
302    // Tell the platform to show its soft keyboard (fires on every focus
303    // request on purpose - see text_input_session module docs).
304    crate::text_input_session::notify_text_input_focus_gained();
305
306    // Only render invalidation needed - cursor is drawn via create_draw_closure()
307    // which checks focus at draw time. No layout change occurs on focus.
308    crate::request_render_invalidation();
309}
310
311/// Clears focus if the currently focused field was composed at exactly
312/// `depth` — called when the modal that occupied `depth` closes, so a field
313/// that went away with it does not strand the platform keyboard open. A
314/// field at another depth (outside that modal, or inside an unrelated one)
315/// is untouched.
316pub(crate) fn clear_focus_for_closed_modal(depth: usize) {
317    let owns_focus =
318        crate::render_state::with_text_field_focus(|state| state.focused_at_depth(depth));
319    if owns_focus {
320        clear_focus();
321    }
322}
323
324/// Clears focus from the currently focused text field.
325pub fn clear_focus() {
326    crate::render_state::with_text_field_focus(|state| state.clear_focus());
327
328    // Stop cursor blink animation
329    crate::cursor_animation::stop_cursor_blink();
330
331    // Tell the platform to hide its soft keyboard.
332    crate::text_input_session::notify_text_input_focus_lost();
333
334    crate::request_render_invalidation();
335}
336
337/// Returns true if any text field currently has focus.
338/// Checks weak ref liveness and clears stale focus state.
339pub fn has_focused_field() -> bool {
340    let has_focus = crate::render_state::with_text_field_focus(|state| state.has_focused_field());
341    if !has_focus {
342        // The focused field may have just been detected as stale (removed from
343        // the composition without clear_focus). Hide the soft keyboard; this
344        // is a gated no-op when no keyboard request is outstanding.
345        crate::text_input_session::notify_text_input_focus_lost();
346    }
347    has_focus
348}
349
350// ============================================================================
351// O(1) Dispatch Functions - Bypass tree scan by using stored handler
352// ============================================================================
353
354/// Dispatches a key event to the focused text field. Returns true if consumed.
355/// O(1) operation using stored handler.
356pub fn dispatch_key_event(event: &KeyEvent) -> bool {
357    crate::render_state::with_text_field_focus(|state| state.dispatch_key_event(event))
358}
359
360/// Inserts text into the focused text field (paste operation).
361/// O(1) operation using stored handler.
362pub fn dispatch_paste(text: &str) -> bool {
363    crate::render_state::with_text_field_focus(|state| state.dispatch_paste(text))
364}
365
366/// Deletes text surrounding the cursor or selection.
367/// O(1) operation using stored handler.
368pub fn dispatch_delete_surrounding(before_bytes: usize, after_bytes: usize) -> bool {
369    crate::render_state::with_text_field_focus(|state| {
370        state.dispatch_delete_surrounding(before_bytes, after_bytes)
371    })
372}
373
374/// Copies selection from focused text field.
375/// O(1) operation using stored handler.
376pub fn dispatch_copy() -> Option<String> {
377    crate::render_state::with_text_field_focus(|state| state.dispatch_copy())
378}
379
380/// Cuts selection from focused text field (copy + delete).
381/// O(1) operation using stored handler.
382pub fn dispatch_cut() -> Option<String> {
383    crate::render_state::with_text_field_focus(|state| state.dispatch_cut())
384}
385
386/// Selects all the text in the focused text field (contextual-menu "Select
387/// all"). Returns true if a text field was focused.
388pub fn dispatch_select_all() -> bool {
389    crate::render_state::with_text_field_focus(|state| state.dispatch_select_all())
390}
391
392/// Dispatches IME preedit (composition) state to the focused text field.
393/// O(1) operation using stored handler.
394/// Returns true if a text field was focused and received the event.
395pub fn dispatch_ime_preedit(text: &str, cursor: Option<(usize, usize)>) -> bool {
396    crate::render_state::with_text_field_focus(|state| state.dispatch_ime_preedit(text, cursor))
397}
398
399/// Finishes the active composition in the focused text field, keeping the
400/// composed text (Android `finishComposingText` semantics).
401/// Returns true if a text field was focused and received the event.
402pub fn dispatch_ime_finish_composing() -> bool {
403    crate::render_state::with_text_field_focus(|state| state.dispatch_ime_finish_composing())
404}
405
406/// Marks existing text in the focused field as the composing region without
407/// changing it (Android `setComposingRegion` semantics).
408/// Returns true if a text field was focused and received the event.
409pub fn dispatch_ime_set_composing_region(start_bytes: usize, end_bytes: usize) -> bool {
410    crate::render_state::with_text_field_focus(|state| {
411        state.dispatch_ime_set_composing_region(start_bytes, end_bytes)
412    })
413}
414
415/// Moves the focused field's selection/caret to `[start_bytes, end_bytes)`
416/// without editing text (Android `setSelection` semantics; the path Gboard's
417/// spacebar-swipe uses to scrub the cursor).
418/// Returns true if a text field was focused and received the event.
419pub fn dispatch_ime_set_selection(start_bytes: usize, end_bytes: usize) -> bool {
420    crate::render_state::with_text_field_focus(|state| {
421        state.dispatch_ime_set_selection(start_bytes, end_bytes)
422    })
423}
424
425/// Returns a snapshot of the focused text field's editable state for
426/// platform IMEs, or `None` when no field is focused (or the handler does
427/// not expose its state).
428pub fn focused_editor_state() -> Option<ImeEditorState> {
429    crate::render_state::with_text_field_focus(|state| state.focused_editor_state())
430}
431
432/// Window-space caret geometry of the focused field (see [`ImeCaretGeometry`]),
433/// or `None` when no field is focused or it exposes no geometry.
434pub fn focused_caret_geometry() -> Option<ImeCaretGeometry> {
435    crate::render_state::with_text_field_focus(|state| state.focused_caret_geometry())
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    // Mock handler for testing
443    struct MockHandler;
444    impl FocusedTextFieldHandler for MockHandler {
445        fn handle_key(&self, _: &KeyEvent) -> bool {
446            false
447        }
448        fn insert_text(&self, _: &str) {}
449        fn delete_surrounding(&self, _: usize, _: usize) {}
450        fn copy_selection(&self) -> Option<String> {
451            None
452        }
453        fn cut_selection(&self) -> Option<String> {
454            None
455        }
456        fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {}
457    }
458
459    fn mock_handler() -> Rc<dyn FocusedTextFieldHandler> {
460        Rc::new(MockHandler)
461    }
462
463    #[test]
464    fn request_focus_sets_flag() {
465        let _app_context = crate::render_state::app_context_test_scope();
466        let focus = Rc::new(RefCell::new(false));
467        request_focus(focus.clone(), mock_handler(), 0);
468        assert!(*focus.borrow());
469        clear_focus();
470    }
471
472    #[test]
473    fn request_focus_clears_previous() {
474        let _app_context = crate::render_state::app_context_test_scope();
475        let focus1 = Rc::new(RefCell::new(false));
476        let focus2 = Rc::new(RefCell::new(false));
477
478        request_focus(focus1.clone(), mock_handler(), 0);
479        assert!(*focus1.borrow());
480
481        request_focus(focus2.clone(), mock_handler(), 0);
482        assert!(!*focus1.borrow()); // First should be unfocused
483        assert!(*focus2.borrow()); // Second should be focused
484        clear_focus();
485    }
486
487    #[test]
488    fn clear_focus_unfocuses_current() {
489        let _app_context = crate::render_state::app_context_test_scope();
490        let focus = Rc::new(RefCell::new(false));
491        request_focus(focus.clone(), mock_handler(), 0);
492        assert!(*focus.borrow());
493
494        clear_focus();
495        assert!(!*focus.borrow());
496    }
497
498    #[derive(Default)]
499    struct DispatchRecordingHandler {
500        key_count: Cell<usize>,
501        insert_count: Cell<usize>,
502        delete_count: Cell<usize>,
503        copy_count: Cell<usize>,
504        cut_count: Cell<usize>,
505        preedit_count: Cell<usize>,
506        last_delete: Cell<Option<(usize, usize)>>,
507    }
508
509    impl DispatchRecordingHandler {
510        fn bump(cell: &Cell<usize>) {
511            cell.set(cell.get() + 1);
512        }
513
514        fn total_calls(&self) -> usize {
515            self.key_count.get()
516                + self.insert_count.get()
517                + self.delete_count.get()
518                + self.copy_count.get()
519                + self.cut_count.get()
520                + self.preedit_count.get()
521        }
522    }
523
524    impl FocusedTextFieldHandler for DispatchRecordingHandler {
525        fn handle_key(&self, _: &KeyEvent) -> bool {
526            Self::bump(&self.key_count);
527            true
528        }
529
530        fn insert_text(&self, _: &str) {
531            Self::bump(&self.insert_count);
532        }
533
534        fn delete_surrounding(&self, before_bytes: usize, after_bytes: usize) {
535            Self::bump(&self.delete_count);
536            self.last_delete.set(Some((before_bytes, after_bytes)));
537        }
538
539        fn copy_selection(&self) -> Option<String> {
540            Self::bump(&self.copy_count);
541            Some("copy".to_string())
542        }
543
544        fn cut_selection(&self) -> Option<String> {
545            Self::bump(&self.cut_count);
546            Some("cut".to_string())
547        }
548
549        fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {
550            Self::bump(&self.preedit_count);
551        }
552    }
553
554    #[test]
555    fn dispatch_delete_surrounding_calls_handler() {
556        let _app_context = crate::render_state::app_context_test_scope();
557        let focus = Rc::new(RefCell::new(false));
558        let handler = Rc::new(DispatchRecordingHandler::default());
559
560        request_focus(Rc::clone(&focus), handler.clone(), 0);
561        assert!(dispatch_delete_surrounding(3, 1));
562        assert_eq!(handler.last_delete.get(), Some((3, 1)));
563
564        clear_focus();
565    }
566
567    #[test]
568    fn dispatch_clears_stale_focus_owner_before_invoking_handler() {
569        let _app_context = crate::render_state::app_context_test_scope();
570        let handler = Rc::new(DispatchRecordingHandler::default());
571
572        {
573            let focus = Rc::new(RefCell::new(false));
574            request_focus(Rc::clone(&focus), handler.clone(), 0);
575            assert!(has_focused_field());
576        }
577
578        let key_event = KeyEvent::key_down(crate::key_event::KeyCode::A, "a");
579
580        assert!(!dispatch_key_event(&key_event));
581        assert!(!dispatch_paste("stale paste"));
582        assert!(!dispatch_delete_surrounding(2, 1));
583        assert_eq!(dispatch_copy(), None);
584        assert_eq!(dispatch_cut(), None);
585        assert!(!dispatch_ime_preedit("preedit", Some((1, 1))));
586        assert!(!has_focused_field());
587        assert_eq!(
588            handler.total_calls(),
589            0,
590            "stale focused-field handlers must not receive input"
591        );
592    }
593
594    #[derive(Default)]
595    struct KeyboardProbe {
596        calls: RefCell<Vec<&'static str>>,
597    }
598
599    impl crate::text_input_session::PlatformTextInputHandler for KeyboardProbe {
600        fn show_keyboard(&self) {
601            self.calls.borrow_mut().push("show");
602        }
603
604        fn hide_keyboard(&self) {
605            self.calls.borrow_mut().push("hide");
606        }
607    }
608
609    #[test]
610    fn focus_transitions_drive_platform_keyboard() {
611        let _app_context = crate::render_state::app_context_test_scope();
612        let keyboard = Rc::new(KeyboardProbe::default());
613        crate::text_input_session::set_platform_text_input_handler(keyboard.clone());
614
615        let focus = Rc::new(RefCell::new(false));
616        request_focus(focus.clone(), mock_handler(), 0);
617        assert_eq!(*keyboard.calls.borrow(), vec!["show"]);
618
619        // Tapping the (already focused) field again must re-request the
620        // keyboard: the user may have dismissed it with the back gesture.
621        request_focus(focus, mock_handler(), 0);
622        assert_eq!(*keyboard.calls.borrow(), vec!["show", "show"]);
623
624        clear_focus();
625        assert_eq!(*keyboard.calls.borrow(), vec!["show", "show", "hide"]);
626    }
627
628    #[test]
629    fn stale_focus_detection_hides_platform_keyboard() {
630        let _app_context = crate::render_state::app_context_test_scope();
631        let keyboard = Rc::new(KeyboardProbe::default());
632        crate::text_input_session::set_platform_text_input_handler(keyboard.clone());
633
634        {
635            let focus = Rc::new(RefCell::new(false));
636            request_focus(focus, mock_handler(), 0);
637            // The focused field's Rc is dropped here (field removed from the
638            // composition without an explicit clear_focus).
639        }
640
641        assert!(!has_focused_field());
642        assert_eq!(*keyboard.calls.borrow(), vec!["show", "hide"]);
643
644        // Repeated stale checks must not re-hide.
645        assert!(!has_focused_field());
646        assert_eq!(*keyboard.calls.borrow(), vec!["show", "hide"]);
647    }
648
649    #[test]
650    fn text_field_focus_is_scoped_by_app_context() {
651        let _app_context = crate::render_state::app_context_test_scope();
652        let first = crate::render_state::AppContext::new_with_density(1.0);
653        let second = crate::render_state::AppContext::new_with_density(1.0);
654        let first_focus = Rc::new(RefCell::new(false));
655        let second_focus = Rc::new(RefCell::new(false));
656
657        first.enter(|| {
658            request_focus(first_focus.clone(), mock_handler(), 0);
659            assert!(has_focused_field());
660            assert!(*first_focus.borrow());
661        });
662
663        second.enter(|| {
664            assert!(!has_focused_field());
665            request_focus(second_focus.clone(), mock_handler(), 0);
666            assert!(has_focused_field());
667            assert!(*second_focus.borrow());
668        });
669
670        first.enter(|| {
671            assert!(has_focused_field());
672            assert!(*first_focus.borrow());
673            clear_focus();
674            assert!(!has_focused_field());
675            assert!(!*first_focus.borrow());
676        });
677
678        second.enter(|| {
679            assert!(has_focused_field());
680            assert!(*second_focus.borrow());
681            clear_focus();
682        });
683    }
684}