Skip to main content

cranpose_ui/
pointer_dispatch.rs

1use std::{cell::RefCell, collections::HashSet};
2
3use cranpose_core::NodeId;
4
5struct PointerDispatchManager {
6    dirty_nodes: HashSet<NodeId>,
7    is_processing: bool,
8}
9
10impl PointerDispatchManager {
11    fn new() -> Self {
12        Self {
13            dirty_nodes: HashSet::new(),
14            is_processing: false,
15        }
16    }
17
18    fn schedule_repass(&mut self, node_id: NodeId) {
19        self.dirty_nodes.insert(node_id);
20    }
21
22    fn has_pending_repass(&self) -> bool {
23        !self.dirty_nodes.is_empty()
24    }
25
26    fn take_pending_for_processing(&mut self) -> Option<Vec<NodeId>> {
27        if self.is_processing {
28            return None;
29        }
30
31        self.is_processing = true;
32        Some(self.dirty_nodes.drain().collect())
33    }
34
35    fn finish_processing<I>(&mut self, remaining: I)
36    where
37        I: IntoIterator<Item = NodeId>,
38    {
39        self.dirty_nodes.extend(remaining);
40        self.is_processing = false;
41    }
42
43    fn clear(&mut self) {
44        self.dirty_nodes.clear();
45    }
46}
47
48pub(crate) struct PointerDispatchState {
49    manager: RefCell<PointerDispatchManager>,
50}
51
52impl PointerDispatchState {
53    pub(crate) fn new() -> Self {
54        Self {
55            manager: RefCell::new(PointerDispatchManager::new()),
56        }
57    }
58
59    fn schedule_repass(&self, node_id: NodeId) {
60        self.manager.borrow_mut().schedule_repass(node_id);
61    }
62
63    fn has_pending_repass(&self) -> bool {
64        self.manager.borrow().has_pending_repass()
65    }
66
67    fn process_repasses<F>(&self, processor: F)
68    where
69        F: FnMut(NodeId),
70    {
71        let Some(nodes) = self.manager.borrow_mut().take_pending_for_processing() else {
72            return;
73        };
74
75        self.process_pending_nodes(nodes, processor);
76    }
77
78    fn clear(&self) {
79        self.manager.borrow_mut().clear();
80    }
81
82    fn process_pending_nodes<F>(&self, nodes: Vec<NodeId>, mut processor: F)
83    where
84        F: FnMut(NodeId),
85    {
86        let mut remaining = nodes.into_iter();
87        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
88            for node_id in remaining.by_ref() {
89                processor(node_id);
90            }
91        }));
92
93        self.manager.borrow_mut().finish_processing(remaining);
94
95        if let Err(payload) = result {
96            std::panic::resume_unwind(payload);
97        }
98    }
99}
100
101/// Schedules a pointer repass for the specified node.
102///
103/// This is called automatically when pointer modifiers invalidate
104/// and mirrors Kotlin's `PointerInputDelegatingNode.requestPointerInput`.
105pub fn schedule_pointer_repass(node_id: NodeId) {
106    crate::render_state::with_pointer_dispatch(|state| state.schedule_repass(node_id));
107}
108
109/// Returns true if any pointer repasses are pending.
110pub fn has_pending_pointer_repasses() -> bool {
111    crate::render_state::with_pointer_dispatch(|state| state.has_pending_repass())
112}
113
114/// Processes all pending pointer repasses.
115///
116/// The host (e.g., app shell or layout engine) should call this after
117/// composition/layout to service pointer invalidations without forcing
118/// measure/layout passes.
119pub fn process_pointer_repasses<F>(processor: F)
120where
121    F: FnMut(NodeId),
122{
123    crate::render_state::with_pointer_dispatch(|state| state.process_repasses(processor));
124}
125
126/// Clears all pending pointer repasses without processing them.
127pub fn clear_pointer_repasses() {
128    crate::render_state::with_pointer_dispatch(|state| state.clear());
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn schedule_and_process_repasses() {
137        let _app_context = crate::render_state::app_context_test_scope();
138        clear_pointer_repasses();
139
140        let node1: NodeId = 1;
141        let node2: NodeId = 2;
142
143        schedule_pointer_repass(node1);
144        schedule_pointer_repass(node2);
145
146        assert!(has_pending_pointer_repasses());
147
148        let mut processed = Vec::new();
149        process_pointer_repasses(|node_id| {
150            processed.push(node_id);
151        });
152
153        assert_eq!(processed.len(), 2);
154        assert!(processed.contains(&node1));
155        assert!(processed.contains(&node2));
156        assert!(!has_pending_pointer_repasses());
157    }
158
159    #[test]
160    fn duplicate_schedules_deduplicated() {
161        let _app_context = crate::render_state::app_context_test_scope();
162        clear_pointer_repasses();
163
164        let node: NodeId = 42;
165        schedule_pointer_repass(node);
166        schedule_pointer_repass(node);
167        schedule_pointer_repass(node);
168
169        let mut count = 0;
170        process_pointer_repasses(|_| {
171            count += 1;
172        });
173
174        assert_eq!(count, 1);
175    }
176
177    #[test]
178    fn process_repasses_recovers_after_processor_panic() {
179        let _app_context = crate::render_state::app_context_test_scope();
180        clear_pointer_repasses();
181
182        schedule_pointer_repass(1);
183        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
184            process_pointer_repasses(|_| panic!("pointer repass processor panic"));
185        }));
186        assert!(result.is_err());
187
188        schedule_pointer_repass(2);
189        let mut processed = Vec::new();
190        process_pointer_repasses(|node_id| processed.push(node_id));
191
192        assert!(
193            processed.contains(&2),
194            "pointer repass processing must not stay stuck after a processor panic"
195        );
196        assert!(!has_pending_pointer_repasses());
197    }
198
199    #[test]
200    fn process_repasses_allows_processor_to_schedule_more_work() {
201        let _app_context = crate::render_state::app_context_test_scope();
202        clear_pointer_repasses();
203
204        schedule_pointer_repass(1);
205        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
206            process_pointer_repasses(|_| schedule_pointer_repass(2));
207        }));
208        assert!(
209            result.is_ok(),
210            "pointer repass processors must be able to enqueue follow-up repasses"
211        );
212        assert!(has_pending_pointer_repasses());
213
214        let mut processed = Vec::new();
215        process_pointer_repasses(|node_id| processed.push(node_id));
216
217        assert_eq!(processed, vec![2]);
218        assert!(!has_pending_pointer_repasses());
219    }
220
221    #[test]
222    fn pointer_repasses_are_scoped_by_app_context() {
223        let _app_context = crate::render_state::app_context_test_scope();
224        let first = crate::render_state::AppContext::new_with_density(1.0);
225        let second = crate::render_state::AppContext::new_with_density(1.0);
226
227        first.enter(|| {
228            clear_pointer_repasses();
229            schedule_pointer_repass(7);
230            assert!(has_pending_pointer_repasses());
231        });
232
233        second.enter(|| {
234            clear_pointer_repasses();
235            assert!(!has_pending_pointer_repasses());
236            schedule_pointer_repass(9);
237        });
238
239        first.enter(|| {
240            let mut processed = Vec::new();
241            process_pointer_repasses(|node_id| processed.push(node_id));
242            assert_eq!(processed, vec![7]);
243        });
244
245        second.enter(|| {
246            let mut processed = Vec::new();
247            process_pointer_repasses(|node_id| processed.push(node_id));
248            assert_eq!(processed, vec![9]);
249        });
250    }
251}