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