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