Skip to main content

slint_interpreter/
item_tree_vtable.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! `ItemTreeVTable` implementation for [`Instance`].
5//!
6//! A single static vtable serves every runtime `Instance`; vtable calls
7//! walk the instance's sub-component tree on demand rather than through
8//! a precomputed offset table.
9
10use crate::instance::Instance;
11use i_slint_core::SharedString;
12use i_slint_core::SlintContext;
13use i_slint_core::accessibility::{
14    AccessibilityAction, AccessibleStringProperty, SupportedAccessibilityAction,
15};
16use i_slint_core::item_tree::{
17    IndexRange, ItemTree, ItemTreeNode, ItemTreeVTable, ItemVisitorVTable, ItemWeak,
18    TraversalOrder, VisitChildrenResult,
19};
20use i_slint_core::items::{AccessibleRole, ItemVTable};
21use i_slint_core::layout::{LayoutInfo, Orientation};
22use i_slint_core::lengths::LogicalRect;
23use i_slint_core::slice::Slice;
24use i_slint_core::window::WindowAdapterRc;
25use std::pin::Pin;
26use vtable::{VRef, VRefMut, VWeak};
27
28i_slint_core::ItemTreeVTable_static!(static INTERPRETER_INSTANCE_VT for Instance);
29
30/// Find the `sub_component_path` (sequence of `SubComponentInstanceIdx`)
31/// from the parent instance's root to the given sub-component. Used by
32/// `parent_node` to match entries in the parent's `dynamic_table`.
33pub(crate) fn sub_component_path_of(
34    target: &crate::instance::SubComponentInstance,
35    parent_root: &Instance,
36) -> Vec<i_slint_compiler::llr::SubComponentInstanceIdx> {
37    fn walk(
38        current: &crate::instance::SubComponentInstance,
39        target_ptr: *const crate::instance::SubComponentInstance,
40        path: &mut Vec<i_slint_compiler::llr::SubComponentInstanceIdx>,
41    ) -> bool {
42        if std::ptr::eq(current as *const _, target_ptr) {
43            return true;
44        }
45        for (idx, nested) in current.sub_components.iter().enumerate() {
46            path.push(idx.into());
47            if walk(nested, target_ptr, path) {
48                return true;
49            }
50            path.pop();
51        }
52        false
53    }
54    let mut path = Vec::new();
55    walk(&parent_root.root_sub_component, target as *const _, &mut path);
56    path
57}
58
59impl i_slint_core::item_tree::ItemTree for Instance {
60    fn visit_children_item(
61        self: Pin<&Self>,
62        index: isize,
63        order: TraversalOrder,
64        visitor: VRefMut<'_, ItemVisitorVTable>,
65    ) -> VisitChildrenResult {
66        let this = self.get_ref();
67        let weak = this.self_weak.get().unwrap().clone();
68        if index >= 0 && this.z_sort_table.get(index as usize).is_some_and(|e| e.is_some()) {
69            i_slint_core::item_tree::visit_item_tree_z_sorted(
70                &vtable::VRc::into_dyn(weak.upgrade().unwrap()),
71                &this.tree_nodes[..],
72                index,
73                order,
74                visitor,
75                &mut |order, visitor, dyn_index| {
76                    self.visit_dynamic_children(dyn_index, order, visitor)
77                },
78                &mut |push| self.collect_z_sorted_children(index, push),
79            )
80        } else {
81            i_slint_core::item_tree::visit_item_tree(
82                &vtable::VRc::into_dyn(weak.upgrade().unwrap()),
83                &this.tree_nodes[..],
84                index,
85                order,
86                visitor,
87                &mut |order, visitor, dyn_index| {
88                    self.visit_dynamic_children(dyn_index, order, visitor)
89                },
90            )
91        }
92    }
93
94    fn get_item_ref(self: Pin<&Self>, index: u32) -> Pin<VRef<'_, ItemVTable>> {
95        // The item_table is indexed by flat tree index (same ordering as
96        // `tree_nodes`), pointing at the sub-component path + item slot
97        // that backs each static item node.
98        let this = self.get_ref();
99        let entry = this
100            .item_table
101            .get(index as usize)
102            .and_then(Option::as_ref)
103            .expect("get_item_ref: tree index is not a static item");
104        // Walk the path by borrowing — every intermediate sub-component
105        // is owned by its parent via `sub_components`, so a reference
106        // to the leaf is valid for the lifetime of `self`.
107        let mut current: &crate::instance::SubComponentInstance = &this.root_sub_component;
108        for &sub_idx in entry.0.iter() {
109            current = &current.sub_components[sub_idx];
110        }
111        Pin::as_ref(&current.items[entry.1]).as_item_ref()
112    }
113
114    fn ensure_instantiated(self: Pin<&Self>) -> bool {
115        self.get_ref().ensure_instantiated()
116    }
117
118    fn get_subtree_range(self: Pin<&Self>, index: u32) -> IndexRange {
119        let Some((sub, rep_idx)) = self.get_ref().dynamic_at(index) else {
120            return IndexRange { start: 0, end: 0 };
121        };
122        if let Some(cc) = crate::instance::component_container_item(&sub, rep_idx) {
123            return cc.subtree_range();
124        }
125        let repeater = &sub.repeaters[rep_idx];
126        repeater.track_instance_changes();
127        let range = repeater.range();
128        IndexRange { start: range.start, end: range.end }
129    }
130
131    fn get_subtree(
132        self: Pin<&Self>,
133        index: u32,
134        subindex: usize,
135        result: &mut VWeak<ItemTreeVTable, vtable::Dyn>,
136    ) {
137        let Some((sub, rep_idx)) = self.get_ref().dynamic_at(index) else {
138            return;
139        };
140        if let Some(cc) = crate::instance::component_container_item(&sub, rep_idx) {
141            if subindex == 0 {
142                *result = cc.subtree_component();
143            }
144            return;
145        }
146        let repeater = &sub.repeaters[rep_idx];
147        if let Some(instance) = repeater.instance_at(subindex) {
148            *result = vtable::VRc::downgrade(&vtable::VRc::into_dyn(instance));
149        }
150    }
151
152    fn get_item_tree(self: Pin<&Self>) -> Slice<'_, ItemTreeNode> {
153        Slice::from(&*self.get_ref().tree_nodes)
154    }
155
156    fn parent_node(self: Pin<&Self>, result: &mut ItemWeak) {
157        // If this is a repeated sub-tree, point at the repeater's placeholder
158        // in the parent instance. For a popup (parented but not repeated),
159        // point at the parent instance's root item.
160        let this = self.get_ref();
161        // `embedded_in` records where in the outer item tree this instance
162        // lives. Return that as the parent; the core walks back through it
163        // the same way as a repeated DynamicTree node.
164        if let Some((outer_weak, outer_index)) = this.embedded_in.get()
165            && let Some(outer) = outer_weak.upgrade()
166        {
167            *result = i_slint_core::items::ItemRc::new(outer, *outer_index).downgrade();
168            return;
169        }
170        let Some(parent_sub) = this.parent_instance.upgrade() else { return };
171        let Some(parent_root_vrc) = parent_sub.root.get().and_then(|w| w.upgrade()) else {
172            return;
173        };
174        let parent_dyn = vtable::VRc::into_dyn(parent_root_vrc.clone());
175        if let Some((_, repeater_idx)) = this.root_sub_component.repeated_in.get() {
176            // Return the DynamicTree node itself in the parent's flat tree.
177            // `parent_item` in i_slint_core detects that the returned parent
178            // is a DynamicTree and walks one more level up to its parent
179            // item. Returning the DynamicTree's own parent here skips that
180            // adjustment and gives the caller the wrong node.
181            let rep_idx = *repeater_idx;
182            let parent_path = sub_component_path_of(&parent_sub, &parent_root_vrc);
183            for (flat, entry) in parent_root_vrc.dynamic_table.iter().enumerate() {
184                if let Some((path, idx)) = entry.as_ref()
185                    && path.as_ref() == parent_path.as_slice()
186                    && *idx == rep_idx
187                {
188                    *result = i_slint_core::items::ItemRc::new(parent_dyn, flat as u32).downgrade();
189                    return;
190                }
191            }
192        } else {
193            // Popup case: ItemRc::new_root on the parent instance, which the
194            // caller uses to traverse up to the window.
195            *result = i_slint_core::items::ItemRc::new(parent_dyn, 0).downgrade();
196        }
197    }
198
199    fn embed_component(
200        self: Pin<&Self>,
201        parent: &VWeak<ItemTreeVTable>,
202        parent_item_tree_index: u32,
203    ) -> bool {
204        // Stash the outer item tree handle so `parent_node` can point at
205        // the ComponentContainer slot that substitutes this instance in.
206        let this = self.get_ref();
207        this.embedded_in.set((parent.clone(), parent_item_tree_index)).is_ok()
208    }
209
210    fn subtree_index(self: Pin<&Self>) -> usize {
211        // For repeated instances, return the model index so tab-focus
212        // traversal can step to the next sibling via get_subtree(idx+1).
213        let this = self.get_ref();
214        let sc = &this.root_sub_component.compilation_unit.sub_components
215            [this.root_sub_component.sub_component_idx];
216        for (idx, prop) in sc.properties.iter_enumerated() {
217            if prop.name == "model_index"
218                && let crate::Value::Number(n) =
219                    Pin::as_ref(&this.root_sub_component.properties[idx]).get()
220            {
221                return n as usize;
222            }
223        }
224        // Conditional: only one instance, index 0.
225        0
226    }
227
228    fn layout_info(self: Pin<&Self>, orientation: Orientation) -> LayoutInfo {
229        let this = self.get_ref();
230        let sc_idx = this.root_sub_component.sub_component_idx;
231        let cu = &this.root_sub_component.compilation_unit;
232        let sc = &cu.sub_components[sc_idx];
233        let expr = match orientation {
234            Orientation::Horizontal => sc.layout_info_h.borrow(),
235            Orientation::Vertical => sc.layout_info_v.borrow(),
236        };
237        let mut ctx = crate::eval::EvalContext::new(this.root_sub_component.clone());
238        crate::eval::eval_expression(&mut ctx, &expr).try_into().unwrap_or_default()
239    }
240
241    fn item_geometry(self: Pin<&Self>, item_index: u32) -> LogicalRect {
242        // `item_index` is the flat tree index. Resolve it via `item_table`
243        // into the owning sub-component, then look up the geometry by
244        // the item's `index_in_tree`. `sc.geometries` is keyed by the
245        // sub-component-local tree index (set by `generate_item_indices`),
246        // not by the raw `ItemInstanceIdx` slot.
247        let this = self.get_ref();
248        let Some(entry) = this.item_table.get(item_index as usize).and_then(Option::as_ref) else {
249            return LogicalRect::default();
250        };
251        let mut owner_rc = this.root_sub_component.clone();
252        for &sub_idx in entry.0.iter() {
253            owner_rc = owner_rc.sub_components[sub_idx].clone();
254        }
255        let cu = owner_rc.compilation_unit.clone();
256        let sc = &cu.sub_components[owner_rc.sub_component_idx];
257        let item = &sc.items[entry.1];
258        // When the flat tree crosses into a sub-component (non-empty path)
259        // and lands on its root element (local tree index 0), the inner
260        // root's geometry can duplicate the parent's placement: the
261        // compiler's `adjust_geometry_for_injected_parent` pass hoists the
262        // original position into an injected wrapper item, and the inner
263        // root applies the same offset again via its `y: root-1_y`
264        // binding. So read the wrapper's geometry from the *parent*
265        // sub-component at the placement slot and never query the inner
266        // root. Fall through when the parent
267        // has no entry (the sub-component was placed directly with no
268        // wrapper, e.g. `box := SpinBox {}` inside a Window — then the
269        // inner root's own geometry is the correct placement).
270        let parent_placement = if !entry.0.is_empty() && item.index_in_tree == 0 {
271            let mut parent_rc = this.root_sub_component.clone();
272            for &sub_idx in &entry.0[..entry.0.len() - 1] {
273                parent_rc = parent_rc.sub_components[sub_idx].clone();
274            }
275            let placement = entry.0[entry.0.len() - 1];
276            let parent_sc = &cu.sub_components[parent_rc.sub_component_idx];
277            let placement_idx = parent_sc.sub_components[placement].index_in_tree as usize;
278            parent_sc
279                .geometries
280                .get(placement_idx)
281                .and_then(|g| g.clone())
282                .map(|expr| (expr, parent_rc))
283        } else {
284            None
285        };
286        let (expr_cell, ctx_owner) = if let Some(pair) = parent_placement {
287            pair
288        } else {
289            let tree_local_idx = item.index_in_tree as usize;
290            match sc.geometries.get(tree_local_idx) {
291                Some(Some(expr)) => (expr.clone(), owner_rc),
292                _ => return LogicalRect::default(),
293            }
294        };
295        let expr = expr_cell.borrow();
296        let mut ctx = crate::eval::EvalContext::new(ctx_owner);
297        let crate::Value::Struct(s) = crate::eval::eval_expression(&mut ctx, &expr) else {
298            return LogicalRect::default();
299        };
300        let as_f32 = |name: &str| -> f32 {
301            match s.get_field(name) {
302                Some(crate::Value::Number(n)) => *n as f32,
303                _ => 0.0,
304            }
305        };
306        LogicalRect::new(
307            i_slint_core::lengths::LogicalPoint::new(as_f32("x"), as_f32("y")),
308            i_slint_core::lengths::LogicalSize::new(as_f32("width"), as_f32("height")),
309        )
310    }
311
312    fn accessible_role(self: Pin<&Self>, item_index: u32) -> AccessibleRole {
313        let Some((owner, local_idx)) = resolve_accessible_item(self.get_ref(), item_index) else {
314            return AccessibleRole::default();
315        };
316        let cu = owner.compilation_unit.clone();
317        let sc = &cu.sub_components[owner.sub_component_idx];
318        let Some(expr) = sc.accessible_prop.get(&(local_idx, "Role".to_string())) else {
319            return AccessibleRole::default();
320        };
321        let mut ctx = crate::eval::EvalContext::new(owner);
322        crate::eval::eval_expression(&mut ctx, &expr.borrow()).try_into().unwrap_or_default()
323    }
324
325    fn accessible_string_property(
326        self: Pin<&Self>,
327        item_index: u32,
328        what: AccessibleStringProperty,
329        result: &mut SharedString,
330    ) -> bool {
331        let what_str = accessible_string_property_name(what);
332        for (owner, local_idx) in resolve_accessible_candidates(self.get_ref(), item_index) {
333            let cu = owner.compilation_unit.clone();
334            let sc = &cu.sub_components[owner.sub_component_idx];
335            if let Some(expr) = sc.accessible_prop.get(&(local_idx, what_str.clone())) {
336                let mut ctx = crate::eval::EvalContext::new(owner);
337                if let crate::Value::String(s) =
338                    crate::eval::eval_expression(&mut ctx, &expr.borrow())
339                {
340                    *result = s;
341                    return true;
342                }
343            }
344        }
345        false
346    }
347
348    fn accessibility_action(self: Pin<&Self>, item_index: u32, action: &AccessibilityAction) {
349        let what = format!("Action{}", accessibility_action_name(action));
350        for (owner, local_idx) in resolve_accessible_candidates(self.get_ref(), item_index) {
351            let cu = owner.compilation_unit.clone();
352            let sc = &cu.sub_components[owner.sub_component_idx];
353            if let Some(expr) = sc.accessible_prop.get(&(local_idx, what.clone())) {
354                let args = accessibility_action_args(action);
355                let mut ctx = crate::eval::EvalContext::with_arguments(owner, args);
356                crate::eval::eval_expression(&mut ctx, &expr.borrow());
357                return;
358            }
359        }
360    }
361
362    fn supported_accessibility_actions(
363        self: Pin<&Self>,
364        item_index: u32,
365    ) -> SupportedAccessibilityAction {
366        let mut actions = SupportedAccessibilityAction::default();
367        for (owner, local_idx) in resolve_accessible_candidates(self.get_ref(), item_index) {
368            let cu = owner.compilation_unit.clone();
369            let sc = &cu.sub_components[owner.sub_component_idx];
370            for (idx, key) in sc.accessible_prop.keys() {
371                if *idx == local_idx
372                    && let Some(action_name) = key.strip_prefix("Action")
373                {
374                    actions |= SupportedAccessibilityAction::from_name(action_name)
375                        .unwrap_or_else(|| panic!("Not an accessible action: {action_name:?}"));
376                }
377            }
378        }
379        actions
380    }
381
382    fn item_element_infos(self: Pin<&Self>, item_index: u32, result: &mut SharedString) -> bool {
383        let this = self.get_ref();
384        let cu = &this.root_sub_component.compilation_unit;
385        // Like the code generators, answer for every item when compiled with debug
386        // info, leaving `result` empty for items the compiler synthesized (such as
387        // the wrapper of a `visible` binding). `false` means that the whole tree
388        // lacks debug info.
389        if !cu.has_debug_info {
390            return false;
391        }
392        let Some(entry) = this.item_table.get(item_index as usize).and_then(Option::as_ref) else {
393            return true;
394        };
395        // The compiler stores `element_infos` per sub-component, keyed by
396        // the element's tree index *within that sub-component*. Walk the
397        // sub_component_path from the root, translating the flat index
398        // into each sub-component's local tree space.
399        //
400        // A native item's info lives on the leaf sub-component; a
401        // component-instance declaration's (`Switch { }`) lives on the
402        // *parent* of the leaf, keyed by the instance's `index_in_tree`.
403        // Check each level before descending — first match wins.
404        let mut owner_sc_idx = this.root_sub_component.sub_component_idx;
405        let mut local_idx = item_index;
406        for &sub_step in entry.0.iter() {
407            let owner_sc = &cu.sub_components[owner_sc_idx];
408            if let Some(info) = owner_sc.element_infos.get(&local_idx) {
409                *result = info.as_str().into();
410                return true;
411            }
412            let nested = &owner_sc.sub_components[sub_step];
413            // Translate `local_idx` into `nested`'s tree.
414            if local_idx == nested.index_in_tree {
415                local_idx = 0;
416            } else if nested.index_of_first_child_in_tree > 0 {
417                local_idx = local_idx + 1 - nested.index_of_first_child_in_tree;
418            }
419            owner_sc_idx = nested.ty;
420        }
421        let owner_sc = &cu.sub_components[owner_sc_idx];
422        let item_local_idx = owner_sc.items[entry.1].index_in_tree;
423        if let Some(infos) = owner_sc.element_infos.get(&item_local_idx) {
424            *result = infos.as_str().into();
425        }
426        true
427    }
428
429    fn slint_context(self: Pin<&Self>, result: &mut Option<SlintContext>) {
430        *result = Some(self.get_ref().globals.context.clone());
431    }
432
433    fn window_adapter(self: Pin<&Self>, do_create: bool, result: &mut Option<WindowAdapterRc>) {
434        // A repeated instance's own `window_adapter` is unset; walk up via
435        // `parent_instance` to the root `Instance` and read its adapter.
436        let this = self.get_ref();
437        if let Some(adapter) = this.window_adapter.get() {
438            *result = Some(adapter.clone());
439            return;
440        }
441        let mut parent_sub = this.parent_instance.upgrade();
442        while let Some(sub) = parent_sub {
443            let Some(root_vrc) = sub.root.get().and_then(|w| w.upgrade()) else { break };
444            if let Some(adapter) = root_vrc.window_adapter.get() {
445                *result = Some(adapter.clone());
446                return;
447            }
448            parent_sub = root_vrc.parent_instance.upgrade();
449        }
450        if do_create {
451            *result = this.window_adapter_or_default();
452        }
453    }
454}
455
456/// Resolve a flat tree index to (owning sub-component, local index_in_tree)
457/// for accessibility lookups.
458fn resolve_accessible_item(
459    instance: &Instance,
460    item_index: u32,
461) -> Option<(Pin<std::rc::Rc<crate::instance::SubComponentInstance>>, u32)> {
462    let entry = instance.item_table.get(item_index as usize).and_then(Option::as_ref)?;
463    let mut owner = instance.root_sub_component.clone();
464    for &sub_idx in entry.0.iter() {
465        let next = owner.sub_components[sub_idx].clone();
466        owner = next;
467    }
468    let cu = &owner.compilation_unit;
469    let sc = &cu.sub_components[owner.sub_component_idx];
470    let local_idx = sc.items[entry.1].index_in_tree;
471    Some((owner, local_idx))
472}
473
474/// Returns the candidates to look up an accessible property for a given
475/// flat tree index. The first candidate is the wrapping sub-component
476/// reference at the root level (if applicable); the second is the
477/// deepest item itself, so an outer-element query wins over the inner
478/// sub-component root's own accessible properties.
479fn resolve_accessible_candidates(
480    instance: &Instance,
481    item_index: u32,
482) -> Vec<(Pin<std::rc::Rc<crate::instance::SubComponentInstance>>, u32)> {
483    let mut out = Vec::new();
484    let Some(entry) = instance.item_table.get(item_index as usize).and_then(Option::as_ref) else {
485        return out;
486    };
487    // First candidate: a wrapping sub-component reference at the root.
488    // Its accessible_prop entry is keyed by the root-local flat index.
489    if !entry.0.is_empty() {
490        out.push((instance.root_sub_component.clone(), item_index));
491    }
492    // Second candidate: the deepest item itself.
493    let mut owner = instance.root_sub_component.clone();
494    for &sub_idx in entry.0.iter() {
495        let next = owner.sub_components[sub_idx].clone();
496        owner = next;
497    }
498    let cu = &owner.compilation_unit;
499    let sc = &cu.sub_components[owner.sub_component_idx];
500    let local_idx = sc.items[entry.1].index_in_tree;
501    out.push((owner, local_idx));
502    out
503}
504
505/// The `accessible_prop` map key for a string property — the same
506/// PascalCase form the lowering derives from the enum's kebab-case
507/// `Display` (see `lower_to_item_tree`).
508fn accessible_string_property_name(what: AccessibleStringProperty) -> String {
509    i_slint_compiler::generator::to_pascal_case(&what.to_string())
510}
511
512fn accessibility_action_name(action: &AccessibilityAction) -> &'static str {
513    match action {
514        AccessibilityAction::Default => "Default",
515        AccessibilityAction::Decrement => "Decrement",
516        AccessibilityAction::Increment => "Increment",
517        AccessibilityAction::Expand => "Expand",
518        AccessibilityAction::ReplaceSelectedText(_) => "ReplaceSelectedText",
519        AccessibilityAction::SetValue(_) => "SetValue",
520        AccessibilityAction::SetSelectionOffsets(..) => "SetSelectionOffsets",
521    }
522}
523
524fn accessibility_action_args(action: &AccessibilityAction) -> Vec<crate::Value> {
525    match action {
526        AccessibilityAction::ReplaceSelectedText(s) | AccessibilityAction::SetValue(s) => {
527            vec![crate::Value::String(s.clone())]
528        }
529        AccessibilityAction::SetSelectionOffsets(anchor, focus) => {
530            vec![crate::Value::Number(*anchor as f64), crate::Value::Number(*focus as f64)]
531        }
532        _ => Vec::new(),
533    }
534}