1use std::collections::HashSet;
2use std::mem::size_of;
3use std::rc::Rc;
4
5use cranpose_core::NodeId;
6use cranpose_foundation::PointerEvent;
7use cranpose_ui::text::AnnotatedString;
8use cranpose_ui::{
9 GraphicsLayer, Point, Rect, RenderEffect, RoundedCornerShape, TextLayoutOptions, TextStyle,
10};
11use cranpose_ui_graphics::{BlendMode, ColorFilter, DrawPrimitive, ShadowPrimitive};
12
13use crate::raster_cache::LayerRasterCacheHashes;
14use crate::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 clip: Option<Rect>,
241}
242
243#[derive(Clone, Debug, PartialEq)]
244pub struct DrawPrimitiveNode {
245 pub primitive: DrawPrimitive,
246 pub clip: Option<Rect>,
247}
248
249#[derive(Clone, Debug, PartialEq)]
250pub struct TextPrimitiveNode {
251 pub node_id: NodeId,
252 pub rect: Rect,
253 pub text: Rc<AnnotatedString>,
256 pub text_style: TextStyle,
257 pub font_size: f32,
258 pub layout_options: TextLayoutOptions,
259 pub clip: Option<Rect>,
260}
261
262#[derive(Clone, Copy, Debug, PartialEq, Eq)]
263pub enum PrimitivePhase {
264 BeforeChildren,
265 AfterChildren,
266}
267
268#[derive(Clone, Debug, PartialEq)]
269pub enum PrimitiveNode {
270 Draw(DrawPrimitiveNode),
271 Text(Box<TextPrimitiveNode>),
272}
273
274#[derive(Clone, Debug, PartialEq)]
275pub struct PrimitiveEntry {
276 pub phase: PrimitivePhase,
277 pub node: PrimitiveNode,
278}
279
280#[derive(Clone)]
281pub struct LayerNode {
282 pub node_id: Option<NodeId>,
283 pub local_bounds: Rect,
284 pub transform_to_parent: ProjectiveTransform,
285 pub content_offset: Point,
286 pub motion_context_animated: bool,
287 pub translated_content_context: bool,
288 pub translated_content_offset: Point,
289 pub scene_children_origin: Point,
296 pub scene_children_layer_translation: Point,
297 pub graphics_layer: GraphicsLayer,
298 pub clip_to_bounds: bool,
299 pub shadow_clip: Option<Rect>,
300 pub hit_test: Option<HitTestNode>,
301 pub has_hit_targets: bool,
302 pub isolation: IsolationReasons,
303 pub cache_policy: CachePolicy,
304 pub cache_hashes: LayerRasterCacheHashes,
305 pub cache_hashes_valid: bool,
306 pub children: Vec<RenderNode>,
307}
308
309impl LayerNode {
310 pub fn clip_rect(&self) -> Option<Rect> {
311 (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
312 }
313
314 pub fn effect(&self) -> Option<&RenderEffect> {
315 self.graphics_layer.render_effect.as_ref()
316 }
317
318 pub fn backdrop(&self) -> Option<&RenderEffect> {
319 self.graphics_layer.backdrop_effect.as_ref()
320 }
321
322 pub fn opacity(&self) -> f32 {
323 self.graphics_layer.alpha
324 }
325
326 pub fn blend_mode(&self) -> BlendMode {
327 self.graphics_layer.blend_mode
328 }
329
330 pub fn color_filter(&self) -> Option<ColorFilter> {
331 self.graphics_layer.color_filter
332 }
333
334 pub fn target_content_hash(&self) -> u64 {
335 if self.cache_hashes_valid {
336 self.cache_hashes.target_content
337 } else {
338 crate::graph_hash::layer_raster_cache_hashes(self).target_content
339 }
340 }
341
342 pub fn motion_source_content_hash(&self) -> u64 {
343 crate::graph_hash::layer_motion_source_content_hash(self)
344 }
345
346 pub fn effect_hash(&self) -> u64 {
347 if self.cache_hashes_valid {
348 self.cache_hashes.effect
349 } else {
350 crate::graph_hash::layer_raster_cache_hashes(self).effect
351 }
352 }
353
354 pub fn recompute_raster_cache_hashes(&mut self) {
355 crate::graph_hash::recompute_layer_raster_cache_hashes(self);
356 }
357}
358
359#[derive(Clone)]
360pub enum RenderNode {
361 Primitive(PrimitiveEntry),
362 DrawRun(DrawRunNode),
369 Layer(Box<LayerNode>),
370}
371
372#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
381pub struct DrawCommandId {
382 pub node_id: NodeId,
383 pub command_index: u32,
384 pub placement: DrawPlacement,
385}
386
387#[derive(Clone, Debug, PartialEq)]
388pub struct DrawRunNode {
389 pub phase: PrimitivePhase,
390 pub command: Option<DrawCommandId>,
393 pub primitives: std::rc::Rc<Vec<DrawPrimitive>>,
399 pub summary: DrawRunSummary,
405 pub replay: Option<Box<cranpose_ui_graphics::CommandReplayFrame>>,
412}
413
414impl DrawRunNode {
415 pub fn new(phase: PrimitivePhase, primitives: Vec<DrawPrimitive>) -> Self {
416 Self::for_command(phase, None, primitives)
417 }
418
419 pub fn for_command(
420 phase: PrimitivePhase,
421 command: Option<DrawCommandId>,
422 primitives: Vec<DrawPrimitive>,
423 ) -> Self {
424 Self::for_command_shared(phase, command, std::rc::Rc::new(primitives))
425 }
426
427 pub fn for_command_shared(
428 phase: PrimitivePhase,
429 command: Option<DrawCommandId>,
430 primitives: std::rc::Rc<Vec<DrawPrimitive>>,
431 ) -> Self {
432 Self::for_command_replayed(phase, command, primitives, None)
433 }
434
435 pub fn for_command_replayed(
436 phase: PrimitivePhase,
437 command: Option<DrawCommandId>,
438 primitives: std::rc::Rc<Vec<DrawPrimitive>>,
439 replay: Option<Box<cranpose_ui_graphics::CommandReplayFrame>>,
440 ) -> Self {
441 debug_assert!(
447 replay.as_ref().is_none_or(|frame| {
448 frame.fallback.is_some()
449 || !frame.spans.iter().any(|span| {
450 matches!(
451 span,
452 cranpose_ui_graphics::FrameSpan::Retained {
453 capture: false,
454 range,
455 ..
456 } if range.1 <= range.0
457 )
458 })
459 }),
460 "a replay frame with bypassed spans must own its fallback recording"
461 );
462 let mut summary = DrawRunSummary::scan(&primitives);
463 if replay.as_ref().is_some_and(|frame| {
466 frame
467 .spans
468 .iter()
469 .any(|span| matches!(span, cranpose_ui_graphics::FrameSpan::Retained { .. }))
470 }) {
471 summary.has_non_shadow = true;
472 }
473 Self {
474 phase,
475 command,
476 primitives,
477 summary,
478 replay,
479 }
480 }
481}
482
483#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
486pub struct DrawRunSummary {
487 pub has_text: bool,
490 pub has_shadow: bool,
491 pub has_non_shadow: bool,
493 pub has_pixel_sensitive: bool,
496}
497
498impl DrawRunSummary {
499 pub fn scan(primitives: &[DrawPrimitive]) -> Self {
500 fn unwrap_blend(mut primitive: &DrawPrimitive) -> &DrawPrimitive {
501 while let DrawPrimitive::Blend {
502 primitive: inner, ..
503 } = primitive
504 {
505 primitive = inner;
506 }
507 primitive
508 }
509 let mut summary = Self::default();
510 for primitive in primitives {
511 if matches!(primitive, DrawPrimitive::Shadow(_)) {
515 summary.has_shadow = true;
516 continue;
517 }
518 summary.has_non_shadow = true;
519 match unwrap_blend(primitive) {
520 DrawPrimitive::Text(_) => {
521 summary.has_text = true;
522 summary.has_pixel_sensitive = true;
523 }
524 DrawPrimitive::Image { .. } => summary.has_pixel_sensitive = true,
525 _ => {}
526 }
527 }
528 summary
529 }
530}
531
532#[derive(Clone)]
533pub struct RenderGraph {
534 pub root: LayerNode,
535}
536
537impl RenderGraph {
538 pub fn new(mut root: LayerNode) -> Self {
539 root.recompute_raster_cache_hashes();
540 Self { root }
541 }
542
543 pub fn node_count(&self) -> usize {
544 fn count_layer(layer: &LayerNode) -> usize {
545 1 + layer
546 .children
547 .iter()
548 .map(|child| match child {
549 RenderNode::Primitive(_) => 1,
550 RenderNode::DrawRun(run) => run.primitives.len(),
551 RenderNode::Layer(child_layer) => count_layer(child_layer),
552 })
553 .sum::<usize>()
554 }
555
556 count_layer(&self.root)
557 }
558
559 pub fn heap_bytes(&self) -> usize {
560 layer_heap_bytes(&self.root)
561 }
562
563 pub fn retained_visual_observation_nodes(&self) -> HashSet<NodeId> {
564 fn collect(layer: &LayerNode, nodes: &mut HashSet<NodeId>) {
565 if let Some(node_id) = layer.node_id {
566 nodes.insert(node_id);
567 }
568 for child in &layer.children {
569 match child {
570 RenderNode::DrawRun(run) => {
571 if let Some(command) = run.command {
572 nodes.insert(command.node_id);
573 }
574 }
575 RenderNode::Layer(child) => collect(child, nodes),
576 RenderNode::Primitive(_) => {}
577 }
578 }
579 }
580
581 let mut nodes = HashSet::new();
582 collect(&self.root, &mut nodes);
583 nodes
584 }
585}
586
587fn layer_heap_bytes(layer: &LayerNode) -> usize {
588 layer.hit_test.as_ref().map_or(0, hit_test_heap_bytes)
589 + size_of::<RenderNode>() * layer.children.capacity()
590 + layer
591 .children
592 .iter()
593 .map(render_node_heap_bytes)
594 .sum::<usize>()
595}
596
597fn render_node_heap_bytes(node: &RenderNode) -> usize {
598 match node {
599 RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
600 RenderNode::DrawRun(run) => {
601 size_of::<DrawPrimitive>() * run.primitives.capacity()
602 + run
603 .primitives
604 .iter()
605 .map(draw_primitive_heap_bytes)
606 .sum::<usize>()
607 }
608 RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
609 }
610}
611
612fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
613 match &entry.node {
614 PrimitiveNode::Draw(draw) => draw_primitive_heap_bytes(&draw.primitive),
615 PrimitiveNode::Text(text) => {
616 size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
617 }
618 }
619}
620
621fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
622 match primitive {
623 DrawPrimitive::Content
624 | DrawPrimitive::Rect { .. }
625 | DrawPrimitive::RoundRect { .. }
626 | DrawPrimitive::Arc { .. } => 0,
627 DrawPrimitive::Blend { primitive, .. } => {
628 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
629 }
630 DrawPrimitive::Image { .. } => 0,
631 DrawPrimitive::Text(text) => {
632 size_of::<cranpose_ui_graphics::TextPrimitive>()
633 + text.text.len()
634 + text
635 .style
636 .font_family
637 .as_ref()
638 .map_or(0, |family| family.capacity())
639 }
640 DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
641 }
642}
643
644fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
645 match shadow {
646 ShadowPrimitive::Drop { shape, .. } => {
647 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
648 }
649 ShadowPrimitive::Inner { fill, cutout, .. } => {
650 size_of::<DrawPrimitive>() * 2
651 + draw_primitive_heap_bytes(fill)
652 + draw_primitive_heap_bytes(cutout)
653 }
654 }
655}
656
657fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
658 text.text.capacity()
659 + text.span_styles.capacity() * size_of::<usize>() * 2
660 + text.paragraph_styles.capacity() * size_of::<usize>() * 2
661 + text.string_annotations.capacity() * size_of::<usize>() * 2
662 + text.link_annotations.capacity() * size_of::<usize>() * 2
663 + text
664 .string_annotations
665 .iter()
666 .map(|annotation| {
667 annotation.item.tag.capacity() + annotation.item.annotation.capacity()
668 })
669 .sum::<usize>()
670 + text
671 .link_annotations
672 .iter()
673 .map(|annotation| match &annotation.item {
674 cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
675 cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
676 })
677 .sum::<usize>()
678}
679
680fn hit_test_heap_bytes(hit_test: &HitTestNode) -> usize {
681 hit_test.click_actions.capacity() * size_of::<Rc<dyn Fn(Point)>>()
682 + hit_test.pointer_inputs.capacity() * size_of::<Rc<dyn Fn(PointerEvent)>>()
683}
684
685pub fn quad_bounds(quad: [[f32; 2]; 4]) -> Rect {
686 let mut min_x = f32::INFINITY;
687 let mut min_y = f32::INFINITY;
688 let mut max_x = f32::NEG_INFINITY;
689 let mut max_y = f32::NEG_INFINITY;
690
691 for [x, y] in quad {
692 min_x = min_x.min(x);
693 min_y = min_y.min(y);
694 max_x = max_x.max(x);
695 max_y = max_y.max(y);
696 }
697
698 Rect {
699 x: min_x,
700 y: min_y,
701 width: (max_x - min_x).max(0.0),
702 height: (max_y - min_y).max(0.0),
703 }
704}
705
706fn multiply_matrices(lhs: [[f32; 3]; 3], rhs: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
707 let mut out = [[0.0; 3]; 3];
708 for row in 0..3 {
709 for col in 0..3 {
710 out[row][col] =
711 lhs[row][0] * rhs[0][col] + lhs[row][1] * rhs[1][col] + lhs[row][2] * rhs[2][col];
712 }
713 }
714 out
715}
716
717fn solve_homography(source: [[f32; 2]; 4], target: [[f32; 2]; 4]) -> Option<[f32; 8]> {
718 let mut matrix = [[0.0f32; 9]; 8];
719 for (index, (src, dst)) in source.into_iter().zip(target).enumerate() {
720 let row = index * 2;
721 let x = src[0];
722 let y = src[1];
723 let u = dst[0];
724 let v = dst[1];
725
726 matrix[row] = [x, y, 1.0, 0.0, 0.0, 0.0, -u * x, -u * y, u];
727 matrix[row + 1] = [0.0, 0.0, 0.0, x, y, 1.0, -v * x, -v * y, v];
728 }
729
730 for pivot in 0..8 {
731 let mut pivot_row = pivot;
732 let mut pivot_value = matrix[pivot][pivot].abs();
733 let mut candidate = pivot + 1;
734 while candidate < 8 {
735 let candidate_value = matrix[candidate][pivot].abs();
736 if candidate_value > pivot_value {
737 pivot_row = candidate;
738 pivot_value = candidate_value;
739 }
740 candidate += 1;
741 }
742
743 if pivot_value <= f32::EPSILON {
744 return None;
745 }
746
747 if pivot_row != pivot {
748 matrix.swap(pivot, pivot_row);
749 }
750
751 let divisor = matrix[pivot][pivot];
752 let mut col = pivot;
753 while col < 9 {
754 matrix[pivot][col] /= divisor;
755 col += 1;
756 }
757
758 for row in 0..8 {
759 if row == pivot {
760 continue;
761 }
762 let factor = matrix[row][pivot];
763 if factor.abs() <= f32::EPSILON {
764 continue;
765 }
766 let mut col = pivot;
767 while col < 9 {
768 matrix[row][col] -= factor * matrix[pivot][col];
769 col += 1;
770 }
771 }
772 }
773
774 let mut solution = [0.0f32; 8];
775 for index in 0..8 {
776 solution[index] = matrix[index][8];
777 }
778 Some(solution)
779}
780
781#[cfg(test)]
782mod tests {
783 use super::*;
784 use crate::raster_cache::LayerRasterCacheHashes;
785 use cranpose_ui_graphics::{Brush, Color, DrawPrimitive};
786
787 fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
788 LayerNode {
789 node_id: None,
790 local_bounds,
791 transform_to_parent: ProjectiveTransform::identity(),
792 content_offset: Point::default(),
793 motion_context_animated: false,
794 translated_content_context: false,
795 translated_content_offset: Point::default(),
796 scene_children_origin: Point::default(),
797 scene_children_layer_translation: Point::default(),
798 graphics_layer: GraphicsLayer::default(),
799 clip_to_bounds: false,
800 shadow_clip: None,
801 hit_test: None,
802 has_hit_targets: false,
803 isolation: IsolationReasons::default(),
804 cache_policy: CachePolicy::None,
805 cache_hashes: LayerRasterCacheHashes::default(),
806 cache_hashes_valid: false,
807 children,
808 }
809 }
810
811 #[test]
812 fn projective_transform_translation_maps_points() {
813 let transform = ProjectiveTransform::translation(7.0, -3.5);
814 let mapped = transform.map_point(Point { x: 2.0, y: 4.0 });
815 assert!((mapped.x - 9.0).abs() < 1e-6);
816 assert!((mapped.y - 0.5).abs() < 1e-6);
817 }
818
819 #[test]
820 fn projective_transform_then_composes_in_parent_order() {
821 let child = ProjectiveTransform::translation(4.0, 2.0);
822 let parent = ProjectiveTransform::translation(10.0, -1.0);
823 let composed = child.then(parent);
824 let mapped = composed.map_point(Point { x: 1.0, y: 1.0 });
825 assert!((mapped.x - 15.0).abs() < 1e-6);
826 assert!((mapped.y - 2.0).abs() < 1e-6);
827 }
828
829 #[test]
830 fn homography_maps_rect_corners_to_target_quad() {
831 let rect = Rect {
832 x: 0.0,
833 y: 0.0,
834 width: 20.0,
835 height: 10.0,
836 };
837 let quad = [[5.0, 7.0], [25.0, 6.0], [7.0, 20.0], [28.0, 21.0]];
838 let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
839 let mapped = transform.map_rect(rect);
840 for (expected, actual) in quad.into_iter().zip(mapped) {
841 assert!((expected[0] - actual[0]).abs() < 1e-4);
842 assert!((expected[1] - actual[1]).abs() < 1e-4);
843 }
844 }
845
846 #[test]
847 fn axis_aligned_rect_to_quad_keeps_exact_affine_matrix() {
848 let rect = Rect {
849 x: 2.0,
850 y: 3.0,
851 width: 20.0,
852 height: 10.0,
853 };
854 let quad = [[12.0, 9.0], [32.0, 9.0], [12.0, 19.0], [32.0, 19.0]];
855 let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
856
857 assert_eq!(
858 transform.matrix(),
859 [[1.0, 0.0, 10.0], [0.0, 1.0, 6.0], [0.0, 0.0, 1.0]]
860 );
861 }
862
863 #[test]
864 fn axis_aligned_rect_to_quad_keeps_exact_axis_aligned_scale() {
865 let rect = Rect {
866 x: 4.0,
867 y: 6.0,
868 width: 10.0,
869 height: 8.0,
870 };
871 let quad = [[20.0, 18.0], [50.0, 18.0], [20.0, 42.0], [50.0, 42.0]];
872 let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
873
874 assert_eq!(
875 transform.matrix(),
876 [[3.0, 0.0, 8.0], [0.0, 3.0, 0.0], [0.0, 0.0, 1.0]]
877 );
878 }
879
880 #[test]
881 fn retained_visual_observation_nodes_collect_layers_and_command_owners() {
882 let bounds = Rect {
883 x: 0.0,
884 y: 0.0,
885 width: 20.0,
886 height: 20.0,
887 };
888 let command = |node_id| DrawCommandId {
889 node_id,
890 command_index: 0,
891 placement: DrawPlacement::Behind,
892 };
893 let mut child = test_layer(
894 bounds,
895 vec![RenderNode::DrawRun(DrawRunNode::for_command(
896 PrimitivePhase::BeforeChildren,
897 Some(command(17)),
898 Vec::new(),
899 ))],
900 );
901 child.node_id = Some(13);
902 let mut root = test_layer(
903 bounds,
904 vec![
905 RenderNode::DrawRun(DrawRunNode::for_command(
906 PrimitivePhase::BeforeChildren,
907 Some(command(9)),
908 Vec::new(),
909 )),
910 RenderNode::DrawRun(DrawRunNode::new(PrimitivePhase::BeforeChildren, Vec::new())),
911 RenderNode::Layer(Box::new(child)),
912 ],
913 );
914
915 root.node_id = Some(5);
916
917 assert_eq!(
918 RenderGraph::new(root).retained_visual_observation_nodes(),
919 HashSet::from([5, 9, 13, 17])
920 );
921 }
922
923 #[test]
924 fn render_graph_new_recomputes_manual_layer_hashes() {
925 let primitive = PrimitiveEntry {
926 phase: PrimitivePhase::BeforeChildren,
927 node: PrimitiveNode::Draw(DrawPrimitiveNode {
928 primitive: DrawPrimitive::Rect {
929 rect: Rect {
930 x: 1.0,
931 y: 2.0,
932 width: 8.0,
933 height: 6.0,
934 },
935 brush: Brush::solid(Color::WHITE),
936 stroke: None,
937 },
938 clip: None,
939 }),
940 };
941 let mut root = test_layer(
942 Rect {
943 x: 0.0,
944 y: 0.0,
945 width: 20.0,
946 height: 20.0,
947 },
948 vec![RenderNode::Primitive(primitive)],
949 );
950 root.graphics_layer.render_effect = Some(RenderEffect::blur(3.0));
951 let mut expected = root.clone();
952 expected.recompute_raster_cache_hashes();
953
954 let graph = RenderGraph::new(root);
955 assert_eq!(
956 graph.root.target_content_hash(),
957 expected.target_content_hash()
958 );
959 assert_eq!(graph.root.effect_hash(), expected.effect_hash());
960 }
961
962 #[test]
963 fn motion_source_content_hash_ignores_translated_content_offset() {
964 let primitive = PrimitiveEntry {
965 phase: PrimitivePhase::BeforeChildren,
966 node: PrimitiveNode::Draw(DrawPrimitiveNode {
967 primitive: DrawPrimitive::Rect {
968 rect: Rect {
969 x: 1.0,
970 y: 2.0,
971 width: 8.0,
972 height: 6.0,
973 },
974 brush: Brush::solid(Color::WHITE),
975 stroke: None,
976 },
977 clip: None,
978 }),
979 };
980 let mut base = test_layer(
981 Rect {
982 x: 0.0,
983 y: 0.0,
984 width: 20.0,
985 height: 20.0,
986 },
987 vec![RenderNode::Primitive(primitive)],
988 );
989 base.translated_content_context = true;
990 base.translated_content_offset = Point::new(0.0, -24.0);
991 base.recompute_raster_cache_hashes();
992
993 let mut moved = base.clone();
994 moved.translated_content_offset = Point::new(0.0, -72.0);
995 moved.recompute_raster_cache_hashes();
996
997 assert_ne!(base.target_content_hash(), moved.target_content_hash());
998 assert_eq!(
999 base.motion_source_content_hash(),
1000 moved.motion_source_content_hash()
1001 );
1002 }
1003}