Skip to main content

cranpose_render_common/
graph.rs

1use std::{mem::size_of, ops::Range, rc::Rc};
2
3use cranpose_core::{NodeId, collections::map::HashSet};
4use cranpose_ui::{
5    GraphicsLayer, ModifierNodeSlices, Point, Rect, RenderEffect, RoundedCornerShape,
6    TextLayoutOptions, TextOverflow, TextStyle,
7    text::{AnnotatedString, RenderString},
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    /// Uniform scale about the origin (device-scale root transform: render
34    /// graphs stay in logical dp; density applies at execution).
35    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    /// The transform a homogeneous matrix describes, scaled so its last entry
78    /// is one wherever that entry is not zero: its other entries then read as
79    /// the scale, turn, translation and perspective they are.
80    pub(crate) fn from_homogeneous(matrix: [[f32; 3]; 3]) -> Self {
81        let w = matrix[2][2];
82        if w == 1.0 || w.abs() <= f32::EPSILON {
83            return Self { matrix };
84        }
85        Self {
86            matrix: matrix.map(|row| row.map(|value| value / w)),
87        }
88    }
89
90    /// Returns the composed transform that applies `self` first and `next` second.
91    pub fn then(self, next: Self) -> Self {
92        Self {
93            matrix: multiply_matrices(next.matrix, self.matrix),
94        }
95    }
96
97    pub fn inverse(self) -> Option<Self> {
98        let m = self.matrix;
99        let a = m[0][0];
100        let b = m[0][1];
101        let c = m[0][2];
102        let d = m[1][0];
103        let e = m[1][1];
104        let f = m[1][2];
105        let g = m[2][0];
106        let h = m[2][1];
107        let i = m[2][2];
108
109        let cofactor00 = e * i - f * h;
110        let cofactor01 = -(d * i - f * g);
111        let cofactor02 = d * h - e * g;
112        let cofactor10 = -(b * i - c * h);
113        let cofactor11 = a * i - c * g;
114        let cofactor12 = -(a * h - b * g);
115        let cofactor20 = b * f - c * e;
116        let cofactor21 = -(a * f - c * d);
117        let cofactor22 = a * e - b * d;
118
119        let determinant = a * cofactor00 + b * cofactor01 + c * cofactor02;
120        if determinant.abs() <= f32::EPSILON {
121            return None;
122        }
123        let inverse_determinant = 1.0 / determinant;
124
125        Some(Self {
126            matrix: [
127                [
128                    cofactor00 * inverse_determinant,
129                    cofactor10 * inverse_determinant,
130                    cofactor20 * inverse_determinant,
131                ],
132                [
133                    cofactor01 * inverse_determinant,
134                    cofactor11 * inverse_determinant,
135                    cofactor21 * inverse_determinant,
136                ],
137                [
138                    cofactor02 * inverse_determinant,
139                    cofactor12 * inverse_determinant,
140                    cofactor22 * inverse_determinant,
141                ],
142            ],
143        })
144    }
145
146    pub fn matrix(self) -> [[f32; 3]; 3] {
147        self.matrix
148    }
149
150    pub fn map_point(self, point: Point) -> Point {
151        let x = point.x;
152        let y = point.y;
153        let w = self.matrix[2][0] * x + self.matrix[2][1] * y + self.matrix[2][2];
154        let safe_w = if w.abs() <= f32::EPSILON { 1.0 } else { w };
155
156        Point {
157            x: (self.matrix[0][0] * x + self.matrix[0][1] * y + self.matrix[0][2]) / safe_w,
158            y: (self.matrix[1][0] * x + self.matrix[1][1] * y + self.matrix[1][2]) / safe_w,
159        }
160    }
161
162    pub fn map_rect(self, rect: Rect) -> [[f32; 2]; 4] {
163        [
164            self.map_point(Point {
165                x: rect.x,
166                y: rect.y,
167            }),
168            self.map_point(Point {
169                x: rect.x + rect.width,
170                y: rect.y,
171            }),
172            self.map_point(Point {
173                x: rect.x,
174                y: rect.y + rect.height,
175            }),
176            self.map_point(Point {
177                x: rect.x + rect.width,
178                y: rect.y + rect.height,
179            }),
180        ]
181        .map(|point| [point.x, point.y])
182    }
183
184    pub fn bounds_for_rect(self, rect: Rect) -> Rect {
185        quad_bounds(self.map_rect(rect))
186    }
187}
188
189fn axis_aligned_rect_from_quad(quad: [[f32; 2]; 4]) -> Option<Rect> {
190    let top_left = quad[0];
191    let top_right = quad[1];
192    let bottom_left = quad[2];
193    let bottom_right = quad[3];
194    let x_epsilon = 1e-4;
195    let y_epsilon = 1e-4;
196
197    if (top_left[1] - top_right[1]).abs() > y_epsilon
198        || (bottom_left[1] - bottom_right[1]).abs() > y_epsilon
199        || (top_left[0] - bottom_left[0]).abs() > x_epsilon
200        || (top_right[0] - bottom_right[0]).abs() > x_epsilon
201    {
202        return None;
203    }
204
205    Some(Rect {
206        x: top_left[0],
207        y: top_left[1],
208        width: top_right[0] - top_left[0],
209        height: bottom_left[1] - top_left[1],
210    })
211}
212
213impl Default for ProjectiveTransform {
214    fn default() -> Self {
215        Self::identity()
216    }
217}
218
219#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
220pub struct IsolationReasons {
221    pub explicit_offscreen: bool,
222    pub shape_clip: bool,
223    pub effect: bool,
224    pub backdrop: bool,
225    pub group_opacity: bool,
226    pub blend_mode: bool,
227}
228
229impl IsolationReasons {
230    pub fn has_any(self) -> bool {
231        self.explicit_offscreen
232            || self.shape_clip
233            || self.effect
234            || self.backdrop
235            || self.group_opacity
236            || self.blend_mode
237    }
238}
239
240#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
241pub enum CachePolicy {
242    #[default]
243    None,
244    Auto,
245}
246
247#[derive(Clone)]
248pub struct HitTestNode {
249    pub shape: Option<RoundedCornerShape>,
250    /// The node's modifier slices, shared rather than copied: they hold its
251    /// pointer inputs and the pointer icon it asks for while hovered. A node
252    /// that only names an icon is still a hit target, which is how a
253    /// decorative panel carries a cursor without handling clicks.
254    pub handlers: Rc<ModifierNodeSlices>,
255}
256
257#[derive(Clone, Debug, PartialEq)]
258pub struct DrawPrimitiveNode {
259    pub primitive: DrawPrimitive,
260    pub clip: Option<Rect>,
261}
262
263#[derive(Clone, Debug, PartialEq)]
264pub struct TextPrimitiveNode {
265    pub node_id: NodeId,
266    pub rect: Rect,
267    /// Shared so the renderer can hand the same allocation to every draw it
268    /// emits for this node instead of deep-copying the string once per emit.
269    pub text: Rc<AnnotatedString>,
270    /// `text` as the renderer's draws carry it across threads, shared with
271    /// the prepared layout it came from so no frame converts it again.
272    pub render_text: std::sync::Arc<RenderString>,
273    /// Shared so every frame's draws of this text hand over the same style
274    /// instead of copying it.
275    pub text_style: std::sync::Arc<TextStyle>,
276    pub font_size: f32,
277    pub layout_options: TextLayoutOptions,
278    pub clip: Option<Rect>,
279}
280
281impl TextPrimitiveNode {
282    /// Whether the text's glyphs stay inside `bounds`: nothing casts a shadow
283    /// past them and the layout does not let the text overflow its rect.
284    fn draws_within(&self, bounds: Rect) -> bool {
285        self.text_style.span_style.shadow.is_none()
286            && self
287                .text
288                .span_styles
289                .iter()
290                .all(|span| span.item.shadow.is_none())
291            && !matches!(self.layout_options.overflow, TextOverflow::Visible)
292            && rect_within(self.rect, bounds)
293    }
294}
295
296#[derive(Clone, Copy, Debug, PartialEq, Eq)]
297pub enum PrimitivePhase {
298    BeforeChildren,
299    AfterChildren,
300}
301
302#[derive(Clone, Debug, PartialEq)]
303pub enum PrimitiveNode {
304    Draw(Box<DrawPrimitiveNode>),
305    Text(Box<TextPrimitiveNode>),
306}
307
308#[derive(Clone, Debug, PartialEq)]
309pub struct PrimitiveEntry {
310    pub phase: PrimitivePhase,
311    pub node: PrimitiveNode,
312}
313
314#[derive(Clone)]
315pub struct LayerNode {
316    pub node_id: Option<NodeId>,
317    /// Set on the synthetic layer that holds a node's outer draws, those chained
318    /// before its graphics layer, around that node's own layer. Scene updates
319    /// address the node through this id because the node's layer has none of the
320    /// outer draws and the wrapper has no node id of its own.
321    pub wraps: Option<NodeId>,
322    /// The bounds of the node's layer, where its clip cuts, its transforms
323    /// pivot and an offscreen pass draws it.
324    pub local_bounds: Rect,
325    /// The node's own rect, `(0, 0, size)`, where its pointer handlers are,
326    /// when it differs from `local_bounds`: a clip or graphics layer that
327    /// wraps a coordinator a later padding or offset moves bounds the layer
328    /// there instead, as in Compose. `None` when the two agree.
329    pub node_bounds: Option<Rect>,
330    pub transform_to_parent: ProjectiveTransform,
331    pub content_offset: Point,
332    pub motion_context_animated: bool,
333    pub translated_content_context: bool,
334    pub translated_content_offset: Point,
335    /// Where layout placed this layer within its parent's content, before the
336    /// parent's content offset. Scene updates add these up from the root, with
337    /// each layer's content offset and graphics-layer translation, to find the
338    /// window origin of a subtree they rebuild.
339    pub origin_in_parent: Point,
340    pub graphics_layer: GraphicsLayer,
341    pub clip_to_bounds: bool,
342    pub shadow_clip: Option<Rect>,
343    pub hit_test: Option<HitTestNode>,
344    pub has_hit_targets: bool,
345    /// Whether this subtree publishes live window origins (a text field's
346    /// popup anchor, a scroll container's viewport rect). Those sinks are
347    /// written during a full lowering, so the scroll fast path may translate
348    /// a retained subtree in place only when this is false.
349    pub has_origin_sinks: bool,
350    /// Whether everything this layer and its subtree draw lies within its
351    /// `local_bounds`, give or take [`CONTAINED_DRAW_SLACK`], so a renderer
352    /// may skip the whole subtree where those bounds are clipped away. Scene
353    /// building keeps it current; `false` promises nothing.
354    pub draws_within_bounds: bool,
355    pub isolation: IsolationReasons,
356    pub cache_policy: CachePolicy,
357    pub cache_hashes: LayerRasterCacheHashes,
358    pub cache_hashes_valid: bool,
359    pub children: Vec<RenderNode>,
360}
361
362impl Default for LayerNode {
363    fn default() -> Self {
364        Self {
365            node_id: None,
366            wraps: None,
367            local_bounds: Rect {
368                x: 0.0,
369                y: 0.0,
370                width: 0.0,
371                height: 0.0,
372            },
373            node_bounds: None,
374            transform_to_parent: ProjectiveTransform::identity(),
375            content_offset: Point::default(),
376            motion_context_animated: false,
377            translated_content_context: false,
378            translated_content_offset: Point::default(),
379            origin_in_parent: Point::default(),
380            graphics_layer: GraphicsLayer::default(),
381            clip_to_bounds: false,
382            shadow_clip: None,
383            hit_test: None,
384            has_hit_targets: false,
385            has_origin_sinks: false,
386            draws_within_bounds: false,
387            isolation: IsolationReasons::default(),
388            cache_policy: CachePolicy::None,
389            cache_hashes: LayerRasterCacheHashes::default(),
390            cache_hashes_valid: false,
391            children: Vec::new(),
392        }
393    }
394}
395
396/// How far past its bounds a layer that draws within them may still put
397/// pixels: glyph and edge antialiasing.
398pub const CONTAINED_DRAW_SLACK: f32 = 1.0;
399
400impl LayerNode {
401    /// Whether this layer's content, as [`LayerNode::draws_within_bounds`]
402    /// describes it, stays within its bounds: it clips to them, or its draws,
403    /// its texts and its children placed where they are all fit inside.
404    pub fn content_draws_within_bounds(&self) -> bool {
405        if self.visual_clip_rect().is_some() {
406            return true;
407        }
408        let bounds = inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK);
409        self.children.iter().all(|child| match child {
410            RenderNode::DrawRun(run) => run
411                .recording
412                .bounds()
413                .is_none_or(|drawn| rect_within(drawn, bounds)),
414            RenderNode::Primitive(entry) => match &entry.node {
415                PrimitiveNode::Text(text) => text.draws_within(bounds),
416                PrimitiveNode::Draw(_) => false,
417            },
418            RenderNode::Layer(layer) => layer.draws_within_parent(bounds),
419        })
420    }
421
422    /// Whether this layer, drawn as a child, puts nothing outside `bounds`:
423    /// it draws within its own bounds, casts no shadow, applies no effect,
424    /// and its bounds placed in its parent lie inside.
425    fn draws_within_parent(&self, bounds: Rect) -> bool {
426        self.draws_within_bounds
427            && self.graphics_layer.shadow_elevation <= 0.0
428            && self.effect().is_none()
429            && self.backdrop().is_none()
430            && rect_within(
431                quad_bounds(self.transform_to_parent.map_rect(self.local_bounds)),
432                bounds,
433            )
434    }
435
436    /// The node's own rect, where its pointer handlers are.
437    pub fn node_rect(&self) -> Rect {
438        self.node_bounds.unwrap_or(self.local_bounds)
439    }
440
441    pub fn clip_rect(&self) -> Option<Rect> {
442        (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
443    }
444
445    /// Where the layer's drawing is cut: its clip or, when it composites
446    /// through an offscreen buffer for its alpha or by request, its bounds,
447    /// as Compose's layer-sized buffer cuts it. Hit testing takes only
448    /// [`Self::clip_rect`]: alpha hides nothing from a finger.
449    pub fn visual_clip_rect(&self) -> Option<Rect> {
450        self.clip_rect().or_else(|| {
451            (self.isolation.group_opacity || self.isolation.explicit_offscreen)
452                .then_some(self.local_bounds)
453        })
454    }
455
456    pub fn effect(&self) -> Option<&RenderEffect> {
457        self.graphics_layer.render_effect.as_ref()
458    }
459
460    pub fn backdrop(&self) -> Option<&RenderEffect> {
461        self.graphics_layer.backdrop_effect.as_ref()
462    }
463
464    pub fn opacity(&self) -> f32 {
465        self.graphics_layer.alpha
466    }
467
468    pub fn blend_mode(&self) -> BlendMode {
469        self.graphics_layer.blend_mode
470    }
471
472    pub fn color_filter(&self) -> Option<ColorFilter> {
473        self.graphics_layer.color_filter
474    }
475
476    pub fn target_content_hash(&self) -> u64 {
477        if self.cache_hashes_valid {
478            self.cache_hashes.target_content
479        } else {
480            crate::graph_hash::layer_raster_cache_hashes(self).target_content
481        }
482    }
483
484    pub fn motion_source_content_hash(&self) -> u64 {
485        crate::graph_hash::layer_motion_source_content_hash(self)
486    }
487
488    pub fn effect_hash(&self) -> u64 {
489        if self.cache_hashes_valid {
490            self.cache_hashes.effect
491        } else {
492            crate::graph_hash::layer_raster_cache_hashes(self).effect
493        }
494    }
495
496    pub fn recompute_raster_cache_hashes(&mut self) {
497        crate::graph_hash::recompute_layer_raster_cache_hashes(self);
498    }
499}
500
501#[derive(Clone)]
502pub enum RenderNode {
503    Primitive(PrimitiveEntry),
504    /// A whole draw command's primitives as one node. A heavy canvas records
505    /// thousands of primitives per frame; wrapping each in its own
506    /// [`RenderNode`] made the graph rebuild move every one of them twice and
507    /// free seventeen thousand nodes per frame on a stress scene. The run
508    /// keeps the recorded vector intact — semantically it is exactly that
509    /// many consecutive `Primitive` draw entries with no per-primitive clip.
510    DrawRun(DrawRunNode),
511    Layer(Box<LayerNode>),
512}
513
514/// Stable identity of the draw command a run was recorded from: the layout
515/// node owning the command, the command's index in that node's command list,
516/// and which placement pass produced this run (a `WithContent` command emits
517/// one run per placement, so the pair alone is not unique). Rendering does
518/// not read it yet; it is the key under which retained recording state lives
519/// as retention moves up to the draw-command recorder, and it must survive
520/// recording, graph construction, normalized-scene creation, and renderer
521/// cache lookup unchanged.
522#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
523pub struct DrawCommandId {
524    pub node_id: NodeId,
525    pub command_index: u32,
526    pub placement: DrawPlacement,
527}
528
529#[derive(Clone, Debug, PartialEq)]
530pub struct DrawRunNode {
531    pub phase: PrimitivePhase,
532    /// Which draw command recorded these primitives. `None` only for runs
533    /// with no per-command provenance (hand-built tests).
534    pub command: Option<DrawCommandId>,
535    /// Shared, not owned: the recording registry keyed by [`DrawCommandId`]
536    /// keeps a handle to the same recording, so its buffers survive this
537    /// node being dropped on the next rebuild and the command re-records
538    /// into them. Nothing mutates a recording after construction, which is
539    /// what makes sharing sound.
540    pub recording: Rc<CommandRecording>,
541    /// The segments of the recording this run draws: one placement's part
542    /// of a with-content command, or the whole recording.
543    pub segments: Range<u32>,
544    /// Content facts consumers keep asking per frame, answered while
545    /// recording and never by rescanning.
546    pub summary: DrawRunSummary,
547}
548
549/// What a run contains, answered while recording.
550pub type DrawRunSummary = RecordingSummary;
551
552impl DrawRunNode {
553    pub fn new(phase: PrimitivePhase, primitives: Vec<DrawPrimitive>) -> Self {
554        Self::for_command(phase, None, primitives)
555    }
556
557    pub fn for_command(
558        phase: PrimitivePhase,
559        command: Option<DrawCommandId>,
560        primitives: Vec<DrawPrimitive>,
561    ) -> Self {
562        let recording = CommandRecording::from_primitives(primitives);
563        let segments = recording.all_segments();
564        Self::for_command_shared(phase, command, Rc::new(recording), segments)
565    }
566
567    pub fn for_command_shared(
568        phase: PrimitivePhase,
569        command: Option<DrawCommandId>,
570        recording: Rc<CommandRecording>,
571        segments: Range<u32>,
572    ) -> Self {
573        let summary = recording.summary_in(&segments);
574        Self {
575            phase,
576            command,
577            recording,
578            segments,
579            summary,
580        }
581    }
582
583    /// The run's primitives, materialised from the recording in order.
584    pub fn primitives(&self) -> impl Iterator<Item = DrawPrimitive> + '_ {
585        self.recording.primitives(self.segments.clone())
586    }
587
588    /// The rect every entry of the run can reach.
589    pub fn coverage_rects(&self) -> impl Iterator<Item = Rect> + '_ {
590        self.recording.coverage_rects(self.segments.clone())
591    }
592
593    pub fn len(&self) -> usize {
594        self.recording.len_in(&self.segments)
595    }
596
597    pub fn is_empty(&self) -> bool {
598        self.recording.is_empty_in(&self.segments)
599    }
600}
601
602#[derive(Clone)]
603pub struct RenderGraph {
604    pub root: LayerNode,
605}
606
607impl RenderGraph {
608    pub fn new(mut root: LayerNode) -> Self {
609        root.recompute_raster_cache_hashes();
610        Self { root }
611    }
612
613    pub fn node_count(&self) -> usize {
614        fn count_layer(layer: &LayerNode) -> usize {
615            1 + layer
616                .children
617                .iter()
618                .map(|child| match child {
619                    RenderNode::Primitive(_) => 1,
620                    RenderNode::DrawRun(run) => run.len(),
621                    RenderNode::Layer(child_layer) => count_layer(child_layer),
622                })
623                .sum::<usize>()
624        }
625
626        count_layer(&self.root)
627    }
628
629    pub fn heap_bytes(&self) -> usize {
630        layer_heap_bytes(&self.root)
631    }
632
633    /// Replaces the set with the graph's visual observation owners, retaining its capacity.
634    pub fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) {
635        fn collect(layer: &LayerNode, nodes: &mut HashSet<NodeId>) {
636            if let Some(node_id) = layer.node_id {
637                nodes.insert(node_id);
638            }
639            for child in &layer.children {
640                match child {
641                    RenderNode::DrawRun(run) => {
642                        if let Some(command) = run.command {
643                            nodes.insert(command.node_id);
644                        }
645                    }
646                    RenderNode::Layer(child) => collect(child, nodes),
647                    RenderNode::Primitive(_) => {}
648                }
649            }
650        }
651
652        nodes.clear();
653        collect(&self.root, nodes);
654    }
655}
656
657fn layer_heap_bytes(layer: &LayerNode) -> usize {
658    size_of::<RenderNode>() * layer.children.capacity()
659        + layer
660            .children
661            .iter()
662            .map(render_node_heap_bytes)
663            .sum::<usize>()
664}
665
666fn render_node_heap_bytes(node: &RenderNode) -> usize {
667    match node {
668        RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
669        RenderNode::DrawRun(run) => {
670            run.recording.pod_heap_bytes()
671                + std::mem::size_of_val(run.recording.others())
672                + run
673                    .recording
674                    .others()
675                    .iter()
676                    .map(draw_primitive_heap_bytes)
677                    .sum::<usize>()
678        }
679        RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
680    }
681}
682
683fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
684    match &entry.node {
685        PrimitiveNode::Draw(draw) => {
686            size_of::<DrawPrimitiveNode>() + draw_primitive_heap_bytes(&draw.primitive)
687        }
688        PrimitiveNode::Text(text) => {
689            size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
690        }
691    }
692}
693
694fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
695    match primitive {
696        DrawPrimitive::Content
697        | DrawPrimitive::Rect { .. }
698        | DrawPrimitive::RoundRect { .. }
699        | DrawPrimitive::Arc { .. } => 0,
700        DrawPrimitive::Blend { primitive, .. } => {
701            size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
702        }
703        DrawPrimitive::Image { .. } => 0,
704        DrawPrimitive::Text(text) => {
705            size_of::<cranpose_ui_graphics::TextPrimitive>()
706                + text.text.len()
707                + text
708                    .style
709                    .font_family
710                    .as_ref()
711                    .map_or(0, std::string::String::capacity)
712        }
713        DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
714    }
715}
716
717fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
718    match shadow {
719        ShadowPrimitive::Drop { shape, .. } => {
720            size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
721        }
722        ShadowPrimitive::Inner { fill, cutout, .. } => {
723            size_of::<DrawPrimitive>() * 2
724                + draw_primitive_heap_bytes(fill)
725                + draw_primitive_heap_bytes(cutout)
726        }
727    }
728}
729
730fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
731    text.text.capacity()
732        + text.span_styles.capacity() * size_of::<usize>() * 2
733        + text.paragraph_styles.capacity() * size_of::<usize>() * 2
734        + text.string_annotations.capacity() * size_of::<usize>() * 2
735        + text.link_annotations.capacity() * size_of::<usize>() * 2
736        + text
737            .string_annotations
738            .iter()
739            .map(|annotation| {
740                annotation.item.tag.capacity() + annotation.item.annotation.capacity()
741            })
742            .sum::<usize>()
743        + text
744            .link_annotations
745            .iter()
746            .map(|annotation| match &annotation.item {
747                cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
748                cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
749            })
750            .sum::<usize>()
751}
752
753pub fn quad_bounds(quad: [[f32; 2]; 4]) -> Rect {
754    let mut min_x = f32::INFINITY;
755    let mut min_y = f32::INFINITY;
756    let mut max_x = f32::NEG_INFINITY;
757    let mut max_y = f32::NEG_INFINITY;
758
759    for [x, y] in quad {
760        min_x = min_x.min(x);
761        min_y = min_y.min(y);
762        max_x = max_x.max(x);
763        max_y = max_y.max(y);
764    }
765
766    Rect {
767        x: min_x,
768        y: min_y,
769        width: (max_x - min_x).max(0.0),
770        height: (max_y - min_y).max(0.0),
771    }
772}
773
774fn multiply_matrices(lhs: [[f32; 3]; 3], rhs: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
775    let mut out = [[0.0; 3]; 3];
776    for row in 0..3 {
777        for col in 0..3 {
778            out[row][col] =
779                lhs[row][0] * rhs[0][col] + lhs[row][1] * rhs[1][col] + lhs[row][2] * rhs[2][col];
780        }
781    }
782    out
783}
784
785fn solve_homography(source: [[f32; 2]; 4], target: [[f32; 2]; 4]) -> Option<[f32; 8]> {
786    let mut matrix = [[0.0f32; 9]; 8];
787    for (index, (src, dst)) in source.into_iter().zip(target).enumerate() {
788        let row = index * 2;
789        let x = src[0];
790        let y = src[1];
791        let u = dst[0];
792        let v = dst[1];
793
794        matrix[row] = [x, y, 1.0, 0.0, 0.0, 0.0, -u * x, -u * y, u];
795        matrix[row + 1] = [0.0, 0.0, 0.0, x, y, 1.0, -v * x, -v * y, v];
796    }
797
798    for pivot in 0..8 {
799        let mut pivot_row = pivot;
800        let mut pivot_value = matrix[pivot][pivot].abs();
801        let mut candidate = pivot + 1;
802        while candidate < 8 {
803            let candidate_value = matrix[candidate][pivot].abs();
804            if candidate_value > pivot_value {
805                pivot_row = candidate;
806                pivot_value = candidate_value;
807            }
808            candidate += 1;
809        }
810
811        if pivot_value <= f32::EPSILON {
812            return None;
813        }
814
815        if pivot_row != pivot {
816            matrix.swap(pivot, pivot_row);
817        }
818
819        let divisor = matrix[pivot][pivot];
820        let mut col = pivot;
821        while col < 9 {
822            matrix[pivot][col] /= divisor;
823            col += 1;
824        }
825
826        for row in 0..8 {
827            if row == pivot {
828                continue;
829            }
830            let factor = matrix[row][pivot];
831            if factor.abs() <= f32::EPSILON {
832                continue;
833            }
834            let mut col = pivot;
835            while col < 9 {
836                matrix[row][col] -= factor * matrix[pivot][col];
837                col += 1;
838            }
839        }
840    }
841
842    let mut solution = [0.0f32; 8];
843    for index in 0..8 {
844        solution[index] = matrix[index][8];
845    }
846    Some(solution)
847}
848
849fn inflate_rect(rect: Rect, by: f32) -> Rect {
850    Rect {
851        x: rect.x - by,
852        y: rect.y - by,
853        width: rect.width + by * 2.0,
854        height: rect.height + by * 2.0,
855    }
856}
857
858fn rect_within(inner: Rect, outer: Rect) -> bool {
859    inner.x >= outer.x
860        && inner.y >= outer.y
861        && inner.x + inner.width <= outer.x + outer.width
862        && inner.y + inner.height <= outer.y + outer.height
863}
864
865#[cfg(test)]
866#[path = "tests/graph_tests.rs"]
867mod tests;