Skip to main content

cranpose_ui/modifier/
semantics.rs

1use std::cell::Cell;
2use std::fmt;
3use std::hash::{Hash, Hasher};
4use std::rc::Rc;
5
6use cranpose_core::NodeId;
7use cranpose_foundation::{
8    DelegatableNode, ModifierNode, ModifierNodeChain, ModifierNodeContext, ModifierNodeElement,
9    NodeCapabilities, NodeState, SemanticsConfiguration, SemanticsNode as SemanticsNodeTrait,
10};
11
12use super::{Modifier, ModifierChainHandle};
13use crate::render_state::AppContextId;
14
15/// A handle an app holds to say that what its semantics recorder would report
16/// has changed.
17///
18/// A `.semantics()` recorder is re-run on every collection, so it always reports
19/// the app's current answer. It cannot say *when* that answer changed, and a
20/// tree is only re-collected when some node marks its semantics dirty — which
21/// until now meant attaching a node, rebuilding its modifier chain, or a layout
22/// pass. A screen that is one `Canvas` does none of those: its layout never
23/// changes and its frame loop draws rather than recomposes, so the tree it
24/// published at boot was the tree a screen reader kept reading.
25///
26/// This is the way out, and it is Jetpack Compose's:
27/// `SemanticsModifierNode.invalidateSemantics()`. Remember one requester, hang
28/// it on the same node as the recorder, and call [`invalidate`] when the content
29/// changes — no recomposition, no layout pass.
30///
31/// ```ignore
32/// let semantics = remember(SemanticsRequester::new).with(Clone::clone);
33/// // ... in the composition:
34/// Modifier::empty()
35///     .semantics_requester(&semantics)
36///     .semantics(move |config| { /* reads live app state */ })
37/// // ... and in the frame loop, guarded by a revision so an unchanged tree is
38/// // not republished every frame:
39/// if revision != last_revision {
40///     last_revision = revision;
41///     semantics.invalidate();
42/// }
43/// ```
44///
45/// [`invalidate`]: SemanticsRequester::invalidate
46#[derive(Clone, Default)]
47pub struct SemanticsRequester {
48    /// Which node, in which app context, the recorder is attached to — learned
49    /// at attach time, because that is when a modifier node is told either.
50    ///
51    /// Shared rather than copied so that a requester cloned into a frame loop
52    /// before the first composition still learns the node when one attaches.
53    /// The app context is carried alongside the node so that `invalidate` can be
54    /// called from a frame loop or an event callback that is inside no context
55    /// at all, and still reach the right queue rather than a neighbour's.
56    binding: Rc<Cell<Option<SemanticsBinding>>>,
57}
58
59#[derive(Clone, Copy)]
60struct SemanticsBinding {
61    app_context: AppContextId,
62    node_id: NodeId,
63}
64
65impl SemanticsRequester {
66    pub fn new() -> Self {
67        Self::default()
68    }
69
70    /// Marks the attached node's semantics for re-collection on the next frame.
71    ///
72    /// Idempotent within a frame and cheap: the node joins a set the shell
73    /// drains once per frame. Before the node attaches, and after it detaches,
74    /// this does nothing — there is no tree to mark.
75    pub fn invalidate(&self) {
76        if let Some(binding) = self.binding.get() {
77            crate::semantics_dispatch::schedule_semantics_invalidation_in(
78                binding.app_context,
79                binding.node_id,
80            );
81        }
82    }
83
84    /// The layout node this requester is bound to, if it is attached.
85    pub fn node_id(&self) -> Option<NodeId> {
86        self.binding.get().map(|binding| binding.node_id)
87    }
88
89    fn bind(&self, binding: Option<SemanticsBinding>) {
90        self.binding.set(binding);
91    }
92
93    /// The binding for the node and context this call is running inside.
94    ///
95    /// `None` when either is unknown, which is the honest answer for a node the
96    /// applier has not given an id: a request naming no node cannot be serviced.
97    fn binding_here(node_id: Option<NodeId>) -> Option<SemanticsBinding> {
98        Some(SemanticsBinding {
99            app_context: crate::render_state::current_app_context_id_opt()?,
100            node_id: node_id?,
101        })
102    }
103}
104
105impl fmt::Debug for SemanticsRequester {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        f.debug_struct("SemanticsRequester")
108            .field("node_id", &self.node_id())
109            .finish()
110    }
111}
112
113pub struct SemanticsModifierNode {
114    recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>,
115    state: NodeState,
116}
117
118impl SemanticsModifierNode {
119    pub fn new(recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>) -> Self {
120        Self {
121            recorder,
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
143/// Binds a [`SemanticsRequester`] to the layout node it is attached to.
144///
145/// Carries no capability of its own: it neither records semantics nor takes part
146/// in any pass. All it does is learn the node id, which a modifier node is only
147/// told at attach time.
148pub struct SemanticsRequesterNode {
149    state: NodeState,
150    requester: SemanticsRequester,
151}
152
153impl SemanticsRequesterNode {
154    pub(crate) fn new(requester: SemanticsRequester) -> Self {
155        Self {
156            state: NodeState::new(),
157            requester,
158        }
159    }
160}
161
162impl DelegatableNode for SemanticsRequesterNode {
163    fn node_state(&self) -> &NodeState {
164        &self.state
165    }
166}
167
168impl ModifierNode for SemanticsRequesterNode {
169    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
170        self.state.set_attached(true);
171        self.requester
172            .bind(SemanticsRequester::binding_here(context.node_id()));
173    }
174
175    fn on_detach(&mut self) {
176        self.state.set_attached(false);
177        // Requests raised after this point would name a node that is gone, and
178        // the queue is keyed by node id, so unbind rather than leave a stale one.
179        self.requester.bind(None);
180    }
181}
182
183/// Modifier element for [`SemanticsRequester`].
184#[derive(Clone)]
185pub struct SemanticsRequesterElement {
186    requester: SemanticsRequester,
187}
188
189impl SemanticsRequesterElement {
190    pub(crate) fn new(requester: SemanticsRequester) -> Self {
191        Self { requester }
192    }
193}
194
195impl fmt::Debug for SemanticsRequesterElement {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        f.write_str("SemanticsRequesterElement")
198    }
199}
200
201impl PartialEq for SemanticsRequesterElement {
202    fn eq(&self, other: &Self) -> bool {
203        Rc::ptr_eq(&self.requester.binding, &other.requester.binding)
204    }
205}
206
207impl Eq for SemanticsRequesterElement {}
208
209impl Hash for SemanticsRequesterElement {
210    fn hash<H: Hasher>(&self, state: &mut H) {
211        Rc::as_ptr(&self.requester.binding).hash(state);
212    }
213}
214
215impl ModifierNodeElement for SemanticsRequesterElement {
216    type Node = SemanticsRequesterNode;
217
218    fn create(&self) -> Self::Node {
219        SemanticsRequesterNode::new(self.requester.clone())
220    }
221
222    fn update(&self, node: &mut Self::Node) {
223        // A recomposition that hands over a different requester moves the
224        // binding with it: the old one must stop naming this node, and the new
225        // one must start.
226        if !Rc::ptr_eq(&node.requester.binding, &self.requester.binding) {
227            let bound = node.requester.binding.get();
228            node.requester.bind(None);
229            node.requester = self.requester.clone();
230            node.requester.bind(bound);
231        }
232    }
233
234    fn inspector_name(&self) -> &'static str {
235        "semanticsRequester"
236    }
237
238    fn capabilities(&self) -> NodeCapabilities {
239        NodeCapabilities::NONE
240    }
241}
242
243impl SemanticsNodeTrait for SemanticsModifierNode {
244    fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
245        (self.recorder)(config);
246    }
247}
248
249#[derive(Clone)]
250pub struct SemanticsElement {
251    recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>,
252}
253
254impl SemanticsElement {
255    /// Takes an already shared recorder so the caller can run the same closure
256    /// for the inspector preview without asking the app to record twice.
257    pub fn new(recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>) -> Self {
258        Self { recorder }
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        // Type matching is sufficient - node will be updated via update() method
271        // This matches JC behavior where nodes are reused for same-type elements,
272        // preventing unnecessary modifier chain recreation
273        true
274    }
275}
276
277impl Eq for SemanticsElement {}
278
279impl Hash for SemanticsElement {
280    fn hash<H: Hasher>(&self, state: &mut H) {
281        // Consistent hash for type-based matching
282        "semantics".hash(state);
283    }
284}
285
286impl ModifierNodeElement for SemanticsElement {
287    type Node = SemanticsModifierNode;
288
289    fn create(&self) -> Self::Node {
290        SemanticsModifierNode::new(self.recorder.clone())
291    }
292
293    fn update(&self, node: &mut Self::Node) {
294        node.recorder = self.recorder.clone();
295    }
296
297    fn capabilities(&self) -> NodeCapabilities {
298        NodeCapabilities::SEMANTICS
299    }
300
301    fn always_update(&self) -> bool {
302        // Recorder closure might change
303        true
304    }
305}
306
307fn merge_semantics_from_node(node: &dyn ModifierNode, config: &mut SemanticsConfiguration) -> bool {
308    let mut merged = false;
309
310    if let Some(semantics) = node.as_semantics_node() {
311        semantics.merge_semantics(config);
312        merged = true;
313    }
314
315    node.for_each_delegate(&mut |delegate| {
316        if merge_semantics_from_node(delegate, config) {
317            merged = true;
318        }
319    });
320
321    merged
322}
323
324/// Collects semantics contributed by a reconciled modifier chain.
325pub fn collect_semantics_from_chain(chain: &ModifierNodeChain) -> Option<SemanticsConfiguration> {
326    if !chain.has_capability(NodeCapabilities::SEMANTICS) {
327        return None;
328    }
329
330    let mut config = SemanticsConfiguration::default();
331    let mut merged = false;
332    chain.for_each_node_with_capability(NodeCapabilities::SEMANTICS, |_ref, node| {
333        if merge_semantics_from_node(node, &mut config) {
334            merged = true;
335        }
336    });
337
338    if merged {
339        Some(config)
340    } else {
341        None
342    }
343}
344
345/// Collects semantics by instantiating a temporary modifier chain from a [`Modifier`].
346pub fn collect_semantics_from_modifier(modifier: &Modifier) -> Option<SemanticsConfiguration> {
347    let mut handle = ModifierChainHandle::new();
348    handle.update(modifier);
349    collect_semantics_from_chain(handle.chain())
350}