Skip to main content

cranpose_render_common/
graph.rs

1use std::{
2    cell::Cell,
3    mem::size_of,
4    ops::{Deref, DerefMut, Range},
5    rc::Rc,
6};
7
8use cranpose_core::{NodeId, collections::map::HashSet};
9use cranpose_ui::{
10    GraphicsLayer, ModifierNodeSlices, Point, Rect, RenderEffect, TextLayoutOptions, TextOverflow,
11    TextStyle,
12    text::{AnnotatedString, RenderString, SpanStyle, TextDrawStyle},
13};
14pub use cranpose_ui_graphics::transform::{ProjectiveTransform, quad_bounds};
15use cranpose_ui_graphics::{
16    BlendMode, ColorFilter, CommandRecording, DrawPrimitive, RecordingSummary, ShadowPrimitive,
17};
18
19use crate::{raster_cache::LayerRasterCacheHashes, style_shared::DrawPlacement};
20
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22pub struct IsolationReasons {
23    pub explicit_offscreen: bool,
24    pub shape_clip: bool,
25    pub effect: bool,
26    pub backdrop: bool,
27    pub group_opacity: bool,
28    pub blend_mode: bool,
29}
30
31impl IsolationReasons {
32    pub fn has_any(self) -> bool {
33        self.explicit_offscreen
34            || self.shape_clip
35            || self.effect
36            || self.backdrop
37            || self.group_opacity
38            || self.blend_mode
39    }
40}
41
42#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
43pub enum CachePolicy {
44    #[default]
45    None,
46    Auto,
47}
48
49#[derive(Clone)]
50pub struct HitTestNode {
51    /// The node's modifier slices, shared rather than copied: they hold its
52    /// pointer inputs and the pointer icon it asks for while hovered. A node
53    /// that only names an icon is still a hit target, which is how a
54    /// decorative panel carries a cursor without handling clicks.
55    pub handlers: Rc<ModifierNodeSlices>,
56}
57
58#[derive(Clone, Debug, PartialEq)]
59pub struct DrawPrimitiveNode {
60    pub primitive: DrawPrimitive,
61    pub clip: Option<Rect>,
62}
63
64#[derive(Clone, Debug, PartialEq)]
65pub struct TextPrimitiveNode {
66    pub node_id: NodeId,
67    pub rect: Rect,
68    /// Shared so the renderer can hand the same allocation to every draw it
69    /// emits for this node instead of deep-copying the string once per emit.
70    pub text: Rc<AnnotatedString>,
71    /// `text` as the renderer's draws carry it across threads, shared with
72    /// the prepared layout it came from so no frame converts it again.
73    pub render_text: std::sync::Arc<RenderString>,
74    /// Shared so every frame's draws of this text hand over the same style
75    /// instead of copying it.
76    pub text_style: std::sync::Arc<TextStyle>,
77    pub font_size: f32,
78    pub layout_options: TextLayoutOptions,
79    pub clip: Option<Rect>,
80}
81
82impl TextPrimitiveNode {
83    /// Conservative bounds for the emitted glyphs and paint. Returns `None`
84    /// when an unclipped layout or style can draw beyond its layout rectangle.
85    pub fn draw_bounds(&self) -> Option<Rect> {
86        if let Some(clip) = self.clip {
87            return Some(clip);
88        }
89        let style = &self.text_style.span_style;
90        let stroked =
91            |style: &SpanStyle| matches!(style.draw_style, Some(TextDrawStyle::Stroke { .. }));
92        (!matches!(self.layout_options.overflow, TextOverflow::Visible)
93            && style.shadow.is_none()
94            && style
95                .baseline_shift
96                .is_none_or(|shift| !shift.is_specified() || shift.0 == 0.0)
97            && !stroked(style)
98            && self
99                .text
100                .span_styles
101                .iter()
102                .all(|span| !stroked(&span.item)))
103        .then_some(self.rect)
104    }
105
106    fn draws_within(&self, bounds: Rect) -> bool {
107        self.draw_bounds()
108            .is_some_and(|drawn| rect_within(drawn, bounds))
109    }
110}
111
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub enum PrimitivePhase {
114    BeforeChildren,
115    AfterChildren,
116}
117
118#[derive(Clone, Debug, PartialEq)]
119pub enum PrimitiveNode {
120    Draw(Box<DrawPrimitiveNode>),
121    Text(Box<TextPrimitiveNode>),
122}
123
124#[derive(Clone, Debug, PartialEq)]
125pub struct PrimitiveEntry {
126    pub phase: PrimitivePhase,
127    pub node: PrimitiveNode,
128}
129
130/// Scene properties with implicit defaults and independently mutable clones.
131#[derive(Clone, Debug, Default)]
132pub struct LayerProperties(Option<Box<GraphicsLayer>>);
133
134impl LayerProperties {
135    pub(crate) fn replace(&mut self, value: Option<GraphicsLayer>) {
136        match (&mut self.0, value) {
137            (Some(current), Some(value)) => **current = value,
138            (storage, value) => *storage = value.map(Box::new),
139        }
140    }
141}
142
143impl Deref for LayerProperties {
144    type Target = GraphicsLayer;
145
146    fn deref(&self) -> &Self::Target {
147        self.0.as_deref().unwrap_or(&GraphicsLayer::DEFAULT)
148    }
149}
150
151impl DerefMut for LayerProperties {
152    fn deref_mut(&mut self) -> &mut Self::Target {
153        self.0.get_or_insert_with(Box::default)
154    }
155}
156
157impl From<GraphicsLayer> for LayerProperties {
158    fn from(value: GraphicsLayer) -> Self {
159        Self(Some(Box::new(value)))
160    }
161}
162
163impl From<Option<GraphicsLayer>> for LayerProperties {
164    fn from(value: Option<GraphicsLayer>) -> Self {
165        Self(value.map(Box::new))
166    }
167}
168
169impl PartialEq for LayerProperties {
170    fn eq(&self, other: &Self) -> bool {
171        **self == **other
172    }
173}
174
175#[derive(Clone)]
176pub struct LayerNode {
177    pub node_id: Option<NodeId>,
178    /// Set on the synthetic layer that holds a node's outer draws, those chained
179    /// before its graphics layer, around that node's own layer. Scene updates
180    /// address the node through this id because the node's layer has none of the
181    /// outer draws and the wrapper has no node id of its own.
182    pub wraps: Option<NodeId>,
183    /// The bounds of the node's layer, where its clip cuts, its transforms
184    /// pivot and an offscreen pass draws it.
185    pub local_bounds: Rect,
186    /// The node's own rect, `(0, 0, size)`, where its pointer handlers are,
187    /// when it differs from `local_bounds`: a clip or graphics layer that
188    /// wraps a coordinator a later padding or offset moves bounds the layer
189    /// there instead, as in Compose. `None` when the two agree.
190    pub node_bounds: Option<Rect>,
191    pub transform_to_parent: ProjectiveTransform,
192    pub content_offset: Point,
193    pub motion_context_animated: bool,
194    pub translated_content_context: bool,
195    pub translated_content_offset: Point,
196    /// Where layout placed this layer within its parent's content, before the
197    /// parent's content offset. Scene updates add these up from the root, with
198    /// each layer's content offset and graphics-layer translation, to find the
199    /// window origin of a subtree they rebuild.
200    pub origin_in_parent: Point,
201    pub graphics_layer: LayerProperties,
202    pub clip_to_bounds: bool,
203    pub hit_test: Option<HitTestNode>,
204    pub has_hit_targets: bool,
205    /// Whether this subtree publishes live window origins (a text field's
206    /// popup anchor, a scroll container's viewport rect). Those sinks are
207    /// written during a full lowering, so the scroll fast path may translate
208    /// a retained subtree in place only when this is false.
209    pub has_origin_sinks: bool,
210    /// Whether everything this layer and its subtree draw lies within its
211    /// `local_bounds`, give or take [`CONTAINED_DRAW_SLACK`], so a renderer
212    /// may skip the whole subtree where those bounds are clipped away. Scene
213    /// building keeps it current; `false` promises nothing.
214    pub draws_within_bounds: bool,
215    pub isolation: IsolationReasons,
216    pub cache_policy: CachePolicy,
217    /// The raster cache hashes, computed the first time a renderer asks and
218    /// kept until the layer or a layer beneath it changes.
219    pub cache_hashes: Cell<Option<LayerRasterCacheHashes>>,
220    /// The scene updates that changed what the layer draws.
221    pub content_changes: ContentChanges,
222    pub children: Vec<RenderNode>,
223}
224
225/// The last scene update that changed what a layer draws, and how many
226/// updates in a row up to it did.
227#[derive(Clone, Copy, Debug, Default)]
228pub struct ContentChanges {
229    last_update: u64,
230    run: u32,
231}
232
233impl ContentChanges {
234    fn note(&mut self, update: u64) {
235        if self.run > 0 && self.last_update == update {
236            return;
237        }
238        self.run = if self.run > 0 && self.last_update + 1 == update {
239            self.run + 1
240        } else {
241            1
242        };
243        self.last_update = update;
244    }
245}
246
247impl Default for LayerNode {
248    fn default() -> Self {
249        Self {
250            node_id: None,
251            wraps: None,
252            local_bounds: Rect {
253                x: 0.0,
254                y: 0.0,
255                width: 0.0,
256                height: 0.0,
257            },
258            node_bounds: None,
259            transform_to_parent: ProjectiveTransform::identity(),
260            content_offset: Point::default(),
261            motion_context_animated: false,
262            translated_content_context: false,
263            translated_content_offset: Point::default(),
264            origin_in_parent: Point::default(),
265            graphics_layer: LayerProperties::default(),
266            clip_to_bounds: false,
267            hit_test: None,
268            has_hit_targets: false,
269            has_origin_sinks: false,
270            draws_within_bounds: false,
271            isolation: IsolationReasons::default(),
272            cache_policy: CachePolicy::None,
273            cache_hashes: Cell::new(None),
274            content_changes: ContentChanges::default(),
275            children: Vec::new(),
276        }
277    }
278}
279
280/// How far past its bounds a layer that draws within them may still put
281/// pixels: glyph and edge antialiasing.
282pub const CONTAINED_DRAW_SLACK: f32 = 1.0;
283
284impl LayerNode {
285    /// Every string this layer and the layers below it paint, in draw order.
286    pub fn painted_text(&self) -> Vec<String> {
287        let mut out = Vec::new();
288        self.collect_painted_text(&mut out);
289        out
290    }
291
292    fn collect_painted_text(&self, out: &mut Vec<String>) {
293        for child in &self.children {
294            match child {
295                RenderNode::Primitive(primitive) => {
296                    if let PrimitiveNode::Text(text) = &primitive.node {
297                        out.push(text.text.text.clone());
298                    }
299                }
300                RenderNode::Layer(child) => child.collect_painted_text(out),
301                RenderNode::DrawRun(run) => {
302                    for primitive in run.primitives() {
303                        if let DrawPrimitive::Text(text) = primitive {
304                            out.push(text.text.to_string());
305                        }
306                    }
307                }
308            }
309        }
310    }
311
312    /// Whether this layer's content, as [`LayerNode::draws_within_bounds`]
313    /// describes it, stays within its bounds: it clips to them, or its draws,
314    /// its texts and its children placed where they are all fit inside.
315    pub fn content_draws_within_bounds(&self) -> bool {
316        if self.visual_clip_rect().is_some() {
317            return true;
318        }
319        let bounds = inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK);
320        self.children.iter().all(|child| match child {
321            RenderNode::DrawRun(run) => {
322                !run.summary.has_shadow
323                    && run
324                        .recording
325                        .bounds()
326                        .is_none_or(|drawn| rect_within(drawn, bounds))
327            }
328            RenderNode::Primitive(entry) => match &entry.node {
329                PrimitiveNode::Text(text) => text.draws_within(bounds),
330                PrimitiveNode::Draw(_) => false,
331            },
332            RenderNode::Layer(layer) => layer.draws_within_parent(bounds),
333        })
334    }
335
336    pub(crate) fn refresh_child_facts(&mut self) {
337        self.has_hit_targets = self.hit_test.is_some()
338            || self.children.iter().any(|child| match child {
339                RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
340                RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
341            });
342        self.draws_within_bounds = self.content_draws_within_bounds();
343    }
344
345    /// Whether this layer, drawn as a child, puts nothing outside `bounds`:
346    /// it draws within its own bounds, casts no shadow, applies no effect,
347    /// and its bounds placed in its parent lie inside.
348    fn draws_within_parent(&self, bounds: Rect) -> bool {
349        self.draws_within_bounds
350            && self.graphics_layer.shadow_elevation <= 0.0
351            && self.effect().is_none()
352            && self.backdrop().is_none()
353            && rect_within(
354                quad_bounds(
355                    self.transform_to_parent
356                        .map_rect(inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK)),
357                ),
358                bounds,
359            )
360    }
361
362    /// The node's own rect, where its pointer handlers are.
363    pub fn node_rect(&self) -> Rect {
364        self.node_bounds.unwrap_or(self.local_bounds)
365    }
366
367    pub fn clip_rect(&self) -> Option<Rect> {
368        (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
369    }
370
371    /// Where the layer's own shadow is cut: its bounds when it clips its
372    /// content to them. A graphics-layer clip leaves the shadow whole.
373    pub fn shadow_clip(&self) -> Option<Rect> {
374        self.clip_to_bounds.then_some(self.local_bounds)
375    }
376
377    /// Where the layer's drawing is cut: its clip or, when it composites
378    /// through an offscreen buffer for its alpha or by request, its bounds,
379    /// as Compose's layer-sized buffer cuts it. Hit testing takes only
380    /// [`Self::clip_rect`]: alpha hides nothing from a finger.
381    pub fn visual_clip_rect(&self) -> Option<Rect> {
382        self.clip_rect().or_else(|| {
383            (self.isolation.group_opacity || self.isolation.explicit_offscreen)
384                .then_some(self.local_bounds)
385        })
386    }
387
388    pub fn effect(&self) -> Option<&RenderEffect> {
389        self.graphics_layer.render_effect.as_ref()
390    }
391
392    pub fn backdrop(&self) -> Option<&RenderEffect> {
393        self.graphics_layer.backdrop_effect.as_ref()
394    }
395
396    pub fn opacity(&self) -> f32 {
397        self.graphics_layer.alpha
398    }
399
400    pub fn blend_mode(&self) -> BlendMode {
401        self.graphics_layer.blend_mode
402    }
403
404    pub fn color_filter(&self) -> Option<ColorFilter> {
405        self.graphics_layer.color_filter
406    }
407
408    pub fn target_content_hash(&self) -> u64 {
409        self.raster_cache_hashes().target_content
410    }
411
412    pub fn motion_source_content_hash(&self) -> u64 {
413        crate::graph_hash::layer_motion_source_content_hash(self)
414    }
415
416    pub fn effect_hash(&self) -> u64 {
417        self.raster_cache_hashes().effect
418    }
419
420    fn raster_cache_hashes(&self) -> LayerRasterCacheHashes {
421        if let Some(hashes) = self.cache_hashes.get() {
422            return hashes;
423        }
424        let hashes = crate::graph_hash::layer_raster_cache_hashes(self);
425        self.cache_hashes.set(Some(hashes));
426        hashes
427    }
428
429    /// Forgets the raster cache hashes of this layer and every layer beneath
430    /// it, after a change the scene builder did not make: they are computed
431    /// again when asked for.
432    pub fn forget_raster_cache_hashes(&mut self) {
433        self.cache_hashes.set(None);
434        for child in &mut self.children {
435            if let RenderNode::Layer(child) = child {
436                child.forget_raster_cache_hashes();
437            }
438        }
439    }
440
441    /// Forgets this layer's own raster cache hashes: a property its hash
442    /// covers, or a layer beneath it, changed.
443    pub(crate) fn forget_own_raster_cache_hashes(&mut self) {
444        self.cache_hashes.set(None);
445    }
446
447    /// Records that scene update `update` changed what this layer draws, and
448    /// forgets its hashes.
449    pub(crate) fn note_content_change(&mut self, update: u64) {
450        self.cache_hashes.set(None);
451        self.content_changes.note(update);
452    }
453
454    /// Whether what this layer draws changed in scene update `update` and in
455    /// the one before it. A raster cache keyed on its content would miss
456    /// again on the next frame, so a renderer neither hashes nor caches it.
457    pub fn content_churns(&self, update: u64) -> bool {
458        self.content_changes.run >= 2 && self.content_changes.last_update == update
459    }
460}
461
462#[derive(Clone)]
463pub enum RenderNode {
464    Primitive(PrimitiveEntry),
465    /// A whole draw command's primitives as one node. A heavy canvas records
466    /// thousands of primitives per frame; wrapping each in its own
467    /// [`RenderNode`] made the graph rebuild move every one of them twice and
468    /// free seventeen thousand nodes per frame on a stress scene. The run
469    /// keeps the recorded vector intact — semantically it is exactly that
470    /// many consecutive `Primitive` draw entries with no per-primitive clip.
471    DrawRun(DrawRunNode),
472    Layer(Box<LayerNode>),
473}
474
475/// Stable identity of one placement of a draw command: its layout node,
476/// command index and placement pass. A `WithContent` command shares one CPU
477/// recording between its two runs, while each placement retains independent
478/// renderer state under this identity.
479#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
480pub struct DrawCommandId {
481    pub node_id: NodeId,
482    pub command_index: u32,
483    pub placement: DrawPlacement,
484}
485
486#[derive(Clone, Debug, PartialEq)]
487pub struct DrawRunNode {
488    pub phase: PrimitivePhase,
489    /// Which draw command recorded these primitives. `None` only for runs
490    /// with no per-command provenance (hand-built tests).
491    pub command: Option<DrawCommandId>,
492    /// The recording registry keeps a handle per layout node and command,
493    /// so this recording's buffers survive the draw run being dropped and
494    /// the command can record into them on a later update. Nothing mutates
495    /// a recording after construction, which is
496    /// what makes sharing sound.
497    pub recording: Rc<CommandRecording>,
498    /// The segments of the recording this run draws: one placement's part
499    /// of a with-content command, or the whole recording.
500    pub segments: Range<u32>,
501    /// Content facts consumers keep asking per frame, answered while
502    /// recording and never by rescanning.
503    pub summary: DrawRunSummary,
504}
505
506/// What a run contains, answered while recording.
507pub type DrawRunSummary = RecordingSummary;
508
509impl DrawRunNode {
510    pub fn new(phase: PrimitivePhase, primitives: Vec<DrawPrimitive>) -> Self {
511        Self::for_command(phase, None, primitives)
512    }
513
514    pub fn for_command(
515        phase: PrimitivePhase,
516        command: Option<DrawCommandId>,
517        primitives: Vec<DrawPrimitive>,
518    ) -> Self {
519        let recording = CommandRecording::from_primitives(primitives);
520        let segments = recording.all_segments();
521        Self::for_command_shared(phase, command, Rc::new(recording), segments)
522    }
523
524    pub fn for_command_shared(
525        phase: PrimitivePhase,
526        command: Option<DrawCommandId>,
527        recording: Rc<CommandRecording>,
528        segments: Range<u32>,
529    ) -> Self {
530        let summary = recording.summary_in(&segments);
531        Self {
532            phase,
533            command,
534            recording,
535            segments,
536            summary,
537        }
538    }
539
540    /// The run's primitives, materialised from the recording in order.
541    pub fn primitives(&self) -> impl Iterator<Item = DrawPrimitive> + '_ {
542        self.recording.primitives(self.segments.clone())
543    }
544
545    /// The rect every entry of the run can reach.
546    pub fn coverage_rects(&self) -> impl Iterator<Item = Rect> + '_ {
547        self.recording.coverage_rects(self.segments.clone())
548    }
549
550    pub fn len(&self) -> usize {
551        self.recording.len_in(&self.segments)
552    }
553
554    pub fn is_empty(&self) -> bool {
555        self.recording.is_empty_in(&self.segments)
556    }
557}
558
559#[derive(Clone)]
560pub struct RenderGraph {
561    pub root: LayerNode,
562    /// How many scene updates the graph took: the number of the latest.
563    pub update: u64,
564}
565
566impl RenderGraph {
567    pub fn new(root: LayerNode) -> Self {
568        Self { root, update: 0 }
569    }
570
571    pub fn node_count(&self) -> usize {
572        fn count_layer(layer: &LayerNode) -> usize {
573            1 + layer
574                .children
575                .iter()
576                .map(|child| match child {
577                    RenderNode::Primitive(_) => 1,
578                    RenderNode::DrawRun(run) => run.len(),
579                    RenderNode::Layer(child_layer) => count_layer(child_layer),
580                })
581                .sum::<usize>()
582        }
583
584        count_layer(&self.root)
585    }
586
587    pub fn heap_bytes(&self) -> usize {
588        layer_heap_bytes(&self.root)
589    }
590
591    /// Replaces the set with the graph's visual observation owners, retaining its capacity.
592    pub fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) {
593        fn collect(layer: &LayerNode, nodes: &mut HashSet<NodeId>) {
594            if let Some(node_id) = layer.node_id {
595                nodes.insert(node_id);
596            }
597            for child in &layer.children {
598                match child {
599                    RenderNode::DrawRun(run) => {
600                        if let Some(command) = run.command {
601                            nodes.insert(command.node_id);
602                        }
603                    }
604                    RenderNode::Layer(child) => collect(child, nodes),
605                    RenderNode::Primitive(_) => {}
606                }
607            }
608        }
609
610        nodes.clear();
611        collect(&self.root, nodes);
612    }
613}
614
615fn layer_heap_bytes(layer: &LayerNode) -> usize {
616    size_of::<RenderNode>() * layer.children.capacity()
617        + layer
618            .graphics_layer
619            .0
620            .as_ref()
621            .map_or(0, |_| size_of::<GraphicsLayer>())
622        + layer
623            .children
624            .iter()
625            .map(render_node_heap_bytes)
626            .sum::<usize>()
627}
628
629fn render_node_heap_bytes(node: &RenderNode) -> usize {
630    match node {
631        RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
632        RenderNode::DrawRun(run) => {
633            run.recording.pod_heap_bytes()
634                + std::mem::size_of_val(run.recording.others())
635                + run
636                    .recording
637                    .others()
638                    .iter()
639                    .map(draw_primitive_heap_bytes)
640                    .sum::<usize>()
641        }
642        RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
643    }
644}
645
646fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
647    match &entry.node {
648        PrimitiveNode::Draw(draw) => {
649            size_of::<DrawPrimitiveNode>() + draw_primitive_heap_bytes(&draw.primitive)
650        }
651        PrimitiveNode::Text(text) => {
652            size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
653        }
654    }
655}
656
657fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
658    match primitive {
659        DrawPrimitive::Content
660        | DrawPrimitive::Rect { .. }
661        | DrawPrimitive::RoundRect { .. }
662        | DrawPrimitive::Arc { .. }
663        | DrawPrimitive::Line { .. }
664        | DrawPrimitive::Trapezoid { .. } => 0,
665        DrawPrimitive::Blend { primitive, .. } => {
666            size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
667        }
668        DrawPrimitive::Image { .. } => 0,
669        DrawPrimitive::Text(text) => {
670            size_of::<cranpose_ui_graphics::TextPrimitive>()
671                + text.text.len()
672                + text
673                    .style
674                    .font_family
675                    .as_ref()
676                    .map_or(0, std::string::String::capacity)
677        }
678        DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
679    }
680}
681
682fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
683    match shadow {
684        ShadowPrimitive::Drop { shape, .. } => {
685            size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
686        }
687        ShadowPrimitive::Inner { fill, cutout, .. } => {
688            size_of::<DrawPrimitive>() * 2
689                + draw_primitive_heap_bytes(fill)
690                + draw_primitive_heap_bytes(cutout)
691        }
692    }
693}
694
695fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
696    text.text.capacity()
697        + text.span_styles.capacity() * size_of::<usize>() * 2
698        + text.paragraph_styles.capacity() * size_of::<usize>() * 2
699        + text.string_annotations.capacity() * size_of::<usize>() * 2
700        + text.link_annotations.capacity() * size_of::<usize>() * 2
701        + text
702            .string_annotations
703            .iter()
704            .map(|annotation| {
705                annotation.item.tag.capacity() + annotation.item.annotation.capacity()
706            })
707            .sum::<usize>()
708        + text
709            .link_annotations
710            .iter()
711            .map(|annotation| match &annotation.item {
712                cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
713                cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
714            })
715            .sum::<usize>()
716}
717
718fn inflate_rect(rect: Rect, by: f32) -> Rect {
719    Rect {
720        x: rect.x - by,
721        y: rect.y - by,
722        width: rect.width + by * 2.0,
723        height: rect.height + by * 2.0,
724    }
725}
726
727fn rect_within(inner: Rect, outer: Rect) -> bool {
728    inner.x >= outer.x
729        && inner.y >= outer.y
730        && inner.x + inner.width <= outer.x + outer.width
731        && inner.y + inner.height <= outer.y + outer.height
732}
733
734#[cfg(test)]
735#[path = "tests/graph_tests.rs"]
736mod tests;