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