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.state.set_attached(true);
166        self.requester
167            .bind(SemanticsRequester::binding_here(context.node_id()));
168    }
169
170    fn on_detach(&mut self) {
171        self.state.set_attached(false);
172        self.requester.bind(None);
173    }
174}
175
176#[derive(Clone)]
177pub(crate) struct SemanticsRequesterElement {
178    requester: SemanticsRequester,
179}
180
181impl SemanticsRequesterElement {
182    pub(crate) fn new(requester: SemanticsRequester) -> Self {
183        Self { requester }
184    }
185}
186
187impl fmt::Debug for SemanticsRequesterElement {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        f.write_str("SemanticsRequesterElement")
190    }
191}
192
193impl PartialEq for SemanticsRequesterElement {
194    fn eq(&self, other: &Self) -> bool {
195        Rc::ptr_eq(&self.requester.binding, &other.requester.binding)
196    }
197}
198
199impl Eq for SemanticsRequesterElement {}
200
201impl Hash for SemanticsRequesterElement {
202    fn hash<H: Hasher>(&self, state: &mut H) {
203        Rc::as_ptr(&self.requester.binding).hash(state);
204    }
205}
206
207impl ModifierNodeElement for SemanticsRequesterElement {
208    type Node = SemanticsRequesterNode;
209
210    fn create(&self) -> Self::Node {
211        SemanticsRequesterNode::new(self.requester.clone())
212    }
213
214    fn update(&self, node: &mut Self::Node) {
215        if !Rc::ptr_eq(&node.requester.binding, &self.requester.binding) {
216            let bound = node.requester.binding.get();
217            node.requester.bind(None);
218            node.requester = self.requester.clone();
219            node.requester.bind(bound);
220        }
221    }
222
223    fn inspector_name(&self) -> &'static str {
224        "semanticsRequester"
225    }
226
227    fn capabilities(&self) -> NodeCapabilities {
228        NodeCapabilities::NONE
229    }
230}
231
232impl SemanticsNodeTrait for SemanticsModifierNode {
233    fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
234        (self.recorder)(config);
235    }
236
237    fn reach(&self) -> cranpose_foundation::SemanticsReach {
238        cranpose_foundation::SemanticsReach::merged_by(self, self.reads_live_state)
239    }
240}
241
242#[derive(Clone)]
243pub(super) struct SemanticsElement {
244    recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>,
245    reads_live_state: bool,
246}
247
248impl SemanticsElement {
249    /// An element running `recorder`; `reads_live_state` says the recorder
250    /// may read state it did not capture.
251    pub(super) fn new(
252        recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>,
253        reads_live_state: bool,
254    ) -> Self {
255        Self {
256            recorder,
257            reads_live_state,
258        }
259    }
260}
261
262impl fmt::Debug for SemanticsElement {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        f.write_str("SemanticsElement")
265    }
266}
267
268impl PartialEq for SemanticsElement {
269    fn eq(&self, _other: &Self) -> bool {
270        true
271    }
272}
273
274impl Eq for SemanticsElement {}
275
276impl Hash for SemanticsElement {
277    fn hash<H: Hasher>(&self, state: &mut H) {
278        "semantics".hash(state);
279    }
280}
281
282impl ModifierNodeElement for SemanticsElement {
283    type Node = SemanticsModifierNode;
284
285    fn create(&self) -> Self::Node {
286        SemanticsModifierNode::new(Rc::clone(&self.recorder), self.reads_live_state)
287    }
288
289    fn update(&self, node: &mut Self::Node) {
290        node.recorder = Rc::clone(&self.recorder);
291        node.reads_live_state = self.reads_live_state;
292    }
293
294    fn capabilities(&self) -> NodeCapabilities {
295        NodeCapabilities::SEMANTICS
296    }
297
298    fn always_update(&self) -> bool {
299        true
300    }
301}
302
303fn merge_semantics_from_node(node: &dyn ModifierNode, config: &mut SemanticsConfiguration) -> bool {
304    let mut merged = false;
305
306    if let Some(semantics) = node.as_semantics_node() {
307        semantics.merge_semantics(config);
308        merged = true;
309    }
310
311    node.for_each_delegate(&mut |delegate| {
312        if merge_semantics_from_node(delegate, config) {
313            merged = true;
314        }
315    });
316
317    merged
318}
319
320pub(crate) fn collect_semantics_from_chain(
321    chain: &ModifierNodeChain,
322) -> Option<SemanticsConfiguration> {
323    if !chain.has_capability(NodeCapabilities::SEMANTICS) {
324        return None;
325    }
326
327    let mut config = SemanticsConfiguration::default();
328    let mut merged = false;
329    chain.for_each_node_with_capability(NodeCapabilities::SEMANTICS, |_ref, node| {
330        if merge_semantics_from_node(node, &mut config) {
331            merged = true;
332        }
333    });
334
335    if merged { Some(config) } else { None }
336}
337
338fn reach_of_node(node: &dyn ModifierNode) -> cranpose_foundation::SemanticsReach {
339    let mut reach = node
340        .as_semantics_node()
341        .map(cranpose_foundation::SemanticsNode::reach)
342        .unwrap_or_default();
343    node.for_each_delegate(&mut |delegate| {
344        reach = reach.union(reach_of_node(delegate));
345    });
346    reach
347}
348
349pub(crate) fn semantics_reach_of_chain(
350    chain: &ModifierNodeChain,
351) -> cranpose_foundation::SemanticsReach {
352    let mut reach = cranpose_foundation::SemanticsReach::default();
353    if !chain.has_capability(NodeCapabilities::SEMANTICS) {
354        return reach;
355    }
356    chain.for_each_node_with_capability(NodeCapabilities::SEMANTICS, |_ref, node| {
357        reach = reach.union(reach_of_node(node));
358    });
359    reach
360}
361
362/// Collects semantics by instantiating a temporary modifier chain from a [`Modifier`].
363pub fn collect_semantics_from_modifier(modifier: &Modifier) -> Option<SemanticsConfiguration> {
364    let mut handle = ModifierChainHandle::new();
365    handle.update(modifier);
366    collect_semantics_from_chain(handle.chain())
367}