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