Skip to main content

fret_ui/tree/
ui_tree_semantics.rs

1use super::*;
2
3impl<H: UiHost> UiTree<H> {
4    pub fn request_semantics_snapshot(&mut self) {
5        self.semantics_requested = true;
6    }
7
8    pub fn semantics_snapshot(&self) -> Option<&SemanticsSnapshot> {
9        self.semantics.as_deref()
10    }
11
12    pub fn semantics_snapshot_arc(&self) -> Option<Arc<SemanticsSnapshot>> {
13        self.semantics.clone()
14    }
15
16    pub(in crate::tree) fn refresh_semantics_snapshot(&mut self, app: &mut H) {
17        let Some(window) = self.window else {
18            self.semantics = None;
19            return;
20        };
21
22        let profile_semantics = crate::runtime_config::ui_runtime_config().semantics_profile;
23        let profile_started = profile_semantics.then(Instant::now);
24        let mut t_element_id_map: Option<Duration> = None;
25        let mut t_window_frame_children: Option<Duration> = None;
26        let mut t_traversal: Option<Duration> = None;
27        let mut t_relations: Option<Duration> = None;
28
29        let base_root = self
30            .base_layer
31            .and_then(|id| self.layers.get(id).map(|l| l.root));
32
33        let visible_layers: Vec<UiLayerId> = self.visible_layers_in_paint_order().collect();
34        if visible_layers.is_empty() {
35            self.semantics = Some(Arc::new(SemanticsSnapshot {
36                window,
37                ..SemanticsSnapshot::default()
38            }));
39            return;
40        }
41
42        let element_id_map = {
43            let started = profile_semantics.then(Instant::now);
44            let out = crate::declarative::frame::element_id_map_for_window(app, window);
45            if let Some(started) = started {
46                t_element_id_map = Some(started.elapsed());
47            }
48            out
49        };
50
51        // View-cache reuse can legitimately skip re-setting `UiTree` child edges for cached
52        // subtrees. `WindowFrame` retains the authoritative element-tree edges, so semantics
53        // traversal should treat the union as the effective child list (mirrors GC reachability
54        // bookkeeping). Only pay the cost when view-cache reuse can occur.
55        let window_frame_children: slotmap::SecondaryMap<NodeId, Arc<[NodeId]>> = {
56            let started = profile_semantics.then(Instant::now);
57            let out = if self.view_cache_active() {
58                crate::declarative::with_window_frame(app, window, |window_frame| {
59                    window_frame.map(|w| w.children.clone()).unwrap_or_default()
60                })
61            } else {
62                slotmap::SecondaryMap::new()
63            };
64            if let Some(started) = started {
65                t_window_frame_children = Some(started.elapsed());
66            }
67            out
68        };
69
70        let mut barrier_index: Option<usize> = None;
71        for (idx, layer) in visible_layers.iter().enumerate() {
72            if self.layers[*layer].blocks_underlay_input {
73                barrier_index = Some(idx);
74            }
75        }
76        let barrier_root = barrier_index.map(|idx| self.layers[visible_layers[idx]].root);
77
78        let mut focus_barrier_index: Option<usize> = None;
79        for (idx, layer) in visible_layers.iter().enumerate() {
80            if self.layers[*layer].blocks_underlay_focus {
81                focus_barrier_index = Some(idx);
82            }
83        }
84        let focus_barrier_root =
85            focus_barrier_index.map(|idx| self.layers[visible_layers[idx]].root);
86
87        let mut roots: Vec<SemanticsRoot> = Vec::with_capacity(visible_layers.len());
88        for (z, layer_id) in visible_layers.iter().enumerate() {
89            let layer = &self.layers[*layer_id];
90            roots.push(SemanticsRoot {
91                root: layer.root,
92                visible: layer.visible,
93                blocks_underlay_input: layer.blocks_underlay_input,
94                hit_testable: layer.hit_testable,
95                z_index: z as u32,
96            });
97        }
98
99        let focus = self.focus;
100        let captured = self.captured_for(PointerId(0));
101
102        let mut nodes: Vec<SemanticsNode> = Vec::with_capacity(self.nodes.len());
103
104        let traversal_started = profile_semantics.then(Instant::now);
105        for root in roots.iter().map(|r| r.root) {
106            let mut visited = self.take_scratch_semantics_visited();
107            visited.clear();
108            // Stack entries carry the transform that maps this node's local bounds into
109            // screen-space (excluding this node's own `render_transform`).
110            let mut stack = self.take_scratch_semantics_stack();
111            stack.clear();
112            stack.push((root, Transform2D::IDENTITY));
113            while let Some((id, before)) = stack.pop() {
114                if !visited.insert(id) {
115                    if crate::strict_runtime::strict_runtime_enabled() {
116                        panic!("cycle detected while building semantics snapshot: node={id:?}");
117                    }
118                    tracing::error!(?id, "cycle detected while building semantics snapshot");
119                    continue;
120                }
121                let (
122                    parent,
123                    bounds,
124                    children,
125                    is_text_input,
126                    is_focusable,
127                    traverse_children,
128                    before_child,
129                ) = {
130                    let Some(node) = self.nodes.get(id) else {
131                        continue;
132                    };
133
134                    // Declarative `InteractivityGate(present=false)` subtrees behave like
135                    // `display: none`: they should not be exposed to the semantics snapshot even if
136                    // the underlying nodes remain mounted (e.g. during close animations / force-mount).
137                    //
138                    // We cannot rely solely on the widget-level `semantics_present()` cache here
139                    // because the layout engine may skip visiting display-none nodes in a frame,
140                    // leaving stale derived flags until the next layout pass.
141                    if node.element.is_some()
142                        && crate::declarative::frame::element_record_for_node(app, window, id)
143                            .is_some_and(|record| {
144                                matches!(
145                                    record.instance,
146                                    crate::declarative::frame::ElementInstance::InteractivityGate(p)
147                                        if !p.present
148                                )
149                            })
150                    {
151                        continue;
152                    }
153                    let widget = node.widget.as_ref();
154                    if widget.is_some_and(|w| !w.semantics_present()) {
155                        continue;
156                    }
157
158                    // Prefer prepaint-derived transforms when they are known to be valid, but
159                    // fall back to live widget transforms while hit-test invalidations are
160                    // pending.
161                    //
162                    // Hit-testing intentionally avoids `prepaint_hit_test` when `hit_test` is
163                    // invalidated (see `hit_test.rs`) to prevent stale transforms from affecting
164                    // pointer routing. Semantics should follow the same rule so scripted
165                    // diagnostics (which pick click points from semantics bounds) remain aligned
166                    // with the actual hit-test coordinate space.
167                    let prepaint = (!self.inspection_active && !node.invalidation.hit_test)
168                        .then_some(node.prepaint_hit_test)
169                        .flatten();
170
171                    let node_transform = prepaint
172                        .as_ref()
173                        .and_then(|p| p.render_transform_inv)
174                        .and_then(|inv| inv.inverse())
175                        .or_else(|| {
176                            widget
177                                .and_then(|w| w.render_transform(node.bounds))
178                                .filter(|t| t.inverse().is_some())
179                        })
180                        .unwrap_or(Transform2D::IDENTITY);
181                    let at_node = before.compose(node_transform);
182                    let bounds = rect_aabb_transformed(node.bounds, at_node);
183                    let ui_children = node.children.clone();
184                    let children = match window_frame_children.get(id) {
185                        None => ui_children,
186                        Some(frame_children) if ui_children.is_empty() => {
187                            frame_children.as_ref().to_vec()
188                        }
189                        Some(frame_children) => {
190                            let mut out = ui_children;
191                            for &child in frame_children.iter() {
192                                if !out.contains(&child) {
193                                    out.push(child);
194                                }
195                            }
196                            out
197                        }
198                    };
199                    let is_text_input = widget.is_some_and(|w| w.is_text_input());
200                    let is_focusable = widget.is_some_and(|w| w.is_focusable());
201                    let traverse_children = widget.map(|w| w.semantics_children()).unwrap_or(true);
202                    let child_transform = prepaint
203                        .as_ref()
204                        .and_then(|p| p.children_render_transform_inv)
205                        .and_then(|inv| inv.inverse())
206                        .or_else(|| {
207                            widget
208                                .and_then(|w| w.children_render_transform(node.bounds))
209                                .filter(|t| t.inverse().is_some())
210                        })
211                        .unwrap_or(Transform2D::IDENTITY);
212                    let before_child = at_node.compose(child_transform);
213
214                    (
215                        node.parent,
216                        bounds,
217                        children,
218                        is_text_input,
219                        is_focusable,
220                        traverse_children,
221                        before_child,
222                    )
223                };
224
225                let mut role = if Some(id) == base_root {
226                    SemanticsRole::Window
227                } else {
228                    SemanticsRole::Generic
229                };
230                // Heuristic baseline: text-input widgets should surface as text fields even if
231                // they don't implement an explicit semantics hook yet.
232                if is_text_input {
233                    role = SemanticsRole::TextField;
234                }
235
236                let mut flags = fret_core::SemanticsFlags {
237                    focused: focus == Some(id),
238                    captured: captured == Some(id),
239                    ..fret_core::SemanticsFlags::default()
240                };
241
242                let mut active_descendant: Option<NodeId> = None;
243                let mut pos_in_set: Option<u32> = None;
244                let mut set_size: Option<u32> = None;
245                let mut label: Option<String> = None;
246                let mut value: Option<String> = None;
247                let mut extra = fret_core::SemanticsNodeExtra::default();
248                let mut test_id: Option<String> = None;
249                let mut text_selection: Option<(u32, u32)> = None;
250                let mut text_composition: Option<(u32, u32)> = None;
251                let mut labelled_by: Vec<NodeId> = Vec::new();
252                let mut described_by: Vec<NodeId> = Vec::new();
253                let mut controls: Vec<NodeId> = Vec::new();
254                let mut inline_spans: Vec<fret_core::SemanticsInlineSpan> = Vec::new();
255                let mut actions = fret_core::SemanticsActions {
256                    focus: is_focusable || is_text_input,
257                    invoke: false,
258                    set_value: is_text_input,
259                    decrement: false,
260                    increment: false,
261                    scroll_by: false,
262                    set_text_selection: is_text_input,
263                };
264
265                // Allow widgets to override semantics metadata.
266                if let Some(widget) = self.nodes.get_mut(id).and_then(|node| node.widget.as_mut()) {
267                    let mut cx = SemanticsCx {
268                        app,
269                        node: id,
270                        window: Some(window),
271                        element_id_map: Some(element_id_map.as_ref()),
272                        bounds,
273                        children: children.as_slice(),
274                        focus,
275                        captured,
276                        role: &mut role,
277                        flags: &mut flags,
278                        label: &mut label,
279                        value: &mut value,
280                        test_id: &mut test_id,
281                        extra: &mut extra,
282                        text_selection: &mut text_selection,
283                        text_composition: &mut text_composition,
284                        actions: &mut actions,
285                        active_descendant: &mut active_descendant,
286                        pos_in_set: &mut pos_in_set,
287                        set_size: &mut set_size,
288                        labelled_by: &mut labelled_by,
289                        described_by: &mut described_by,
290                        controls: &mut controls,
291                        inline_spans: &mut inline_spans,
292                    };
293                    widget.semantics(&mut cx);
294                }
295
296                // Derive a conservative slider `SetValue` surface.
297                //
298                // Rationale: many assistive technology stacks issue `SetValue(NumericValue)` for
299                // sliders. However, this should only be exposed when we have enough structured
300                // numeric metadata to act on it deterministically.
301                if (role == SemanticsRole::Slider
302                    || role == SemanticsRole::SpinButton
303                    || role == SemanticsRole::Splitter)
304                    && (actions.increment || actions.decrement)
305                {
306                    let numeric = extra.numeric;
307                    let has_range = numeric.min.is_some() && numeric.max.is_some();
308                    let has_value = numeric.value.is_some();
309                    let has_step = numeric.step.is_some_and(|v| v.is_finite() && v > 0.0);
310                    actions.set_value = has_range && has_value && has_step;
311                } else if role == SemanticsRole::Slider
312                    || role == SemanticsRole::SpinButton
313                    || role == SemanticsRole::Splitter
314                {
315                    actions.set_value = false;
316                }
317
318                if pos_in_set.is_some_and(|p| p == 0) {
319                    pos_in_set = None;
320                }
321                if set_size.is_some_and(|s| s == 0) {
322                    set_size = None;
323                }
324                if let (Some(pos), Some(size)) = (pos_in_set, set_size)
325                    && pos > size
326                {
327                    pos_in_set = None;
328                    set_size = None;
329                }
330
331                nodes.push(SemanticsNode {
332                    id,
333                    parent,
334                    role,
335                    bounds,
336                    flags,
337                    test_id,
338                    active_descendant,
339                    pos_in_set,
340                    set_size,
341                    label,
342                    value,
343                    extra,
344                    text_selection,
345                    text_composition,
346                    actions,
347                    labelled_by,
348                    described_by,
349                    controls,
350                    inline_spans,
351                });
352
353                if traverse_children {
354                    // Preserve a stable-ish order: visit children in declared order.
355                    for &child in children.iter().rev() {
356                        stack.push((child, before_child));
357                    }
358                }
359            }
360
361            visited.clear();
362            stack.clear();
363            self.restore_scratch_semantics_visited(visited);
364            self.restore_scratch_semantics_stack(stack);
365        }
366        if let Some(started) = traversal_started {
367            t_traversal = Some(started.elapsed());
368        }
369
370        // Normalize relation edges: for some composite widgets, authoring only sets `labelled_by`
371        // (e.g. TabPanel -> Tab) but the platform-facing semantics want the controller to also
372        // advertise `controls` (e.g. Tab -> TabPanel). We derive that edge for the subset of
373        // role pairs where this bidirectional link is expected.
374        let relations_started = profile_semantics.then(Instant::now);
375        let mut index_by_id: HashMap<NodeId, usize> = HashMap::with_capacity(nodes.len());
376        for (idx, node) in nodes.iter().enumerate() {
377            index_by_id.insert(node.id, idx);
378        }
379        for idx in 0..nodes.len() {
380            let controlled = nodes[idx].id;
381            let controlled_role = nodes[idx].role;
382            let controllers = nodes[idx].labelled_by.clone();
383            for controller in controllers {
384                if let Some(&controller_idx) = index_by_id.get(&controller) {
385                    let controller_role = nodes[controller_idx].role;
386                    let derive = matches!(
387                        controlled_role,
388                        SemanticsRole::TabPanel | SemanticsRole::ListBox
389                    ) && matches!(
390                        controller_role,
391                        SemanticsRole::Tab
392                            | SemanticsRole::TextField
393                            | SemanticsRole::ComboBox
394                            | SemanticsRole::Button
395                    );
396                    if !derive {
397                        continue;
398                    }
399                    if !nodes[controller_idx].controls.contains(&controlled) {
400                        nodes[controller_idx].controls.push(controlled);
401                    }
402                }
403            }
404        }
405        if let Some(started) = relations_started {
406            t_relations = Some(started.elapsed());
407        }
408
409        let nodes_len = nodes.len();
410        self.semantics = Some(Arc::new(SemanticsSnapshot {
411            window,
412            roots,
413            barrier_root,
414            focus_barrier_root,
415            focus,
416            captured,
417            nodes,
418        }));
419
420        if let Some(snapshot) = self.semantics.as_deref() {
421            semantics::validate_semantics_if_enabled(snapshot);
422        }
423
424        if let Some(started) = profile_started {
425            let total = started.elapsed();
426            tracing::info!(
427                window = ?window,
428                view_cache_active = self.view_cache_active(),
429                nodes = nodes_len,
430                total_ms = total.as_millis(),
431                element_id_map_ms = t_element_id_map.map(|d| d.as_millis()),
432                window_frame_children_ms = t_window_frame_children.map(|d| d.as_millis()),
433                traversal_ms = t_traversal.map(|d| d.as_millis()),
434                relations_ms = t_relations.map(|d| d.as_millis()),
435                "semantics snapshot built"
436            );
437        }
438    }
439
440    pub(in crate::tree) fn node_root(&self, mut node: NodeId) -> Option<NodeId> {
441        while let Some(parent) = self.nodes.get(node).and_then(|n| n.parent) {
442            node = parent;
443        }
444        self.nodes.contains_key(node).then_some(node)
445    }
446
447    pub fn is_descendant(&self, root: NodeId, mut node: NodeId) -> bool {
448        if root == node {
449            return true;
450        }
451        while let Some(parent) = self.nodes.get(node).and_then(|n| n.parent) {
452            if parent == root {
453                return true;
454            }
455            node = parent;
456        }
457        false
458    }
459}