Skip to main content

cranpose_ui/
focus_dispatch.rs

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