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