1use std::{mem::size_of, ops::Range, rc::Rc};
2
3use cranpose_core::{NodeId, collections::map::HashSet};
4use cranpose_foundation::PointerEvent;
5use cranpose_ui::{
6 GraphicsLayer, Point, Rect, RenderEffect, RoundedCornerShape, TextLayoutOptions, TextStyle,
7 text::AnnotatedString,
8};
9use cranpose_ui_graphics::{
10 BlendMode, ColorFilter, CommandRecording, DrawPrimitive, PointerIcon, RecordingSummary,
11 ShadowPrimitive,
12};
13
14use crate::{raster_cache::LayerRasterCacheHashes, style_shared::DrawPlacement};
15
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct ProjectiveTransform {
18 matrix: [[f32; 3]; 3],
19}
20
21impl ProjectiveTransform {
22 pub const fn identity() -> Self {
23 Self {
24 matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
25 }
26 }
27
28 pub fn translation(tx: f32, ty: f32) -> Self {
29 Self {
30 matrix: [[1.0, 0.0, tx], [0.0, 1.0, ty], [0.0, 0.0, 1.0]],
31 }
32 }
33
34 pub fn uniform_scale(scale: f32) -> Self {
37 Self {
38 matrix: [[scale, 0.0, 0.0], [0.0, scale, 0.0], [0.0, 0.0, 1.0]],
39 }
40 }
41
42 pub fn from_rect_to_quad(rect: Rect, quad: [[f32; 2]; 4]) -> Self {
43 if rect.width.abs() <= f32::EPSILON || rect.height.abs() <= f32::EPSILON {
44 return Self::translation(quad[0][0], quad[0][1]);
45 }
46
47 if let Some(axis_aligned) = axis_aligned_rect_from_quad(quad) {
48 let scale_x = axis_aligned.width / rect.width;
49 let scale_y = axis_aligned.height / rect.height;
50 return Self {
51 matrix: [
52 [scale_x, 0.0, axis_aligned.x - rect.x * scale_x],
53 [0.0, scale_y, axis_aligned.y - rect.y * scale_y],
54 [0.0, 0.0, 1.0],
55 ],
56 };
57 }
58
59 let source = [
60 [rect.x, rect.y],
61 [rect.x + rect.width, rect.y],
62 [rect.x, rect.y + rect.height],
63 [rect.x + rect.width, rect.y + rect.height],
64 ];
65 let Some(coefficients) = solve_homography(source, quad) else {
66 return Self::identity();
67 };
68
69 Self {
70 matrix: [
71 [coefficients[0], coefficients[1], coefficients[2]],
72 [coefficients[3], coefficients[4], coefficients[5]],
73 [coefficients[6], coefficients[7], 1.0],
74 ],
75 }
76 }
77
78 pub fn then(self, next: Self) -> Self {
80 Self {
81 matrix: multiply_matrices(next.matrix, self.matrix),
82 }
83 }
84
85 pub fn inverse(self) -> Option<Self> {
86 let m = self.matrix;
87 let a = m[0][0];
88 let b = m[0][1];
89 let c = m[0][2];
90 let d = m[1][0];
91 let e = m[1][1];
92 let f = m[1][2];
93 let g = m[2][0];
94 let h = m[2][1];
95 let i = m[2][2];
96
97 let cofactor00 = e * i - f * h;
98 let cofactor01 = -(d * i - f * g);
99 let cofactor02 = d * h - e * g;
100 let cofactor10 = -(b * i - c * h);
101 let cofactor11 = a * i - c * g;
102 let cofactor12 = -(a * h - b * g);
103 let cofactor20 = b * f - c * e;
104 let cofactor21 = -(a * f - c * d);
105 let cofactor22 = a * e - b * d;
106
107 let determinant = a * cofactor00 + b * cofactor01 + c * cofactor02;
108 if determinant.abs() <= f32::EPSILON {
109 return None;
110 }
111 let inverse_determinant = 1.0 / determinant;
112
113 Some(Self {
114 matrix: [
115 [
116 cofactor00 * inverse_determinant,
117 cofactor10 * inverse_determinant,
118 cofactor20 * inverse_determinant,
119 ],
120 [
121 cofactor01 * inverse_determinant,
122 cofactor11 * inverse_determinant,
123 cofactor21 * inverse_determinant,
124 ],
125 [
126 cofactor02 * inverse_determinant,
127 cofactor12 * inverse_determinant,
128 cofactor22 * inverse_determinant,
129 ],
130 ],
131 })
132 }
133
134 pub fn matrix(self) -> [[f32; 3]; 3] {
135 self.matrix
136 }
137
138 pub fn map_point(self, point: Point) -> Point {
139 let x = point.x;
140 let y = point.y;
141 let w = self.matrix[2][0] * x + self.matrix[2][1] * y + self.matrix[2][2];
142 let safe_w = if w.abs() <= f32::EPSILON { 1.0 } else { w };
143
144 Point {
145 x: (self.matrix[0][0] * x + self.matrix[0][1] * y + self.matrix[0][2]) / safe_w,
146 y: (self.matrix[1][0] * x + self.matrix[1][1] * y + self.matrix[1][2]) / safe_w,
147 }
148 }
149
150 pub fn map_rect(self, rect: Rect) -> [[f32; 2]; 4] {
151 [
152 self.map_point(Point {
153 x: rect.x,
154 y: rect.y,
155 }),
156 self.map_point(Point {
157 x: rect.x + rect.width,
158 y: rect.y,
159 }),
160 self.map_point(Point {
161 x: rect.x,
162 y: rect.y + rect.height,
163 }),
164 self.map_point(Point {
165 x: rect.x + rect.width,
166 y: rect.y + rect.height,
167 }),
168 ]
169 .map(|point| [point.x, point.y])
170 }
171
172 pub fn bounds_for_rect(self, rect: Rect) -> Rect {
173 quad_bounds(self.map_rect(rect))
174 }
175}
176
177fn axis_aligned_rect_from_quad(quad: [[f32; 2]; 4]) -> Option<Rect> {
178 let top_left = quad[0];
179 let top_right = quad[1];
180 let bottom_left = quad[2];
181 let bottom_right = quad[3];
182 let x_epsilon = 1e-4;
183 let y_epsilon = 1e-4;
184
185 if (top_left[1] - top_right[1]).abs() > y_epsilon
186 || (bottom_left[1] - bottom_right[1]).abs() > y_epsilon
187 || (top_left[0] - bottom_left[0]).abs() > x_epsilon
188 || (top_right[0] - bottom_right[0]).abs() > x_epsilon
189 {
190 return None;
191 }
192
193 Some(Rect {
194 x: top_left[0],
195 y: top_left[1],
196 width: top_right[0] - top_left[0],
197 height: bottom_left[1] - top_left[1],
198 })
199}
200
201impl Default for ProjectiveTransform {
202 fn default() -> Self {
203 Self::identity()
204 }
205}
206
207#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
208pub struct IsolationReasons {
209 pub explicit_offscreen: bool,
210 pub shape_clip: bool,
211 pub effect: bool,
212 pub backdrop: bool,
213 pub group_opacity: bool,
214 pub blend_mode: bool,
215}
216
217impl IsolationReasons {
218 pub fn has_any(self) -> bool {
219 self.explicit_offscreen
220 || self.shape_clip
221 || self.effect
222 || self.backdrop
223 || self.group_opacity
224 || self.blend_mode
225 }
226}
227
228#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
229pub enum CachePolicy {
230 #[default]
231 None,
232 Auto,
233}
234
235#[derive(Clone)]
236pub struct HitTestNode {
237 pub shape: Option<RoundedCornerShape>,
238 pub click_actions: Vec<Rc<dyn Fn(Point)>>,
239 pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
240 pub pointer_icon: Option<PointerIcon>,
244 pub clip: Option<Rect>,
245}
246
247#[derive(Clone, Debug, PartialEq)]
248pub struct DrawPrimitiveNode {
249 pub primitive: DrawPrimitive,
250 pub clip: Option<Rect>,
251}
252
253#[derive(Clone, Debug, PartialEq)]
254pub struct TextPrimitiveNode {
255 pub node_id: NodeId,
256 pub rect: Rect,
257 pub text: Rc<AnnotatedString>,
260 pub text_style: TextStyle,
261 pub font_size: f32,
262 pub layout_options: TextLayoutOptions,
263 pub clip: Option<Rect>,
264}
265
266#[derive(Clone, Copy, Debug, PartialEq, Eq)]
267pub enum PrimitivePhase {
268 BeforeChildren,
269 AfterChildren,
270}
271
272#[derive(Clone, Debug, PartialEq)]
273pub enum PrimitiveNode {
274 Draw(DrawPrimitiveNode),
275 Text(Box<TextPrimitiveNode>),
276}
277
278#[derive(Clone, Debug, PartialEq)]
279pub struct PrimitiveEntry {
280 pub phase: PrimitivePhase,
281 pub node: PrimitiveNode,
282}
283
284#[derive(Clone)]
285pub struct LayerNode {
286 pub node_id: Option<NodeId>,
287 pub wraps: Option<NodeId>,
292 pub local_bounds: Rect,
293 pub transform_to_parent: ProjectiveTransform,
294 pub content_offset: Point,
295 pub motion_context_animated: bool,
296 pub translated_content_context: bool,
297 pub translated_content_offset: Point,
298 pub scene_children_origin: Point,
305 pub scene_children_layer_translation: Point,
306 pub graphics_layer: GraphicsLayer,
307 pub clip_to_bounds: bool,
308 pub shadow_clip: Option<Rect>,
309 pub hit_test: Option<HitTestNode>,
310 pub has_hit_targets: bool,
311 pub has_origin_sinks: bool,
316 pub isolation: IsolationReasons,
317 pub cache_policy: CachePolicy,
318 pub cache_hashes: LayerRasterCacheHashes,
319 pub cache_hashes_valid: bool,
320 pub children: Vec<RenderNode>,
321}
322
323impl Default for LayerNode {
324 fn default() -> Self {
325 Self {
326 node_id: None,
327 wraps: None,
328 local_bounds: Rect {
329 x: 0.0,
330 y: 0.0,
331 width: 0.0,
332 height: 0.0,
333 },
334 transform_to_parent: ProjectiveTransform::identity(),
335 content_offset: Point::default(),
336 motion_context_animated: false,
337 translated_content_context: false,
338 translated_content_offset: Point::default(),
339 scene_children_origin: Point::default(),
340 scene_children_layer_translation: Point::default(),
341 graphics_layer: GraphicsLayer::default(),
342 clip_to_bounds: false,
343 shadow_clip: None,
344 hit_test: None,
345 has_hit_targets: false,
346 has_origin_sinks: false,
347 isolation: IsolationReasons::default(),
348 cache_policy: CachePolicy::None,
349 cache_hashes: LayerRasterCacheHashes::default(),
350 cache_hashes_valid: false,
351 children: Vec::new(),
352 }
353 }
354}
355
356impl LayerNode {
357 pub fn clip_rect(&self) -> Option<Rect> {
358 (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
359 }
360
361 pub fn effect(&self) -> Option<&RenderEffect> {
362 self.graphics_layer.render_effect.as_ref()
363 }
364
365 pub fn backdrop(&self) -> Option<&RenderEffect> {
366 self.graphics_layer.backdrop_effect.as_ref()
367 }
368
369 pub fn opacity(&self) -> f32 {
370 self.graphics_layer.alpha
371 }
372
373 pub fn blend_mode(&self) -> BlendMode {
374 self.graphics_layer.blend_mode
375 }
376
377 pub fn color_filter(&self) -> Option<ColorFilter> {
378 self.graphics_layer.color_filter
379 }
380
381 pub fn target_content_hash(&self) -> u64 {
382 if self.cache_hashes_valid {
383 self.cache_hashes.target_content
384 } else {
385 crate::graph_hash::layer_raster_cache_hashes(self).target_content
386 }
387 }
388
389 pub fn motion_source_content_hash(&self) -> u64 {
390 crate::graph_hash::layer_motion_source_content_hash(self)
391 }
392
393 pub fn effect_hash(&self) -> u64 {
394 if self.cache_hashes_valid {
395 self.cache_hashes.effect
396 } else {
397 crate::graph_hash::layer_raster_cache_hashes(self).effect
398 }
399 }
400
401 pub fn recompute_raster_cache_hashes(&mut self) {
402 crate::graph_hash::recompute_layer_raster_cache_hashes(self);
403 }
404}
405
406#[derive(Clone)]
407pub enum RenderNode {
408 Primitive(PrimitiveEntry),
409 DrawRun(DrawRunNode),
416 Layer(Box<LayerNode>),
417}
418
419#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
428pub struct DrawCommandId {
429 pub node_id: NodeId,
430 pub command_index: u32,
431 pub placement: DrawPlacement,
432}
433
434#[derive(Clone, Debug, PartialEq)]
435pub struct DrawRunNode {
436 pub phase: PrimitivePhase,
437 pub command: Option<DrawCommandId>,
440 pub recording: Rc<CommandRecording>,
446 pub segments: Range<u32>,
449 pub summary: DrawRunSummary,
452}
453
454pub type DrawRunSummary = RecordingSummary;
456
457impl DrawRunNode {
458 pub fn new(phase: PrimitivePhase, primitives: Vec<DrawPrimitive>) -> Self {
459 Self::for_command(phase, None, primitives)
460 }
461
462 pub fn for_command(
463 phase: PrimitivePhase,
464 command: Option<DrawCommandId>,
465 primitives: Vec<DrawPrimitive>,
466 ) -> Self {
467 let recording = CommandRecording::from_primitives(primitives);
468 let segments = recording.all_segments();
469 Self::for_command_shared(phase, command, Rc::new(recording), segments)
470 }
471
472 pub fn for_command_shared(
473 phase: PrimitivePhase,
474 command: Option<DrawCommandId>,
475 recording: Rc<CommandRecording>,
476 segments: Range<u32>,
477 ) -> Self {
478 let summary = recording.summary_in(&segments);
479 Self {
480 phase,
481 command,
482 recording,
483 segments,
484 summary,
485 }
486 }
487
488 pub fn primitives(&self) -> impl Iterator<Item = DrawPrimitive> + '_ {
490 self.recording.primitives(self.segments.clone())
491 }
492
493 pub fn coverage_rects(&self) -> impl Iterator<Item = Rect> + '_ {
495 self.recording.coverage_rects(self.segments.clone())
496 }
497
498 pub fn len(&self) -> usize {
499 self.recording.len_in(&self.segments)
500 }
501
502 pub fn is_empty(&self) -> bool {
503 self.recording.is_empty_in(&self.segments)
504 }
505}
506
507#[derive(Clone)]
508pub struct RenderGraph {
509 pub root: LayerNode,
510}
511
512impl RenderGraph {
513 pub fn new(mut root: LayerNode) -> Self {
514 root.recompute_raster_cache_hashes();
515 Self { root }
516 }
517
518 pub fn node_count(&self) -> usize {
519 fn count_layer(layer: &LayerNode) -> usize {
520 1 + layer
521 .children
522 .iter()
523 .map(|child| match child {
524 RenderNode::Primitive(_) => 1,
525 RenderNode::DrawRun(run) => run.len(),
526 RenderNode::Layer(child_layer) => count_layer(child_layer),
527 })
528 .sum::<usize>()
529 }
530
531 count_layer(&self.root)
532 }
533
534 pub fn heap_bytes(&self) -> usize {
535 layer_heap_bytes(&self.root)
536 }
537
538 pub fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) {
540 fn collect(layer: &LayerNode, nodes: &mut HashSet<NodeId>) {
541 if let Some(node_id) = layer.node_id {
542 nodes.insert(node_id);
543 }
544 for child in &layer.children {
545 match child {
546 RenderNode::DrawRun(run) => {
547 if let Some(command) = run.command {
548 nodes.insert(command.node_id);
549 }
550 }
551 RenderNode::Layer(child) => collect(child, nodes),
552 RenderNode::Primitive(_) => {}
553 }
554 }
555 }
556
557 nodes.clear();
558 collect(&self.root, nodes);
559 }
560}
561
562fn layer_heap_bytes(layer: &LayerNode) -> usize {
563 layer.hit_test.as_ref().map_or(0, hit_test_heap_bytes)
564 + size_of::<RenderNode>() * layer.children.capacity()
565 + layer
566 .children
567 .iter()
568 .map(render_node_heap_bytes)
569 .sum::<usize>()
570}
571
572fn render_node_heap_bytes(node: &RenderNode) -> usize {
573 match node {
574 RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
575 RenderNode::DrawRun(run) => {
576 run.recording.pod_heap_bytes()
577 + std::mem::size_of_val(run.recording.others())
578 + run
579 .recording
580 .others()
581 .iter()
582 .map(draw_primitive_heap_bytes)
583 .sum::<usize>()
584 }
585 RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
586 }
587}
588
589fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
590 match &entry.node {
591 PrimitiveNode::Draw(draw) => draw_primitive_heap_bytes(&draw.primitive),
592 PrimitiveNode::Text(text) => {
593 size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
594 }
595 }
596}
597
598fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
599 match primitive {
600 DrawPrimitive::Content
601 | DrawPrimitive::Rect { .. }
602 | DrawPrimitive::RoundRect { .. }
603 | DrawPrimitive::Arc { .. } => 0,
604 DrawPrimitive::Blend { primitive, .. } => {
605 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
606 }
607 DrawPrimitive::Image { .. } => 0,
608 DrawPrimitive::Text(text) => {
609 size_of::<cranpose_ui_graphics::TextPrimitive>()
610 + text.text.len()
611 + text
612 .style
613 .font_family
614 .as_ref()
615 .map_or(0, |family| family.capacity())
616 }
617 DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
618 }
619}
620
621fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
622 match shadow {
623 ShadowPrimitive::Drop { shape, .. } => {
624 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
625 }
626 ShadowPrimitive::Inner { fill, cutout, .. } => {
627 size_of::<DrawPrimitive>() * 2
628 + draw_primitive_heap_bytes(fill)
629 + draw_primitive_heap_bytes(cutout)
630 }
631 }
632}
633
634fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
635 text.text.capacity()
636 + text.span_styles.capacity() * size_of::<usize>() * 2
637 + text.paragraph_styles.capacity() * size_of::<usize>() * 2
638 + text.string_annotations.capacity() * size_of::<usize>() * 2
639 + text.link_annotations.capacity() * size_of::<usize>() * 2
640 + text
641 .string_annotations
642 .iter()
643 .map(|annotation| {
644 annotation.item.tag.capacity() + annotation.item.annotation.capacity()
645 })
646 .sum::<usize>()
647 + text
648 .link_annotations
649 .iter()
650 .map(|annotation| match &annotation.item {
651 cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
652 cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
653 })
654 .sum::<usize>()
655}
656
657fn hit_test_heap_bytes(hit_test: &HitTestNode) -> usize {
658 hit_test.click_actions.capacity() * size_of::<Rc<dyn Fn(Point)>>()
659 + hit_test.pointer_inputs.capacity() * size_of::<Rc<dyn Fn(PointerEvent)>>()
660}
661
662pub fn quad_bounds(quad: [[f32; 2]; 4]) -> Rect {
663 let mut min_x = f32::INFINITY;
664 let mut min_y = f32::INFINITY;
665 let mut max_x = f32::NEG_INFINITY;
666 let mut max_y = f32::NEG_INFINITY;
667
668 for [x, y] in quad {
669 min_x = min_x.min(x);
670 min_y = min_y.min(y);
671 max_x = max_x.max(x);
672 max_y = max_y.max(y);
673 }
674
675 Rect {
676 x: min_x,
677 y: min_y,
678 width: (max_x - min_x).max(0.0),
679 height: (max_y - min_y).max(0.0),
680 }
681}
682
683fn multiply_matrices(lhs: [[f32; 3]; 3], rhs: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
684 let mut out = [[0.0; 3]; 3];
685 for row in 0..3 {
686 for col in 0..3 {
687 out[row][col] =
688 lhs[row][0] * rhs[0][col] + lhs[row][1] * rhs[1][col] + lhs[row][2] * rhs[2][col];
689 }
690 }
691 out
692}
693
694fn solve_homography(source: [[f32; 2]; 4], target: [[f32; 2]; 4]) -> Option<[f32; 8]> {
695 let mut matrix = [[0.0f32; 9]; 8];
696 for (index, (src, dst)) in source.into_iter().zip(target).enumerate() {
697 let row = index * 2;
698 let x = src[0];
699 let y = src[1];
700 let u = dst[0];
701 let v = dst[1];
702
703 matrix[row] = [x, y, 1.0, 0.0, 0.0, 0.0, -u * x, -u * y, u];
704 matrix[row + 1] = [0.0, 0.0, 0.0, x, y, 1.0, -v * x, -v * y, v];
705 }
706
707 for pivot in 0..8 {
708 let mut pivot_row = pivot;
709 let mut pivot_value = matrix[pivot][pivot].abs();
710 let mut candidate = pivot + 1;
711 while candidate < 8 {
712 let candidate_value = matrix[candidate][pivot].abs();
713 if candidate_value > pivot_value {
714 pivot_row = candidate;
715 pivot_value = candidate_value;
716 }
717 candidate += 1;
718 }
719
720 if pivot_value <= f32::EPSILON {
721 return None;
722 }
723
724 if pivot_row != pivot {
725 matrix.swap(pivot, pivot_row);
726 }
727
728 let divisor = matrix[pivot][pivot];
729 let mut col = pivot;
730 while col < 9 {
731 matrix[pivot][col] /= divisor;
732 col += 1;
733 }
734
735 for row in 0..8 {
736 if row == pivot {
737 continue;
738 }
739 let factor = matrix[row][pivot];
740 if factor.abs() <= f32::EPSILON {
741 continue;
742 }
743 let mut col = pivot;
744 while col < 9 {
745 matrix[row][col] -= factor * matrix[pivot][col];
746 col += 1;
747 }
748 }
749 }
750
751 let mut solution = [0.0f32; 8];
752 for index in 0..8 {
753 solution[index] = matrix[index][8];
754 }
755 Some(solution)
756}
757
758#[cfg(test)]
759mod tests {
760 use cranpose_ui_graphics::{Brush, Color, DrawPrimitive};
761
762 use super::*;
763
764 fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
765 LayerNode {
766 local_bounds,
767 children,
768 ..Default::default()
769 }
770 }
771
772 #[test]
773 fn projective_transform_translation_maps_points() {
774 let transform = ProjectiveTransform::translation(7.0, -3.5);
775 let mapped = transform.map_point(Point { x: 2.0, y: 4.0 });
776 assert!((mapped.x - 9.0).abs() < 1e-6);
777 assert!((mapped.y - 0.5).abs() < 1e-6);
778 }
779
780 #[test]
781 fn projective_transform_then_composes_in_parent_order() {
782 let child = ProjectiveTransform::translation(4.0, 2.0);
783 let parent = ProjectiveTransform::translation(10.0, -1.0);
784 let composed = child.then(parent);
785 let mapped = composed.map_point(Point { x: 1.0, y: 1.0 });
786 assert!((mapped.x - 15.0).abs() < 1e-6);
787 assert!((mapped.y - 2.0).abs() < 1e-6);
788 }
789
790 #[test]
791 fn homography_maps_rect_corners_to_target_quad() {
792 let rect = Rect {
793 x: 0.0,
794 y: 0.0,
795 width: 20.0,
796 height: 10.0,
797 };
798 let quad = [[5.0, 7.0], [25.0, 6.0], [7.0, 20.0], [28.0, 21.0]];
799 let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
800 let mapped = transform.map_rect(rect);
801 for (expected, actual) in quad.into_iter().zip(mapped) {
802 assert!((expected[0] - actual[0]).abs() < 1e-4);
803 assert!((expected[1] - actual[1]).abs() < 1e-4);
804 }
805 }
806
807 #[test]
808 fn axis_aligned_rect_to_quad_keeps_exact_affine_matrix() {
809 let rect = Rect {
810 x: 2.0,
811 y: 3.0,
812 width: 20.0,
813 height: 10.0,
814 };
815 let quad = [[12.0, 9.0], [32.0, 9.0], [12.0, 19.0], [32.0, 19.0]];
816 let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
817
818 assert_eq!(
819 transform.matrix(),
820 [[1.0, 0.0, 10.0], [0.0, 1.0, 6.0], [0.0, 0.0, 1.0]]
821 );
822 }
823
824 #[test]
825 fn axis_aligned_rect_to_quad_keeps_exact_axis_aligned_scale() {
826 let rect = Rect {
827 x: 4.0,
828 y: 6.0,
829 width: 10.0,
830 height: 8.0,
831 };
832 let quad = [[20.0, 18.0], [50.0, 18.0], [20.0, 42.0], [50.0, 42.0]];
833 let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
834
835 assert_eq!(
836 transform.matrix(),
837 [[3.0, 0.0, 8.0], [0.0, 3.0, 0.0], [0.0, 0.0, 1.0]]
838 );
839 }
840
841 #[test]
842 fn retained_visual_observation_nodes_collect_layers_and_command_owners() {
843 let bounds = Rect {
844 x: 0.0,
845 y: 0.0,
846 width: 20.0,
847 height: 20.0,
848 };
849 let command = |node_id| DrawCommandId {
850 node_id,
851 command_index: 0,
852 placement: DrawPlacement::Behind,
853 };
854 let mut child = test_layer(
855 bounds,
856 vec![RenderNode::DrawRun(DrawRunNode::for_command(
857 PrimitivePhase::BeforeChildren,
858 Some(command(17)),
859 Vec::new(),
860 ))],
861 );
862 child.node_id = Some(13);
863 let mut root = test_layer(
864 bounds,
865 vec![
866 RenderNode::DrawRun(DrawRunNode::for_command(
867 PrimitivePhase::BeforeChildren,
868 Some(command(9)),
869 Vec::new(),
870 )),
871 RenderNode::DrawRun(DrawRunNode::new(PrimitivePhase::BeforeChildren, Vec::new())),
872 RenderNode::Layer(Box::new(child)),
873 ],
874 );
875
876 root.node_id = Some(5);
877
878 let mut graph = RenderGraph::new(root);
879 let mut nodes = HashSet::from([999]);
880 graph.collect_retained_visual_observation_nodes(&mut nodes);
881 assert_eq!(nodes, HashSet::from([5, 9, 13, 17]));
882 let capacity = nodes.capacity();
883 graph.root.children.clear();
884 graph.root.node_id = Some(23);
885 graph.collect_retained_visual_observation_nodes(&mut nodes);
886 assert_eq!(nodes, HashSet::from([23]));
887 assert_eq!(nodes.capacity(), capacity);
888 }
889
890 #[test]
891 fn render_graph_new_recomputes_manual_layer_hashes() {
892 let primitive = PrimitiveEntry {
893 phase: PrimitivePhase::BeforeChildren,
894 node: PrimitiveNode::Draw(DrawPrimitiveNode {
895 primitive: DrawPrimitive::Rect {
896 rect: Rect {
897 x: 1.0,
898 y: 2.0,
899 width: 8.0,
900 height: 6.0,
901 },
902 brush: Brush::solid(Color::WHITE),
903 stroke: None,
904 },
905 clip: None,
906 }),
907 };
908 let mut root = test_layer(
909 Rect {
910 x: 0.0,
911 y: 0.0,
912 width: 20.0,
913 height: 20.0,
914 },
915 vec![RenderNode::Primitive(primitive)],
916 );
917 root.graphics_layer.render_effect = Some(RenderEffect::blur(3.0));
918 let mut expected = root.clone();
919 expected.recompute_raster_cache_hashes();
920
921 let graph = RenderGraph::new(root);
922 assert_eq!(
923 graph.root.target_content_hash(),
924 expected.target_content_hash()
925 );
926 assert_eq!(graph.root.effect_hash(), expected.effect_hash());
927 }
928
929 #[test]
930 fn motion_source_content_hash_ignores_translated_content_offset() {
931 let primitive = PrimitiveEntry {
932 phase: PrimitivePhase::BeforeChildren,
933 node: PrimitiveNode::Draw(DrawPrimitiveNode {
934 primitive: DrawPrimitive::Rect {
935 rect: Rect {
936 x: 1.0,
937 y: 2.0,
938 width: 8.0,
939 height: 6.0,
940 },
941 brush: Brush::solid(Color::WHITE),
942 stroke: None,
943 },
944 clip: None,
945 }),
946 };
947 let mut base = test_layer(
948 Rect {
949 x: 0.0,
950 y: 0.0,
951 width: 20.0,
952 height: 20.0,
953 },
954 vec![RenderNode::Primitive(primitive)],
955 );
956 base.translated_content_context = true;
957 base.translated_content_offset = Point::new(0.0, -24.0);
958 base.recompute_raster_cache_hashes();
959
960 let mut moved = base.clone();
961 moved.translated_content_offset = Point::new(0.0, -72.0);
962 moved.recompute_raster_cache_hashes();
963
964 assert_ne!(base.target_content_hash(), moved.target_content_hash());
965 assert_eq!(
966 base.motion_source_content_hash(),
967 moved.motion_source_content_hash()
968 );
969 }
970}