cranpose_ui/
pointer_dispatch.rs1use 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
101pub fn schedule_pointer_repass(node_id: NodeId) {
106 crate::render_state::with_pointer_dispatch(|state| state.schedule_repass(node_id));
107}
108
109pub fn has_pending_pointer_repasses() -> bool {
111 crate::render_state::with_pointer_dispatch(|state| state.has_pending_repass())
112}
113
114pub 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
126pub 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}