1use std::{
2 cell::Cell,
3 mem::size_of,
4 ops::{Deref, DerefMut, Range},
5 rc::Rc,
6 sync::atomic::{AtomicU32, Ordering},
7};
8
9use cranpose_core::{NodeId, collections::map::HashSet};
10use cranpose_ui::{
11 GraphicsLayer, ModifierNodeSlices, Point, Rect, RenderEffect, TextLayoutOptions, TextOverflow,
12 TextStyle,
13 text::{AnnotatedString, RenderString, SpanStyle, TextDrawStyle},
14};
15pub use cranpose_ui_graphics::transform::{ProjectiveTransform, quad_bounds};
16use cranpose_ui_graphics::{
17 BlendMode, ColorFilter, CommandRecording, DrawPrimitive, RecordingSummary, ShadowPrimitive,
18};
19
20use crate::{
21 raster_cache::LayerRasterCacheHashes,
22 style_shared::DrawPlacement,
23 text_paint::{TextPaint, TextPaintCache},
24};
25
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
28pub struct IsolationReasons {
29 pub explicit_offscreen: bool,
31 pub shape_clip: bool,
33 pub effect: bool,
35 pub backdrop: bool,
37 pub group_opacity: bool,
39 pub blend_mode: bool,
41}
42
43impl IsolationReasons {
44 pub fn has_any(self) -> bool {
46 self.explicit_offscreen
47 || self.shape_clip
48 || self.effect
49 || self.backdrop
50 || self.group_opacity
51 || self.blend_mode
52 }
53}
54
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
57pub enum CachePolicy {
58 #[default]
60 None,
61 Auto,
63}
64
65#[derive(Clone)]
67pub struct HitTestNode {
68 pub handlers: Rc<ModifierNodeSlices>,
73}
74
75#[derive(Clone, Debug, PartialEq)]
77pub struct DrawPrimitiveNode {
78 pub primitive: DrawPrimitive,
80 pub clip: Option<Rect>,
82}
83
84#[derive(Clone, Debug, PartialEq)]
86pub struct TextPrimitiveNode {
87 pub node_id: NodeId,
89 pub rect: Rect,
91 pub text: Rc<AnnotatedString>,
94 pub render_text: std::sync::Arc<RenderString>,
97 pub text_style: std::sync::Arc<TextStyle>,
100 pub font_size: f32,
102 pub layout_options: TextLayoutOptions,
104 pub clip: Option<Rect>,
107 pub paint: TextPaintCache,
110}
111
112impl TextPrimitiveNode {
113 pub fn paint(&self) -> TextPaint {
115 self.paint.get_or(|| {
116 TextPaint::of(
117 &self.text_style,
118 self.text_style.render_hash(),
119 self.render_text.span_styles(),
120 self.font_size,
121 (self.render_text.render_hash(), self.render_text.is_empty()),
122 )
123 })
124 }
125
126 pub fn draw_bounds(&self) -> Option<Rect> {
129 if let Some(clip) = self.clip {
130 return Some(clip);
131 }
132 let style = &self.text_style.span_style;
133 let stroked =
134 |style: &SpanStyle| matches!(style.draw_style, Some(TextDrawStyle::Stroke { .. }));
135 (!matches!(self.layout_options.overflow, TextOverflow::Visible)
136 && style.shadow.is_none()
137 && style
138 .baseline_shift
139 .is_none_or(|shift| !shift.is_specified() || shift.0 == 0.0)
140 && !stroked(style)
141 && self
142 .text
143 .span_styles
144 .iter()
145 .all(|span| !stroked(&span.item)))
146 .then_some(self.rect)
147 }
148
149 fn draws_within(&self, bounds: Rect) -> bool {
150 self.draw_bounds()
151 .is_some_and(|drawn| rect_within(drawn, bounds))
152 }
153}
154
155#[derive(Clone, Copy, Debug, PartialEq, Eq)]
157pub enum PrimitivePhase {
158 BeforeChildren,
160 AfterChildren,
162}
163
164#[derive(Clone, Debug, PartialEq)]
166pub enum PrimitiveNode {
167 Draw(Box<DrawPrimitiveNode>),
169 Text(Box<TextPrimitiveNode>),
171}
172
173#[derive(Clone, Debug, PartialEq)]
175pub struct PrimitiveEntry {
176 pub phase: PrimitivePhase,
178 pub node: PrimitiveNode,
180}
181
182#[derive(Clone, Debug, Default)]
184pub struct LayerProperties(Option<Box<GraphicsLayer>>);
185
186impl LayerProperties {
187 pub(crate) fn replace(&mut self, value: Option<GraphicsLayer>) {
188 match (&mut self.0, value) {
189 (Some(current), Some(value)) => **current = value,
190 (storage, value) => *storage = value.map(Box::new),
191 }
192 }
193}
194
195impl Deref for LayerProperties {
196 type Target = GraphicsLayer;
197
198 fn deref(&self) -> &Self::Target {
199 self.0.as_deref().unwrap_or(&GraphicsLayer::DEFAULT)
200 }
201}
202
203impl DerefMut for LayerProperties {
204 fn deref_mut(&mut self) -> &mut Self::Target {
205 self.0.get_or_insert_with(Box::default)
206 }
207}
208
209impl From<GraphicsLayer> for LayerProperties {
210 fn from(value: GraphicsLayer) -> Self {
211 Self(Some(Box::new(value)))
212 }
213}
214
215impl From<Option<GraphicsLayer>> for LayerProperties {
216 fn from(value: Option<GraphicsLayer>) -> Self {
217 Self(value.map(Box::new))
218 }
219}
220
221impl PartialEq for LayerProperties {
222 fn eq(&self, other: &Self) -> bool {
223 **self == **other
224 }
225}
226
227#[derive(Clone)]
230pub struct LayerNode {
231 pub node_id: Option<NodeId>,
233 pub wraps: Option<NodeId>,
238 pub local_bounds: Rect,
241 pub node_bounds: Option<Rect>,
246 pub transform_to_parent: ProjectiveTransform,
249 pub content_offset: Point,
252 pub motion_context_animated: bool,
255 pub translated_content_context: bool,
258 pub translated_content_offset: Point,
260 pub origin_in_parent: Point,
265 pub graphics_layer: LayerProperties,
267 pub clip_to_bounds: bool,
269 pub hit_test: Option<HitTestNode>,
271 pub has_hit_targets: bool,
274 pub has_origin_sinks: bool,
279 pub draws_within_bounds: bool,
284 pub isolation: IsolationReasons,
286 pub cache_policy: CachePolicy,
288 pub cache_hashes: Cell<Option<LayerRasterCacheHashes>>,
291 pub content_changes: ContentChanges,
293 pub change: LayerChange,
295 pub children: Vec<RenderNode>,
297}
298
299#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
304pub struct LayerChange(u32);
305
306impl LayerChange {
307 fn next() -> Self {
309 static LAST: AtomicU32 = AtomicU32::new(0);
310 Self(LAST.fetch_add(1, Ordering::Relaxed).wrapping_add(1))
311 }
312}
313
314impl Default for LayerChange {
315 fn default() -> Self {
316 Self::next()
317 }
318}
319
320#[derive(Clone, Copy, Debug, Default)]
323pub struct ContentChanges {
324 last_update: u64,
325 run: u32,
326}
327
328impl ContentChanges {
329 fn note(&mut self, update: u64) {
330 if self.run > 0 && self.last_update == update {
331 return;
332 }
333 self.run = if self.run > 0 && self.last_update + 1 == update {
334 self.run + 1
335 } else {
336 1
337 };
338 self.last_update = update;
339 }
340}
341
342impl Default for LayerNode {
343 fn default() -> Self {
344 Self {
345 node_id: None,
346 wraps: None,
347 local_bounds: Rect {
348 x: 0.0,
349 y: 0.0,
350 width: 0.0,
351 height: 0.0,
352 },
353 node_bounds: None,
354 transform_to_parent: ProjectiveTransform::identity(),
355 content_offset: Point::default(),
356 motion_context_animated: false,
357 translated_content_context: false,
358 translated_content_offset: Point::default(),
359 origin_in_parent: Point::default(),
360 graphics_layer: LayerProperties::default(),
361 clip_to_bounds: false,
362 hit_test: None,
363 has_hit_targets: false,
364 has_origin_sinks: false,
365 draws_within_bounds: false,
366 isolation: IsolationReasons::default(),
367 cache_policy: CachePolicy::None,
368 cache_hashes: Cell::new(None),
369 content_changes: ContentChanges::default(),
370 change: LayerChange::default(),
371 children: Vec::new(),
372 }
373 }
374}
375
376pub const CONTAINED_DRAW_SLACK: f32 = 1.0;
379
380impl LayerNode {
381 pub fn painted_text(&self) -> Vec<String> {
383 let mut out = Vec::new();
384 self.collect_painted_text(&mut out);
385 out
386 }
387
388 fn collect_painted_text(&self, out: &mut Vec<String>) {
389 for child in &self.children {
390 match child {
391 RenderNode::Primitive(primitive) => {
392 if let PrimitiveNode::Text(text) = &primitive.node {
393 out.push(text.text.text.clone());
394 }
395 }
396 RenderNode::Layer(child) => child.collect_painted_text(out),
397 RenderNode::DrawRun(run) => {
398 for primitive in run.primitives() {
399 if let DrawPrimitive::Text(text) = primitive {
400 out.push(text.text.to_string());
401 }
402 }
403 }
404 }
405 }
406 }
407
408 pub fn content_draws_within_bounds(&self) -> bool {
412 if self.visual_clip_rect().is_some() {
413 return true;
414 }
415 let bounds = inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK);
416 self.children.iter().all(|child| match child {
417 RenderNode::DrawRun(run) => {
418 !run.summary.has_shadow
419 && run
420 .recording
421 .bounds()
422 .is_none_or(|drawn| rect_within(drawn, bounds))
423 }
424 RenderNode::Primitive(entry) => match &entry.node {
425 PrimitiveNode::Text(text) => text.draws_within(bounds),
426 PrimitiveNode::Draw(_) => false,
427 },
428 RenderNode::Layer(layer) => layer.draws_within_parent(bounds),
429 })
430 }
431
432 pub(crate) fn refresh_child_facts(&mut self) {
433 self.has_hit_targets = self.hit_test.is_some()
434 || self.children.iter().any(|child| match child {
435 RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
436 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
437 });
438 self.draws_within_bounds = self.content_draws_within_bounds();
439 }
440
441 fn draws_within_parent(&self, bounds: Rect) -> bool {
445 self.draws_within_bounds
446 && self.graphics_layer.shadow_elevation <= 0.0
447 && self.effect().is_none()
448 && self.backdrop().is_none()
449 && rect_within(
450 quad_bounds(
451 self.transform_to_parent
452 .map_rect(inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK)),
453 ),
454 bounds,
455 )
456 }
457
458 pub fn node_rect(&self) -> Rect {
460 self.node_bounds.unwrap_or(self.local_bounds)
461 }
462
463 pub fn clip_rect(&self) -> Option<Rect> {
466 (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
467 }
468
469 pub fn shadow_clip(&self) -> Option<Rect> {
472 self.clip_to_bounds.then_some(self.local_bounds)
473 }
474
475 pub fn visual_clip_rect(&self) -> Option<Rect> {
480 self.clip_rect().or_else(|| {
481 (self.isolation.group_opacity || self.isolation.explicit_offscreen)
482 .then_some(self.local_bounds)
483 })
484 }
485
486 pub fn effect(&self) -> Option<&RenderEffect> {
488 self.graphics_layer.render_effect.as_ref()
489 }
490
491 pub fn backdrop(&self) -> Option<&RenderEffect> {
493 self.graphics_layer.backdrop_effect.as_ref()
494 }
495
496 pub fn opacity(&self) -> f32 {
498 self.graphics_layer.alpha
499 }
500
501 pub fn blend_mode(&self) -> BlendMode {
503 self.graphics_layer.blend_mode
504 }
505
506 pub fn color_filter(&self) -> Option<&ColorFilter> {
508 self.graphics_layer.color_filter.as_ref()
509 }
510
511 pub fn target_content_hash(&self) -> u64 {
514 self.raster_cache_hashes().target_content
515 }
516
517 pub fn motion_source_content_hash(&self) -> u64 {
520 crate::graph_hash::layer_motion_source_content_hash(self)
521 }
522
523 pub fn effect_hash(&self) -> u64 {
525 self.raster_cache_hashes().effect
526 }
527
528 fn raster_cache_hashes(&self) -> LayerRasterCacheHashes {
529 if let Some(hashes) = self.cache_hashes.get() {
530 return hashes;
531 }
532 let hashes = crate::graph_hash::layer_raster_cache_hashes(self);
533 self.cache_hashes.set(Some(hashes));
534 hashes
535 }
536
537 pub fn forget_raster_cache_hashes(&mut self) {
541 self.forget_own_raster_cache_hashes();
542 for child in &mut self.children {
543 if let RenderNode::Layer(child) = child {
544 child.forget_raster_cache_hashes();
545 }
546 }
547 }
548
549 pub(crate) fn forget_own_raster_cache_hashes(&mut self) {
552 self.cache_hashes.set(None);
553 self.change = LayerChange::next();
554 }
555
556 pub(crate) fn note_content_change(&mut self, update: u64) {
559 self.forget_own_raster_cache_hashes();
560 self.content_changes.note(update);
561 }
562
563 pub fn content_churns(&self, update: u64) -> bool {
567 self.content_changes.run >= 2 && self.content_changes.last_update == update
568 }
569}
570
571#[derive(Clone)]
573pub enum RenderNode {
574 Primitive(PrimitiveEntry),
576 DrawRun(DrawRunNode),
583 Layer(Box<LayerNode>),
585}
586
587#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
592pub struct DrawCommandId {
593 pub node_id: NodeId,
595 pub command_index: u32,
597 pub placement: DrawPlacement,
599}
600
601#[derive(Clone, Debug, PartialEq)]
603pub struct DrawRunNode {
604 pub phase: PrimitivePhase,
606 pub command: Option<DrawCommandId>,
609 pub recording: Rc<CommandRecording>,
615 pub segments: Range<u32>,
618 pub summary: DrawRunSummary,
621}
622
623pub type DrawRunSummary = RecordingSummary;
625
626impl DrawRunNode {
627 pub fn new(phase: PrimitivePhase, primitives: Vec<DrawPrimitive>) -> Self {
629 Self::for_command(phase, None, primitives)
630 }
631
632 pub fn for_command(
634 phase: PrimitivePhase,
635 command: Option<DrawCommandId>,
636 primitives: Vec<DrawPrimitive>,
637 ) -> Self {
638 let recording = CommandRecording::from_primitives(primitives);
639 let segments = recording.all_segments();
640 Self::for_command_shared(phase, command, Rc::new(recording), segments)
641 }
642
643 pub fn for_command_shared(
645 phase: PrimitivePhase,
646 command: Option<DrawCommandId>,
647 recording: Rc<CommandRecording>,
648 segments: Range<u32>,
649 ) -> Self {
650 let summary = recording.summary_in(&segments);
651 Self {
652 phase,
653 command,
654 recording,
655 segments,
656 summary,
657 }
658 }
659
660 pub fn primitives(&self) -> impl Iterator<Item = DrawPrimitive> + '_ {
662 self.recording.primitives(self.segments.clone())
663 }
664
665 pub fn coverage_rects(&self) -> impl Iterator<Item = Rect> + '_ {
667 self.recording.coverage_rects(self.segments.clone())
668 }
669
670 pub fn len(&self) -> usize {
672 self.recording.len_in(&self.segments)
673 }
674
675 pub fn is_empty(&self) -> bool {
677 self.recording.is_empty_in(&self.segments)
678 }
679}
680
681#[derive(Clone)]
683pub struct RenderGraph {
684 pub root: LayerNode,
686 pub update: u64,
688}
689
690impl RenderGraph {
691 pub fn new(root: LayerNode) -> Self {
693 Self { root, update: 0 }
694 }
695
696 pub fn node_count(&self) -> usize {
699 fn count_layer(layer: &LayerNode) -> usize {
700 1 + layer
701 .children
702 .iter()
703 .map(|child| match child {
704 RenderNode::Primitive(_) => 1,
705 RenderNode::DrawRun(run) => run.len(),
706 RenderNode::Layer(child_layer) => count_layer(child_layer),
707 })
708 .sum::<usize>()
709 }
710
711 count_layer(&self.root)
712 }
713
714 pub fn heap_bytes(&self) -> usize {
716 layer_heap_bytes(&self.root)
717 }
718
719 pub fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) {
721 fn collect(layer: &LayerNode, nodes: &mut HashSet<NodeId>) {
722 if let Some(node_id) = layer.node_id {
723 nodes.insert(node_id);
724 }
725 for child in &layer.children {
726 match child {
727 RenderNode::DrawRun(run) => {
728 if let Some(command) = run.command {
729 nodes.insert(command.node_id);
730 }
731 }
732 RenderNode::Layer(child) => collect(child, nodes),
733 RenderNode::Primitive(_) => {}
734 }
735 }
736 }
737
738 nodes.clear();
739 collect(&self.root, nodes);
740 }
741}
742
743fn layer_heap_bytes(layer: &LayerNode) -> usize {
744 size_of::<RenderNode>() * layer.children.capacity()
745 + layer
746 .graphics_layer
747 .0
748 .as_ref()
749 .map_or(0, |_| size_of::<GraphicsLayer>())
750 + layer
751 .children
752 .iter()
753 .map(render_node_heap_bytes)
754 .sum::<usize>()
755}
756
757fn render_node_heap_bytes(node: &RenderNode) -> usize {
758 match node {
759 RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
760 RenderNode::DrawRun(run) => {
761 run.recording.pod_heap_bytes()
762 + std::mem::size_of_val(run.recording.others())
763 + run
764 .recording
765 .others()
766 .iter()
767 .map(draw_primitive_heap_bytes)
768 .sum::<usize>()
769 }
770 RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
771 }
772}
773
774fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
775 match &entry.node {
776 PrimitiveNode::Draw(draw) => {
777 size_of::<DrawPrimitiveNode>() + draw_primitive_heap_bytes(&draw.primitive)
778 }
779 PrimitiveNode::Text(text) => {
780 size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
781 }
782 }
783}
784
785fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
786 match primitive {
787 DrawPrimitive::Content
788 | DrawPrimitive::Rect { .. }
789 | DrawPrimitive::RoundRect { .. }
790 | DrawPrimitive::Arc { .. }
791 | DrawPrimitive::Line { .. }
792 | DrawPrimitive::Trapezoid { .. } => 0,
793 DrawPrimitive::Blend { primitive, .. } => {
794 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
795 }
796 DrawPrimitive::Image { .. } => 0,
797 DrawPrimitive::Text(text) => {
798 size_of::<cranpose_ui_graphics::TextPrimitive>()
799 + text.text.len()
800 + text
801 .style
802 .font_family
803 .as_ref()
804 .map_or(0, std::string::String::capacity)
805 }
806 DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
807 }
808}
809
810fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
811 match shadow {
812 ShadowPrimitive::Drop { shape, .. } => {
813 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
814 }
815 ShadowPrimitive::Inner { fill, cutout, .. } => {
816 size_of::<DrawPrimitive>() * 2
817 + draw_primitive_heap_bytes(fill)
818 + draw_primitive_heap_bytes(cutout)
819 }
820 }
821}
822
823fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
824 text.text.capacity()
825 + text.span_styles.capacity() * size_of::<usize>() * 2
826 + text.paragraph_styles.capacity() * size_of::<usize>() * 2
827 + text.string_annotations.capacity() * size_of::<usize>() * 2
828 + text.link_annotations.capacity() * size_of::<usize>() * 2
829 + text
830 .string_annotations
831 .iter()
832 .map(|annotation| {
833 annotation.item.tag.capacity() + annotation.item.annotation.capacity()
834 })
835 .sum::<usize>()
836 + text
837 .link_annotations
838 .iter()
839 .map(|annotation| match &annotation.item {
840 cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
841 cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
842 })
843 .sum::<usize>()
844}
845
846fn inflate_rect(rect: Rect, by: f32) -> Rect {
847 Rect {
848 x: rect.x - by,
849 y: rect.y - by,
850 width: rect.width + by * 2.0,
851 height: rect.height + by * 2.0,
852 }
853}
854
855fn rect_within(inner: Rect, outer: Rect) -> bool {
856 inner.x >= outer.x
857 && inner.y >= outer.y
858 && inner.x + inner.width <= outer.x + outer.width
859 && inner.y + inner.height <= outer.y + outer.height
860}
861
862#[cfg(test)]
863#[path = "tests/graph_tests.rs"]
864mod tests;