Skip to main content

cranpose_ui/
semantics_dispatch.rs

1//! Semantics invalidations raised from outside composition.
2//!
3//! A `.semantics()` recorder is re-run whenever the tree is collected, so a
4//! recorder that reads live app state always reports the app's current answer.
5//! What it could not do until now is say *when* that answer changed: the tree is
6//! only re-collected when a node marks its semantics dirty, and the only things
7//! that did so were attaching a node, updating its modifier chain (which needs a
8//! recomposition) and a layout pass.
9//!
10//! An app whose screen is one `Canvas` has none of those. Its layout never
11//! changes and its frame loop drives draws rather than recompositions, so the
12//! semantics tree was published once — at boot, before there was anything on
13//! screen — and never again. Every later screen inherited whatever the tree
14//! happened to hold at the last pass, which for a screen reader means reading
15//! out controls the user is no longer looking at.
16//!
17//! This is the missing half, and it mirrors Jetpack Compose's
18//! `SemanticsModifierNode.invalidateSemantics()`: a node can be marked for
19//! re-collection directly, with no recomposition and no layout pass. The queue
20//! is drained by the app shell each frame with the applier in hand, which is
21//! where the dirty flag can actually be bubbled to the root that
22//! [`crate::tree_needs_semantics`] reads.
23//!
24//! Structured like [`crate::focus_dispatch`], for the same reason: the request
25//! arrives on the app's own thread at an arbitrary moment, and the tree may only
26//! be touched at a defined point in the frame.
27
28use cranpose_core::NodeId;
29use std::cell::RefCell;
30use std::collections::HashSet;
31
32/// Layout nodes whose semantics need re-collecting.
33struct SemanticsInvalidationManager {
34    dirty_nodes: HashSet<NodeId>,
35    is_processing: bool,
36}
37
38impl SemanticsInvalidationManager {
39    fn new() -> Self {
40        Self {
41            dirty_nodes: HashSet::new(),
42            is_processing: false,
43        }
44    }
45
46    fn schedule_invalidation(&mut self, node_id: NodeId) {
47        self.dirty_nodes.insert(node_id);
48    }
49
50    fn has_pending_invalidation(&self) -> bool {
51        !self.dirty_nodes.is_empty()
52    }
53
54    fn take_pending_for_processing(&mut self) -> Option<Vec<NodeId>> {
55        if self.is_processing {
56            return None;
57        }
58
59        self.is_processing = true;
60        Some(self.dirty_nodes.drain().collect())
61    }
62
63    fn finish_processing<I>(&mut self, remaining: I)
64    where
65        I: IntoIterator<Item = NodeId>,
66    {
67        self.dirty_nodes.extend(remaining);
68        self.is_processing = false;
69    }
70
71    fn clear(&mut self) {
72        self.dirty_nodes.clear();
73    }
74}
75
76pub(crate) struct SemanticsInvalidationState {
77    manager: RefCell<SemanticsInvalidationManager>,
78}
79
80impl SemanticsInvalidationState {
81    pub(crate) fn new() -> Self {
82        Self {
83            manager: RefCell::new(SemanticsInvalidationManager::new()),
84        }
85    }
86
87    fn schedule_invalidation(&self, node_id: NodeId) {
88        self.manager.borrow_mut().schedule_invalidation(node_id);
89    }
90
91    fn has_pending_invalidation(&self) -> bool {
92        self.manager.borrow().has_pending_invalidation()
93    }
94
95    fn process_invalidations<F>(&self, processor: F)
96    where
97        F: FnMut(NodeId),
98    {
99        let Some(nodes) = self.manager.borrow_mut().take_pending_for_processing() else {
100            return;
101        };
102
103        self.process_pending_nodes(nodes, processor);
104    }
105
106    fn clear(&self) {
107        self.manager.borrow_mut().clear();
108    }
109
110    fn process_pending_nodes<F>(&self, nodes: Vec<NodeId>, mut processor: F)
111    where
112        F: FnMut(NodeId),
113    {
114        let mut remaining = nodes.into_iter();
115        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
116            for node_id in remaining.by_ref() {
117                processor(node_id);
118            }
119        }));
120
121        self.manager.borrow_mut().finish_processing(remaining);
122
123        if let Err(payload) = result {
124            std::panic::resume_unwind(payload);
125        }
126    }
127}
128
129/// Marks `node_id`'s semantics for re-collection on the next frame of the app
130/// context that is current.
131///
132/// [`crate::SemanticsRequester::invalidate`] uses the by-id form instead: it
133/// remembers the context its node attached in, so a request raised from a frame
134/// loop or an event callback outside any context still reaches the right queue.
135pub fn schedule_semantics_invalidation(node_id: NodeId) {
136    crate::render_state::with_semantics_dispatch(|state| {
137        state.schedule_invalidation(node_id);
138    });
139}
140
141pub(crate) fn schedule_semantics_invalidation_in(
142    app_context: crate::render_state::AppContextId,
143    node_id: NodeId,
144) {
145    crate::render_state::with_semantics_dispatch_by_app_context(app_context, |state| {
146        state.schedule_invalidation(node_id);
147    });
148}
149
150/// Whether any semantics invalidations are waiting to be serviced.
151pub fn has_pending_semantics_invalidations() -> bool {
152    crate::render_state::with_semantics_dispatch(|state| state.has_pending_invalidation())
153}
154
155/// Services every pending semantics invalidation.
156///
157/// The host calls this once per frame with the applier available, and bubbles
158/// the dirty flag from each node to the root.
159pub fn process_semantics_invalidations<F>(processor: F)
160where
161    F: FnMut(NodeId),
162{
163    crate::render_state::with_semantics_dispatch(|state| state.process_invalidations(processor));
164}
165
166/// Drops every pending semantics invalidation without servicing it.
167pub fn clear_semantics_invalidations() {
168    crate::render_state::with_semantics_dispatch(|state| state.clear());
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn a_scheduled_node_is_handed_to_the_processor_once() {
177        let _app_context = crate::render_state::app_context_test_scope();
178        clear_semantics_invalidations();
179
180        schedule_semantics_invalidation(7);
181        schedule_semantics_invalidation(7);
182        schedule_semantics_invalidation(9);
183        assert!(has_pending_semantics_invalidations());
184
185        let mut seen = Vec::new();
186        process_semantics_invalidations(|node_id| seen.push(node_id));
187        seen.sort_unstable();
188        assert_eq!(seen, vec![7, 9]);
189        assert!(!has_pending_semantics_invalidations());
190    }
191
192    #[test]
193    fn nothing_is_pending_until_something_asks() {
194        let _app_context = crate::render_state::app_context_test_scope();
195        clear_semantics_invalidations();
196        assert!(!has_pending_semantics_invalidations());
197
198        let mut seen = 0;
199        process_semantics_invalidations(|_| seen += 1);
200        assert_eq!(seen, 0);
201    }
202}