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/// Handler trait for focused text field operations.
16/// Stored in focus module for O(1) key/clipboard dispatch.
17pub trait FocusedTextFieldHandler {
18    /// Handle a key event. Returns true if consumed.
19    fn handle_key(&self, event: &KeyEvent) -> bool;
20    /// Insert pasted text.
21    fn insert_text(&self, text: &str);
22    /// Delete text surrounding the cursor or selection.
23    fn delete_surrounding(&self, before_bytes: usize, after_bytes: usize);
24    /// Copy current selection. Returns None if nothing selected.
25    fn copy_selection(&self) -> Option<String>;
26    /// Cut current selection (copy + delete). Returns None if nothing selected.
27    fn cut_selection(&self) -> Option<String>;
28    /// Set IME composition (preedit) state.
29    /// - `text`: The composition text being typed (empty string to clear)
30    /// - `cursor`: Optional cursor position within composition (start, end)
31    fn set_composition(&self, text: &str, cursor: Option<(usize, usize)>);
32}
33
34pub(crate) struct TextFieldFocusState {
35    focused_field: RefCell<Option<Weak<RefCell<bool>>>>,
36    focused_handler: RefCell<Option<Rc<dyn FocusedTextFieldHandler>>>,
37}
38
39impl TextFieldFocusState {
40    pub(crate) fn new() -> Self {
41        Self {
42            focused_field: RefCell::new(None),
43            focused_handler: RefCell::new(None),
44        }
45    }
46
47    fn request_focus(
48        &self,
49        is_focused: Rc<RefCell<bool>>,
50        handler: Rc<dyn FocusedTextFieldHandler>,
51    ) {
52        let mut current = self.focused_field.borrow_mut();
53
54        if let Some(ref weak) = *current {
55            if let Some(old_focused) = weak.upgrade() {
56                *old_focused.borrow_mut() = false;
57            }
58        }
59
60        *is_focused.borrow_mut() = true;
61        *current = Some(Rc::downgrade(&is_focused));
62        *self.focused_handler.borrow_mut() = Some(handler);
63    }
64
65    fn clear_focus(&self) {
66        let mut current = self.focused_field.borrow_mut();
67
68        if let Some(ref weak) = *current {
69            if let Some(focused) = weak.upgrade() {
70                *focused.borrow_mut() = false;
71            }
72        }
73
74        *current = None;
75        *self.focused_handler.borrow_mut() = None;
76    }
77
78    fn has_focused_field(&self) -> bool {
79        let mut current = self.focused_field.borrow_mut();
80        if let Some(ref weak) = *current {
81            if weak.upgrade().is_some() {
82                return true;
83            }
84            *current = None;
85            *self.focused_handler.borrow_mut() = None;
86            crate::cursor_animation::stop_cursor_blink();
87        }
88        false
89    }
90
91    fn focused_handler(&self) -> Option<Rc<dyn FocusedTextFieldHandler>> {
92        if !self.has_focused_field() {
93            return None;
94        }
95        self.focused_handler.borrow().as_ref().cloned()
96    }
97
98    fn dispatch_key_event(&self, event: &KeyEvent) -> bool {
99        if let Some(handler) = self.focused_handler() {
100            handler.handle_key(event)
101        } else {
102            false
103        }
104    }
105
106    fn dispatch_paste(&self, text: &str) -> bool {
107        if let Some(handler) = self.focused_handler() {
108            handler.insert_text(text);
109            true
110        } else {
111            false
112        }
113    }
114
115    fn dispatch_delete_surrounding(&self, before_bytes: usize, after_bytes: usize) -> bool {
116        if let Some(handler) = self.focused_handler() {
117            handler.delete_surrounding(before_bytes, after_bytes);
118            true
119        } else {
120            false
121        }
122    }
123
124    fn dispatch_copy(&self) -> Option<String> {
125        self.focused_handler()
126            .and_then(|handler| handler.copy_selection())
127    }
128
129    fn dispatch_cut(&self) -> Option<String> {
130        self.focused_handler()
131            .and_then(|handler| handler.cut_selection())
132    }
133
134    fn dispatch_ime_preedit(&self, text: &str, cursor: Option<(usize, usize)>) -> bool {
135        if let Some(handler) = self.focused_handler() {
136            handler.set_composition(text, cursor);
137            true
138        } else {
139            false
140        }
141    }
142}
143
144/// Requests focus for a text field.
145///
146/// If another text field was previously focused, it will be unfocused first.
147/// The provided `is_focused` handle should be the field's focus state.
148/// The handler is stored for O(1) key dispatch.
149pub fn request_focus(is_focused: Rc<RefCell<bool>>, handler: Rc<dyn FocusedTextFieldHandler>) {
150    crate::render_state::with_text_field_focus(|state| state.request_focus(is_focused, handler));
151
152    // Start cursor blink animation (timer-based, not continuous redraw)
153    crate::cursor_animation::start_cursor_blink();
154
155    // Only render invalidation needed - cursor is drawn via create_draw_closure()
156    // which checks focus at draw time. No layout change occurs on focus.
157    crate::request_render_invalidation();
158}
159
160/// Clears focus from the currently focused text field.
161pub fn clear_focus() {
162    crate::render_state::with_text_field_focus(|state| state.clear_focus());
163
164    // Stop cursor blink animation
165    crate::cursor_animation::stop_cursor_blink();
166
167    crate::request_render_invalidation();
168}
169
170/// Returns true if any text field currently has focus.
171/// Checks weak ref liveness and clears stale focus state.
172pub fn has_focused_field() -> bool {
173    crate::render_state::with_text_field_focus(|state| state.has_focused_field())
174}
175
176// ============================================================================
177// O(1) Dispatch Functions - Bypass tree scan by using stored handler
178// ============================================================================
179
180/// Dispatches a key event to the focused text field. Returns true if consumed.
181/// O(1) operation using stored handler.
182pub fn dispatch_key_event(event: &KeyEvent) -> bool {
183    crate::render_state::with_text_field_focus(|state| state.dispatch_key_event(event))
184}
185
186/// Inserts text into the focused text field (paste operation).
187/// O(1) operation using stored handler.
188pub fn dispatch_paste(text: &str) -> bool {
189    crate::render_state::with_text_field_focus(|state| state.dispatch_paste(text))
190}
191
192/// Deletes text surrounding the cursor or selection.
193/// O(1) operation using stored handler.
194pub fn dispatch_delete_surrounding(before_bytes: usize, after_bytes: usize) -> bool {
195    crate::render_state::with_text_field_focus(|state| {
196        state.dispatch_delete_surrounding(before_bytes, after_bytes)
197    })
198}
199
200/// Copies selection from focused text field.
201/// O(1) operation using stored handler.
202pub fn dispatch_copy() -> Option<String> {
203    crate::render_state::with_text_field_focus(|state| state.dispatch_copy())
204}
205
206/// Cuts selection from focused text field (copy + delete).
207/// O(1) operation using stored handler.
208pub fn dispatch_cut() -> Option<String> {
209    crate::render_state::with_text_field_focus(|state| state.dispatch_cut())
210}
211
212/// Dispatches IME preedit (composition) state to the focused text field.
213/// O(1) operation using stored handler.
214/// Returns true if a text field was focused and received the event.
215pub fn dispatch_ime_preedit(text: &str, cursor: Option<(usize, usize)>) -> bool {
216    crate::render_state::with_text_field_focus(|state| state.dispatch_ime_preedit(text, cursor))
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use std::cell::Cell;
223
224    // Mock handler for testing
225    struct MockHandler;
226    impl FocusedTextFieldHandler for MockHandler {
227        fn handle_key(&self, _: &KeyEvent) -> bool {
228            false
229        }
230        fn insert_text(&self, _: &str) {}
231        fn delete_surrounding(&self, _: usize, _: usize) {}
232        fn copy_selection(&self) -> Option<String> {
233            None
234        }
235        fn cut_selection(&self) -> Option<String> {
236            None
237        }
238        fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {}
239    }
240
241    fn mock_handler() -> Rc<dyn FocusedTextFieldHandler> {
242        Rc::new(MockHandler)
243    }
244
245    #[test]
246    fn request_focus_sets_flag() {
247        let _app_context = crate::render_state::app_context_test_scope();
248        let focus = Rc::new(RefCell::new(false));
249        request_focus(focus.clone(), mock_handler());
250        assert!(*focus.borrow());
251        clear_focus();
252    }
253
254    #[test]
255    fn request_focus_clears_previous() {
256        let _app_context = crate::render_state::app_context_test_scope();
257        let focus1 = Rc::new(RefCell::new(false));
258        let focus2 = Rc::new(RefCell::new(false));
259
260        request_focus(focus1.clone(), mock_handler());
261        assert!(*focus1.borrow());
262
263        request_focus(focus2.clone(), mock_handler());
264        assert!(!*focus1.borrow()); // First should be unfocused
265        assert!(*focus2.borrow()); // Second should be focused
266        clear_focus();
267    }
268
269    #[test]
270    fn clear_focus_unfocuses_current() {
271        let _app_context = crate::render_state::app_context_test_scope();
272        let focus = Rc::new(RefCell::new(false));
273        request_focus(focus.clone(), mock_handler());
274        assert!(*focus.borrow());
275
276        clear_focus();
277        assert!(!*focus.borrow());
278    }
279
280    #[derive(Default)]
281    struct DispatchRecordingHandler {
282        key_count: Cell<usize>,
283        insert_count: Cell<usize>,
284        delete_count: Cell<usize>,
285        copy_count: Cell<usize>,
286        cut_count: Cell<usize>,
287        preedit_count: Cell<usize>,
288        last_delete: Cell<Option<(usize, usize)>>,
289    }
290
291    impl DispatchRecordingHandler {
292        fn bump(cell: &Cell<usize>) {
293            cell.set(cell.get() + 1);
294        }
295
296        fn total_calls(&self) -> usize {
297            self.key_count.get()
298                + self.insert_count.get()
299                + self.delete_count.get()
300                + self.copy_count.get()
301                + self.cut_count.get()
302                + self.preedit_count.get()
303        }
304    }
305
306    impl FocusedTextFieldHandler for DispatchRecordingHandler {
307        fn handle_key(&self, _: &KeyEvent) -> bool {
308            Self::bump(&self.key_count);
309            true
310        }
311
312        fn insert_text(&self, _: &str) {
313            Self::bump(&self.insert_count);
314        }
315
316        fn delete_surrounding(&self, before_bytes: usize, after_bytes: usize) {
317            Self::bump(&self.delete_count);
318            self.last_delete.set(Some((before_bytes, after_bytes)));
319        }
320
321        fn copy_selection(&self) -> Option<String> {
322            Self::bump(&self.copy_count);
323            Some("copy".to_string())
324        }
325
326        fn cut_selection(&self) -> Option<String> {
327            Self::bump(&self.cut_count);
328            Some("cut".to_string())
329        }
330
331        fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {
332            Self::bump(&self.preedit_count);
333        }
334    }
335
336    #[test]
337    fn dispatch_delete_surrounding_calls_handler() {
338        let _app_context = crate::render_state::app_context_test_scope();
339        let focus = Rc::new(RefCell::new(false));
340        let handler = Rc::new(DispatchRecordingHandler::default());
341
342        request_focus(Rc::clone(&focus), handler.clone());
343        assert!(dispatch_delete_surrounding(3, 1));
344        assert_eq!(handler.last_delete.get(), Some((3, 1)));
345
346        clear_focus();
347    }
348
349    #[test]
350    fn dispatch_clears_stale_focus_owner_before_invoking_handler() {
351        let _app_context = crate::render_state::app_context_test_scope();
352        let handler = Rc::new(DispatchRecordingHandler::default());
353
354        {
355            let focus = Rc::new(RefCell::new(false));
356            request_focus(Rc::clone(&focus), handler.clone());
357            assert!(has_focused_field());
358        }
359
360        let key_event = KeyEvent::key_down(crate::key_event::KeyCode::A, "a");
361
362        assert!(!dispatch_key_event(&key_event));
363        assert!(!dispatch_paste("stale paste"));
364        assert!(!dispatch_delete_surrounding(2, 1));
365        assert_eq!(dispatch_copy(), None);
366        assert_eq!(dispatch_cut(), None);
367        assert!(!dispatch_ime_preedit("preedit", Some((1, 1))));
368        assert!(!has_focused_field());
369        assert_eq!(
370            handler.total_calls(),
371            0,
372            "stale focused-field handlers must not receive input"
373        );
374    }
375
376    #[test]
377    fn text_field_focus_is_scoped_by_app_context() {
378        let _app_context = crate::render_state::app_context_test_scope();
379        let first = crate::render_state::AppContext::new_with_density(1.0);
380        let second = crate::render_state::AppContext::new_with_density(1.0);
381        let first_focus = Rc::new(RefCell::new(false));
382        let second_focus = Rc::new(RefCell::new(false));
383
384        first.enter(|| {
385            request_focus(first_focus.clone(), mock_handler());
386            assert!(has_focused_field());
387            assert!(*first_focus.borrow());
388        });
389
390        second.enter(|| {
391            assert!(!has_focused_field());
392            request_focus(second_focus.clone(), mock_handler());
393            assert!(has_focused_field());
394            assert!(*second_focus.borrow());
395        });
396
397        first.enter(|| {
398            assert!(has_focused_field());
399            assert!(*first_focus.borrow());
400            clear_focus();
401            assert!(!has_focused_field());
402            assert!(!*first_focus.borrow());
403        });
404
405        second.enter(|| {
406            assert!(has_focused_field());
407            assert!(*second_focus.borrow());
408            clear_focus();
409        });
410    }
411}