1use 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::{ItemTransform, LogicalPoint, LogicalRect, LogicalVector};
18use std::path::Path;
19use std::pin::Pin;
20use std::rc::Rc;
21use vtable::VRc;
22
23#[derive(Clone, Copy, Debug, Default)]
25pub struct HighlightedRect {
26 pub rect: LogicalRect,
28 pub angle: f32,
30 pub renders_as_rectangle: bool,
38 pub local_rect: LogicalRect,
43 pub parent_transform: ItemTransform,
51 pub transform_rotation: f32,
53 pub corner_radii: CornerRadii,
55}
56#[derive(Clone, Copy, Debug, Default)]
58pub struct CornerRadii {
59 pub top_left: f32,
61 pub top_right: f32,
63 pub bottom_left: f32,
65 pub bottom_right: f32,
67}
68
69impl HighlightedRect {
70 pub fn parent_origin(&self) -> LogicalPoint {
72 self.parent_transform.transform_point(LogicalPoint::default().cast()).cast()
73 }
74
75 pub fn parent_rotation(&self) -> f32 {
81 self.parent_transform.m12.atan2(self.parent_transform.m11).to_degrees()
82 }
83
84 pub fn contains(&self, position: LogicalPoint) -> bool {
86 let center = self.rect.center();
87 let rotation = euclid::Rotation2D::radians((-self.angle).to_radians());
88 let transformed = center + rotation.transform_vector(position - center);
89 self.rect.contains(transformed)
90 }
91}
92
93#[derive(Copy, Clone, Eq, PartialEq)]
95pub enum ElementPositionFilter {
96 IncludeClipped,
98 ExcludeClipped,
100}
101
102pub fn element_positions(
107 instance: &VRc<ItemTreeVTable, Instance>,
108 element: &ElementRc,
109 filter: ElementPositionFilter,
110) -> Vec<HighlightedRect> {
111 let target = walk_to_native_root(element);
117 let Some(target_loc) = source_location_of(&target) else {
118 return Vec::new();
119 };
120 let use_site = if Rc::ptr_eq(&target, element) { None } else { source_location_of(element) };
125 positions_by_source(
126 instance,
127 &target_loc.0,
128 target_loc.1,
129 use_site.as_ref().map(|(p, o)| (p.as_path(), *o)),
130 filter,
131 )
132}
133
134fn source_location_of(element: &ElementRc) -> Option<(std::path::PathBuf, u32)> {
138 use i_slint_compiler::diagnostics::Spanned;
139 let e = element.borrow();
140 let path = e.source_file()?.path().to_path_buf();
141 Some((path, e.span().offset as u32))
142}
143
144fn walk_to_native_root(element: &ElementRc) -> ElementRc {
149 let mut current = element.clone();
150 loop {
151 let next = {
152 let b = current.borrow();
153 if let i_slint_compiler::langtype::ElementType::Component(c) = &b.base_type {
154 Some(c.root_element.clone())
155 } else {
156 None
157 }
158 };
159 match next {
160 Some(n) => current = n,
161 None => return current,
162 }
163 }
164}
165
166pub(crate) fn component_positions(
169 instance: &VRc<ItemTreeVTable, Instance>,
170 path: &Path,
171 offset: u32,
172) -> Vec<HighlightedRect> {
173 element_node_at_source_code_position(instance, path, offset)
174 .into_iter()
175 .flat_map(|(element, _)| {
176 element_positions(instance, &element, ElementPositionFilter::IncludeClipped)
177 })
178 .collect()
179}
180
181pub(crate) fn element_node_at_source_code_position(
185 instance: &VRc<ItemTreeVTable, Instance>,
186 path: &Path,
187 offset: u32,
188) -> Vec<(ElementRc, usize)> {
189 let Some(type_loader) = instance.type_loaders.type_loader.as_ref() else {
190 return Vec::new();
191 };
192 let Some(doc) = type_loader.get_document(path) else {
193 return Vec::new();
194 };
195 let mut result = Vec::new();
196 for component in &doc.inner_components {
199 visit_element_for_position(&component.root_element, path, offset, &mut result);
200 }
201 result
202}
203
204fn visit_element_for_position(
205 element: &ElementRc,
206 path: &Path,
207 offset: u32,
208 result: &mut Vec<(ElementRc, usize)>,
209) {
210 if element.borrow().repeated.is_some() {
211 let base = match &element.borrow().base_type {
216 i_slint_compiler::langtype::ElementType::Component(c) => Some(c.root_element.clone()),
217 _ => None,
218 };
219 if let Some(root) = base {
220 visit_element_for_position(&root, path, offset, result);
221 }
222 return;
223 }
224 for (index, node_path, node_range) in element.borrow().debug.iter().enumerate().map(|(i, n)| {
225 let text_range = n
226 .node
227 .QualifiedName()
228 .map(|n| n.text_range())
229 .or_else(|| {
230 n.node
231 .child_token(i_slint_compiler::parser::SyntaxKind::LBrace)
232 .map(|n| n.text_range())
233 })
234 .expect("An Element must contain a LBrace somewhere");
235 (i, n.node.source_file.path(), text_range)
236 }) {
237 if node_path == path && node_range.contains(offset.into()) {
238 result.push((element.clone(), index));
239 }
240 }
241 let children = element.borrow().children.clone();
242 for child in &children {
243 visit_element_for_position(child, path, offset, result);
244 }
245}
246
247fn find_flat_indices_for_item(
252 instance: &VRc<ItemTreeVTable, Instance>,
253 target_sc_idx: SubComponentIdx,
254 target_local: ItemInstanceIdx,
255 use_site: Option<(&Path, u32)>,
256) -> Vec<usize> {
257 let cu = &instance.root_sub_component.compilation_unit;
258 let root_ty = instance.root_sub_component.sub_component_idx;
259 let mut out = Vec::new();
260 for (flat, entry) in instance.item_table.iter().enumerate() {
261 let Some((path, local_idx)) = entry.as_ref() else { continue };
262 if *local_idx != target_local {
263 continue;
264 }
265 if sub_component_idx_at_path(cu, root_ty, path) != target_sc_idx {
266 continue;
267 }
268 if let Some((us_path, us_offset)) = use_site
269 && !path_passes_use_site(cu, root_ty, path, us_path, us_offset)
270 {
271 continue;
272 }
273 out.push(flat);
274 }
275 out
276}
277
278fn path_passes_use_site(
281 cu: &i_slint_compiler::llr::CompilationUnit,
282 mut current: SubComponentIdx,
283 path: &[SubComponentInstanceIdx],
284 us_path: &Path,
285 us_offset: u32,
286) -> bool {
287 for &instance_idx in path {
288 if let Some(debug) = cu.sub_components[current].debug_info.as_ref()
289 && let Some(loc) = debug.sub_component_use_sites.get(instance_idx)
290 && loc.source_file.as_ref().is_some_and(|f| f.path() == us_path)
291 && loc.span.offset as u32 == us_offset
292 {
293 return true;
294 }
295 current = cu.sub_components[current].sub_components[instance_idx].ty;
296 }
297 false
298}
299
300fn all_instances(root: &VRc<ItemTreeVTable, Instance>) -> Vec<VRc<ItemTreeVTable, Instance>> {
303 let mut out = Vec::new();
304 collect_instances(root, &mut out);
305 out
306}
307
308fn collect_instances(
309 inst: &VRc<ItemTreeVTable, Instance>,
310 out: &mut Vec<VRc<ItemTreeVTable, Instance>>,
311) {
312 out.push(inst.clone());
313 collect_row_instances(&inst.root_sub_component, out);
314}
315
316fn collect_row_instances(
317 sub: &Pin<Rc<SubComponentInstance>>,
318 out: &mut Vec<VRc<ItemTreeVTable, Instance>>,
319) {
320 for repeater in sub.repeaters.iter() {
321 repeater.track_instance_changes();
322 for row in repeater.instances_vec() {
323 collect_instances(&row, out);
324 }
325 }
326 for nested in sub.sub_components.iter() {
327 collect_row_instances(nested, out);
328 }
329}
330
331fn sub_component_idx_at_path(
334 cu: &i_slint_compiler::llr::CompilationUnit,
335 root_idx: SubComponentIdx,
336 path: &[SubComponentInstanceIdx],
337) -> SubComponentIdx {
338 let mut current = root_idx;
339 for &instance_idx in path {
340 let nested = &cu.sub_components[current].sub_components[instance_idx];
341 current = nested.ty;
342 }
343 current
344}
345
346fn is_injected_wrapper_element(instance: &VRc<ItemTreeVTable, Instance>, flat_idx: usize) -> bool {
349 let cu = &instance.root_sub_component.compilation_unit;
350 let root_ty = instance.root_sub_component.sub_component_idx;
351 let Some(Some((path, local_idx))) = instance.item_table.get(flat_idx) else {
352 return false;
353 };
354 let sc_idx = sub_component_idx_at_path(cu, root_ty, path);
355 cu.sub_components[sc_idx]
356 .debug_info
357 .as_ref()
358 .and_then(|debug| debug.items.get(*local_idx))
359 .is_some_and(|item_debug| item_debug.is_injected_wrapper_element)
360}
361
362fn item_flat_index_to_rect(
363 instance: &VRc<ItemTreeVTable, Instance>,
364 root: &VRc<ItemTreeVTable, Instance>,
365 flat_idx: usize,
366) -> Option<HighlightedRect> {
367 let vrc = VRc::into_dyn(instance.clone());
368 let root_vrc = VRc::into_dyn(root.clone());
369 let item_rc = ItemRc::new(vrc.clone(), flat_idx as u32);
370 let geometry = item_rc.geometry();
371 if geometry.size.is_empty() {
372 return None;
373 }
374 let mut transform_rotation = 0.;
379 let mut anchor = item_rc.clone();
380 while let Some(parent) =
381 anchor.parent_item(i_slint_core::item_tree::ParentItemTraversalMode::StopAtPopups)
382 {
383 if !VRc::ptr_eq(parent.item_tree(), &vrc) {
384 break; }
386 if !is_injected_wrapper_element(instance, parent.index() as usize) {
387 break;
388 }
389 if let Some(transform) = i_slint_core::items::ItemRef::downcast_pin::<
390 i_slint_core::items::Transform,
391 >(parent.borrow())
392 {
393 transform_rotation += transform.transform_rotation();
394 }
395 anchor = parent;
396 }
397
398 let to_root = item_rc.transform_to_item_tree(&root_vrc);
402 let parent_transform = anchor.transform_to_item_tree(&root_vrc);
403 let map_axis = |axis: euclid::Vector2D<f32, _>| to_root.transform_vector(axis).cast();
404
405 let origin: LogicalPoint = to_root.transform_point(geometry.origin.cast()).cast();
406 let size = geometry.size.cast::<f32>();
408 let x_axis = map_axis(euclid::vec2(size.width, 0.));
409 let y_axis = map_axis(euclid::vec2(0., size.height));
410 let width = x_axis.length();
411 let height = y_axis.length();
412 let center = origin + (x_axis + y_axis) / 2.0;
413 Some(HighlightedRect {
414 rect: LogicalRect {
415 origin: center - euclid::vec2(width / 2.0, height / 2.0),
416 size: euclid::size2(width, height),
417 },
418 angle: x_axis.y.atan2(x_axis.x).to_degrees(),
419 renders_as_rectangle: are_perpendicular(x_axis, y_axis),
420 local_rect: if anchor == item_rc { geometry } else { anchor.geometry() },
421 parent_transform,
422 transform_rotation,
423 corner_radii: item_corner_radii(item_rc.borrow()),
424 })
425}
426
427fn are_perpendicular(x: LogicalVector, y: LogicalVector) -> bool {
430 let squared_lengths = x.square_length() * y.square_length();
431 let dot = x.dot(y);
432 squared_lengths == 0. || dot * dot < 1.0e-6 * squared_lengths
433}
434
435fn positions_by_source(
436 root: &VRc<ItemTreeVTable, Instance>,
437 target_path: &Path,
438 target_offset: u32,
439 use_site: Option<(&Path, u32)>,
440 filter: ElementPositionFilter,
441) -> Vec<HighlightedRect> {
442 items_by_source(root, target_path, target_offset, use_site)
443 .into_iter()
444 .filter_map(|(instance, flat_idx)| {
445 if filter == ElementPositionFilter::ExcludeClipped {
446 let item = ItemRc::new(VRc::into_dyn(instance.clone()), flat_idx as u32);
447 if !item.is_visible() {
448 return None;
449 }
450 }
451 item_flat_index_to_rect(&instance, root, flat_idx)
452 })
453 .collect()
454}
455
456fn item_corner_radii(item: Pin<i_slint_core::items::ItemRef<'_>>) -> CornerRadii {
457 use i_slint_core::items::{BasicBorderRectangle, BorderRectangle, ItemRef};
458 if let Some(rect) = ItemRef::downcast_pin::<BorderRectangle>(item) {
459 CornerRadii {
460 top_left: rect.border_top_left_radius().get(),
461 top_right: rect.border_top_right_radius().get(),
462 bottom_left: rect.border_bottom_left_radius().get(),
463 bottom_right: rect.border_bottom_right_radius().get(),
464 }
465 } else if let Some(rect) = ItemRef::downcast_pin::<BasicBorderRectangle>(item) {
466 let radius = rect.border_radius().get();
467 CornerRadii {
468 top_left: radius,
469 top_right: radius,
470 bottom_left: radius,
471 bottom_right: radius,
472 }
473 } else {
474 CornerRadii::default()
475 }
476}
477
478fn items_by_source(
479 root: &VRc<ItemTreeVTable, Instance>,
480 target_path: &Path,
481 target_offset: u32,
482 use_site: Option<(&Path, u32)>,
483) -> Vec<(VRc<ItemTreeVTable, Instance>, usize)> {
484 let cu = root.root_sub_component.compilation_unit.clone();
485 let mut results = Vec::new();
486 for instance in all_instances(root) {
490 for sc_idx in 0..cu.sub_components.len() {
491 let sc_idx: SubComponentIdx = sc_idx.into();
492 let sc = &cu.sub_components[sc_idx];
493 let Some(debug) = sc.debug_info.as_ref() else { continue };
494 for (local_idx, item_dbg) in debug.items.iter_enumerated() {
495 let Some(source_file) = item_dbg.source_location.source_file.as_ref() else {
496 continue;
497 };
498 if source_file.path() != target_path {
499 continue;
500 }
501 if item_dbg.source_location.span.offset as u32 != target_offset {
502 continue;
503 }
504 for flat_idx in find_flat_indices_for_item(&instance, sc_idx, local_idx, use_site) {
505 results.push((instance.clone(), flat_idx));
506 }
507 }
508 }
509 }
510 results
511}
512
513#[cfg(test)]
514mod tests {
515 use crate::{
516 ComponentInstance,
517 debug_hook::tests::{compile_with_debug_hooks, test_path},
518 };
519
520 fn geometry_of(
521 instance: &ComponentInstance,
522 code: &str,
523 id: &str,
524 ) -> crate::highlight::HighlightedRect {
525 let id_position = code.find(id).unwrap_or_else(|| panic!("{id} not found"));
526 let offset = id_position + code[id_position..].find("Rectangle").unwrap();
527 let (element, _) = instance
528 .element_node_at_source_code_position(&test_path(), offset as u32)
529 .first()
530 .cloned()
531 .unwrap_or_else(|| panic!("element {id} not resolved"));
532 *instance.element_positions(&element).first().expect("geometry")
533 }
534
535 #[test]
542 fn debug_hooks_parent_origin() {
543 let code = r#"
544export component Win inherits Window {
545 width: 300px;
546 height: 200px;
547 plain := Rectangle {
548 x: 30px;
549 y: 40px;
550 width: 50px;
551 height: 60px;
552 }
553 faded := Rectangle {
554 // extra Opacity and visibility-Clip wrappers stacked around the Transform wrapper
555 opacity: 0.5;
556 visible: true;
557 x: 70px;
558 y: 80px;
559 width: 40px;
560 height: 30px;
561 }
562 outer := Rectangle {
563 x: 10px;
564 y: 20px;
565 width: 120px;
566 height: 100px;
567 nested := Rectangle {
568 x: 5px;
569 y: 7px;
570 width: 20px;
571 height: 20px;
572 }
573 }
574}"#;
575 let instance = compile_with_debug_hooks(code);
576
577 let check = |id: &str, expected: (f32, f32)| {
578 let geometry = geometry_of(&instance, code, id);
579 let x = geometry.rect.origin.x - geometry.parent_origin().x;
580 let y = geometry.rect.origin.y - geometry.parent_origin().y;
581 assert!(
582 (x - expected.0).abs() < 0.5 && (y - expected.1).abs() < 0.5,
583 "{id}: source-relative position ({x}, {y}) should be {expected:?}"
584 );
585 };
586
587 check("plain", (30.0, 40.0));
588 check("faded", (70.0, 80.0));
589 check("nested", (5.0, 7.0));
590 }
591
592 #[test]
593 fn debug_hooks_local_rect() {
594 let code = r#"
595export component Win inherits Window {
596 width: 300px;
597 height: 300px;
598 outer := Rectangle {
599 x: 50px;
600 y: 60px;
601 width: 160px;
602 height: 140px;
603 transform-rotation: 30deg;
604 inner := Rectangle {
605 x: 20px;
606 y: 25px;
607 width: 40px;
608 height: 30px;
609 transform-rotation: 15deg;
610 transform-origin: { x: 0px, y: 0px };
611 }
612 }
613}"#;
614 let instance = compile_with_debug_hooks(code);
615
616 let check = |id: &str, expected: (f32, f32, f32, f32)| {
617 let rect = geometry_of(&instance, code, id).local_rect;
618 let actual = (rect.origin.x, rect.origin.y, rect.width(), rect.height());
619 assert!(
620 (actual.0 - expected.0).abs() < 0.5
621 && (actual.1 - expected.1).abs() < 0.5
622 && (actual.2 - expected.2).abs() < 0.5
623 && (actual.3 - expected.3).abs() < 0.5,
624 "{id}: parent-relative rectangle {actual:?} should be {expected:?}"
625 );
626 };
627
628 check("outer", (50.0, 60.0, 160.0, 140.0));
629 check("inner", (20.0, 25.0, 40.0, 30.0));
630 }
631
632 #[test]
633 fn scaled_geometry_is_measured_per_axis() {
634 let code = r#"
635export component Win inherits Window {
636 width: 400px;
637 height: 400px;
638 stretched := Rectangle {
639 x: 20px;
640 y: 20px;
641 width: 80px;
642 height: 40px;
643 transform-scale-x: 3;
644 }
645 wide := Rectangle {
646 x: 20px;
647 y: 200px;
648 width: 200px;
649 height: 100px;
650 transform-scale-x: 3;
651 sheared := Rectangle {
652 width: 80px;
653 height: 40px;
654 transform-rotation: 30deg;
655 }
656 }
657}"#;
658 let instance = compile_with_debug_hooks(code);
659
660 let stretched = geometry_of(&instance, code, "stretched");
662 assert!(
663 (stretched.rect.width() - 240.).abs() < 0.5
664 && (stretched.rect.height() - 40.).abs() < 0.5,
665 "stretched: {:?}",
666 stretched.rect
667 );
668 assert!(stretched.renders_as_rectangle);
669
670 let sheared = geometry_of(&instance, code, "sheared");
672 assert!(!sheared.renders_as_rectangle);
673 assert!((sheared.angle - 30.).abs() > 1., "sheared angle: {}", sheared.angle);
674 }
675
676 #[test]
677 fn debug_hooks_parent_rotation() {
678 let code = r#"
679export component Win inherits Window {
680 width: 300px;
681 height: 300px;
682 outer := Rectangle {
683 x: 50px;
684 y: 50px;
685 width: 160px;
686 height: 160px;
687 transform-rotation: 30deg;
688 inner := Rectangle {
689 x: 20px;
690 y: 20px;
691 width: 40px;
692 height: 40px;
693 transform-rotation: 15deg;
694 }
695 }
696}"#;
697 let instance = compile_with_debug_hooks(code);
698
699 let check = |id: &str, expected: f32| {
700 let geometry = geometry_of(&instance, code, id);
701 let rotation = geometry.angle - geometry.parent_rotation();
702 assert!(
703 (rotation - expected).abs() < 0.5,
704 "{id}: source-relative rotation {rotation} should be {expected}"
705 );
706 };
707
708 check("outer", 30.0);
709 check("inner", 15.0);
710 }
711 #[test]
712 fn evaluated_rotation_and_radii_share_geometry_instances() {
713 let code = r#"
714export component Win inherits Window {
715 width: 300px;
716 height: 300px;
717 outer := Rectangle {
718 width: 200px;
719 height: 200px;
720 transform-rotation: 30deg;
721 for angle in [382.25deg, -397.5deg]: Rectangle {
722 width: 40px;
723 height: 40px;
724 transform-rotation: angle;
725 border-top-left-radius: 1.5px;
726 border-top-right-radius: 2.5px;
727 border-bottom-left-radius: 3.5px;
728 border-bottom-right-radius: 4.5px;
729 }
730 }
731}"#;
732 let instance = compile_with_debug_hooks(code);
733 let offset = code
734 .find(
735 "Rectangle {
736 width",
737 )
738 .unwrap();
739 let (element, _) = instance
740 .element_node_at_source_code_position(&test_path(), offset as u32)
741 .first()
742 .cloned()
743 .unwrap();
744 let geometries = instance.element_positions(&element);
745 assert_eq!(geometries.len(), 2);
746 for (geometry, expected) in geometries.iter().zip([382.25, -397.5]) {
747 assert_eq!(geometry.transform_rotation, expected);
748 assert!((geometry.parent_rotation() - 30.).abs() < 0.001);
749 let radii = geometry.corner_radii;
750 assert_eq!(
751 [radii.top_left, radii.top_right, radii.bottom_left, radii.bottom_right],
752 [1.5, 2.5, 3.5, 4.5]
753 );
754 }
755 }
756}