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    /// The recorder may read state it did not capture.
106    reads_live_state: bool,
107    state: NodeState,
108}
109
110impl SemanticsModifierNode {
111    /// A node running `recorder`; `reads_live_state` says the recorder may
112    /// read state it did not capture, so every semantics update runs it.
113    pub fn new(recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>, reads_live_state: bool) -> Self {
114        Self {
115            recorder,
116            reads_live_state,
117            state: NodeState::new(),
118        }
119    }
120}
121
122impl DelegatableNode for SemanticsModifierNode {
123    fn node_state(&self) -> &NodeState {
124        &self.state
125    }
126}
127
128impl ModifierNode for SemanticsModifierNode {
129    fn as_semantics_node(&self) -> Option<&dyn SemanticsNodeTrait> {
130        Some(self)
131    }
132
133    fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNodeTrait> {
134        Some(self)
135    }
136}
137
138pub struct SemanticsRequesterNode {
139    state: NodeState,
140    requester: SemanticsRequester,
141}
142
143impl SemanticsRequesterNode {
144    pub(crate) fn new(requester: SemanticsRequester) -> Self {
145        Self {
146            state: NodeState::new(),
147            requester,
148        }
149    }
150}
151
152impl DelegatableNode for SemanticsRequesterNode {
153    fn node_state(&self) -> &NodeState {
154        &self.state
155    }
156}
157
158impl ModifierNode for SemanticsRequesterNode {
159    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
160        self.state.set_attached(true);
161        self.requester
162            .bind(SemanticsRequester::binding_here(context.node_id()));
163    }
164
165    fn on_detach(&mut self) {
166        self.state.set_attached(false);
167        self.requester.bind(None);
168    }
169}
170
171/// Modifier element for [`SemanticsRequester`].
172#[derive(Clone)]
173pub struct SemanticsRequesterElement {
174    requester: SemanticsRequester,
175}
176
177impl SemanticsRequesterElement {
178    pub(crate) fn new(requester: SemanticsRequester) -> Self {
179        Self { requester }
180    }
181}
182
183impl fmt::Debug for SemanticsRequesterElement {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        f.write_str("SemanticsRequesterElement")
186    }
187}
188
189impl PartialEq for SemanticsRequesterElement {
190    fn eq(&self, other: &Self) -> bool {
191        Rc::ptr_eq(&self.requester.binding, &other.requester.binding)
192    }
193}
194
195impl Eq for SemanticsRequesterElement {}
196
197impl Hash for SemanticsRequesterElement {
198    fn hash<H: Hasher>(&self, state: &mut H) {
199        Rc::as_ptr(&self.requester.binding).hash(state);
200    }
201}
202
203impl ModifierNodeElement for SemanticsRequesterElement {
204    type Node = SemanticsRequesterNode;
205
206    fn create(&self) -> Self::Node {
207        SemanticsRequesterNode::new(self.requester.clone())
208    }
209
210    fn update(&self, node: &mut Self::Node) {
211        if !Rc::ptr_eq(&node.requester.binding, &self.requester.binding) {
212            let bound = node.requester.binding.get();
213            node.requester.bind(None);
214            node.requester = self.requester.clone();
215            node.requester.bind(bound);
216        }
217    }
218
219    fn inspector_name(&self) -> &'static str {
220        "semanticsRequester"
221    }
222
223    fn capabilities(&self) -> NodeCapabilities {
224        NodeCapabilities::NONE
225    }
226}
227
228impl SemanticsNodeTrait for SemanticsModifierNode {
229    fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
230        (self.recorder)(config);
231    }
232
233    fn reach(&self) -> cranpose_foundation::SemanticsReach {
234        cranpose_foundation::SemanticsReach::merged_by(self, self.reads_live_state)
235    }
236}
237
238#[derive(Clone)]
239pub struct SemanticsElement {
240    recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>,
241    reads_live_state: bool,
242}
243
244impl SemanticsElement {
245    /// An element running `recorder`; `reads_live_state` says the recorder
246    /// may read state it did not capture.
247    pub fn new(recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>, reads_live_state: bool) -> Self {
248        Self {
249            recorder,
250            reads_live_state,
251        }
252    }
253}
254
255impl fmt::Debug for SemanticsElement {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        f.write_str("SemanticsElement")
258    }
259}
260
261impl PartialEq for SemanticsElement {
262    fn eq(&self, _other: &Self) -> bool {
263        true
264    }
265}
266
267impl Eq for SemanticsElement {}
268
269impl Hash for SemanticsElement {
270    fn hash<H: Hasher>(&self, state: &mut H) {
271        "semantics".hash(state);
272    }
273}
274
275impl ModifierNodeElement for SemanticsElement {
276    type Node = SemanticsModifierNode;
277
278    fn create(&self) -> Self::Node {
279        SemanticsModifierNode::new(self.recorder.clone(), self.reads_live_state)
280    }
281
282    fn update(&self, node: &mut Self::Node) {
283        node.recorder = self.recorder.clone();
284        node.reads_live_state = self.reads_live_state;
285    }
286
287    fn capabilities(&self) -> NodeCapabilities {
288        NodeCapabilities::SEMANTICS
289    }
290
291    fn always_update(&self) -> bool {
292        true
293    }
294}
295
296fn merge_semantics_from_node(node: &dyn ModifierNode, config: &mut SemanticsConfiguration) -> bool {
297    let mut merged = false;
298
299    if let Some(semantics) = node.as_semantics_node() {
300        semantics.merge_semantics(config);
301        merged = true;
302    }
303
304    node.for_each_delegate(&mut |delegate| {
305        if merge_semantics_from_node(delegate, config) {
306            merged = true;
307        }
308    });
309
310    merged
311}
312
313/// Collects semantics contributed by a reconciled modifier chain.
314pub fn collect_semantics_from_chain(chain: &ModifierNodeChain) -> Option<SemanticsConfiguration> {
315    if !chain.has_capability(NodeCapabilities::SEMANTICS) {
316        return None;
317    }
318
319    let mut config = SemanticsConfiguration::default();
320    let mut merged = false;
321    chain.for_each_node_with_capability(NodeCapabilities::SEMANTICS, |_ref, node| {
322        if merge_semantics_from_node(node, &mut config) {
323            merged = true;
324        }
325    });
326
327    if merged { Some(config) } else { None }
328}
329
330fn reach_of_node(node: &dyn ModifierNode) -> cranpose_foundation::SemanticsReach {
331    let mut reach = node
332        .as_semantics_node()
333        .map(cranpose_foundation::SemanticsNode::reach)
334        .unwrap_or_default();
335    node.for_each_delegate(&mut |delegate| {
336        reach = reach.union(reach_of_node(delegate));
337    });
338    reach
339}
340
341/// Whether a reconciled modifier chain makes its node modal or hidden,
342/// without collecting the rest of its semantics.
343pub fn semantics_reach_of_chain(chain: &ModifierNodeChain) -> cranpose_foundation::SemanticsReach {
344    let mut reach = cranpose_foundation::SemanticsReach::default();
345    if !chain.has_capability(NodeCapabilities::SEMANTICS) {
346        return reach;
347    }
348    chain.for_each_node_with_capability(NodeCapabilities::SEMANTICS, |_ref, node| {
349        reach = reach.union(reach_of_node(node));
350    });
351    reach
352}
353
354/// Collects semantics by instantiating a temporary modifier chain from a [`Modifier`].
355pub fn collect_semantics_from_modifier(modifier: &Modifier) -> Option<SemanticsConfiguration> {
356    let mut handle = ModifierChainHandle::new();
357    handle.update(modifier);
358    collect_semantics_from_chain(handle.chain())
359}