cranpose_ui/
text_field_focus.rs1use std::cell::RefCell;
11use std::rc::{Rc, Weak};
12
13use crate::key_event::KeyEvent;
14
15pub trait FocusedTextFieldHandler {
18 fn handle_key(&self, event: &KeyEvent) -> bool;
20 fn insert_text(&self, text: &str);
22 fn delete_surrounding(&self, before_bytes: usize, after_bytes: usize);
24 fn copy_selection(&self) -> Option<String>;
26 fn cut_selection(&self) -> Option<String>;
28 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
144pub 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 crate::cursor_animation::start_cursor_blink();
154
155 crate::request_render_invalidation();
158}
159
160pub fn clear_focus() {
162 crate::render_state::with_text_field_focus(|state| state.clear_focus());
163
164 crate::cursor_animation::stop_cursor_blink();
166
167 crate::request_render_invalidation();
168}
169
170pub fn has_focused_field() -> bool {
173 crate::render_state::with_text_field_focus(|state| state.has_focused_field())
174}
175
176pub fn dispatch_key_event(event: &KeyEvent) -> bool {
183 crate::render_state::with_text_field_focus(|state| state.dispatch_key_event(event))
184}
185
186pub fn dispatch_paste(text: &str) -> bool {
189 crate::render_state::with_text_field_focus(|state| state.dispatch_paste(text))
190}
191
192pub 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
200pub fn dispatch_copy() -> Option<String> {
203 crate::render_state::with_text_field_focus(|state| state.dispatch_copy())
204}
205
206pub fn dispatch_cut() -> Option<String> {
209 crate::render_state::with_text_field_focus(|state| state.dispatch_cut())
210}
211
212pub 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 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()); assert!(*focus2.borrow()); 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}