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