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