Skip to main content

cranpose_ui/modifier/
semantics.rs

1use std::{
2    cell::Cell,
3    fmt,
4    hash::{Hash, Hasher},
5    rc::Rc,
6};
7
8use cranpose_core::NodeId;
9use cranpose_foundation::{
10    DelegatableNode, ModifierNode, ModifierNodeChain, ModifierNodeContext, ModifierNodeElement,
11    NodeCapabilities, NodeState, SemanticsConfiguration, SemanticsNode as SemanticsNodeTrait,
12};
13
14use super::{Modifier, ModifierChainHandle};
15use crate::render_state::AppContextId;
16
17/// A handle an app holds to say that what its semantics recorder would report
18/// has changed.
19///
20/// A `.semantics()` recorder is re-run on every collection, so it always reports
21/// the app's current answer. It cannot say *when* that answer changed, and a
22/// tree is only re-collected when some node marks its semantics dirty — which
23/// until now meant attaching a node, rebuilding its modifier chain, or a layout
24/// pass. A screen that is one `Canvas` does none of those: its layout never
25/// changes and its frame loop draws rather than recomposes, so the tree it
26/// published at boot was the tree a screen reader kept reading.
27///
28/// This is the way out, and it is Jetpack Compose's:
29/// `SemanticsModifierNode.invalidateSemantics()`. Remember one requester, hang
30/// it on the same node as the recorder, and call [`invalidate`] when the content
31/// changes — no recomposition, no layout pass.
32///
33/// ```ignore
34/// let semantics = remember(SemanticsRequester::new).with(Clone::clone);
35/// // ... in the composition:
36/// Modifier::empty()
37///     .semantics_requester(&semantics)
38///     .semantics(move |config| { /* reads live app state */ })
39/// // ... and in the frame loop, guarded by a revision so an unchanged tree is
40/// // not republished every frame:
41/// if revision != last_revision {
42///     last_revision = revision;
43///     semantics.invalidate();
44/// }
45/// ```
46///
47/// [`invalidate`]: SemanticsRequester::invalidate
48#[derive(Clone, Default)]
49pub struct SemanticsRequester {
50    binding: Rc<Cell<Option<SemanticsBinding>>>,
51}
52
53#[derive(Clone, Copy)]
54struct SemanticsBinding {
55    app_context: AppContextId,
56    node_id: NodeId,
57}
58
59impl SemanticsRequester {
60    /// Creates a requester that no node uses yet. [`Self::invalidate`] does nothing until a
61    /// `semantics_requester` modifier attaches it.
62    pub fn new() -> Self {
63        Self::default()
64    }
65
66    /// Marks the attached node's semantics for re-collection on the next frame.
67    ///
68    /// Idempotent within a frame and cheap: the node joins a set the shell
69    /// drains once per frame. Before the node attaches, and after it detaches,
70    /// this does nothing — there is no tree to mark.
71    pub fn invalidate(&self) {
72        if let Some(binding) = self.binding.get() {
73            crate::semantics_dispatch::schedule_semantics_invalidation_in(
74                binding.app_context,
75                binding.node_id,
76            );
77        }
78    }
79
80    /// The layout node this requester is bound to, if it is attached.
81    pub fn node_id(&self) -> Option<NodeId> {
82        self.binding.get().map(|binding| binding.node_id)
83    }
84
85    fn bind(&self, binding: Option<SemanticsBinding>) {
86        self.binding.set(binding);
87    }
88
89    fn binding_here(node_id: Option<NodeId>) -> Option<SemanticsBinding> {
90        Some(SemanticsBinding {
91            app_context: crate::render_state::current_app_context_id_opt()?,
92            node_id: node_id?,
93        })
94    }
95}
96
97impl fmt::Debug for SemanticsRequester {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        f.debug_struct("SemanticsRequester")
100            .field("node_id", &self.node_id())
101            .finish()
102    }
103}
104
105pub(super) struct SemanticsModifierNode {
106    recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>,
107    /// The recorder may read state it did not capture.
108    reads_live_state: bool,
109    state: NodeState,
110}
111
112impl SemanticsModifierNode {
113    /// A node running `recorder`; `reads_live_state` says the recorder may
114    /// read state it did not capture, so every semantics update runs it.
115    pub(super) fn new(
116        recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>,
117        reads_live_state: bool,
118    ) -> Self {
119        Self {
120            recorder,
121            reads_live_state,
122            state: NodeState::new(),
123        }
124    }
125}
126
127impl DelegatableNode for SemanticsModifierNode {
128    fn node_state(&self) -> &NodeState {
129        &self.state
130    }
131}
132
133impl ModifierNode for SemanticsModifierNode {
134    fn as_semantics_node(&self) -> Option<&dyn SemanticsNodeTrait> {
135        Some(self)
136    }
137
138    fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNodeTrait> {
139        Some(self)
140    }
141}
142
143pub(crate) struct SemanticsRequesterNode {
144    state: NodeState,
145    requester: SemanticsRequester,
146}
147
148impl SemanticsRequesterNode {
149    pub(crate) fn new(requester: SemanticsRequester) -> Self {
150        Self {
151            state: NodeState::new(),
152            requester,
153        }
154    }
155}
156
157impl DelegatableNode for SemanticsRequesterNode {
158    fn node_state(&self) -> &NodeState {
159        &self.state
160    }
161}
162
163impl ModifierNode for SemanticsRequesterNode {
164    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
165        self.requester
166            .bind(SemanticsRequester::binding_here(context.node_id()));
167    }
168
169    fn on_detach(&mut self) {
170        self.requester.bind(None);
171    }
172}
173
174#[derive(Clone)]
175pub(crate) struct SemanticsRequesterElement {
176    requester: SemanticsRequester,
177}
178
179impl SemanticsRequesterElement {
180    pub(crate) fn new(requester: SemanticsRequester) -> Self {
181        Self { requester }
182    }
183}
184
185impl fmt::Debug for SemanticsRequesterElement {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        f.write_str("SemanticsRequesterElement")
188    }
189}
190
191impl PartialEq for SemanticsRequesterElement {
192    fn eq(&self, other: &Self) -> bool {
193        Rc::ptr_eq(&self.requester.binding, &other.requester.binding)
194    }
195}
196
197impl Eq for SemanticsRequesterElement {}
198
199impl Hash for SemanticsRequesterElement {
200    fn hash<H: Hasher>(&self, state: &mut H) {
201        Rc::as_ptr(&self.requester.binding).hash(state);
202    }
203}
204
205impl ModifierNodeElement for SemanticsRequesterElement {
206    type Node = SemanticsRequesterNode;
207
208    fn create(&self) -> Self::Node {
209        SemanticsRequesterNode::new(self.requester.clone())
210    }
211
212    fn update(&self, node: &mut Self::Node) {
213        if !Rc::ptr_eq(&node.requester.binding, &self.requester.binding) {
214            let bound = node.requester.binding.get();
215            node.requester.bind(None);
216            node.requester = self.requester.clone();
217            node.requester.bind(bound);
218        }
219    }
220
221    fn inspector_name(&self) -> &'static str {
222        "semanticsRequester"
223    }
224
225    fn capabilities(&self) -> NodeCapabilities {
226        NodeCapabilities::NONE
227    }
228}
229
230impl SemanticsNodeTrait for SemanticsModifierNode {
231    fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
232        (self.recorder)(config);
233    }
234
235    fn reach(&self) -> cranpose_foundation::SemanticsReach {
236        cranpose_foundation::SemanticsReach::merged_by(self, self.reads_live_state)
237    }
238}
239
240#[derive(Clone)]
241pub(super) struct SemanticsElement {
242    recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>,
243    reads_live_state: bool,
244}
245
246impl SemanticsElement {
247    /// An element running `recorder`; `reads_live_state` says the recorder
248    /// may read state it did not capture.
249    pub(super) fn new(
250        recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>,
251        reads_live_state: bool,
252    ) -> Self {
253        Self {
254            recorder,
255            reads_live_state,
256        }
257    }
258}
259
260impl fmt::Debug for SemanticsElement {
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        f.write_str("SemanticsElement")
263    }
264}
265
266impl PartialEq for SemanticsElement {
267    fn eq(&self, _other: &Self) -> bool {
268        true
269    }
270}
271
272impl Eq for SemanticsElement {}
273
274impl Hash for SemanticsElement {
275    fn hash<H: Hasher>(&self, state: &mut H) {
276        "semantics".hash(state);
277    }
278}
279
280impl ModifierNodeElement for SemanticsElement {
281    type Node = SemanticsModifierNode;
282
283    fn create(&self) -> Self::Node {
284        SemanticsModifierNode::new(Rc::clone(&self.recorder), self.reads_live_state)
285    }
286
287    fn update(&self, node: &mut Self::Node) {
288        node.recorder = Rc::clone(&self.recorder);
289        node.reads_live_state = self.reads_live_state;
290    }
291
292    fn capabilities(&self) -> NodeCapabilities {
293        NodeCapabilities::SEMANTICS
294    }
295
296    fn always_update(&self) -> bool {
297        true
298    }
299}
300
301fn merge_semantics_from_node(node: &dyn ModifierNode, config: &mut SemanticsConfiguration) -> bool {
302    let mut merged = false;
303
304    if let Some(semantics) = node.as_semantics_node() {
305        semantics.merge_semantics(config);
306        merged = true;
307    }
308
309    node.for_each_delegate(&mut |delegate| {
310        if merge_semantics_from_node(delegate, config) {
311            merged = true;
312        }
313    });
314
315    merged
316}
317
318pub(crate) fn collect_semantics_from_chain(
319    chain: &ModifierNodeChain,
320) -> Option<SemanticsConfiguration> {
321    if !chain.has_capability(NodeCapabilities::SEMANTICS) {
322        return None;
323    }
324
325    let mut config = SemanticsConfiguration::default();
326    let mut merged = false;
327    chain.for_each_node_with_capability(NodeCapabilities::SEMANTICS, |_ref, node| {
328        if merge_semantics_from_node(node, &mut config) {
329            merged = true;
330        }
331    });
332
333    if merged { Some(config) } else { None }
334}
335
336fn reach_of_node(node: &dyn ModifierNode) -> cranpose_foundation::SemanticsReach {
337    let mut reach = node
338        .as_semantics_node()
339        .map(cranpose_foundation::SemanticsNode::reach)
340        .unwrap_or_default();
341    node.for_each_delegate(&mut |delegate| {
342        reach = reach.union(reach_of_node(delegate));
343    });
344    reach
345}
346
347pub(crate) fn semantics_reach_of_chain(
348    chain: &ModifierNodeChain,
349) -> cranpose_foundation::SemanticsReach {
350    let mut reach = cranpose_foundation::SemanticsReach::default();
351    if !chain.has_capability(NodeCapabilities::SEMANTICS) {
352        return reach;
353    }
354    chain.for_each_node_with_capability(NodeCapabilities::SEMANTICS, |_ref, node| {
355        reach = reach.union(reach_of_node(node));
356    });
357    reach
358}
359
360/// Collects semantics by instantiating a temporary modifier chain from a [`Modifier`].
361pub fn collect_semantics_from_modifier(modifier: &Modifier) -> Option<SemanticsConfiguration> {
362    let mut handle = ModifierChainHandle::new();
363    handle.update(modifier);
364    collect_semantics_from_chain(handle.chain())
365}