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::{LogicalPoint, LogicalRect};
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 parent_origin: LogicalPoint,
42 pub parent_rotation: f32,
47}
48impl HighlightedRect {
49 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#[derive(Copy, Clone, Eq, PartialEq)]
60pub enum ElementPositionFilter {
61 IncludeClipped,
63 ExcludeClipped,
65}
66
67pub fn element_positions(
72 instance: &VRc<ItemTreeVTable, Instance>,
73 element: &ElementRc,
74 filter: ElementPositionFilter,
75) -> Vec<HighlightedRect> {
76 let target = walk_to_native_root(element);
82 let Some(target_loc) = source_location_of(&target) else {
83 return Vec::new();
84 };
85 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
99fn 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
109fn 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
131pub(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
146pub(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 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 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
212fn 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
243fn 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
265fn 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
295fn 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
310fn 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 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; }
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 let parent_origin = anchor.map_to_item_tree(LogicalPoint::default(), &root_vrc);
360 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 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 #[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}