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