Skip to main content

slint_interpreter/
highlight.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//! Highlight support for running component instances.
5//!
6//! Walks the LLR `debug_info` side table to map either a source location
7//! or an object-tree `ElementRc` back to runtime flat item indices, then
8//! reads geometries via `ItemRc::geometry()` and transforms them through
9//! `map_to_item_tree`.
10
11use crate::instance::{Instance, SubComponentInstance};
12use i_slint_compiler::llr::{ItemInstanceIdx, SubComponentIdx, SubComponentInstanceIdx};
13use i_slint_compiler::object_tree::ElementRc;
14use i_slint_core::graphics::euclid;
15use i_slint_core::item_tree::ItemTreeVTable;
16use i_slint_core::items::ItemRc;
17use i_slint_core::lengths::{LogicalPoint, LogicalRect};
18use std::path::Path;
19use std::pin::Pin;
20use std::rc::Rc;
21use vtable::VRc;
22
23/// The rectangle of an element, which may be rotated around its center.
24#[derive(Clone, Copy, Debug, Default)]
25pub struct HighlightedRect {
26    /// The element's geometry.
27    pub rect: LogicalRect,
28    /// In degrees, around the center of the element.
29    pub angle: f32,
30}
31impl HighlightedRect {
32    /// Returns true if `position` lies inside the (potentially rotated) rectangle.
33    pub fn contains(&self, position: LogicalPoint) -> bool {
34        let center = self.rect.center();
35        let rotation = euclid::Rotation2D::radians((-self.angle).to_radians());
36        let transformed = center + rotation.transform_vector(position - center);
37        self.rect.contains(transformed)
38    }
39}
40
41/// Argument to filter the elements returned by the highlight helpers.
42#[derive(Copy, Clone, Eq, PartialEq)]
43pub enum ElementPositionFilter {
44    /// Include all elements.
45    IncludeClipped,
46    /// Exclude elements clipped by an ancestor `Clip` / `Flickable`.
47    ExcludeClipped,
48}
49
50/// Return the screen rectangles of every runtime item matching the
51/// given `ElementRc`, optionally filtering out those clipped by an
52/// ancestor. Public for downstream tooling such as the LSP element
53/// selection, whose hit-testing needs the `ExcludeClipped` filter.
54pub fn element_positions(
55    instance: &VRc<ItemTreeVTable, Instance>,
56    element: &ElementRc,
57    filter: ElementPositionFilter,
58) -> Vec<HighlightedRect> {
59    // Match by source location: the LLR copies the element's
60    // `source_location` onto every item it lowers, and the object-tree
61    // element keeps the original node. `element_hash` would be more
62    // compact, but passes that run after `inject_debug_hooks` (layout
63    // lowering, property hoisting) create elements without a hash.
64    let target = walk_to_native_root(element);
65    let Some(target_loc) = source_location_of(&target) else {
66        return Vec::new();
67    };
68    // A component use (`Button { }`) resolves to the definition's root
69    // element, whose location matches every instantiation of the component.
70    // Constrain the matches to item-table paths that descend through this
71    // specific use site.
72    let use_site = if Rc::ptr_eq(&target, element) { None } else { source_location_of(element) };
73    positions_by_source(
74        instance,
75        &target_loc.0,
76        target_loc.1,
77        use_site.as_ref().map(|(p, o)| (p.as_path(), *o)),
78        filter,
79    )
80}
81
82/// The `(path, offset)` key under which the LLR debug info records
83/// `element` — `Spanned::to_source_location` semantics (the qualified
84/// name's start).
85fn source_location_of(element: &ElementRc) -> Option<(std::path::PathBuf, u32)> {
86    use i_slint_compiler::diagnostics::Spanned;
87    let e = element.borrow();
88    let path = e.source_file()?.path().to_path_buf();
89    Some((path, e.span().offset as u32))
90}
91
92/// Descend into `base_type = Component(_)` wrappers until the element
93/// has its own native item. For a component use like `Button { }`, the
94/// runtime items belong to the wrapped component's root element, not to
95/// the use-site element itself.
96fn walk_to_native_root(element: &ElementRc) -> ElementRc {
97    let mut current = element.clone();
98    loop {
99        let next = {
100            let b = current.borrow();
101            if let i_slint_compiler::langtype::ElementType::Component(c) = &b.base_type {
102                Some(c.root_element.clone())
103            } else {
104                None
105            }
106        };
107        match next {
108            Some(n) => current = n,
109            None => return current,
110        }
111    }
112}
113
114/// Return the geometry of every runtime item whose source location covers
115/// the given `(path, offset)` pair.
116pub(crate) fn component_positions(
117    instance: &VRc<ItemTreeVTable, Instance>,
118    path: &Path,
119    offset: u32,
120) -> Vec<HighlightedRect> {
121    element_node_at_source_code_position(instance, path, offset)
122        .into_iter()
123        .flat_map(|(element, _)| {
124            element_positions(instance, &element, ElementPositionFilter::IncludeClipped)
125        })
126        .collect()
127}
128
129/// Look up the `(ElementRc, index)` tuples whose `debug` entries cover
130/// the given source offset. Uses the `TypeLoader` stored on the instance
131/// (if available) to walk the original object-tree `Document`.
132pub(crate) fn element_node_at_source_code_position(
133    instance: &VRc<ItemTreeVTable, Instance>,
134    path: &Path,
135    offset: u32,
136) -> Vec<(ElementRc, usize)> {
137    let Some(type_loader) = instance.type_loaders.type_loader.as_ref() else {
138        return Vec::new();
139    };
140    let Some(doc) = type_loader.get_document(path) else {
141        return Vec::new();
142    };
143    let mut result = Vec::new();
144    // `inner_components` lists every component defined in the file,
145    // exported or not.
146    for component in &doc.inner_components {
147        visit_element_for_position(&component.root_element, path, offset, &mut result);
148    }
149    result
150}
151
152fn visit_element_for_position(
153    element: &ElementRc,
154    path: &Path,
155    offset: u32,
156    result: &mut Vec<(ElementRc, usize)>,
157) {
158    if element.borrow().repeated.is_some() {
159        // The children of a repeated element live in the component the
160        // repeater pass wrapped around it, which is not part of
161        // `inner_components` — descend explicitly. The wrapper's root
162        // element carries the same source node as the repeated element.
163        let base = match &element.borrow().base_type {
164            i_slint_compiler::langtype::ElementType::Component(c) => Some(c.root_element.clone()),
165            _ => None,
166        };
167        if let Some(root) = base {
168            visit_element_for_position(&root, path, offset, result);
169        }
170        return;
171    }
172    for (index, node_path, node_range) in element.borrow().debug.iter().enumerate().map(|(i, n)| {
173        let text_range = n
174            .node
175            .QualifiedName()
176            .map(|n| n.text_range())
177            .or_else(|| {
178                n.node
179                    .child_token(i_slint_compiler::parser::SyntaxKind::LBrace)
180                    .map(|n| n.text_range())
181            })
182            .expect("An Element must contain a LBrace somewhere");
183        (i, n.node.source_file.path(), text_range)
184    }) {
185        if node_path == path && node_range.contains(offset.into()) {
186            result.push((element.clone(), index));
187        }
188    }
189    let children = element.borrow().children.clone();
190    for child in &children {
191        visit_element_for_position(child, path, offset, result);
192    }
193}
194
195/// Scan the instance's flat `item_table` and return every flat index
196/// whose entry points at `(sub_component_path → target_sc_idx, target_local)`.
197/// With `use_site` set, only paths descending through a sub-component
198/// instance whose use-site element sits at that `(path, offset)` match.
199fn find_flat_indices_for_item(
200    instance: &VRc<ItemTreeVTable, Instance>,
201    target_sc_idx: SubComponentIdx,
202    target_local: ItemInstanceIdx,
203    use_site: Option<(&Path, u32)>,
204) -> Vec<usize> {
205    let cu = &instance.root_sub_component.compilation_unit;
206    let root_ty = instance.root_sub_component.sub_component_idx;
207    let mut out = Vec::new();
208    for (flat, entry) in instance.item_table.iter().enumerate() {
209        let Some((path, local_idx)) = entry.as_ref() else { continue };
210        if *local_idx != target_local {
211            continue;
212        }
213        if sub_component_idx_at_path(cu, root_ty, path) != target_sc_idx {
214            continue;
215        }
216        if let Some((us_path, us_offset)) = use_site
217            && !path_passes_use_site(cu, root_ty, path, us_path, us_offset)
218        {
219            continue;
220        }
221        out.push(flat);
222    }
223    out
224}
225
226/// Whether any step of `path` descends through a sub-component instance
227/// whose use-site element is recorded at `(us_path, us_offset)`.
228fn path_passes_use_site(
229    cu: &i_slint_compiler::llr::CompilationUnit,
230    mut current: SubComponentIdx,
231    path: &[SubComponentInstanceIdx],
232    us_path: &Path,
233    us_offset: u32,
234) -> bool {
235    for &instance_idx in path {
236        if let Some(debug) = cu.sub_components[current].debug_info.as_ref()
237            && let Some(loc) = debug.sub_component_use_sites.get(instance_idx)
238            && loc.source_file.as_ref().is_some_and(|f| f.path() == us_path)
239            && loc.span.offset as u32 == us_offset
240        {
241            return true;
242        }
243        current = cu.sub_components[current].sub_components[instance_idx].ty;
244    }
245    false
246}
247
248/// `root` plus every instantiated repeated / conditional row instance
249/// below it, recursively.
250fn all_instances(root: &VRc<ItemTreeVTable, Instance>) -> Vec<VRc<ItemTreeVTable, Instance>> {
251    let mut out = Vec::new();
252    collect_instances(root, &mut out);
253    out
254}
255
256fn collect_instances(
257    inst: &VRc<ItemTreeVTable, Instance>,
258    out: &mut Vec<VRc<ItemTreeVTable, Instance>>,
259) {
260    out.push(inst.clone());
261    collect_row_instances(&inst.root_sub_component, out);
262}
263
264fn collect_row_instances(
265    sub: &Pin<Rc<SubComponentInstance>>,
266    out: &mut Vec<VRc<ItemTreeVTable, Instance>>,
267) {
268    for rep in sub.repeaters.iter() {
269        for row in rep.instances_vec() {
270            collect_instances(&row, out);
271        }
272    }
273    for nested in sub.sub_components.iter() {
274        collect_row_instances(nested, out);
275    }
276}
277
278/// Walk the LLR sub_components tree to resolve `path` into its concrete
279/// [`SubComponentIdx`].
280fn sub_component_idx_at_path(
281    cu: &i_slint_compiler::llr::CompilationUnit,
282    root_idx: SubComponentIdx,
283    path: &[SubComponentInstanceIdx],
284) -> SubComponentIdx {
285    let mut current = root_idx;
286    for &instance_idx in path {
287        let nested = &cu.sub_components[current].sub_components[instance_idx];
288        current = nested.ty;
289    }
290    current
291}
292
293fn item_flat_index_to_rect(
294    instance: &VRc<ItemTreeVTable, Instance>,
295    root: &VRc<ItemTreeVTable, Instance>,
296    flat_idx: usize,
297) -> Option<HighlightedRect> {
298    let vrc = VRc::into_dyn(instance.clone());
299    let root_vrc = VRc::into_dyn(root.clone());
300    let item_rc = ItemRc::new(vrc, flat_idx as u32);
301    let geometry = item_rc.geometry();
302    if geometry.size.is_empty() {
303        return None;
304    }
305    let origin = item_rc.map_to_item_tree(geometry.origin, &root_vrc);
306    let top_right = item_rc
307        .map_to_item_tree(geometry.origin + euclid::vec2(geometry.size.width, 0.), &root_vrc);
308    let delta = top_right - origin;
309    let width = delta.length();
310    let height = if geometry.size.width == 0.0 {
311        0.0
312    } else {
313        geometry.size.height * width / geometry.size.width
314    };
315    let angle_rad = delta.y.atan2(delta.x);
316    let (sin, cos) = angle_rad.sin_cos();
317    let center = euclid::point2(
318        origin.x + (width / 2.0) * cos - (height / 2.0) * sin,
319        origin.y + (width / 2.0) * sin + (height / 2.0) * cos,
320    );
321    Some(HighlightedRect {
322        rect: LogicalRect {
323            origin: center - euclid::vec2(width / 2.0, height / 2.0),
324            size: euclid::size2(width, height),
325        },
326        angle: angle_rad.to_degrees(),
327    })
328}
329
330fn positions_by_source(
331    root: &VRc<ItemTreeVTable, Instance>,
332    target_path: &Path,
333    target_offset: u32,
334    use_site: Option<(&Path, u32)>,
335    filter: ElementPositionFilter,
336) -> Vec<HighlightedRect> {
337    let cu = root.root_sub_component.compilation_unit.clone();
338    let mut results = Vec::new();
339    // Repeated / conditional rows are separate instances with their own
340    // item tables, so search all of them, mapping geometry back into the
341    // root instance's coordinates.
342    for instance in all_instances(root) {
343        for sc_idx in 0..cu.sub_components.len() {
344            let sc_idx: SubComponentIdx = sc_idx.into();
345            let sc = &cu.sub_components[sc_idx];
346            let Some(debug) = sc.debug_info.as_ref() else { continue };
347            for (local_idx, item_dbg) in debug.items.iter_enumerated() {
348                let Some(source_file) = item_dbg.source_location.source_file.as_ref() else {
349                    continue;
350                };
351                if source_file.path() != target_path {
352                    continue;
353                }
354                if item_dbg.source_location.span.offset as u32 != target_offset {
355                    continue;
356                }
357                for flat_idx in find_flat_indices_for_item(&instance, sc_idx, local_idx, use_site) {
358                    if filter == ElementPositionFilter::ExcludeClipped {
359                        let dyn_rc = vtable::VRc::into_dyn(instance.clone());
360                        let item_rc = i_slint_core::items::ItemRc::new(dyn_rc, flat_idx as u32);
361                        if !item_rc.is_visible() {
362                            continue;
363                        }
364                    }
365                    if let Some(rect) = item_flat_index_to_rect(&instance, root, flat_idx) {
366                        results.push(rect);
367                    }
368                }
369            }
370        }
371    }
372    results
373}