cranpose_ui/
focus_dispatch.rs1use cranpose_core::NodeId;
8use std::cell::RefCell;
9use std::collections::HashSet;
10
11thread_local! {
12 static FOCUS_INVALIDATION_MANAGER: RefCell<FocusInvalidationManager> =
13 RefCell::new(FocusInvalidationManager::new());
14}
15
16struct FocusInvalidationManager {
22 dirty_nodes: HashSet<NodeId>,
23 is_processing: bool,
24 active_focus_target: Option<NodeId>,
25}
26
27impl FocusInvalidationManager {
28 fn new() -> Self {
29 Self {
30 dirty_nodes: HashSet::new(),
31 is_processing: false,
32 active_focus_target: None,
33 }
34 }
35
36 fn schedule_invalidation(&mut self, node_id: NodeId) {
37 self.dirty_nodes.insert(node_id);
38 }
39
40 fn has_pending_invalidation(&self) -> bool {
41 !self.dirty_nodes.is_empty()
42 }
43
44 fn set_active_focus_target(&mut self, node_id: Option<NodeId>) {
45 self.active_focus_target = node_id;
46 }
47
48 fn active_focus_target(&self) -> Option<NodeId> {
49 self.active_focus_target
50 }
51
52 fn process_invalidations<F>(&mut self, mut processor: F)
53 where
54 F: FnMut(NodeId),
55 {
56 if self.is_processing {
57 return;
58 }
59
60 self.is_processing = true;
61
62 let nodes: Vec<NodeId> = self.dirty_nodes.drain().collect();
64 for node_id in nodes {
65 processor(node_id);
66 }
67
68 self.is_processing = false;
69 }
70
71 fn clear(&mut self) {
72 self.dirty_nodes.clear();
73 }
74}
75
76pub fn schedule_focus_invalidation(node_id: NodeId) {
81 FOCUS_INVALIDATION_MANAGER.with(|manager| {
82 manager.borrow_mut().schedule_invalidation(node_id);
83 });
84}
85
86pub fn has_pending_focus_invalidations() -> bool {
88 FOCUS_INVALIDATION_MANAGER.with(|manager| manager.borrow().has_pending_invalidation())
89}
90
91pub fn set_active_focus_target(node_id: Option<NodeId>) {
96 FOCUS_INVALIDATION_MANAGER.with(|manager| {
97 manager.borrow_mut().set_active_focus_target(node_id);
98 });
99}
100
101pub fn active_focus_target() -> Option<NodeId> {
103 FOCUS_INVALIDATION_MANAGER.with(|manager| manager.borrow().active_focus_target())
104}
105
106pub fn process_focus_invalidations<F>(processor: F)
112where
113 F: FnMut(NodeId),
114{
115 FOCUS_INVALIDATION_MANAGER.with(|manager| {
116 manager.borrow_mut().process_invalidations(processor);
117 });
118}
119
120pub fn clear_focus_invalidations() {
122 FOCUS_INVALIDATION_MANAGER.with(|manager| {
123 manager.borrow_mut().clear();
124 });
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn schedule_and_process_invalidations() {
133 clear_focus_invalidations();
134
135 let node1: NodeId = 1;
136 let node2: NodeId = 2;
137
138 schedule_focus_invalidation(node1);
139 schedule_focus_invalidation(node2);
140
141 assert!(has_pending_focus_invalidations());
142
143 let mut processed = Vec::new();
144 process_focus_invalidations(|node_id| {
145 processed.push(node_id);
146 });
147
148 assert_eq!(processed.len(), 2);
149 assert!(processed.contains(&node1));
150 assert!(processed.contains(&node2));
151 assert!(!has_pending_focus_invalidations());
152 }
153
154 #[test]
155 fn active_focus_target_tracking() {
156 set_active_focus_target(None);
157 assert_eq!(active_focus_target(), None);
158
159 let node: NodeId = 42;
160 set_active_focus_target(Some(node));
161 assert_eq!(active_focus_target(), Some(node));
162
163 set_active_focus_target(None);
164 assert_eq!(active_focus_target(), None);
165 }
166
167 #[test]
168 fn duplicate_invalidations_deduplicated() {
169 clear_focus_invalidations();
170
171 let node: NodeId = 42;
172 schedule_focus_invalidation(node);
173 schedule_focus_invalidation(node);
174 schedule_focus_invalidation(node);
175
176 let mut count = 0;
177 process_focus_invalidations(|_| {
178 count += 1;
179 });
180
181 assert_eq!(count, 1);
182 }
183}