Skip to main content

cranpose_ui/
pointer_dispatch.rs

1//! Pointer input dispatch manager for Cranpose.
2//!
3//! This module manages pointer input invalidations across the UI tree.
4//! Hit path tracking for gesture state preservation is handled by
5//! `AppShell::cached_hits` which caches hit targets on pointer DOWN
6//! and dispatches subsequent MOVE/UP events to the same cached nodes.
7
8use cranpose_core::NodeId;
9use std::cell::RefCell;
10use std::collections::HashSet;
11
12// ============================================================================
13// PointerDispatchManager - Invalidation tracking
14// ============================================================================
15
16/// Manages pointer input invalidations across the UI tree.
17///
18/// Similar to Kotlin's pointer input invalidation system, this tracks
19/// which layout nodes need pointer input reprocessing and provides
20/// hooks for the runtime to service those invalidations.
21struct 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
117/// Schedules a pointer repass for the specified node.
118///
119/// This is called automatically when pointer modifiers invalidate
120/// and mirrors Kotlin's `PointerInputDelegatingNode.requestPointerInput`.
121pub fn schedule_pointer_repass(node_id: NodeId) {
122    crate::render_state::with_pointer_dispatch(|state| state.schedule_repass(node_id));
123}
124
125/// Returns true if any pointer repasses are pending.
126pub fn has_pending_pointer_repasses() -> bool {
127    crate::render_state::with_pointer_dispatch(|state| state.has_pending_repass())
128}
129
130/// Processes all pending pointer repasses.
131///
132/// The host (e.g., app shell or layout engine) should call this after
133/// composition/layout to service pointer invalidations without forcing
134/// measure/layout passes.
135pub 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
142/// Clears all pending pointer repasses without processing them.
143pub 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}