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;
13
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct ProjectiveTransform {
16 matrix: [[f32; 3]; 3],
17}
18
19impl ProjectiveTransform {
20 pub const fn identity() -> Self {
21 Self {
22 matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
23 }
24 }
25
26 pub fn translation(tx: f32, ty: f32) -> Self {
27 Self {
28 matrix: [[1.0, 0.0, tx], [0.0, 1.0, ty], [0.0, 0.0, 1.0]],
29 }
30 }
31
32 pub fn from_rect_to_quad(rect: Rect, quad: [[f32; 2]; 4]) -> Self {
33 if rect.width.abs() <= f32::EPSILON || rect.height.abs() <= f32::EPSILON {
34 return Self::translation(quad[0][0], quad[0][1]);
35 }
36
37 if let Some(axis_aligned) = axis_aligned_rect_from_quad(quad) {
38 let scale_x = axis_aligned.width / rect.width;
39 let scale_y = axis_aligned.height / rect.height;
40 return Self {
41 matrix: [
42 [scale_x, 0.0, axis_aligned.x - rect.x * scale_x],
43 [0.0, scale_y, axis_aligned.y - rect.y * scale_y],
44 [0.0, 0.0, 1.0],
45 ],
46 };
47 }
48
49 let source = [
50 [rect.x, rect.y],
51 [rect.x + rect.width, rect.y],
52 [rect.x, rect.y + rect.height],
53 [rect.x + rect.width, rect.y + rect.height],
54 ];
55 let Some(coefficients) = solve_homography(source, quad) else {
56 return Self::identity();
57 };
58
59 Self {
60 matrix: [
61 [coefficients[0], coefficients[1], coefficients[2]],
62 [coefficients[3], coefficients[4], coefficients[5]],
63 [coefficients[6], coefficients[7], 1.0],
64 ],
65 }
66 }
67
68 pub fn then(self, next: Self) -> Self {
70 Self {
71 matrix: multiply_matrices(next.matrix, self.matrix),
72 }
73 }
74
75 pub fn inverse(self) -> Option<Self> {
76 let m = self.matrix;
77 let a = m[0][0];
78 let b = m[0][1];
79 let c = m[0][2];
80 let d = m[1][0];
81 let e = m[1][1];
82 let f = m[1][2];
83 let g = m[2][0];
84 let h = m[2][1];
85 let i = m[2][2];
86
87 let cofactor00 = e * i - f * h;
88 let cofactor01 = -(d * i - f * g);
89 let cofactor02 = d * h - e * g;
90 let cofactor10 = -(b * i - c * h);
91 let cofactor11 = a * i - c * g;
92 let cofactor12 = -(a * h - b * g);
93 let cofactor20 = b * f - c * e;
94 let cofactor21 = -(a * f - c * d);
95 let cofactor22 = a * e - b * d;
96
97 let determinant = a * cofactor00 + b * cofactor01 + c * cofactor02;
98 if determinant.abs() <= f32::EPSILON {
99 return None;
100 }
101 let inverse_determinant = 1.0 / determinant;
102
103 Some(Self {
104 matrix: [
105 [
106 cofactor00 * inverse_determinant,
107 cofactor10 * inverse_determinant,
108 cofactor20 * inverse_determinant,
109 ],
110 [
111 cofactor01 * inverse_determinant,
112 cofactor11 * inverse_determinant,
113 cofactor21 * inverse_determinant,
114 ],
115 [
116 cofactor02 * inverse_determinant,
117 cofactor12 * inverse_determinant,
118 cofactor22 * inverse_determinant,
119 ],
120 ],
121 })
122 }
123
124 pub fn matrix(self) -> [[f32; 3]; 3] {
125 self.matrix
126 }
127
128 pub fn map_point(self, point: Point) -> Point {
129 let x = point.x;
130 let y = point.y;
131 let w = self.matrix[2][0] * x + self.matrix[2][1] * y + self.matrix[2][2];
132 let safe_w = if w.abs() <= f32::EPSILON { 1.0 } else { w };
133
134 Point {
135 x: (self.matrix[0][0] * x + self.matrix[0][1] * y + self.matrix[0][2]) / safe_w,
136 y: (self.matrix[1][0] * x + self.matrix[1][1] * y + self.matrix[1][2]) / safe_w,
137 }
138 }
139
140 pub fn map_rect(self, rect: Rect) -> [[f32; 2]; 4] {
141 [
142 self.map_point(Point {
143 x: rect.x,
144 y: rect.y,
145 }),
146 self.map_point(Point {
147 x: rect.x + rect.width,
148 y: rect.y,
149 }),
150 self.map_point(Point {
151 x: rect.x,
152 y: rect.y + rect.height,
153 }),
154 self.map_point(Point {
155 x: rect.x + rect.width,
156 y: rect.y + rect.height,
157 }),
158 ]
159 .map(|point| [point.x, point.y])
160 }
161
162 pub fn bounds_for_rect(self, rect: Rect) -> Rect {
163 quad_bounds(self.map_rect(rect))
164 }
165}
166
167fn axis_aligned_rect_from_quad(quad: [[f32; 2]; 4]) -> Option<Rect> {
168 let top_left = quad[0];
169 let top_right = quad[1];
170 let bottom_left = quad[2];
171 let bottom_right = quad[3];
172 let x_epsilon = 1e-4;
173 let y_epsilon = 1e-4;
174
175 if (top_left[1] - top_right[1]).abs() > y_epsilon
176 || (bottom_left[1] - bottom_right[1]).abs() > y_epsilon
177 || (top_left[0] - bottom_left[0]).abs() > x_epsilon
178 || (top_right[0] - bottom_right[0]).abs() > x_epsilon
179 {
180 return None;
181 }
182
183 Some(Rect {
184 x: top_left[0],
185 y: top_left[1],
186 width: top_right[0] - top_left[0],
187 height: bottom_left[1] - top_left[1],
188 })
189}
190
191impl Default for ProjectiveTransform {
192 fn default() -> Self {
193 Self::identity()
194 }
195}
196
197#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
198pub struct IsolationReasons {
199 pub explicit_offscreen: bool,
200 pub shape_clip: bool,
201 pub effect: bool,
202 pub backdrop: bool,
203 pub group_opacity: bool,
204 pub blend_mode: bool,
205}
206
207impl IsolationReasons {
208 pub fn has_any(self) -> bool {
209 self.explicit_offscreen
210 || self.shape_clip
211 || self.effect
212 || self.backdrop
213 || self.group_opacity
214 || self.blend_mode
215 }
216}
217
218#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
219pub enum CachePolicy {
220 #[default]
221 None,
222 Auto,
223}
224
225#[derive(Clone)]
226pub struct HitTestNode {
227 pub shape: Option<RoundedCornerShape>,
228 pub click_actions: Vec<Rc<dyn Fn(Point)>>,
229 pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
230 pub clip: Option<Rect>,
231}
232
233#[derive(Clone, Debug, PartialEq)]
234pub struct DrawPrimitiveNode {
235 pub primitive: DrawPrimitive,
236 pub clip: Option<Rect>,
237}
238
239#[derive(Clone, Debug, PartialEq)]
240pub struct TextPrimitiveNode {
241 pub node_id: NodeId,
242 pub rect: Rect,
243 pub text: AnnotatedString,
244 pub text_style: TextStyle,
245 pub font_size: f32,
246 pub layout_options: TextLayoutOptions,
247 pub clip: Option<Rect>,
248}
249
250#[derive(Clone, Copy, Debug, PartialEq, Eq)]
251pub enum PrimitivePhase {
252 BeforeChildren,
253 AfterChildren,
254}
255
256#[derive(Clone, Debug, PartialEq)]
257pub enum PrimitiveNode {
258 Draw(DrawPrimitiveNode),
259 Text(Box<TextPrimitiveNode>),
260}
261
262#[derive(Clone, Debug, PartialEq)]
263pub struct PrimitiveEntry {
264 pub phase: PrimitivePhase,
265 pub node: PrimitiveNode,
266}
267
268#[derive(Clone)]
269pub struct LayerNode {
270 pub node_id: Option<NodeId>,
271 pub local_bounds: Rect,
272 pub transform_to_parent: ProjectiveTransform,
273 pub content_offset: Point,
274 pub motion_context_animated: bool,
275 pub translated_content_context: bool,
276 pub translated_content_offset: Point,
277 pub graphics_layer: GraphicsLayer,
278 pub clip_to_bounds: bool,
279 pub shadow_clip: Option<Rect>,
280 pub hit_test: Option<HitTestNode>,
281 pub has_hit_targets: bool,
282 pub isolation: IsolationReasons,
283 pub cache_policy: CachePolicy,
284 pub cache_hashes: LayerRasterCacheHashes,
285 pub cache_hashes_valid: bool,
286 pub children: Vec<RenderNode>,
287}
288
289impl LayerNode {
290 pub fn clip_rect(&self) -> Option<Rect> {
291 (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
292 }
293
294 pub fn effect(&self) -> Option<&RenderEffect> {
295 self.graphics_layer.render_effect.as_ref()
296 }
297
298 pub fn backdrop(&self) -> Option<&RenderEffect> {
299 self.graphics_layer.backdrop_effect.as_ref()
300 }
301
302 pub fn opacity(&self) -> f32 {
303 self.graphics_layer.alpha
304 }
305
306 pub fn blend_mode(&self) -> BlendMode {
307 self.graphics_layer.blend_mode
308 }
309
310 pub fn color_filter(&self) -> Option<ColorFilter> {
311 self.graphics_layer.color_filter
312 }
313
314 pub fn target_content_hash(&self) -> u64 {
315 if self.cache_hashes_valid {
316 self.cache_hashes.target_content
317 } else {
318 crate::graph_hash::layer_raster_cache_hashes(self).target_content
319 }
320 }
321
322 pub fn motion_source_content_hash(&self) -> u64 {
323 crate::graph_hash::layer_motion_source_content_hash(self)
324 }
325
326 pub fn effect_hash(&self) -> u64 {
327 if self.cache_hashes_valid {
328 self.cache_hashes.effect
329 } else {
330 crate::graph_hash::layer_raster_cache_hashes(self).effect
331 }
332 }
333
334 pub fn recompute_raster_cache_hashes(&mut self) {
335 crate::graph_hash::recompute_layer_raster_cache_hashes(self);
336 }
337}
338
339#[derive(Clone)]
340pub enum RenderNode {
341 Primitive(PrimitiveEntry),
342 Layer(Box<LayerNode>),
343}
344
345#[derive(Clone)]
346pub struct RenderGraph {
347 pub root: LayerNode,
348}
349
350impl RenderGraph {
351 pub fn new(mut root: LayerNode) -> Self {
352 root.recompute_raster_cache_hashes();
353 Self { root }
354 }
355
356 pub fn node_count(&self) -> usize {
357 fn count_layer(layer: &LayerNode) -> usize {
358 1 + layer
359 .children
360 .iter()
361 .map(|child| match child {
362 RenderNode::Primitive(_) => 1,
363 RenderNode::Layer(child_layer) => count_layer(child_layer),
364 })
365 .sum::<usize>()
366 }
367
368 count_layer(&self.root)
369 }
370
371 pub fn heap_bytes(&self) -> usize {
372 layer_heap_bytes(&self.root)
373 }
374}
375
376fn layer_heap_bytes(layer: &LayerNode) -> usize {
377 layer.hit_test.as_ref().map_or(0, hit_test_heap_bytes)
378 + size_of::<RenderNode>() * layer.children.capacity()
379 + layer
380 .children
381 .iter()
382 .map(render_node_heap_bytes)
383 .sum::<usize>()
384}
385
386fn render_node_heap_bytes(node: &RenderNode) -> usize {
387 match node {
388 RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
389 RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
390 }
391}
392
393fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
394 match &entry.node {
395 PrimitiveNode::Draw(draw) => draw_primitive_heap_bytes(&draw.primitive),
396 PrimitiveNode::Text(text) => {
397 size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
398 }
399 }
400}
401
402fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
403 match primitive {
404 DrawPrimitive::Content | DrawPrimitive::Rect { .. } | DrawPrimitive::RoundRect { .. } => 0,
405 DrawPrimitive::Blend { primitive, .. } => {
406 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
407 }
408 DrawPrimitive::Image { .. } => 0,
409 DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
410 }
411}
412
413fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
414 match shadow {
415 ShadowPrimitive::Drop { shape, .. } => {
416 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
417 }
418 ShadowPrimitive::Inner { fill, cutout, .. } => {
419 size_of::<DrawPrimitive>() * 2
420 + draw_primitive_heap_bytes(fill)
421 + draw_primitive_heap_bytes(cutout)
422 }
423 }
424}
425
426fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
427 text.text.capacity()
428 + text.span_styles.capacity() * size_of::<usize>() * 2
429 + text.paragraph_styles.capacity() * size_of::<usize>() * 2
430 + text.string_annotations.capacity() * size_of::<usize>() * 2
431 + text.link_annotations.capacity() * size_of::<usize>() * 2
432 + text
433 .string_annotations
434 .iter()
435 .map(|annotation| {
436 annotation.item.tag.capacity() + annotation.item.annotation.capacity()
437 })
438 .sum::<usize>()
439 + text
440 .link_annotations
441 .iter()
442 .map(|annotation| match &annotation.item {
443 cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
444 cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
445 })
446 .sum::<usize>()
447}
448
449fn hit_test_heap_bytes(hit_test: &HitTestNode) -> usize {
450 hit_test.click_actions.capacity() * size_of::<Rc<dyn Fn(Point)>>()
451 + hit_test.pointer_inputs.capacity() * size_of::<Rc<dyn Fn(PointerEvent)>>()
452}
453
454pub fn quad_bounds(quad: [[f32; 2]; 4]) -> Rect {
455 let mut min_x = f32::INFINITY;
456 let mut min_y = f32::INFINITY;
457 let mut max_x = f32::NEG_INFINITY;
458 let mut max_y = f32::NEG_INFINITY;
459
460 for [x, y] in quad {
461 min_x = min_x.min(x);
462 min_y = min_y.min(y);
463 max_x = max_x.max(x);
464 max_y = max_y.max(y);
465 }
466
467 Rect {
468 x: min_x,
469 y: min_y,
470 width: (max_x - min_x).max(0.0),
471 height: (max_y - min_y).max(0.0),
472 }
473}
474
475fn multiply_matrices(lhs: [[f32; 3]; 3], rhs: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
476 let mut out = [[0.0; 3]; 3];
477 for row in 0..3 {
478 for col in 0..3 {
479 out[row][col] =
480 lhs[row][0] * rhs[0][col] + lhs[row][1] * rhs[1][col] + lhs[row][2] * rhs[2][col];
481 }
482 }
483 out
484}
485
486fn solve_homography(source: [[f32; 2]; 4], target: [[f32; 2]; 4]) -> Option<[f32; 8]> {
487 let mut matrix = [[0.0f32; 9]; 8];
488 for (index, (src, dst)) in source.into_iter().zip(target).enumerate() {
489 let row = index * 2;
490 let x = src[0];
491 let y = src[1];
492 let u = dst[0];
493 let v = dst[1];
494
495 matrix[row] = [x, y, 1.0, 0.0, 0.0, 0.0, -u * x, -u * y, u];
496 matrix[row + 1] = [0.0, 0.0, 0.0, x, y, 1.0, -v * x, -v * y, v];
497 }
498
499 for pivot in 0..8 {
500 let mut pivot_row = pivot;
501 let mut pivot_value = matrix[pivot][pivot].abs();
502 let mut candidate = pivot + 1;
503 while candidate < 8 {
504 let candidate_value = matrix[candidate][pivot].abs();
505 if candidate_value > pivot_value {
506 pivot_row = candidate;
507 pivot_value = candidate_value;
508 }
509 candidate += 1;
510 }
511
512 if pivot_value <= f32::EPSILON {
513 return None;
514 }
515
516 if pivot_row != pivot {
517 matrix.swap(pivot, pivot_row);
518 }
519
520 let divisor = matrix[pivot][pivot];
521 let mut col = pivot;
522 while col < 9 {
523 matrix[pivot][col] /= divisor;
524 col += 1;
525 }
526
527 for row in 0..8 {
528 if row == pivot {
529 continue;
530 }
531 let factor = matrix[row][pivot];
532 if factor.abs() <= f32::EPSILON {
533 continue;
534 }
535 let mut col = pivot;
536 while col < 9 {
537 matrix[row][col] -= factor * matrix[pivot][col];
538 col += 1;
539 }
540 }
541 }
542
543 let mut solution = [0.0f32; 8];
544 for index in 0..8 {
545 solution[index] = matrix[index][8];
546 }
547 Some(solution)
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use crate::raster_cache::LayerRasterCacheHashes;
554 use cranpose_ui_graphics::{Brush, Color, DrawPrimitive};
555
556 fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
557 LayerNode {
558 node_id: None,
559 local_bounds,
560 transform_to_parent: ProjectiveTransform::identity(),
561 content_offset: Point::default(),
562 motion_context_animated: false,
563 translated_content_context: false,
564 translated_content_offset: Point::default(),
565 graphics_layer: GraphicsLayer::default(),
566 clip_to_bounds: false,
567 shadow_clip: None,
568 hit_test: None,
569 has_hit_targets: false,
570 isolation: IsolationReasons::default(),
571 cache_policy: CachePolicy::None,
572 cache_hashes: LayerRasterCacheHashes::default(),
573 cache_hashes_valid: false,
574 children,
575 }
576 }
577
578 #[test]
579 fn projective_transform_translation_maps_points() {
580 let transform = ProjectiveTransform::translation(7.0, -3.5);
581 let mapped = transform.map_point(Point { x: 2.0, y: 4.0 });
582 assert!((mapped.x - 9.0).abs() < 1e-6);
583 assert!((mapped.y - 0.5).abs() < 1e-6);
584 }
585
586 #[test]
587 fn projective_transform_then_composes_in_parent_order() {
588 let child = ProjectiveTransform::translation(4.0, 2.0);
589 let parent = ProjectiveTransform::translation(10.0, -1.0);
590 let composed = child.then(parent);
591 let mapped = composed.map_point(Point { x: 1.0, y: 1.0 });
592 assert!((mapped.x - 15.0).abs() < 1e-6);
593 assert!((mapped.y - 2.0).abs() < 1e-6);
594 }
595
596 #[test]
597 fn homography_maps_rect_corners_to_target_quad() {
598 let rect = Rect {
599 x: 0.0,
600 y: 0.0,
601 width: 20.0,
602 height: 10.0,
603 };
604 let quad = [[5.0, 7.0], [25.0, 6.0], [7.0, 20.0], [28.0, 21.0]];
605 let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
606 let mapped = transform.map_rect(rect);
607 for (expected, actual) in quad.into_iter().zip(mapped) {
608 assert!((expected[0] - actual[0]).abs() < 1e-4);
609 assert!((expected[1] - actual[1]).abs() < 1e-4);
610 }
611 }
612
613 #[test]
614 fn axis_aligned_rect_to_quad_keeps_exact_affine_matrix() {
615 let rect = Rect {
616 x: 2.0,
617 y: 3.0,
618 width: 20.0,
619 height: 10.0,
620 };
621 let quad = [[12.0, 9.0], [32.0, 9.0], [12.0, 19.0], [32.0, 19.0]];
622 let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
623
624 assert_eq!(
625 transform.matrix(),
626 [[1.0, 0.0, 10.0], [0.0, 1.0, 6.0], [0.0, 0.0, 1.0]]
627 );
628 }
629
630 #[test]
631 fn axis_aligned_rect_to_quad_keeps_exact_axis_aligned_scale() {
632 let rect = Rect {
633 x: 4.0,
634 y: 6.0,
635 width: 10.0,
636 height: 8.0,
637 };
638 let quad = [[20.0, 18.0], [50.0, 18.0], [20.0, 42.0], [50.0, 42.0]];
639 let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
640
641 assert_eq!(
642 transform.matrix(),
643 [[3.0, 0.0, 8.0], [0.0, 3.0, 0.0], [0.0, 0.0, 1.0]]
644 );
645 }
646
647 #[test]
648 fn render_graph_new_recomputes_manual_layer_hashes() {
649 let primitive = PrimitiveEntry {
650 phase: PrimitivePhase::BeforeChildren,
651 node: PrimitiveNode::Draw(DrawPrimitiveNode {
652 primitive: DrawPrimitive::Rect {
653 rect: Rect {
654 x: 1.0,
655 y: 2.0,
656 width: 8.0,
657 height: 6.0,
658 },
659 brush: Brush::solid(Color::WHITE),
660 },
661 clip: None,
662 }),
663 };
664 let mut root = test_layer(
665 Rect {
666 x: 0.0,
667 y: 0.0,
668 width: 20.0,
669 height: 20.0,
670 },
671 vec![RenderNode::Primitive(primitive)],
672 );
673 root.graphics_layer.render_effect = Some(RenderEffect::blur(3.0));
674 let mut expected = root.clone();
675 expected.recompute_raster_cache_hashes();
676
677 let graph = RenderGraph::new(root);
678 assert_eq!(
679 graph.root.target_content_hash(),
680 expected.target_content_hash()
681 );
682 assert_eq!(graph.root.effect_hash(), expected.effect_hash());
683 }
684
685 #[test]
686 fn motion_source_content_hash_ignores_translated_content_offset() {
687 let primitive = PrimitiveEntry {
688 phase: PrimitivePhase::BeforeChildren,
689 node: PrimitiveNode::Draw(DrawPrimitiveNode {
690 primitive: DrawPrimitive::Rect {
691 rect: Rect {
692 x: 1.0,
693 y: 2.0,
694 width: 8.0,
695 height: 6.0,
696 },
697 brush: Brush::solid(Color::WHITE),
698 },
699 clip: None,
700 }),
701 };
702 let mut base = test_layer(
703 Rect {
704 x: 0.0,
705 y: 0.0,
706 width: 20.0,
707 height: 20.0,
708 },
709 vec![RenderNode::Primitive(primitive)],
710 );
711 base.translated_content_context = true;
712 base.translated_content_offset = Point::new(0.0, -24.0);
713 base.recompute_raster_cache_hashes();
714
715 let mut moved = base.clone();
716 moved.translated_content_offset = Point::new(0.0, -72.0);
717 moved.recompute_raster_cache_hashes();
718
719 assert_ne!(base.target_content_hash(), moved.target_content_hash());
720 assert_eq!(
721 base.motion_source_content_hash(),
722 moved.motion_source_content_hash()
723 );
724 }
725}