Skip to main content

cranpose_ui/
text_field_focus.rs

1//! Global 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//!
10//! ARCHITECTURE: Uses thread-local storage as the single source of truth for focus
11//! state. This is correct for single-threaded UI frameworks like this one.
12
13use std::cell::RefCell;
14use std::rc::{Rc, Weak};
15
16use crate::key_event::KeyEvent;
17
18/// Handler trait for focused text field operations.
19/// Stored in focus module for O(1) key/clipboard dispatch.
20pub trait FocusedTextFieldHandler {
21    /// Handle a key event. Returns true if consumed.
22    fn handle_key(&self, event: &KeyEvent) -> bool;
23    /// Insert pasted text.
24    fn insert_text(&self, text: &str);
25    /// Delete text surrounding the cursor or selection.
26    fn delete_surrounding(&self, before_bytes: usize, after_bytes: usize);
27    /// Copy current selection. Returns None if nothing selected.
28    fn copy_selection(&self) -> Option<String>;
29    /// Cut current selection (copy + delete). Returns None if nothing selected.
30    fn cut_selection(&self) -> Option<String>;
31    /// Set IME composition (preedit) state.
32    /// - `text`: The composition text being typed (empty string to clear)
33    /// - `cursor`: Optional cursor position within composition (start, end)
34    fn set_composition(&self, text: &str, cursor: Option<(usize, usize)>);
35}
36
37// Thread-local for focus state - the SINGLE source of truth for focus.
38// This is correct for single-threaded UI frameworks.
39thread_local! {
40    static FOCUSED_FIELD: RefCell<Option<Weak<RefCell<bool>>>> = const { RefCell::new(None) };
41    // O(1) handler for key dispatch - avoids tree scan
42    static FOCUSED_HANDLER: RefCell<Option<Rc<dyn FocusedTextFieldHandler>>> = const { RefCell::new(None) };
43}
44
45/// Requests focus for a text field.
46///
47/// If another text field was previously focused, it will be unfocused first.
48/// The provided `is_focused` handle should be the field's focus state.
49/// The handler is stored for O(1) key dispatch.
50pub fn request_focus(is_focused: Rc<RefCell<bool>>, handler: Rc<dyn FocusedTextFieldHandler>) {
51    FOCUSED_FIELD.with(|current| {
52        let mut current = current.borrow_mut();
53
54        // Unfocus the previously focused field (if any and still alive)
55        if let Some(ref weak) = *current {
56            if let Some(old_focused) = weak.upgrade() {
57                *old_focused.borrow_mut() = false;
58            }
59        }
60
61        // Set the new field as focused
62        *is_focused.borrow_mut() = true;
63        *current = Some(Rc::downgrade(&is_focused));
64    });
65
66    // Store handler for O(1) dispatch
67    FOCUSED_HANDLER.with(|h| {
68        *h.borrow_mut() = Some(handler);
69    });
70
71    // Start cursor blink animation (timer-based, not continuous redraw)
72    crate::cursor_animation::start_cursor_blink();
73
74    // Only render invalidation needed - cursor is drawn via create_draw_closure()
75    // which checks focus at draw time. No layout change occurs on focus.
76    crate::request_render_invalidation();
77}
78
79/// Clears focus from the currently focused text field.
80#[allow(dead_code)]
81pub fn clear_focus() {
82    FOCUSED_FIELD.with(|current| {
83        let mut current = current.borrow_mut();
84
85        if let Some(ref weak) = *current {
86            if let Some(focused) = weak.upgrade() {
87                *focused.borrow_mut() = false;
88            }
89        }
90
91        *current = None;
92    });
93
94    // Clear handler
95    FOCUSED_HANDLER.with(|h| {
96        *h.borrow_mut() = None;
97    });
98
99    // Stop cursor blink animation
100    crate::cursor_animation::stop_cursor_blink();
101
102    crate::request_render_invalidation();
103}
104
105/// Returns true if any text field currently has focus.
106/// Checks weak ref liveness and clears stale focus state.
107pub fn has_focused_field() -> bool {
108    FOCUSED_FIELD.with(|current| {
109        let mut borrow = current.borrow_mut();
110        if let Some(ref weak) = *borrow {
111            if weak.upgrade().is_some() {
112                return true;
113            }
114            // Weak ref is dead - clean up to prevent stuck-true
115            *borrow = None;
116            // Clear handler too
117            FOCUSED_HANDLER.with(|h| {
118                *h.borrow_mut() = None;
119            });
120            // Also stop cursor blink since focus is lost
121            crate::cursor_animation::stop_cursor_blink();
122        }
123        false
124    })
125}
126
127// ============================================================================
128// O(1) Dispatch Functions - Bypass tree scan by using stored handler
129// ============================================================================
130
131/// Dispatches a key event to the focused text field. Returns true if consumed.
132/// O(1) operation using stored handler.
133pub fn dispatch_key_event(event: &KeyEvent) -> bool {
134    FOCUSED_HANDLER.with(|h| {
135        if let Some(handler) = h.borrow().as_ref() {
136            handler.handle_key(event)
137        } else {
138            false
139        }
140    })
141}
142
143/// Inserts text into the focused text field (paste operation).
144/// O(1) operation using stored handler.
145pub fn dispatch_paste(text: &str) -> bool {
146    FOCUSED_HANDLER.with(|h| {
147        if let Some(handler) = h.borrow().as_ref() {
148            handler.insert_text(text);
149            true
150        } else {
151            false
152        }
153    })
154}
155
156/// Deletes text surrounding the cursor or selection.
157/// O(1) operation using stored handler.
158pub fn dispatch_delete_surrounding(before_bytes: usize, after_bytes: usize) -> bool {
159    FOCUSED_HANDLER.with(|h| {
160        if let Some(handler) = h.borrow().as_ref() {
161            handler.delete_surrounding(before_bytes, after_bytes);
162            true
163        } else {
164            false
165        }
166    })
167}
168
169/// Copies selection from focused text field.
170/// O(1) operation using stored handler.
171pub fn dispatch_copy() -> Option<String> {
172    FOCUSED_HANDLER.with(|h| {
173        if let Some(handler) = h.borrow().as_ref() {
174            handler.copy_selection()
175        } else {
176            None
177        }
178    })
179}
180
181/// Cuts selection from focused text field (copy + delete).
182/// O(1) operation using stored handler.
183pub fn dispatch_cut() -> Option<String> {
184    FOCUSED_HANDLER.with(|h| {
185        if let Some(handler) = h.borrow().as_ref() {
186            handler.cut_selection()
187        } else {
188            None
189        }
190    })
191}
192
193/// Dispatches IME preedit (composition) state to the focused text field.
194/// O(1) operation using stored handler.
195/// Returns true if a text field was focused and received the event.
196pub fn dispatch_ime_preedit(text: &str, cursor: Option<(usize, usize)>) -> bool {
197    FOCUSED_HANDLER.with(|h| {
198        if let Some(handler) = h.borrow().as_ref() {
199            handler.set_composition(text, cursor);
200            true
201        } else {
202            false
203        }
204    })
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use std::cell::Cell;
211
212    // Mock handler for testing
213    struct MockHandler;
214    impl FocusedTextFieldHandler for MockHandler {
215        fn handle_key(&self, _: &KeyEvent) -> bool {
216            false
217        }
218        fn insert_text(&self, _: &str) {}
219        fn delete_surrounding(&self, _: usize, _: usize) {}
220        fn copy_selection(&self) -> Option<String> {
221            None
222        }
223        fn cut_selection(&self) -> Option<String> {
224            None
225        }
226        fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {}
227    }
228
229    fn mock_handler() -> Rc<dyn FocusedTextFieldHandler> {
230        Rc::new(MockHandler)
231    }
232
233    #[test]
234    fn request_focus_sets_flag() {
235        let focus = Rc::new(RefCell::new(false));
236        request_focus(focus.clone(), mock_handler());
237        assert!(*focus.borrow());
238        clear_focus();
239    }
240
241    #[test]
242    fn request_focus_clears_previous() {
243        let focus1 = Rc::new(RefCell::new(false));
244        let focus2 = Rc::new(RefCell::new(false));
245
246        request_focus(focus1.clone(), mock_handler());
247        assert!(*focus1.borrow());
248
249        request_focus(focus2.clone(), mock_handler());
250        assert!(!*focus1.borrow()); // First should be unfocused
251        assert!(*focus2.borrow()); // Second should be focused
252        clear_focus();
253    }
254
255    #[test]
256    fn clear_focus_unfocuses_current() {
257        let focus = Rc::new(RefCell::new(false));
258        request_focus(focus.clone(), mock_handler());
259        assert!(*focus.borrow());
260
261        clear_focus();
262        assert!(!*focus.borrow());
263    }
264
265    struct DeleteHandler {
266        last_delete: Cell<Option<(usize, usize)>>,
267    }
268
269    impl FocusedTextFieldHandler for DeleteHandler {
270        fn handle_key(&self, _: &KeyEvent) -> bool {
271            false
272        }
273
274        fn insert_text(&self, _: &str) {}
275
276        fn delete_surrounding(&self, before_bytes: usize, after_bytes: usize) {
277            self.last_delete.set(Some((before_bytes, after_bytes)));
278        }
279
280        fn copy_selection(&self) -> Option<String> {
281            None
282        }
283
284        fn cut_selection(&self) -> Option<String> {
285            None
286        }
287
288        fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {}
289    }
290
291    #[test]
292    fn dispatch_delete_surrounding_calls_handler() {
293        let focus = Rc::new(RefCell::new(false));
294        let handler = Rc::new(DeleteHandler {
295            last_delete: Cell::new(None),
296        });
297
298        request_focus(focus, handler.clone());
299        assert!(dispatch_delete_surrounding(3, 1));
300        assert_eq!(handler.last_delete.get(), Some((3, 1)));
301
302        clear_focus();
303    }
304}