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 repeater in sub.repeaters.iter() {
286 repeater.track_instance_changes();
287 for row in repeater.instances_vec() {
288 collect_instances(&row, out);
289 }
290 }
291 for nested in sub.sub_components.iter() {
292 collect_row_instances(nested, out);
293 }
294}
295
296fn sub_component_idx_at_path(
299 cu: &i_slint_compiler::llr::CompilationUnit,
300 root_idx: SubComponentIdx,
301 path: &[SubComponentInstanceIdx],
302) -> SubComponentIdx {
303 let mut current = root_idx;
304 for &instance_idx in path {
305 let nested = &cu.sub_components[current].sub_components[instance_idx];
306 current = nested.ty;
307 }
308 current
309}
310
311fn is_injected_wrapper_element(instance: &VRc<ItemTreeVTable, Instance>, flat_idx: usize) -> bool {
314 let cu = &instance.root_sub_component.compilation_unit;
315 let root_ty = instance.root_sub_component.sub_component_idx;
316 let Some(Some((path, local_idx))) = instance.item_table.get(flat_idx) else {
317 return false;
318 };
319 let sc_idx = sub_component_idx_at_path(cu, root_ty, path);
320 cu.sub_components[sc_idx]
321 .debug_info
322 .as_ref()
323 .and_then(|debug| debug.items.get(*local_idx))
324 .is_some_and(|item_debug| item_debug.is_injected_wrapper_element)
325}
326
327fn item_flat_index_to_rect(
328 instance: &VRc<ItemTreeVTable, Instance>,
329 root: &VRc<ItemTreeVTable, Instance>,
330 flat_idx: usize,
331) -> Option<HighlightedRect> {
332 let vrc = VRc::into_dyn(instance.clone());
333 let root_vrc = VRc::into_dyn(root.clone());
334 let item_rc = ItemRc::new(vrc.clone(), flat_idx as u32);
335 let geometry = item_rc.geometry();
336 if geometry.size.is_empty() {
337 return None;
338 }
339 let mut anchor = item_rc.clone();
344 while let Some(parent) =
345 anchor.parent_item(i_slint_core::item_tree::ParentItemTraversalMode::StopAtPopups)
346 {
347 if !VRc::ptr_eq(parent.item_tree(), &vrc) {
348 break; }
350 if !is_injected_wrapper_element(instance, parent.index() as usize) {
351 break;
352 }
353 anchor = parent;
354 }
355
356 let origin = item_rc.map_to_item_tree(geometry.origin, &root_vrc);
357 let parent_origin = anchor.map_to_item_tree(LogicalPoint::default(), &root_vrc);
361 let parent_rotation = {
365 let frame_x_axis = anchor.map_to_item_tree(LogicalPoint::new(1.0, 0.0), &root_vrc);
366 let delta = frame_x_axis - parent_origin;
367 delta.y.atan2(delta.x).to_degrees()
368 };
369 let top_right = item_rc
370 .map_to_item_tree(geometry.origin + euclid::vec2(geometry.size.width, 0.), &root_vrc);
371 let delta = top_right - origin;
372 let width = delta.length();
373 let height = if geometry.size.width == 0.0 {
374 0.0
375 } else {
376 geometry.size.height * width / geometry.size.width
377 };
378 let angle_rad = delta.y.atan2(delta.x);
379 let (sin, cos) = angle_rad.sin_cos();
380 let center = euclid::point2(
381 origin.x + (width / 2.0) * cos - (height / 2.0) * sin,
382 origin.y + (width / 2.0) * sin + (height / 2.0) * cos,
383 );
384 Some(HighlightedRect {
385 rect: LogicalRect {
386 origin: center - euclid::vec2(width / 2.0, height / 2.0),
387 size: euclid::size2(width, height),
388 },
389 angle: angle_rad.to_degrees(),
390 parent_origin,
391 parent_rotation,
392 })
393}
394
395fn positions_by_source(
396 root: &VRc<ItemTreeVTable, Instance>,
397 target_path: &Path,
398 target_offset: u32,
399 use_site: Option<(&Path, u32)>,
400 filter: ElementPositionFilter,
401) -> Vec<HighlightedRect> {
402 let cu = root.root_sub_component.compilation_unit.clone();
403 let mut results = Vec::new();
404 for instance in all_instances(root) {
408 for sc_idx in 0..cu.sub_components.len() {
409 let sc_idx: SubComponentIdx = sc_idx.into();
410 let sc = &cu.sub_components[sc_idx];
411 let Some(debug) = sc.debug_info.as_ref() else { continue };
412 for (local_idx, item_dbg) in debug.items.iter_enumerated() {
413 let Some(source_file) = item_dbg.source_location.source_file.as_ref() else {
414 continue;
415 };
416 if source_file.path() != target_path {
417 continue;
418 }
419 if item_dbg.source_location.span.offset as u32 != target_offset {
420 continue;
421 }
422 for flat_idx in find_flat_indices_for_item(&instance, sc_idx, local_idx, use_site) {
423 if filter == ElementPositionFilter::ExcludeClipped {
424 let dyn_rc = vtable::VRc::into_dyn(instance.clone());
425 let item_rc = i_slint_core::items::ItemRc::new(dyn_rc, flat_idx as u32);
426 if !item_rc.is_visible() {
427 continue;
428 }
429 }
430 if let Some(rect) = item_flat_index_to_rect(&instance, root, flat_idx) {
431 results.push(rect);
432 }
433 }
434 }
435 }
436 }
437 results
438}
439
440#[cfg(test)]
441mod tests {
442 use crate::{
443 ComponentInstance,
444 debug_hook::tests::{compile_with_debug_hooks, test_path},
445 };
446
447 fn geometry_of(
448 instance: &ComponentInstance,
449 code: &str,
450 id: &str,
451 ) -> crate::highlight::HighlightedRect {
452 let id_position = code.find(id).unwrap_or_else(|| panic!("{id} not found"));
453 let offset = id_position + code[id_position..].find("Rectangle").unwrap();
454 let (element, _) = instance
455 .element_node_at_source_code_position(&test_path(), offset as u32)
456 .first()
457 .cloned()
458 .unwrap_or_else(|| panic!("element {id} not resolved"));
459 *instance.element_positions(&element).first().expect("geometry")
460 }
461
462 #[test]
469 fn debug_hooks_parent_origin() {
470 let code = r#"
471export component Win inherits Window {
472 width: 300px;
473 height: 200px;
474 plain := Rectangle {
475 x: 30px;
476 y: 40px;
477 width: 50px;
478 height: 60px;
479 }
480 faded := Rectangle {
481 // extra Opacity and visibility-Clip wrappers stacked around the Transform wrapper
482 opacity: 0.5;
483 visible: true;
484 x: 70px;
485 y: 80px;
486 width: 40px;
487 height: 30px;
488 }
489 outer := Rectangle {
490 x: 10px;
491 y: 20px;
492 width: 120px;
493 height: 100px;
494 nested := Rectangle {
495 x: 5px;
496 y: 7px;
497 width: 20px;
498 height: 20px;
499 }
500 }
501}"#;
502 let instance = compile_with_debug_hooks(code);
503
504 let check = |id: &str, expected: (f32, f32)| {
505 let geometry = geometry_of(&instance, code, id);
506 let x = geometry.rect.origin.x - geometry.parent_origin.x;
507 let y = geometry.rect.origin.y - geometry.parent_origin.y;
508 assert!(
509 (x - expected.0).abs() < 0.5 && (y - expected.1).abs() < 0.5,
510 "{id}: source-relative position ({x}, {y}) should be {expected:?}"
511 );
512 };
513
514 check("plain", (30.0, 40.0));
515 check("faded", (70.0, 80.0));
516 check("nested", (5.0, 7.0));
517 }
518
519 #[test]
520 fn debug_hooks_parent_rotation() {
521 let code = r#"
522export component Win inherits Window {
523 width: 300px;
524 height: 300px;
525 outer := Rectangle {
526 x: 50px;
527 y: 50px;
528 width: 160px;
529 height: 160px;
530 transform-rotation: 30deg;
531 inner := Rectangle {
532 x: 20px;
533 y: 20px;
534 width: 40px;
535 height: 40px;
536 transform-rotation: 15deg;
537 }
538 }
539}"#;
540 let instance = compile_with_debug_hooks(code);
541
542 let check = |id: &str, expected: f32| {
543 let geometry = geometry_of(&instance, code, id);
544 let rotation = geometry.angle - geometry.parent_rotation;
545 assert!(
546 (rotation - expected).abs() < 0.5,
547 "{id}: source-relative rotation {rotation} should be {expected}"
548 );
549 };
550
551 check("outer", 30.0);
552 check("inner", 15.0);
553 }
554}