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    /// Absolute origin of this instance's parent coordinate system (in root coordinates).
31    ///
32    /// `rect.origin - parent_origin` yields the element's position relative to its parent,
33    /// which matches the `x`/`y` properties written to the source. This is computed from the
34    /// instance's own ancestors, so it stays correct even if the element is positioned outside
35    /// of (or with a negative offset relative to) its parent.
36    ///
37    /// Both values are in root coordinates, so the subtraction only recovers the source `x`/`y`
38    /// while the parent frame is axis-aligned and unscaled — recovering it under a rotated or
39    /// scaled ancestor would additionally need to map the delta through the inverse ancestor
40    /// transform.
41    pub parent_origin: LogicalPoint,
42    /// Absolute rotation (in degrees) of this instance's parent coordinate system.
43    ///
44    /// `angle - parent_rotation` yields the element's own rotation relative to its parent, which
45    /// matches the `rotation-angle`/`transform-rotation` property written to the source.
46    pub parent_rotation: f32,
47}
48impl HighlightedRect {
49    /// Returns true if `position` lies inside the (potentially rotated) rectangle.
50    pub fn contains(&self, position: LogicalPoint) -> bool {
51        let center = self.rect.center();
52        let rotation = euclid::Rotation2D::radians((-self.angle).to_radians());
53        let transformed = center + rotation.transform_vector(position - center);
54        self.rect.contains(transformed)
55    }
56}
57
58/// Argument to filter the elements returned by the highlight helpers.
59#[derive(Copy, Clone, Eq, PartialEq)]
60pub enum ElementPositionFilter {
61    /// Include all elements.
62    IncludeClipped,
63    /// Exclude elements clipped by an ancestor `Clip` / `Flickable`.
64    ExcludeClipped,
65}
66
67/// Return the screen rectangles of every runtime item matching the
68/// given `ElementRc`, optionally filtering out those clipped by an
69/// ancestor. Public for downstream tooling such as the LSP element
70/// selection, whose hit-testing needs the `ExcludeClipped` filter.
71pub fn element_positions(
72    instance: &VRc<ItemTreeVTable, Instance>,
73    element: &ElementRc,
74    filter: ElementPositionFilter,
75) -> Vec<HighlightedRect> {
76    // Match by source location: the LLR copies the element's
77    // `source_location` onto every item it lowers, and the object-tree
78    // element keeps the original node. `element_hash` would be more
79    // compact, but passes that run after `inject_debug_hooks` (layout
80    // lowering, property hoisting) create elements without a hash.
81    let target = walk_to_native_root(element);
82    let Some(target_loc) = source_location_of(&target) else {
83        return Vec::new();
84    };
85    // A component use (`Button { }`) resolves to the definition's root
86    // element, whose location matches every instantiation of the component.
87    // Constrain the matches to item-table paths that descend through this
88    // specific use site.
89    let use_site = if Rc::ptr_eq(&target, element) { None } else { source_location_of(element) };
90    positions_by_source(
91        instance,
92        &target_loc.0,
93        target_loc.1,
94        use_site.as_ref().map(|(p, o)| (p.as_path(), *o)),
95        filter,
96    )
97}
98
99/// The `(path, offset)` key under which the LLR debug info records
100/// `element` — `Spanned::to_source_location` semantics (the qualified
101/// name's start).
102fn source_location_of(element: &ElementRc) -> Option<(std::path::PathBuf, u32)> {
103    use i_slint_compiler::diagnostics::Spanned;
104    let e = element.borrow();
105    let path = e.source_file()?.path().to_path_buf();
106    Some((path, e.span().offset as u32))
107}
108
109/// Descend into `base_type = Component(_)` wrappers until the element
110/// has its own native item. For a component use like `Button { }`, the
111/// runtime items belong to the wrapped component's root element, not to
112/// the use-site element itself.
113fn walk_to_native_root(element: &ElementRc) -> ElementRc {
114    let mut current = element.clone();
115    loop {
116        let next = {
117            let b = current.borrow();
118            if let i_slint_compiler::langtype::ElementType::Component(c) = &b.base_type {
119                Some(c.root_element.clone())
120            } else {
121                None
122            }
123        };
124        match next {
125            Some(n) => current = n,
126            None => return current,
127        }
128    }
129}
130
131/// Return the geometry of every runtime item whose source location covers
132/// the given `(path, offset)` pair.
133pub(crate) fn component_positions(
134    instance: &VRc<ItemTreeVTable, Instance>,
135    path: &Path,
136    offset: u32,
137) -> Vec<HighlightedRect> {
138    element_node_at_source_code_position(instance, path, offset)
139        .into_iter()
140        .flat_map(|(element, _)| {
141            element_positions(instance, &element, ElementPositionFilter::IncludeClipped)
142        })
143        .collect()
144}
145
146/// Look up the `(ElementRc, index)` tuples whose `debug` entries cover
147/// the given source offset. Uses the `TypeLoader` stored on the instance
148/// (if available) to walk the original object-tree `Document`.
149pub(crate) fn element_node_at_source_code_position(
150    instance: &VRc<ItemTreeVTable, Instance>,
151    path: &Path,
152    offset: u32,
153) -> Vec<(ElementRc, usize)> {
154    let Some(type_loader) = instance.type_loaders.type_loader.as_ref() else {
155        return Vec::new();
156    };
157    let Some(doc) = type_loader.get_document(path) else {
158        return Vec::new();
159    };
160    let mut result = Vec::new();
161    // `inner_components` lists every component defined in the file,
162    // exported or not.
163    for component in &doc.inner_components {
164        visit_element_for_position(&component.root_element, path, offset, &mut result);
165    }
166    result
167}
168
169fn visit_element_for_position(
170    element: &ElementRc,
171    path: &Path,
172    offset: u32,
173    result: &mut Vec<(ElementRc, usize)>,
174) {
175    if element.borrow().repeated.is_some() {
176        // The children of a repeated element live in the component the
177        // repeater pass wrapped around it, which is not part of
178        // `inner_components` — descend explicitly. The wrapper's root
179        // element carries the same source node as the repeated element.
180        let base = match &element.borrow().base_type {
181            i_slint_compiler::langtype::ElementType::Component(c) => Some(c.root_element.clone()),
182            _ => None,
183        };
184        if let Some(root) = base {
185            visit_element_for_position(&root, path, offset, result);
186        }
187        return;
188    }
189    for (index, node_path, node_range) in element.borrow().debug.iter().enumerate().map(|(i, n)| {
190        let text_range = n
191            .node
192            .QualifiedName()
193            .map(|n| n.text_range())
194            .or_else(|| {
195                n.node
196                    .child_token(i_slint_compiler::parser::SyntaxKind::LBrace)
197                    .map(|n| n.text_range())
198            })
199            .expect("An Element must contain a LBrace somewhere");
200        (i, n.node.source_file.path(), text_range)
201    }) {
202        if node_path == path && node_range.contains(offset.into()) {
203            result.push((element.clone(), index));
204        }
205    }
206    let children = element.borrow().children.clone();
207    for child in &children {
208        visit_element_for_position(child, path, offset, result);
209    }
210}
211
212/// Scan the instance's flat `item_table` and return every flat index
213/// whose entry points at `(sub_component_path → target_sc_idx, target_local)`.
214/// With `use_site` set, only paths descending through a sub-component
215/// instance whose use-site element sits at that `(path, offset)` match.
216fn find_flat_indices_for_item(
217    instance: &VRc<ItemTreeVTable, Instance>,
218    target_sc_idx: SubComponentIdx,
219    target_local: ItemInstanceIdx,
220    use_site: Option<(&Path, u32)>,
221) -> Vec<usize> {
222    let cu = &instance.root_sub_component.compilation_unit;
223    let root_ty = instance.root_sub_component.sub_component_idx;
224    let mut out = Vec::new();
225    for (flat, entry) in instance.item_table.iter().enumerate() {
226        let Some((path, local_idx)) = entry.as_ref() else { continue };
227        if *local_idx != target_local {
228            continue;
229        }
230        if sub_component_idx_at_path(cu, root_ty, path) != target_sc_idx {
231            continue;
232        }
233        if let Some((us_path, us_offset)) = use_site
234            && !path_passes_use_site(cu, root_ty, path, us_path, us_offset)
235        {
236            continue;
237        }
238        out.push(flat);
239    }
240    out
241}
242
243/// Whether any step of `path` descends through a sub-component instance
244/// whose use-site element is recorded at `(us_path, us_offset)`.
245fn path_passes_use_site(
246    cu: &i_slint_compiler::llr::CompilationUnit,
247    mut current: SubComponentIdx,
248    path: &[SubComponentInstanceIdx],
249    us_path: &Path,
250    us_offset: u32,
251) -> bool {
252    for &instance_idx in path {
253        if let Some(debug) = cu.sub_components[current].debug_info.as_ref()
254            && let Some(loc) = debug.sub_component_use_sites.get(instance_idx)
255            && loc.source_file.as_ref().is_some_and(|f| f.path() == us_path)
256            && loc.span.offset as u32 == us_offset
257        {
258            return true;
259        }
260        current = cu.sub_components[current].sub_components[instance_idx].ty;
261    }
262    false
263}
264
265/// `root` plus every instantiated repeated / conditional row instance
266/// below it, recursively.
267fn all_instances(root: &VRc<ItemTreeVTable, Instance>) -> Vec<VRc<ItemTreeVTable, Instance>> {
268    let mut out = Vec::new();
269    collect_instances(root, &mut out);
270    out
271}
272
273fn collect_instances(
274    inst: &VRc<ItemTreeVTable, Instance>,
275    out: &mut Vec<VRc<ItemTreeVTable, Instance>>,
276) {
277    out.push(inst.clone());
278    collect_row_instances(&inst.root_sub_component, out);
279}
280
281fn collect_row_instances(
282    sub: &Pin<Rc<SubComponentInstance>>,
283    out: &mut Vec<VRc<ItemTreeVTable, Instance>>,
284) {
285    for rep in sub.repeaters.iter() {
286        for row in rep.instances_vec() {
287            collect_instances(&row, out);
288        }
289    }
290    for nested in sub.sub_components.iter() {
291        collect_row_instances(nested, out);
292    }
293}
294
295/// Walk the LLR sub_components tree to resolve `path` into its concrete
296/// [`SubComponentIdx`].
297fn sub_component_idx_at_path(
298    cu: &i_slint_compiler::llr::CompilationUnit,
299    root_idx: SubComponentIdx,
300    path: &[SubComponentInstanceIdx],
301) -> SubComponentIdx {
302    let mut current = root_idx;
303    for &instance_idx in path {
304        let nested = &cu.sub_components[current].sub_components[instance_idx];
305        current = nested.ty;
306    }
307    current
308}
309
310/// Whether the item's LLR debug info marks it as an injected geometry
311/// wrapper (`Element::is_injected_wrapper_element`).
312fn is_injected_wrapper_element(instance: &VRc<ItemTreeVTable, Instance>, flat_idx: usize) -> bool {
313    let cu = &instance.root_sub_component.compilation_unit;
314    let root_ty = instance.root_sub_component.sub_component_idx;
315    let Some(Some((path, local_idx))) = instance.item_table.get(flat_idx) else {
316        return false;
317    };
318    let sc_idx = sub_component_idx_at_path(cu, root_ty, path);
319    cu.sub_components[sc_idx]
320        .debug_info
321        .as_ref()
322        .and_then(|debug| debug.items.get(*local_idx))
323        .is_some_and(|item_debug| item_debug.is_injected_wrapper_element)
324}
325
326fn item_flat_index_to_rect(
327    instance: &VRc<ItemTreeVTable, Instance>,
328    root: &VRc<ItemTreeVTable, Instance>,
329    flat_idx: usize,
330) -> Option<HighlightedRect> {
331    let vrc = VRc::into_dyn(instance.clone());
332    let root_vrc = VRc::into_dyn(root.clone());
333    let item_rc = ItemRc::new(vrc.clone(), flat_idx as u32);
334    let geometry = item_rc.geometry();
335    if geometry.size.is_empty() {
336        return None;
337    }
338    // Injected geometry wrappers (opacity/transform/clip/... created by
339    // `lower_property_to_element`) take over the element's geometry and lay the element
340    // out at (0,0) inside themselves, so measuring the parent frame from the element
341    // directly would collapse `rect.origin - parent_origin` to ~0.
342    let mut anchor = item_rc.clone();
343    while let Some(parent) =
344        anchor.parent_item(i_slint_core::item_tree::ParentItemTraversalMode::StopAtPopups)
345    {
346        if !VRc::ptr_eq(parent.item_tree(), &vrc) {
347            break; // crossed into another component instance's item tree
348        }
349        if !is_injected_wrapper_element(instance, parent.index() as usize) {
350            break;
351        }
352        anchor = parent;
353    }
354
355    let origin = item_rc.map_to_item_tree(geometry.origin, &root_vrc);
356    // `map_to_item_tree` does not add the item's own x/y, so mapping the zero point of
357    // the anchor yields the absolute origin of the element's source-parent coordinate
358    // system.
359    let parent_origin = anchor.map_to_item_tree(LogicalPoint::default(), &root_vrc);
360    // The source parent's absolute rotation: map a unit x-vector of the anchor's frame.
361    // `map_to_item_tree` applies the ancestors' transforms but not the anchor's own, so
362    // this excludes the element's own rotation (applied by its injected `Transform`).
363    let parent_rotation = {
364        let frame_x_axis = anchor.map_to_item_tree(LogicalPoint::new(1.0, 0.0), &root_vrc);
365        let delta = frame_x_axis - parent_origin;
366        delta.y.atan2(delta.x).to_degrees()
367    };
368    let top_right = item_rc
369        .map_to_item_tree(geometry.origin + euclid::vec2(geometry.size.width, 0.), &root_vrc);
370    let delta = top_right - origin;
371    let width = delta.length();
372    let height = if geometry.size.width == 0.0 {
373        0.0
374    } else {
375        geometry.size.height * width / geometry.size.width
376    };
377    let angle_rad = delta.y.atan2(delta.x);
378    let (sin, cos) = angle_rad.sin_cos();
379    let center = euclid::point2(
380        origin.x + (width / 2.0) * cos - (height / 2.0) * sin,
381        origin.y + (width / 2.0) * sin + (height / 2.0) * cos,
382    );
383    Some(HighlightedRect {
384        rect: LogicalRect {
385            origin: center - euclid::vec2(width / 2.0, height / 2.0),
386            size: euclid::size2(width, height),
387        },
388        angle: angle_rad.to_degrees(),
389        parent_origin,
390        parent_rotation,
391    })
392}
393
394fn positions_by_source(
395    root: &VRc<ItemTreeVTable, Instance>,
396    target_path: &Path,
397    target_offset: u32,
398    use_site: Option<(&Path, u32)>,
399    filter: ElementPositionFilter,
400) -> Vec<HighlightedRect> {
401    let cu = root.root_sub_component.compilation_unit.clone();
402    let mut results = Vec::new();
403    // Repeated / conditional rows are separate instances with their own
404    // item tables, so search all of them, mapping geometry back into the
405    // root instance's coordinates.
406    for instance in all_instances(root) {
407        for sc_idx in 0..cu.sub_components.len() {
408            let sc_idx: SubComponentIdx = sc_idx.into();
409            let sc = &cu.sub_components[sc_idx];
410            let Some(debug) = sc.debug_info.as_ref() else { continue };
411            for (local_idx, item_dbg) in debug.items.iter_enumerated() {
412                let Some(source_file) = item_dbg.source_location.source_file.as_ref() else {
413                    continue;
414                };
415                if source_file.path() != target_path {
416                    continue;
417                }
418                if item_dbg.source_location.span.offset as u32 != target_offset {
419                    continue;
420                }
421                for flat_idx in find_flat_indices_for_item(&instance, sc_idx, local_idx, use_site) {
422                    if filter == ElementPositionFilter::ExcludeClipped {
423                        let dyn_rc = vtable::VRc::into_dyn(instance.clone());
424                        let item_rc = i_slint_core::items::ItemRc::new(dyn_rc, flat_idx as u32);
425                        if !item_rc.is_visible() {
426                            continue;
427                        }
428                    }
429                    if let Some(rect) = item_flat_index_to_rect(&instance, root, flat_idx) {
430                        results.push(rect);
431                    }
432                }
433            }
434        }
435    }
436    results
437}
438
439#[cfg(test)]
440mod tests {
441    use crate::{
442        ComponentInstance,
443        debug_hook::tests::{compile_with_debug_hooks, test_path},
444    };
445
446    fn geometry_of(
447        instance: &ComponentInstance,
448        code: &str,
449        id: &str,
450    ) -> crate::highlight::HighlightedRect {
451        let id_position = code.find(id).unwrap_or_else(|| panic!("{id} not found"));
452        let offset = id_position + code[id_position..].find("Rectangle").unwrap();
453        let (element, _) = instance
454            .element_node_at_source_code_position(&test_path(), offset as u32)
455            .first()
456            .cloned()
457            .unwrap_or_else(|| panic!("element {id} not resolved"));
458        *instance.element_positions(&element).first().expect("geometry")
459    }
460
461    // With debug_hooks enabled every element is wrapped in injected geometry wrappers
462    // (`Transform`, plus `Opacity` etc. when those props are set), which take over the element's
463    // geometry. `element_positions` must still report a `parent_origin` from which the element's
464    // own `x`/`y` can be recovered (`rect.origin - parent_origin == x/y`), otherwise the editor
465    // commits wrong coordinates when repositioning. This must hold through stacked wrappers and
466    // for elements nested below a non-root parent.
467    #[test]
468    fn debug_hooks_parent_origin() {
469        let code = r#"
470export component Win inherits Window {
471    width: 300px;
472    height: 200px;
473    plain := Rectangle {
474        x: 30px;
475        y: 40px;
476        width: 50px;
477        height: 60px;
478    }
479    faded := Rectangle {
480        // extra Opacity and visibility-Clip wrappers stacked around the Transform wrapper
481        opacity: 0.5;
482        visible: true;
483        x: 70px;
484        y: 80px;
485        width: 40px;
486        height: 30px;
487    }
488    outer := Rectangle {
489        x: 10px;
490        y: 20px;
491        width: 120px;
492        height: 100px;
493        nested := Rectangle {
494            x: 5px;
495            y: 7px;
496            width: 20px;
497            height: 20px;
498        }
499    }
500}"#;
501        let instance = compile_with_debug_hooks(code);
502
503        let check = |id: &str, expected: (f32, f32)| {
504            let geometry = geometry_of(&instance, code, id);
505            let x = geometry.rect.origin.x - geometry.parent_origin.x;
506            let y = geometry.rect.origin.y - geometry.parent_origin.y;
507            assert!(
508                (x - expected.0).abs() < 0.5 && (y - expected.1).abs() < 0.5,
509                "{id}: source-relative position ({x}, {y}) should be {expected:?}"
510            );
511        };
512
513        check("plain", (30.0, 40.0));
514        check("faded", (70.0, 80.0));
515        check("nested", (5.0, 7.0));
516    }
517
518    #[test]
519    fn debug_hooks_parent_rotation() {
520        let code = r#"
521export component Win inherits Window {
522    width: 300px;
523    height: 300px;
524    outer := Rectangle {
525        x: 50px;
526        y: 50px;
527        width: 160px;
528        height: 160px;
529        transform-rotation: 30deg;
530        inner := Rectangle {
531            x: 20px;
532            y: 20px;
533            width: 40px;
534            height: 40px;
535            transform-rotation: 15deg;
536        }
537    }
538}"#;
539        let instance = compile_with_debug_hooks(code);
540
541        let check = |id: &str, expected: f32| {
542            let geometry = geometry_of(&instance, code, id);
543            let rotation = geometry.angle - geometry.parent_rotation;
544            assert!(
545                (rotation - expected).abs() < 0.5,
546                "{id}: source-relative rotation {rotation} should be {expected}"
547            );
548        };
549
550        check("outer", 30.0);
551        check("inner", 15.0);
552    }
553}