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