Skip to main content

cranpose_ui/
focus_dispatch.rs

1use std::{
2    cell::{Cell, RefCell},
3    collections::{HashMap, HashSet, VecDeque},
4    rc::Rc,
5};
6
7use cranpose_core::NodeId;
8use cranpose_foundation::FocusState;
9
10pub(crate) trait FocusTargetHandle {
11    fn set_focus_state(&self, state: FocusState);
12}
13
14struct FocusInvalidationManager {
15    dirty_nodes: HashSet<NodeId>,
16    is_processing: bool,
17    active_focus_target: Option<NodeId>,
18    focus_targets: HashMap<NodeId, Vec<Rc<dyn FocusTargetHandle>>>,
19    pending_focus_requests: VecDeque<NodeId>,
20    dispatching_focus: bool,
21}
22
23impl FocusInvalidationManager {
24    fn new() -> Self {
25        Self {
26            dirty_nodes: HashSet::new(),
27            is_processing: false,
28            active_focus_target: None,
29            focus_targets: HashMap::new(),
30            pending_focus_requests: VecDeque::new(),
31            dispatching_focus: false,
32        }
33    }
34
35    fn schedule_invalidation(&mut self, node_id: NodeId) {
36        self.dirty_nodes.insert(node_id);
37    }
38
39    fn has_pending_invalidation(&self) -> bool {
40        !self.dirty_nodes.is_empty()
41    }
42
43    fn set_active_focus_target(&mut self, node_id: Option<NodeId>) {
44        if self.active_focus_target == node_id {
45            return;
46        }
47        for changed in self.active_focus_target.into_iter().chain(node_id) {
48            crate::semantics_dispatch::schedule_semantics_invalidation(changed);
49        }
50        self.active_focus_target = node_id;
51        crate::request_render_invalidation();
52    }
53
54    fn active_focus_target(&self) -> Option<NodeId> {
55        self.active_focus_target
56    }
57
58    fn register_focus_target(&mut self, node_id: NodeId, handle: Rc<dyn FocusTargetHandle>) {
59        self.focus_targets.entry(node_id).or_default().push(handle);
60    }
61
62    fn unregister_focus_target(&mut self, node_id: NodeId, handle: &Rc<dyn FocusTargetHandle>) {
63        let Some(handles) = self.focus_targets.get_mut(&node_id) else {
64            return;
65        };
66        handles.retain(|existing| !Rc::ptr_eq(existing, handle));
67        if handles.is_empty() {
68            self.focus_targets.remove(&node_id);
69            if self.active_focus_target == Some(node_id) {
70                self.set_active_focus_target(None);
71            }
72        }
73    }
74
75    fn has_focus_target(&self, node_id: NodeId) -> bool {
76        self.focus_targets.contains_key(&node_id)
77    }
78
79    fn focus_target_handles(&self, node_id: NodeId) -> Vec<Rc<dyn FocusTargetHandle>> {
80        self.focus_targets
81            .get(&node_id)
82            .cloned()
83            .unwrap_or_default()
84    }
85
86    fn swap_active_focus_target(&mut self, node_id: NodeId) -> Option<NodeId> {
87        if self.active_focus_target == Some(node_id) {
88            return None;
89        }
90        let previous = self.active_focus_target;
91        self.set_active_focus_target(Some(node_id));
92        previous
93    }
94
95    fn take_first_focus_request_if_idle(&mut self) -> Option<NodeId> {
96        if self.dispatching_focus {
97            return None;
98        }
99        let next = self.pending_focus_requests.pop_front()?;
100        self.dispatching_focus = true;
101        Some(next)
102    }
103
104    fn take_next_focus_request(&mut self) -> Option<NodeId> {
105        self.pending_focus_requests.pop_front()
106    }
107
108    fn finish_focus_dispatch(&mut self) {
109        self.dispatching_focus = false;
110    }
111
112    fn take_pending_for_processing(&mut self) -> Option<Vec<NodeId>> {
113        if self.is_processing {
114            return None;
115        }
116
117        self.is_processing = true;
118        Some(self.dirty_nodes.drain().collect())
119    }
120
121    fn finish_processing<I>(&mut self, remaining: I)
122    where
123        I: IntoIterator<Item = NodeId>,
124    {
125        self.dirty_nodes.extend(remaining);
126        self.is_processing = false;
127    }
128
129    fn clear(&mut self) {
130        self.dirty_nodes.clear();
131    }
132}
133
134pub(crate) struct FocusInvalidationState {
135    manager: RefCell<FocusInvalidationManager>,
136    order: RefCell<Vec<crate::FocusEntry>>,
137}
138
139impl FocusInvalidationState {
140    pub(crate) fn new() -> Self {
141        Self {
142            manager: RefCell::new(FocusInvalidationManager::new()),
143            order: RefCell::new(Vec::new()),
144        }
145    }
146
147    pub(crate) fn set_focus_order(&self, entries: Vec<crate::FocusEntry>) {
148        *self.order.borrow_mut() = entries;
149    }
150
151    pub(crate) fn with_focus_order<T>(&self, reader: impl FnOnce(&[crate::FocusEntry]) -> T) -> T {
152        reader(&self.order.borrow())
153    }
154
155    fn schedule_invalidation(&self, node_id: NodeId) {
156        self.manager.borrow_mut().schedule_invalidation(node_id);
157    }
158
159    fn has_pending_invalidation(&self) -> bool {
160        self.manager.borrow().has_pending_invalidation()
161    }
162
163    fn set_active_focus_target(&self, node_id: Option<NodeId>) {
164        self.manager.borrow_mut().set_active_focus_target(node_id);
165    }
166
167    fn active_focus_target(&self) -> Option<NodeId> {
168        self.manager.borrow().active_focus_target()
169    }
170
171    fn register_focus_target(&self, node_id: NodeId, handle: Rc<dyn FocusTargetHandle>) {
172        self.manager
173            .borrow_mut()
174            .register_focus_target(node_id, handle);
175    }
176
177    fn unregister_focus_target(&self, node_id: NodeId, handle: &Rc<dyn FocusTargetHandle>) {
178        self.manager
179            .borrow_mut()
180            .unregister_focus_target(node_id, handle);
181    }
182
183    pub(crate) fn has_focus_target(&self, node_id: NodeId) -> bool {
184        self.manager.borrow().has_focus_target(node_id)
185    }
186
187    pub(crate) fn clear_active_focus(&self) -> bool {
188        let previous = {
189            let mut manager = self.manager.borrow_mut();
190            let previous = manager.active_focus_target();
191            manager.set_active_focus_target(None);
192            previous
193        };
194        let Some(previous) = previous else {
195            return false;
196        };
197        let handles = self.manager.borrow().focus_target_handles(previous);
198        for handle in handles {
199            handle.set_focus_state(FocusState::Inactive);
200        }
201        true
202    }
203
204    pub(crate) fn request_focus(&self, node_id: NodeId) -> bool {
205        let accepted = {
206            let mut manager = self.manager.borrow_mut();
207            if !manager.has_focus_target(node_id) {
208                false
209            } else {
210                manager.pending_focus_requests.push_back(node_id);
211                true
212            }
213        };
214        if accepted {
215            self.drain_focus_requests();
216        }
217        accepted
218    }
219
220    fn drain_focus_requests(&self) {
221        let Some(first) = self.manager.borrow_mut().take_first_focus_request_if_idle() else {
222            return;
223        };
224
225        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
226            let mut current = first;
227            loop {
228                self.apply_focus_change(current);
229                match self.manager.borrow_mut().take_next_focus_request() {
230                    Some(next) => current = next,
231                    None => break,
232                }
233            }
234        }));
235
236        self.manager.borrow_mut().finish_focus_dispatch();
237
238        if let Err(payload) = result {
239            std::panic::resume_unwind(payload);
240        }
241    }
242
243    fn apply_focus_change(&self, node_id: NodeId) {
244        let previous = self.manager.borrow_mut().swap_active_focus_target(node_id);
245
246        if let Some(previous) = previous {
247            let losing_handles = self.manager.borrow().focus_target_handles(previous);
248            for handle in losing_handles {
249                handle.set_focus_state(FocusState::Inactive);
250            }
251        }
252
253        let gaining_handles = self.manager.borrow().focus_target_handles(node_id);
254        for handle in gaining_handles {
255            handle.set_focus_state(FocusState::Active);
256        }
257    }
258
259    fn process_invalidations<F>(&self, processor: F)
260    where
261        F: FnMut(NodeId),
262    {
263        let Some(nodes) = self.manager.borrow_mut().take_pending_for_processing() else {
264            return;
265        };
266
267        self.process_pending_nodes(nodes, processor);
268    }
269
270    fn clear(&self) {
271        self.manager.borrow_mut().clear();
272    }
273
274    fn process_pending_nodes<F>(&self, nodes: Vec<NodeId>, mut processor: F)
275    where
276        F: FnMut(NodeId),
277    {
278        let mut remaining = nodes.into_iter();
279        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
280            for node_id in remaining.by_ref() {
281                processor(node_id);
282            }
283        }));
284
285        self.manager.borrow_mut().finish_processing(remaining);
286
287        if let Err(payload) = result {
288            std::panic::resume_unwind(payload);
289        }
290    }
291}
292
293/// Schedules a focus invalidation for the specified node.
294///
295/// This is called automatically when focus modifiers invalidate
296/// and mirrors Kotlin's `FocusInvalidationManager.scheduleInvalidation`.
297pub fn schedule_focus_invalidation(node_id: NodeId) {
298    crate::render_state::with_focus_dispatch(|state| state.schedule_invalidation(node_id));
299}
300
301/// Returns true if any focus invalidations are pending.
302pub fn has_pending_focus_invalidations() -> bool {
303    crate::render_state::with_focus_dispatch(|state| state.has_pending_invalidation())
304}
305
306/// Sets the currently active focus target.
307///
308/// This mirrors Kotlin's `FocusOwner.activeFocusTargetNode` and allows
309/// the focus system to track which node currently has focus.
310pub fn set_active_focus_target(node_id: Option<NodeId>) {
311    crate::render_state::with_focus_dispatch(|state| state.set_active_focus_target(node_id));
312}
313
314/// Returns the currently active focus target, if any.
315pub fn active_focus_target() -> Option<NodeId> {
316    crate::render_state::with_focus_dispatch(|state| state.active_focus_target())
317}
318
319pub(crate) fn register_focus_target(node_id: NodeId, handle: Rc<dyn FocusTargetHandle>) {
320    crate::render_state::with_focus_dispatch(|state| state.register_focus_target(node_id, handle));
321}
322
323pub(crate) fn unregister_focus_target(node_id: NodeId, handle: &Rc<dyn FocusTargetHandle>) {
324    crate::render_state::with_focus_dispatch(|state| {
325        state.unregister_focus_target(node_id, handle)
326    });
327}
328
329#[cfg(test)]
330pub(crate) fn request_focus(node_id: NodeId) -> bool {
331    crate::render_state::with_focus_dispatch(|state| state.request_focus(node_id))
332}
333
334/// Whether `node_id` registered a focus target in this app context.
335pub(crate) fn has_focus_target(node_id: NodeId) -> bool {
336    crate::render_state::with_focus_dispatch(|state| state.has_focus_target(node_id))
337}
338
339pub(crate) fn request_focus_in_context(node_id: NodeId) -> bool {
340    let Some(app_context) = crate::render_state::current_app_context_id_opt() else {
341        return false;
342    };
343    request_focus_for(app_context, node_id).unwrap_or(false)
344}
345
346/// Drops focus from the active target and answers whether one held it.
347pub(crate) fn clear_active_focus() -> bool {
348    crate::render_state::with_focus_dispatch(|state| state.clear_active_focus())
349}
350
351pub(crate) fn request_focus_for(
352    app_context: crate::render_state::AppContextId,
353    node_id: NodeId,
354) -> Option<bool> {
355    crate::render_state::with_focus_dispatch_by_app_context(app_context, |state| {
356        state.request_focus(node_id)
357    })
358}
359
360/// Processes all pending focus invalidations.
361///
362/// The host (e.g., app shell or layout engine) should call this after
363/// composition/layout to service focus invalidations without forcing
364/// measure/layout passes.
365pub fn process_focus_invalidations<F>(processor: F)
366where
367    F: FnMut(NodeId),
368{
369    crate::render_state::with_focus_dispatch(|state| state.process_invalidations(processor));
370}
371
372/// Clears all pending focus invalidations without processing them.
373pub fn clear_focus_invalidations() {
374    crate::render_state::with_focus_dispatch(|state| state.clear());
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380
381    #[test]
382    fn schedule_and_process_invalidations() {
383        let _app_context = crate::render_state::app_context_test_scope();
384        clear_focus_invalidations();
385
386        let node1: NodeId = 1;
387        let node2: NodeId = 2;
388
389        schedule_focus_invalidation(node1);
390        schedule_focus_invalidation(node2);
391
392        assert!(has_pending_focus_invalidations());
393
394        let mut processed = Vec::new();
395        process_focus_invalidations(|node_id| {
396            processed.push(node_id);
397        });
398
399        assert_eq!(processed.len(), 2);
400        assert!(processed.contains(&node1));
401        assert!(processed.contains(&node2));
402        assert!(!has_pending_focus_invalidations());
403    }
404
405    #[test]
406    fn active_focus_target_tracking() {
407        let _app_context = crate::render_state::app_context_test_scope();
408        set_active_focus_target(None);
409        assert_eq!(active_focus_target(), None);
410
411        let node: NodeId = 42;
412        set_active_focus_target(Some(node));
413        assert_eq!(active_focus_target(), Some(node));
414
415        set_active_focus_target(None);
416        assert_eq!(active_focus_target(), None);
417    }
418
419    #[test]
420    fn duplicate_invalidations_deduplicated() {
421        let _app_context = crate::render_state::app_context_test_scope();
422        clear_focus_invalidations();
423
424        let node: NodeId = 42;
425        schedule_focus_invalidation(node);
426        schedule_focus_invalidation(node);
427        schedule_focus_invalidation(node);
428
429        let mut count = 0;
430        process_focus_invalidations(|_| {
431            count += 1;
432        });
433
434        assert_eq!(count, 1);
435    }
436
437    #[test]
438    fn process_invalidations_recovers_after_processor_panic() {
439        let _app_context = crate::render_state::app_context_test_scope();
440        clear_focus_invalidations();
441
442        schedule_focus_invalidation(1);
443        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
444            process_focus_invalidations(|_| panic!("focus processor panic"));
445        }));
446        assert!(result.is_err());
447
448        schedule_focus_invalidation(2);
449        let mut processed = Vec::new();
450        process_focus_invalidations(|node_id| processed.push(node_id));
451
452        assert!(
453            processed.contains(&2),
454            "focus invalidation processing must not stay stuck after a processor panic"
455        );
456        assert!(!has_pending_focus_invalidations());
457    }
458
459    #[test]
460    fn process_invalidations_allows_processor_to_schedule_more_work() {
461        let _app_context = crate::render_state::app_context_test_scope();
462        clear_focus_invalidations();
463
464        schedule_focus_invalidation(1);
465        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
466            process_focus_invalidations(|_| schedule_focus_invalidation(2));
467        }));
468        assert!(
469            result.is_ok(),
470            "focus processors must be able to enqueue follow-up invalidations"
471        );
472        assert!(has_pending_focus_invalidations());
473
474        let mut processed = Vec::new();
475        process_focus_invalidations(|node_id| processed.push(node_id));
476
477        assert_eq!(processed, vec![2]);
478        assert!(!has_pending_focus_invalidations());
479    }
480
481    #[test]
482    fn focus_state_is_scoped_by_app_context() {
483        let _app_context = crate::render_state::app_context_test_scope();
484        let first = crate::render_state::AppContext::new_with_density(1.0);
485        let second = crate::render_state::AppContext::new_with_density(1.0);
486
487        first.enter(|| {
488            clear_focus_invalidations();
489            schedule_focus_invalidation(7);
490            set_active_focus_target(Some(17));
491            assert!(has_pending_focus_invalidations());
492            assert_eq!(active_focus_target(), Some(17));
493        });
494
495        second.enter(|| {
496            clear_focus_invalidations();
497            assert!(!has_pending_focus_invalidations());
498            assert_eq!(active_focus_target(), None);
499            schedule_focus_invalidation(9);
500            set_active_focus_target(Some(19));
501        });
502
503        first.enter(|| {
504            let mut processed = Vec::new();
505            process_focus_invalidations(|node_id| processed.push(node_id));
506            assert_eq!(processed, vec![7]);
507            assert_eq!(active_focus_target(), Some(17));
508        });
509
510        second.enter(|| {
511            let mut processed = Vec::new();
512            process_focus_invalidations(|node_id| processed.push(node_id));
513            assert_eq!(processed, vec![9]);
514            assert_eq!(active_focus_target(), Some(19));
515        });
516    }
517
518    struct RecordingTarget {
519        states: RefCell<Vec<FocusState>>,
520        on_active: RefCell<Option<Box<dyn Fn()>>>,
521    }
522
523    impl RecordingTarget {
524        fn new() -> Rc<Self> {
525            Rc::new(Self {
526                states: RefCell::new(Vec::new()),
527                on_active: RefCell::new(None),
528            })
529        }
530
531        fn states(&self) -> Vec<FocusState> {
532            self.states.borrow().clone()
533        }
534    }
535
536    impl FocusTargetHandle for RecordingTarget {
537        fn set_focus_state(&self, state: FocusState) {
538            self.states.borrow_mut().push(state);
539            if state == FocusState::Active
540                && let Some(callback) = self.on_active.borrow_mut().take()
541            {
542                callback();
543            }
544        }
545    }
546
547    fn as_handle(target: &Rc<RecordingTarget>) -> Rc<dyn FocusTargetHandle> {
548        Rc::clone(target) as Rc<dyn FocusTargetHandle>
549    }
550
551    #[test]
552    fn request_focus_moves_focus_between_two_registered_targets() {
553        let _app_context = crate::render_state::app_context_test_scope();
554        clear_focus_invalidations();
555        set_active_focus_target(None);
556
557        let a = RecordingTarget::new();
558        let b = RecordingTarget::new();
559        register_focus_target(1, as_handle(&a));
560        register_focus_target(2, as_handle(&b));
561
562        assert!(request_focus(1));
563        assert_eq!(a.states(), vec![FocusState::Active]);
564        assert_eq!(active_focus_target(), Some(1));
565
566        assert!(request_focus(2));
567        assert_eq!(a.states(), vec![FocusState::Active, FocusState::Inactive]);
568        assert_eq!(b.states(), vec![FocusState::Active]);
569        assert_eq!(active_focus_target(), Some(2));
570    }
571
572    #[test]
573    fn request_focus_on_an_unregistered_node_fails_without_changing_anything() {
574        let _app_context = crate::render_state::app_context_test_scope();
575        clear_focus_invalidations();
576        set_active_focus_target(None);
577
578        assert!(!request_focus(99));
579        assert_eq!(active_focus_target(), None);
580    }
581
582    #[test]
583    fn unregistering_the_active_targets_last_handle_clears_active_focus() {
584        let _app_context = crate::render_state::app_context_test_scope();
585        clear_focus_invalidations();
586        set_active_focus_target(None);
587
588        let a = RecordingTarget::new();
589        let handle = as_handle(&a);
590        register_focus_target(1, Rc::clone(&handle));
591        assert!(request_focus(1));
592        assert_eq!(active_focus_target(), Some(1));
593
594        unregister_focus_target(1, &handle);
595        assert_eq!(active_focus_target(), None);
596        assert!(!request_focus(1));
597    }
598
599    #[test]
600    fn request_focus_from_inside_a_callback_does_not_double_borrow_or_recurse() {
601        let _app_context = crate::render_state::app_context_test_scope();
602        clear_focus_invalidations();
603        set_active_focus_target(None);
604
605        let a = RecordingTarget::new();
606        let b = RecordingTarget::new();
607        register_focus_target(1, as_handle(&a));
608        register_focus_target(2, as_handle(&b));
609
610        *a.on_active.borrow_mut() = Some(Box::new({
611            let bounced_back = RefCell::new(false);
612            move || {
613                if !*bounced_back.borrow() {
614                    *bounced_back.borrow_mut() = true;
615                    assert!(request_focus(2));
616                }
617            }
618        }));
619
620        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
621            assert!(request_focus(1));
622        }));
623        assert!(
624            result.is_ok(),
625            "a request_focus call from inside a callback must not panic or overflow the stack"
626        );
627
628        assert_eq!(a.states(), vec![FocusState::Active, FocusState::Inactive]);
629        assert_eq!(b.states(), vec![FocusState::Active]);
630        assert_eq!(active_focus_target(), Some(2));
631    }
632
633    #[test]
634    fn a_panicking_callback_still_releases_the_dispatch_lock() {
635        let _app_context = crate::render_state::app_context_test_scope();
636        clear_focus_invalidations();
637        set_active_focus_target(None);
638
639        struct PanicsOnActivate;
640        impl FocusTargetHandle for PanicsOnActivate {
641            fn set_focus_state(&self, state: FocusState) {
642                if state == FocusState::Active {
643                    panic!("focus target panicked while activating");
644                }
645            }
646        }
647
648        register_focus_target(1, Rc::new(PanicsOnActivate) as Rc<dyn FocusTargetHandle>);
649        let b = RecordingTarget::new();
650        register_focus_target(2, as_handle(&b));
651
652        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
653            request_focus(1);
654        }));
655        assert!(result.is_err());
656
657        assert!(request_focus(2));
658        assert_eq!(
659            b.states(),
660            vec![FocusState::Active],
661            "the dispatch lock must be released after a callback panic so later requests proceed"
662        );
663    }
664}
665
666thread_local! {
667    static KEYBOARD_FOCUS_VISIBLE: Cell<bool> = const { Cell::new(false) };
668}
669
670/// Records how focus last moved: true after Tab or an arrow key, false after
671/// a pointer press. A [`Modifier::focusable`](crate::Modifier::focusable)
672/// draws its ring only while this is true. Returns whether the value changed,
673/// so the caller can ask for a redraw.
674pub fn set_keyboard_focus_visible(visible: bool) -> bool {
675    KEYBOARD_FOCUS_VISIBLE.with(|cell| cell.replace(visible) != visible)
676}
677
678/// Whether the keyboard, and not a pointer, made the last focus move.
679pub fn keyboard_focus_visible() -> bool {
680    KEYBOARD_FOCUS_VISIBLE.with(|cell| cell.get())
681}