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    /// Which node, in which app context, the recorder is attached to — learned
51    /// at attach time, because that is when a modifier node is told either.
52    ///
53    /// Shared rather than copied so that a requester cloned into a frame loop
54    /// before the first composition still learns the node when one attaches.
55    /// The app context is carried alongside the node so that `invalidate` can be
56    /// called from a frame loop or an event callback that is inside no context
57    /// at all, and still reach the right queue rather than a neighbour's.
58    binding: Rc<Cell<Option<SemanticsBinding>>>,
59}
60
61#[derive(Clone, Copy)]
62struct SemanticsBinding {
63    app_context: AppContextId,
64    node_id: NodeId,
65}
66
67impl SemanticsRequester {
68    pub fn new() -> Self {
69        Self::default()
70    }
71
72    /// Marks the attached node's semantics for re-collection on the next frame.
73    ///
74    /// Idempotent within a frame and cheap: the node joins a set the shell
75    /// drains once per frame. Before the node attaches, and after it detaches,
76    /// this does nothing — there is no tree to mark.
77    pub fn invalidate(&self) {
78        if let Some(binding) = self.binding.get() {
79            crate::semantics_dispatch::schedule_semantics_invalidation_in(
80                binding.app_context,
81                binding.node_id,
82            );
83        }
84    }
85
86    /// The layout node this requester is bound to, if it is attached.
87    pub fn node_id(&self) -> Option<NodeId> {
88        self.binding.get().map(|binding| binding.node_id)
89    }
90
91    fn bind(&self, binding: Option<SemanticsBinding>) {
92        self.binding.set(binding);
93    }
94
95    /// The binding for the node and context this call is running inside.
96    ///
97    /// `None` when either is unknown, which is the honest answer for a node the
98    /// applier has not given an id: a request naming no node cannot be serviced.
99    fn binding_here(node_id: Option<NodeId>) -> Option<SemanticsBinding> {
100        Some(SemanticsBinding {
101            app_context: crate::render_state::current_app_context_id_opt()?,
102            node_id: node_id?,
103        })
104    }
105}
106
107impl fmt::Debug for SemanticsRequester {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        f.debug_struct("SemanticsRequester")
110            .field("node_id", &self.node_id())
111            .finish()
112    }
113}
114
115pub struct SemanticsModifierNode {
116    recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>,
117    state: NodeState,
118}
119
120impl SemanticsModifierNode {
121    pub fn new(recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>) -> Self {
122        Self {
123            recorder,
124            state: NodeState::new(),
125        }
126    }
127}
128
129impl DelegatableNode for SemanticsModifierNode {
130    fn node_state(&self) -> &NodeState {
131        &self.state
132    }
133}
134
135impl ModifierNode for SemanticsModifierNode {
136    fn as_semantics_node(&self) -> Option<&dyn SemanticsNodeTrait> {
137        Some(self)
138    }
139
140    fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNodeTrait> {
141        Some(self)
142    }
143}
144
145/// Binds a [`SemanticsRequester`] to the layout node it is attached to.
146///
147/// Carries no capability of its own: it neither records semantics nor takes part
148/// in any pass. All it does is learn the node id, which a modifier node is only
149/// told at attach time.
150pub struct SemanticsRequesterNode {
151    state: NodeState,
152    requester: SemanticsRequester,
153}
154
155impl SemanticsRequesterNode {
156    pub(crate) fn new(requester: SemanticsRequester) -> Self {
157        Self {
158            state: NodeState::new(),
159            requester,
160        }
161    }
162}
163
164impl DelegatableNode for SemanticsRequesterNode {
165    fn node_state(&self) -> &NodeState {
166        &self.state
167    }
168}
169
170impl ModifierNode for SemanticsRequesterNode {
171    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
172        self.state.set_attached(true);
173        self.requester
174            .bind(SemanticsRequester::binding_here(context.node_id()));
175    }
176
177    fn on_detach(&mut self) {
178        self.state.set_attached(false);
179        // Requests raised after this point would name a node that is gone, and
180        // the queue is keyed by node id, so unbind rather than leave a stale one.
181        self.requester.bind(None);
182    }
183}
184
185/// Modifier element for [`SemanticsRequester`].
186#[derive(Clone)]
187pub struct SemanticsRequesterElement {
188    requester: SemanticsRequester,
189}
190
191impl SemanticsRequesterElement {
192    pub(crate) fn new(requester: SemanticsRequester) -> Self {
193        Self { requester }
194    }
195}
196
197impl fmt::Debug for SemanticsRequesterElement {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        f.write_str("SemanticsRequesterElement")
200    }
201}
202
203impl PartialEq for SemanticsRequesterElement {
204    fn eq(&self, other: &Self) -> bool {
205        Rc::ptr_eq(&self.requester.binding, &other.requester.binding)
206    }
207}
208
209impl Eq for SemanticsRequesterElement {}
210
211impl Hash for SemanticsRequesterElement {
212    fn hash<H: Hasher>(&self, state: &mut H) {
213        Rc::as_ptr(&self.requester.binding).hash(state);
214    }
215}
216
217impl ModifierNodeElement for SemanticsRequesterElement {
218    type Node = SemanticsRequesterNode;
219
220    fn create(&self) -> Self::Node {
221        SemanticsRequesterNode::new(self.requester.clone())
222    }
223
224    fn update(&self, node: &mut Self::Node) {
225        // A recomposition that hands over a different requester moves the
226        // binding with it: the old one must stop naming this node, and the new
227        // one must start.
228        if !Rc::ptr_eq(&node.requester.binding, &self.requester.binding) {
229            let bound = node.requester.binding.get();
230            node.requester.bind(None);
231            node.requester = self.requester.clone();
232            node.requester.bind(bound);
233        }
234    }
235
236    fn inspector_name(&self) -> &'static str {
237        "semanticsRequester"
238    }
239
240    fn capabilities(&self) -> NodeCapabilities {
241        NodeCapabilities::NONE
242    }
243}
244
245impl SemanticsNodeTrait for SemanticsModifierNode {
246    fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
247        (self.recorder)(config);
248    }
249}
250
251#[derive(Clone)]
252pub struct SemanticsElement {
253    recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>,
254}
255
256impl SemanticsElement {
257    /// Takes an already shared recorder so the caller can run the same closure
258    /// for the inspector preview without asking the app to record twice.
259    pub fn new(recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>) -> Self {
260        Self { recorder }
261    }
262}
263
264impl fmt::Debug for SemanticsElement {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        f.write_str("SemanticsElement")
267    }
268}
269
270impl PartialEq for SemanticsElement {
271    fn eq(&self, _other: &Self) -> bool {
272        // Type matching is sufficient - node will be updated via update() method
273        // This matches JC behavior where nodes are reused for same-type elements,
274        // preventing unnecessary modifier chain recreation
275        true
276    }
277}
278
279impl Eq for SemanticsElement {}
280
281impl Hash for SemanticsElement {
282    fn hash<H: Hasher>(&self, state: &mut H) {
283        // Consistent hash for type-based matching
284        "semantics".hash(state);
285    }
286}
287
288impl ModifierNodeElement for SemanticsElement {
289    type Node = SemanticsModifierNode;
290
291    fn create(&self) -> Self::Node {
292        SemanticsModifierNode::new(self.recorder.clone())
293    }
294
295    fn update(&self, node: &mut Self::Node) {
296        node.recorder = self.recorder.clone();
297    }
298
299    fn capabilities(&self) -> NodeCapabilities {
300        NodeCapabilities::SEMANTICS
301    }
302
303    fn always_update(&self) -> bool {
304        // Recorder closure might change
305        true
306    }
307}
308
309fn merge_semantics_from_node(node: &dyn ModifierNode, config: &mut SemanticsConfiguration) -> bool {
310    let mut merged = false;
311
312    if let Some(semantics) = node.as_semantics_node() {
313        semantics.merge_semantics(config);
314        merged = true;
315    }
316
317    node.for_each_delegate(&mut |delegate| {
318        if merge_semantics_from_node(delegate, config) {
319            merged = true;
320        }
321    });
322
323    merged
324}
325
326/// Collects semantics contributed by a reconciled modifier chain.
327pub fn collect_semantics_from_chain(chain: &ModifierNodeChain) -> Option<SemanticsConfiguration> {
328    if !chain.has_capability(NodeCapabilities::SEMANTICS) {
329        return None;
330    }
331
332    let mut config = SemanticsConfiguration::default();
333    let mut merged = false;
334    chain.for_each_node_with_capability(NodeCapabilities::SEMANTICS, |_ref, node| {
335        if merge_semantics_from_node(node, &mut config) {
336            merged = true;
337        }
338    });
339
340    if merged {
341        Some(config)
342    } else {
343        None
344    }
345}
346
347/// Collects semantics by instantiating a temporary modifier chain from a [`Modifier`].
348pub fn collect_semantics_from_modifier(modifier: &Modifier) -> Option<SemanticsConfiguration> {
349    let mut handle = ModifierChainHandle::new();
350    handle.update(modifier);
351    collect_semantics_from_chain(handle.chain())
352}