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
12thread_local! {
13    static POINTER_DISPATCH_MANAGER: RefCell<PointerDispatchManager> =
14        RefCell::new(PointerDispatchManager::new());
15}
16
17// ============================================================================
18// PointerDispatchManager - Invalidation tracking
19// ============================================================================
20
21/// Manages pointer input invalidations across the UI tree.
22///
23/// Similar to Kotlin's pointer input invalidation system, this tracks
24/// which layout nodes need pointer input reprocessing and provides
25/// hooks for the runtime to service those invalidations.
26struct PointerDispatchManager {
27    dirty_nodes: HashSet<NodeId>,
28    is_processing: bool,
29}
30
31impl PointerDispatchManager {
32    fn new() -> Self {
33        Self {
34            dirty_nodes: HashSet::new(),
35            is_processing: false,
36        }
37    }
38
39    fn schedule_repass(&mut self, node_id: NodeId) {
40        self.dirty_nodes.insert(node_id);
41    }
42
43    fn has_pending_repass(&self) -> bool {
44        !self.dirty_nodes.is_empty()
45    }
46
47    fn process_repasses<F>(&mut self, mut processor: F)
48    where
49        F: FnMut(NodeId),
50    {
51        if self.is_processing {
52            return;
53        }
54
55        self.is_processing = true;
56
57        // Process all dirty nodes
58        let nodes: Vec<NodeId> = self.dirty_nodes.drain().collect();
59        for node_id in nodes {
60            processor(node_id);
61        }
62
63        self.is_processing = false;
64    }
65
66    fn clear(&mut self) {
67        self.dirty_nodes.clear();
68    }
69}
70
71/// Schedules a pointer repass for the specified node.
72///
73/// This is called automatically when pointer modifiers invalidate
74/// and mirrors Kotlin's `PointerInputDelegatingNode.requestPointerInput`.
75pub fn schedule_pointer_repass(node_id: NodeId) {
76    POINTER_DISPATCH_MANAGER.with(|manager| {
77        manager.borrow_mut().schedule_repass(node_id);
78    });
79}
80
81/// Returns true if any pointer repasses are pending.
82pub fn has_pending_pointer_repasses() -> bool {
83    POINTER_DISPATCH_MANAGER.with(|manager| manager.borrow().has_pending_repass())
84}
85
86/// Processes all pending pointer repasses.
87///
88/// The host (e.g., app shell or layout engine) should call this after
89/// composition/layout to service pointer invalidations without forcing
90/// measure/layout passes.
91pub fn process_pointer_repasses<F>(processor: F)
92where
93    F: FnMut(NodeId),
94{
95    POINTER_DISPATCH_MANAGER.with(|manager| {
96        manager.borrow_mut().process_repasses(processor);
97    });
98}
99
100/// Clears all pending pointer repasses without processing them.
101pub fn clear_pointer_repasses() {
102    POINTER_DISPATCH_MANAGER.with(|manager| {
103        manager.borrow_mut().clear();
104    });
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn schedule_and_process_repasses() {
113        clear_pointer_repasses();
114
115        let node1: NodeId = 1;
116        let node2: NodeId = 2;
117
118        schedule_pointer_repass(node1);
119        schedule_pointer_repass(node2);
120
121        assert!(has_pending_pointer_repasses());
122
123        let mut processed = Vec::new();
124        process_pointer_repasses(|node_id| {
125            processed.push(node_id);
126        });
127
128        assert_eq!(processed.len(), 2);
129        assert!(processed.contains(&node1));
130        assert!(processed.contains(&node2));
131        assert!(!has_pending_pointer_repasses());
132    }
133
134    #[test]
135    fn duplicate_schedules_deduplicated() {
136        clear_pointer_repasses();
137
138        let node: NodeId = 42;
139        schedule_pointer_repass(node);
140        schedule_pointer_repass(node);
141        schedule_pointer_repass(node);
142
143        let mut count = 0;
144        process_pointer_repasses(|_| {
145            count += 1;
146        });
147
148        assert_eq!(count, 1);
149    }
150}