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