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