1use 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#[derive(Clone, Copy, Debug, Default)]
23pub struct HighlightedRect {
24 pub rect: LogicalRect,
26 pub angle: f32,
28 pub renders_as_rectangle: bool,
36 pub local_rect: LogicalRect,
41 pub parent_transform: ItemTransform,
49 pub transform_rotation: f32,
51 pub corner_radii: CornerRadii,
53}
54#[derive(Clone, Copy, Debug, Default)]
56pub struct CornerRadii {
57 pub top_left: f32,
59 pub top_right: f32,
61 pub bottom_left: f32,
63 pub bottom_right: f32,
65}
66
67impl HighlightedRect {
68 pub fn parent_origin(&self) -> LogicalPoint {
70 self.parent_transform.transform_point(LogicalPoint::default().cast()).cast()
71 }
72
73 pub fn parent_rotation(&self) -> f32 {
79 self.parent_transform.m12.atan2(self.parent_transform.m11).to_degrees()
80 }
81
82 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#[derive(Clone, Debug)]
93pub struct ElementCandidate {
94 pub source_location: i_slint_compiler::diagnostics::SourceLocation,
96 pub geometry: HighlightedRect,
98 pub instance_index: usize,
100}
101
102pub(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
174type 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 item_indices: Vec<usize>,
208 priority: SourcePriority,
209 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 item_indices: HashMap<(*const Instance, usize), usize>,
243 elements: SourceElements,
244 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 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
452pub(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 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 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
521fn 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 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; }
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 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 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
659fn 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 #[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 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 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}