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    pub fn new() -> Self {
61        Self::default()
62    }
63
64    /// Marks the attached node's semantics for re-collection on the next frame.
65    ///
66    /// Idempotent within a frame and cheap: the node joins a set the shell
67    /// drains once per frame. Before the node attaches, and after it detaches,
68    /// this does nothing — there is no tree to mark.
69    pub fn invalidate(&self) {
70        if let Some(binding) = self.binding.get() {
71            crate::semantics_dispatch::schedule_semantics_invalidation_in(
72                binding.app_context,
73                binding.node_id,
74            );
75        }
76    }
77
78    /// The layout node this requester is bound to, if it is attached.
79    pub fn node_id(&self) -> Option<NodeId> {
80        self.binding.get().map(|binding| binding.node_id)
81    }
82
83    fn bind(&self, binding: Option<SemanticsBinding>) {
84        self.binding.set(binding);
85    }
86
87    fn binding_here(node_id: Option<NodeId>) -> Option<SemanticsBinding> {
88        Some(SemanticsBinding {
89            app_context: crate::render_state::current_app_context_id_opt()?,
90            node_id: node_id?,
91        })
92    }
93}
94
95impl fmt::Debug for SemanticsRequester {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.debug_struct("SemanticsRequester")
98            .field("node_id", &self.node_id())
99            .finish()
100    }
101}
102
103pub struct SemanticsModifierNode {
104    recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>,
105    state: NodeState,
106}
107
108impl SemanticsModifierNode {
109    pub fn new(recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>) -> Self {
110        Self {
111            recorder,
112            state: NodeState::new(),
113        }
114    }
115}
116
117impl DelegatableNode for SemanticsModifierNode {
118    fn node_state(&self) -> &NodeState {
119        &self.state
120    }
121}
122
123impl ModifierNode for SemanticsModifierNode {
124    fn as_semantics_node(&self) -> Option<&dyn SemanticsNodeTrait> {
125        Some(self)
126    }
127
128    fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNodeTrait> {
129        Some(self)
130    }
131}
132
133pub struct SemanticsRequesterNode {
134    state: NodeState,
135    requester: SemanticsRequester,
136}
137
138impl SemanticsRequesterNode {
139    pub(crate) fn new(requester: SemanticsRequester) -> Self {
140        Self {
141            state: NodeState::new(),
142            requester,
143        }
144    }
145}
146
147impl DelegatableNode for SemanticsRequesterNode {
148    fn node_state(&self) -> &NodeState {
149        &self.state
150    }
151}
152
153impl ModifierNode for SemanticsRequesterNode {
154    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
155        self.state.set_attached(true);
156        self.requester
157            .bind(SemanticsRequester::binding_here(context.node_id()));
158    }
159
160    fn on_detach(&mut self) {
161        self.state.set_attached(false);
162        self.requester.bind(None);
163    }
164}
165
166/// Modifier element for [`SemanticsRequester`].
167#[derive(Clone)]
168pub struct SemanticsRequesterElement {
169    requester: SemanticsRequester,
170}
171
172impl SemanticsRequesterElement {
173    pub(crate) fn new(requester: SemanticsRequester) -> Self {
174        Self { requester }
175    }
176}
177
178impl fmt::Debug for SemanticsRequesterElement {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        f.write_str("SemanticsRequesterElement")
181    }
182}
183
184impl PartialEq for SemanticsRequesterElement {
185    fn eq(&self, other: &Self) -> bool {
186        Rc::ptr_eq(&self.requester.binding, &other.requester.binding)
187    }
188}
189
190impl Eq for SemanticsRequesterElement {}
191
192impl Hash for SemanticsRequesterElement {
193    fn hash<H: Hasher>(&self, state: &mut H) {
194        Rc::as_ptr(&self.requester.binding).hash(state);
195    }
196}
197
198impl ModifierNodeElement for SemanticsRequesterElement {
199    type Node = SemanticsRequesterNode;
200
201    fn create(&self) -> Self::Node {
202        SemanticsRequesterNode::new(self.requester.clone())
203    }
204
205    fn update(&self, node: &mut Self::Node) {
206        if !Rc::ptr_eq(&node.requester.binding, &self.requester.binding) {
207            let bound = node.requester.binding.get();
208            node.requester.bind(None);
209            node.requester = self.requester.clone();
210            node.requester.bind(bound);
211        }
212    }
213
214    fn inspector_name(&self) -> &'static str {
215        "semanticsRequester"
216    }
217
218    fn capabilities(&self) -> NodeCapabilities {
219        NodeCapabilities::NONE
220    }
221}
222
223impl SemanticsNodeTrait for SemanticsModifierNode {
224    fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
225        (self.recorder)(config);
226    }
227}
228
229#[derive(Clone)]
230pub struct SemanticsElement {
231    recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>,
232}
233
234impl SemanticsElement {
235    pub fn new(recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>) -> Self {
236        Self { recorder }
237    }
238}
239
240impl fmt::Debug for SemanticsElement {
241    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        f.write_str("SemanticsElement")
243    }
244}
245
246impl PartialEq for SemanticsElement {
247    fn eq(&self, _other: &Self) -> bool {
248        true
249    }
250}
251
252impl Eq for SemanticsElement {}
253
254impl Hash for SemanticsElement {
255    fn hash<H: Hasher>(&self, state: &mut H) {
256        "semantics".hash(state);
257    }
258}
259
260impl ModifierNodeElement for SemanticsElement {
261    type Node = SemanticsModifierNode;
262
263    fn create(&self) -> Self::Node {
264        SemanticsModifierNode::new(self.recorder.clone())
265    }
266
267    fn update(&self, node: &mut Self::Node) {
268        node.recorder = self.recorder.clone();
269    }
270
271    fn capabilities(&self) -> NodeCapabilities {
272        NodeCapabilities::SEMANTICS
273    }
274
275    fn always_update(&self) -> bool {
276        true
277    }
278}
279
280fn merge_semantics_from_node(node: &dyn ModifierNode, config: &mut SemanticsConfiguration) -> bool {
281    let mut merged = false;
282
283    if let Some(semantics) = node.as_semantics_node() {
284        semantics.merge_semantics(config);
285        merged = true;
286    }
287
288    node.for_each_delegate(&mut |delegate| {
289        if merge_semantics_from_node(delegate, config) {
290            merged = true;
291        }
292    });
293
294    merged
295}
296
297/// Collects semantics contributed by a reconciled modifier chain.
298pub fn collect_semantics_from_chain(chain: &ModifierNodeChain) -> Option<SemanticsConfiguration> {
299    if !chain.has_capability(NodeCapabilities::SEMANTICS) {
300        return None;
301    }
302
303    let mut config = SemanticsConfiguration::default();
304    let mut merged = false;
305    chain.for_each_node_with_capability(NodeCapabilities::SEMANTICS, |_ref, node| {
306        if merge_semantics_from_node(node, &mut config) {
307            merged = true;
308        }
309    });
310
311    if merged { Some(config) } else { None }
312}
313
314/// Collects semantics by instantiating a temporary modifier chain from a [`Modifier`].
315pub fn collect_semantics_from_modifier(modifier: &Modifier) -> Option<SemanticsConfiguration> {
316    let mut handle = ModifierChainHandle::new();
317    handle.update(modifier);
318    collect_semantics_from_chain(handle.chain())
319}