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//!
6use crate::instance::{Instance, SubComponentInstance};
7use i_slint_compiler::diagnostics::SourceLocation;
8use i_slint_compiler::llr::ItemInstanceIdx;
9use i_slint_compiler::object_tree::ElementRc;
10use i_slint_compiler::source_path::SourcePath;
11use i_slint_core::graphics::euclid;
12use i_slint_core::item_tree::{ItemTreeRc, ItemTreeVTable, TraversalOrder, VisitChildrenResult};
13use i_slint_core::items::ItemRc;
14use i_slint_core::lengths::{ItemTransform, LogicalPoint, LogicalRect, LogicalVector};
15use std::cell::OnceCell;
16use std::collections::{BTreeMap, HashMap, HashSet};
17use std::pin::Pin;
18use std::rc::Rc;
19use vtable::VRc;
20
21/// The rectangle of an element, which may be rotated around its center.
22#[derive(Clone, Copy, Debug, Default)]
23pub struct HighlightedRect {
24    /// The element's geometry.
25    pub rect: LogicalRect,
26    /// In degrees, around the center of the element.
27    pub angle: f32,
28    /// Whether `rect` and `angle` describe the element's rendered shape.
29    ///
30    /// The two of them describe a rotated rectangle.
31    /// An element renders as one while its own and its ancestors' transforms keep its two axes
32    /// at a right angle.
33    /// A non-uniform scale combined with a rotation shears it into a parallelogram instead,
34    /// which only `local_rect` still describes.
35    pub renders_as_rectangle: bool,
36    /// The element's rectangle in its parent's coordinate system.
37    ///
38    /// This is what the source `x`, `y`, `width` and `height` describe.
39    /// Unlike `rect`, no transform of the element or of any of its ancestors applies to it.
40    pub local_rect: LogicalRect,
41    /// Maps this instance's parent coordinate system to root coordinates.
42    ///
43    /// Invert it to turn a position picked in root coordinates into one that can be written to
44    /// the source.
45    /// It is composed from the instance's own ancestors,
46    /// so it stays correct even if the element is positioned outside of
47    /// (or with a negative offset relative to) its parent.
48    pub parent_transform: ItemTransform,
49    /// Evaluated parent-relative rotation in degrees, including complete turns.
50    pub transform_rotation: f32,
51    /// Evaluated corner radii in logical pixels.
52    pub corner_radii: CornerRadii,
53}
54/// Evaluated rectangle corner radii in logical pixels.
55#[derive(Clone, Copy, Debug, Default)]
56pub struct CornerRadii {
57    /// Top-left radius.
58    pub top_left: f32,
59    /// Top-right radius.
60    pub top_right: f32,
61    /// Bottom-left radius.
62    pub bottom_left: f32,
63    /// Bottom-right radius.
64    pub bottom_right: f32,
65}
66
67impl HighlightedRect {
68    /// Absolute origin of this instance's parent coordinate system, in root coordinates.
69    pub fn parent_origin(&self) -> LogicalPoint {
70        self.parent_transform.transform_point(LogicalPoint::default().cast()).cast()
71    }
72
73    /// Absolute rotation (in degrees) of this instance's parent coordinate system.
74    ///
75    /// `angle - parent_rotation()` yields the element's own rotation relative to its parent,
76    /// which matches `transform-rotation` modulo complete turns.
77    /// Use `transform_rotation` to retain those turns.
78    pub fn parent_rotation(&self) -> f32 {
79        self.parent_transform.m12.atan2(self.parent_transform.m11).to_degrees()
80    }
81
82    /// Returns true if `position` lies inside the (potentially rotated) rectangle.
83    pub fn contains(&self, position: LogicalPoint) -> bool {
84        let center = self.rect.center();
85        let rotation = euclid::Rotation2D::radians((-self.angle).to_radians());
86        let transformed = center + rotation.transform_vector(position - center);
87        self.rect.contains(transformed)
88    }
89}
90
91/// A rendered item under a point and one source element that represents it.
92#[derive(Clone, Debug)]
93pub struct ElementCandidate {
94    /// Source element represented by the runtime item.
95    pub source_location: i_slint_compiler::diagnostics::SourceLocation,
96    /// Runtime geometry of the source element.
97    pub geometry: HighlightedRect,
98    /// Position of this runtime item among the instances of the source element.
99    pub instance_index: usize,
100}
101
102/// Return the geometry of every runtime item whose source location covers
103/// the given `(path, offset)` pair.
104pub(crate) fn component_positions(
105    root: &VRc<ItemTreeVTable, Instance>,
106    path: &SourcePath,
107    offset: u32,
108) -> Vec<HighlightedRect> {
109    let sources = SourceOccurrences::new(root);
110    sources.matching_occurrences(path, offset).map(|(_, geometry)| geometry).collect()
111}
112
113pub(crate) fn element_candidates_at(
114    root: &VRc<ItemTreeVTable, Instance>,
115    position: LogicalPoint,
116) -> Vec<ElementCandidate> {
117    let sources = SourceOccurrences::new(root);
118    let root_item_tree = VRc::into_dyn(root.clone());
119    let mut runtime_items = Vec::new();
120    collect_runtime_items_front_to_back(&root_item_tree, 0, Some(root), &mut runtime_items);
121
122    let mut candidates = Vec::new();
123    let mut seen = HashSet::new();
124    let mut seen_enclosing_elements = HashSet::new();
125    for (instance, flat_index) in runtime_items {
126        let Some(&item_index) =
127            sources.item_indices.get(&(std::ptr::from_ref(&*instance), flat_index))
128        else {
129            continue;
130        };
131        let item = &sources.items[item_index];
132        let Some(geometry) = item.geometry(root) else { continue };
133        if !geometry.contains(position) || !item.as_item_rc().is_visible() {
134            continue;
135        }
136
137        let mut add_candidates = |source_location: &SourceLocation, item_indices: &[usize]| {
138            for &item_index in item_indices {
139                let Some(candidate) = sources.candidate_at(source_location, item_index, position)
140                else {
141                    continue;
142                };
143                if let Some(key) = source_key(source_location)
144                    && seen.insert((key, candidate.instance_index))
145                {
146                    candidates.push(candidate);
147                }
148            }
149        };
150        let definition = &item.owner.compilation_unit.sub_components[item.owner.sub_component_idx];
151        if let Some(debug_info) = &definition.debug_info
152            && let Some(debug_entries) = debug_info.items.get(item.local_item_index)
153        {
154            for debug_entry in debug_entries.iter().rev() {
155                add_candidates(&debug_entry.source_location, &[item_index]);
156            }
157        }
158
159        let mut owner = Some(item.owner.clone());
160        while let Some(component) = owner {
161            let component_pointer = std::ptr::from_ref(&*component);
162            if seen_enclosing_elements.insert(component_pointer)
163                && let Some((source_location, item_indices)) =
164                    sources.enclosing_elements.get(&component_pointer)
165            {
166                add_candidates(source_location, item_indices);
167            }
168            owner = component.parent.upgrade().map(Pin::new);
169        }
170    }
171    candidates
172}
173
174// The same element can have source ranges of different lengths.
175// Use only its file path and starting byte offset to group those entries.
176type SourceKey = (SourcePath, usize);
177type SourceElements = BTreeMap<SourceKey, SourceElement>;
178
179struct RuntimeItem {
180    instance: VRc<ItemTreeVTable, Instance>,
181    flat_index: usize,
182    owner: Pin<Rc<SubComponentInstance>>,
183    local_item_index: ItemInstanceIdx,
184    geometry: OnceCell<Option<HighlightedRect>>,
185}
186
187impl RuntimeItem {
188    fn as_item_rc(&self) -> ItemRc {
189        ItemRc::new(VRc::into_dyn(self.instance.clone()), self.flat_index as u32)
190    }
191
192    fn geometry(&self, root: &VRc<ItemTreeVTable, Instance>) -> Option<HighlightedRect> {
193        *self.geometry.get_or_init(|| item_geometry(&self.instance, root, self.flat_index))
194    }
195}
196
197#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
198enum SourcePriority {
199    Item,
200    ComponentUse,
201    RepeatedElement,
202}
203
204struct SourceElement {
205    source_location: SourceLocation,
206    // Indices into `SourceOccurrences.items` for the runtime items representing this source element.
207    item_indices: Vec<usize>,
208    priority: SourcePriority,
209    // Maps an index in `SourceOccurrences.items` to its position in `component_positions()`, skipping empty geometry.
210    instance_indices: OnceCell<HashMap<usize, usize>>,
211}
212
213impl SourceElement {
214    fn new(
215        source_location: &SourceLocation,
216        item_indices: Vec<usize>,
217        priority: SourcePriority,
218    ) -> Self {
219        Self {
220            source_location: source_location.clone(),
221            item_indices,
222            priority,
223            instance_indices: OnceCell::new(),
224        }
225    }
226
227    fn occurrences<'a>(
228        &'a self,
229        items: &'a [RuntimeItem],
230        root: &'a VRc<ItemTreeVTable, Instance>,
231    ) -> impl Iterator<Item = (usize, HighlightedRect)> + 'a {
232        self.item_indices.iter().filter_map(move |&item_index| {
233            items[item_index].geometry(root).map(|geometry| (item_index, geometry))
234        })
235    }
236}
237
238struct SourceOccurrences<'a> {
239    root: &'a VRc<ItemTreeVTable, Instance>,
240    items: Vec<RuntimeItem>,
241    // Maps an owning `Instance` and its flat item index to an index in `items`.
242    item_indices: HashMap<(*const Instance, usize), usize>,
243    elements: SourceElements,
244    // Indices into `items` for the items whose geometry represents this component use or repeated row.
245    enclosing_elements: HashMap<*const SubComponentInstance, (SourceLocation, Vec<usize>)>,
246}
247
248impl<'a> SourceOccurrences<'a> {
249    fn new(root: &'a VRc<ItemTreeVTable, Instance>) -> Self {
250        i_slint_core::item_tree::ensure_item_tree_instantiated(&VRc::into_dyn(root.clone()));
251        let mut sources = Self {
252            root,
253            items: Vec::new(),
254            item_indices: HashMap::new(),
255            elements: BTreeMap::new(),
256            enclosing_elements: HashMap::new(),
257        };
258        let mut local_item_indices = HashMap::new();
259        for instance in all_instances(root) {
260            for (flat_index, entry) in instance.item_table.iter().enumerate() {
261                let Some((path, local_item_index)) = entry else { continue };
262                let mut owner = instance.root_sub_component.clone();
263                for &component_index in path {
264                    owner = owner.sub_components[component_index].clone();
265                }
266                let item_index = sources.items.len();
267                local_item_indices
268                    .insert((std::ptr::from_ref(&*owner), *local_item_index), item_index);
269                sources
270                    .item_indices
271                    .insert((std::ptr::from_ref(&*instance), flat_index), item_index);
272                sources.items.push(RuntimeItem {
273                    instance: instance.clone(),
274                    flat_index,
275                    owner,
276                    local_item_index: *local_item_index,
277                    geometry: OnceCell::new(),
278                });
279            }
280        }
281        sources.elements = sources.collect_sources(&root.root_sub_component, &local_item_indices);
282        for element in sources.elements.values_mut() {
283            element.item_indices.sort_unstable();
284            element.item_indices.dedup();
285        }
286        for (_, item_indices) in sources.enclosing_elements.values_mut() {
287            item_indices.sort_unstable();
288            item_indices.dedup();
289        }
290        sources
291    }
292
293    fn collect_sources(
294        &mut self,
295        component: &Pin<Rc<SubComponentInstance>>,
296        // Maps a sub-component instance and an index in its `items` array to an index in `SourceOccurrences.items`.
297        local_item_indices: &HashMap<(*const SubComponentInstance, ItemInstanceIdx), usize>,
298    ) -> SourceElements {
299        let definition = &component.compilation_unit.sub_components[component.sub_component_idx];
300        let mut elements = BTreeMap::new();
301        if let Some(debug_info) = &definition.debug_info {
302            for (local_item_index, debug_entries) in debug_info.items.iter_enumerated() {
303                let Some(&item_index) =
304                    local_item_indices.get(&(std::ptr::from_ref(&**component), local_item_index))
305                else {
306                    continue;
307                };
308                for debug_entry in debug_entries {
309                    merge_sources(
310                        &mut elements,
311                        [SourceElement::new(
312                            &debug_entry.source_location,
313                            vec![item_index],
314                            SourcePriority::Item,
315                        )],
316                    );
317                }
318            }
319        }
320
321        for (component_index, child) in component.sub_components.iter_enumerated() {
322            let child_elements = self.collect_sources(child, local_item_indices);
323            if let Some(source_location) = definition
324                .debug_info
325                .as_ref()
326                .and_then(|debug_info| debug_info.sub_component_use_sites.get(component_index))
327            {
328                let root_items = component_root_items(child, &child_elements, source_location);
329                self.enclosing_elements.insert(
330                    std::ptr::from_ref(&**child),
331                    (source_location.clone(), root_items.clone()),
332                );
333                merge_sources(
334                    &mut elements,
335                    [SourceElement::new(source_location, root_items, SourcePriority::ComponentUse)],
336                );
337            }
338            merge_sources(&mut elements, child_elements.into_values());
339        }
340
341        for (repeated_index, repeater) in component.repeaters.iter_enumerated() {
342            let source_location = definition
343                .debug_info
344                .as_ref()
345                .and_then(|debug_info| debug_info.repeated_elements.get(repeated_index));
346            let mut root_items = Vec::new();
347            for row in repeater.instances_vec() {
348                let row_elements =
349                    self.collect_sources(&row.root_sub_component, local_item_indices);
350                if let Some(source_location) = source_location {
351                    let row_root_items = component_root_items(
352                        &row.root_sub_component,
353                        &row_elements,
354                        source_location,
355                    );
356                    self.enclosing_elements.insert(
357                        std::ptr::from_ref(&*row.root_sub_component),
358                        (source_location.clone(), row_root_items.clone()),
359                    );
360                    root_items.extend(row_root_items);
361                }
362                merge_sources(&mut elements, row_elements.into_values());
363            }
364            if let Some(source_location) = source_location {
365                merge_sources(
366                    &mut elements,
367                    [SourceElement::new(
368                        source_location,
369                        root_items,
370                        SourcePriority::RepeatedElement,
371                    )],
372                );
373            }
374        }
375        elements
376    }
377
378    fn matching_occurrences(
379        &self,
380        path: &SourcePath,
381        offset: u32,
382    ) -> impl Iterator<Item = (usize, HighlightedRect)> {
383        self.elements
384            .values()
385            .filter(|element| source_location_contains(&element.source_location, path, offset))
386            .flat_map(|element| element.occurrences(&self.items, self.root))
387            .collect::<BTreeMap<_, _>>()
388            .into_iter()
389    }
390
391    fn candidate_at(
392        &self,
393        source_location: &SourceLocation,
394        item_index: usize,
395        position: LogicalPoint,
396    ) -> Option<ElementCandidate> {
397        let element = self.elements.get(&source_key(source_location)?)?;
398        let instance_indices = element.instance_indices.get_or_init(|| {
399            element
400                .occurrences(&self.items, self.root)
401                .enumerate()
402                .map(|(instance_index, (item_index, _))| (item_index, instance_index))
403                .collect()
404        });
405        let &instance_index = instance_indices.get(&item_index)?;
406        let item = &self.items[item_index];
407        let geometry = item.geometry(self.root)?;
408        if !geometry.contains(position) || !item.as_item_rc().is_visible() {
409            return None;
410        }
411        Some(ElementCandidate {
412            source_location: element.source_location.clone(),
413            geometry,
414            instance_index,
415        })
416    }
417}
418
419fn source_key(source_location: &SourceLocation) -> Option<SourceKey> {
420    Some((source_location.source_file.as_ref()?.path().clone(), source_location.span.offset))
421}
422
423fn merge_sources(elements: &mut SourceElements, sources: impl IntoIterator<Item = SourceElement>) {
424    for source in sources {
425        let Some(key) = source_key(&source.source_location) else { continue };
426        let element = elements.entry(key).or_insert_with(|| {
427            SourceElement::new(&source.source_location, Vec::new(), source.priority)
428        });
429        if source.priority > element.priority {
430            *element = source;
431        } else if source.priority == element.priority {
432            element.item_indices.extend(source.item_indices);
433        }
434    }
435}
436
437fn component_root_items(
438    component: &SubComponentInstance,
439    elements: &SourceElements,
440    source_location: &SourceLocation,
441) -> Vec<usize> {
442    component.compilation_unit.sub_components[component.sub_component_idx]
443        .debug_info
444        .as_ref()
445        .and_then(|debug_info| source_key(&debug_info.source_location))
446        .or_else(|| source_key(source_location))
447        .and_then(|key| elements.get(&key))
448        .map(|element| element.item_indices.clone())
449        .unwrap_or_default()
450}
451
452/// Look up the `(ElementRc, index)` tuples whose `debug` entries cover
453/// the given source offset. Uses the `TypeLoader` stored on the instance
454/// (if available) to walk the original object-tree `Document`.
455pub(crate) fn element_node_at_source_code_position(
456    instance: &VRc<ItemTreeVTable, Instance>,
457    path: &SourcePath,
458    offset: u32,
459) -> Vec<(ElementRc, usize)> {
460    let Some(type_loader) = instance.type_loaders.type_loader.as_ref() else {
461        return Vec::new();
462    };
463    let Some(doc) = type_loader.get_document(path) else {
464        return Vec::new();
465    };
466    let mut result = Vec::new();
467    // `inner_components` lists every component defined in the file,
468    // exported or not.
469    for component in &doc.inner_components {
470        visit_element_for_position(&component.root_element, path, offset, &mut result);
471    }
472    result
473}
474
475fn visit_element_for_position(
476    element: &ElementRc,
477    path: &SourcePath,
478    offset: u32,
479    result: &mut Vec<(ElementRc, usize)>,
480) {
481    if element.borrow().repeated.is_some() {
482        // The children of a repeated element live in the component the
483        // repeater pass wrapped around it, which is not part of
484        // `inner_components` — descend explicitly. The wrapper's root
485        // element carries the same source node as the repeated element.
486        let base = match &element.borrow().base_type {
487            i_slint_compiler::langtype::ElementType::Component(c) => Some(c.root_element.clone()),
488            _ => None,
489        };
490        if let Some(root) = base {
491            visit_element_for_position(&root, path, offset, result);
492        }
493        return;
494    }
495    for (index, node_path, node_range) in
496        element.borrow().debug.iter().enumerate().map(|(index, debug_info)| {
497            let text_range = debug_info
498                .node
499                .QualifiedName()
500                .map(|qualified_name| qualified_name.text_range())
501                .or_else(|| {
502                    debug_info
503                        .node
504                        .child_token(i_slint_compiler::parser::SyntaxKind::LBrace)
505                        .map(|left_brace| left_brace.text_range())
506                })
507                .expect("An Element must contain a LBrace somewhere");
508            (index, debug_info.node.source_file.path(), text_range)
509        })
510    {
511        if node_path == path && node_range.contains(offset.into()) {
512            result.push((element.clone(), index));
513        }
514    }
515    let children = element.borrow().children.clone();
516    for child in &children {
517        visit_element_for_position(child, path, offset, result);
518    }
519}
520
521/// `root` plus every instantiated repeated / conditional row instance
522/// below it, recursively.
523fn all_instances(root: &VRc<ItemTreeVTable, Instance>) -> Vec<VRc<ItemTreeVTable, Instance>> {
524    let mut instances = Vec::new();
525    collect_instances(root, &mut instances);
526    instances
527}
528
529fn collect_instances(
530    instance: &VRc<ItemTreeVTable, Instance>,
531    instances: &mut Vec<VRc<ItemTreeVTable, Instance>>,
532) {
533    instances.push(instance.clone());
534    collect_row_instances(&instance.root_sub_component, instances);
535}
536
537fn collect_row_instances(
538    sub_component: &Pin<Rc<SubComponentInstance>>,
539    instances: &mut Vec<VRc<ItemTreeVTable, Instance>>,
540) {
541    for repeater in sub_component.repeaters.iter() {
542        repeater.track_instance_changes();
543        for row in repeater.instances_vec() {
544            collect_instances(&row, instances);
545        }
546    }
547    for nested_sub_component in sub_component.sub_components.iter() {
548        collect_row_instances(nested_sub_component, instances);
549    }
550}
551
552fn collect_runtime_items_front_to_back(
553    item_tree: &ItemTreeRc,
554    index: u32,
555    instance: Option<&VRc<ItemTreeVTable, Instance>>,
556    runtime_items: &mut Vec<(VRc<ItemTreeVTable, Instance>, usize)>,
557) {
558    let mut visit_child = |child_item_tree: &ItemTreeRc,
559                           child_index: u32,
560                           _: Pin<i_slint_core::items::ItemRef<'_>>|
561     -> VisitChildrenResult {
562        let resolved_instance;
563        let child_instance = if VRc::ptr_eq(child_item_tree, item_tree) {
564            instance
565        } else {
566            resolved_instance = VRc::borrow(child_item_tree)
567                .downcast::<Instance>()
568                .and_then(|instance| instance.self_weak.get())
569                .and_then(|instance| instance.upgrade());
570            resolved_instance.as_ref()
571        };
572        collect_runtime_items_front_to_back(
573            child_item_tree,
574            child_index,
575            child_instance,
576            runtime_items,
577        );
578        VisitChildrenResult::CONTINUE
579    };
580    vtable::new_vref!(
581        let mut visit_child: VRefMut<i_slint_core::item_tree::ItemVisitorVTable>
582            for i_slint_core::item_tree::ItemVisitor = &mut visit_child
583    );
584    VRc::borrow_pin(item_tree).as_ref().visit_children_item(
585        index as isize,
586        TraversalOrder::FrontToBack,
587        visit_child,
588    );
589    if let Some(instance) = instance {
590        runtime_items.push((instance.clone(), index as usize));
591    }
592}
593
594fn item_geometry(
595    instance: &VRc<ItemTreeVTable, Instance>,
596    root: &VRc<ItemTreeVTable, Instance>,
597    flat_idx: usize,
598) -> Option<HighlightedRect> {
599    let vrc = VRc::into_dyn(instance.clone());
600    let root_vrc = VRc::into_dyn(root.clone());
601    let item_rc = ItemRc::new(vrc.clone(), flat_idx as u32);
602    let geometry = item_rc.geometry();
603    if geometry.size.is_empty() {
604        return None;
605    }
606    // Injected geometry wrappers (opacity/transform/clip/... created by
607    // `lower_property_to_element`) take over the element's geometry and lay the element
608    // out at (0,0) inside themselves, so measuring the parent frame from the element
609    // directly would collapse `rect.origin - parent_origin` to ~0.
610    let mut transform_rotation = 0.;
611    let mut anchor = item_rc.clone();
612    while let Some(parent) =
613        anchor.parent_item(i_slint_core::item_tree::ParentItemTraversalMode::StopAtPopups)
614    {
615        if !VRc::ptr_eq(parent.item_tree(), &vrc) {
616            break; // crossed into another component instance's item tree
617        }
618        if !parent.is_geometry_wrapper() {
619            break;
620        }
621        if let Some(transform) = i_slint_core::items::ItemRef::downcast_pin::<
622            i_slint_core::items::Transform,
623        >(parent.borrow())
624        {
625            transform_rotation += transform.transform_rotation();
626        }
627        anchor = parent;
628    }
629
630    // Neither transform adds its item's own x/y, so `parent_transform` maps the element's
631    // source-parent coordinate system, and `to_root` the coordinate system the injected
632    // wrappers place the element in — the element's own transform included.
633    let to_root = item_rc.transform_to_item_tree(&root_vrc);
634    let parent_transform = anchor.transform_to_item_tree(&root_vrc);
635    let map_axis = |axis: euclid::Vector2D<f32, _>| to_root.transform_vector(axis).cast();
636
637    let origin: LogicalPoint = to_root.transform_point(geometry.origin.cast()).cast();
638    // Both edges are measured, so a scale along one axis is not assumed to match the other.
639    let size = geometry.size.cast::<f32>();
640    let x_axis = map_axis(euclid::vec2(size.width, 0.));
641    let y_axis = map_axis(euclid::vec2(0., size.height));
642    let width = x_axis.length();
643    let height = y_axis.length();
644    let center = origin + (x_axis + y_axis) / 2.0;
645    Some(HighlightedRect {
646        rect: LogicalRect {
647            origin: center - euclid::vec2(width / 2.0, height / 2.0),
648            size: euclid::size2(width, height),
649        },
650        angle: x_axis.y.atan2(x_axis.x).to_degrees(),
651        renders_as_rectangle: are_perpendicular(x_axis, y_axis),
652        local_rect: if anchor == item_rc { geometry } else { anchor.geometry() },
653        parent_transform,
654        transform_rotation,
655        corner_radii: item_corner_radii(item_rc.borrow()),
656    })
657}
658
659/// Whether two axes still meet at a right angle.
660/// The cosine between them is compared, so the tolerance does not depend on how long they are.
661fn are_perpendicular(x: LogicalVector, y: LogicalVector) -> bool {
662    let squared_lengths = x.square_length() * y.square_length();
663    let dot = x.dot(y);
664    squared_lengths == 0. || dot * dot < 1.0e-6 * squared_lengths
665}
666
667fn item_corner_radii(item: Pin<i_slint_core::items::ItemRef<'_>>) -> CornerRadii {
668    use i_slint_core::items::{BasicBorderRectangle, BorderRectangle, ItemRef};
669    if let Some(rect) = ItemRef::downcast_pin::<BorderRectangle>(item) {
670        CornerRadii {
671            top_left: rect.border_top_left_radius().get(),
672            top_right: rect.border_top_right_radius().get(),
673            bottom_left: rect.border_bottom_left_radius().get(),
674            bottom_right: rect.border_bottom_right_radius().get(),
675        }
676    } else if let Some(rect) = ItemRef::downcast_pin::<BasicBorderRectangle>(item) {
677        let radius = rect.border_radius().get();
678        CornerRadii {
679            top_left: radius,
680            top_right: radius,
681            bottom_left: radius,
682            bottom_right: radius,
683        }
684    } else {
685        CornerRadii::default()
686    }
687}
688
689#[cfg(all(test, feature = "internal"))]
690fn items_by_source(
691    root: &VRc<ItemTreeVTable, Instance>,
692    target_path: &SourcePath,
693    target_offset: u32,
694    _source_match: SourceMatch,
695) -> Vec<(VRc<ItemTreeVTable, Instance>, usize)> {
696    let sources = SourceOccurrences::new(root);
697    sources
698        .elements
699        .get(&(target_path.clone(), target_offset as usize))
700        .into_iter()
701        .flat_map(|element| &element.item_indices)
702        .map(|&item_index| {
703            let item = &sources.items[item_index];
704            (item.instance.clone(), item.flat_index)
705        })
706        .collect()
707}
708
709#[cfg(all(test, feature = "internal"))]
710enum SourceMatch {
711    Start,
712}
713
714fn source_location_contains(
715    source_location: &SourceLocation,
716    target_path: &SourcePath,
717    target_offset: u32,
718) -> bool {
719    let Some(source_file) = source_location.source_file.as_ref() else { return false };
720    if source_file.path() != target_path {
721        return false;
722    }
723    let target_offset = target_offset as usize;
724    source_location.span.offset <= target_offset
725        && target_offset < source_location.span.offset.saturating_add(source_location.span.length)
726}
727
728#[cfg(all(test, feature = "internal"))]
729mod tests {
730    use crate::{
731        ComponentInstance,
732        debug_hook::tests::{compile_with_debug_hooks, test_path},
733    };
734    use i_slint_core::item_tree::ParentItemTraversalMode;
735    use i_slint_core::items::{BoxShadow, Clip, ItemRc, Layer, Opacity, Transform};
736    use vtable::VRc;
737
738    fn geometry_of(
739        instance: &ComponentInstance,
740        code: &str,
741        id: &str,
742    ) -> crate::highlight::HighlightedRect {
743        let id_position = code.find(id).unwrap_or_else(|| panic!("{id} not found"));
744        let offset = id_position + code[id_position..].find("Rectangle").unwrap();
745        *instance.component_positions(&test_path(), offset as u32).first().expect("geometry")
746    }
747
748    fn runtime_item_of(instance: &ComponentInstance, source: &str, element_id: &str) -> ItemRc {
749        let element_id_position =
750            source.find(element_id).unwrap_or_else(|| panic!("{element_id} not found"));
751        let offset = element_id_position + source[element_id_position..].find("Rectangle").unwrap();
752        let (runtime_instance, flat_index) = super::items_by_source(
753            instance.inner.vrc(),
754            &test_path(),
755            offset as u32,
756            super::SourceMatch::Start,
757        )
758        .into_iter()
759        .next()
760        .unwrap_or_else(|| panic!("runtime item for {element_id}"));
761        ItemRc::new(VRc::into_dyn(runtime_instance), flat_index as u32)
762    }
763
764    #[test]
765    fn geometry_wrapper_classification() {
766        let code = r#"
767export component Win inherits Window {
768    wrapped := Rectangle {
769        opacity: 0.5;
770        visible: true;
771        transform-rotation: 45deg;
772        cache-rendering-hint: true;
773        drop-shadow-blur: 2px;
774        drop-shadow-color: black;
775    }
776    ordinary-clip := Rectangle { clip: true; }
777}
778"#;
779        let instance = compile_with_debug_hooks(code);
780        let mut ancestors = Vec::new();
781        let mut current = Some(runtime_item_of(&instance, code, "wrapped"));
782        while let Some(item) = current {
783            current = item.parent_item(ParentItemTraversalMode::StopAtPopups);
784            ancestors.push(item);
785        }
786
787        for wrapper_classification in [
788            ancestors
789                .iter()
790                .find(|item| item.downcast::<Transform>().is_some())
791                .map(ItemRc::is_geometry_wrapper),
792            ancestors
793                .iter()
794                .find(|item| item.downcast::<Opacity>().is_some())
795                .map(ItemRc::is_geometry_wrapper),
796            ancestors
797                .iter()
798                .find(|item| item.downcast::<Layer>().is_some())
799                .map(ItemRc::is_geometry_wrapper),
800            ancestors
801                .iter()
802                .find(|item| {
803                    item.downcast::<Clip>()
804                        .is_some_and(|clip| clip.as_pin_ref().is_visibility_clip())
805                })
806                .map(ItemRc::is_geometry_wrapper),
807        ] {
808            assert_eq!(wrapper_classification, Some(true));
809        }
810
811        let root_item_tree = VRc::into_dyn(instance.inner.vrc().clone());
812        let box_shadow = (0..instance.inner.vrc().item_table.len())
813            .map(|flat_index| ItemRc::new(root_item_tree.clone(), flat_index as u32))
814            .find(|item| item.downcast::<BoxShadow>().is_some())
815            .expect("box shadow");
816        assert!(!box_shadow.is_geometry_wrapper());
817
818        let ordinary_clip = (0..instance.inner.vrc().item_table.len())
819            .map(|flat_index| ItemRc::new(root_item_tree.clone(), flat_index as u32))
820            .find(|item| {
821                item.downcast::<Clip>().is_some_and(|clip| !clip.as_pin_ref().is_visibility_clip())
822            })
823            .expect("ordinary clip");
824        assert!(!ordinary_clip.is_geometry_wrapper());
825    }
826
827    // With debug_hooks enabled every element is wrapped in injected geometry wrappers
828    // (`Transform`, plus `Opacity` etc. when those props are set), which take over the element's
829    // geometry. `element_positions` must still report a `parent_origin` from which the element's
830    // own `x`/`y` can be recovered (`rect.origin - parent_origin == x/y`), otherwise the editor
831    // commits wrong coordinates when repositioning. This must hold through stacked wrappers and
832    // for elements nested below a non-root parent.
833    #[test]
834    fn debug_hooks_parent_origin() {
835        let code = r#"
836export component Win inherits Window {
837    width: 300px;
838    height: 200px;
839    plain := Rectangle {
840        x: 30px;
841        y: 40px;
842        width: 50px;
843        height: 60px;
844    }
845    faded := Rectangle {
846        // extra Opacity and visibility-Clip wrappers stacked around the Transform wrapper
847        opacity: 0.5;
848        visible: true;
849        x: 70px;
850        y: 80px;
851        width: 40px;
852        height: 30px;
853    }
854    clipped := Rectangle {
855        clip: true;
856        x: 90px;
857        y: 15px;
858        width: 30px;
859        height: 25px;
860        clipped-child := Rectangle {
861            x: 3px;
862            y: 4px;
863            width: 10px;
864            height: 12px;
865        }
866    }
867    outer := Rectangle {
868        x: 10px;
869        y: 20px;
870        width: 120px;
871        height: 100px;
872        nested := Rectangle {
873            x: 5px;
874            y: 7px;
875            width: 20px;
876            height: 20px;
877        }
878    }
879}"#;
880        let instance = compile_with_debug_hooks(code);
881
882        for (element_id, expected_position) in [
883            ("plain", (30.0, 40.0)),
884            ("faded", (70.0, 80.0)),
885            ("clipped", (90.0, 15.0)),
886            ("clipped-child", (3.0, 4.0)),
887            ("nested", (5.0, 7.0)),
888        ] {
889            let geometry = geometry_of(&instance, code, element_id);
890            let horizontal_position = geometry.rect.origin.x - geometry.parent_origin().x;
891            let vertical_position = geometry.rect.origin.y - geometry.parent_origin().y;
892            assert!(
893                (horizontal_position - expected_position.0).abs() < 0.5
894                    && (vertical_position - expected_position.1).abs() < 0.5,
895                "{element_id}: source-relative position ({horizontal_position}, {vertical_position}) should be {expected_position:?}"
896            );
897        }
898    }
899
900    #[test]
901    fn debug_hooks_local_rect() {
902        let code = r#"
903export component Win inherits Window {
904    width: 300px;
905    height: 300px;
906    outer := Rectangle {
907        x: 50px;
908        y: 60px;
909        width: 160px;
910        height: 140px;
911        transform-rotation: 30deg;
912        inner := Rectangle {
913            x: 20px;
914            y: 25px;
915            width: 40px;
916            height: 30px;
917            transform-rotation: 15deg;
918            transform-origin: { x: 0px, y: 0px };
919        }
920    }
921}"#;
922        let instance = compile_with_debug_hooks(code);
923
924        let check = |id: &str, expected: (f32, f32, f32, f32)| {
925            let rect = geometry_of(&instance, code, id).local_rect;
926            let actual = (rect.origin.x, rect.origin.y, rect.width(), rect.height());
927            assert!(
928                (actual.0 - expected.0).abs() < 0.5
929                    && (actual.1 - expected.1).abs() < 0.5
930                    && (actual.2 - expected.2).abs() < 0.5
931                    && (actual.3 - expected.3).abs() < 0.5,
932                "{id}: parent-relative rectangle {actual:?} should be {expected:?}"
933            );
934        };
935
936        check("outer", (50.0, 60.0, 160.0, 140.0));
937        check("inner", (20.0, 25.0, 40.0, 30.0));
938    }
939
940    #[test]
941    fn scaled_geometry_is_measured_per_axis() {
942        let code = r#"
943export component Win inherits Window {
944    width: 400px;
945    height: 400px;
946    stretched := Rectangle {
947        x: 20px;
948        y: 20px;
949        width: 80px;
950        height: 40px;
951        transform-scale-x: 3;
952    }
953    wide := Rectangle {
954        x: 20px;
955        y: 200px;
956        width: 200px;
957        height: 100px;
958        transform-scale-x: 3;
959        sheared := Rectangle {
960            width: 80px;
961            height: 40px;
962            transform-rotation: 30deg;
963        }
964    }
965}"#;
966        let instance = compile_with_debug_hooks(code);
967
968        // Both axes are measured, so a scale along one of them doesn't stretch the other.
969        let stretched = geometry_of(&instance, code, "stretched");
970        assert!(
971            (stretched.rect.width() - 240.).abs() < 0.5
972                && (stretched.rect.height() - 40.).abs() < 0.5,
973            "stretched: {:?}",
974            stretched.rect
975        );
976        assert!(stretched.renders_as_rectangle);
977
978        // A rotation below a non-uniform scale is a parallelogram, which `rect` can't express.
979        let sheared = geometry_of(&instance, code, "sheared");
980        assert!(!sheared.renders_as_rectangle);
981        assert!((sheared.angle - 30.).abs() > 1., "sheared angle: {}", sheared.angle);
982    }
983
984    #[test]
985    fn debug_hooks_parent_rotation() {
986        let code = r#"
987export component Win inherits Window {
988    width: 300px;
989    height: 300px;
990    outer := Rectangle {
991        x: 50px;
992        y: 50px;
993        width: 160px;
994        height: 160px;
995        transform-rotation: 30deg;
996        inner := Rectangle {
997            x: 20px;
998            y: 20px;
999            width: 40px;
1000            height: 40px;
1001            transform-rotation: 15deg;
1002        }
1003    }
1004}"#;
1005        let instance = compile_with_debug_hooks(code);
1006
1007        let check = |id: &str, expected: f32| {
1008            let geometry = geometry_of(&instance, code, id);
1009            let rotation = geometry.angle - geometry.parent_rotation();
1010            assert!(
1011                (rotation - expected).abs() < 0.5,
1012                "{id}: source-relative rotation {rotation} should be {expected}"
1013            );
1014        };
1015
1016        check("outer", 30.0);
1017        check("inner", 15.0);
1018    }
1019    #[test]
1020    fn evaluated_rotation_and_radii_share_geometry_instances() {
1021        let code = r#"
1022export component Win inherits Window {
1023    width: 300px;
1024    height: 300px;
1025    outer := Rectangle {
1026        width: 200px;
1027        height: 200px;
1028        transform-rotation: 30deg;
1029        for angle in [382.25deg, -397.5deg]: Rectangle {
1030            width: 40px;
1031            height: 40px;
1032            transform-rotation: angle;
1033            border-top-left-radius: 1.5px;
1034            border-top-right-radius: 2.5px;
1035            border-bottom-left-radius: 3.5px;
1036            border-bottom-right-radius: 4.5px;
1037        }
1038    }
1039}"#;
1040        let instance = compile_with_debug_hooks(code);
1041        let offset = code
1042            .find(
1043                "Rectangle {
1044            width",
1045            )
1046            .unwrap();
1047        let geometries = instance.component_positions(&test_path(), offset as u32);
1048        assert_eq!(geometries.len(), 2);
1049        for (geometry, expected) in geometries.iter().zip([382.25, -397.5]) {
1050            assert_eq!(geometry.transform_rotation, expected);
1051            assert!((geometry.parent_rotation() - 30.).abs() < 0.001);
1052            let radii = geometry.corner_radii;
1053            assert_eq!(
1054                [radii.top_left, radii.top_right, radii.bottom_left, radii.bottom_right],
1055                [1.5, 2.5, 3.5, 4.5]
1056            );
1057        }
1058    }
1059}