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)]
27pub struct IsolationReasons {
28 pub explicit_offscreen: bool,
30 pub shape_clip: bool,
32 pub effect: bool,
34 pub backdrop: bool,
36 pub group_opacity: bool,
38 pub blend_mode: bool,
40}
41
42impl IsolationReasons {
43 pub fn has_any(self) -> bool {
45 self.explicit_offscreen
46 || self.shape_clip
47 || self.effect
48 || self.backdrop
49 || self.group_opacity
50 || self.blend_mode
51 }
52}
53
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
56pub enum CachePolicy {
57 #[default]
59 None,
60 Auto,
62}
63
64#[derive(Clone)]
66pub struct HitTestNode {
67 pub handlers: Rc<ModifierNodeSlices>,
72}
73
74#[derive(Clone, Debug, PartialEq)]
76pub struct DrawPrimitiveNode {
77 pub primitive: DrawPrimitive,
79 pub clip: Option<Rect>,
81}
82
83#[derive(Clone, Debug, PartialEq)]
85pub struct TextPrimitiveNode {
86 pub node_id: NodeId,
88 pub rect: Rect,
90 pub text: Rc<AnnotatedString>,
93 pub render_text: std::sync::Arc<RenderString>,
96 pub text_style: std::sync::Arc<TextStyle>,
99 pub font_size: f32,
101 pub layout_options: TextLayoutOptions,
103 pub clip: Option<Rect>,
106 pub paint: TextPaintCache,
109}
110
111impl TextPrimitiveNode {
112 pub fn paint(&self) -> TextPaint {
114 self.paint.get_or(|| {
115 TextPaint::of(
116 &self.text_style,
117 self.text_style.render_hash(),
118 self.render_text.span_styles(),
119 self.font_size,
120 (self.render_text.render_hash(), self.render_text.is_empty()),
121 )
122 })
123 }
124
125 pub fn draw_bounds(&self) -> Option<Rect> {
128 if let Some(clip) = self.clip {
129 return Some(clip);
130 }
131 let style = &self.text_style.span_style;
132 let stroked =
133 |style: &SpanStyle| matches!(style.draw_style, Some(TextDrawStyle::Stroke { .. }));
134 (!matches!(self.layout_options.overflow, TextOverflow::Visible)
135 && style.shadow.is_none()
136 && style
137 .baseline_shift
138 .is_none_or(|shift| !shift.is_specified() || shift.0 == 0.0)
139 && !stroked(style)
140 && self
141 .text
142 .span_styles
143 .iter()
144 .all(|span| !stroked(&span.item)))
145 .then_some(self.rect)
146 }
147
148 fn draws_within(&self, bounds: Rect) -> bool {
149 self.draw_bounds()
150 .is_some_and(|drawn| rect_within(drawn, bounds))
151 }
152}
153
154#[derive(Clone, Copy, Debug, PartialEq, Eq)]
156pub enum PrimitivePhase {
157 BeforeChildren,
159 AfterChildren,
161}
162
163#[derive(Clone, Debug, PartialEq)]
165pub enum PrimitiveNode {
166 Draw(Box<DrawPrimitiveNode>),
168 Text(Box<TextPrimitiveNode>),
170}
171
172#[derive(Clone, Debug, PartialEq)]
174pub struct PrimitiveEntry {
175 pub phase: PrimitivePhase,
177 pub node: PrimitiveNode,
179}
180
181#[derive(Clone, Debug, Default)]
183pub struct LayerProperties(Option<Box<GraphicsLayer>>);
184
185impl LayerProperties {
186 pub(crate) fn replace(&mut self, value: Option<GraphicsLayer>) {
187 match (&mut self.0, value) {
188 (Some(current), Some(value)) => **current = value,
189 (storage, value) => *storage = value.map(Box::new),
190 }
191 }
192}
193
194impl Deref for LayerProperties {
195 type Target = GraphicsLayer;
196
197 fn deref(&self) -> &Self::Target {
198 self.0.as_deref().unwrap_or(&GraphicsLayer::DEFAULT)
199 }
200}
201
202impl DerefMut for LayerProperties {
203 fn deref_mut(&mut self) -> &mut Self::Target {
204 self.0.get_or_insert_with(Box::default)
205 }
206}
207
208impl From<GraphicsLayer> for LayerProperties {
209 fn from(value: GraphicsLayer) -> Self {
210 Self(Some(Box::new(value)))
211 }
212}
213
214impl From<Option<GraphicsLayer>> for LayerProperties {
215 fn from(value: Option<GraphicsLayer>) -> Self {
216 Self(value.map(Box::new))
217 }
218}
219
220impl PartialEq for LayerProperties {
221 fn eq(&self, other: &Self) -> bool {
222 **self == **other
223 }
224}
225
226#[derive(Clone)]
229pub struct LayerNode {
230 pub node_id: Option<NodeId>,
232 pub wraps: Option<NodeId>,
237 pub local_bounds: Rect,
240 pub node_bounds: Option<Rect>,
245 pub transform_to_parent: ProjectiveTransform,
248 pub content_offset: Point,
251 pub motion_context_animated: bool,
254 pub translated_content_context: bool,
257 pub translated_content_offset: Point,
259 pub origin_in_parent: Point,
264 pub graphics_layer: LayerProperties,
266 pub clip_to_bounds: bool,
268 pub hit_test: Option<HitTestNode>,
270 pub has_hit_targets: bool,
273 pub has_origin_sinks: bool,
278 pub draws_within_bounds: bool,
283 pub isolation: IsolationReasons,
285 pub cache_policy: CachePolicy,
287 pub cache_hashes: Cell<Option<LayerRasterCacheHashes>>,
290 pub content_changes: ContentChanges,
292 pub children: Vec<RenderNode>,
294}
295
296#[derive(Clone, Copy, Debug, Default)]
299pub struct ContentChanges {
300 last_update: u64,
301 run: u32,
302}
303
304impl ContentChanges {
305 fn note(&mut self, update: u64) {
306 if self.run > 0 && self.last_update == update {
307 return;
308 }
309 self.run = if self.run > 0 && self.last_update + 1 == update {
310 self.run + 1
311 } else {
312 1
313 };
314 self.last_update = update;
315 }
316}
317
318impl Default for LayerNode {
319 fn default() -> Self {
320 Self {
321 node_id: None,
322 wraps: None,
323 local_bounds: Rect {
324 x: 0.0,
325 y: 0.0,
326 width: 0.0,
327 height: 0.0,
328 },
329 node_bounds: None,
330 transform_to_parent: ProjectiveTransform::identity(),
331 content_offset: Point::default(),
332 motion_context_animated: false,
333 translated_content_context: false,
334 translated_content_offset: Point::default(),
335 origin_in_parent: Point::default(),
336 graphics_layer: LayerProperties::default(),
337 clip_to_bounds: false,
338 hit_test: None,
339 has_hit_targets: false,
340 has_origin_sinks: false,
341 draws_within_bounds: false,
342 isolation: IsolationReasons::default(),
343 cache_policy: CachePolicy::None,
344 cache_hashes: Cell::new(None),
345 content_changes: ContentChanges::default(),
346 children: Vec::new(),
347 }
348 }
349}
350
351pub const CONTAINED_DRAW_SLACK: f32 = 1.0;
354
355impl LayerNode {
356 pub fn painted_text(&self) -> Vec<String> {
358 let mut out = Vec::new();
359 self.collect_painted_text(&mut out);
360 out
361 }
362
363 fn collect_painted_text(&self, out: &mut Vec<String>) {
364 for child in &self.children {
365 match child {
366 RenderNode::Primitive(primitive) => {
367 if let PrimitiveNode::Text(text) = &primitive.node {
368 out.push(text.text.text.clone());
369 }
370 }
371 RenderNode::Layer(child) => child.collect_painted_text(out),
372 RenderNode::DrawRun(run) => {
373 for primitive in run.primitives() {
374 if let DrawPrimitive::Text(text) = primitive {
375 out.push(text.text.to_string());
376 }
377 }
378 }
379 }
380 }
381 }
382
383 pub fn content_draws_within_bounds(&self) -> bool {
387 if self.visual_clip_rect().is_some() {
388 return true;
389 }
390 let bounds = inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK);
391 self.children.iter().all(|child| match child {
392 RenderNode::DrawRun(run) => {
393 !run.summary.has_shadow
394 && run
395 .recording
396 .bounds()
397 .is_none_or(|drawn| rect_within(drawn, bounds))
398 }
399 RenderNode::Primitive(entry) => match &entry.node {
400 PrimitiveNode::Text(text) => text.draws_within(bounds),
401 PrimitiveNode::Draw(_) => false,
402 },
403 RenderNode::Layer(layer) => layer.draws_within_parent(bounds),
404 })
405 }
406
407 pub(crate) fn refresh_child_facts(&mut self) {
408 self.has_hit_targets = self.hit_test.is_some()
409 || self.children.iter().any(|child| match child {
410 RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
411 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
412 });
413 self.draws_within_bounds = self.content_draws_within_bounds();
414 }
415
416 fn draws_within_parent(&self, bounds: Rect) -> bool {
420 self.draws_within_bounds
421 && self.graphics_layer.shadow_elevation <= 0.0
422 && self.effect().is_none()
423 && self.backdrop().is_none()
424 && rect_within(
425 quad_bounds(
426 self.transform_to_parent
427 .map_rect(inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK)),
428 ),
429 bounds,
430 )
431 }
432
433 pub fn node_rect(&self) -> Rect {
435 self.node_bounds.unwrap_or(self.local_bounds)
436 }
437
438 pub fn clip_rect(&self) -> Option<Rect> {
441 (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
442 }
443
444 pub fn shadow_clip(&self) -> Option<Rect> {
447 self.clip_to_bounds.then_some(self.local_bounds)
448 }
449
450 pub fn visual_clip_rect(&self) -> Option<Rect> {
455 self.clip_rect().or_else(|| {
456 (self.isolation.group_opacity || self.isolation.explicit_offscreen)
457 .then_some(self.local_bounds)
458 })
459 }
460
461 pub fn effect(&self) -> Option<&RenderEffect> {
463 self.graphics_layer.render_effect.as_ref()
464 }
465
466 pub fn backdrop(&self) -> Option<&RenderEffect> {
468 self.graphics_layer.backdrop_effect.as_ref()
469 }
470
471 pub fn opacity(&self) -> f32 {
473 self.graphics_layer.alpha
474 }
475
476 pub fn blend_mode(&self) -> BlendMode {
478 self.graphics_layer.blend_mode
479 }
480
481 pub fn color_filter(&self) -> Option<ColorFilter> {
483 self.graphics_layer.color_filter
484 }
485
486 pub fn target_content_hash(&self) -> u64 {
489 self.raster_cache_hashes().target_content
490 }
491
492 pub fn motion_source_content_hash(&self) -> u64 {
495 crate::graph_hash::layer_motion_source_content_hash(self)
496 }
497
498 pub fn effect_hash(&self) -> u64 {
500 self.raster_cache_hashes().effect
501 }
502
503 fn raster_cache_hashes(&self) -> LayerRasterCacheHashes {
504 if let Some(hashes) = self.cache_hashes.get() {
505 return hashes;
506 }
507 let hashes = crate::graph_hash::layer_raster_cache_hashes(self);
508 self.cache_hashes.set(Some(hashes));
509 hashes
510 }
511
512 pub fn forget_raster_cache_hashes(&mut self) {
516 self.cache_hashes.set(None);
517 for child in &mut self.children {
518 if let RenderNode::Layer(child) = child {
519 child.forget_raster_cache_hashes();
520 }
521 }
522 }
523
524 pub(crate) fn forget_own_raster_cache_hashes(&mut self) {
527 self.cache_hashes.set(None);
528 }
529
530 pub(crate) fn note_content_change(&mut self, update: u64) {
533 self.cache_hashes.set(None);
534 self.content_changes.note(update);
535 }
536
537 pub fn content_churns(&self, update: u64) -> bool {
541 self.content_changes.run >= 2 && self.content_changes.last_update == update
542 }
543}
544
545#[derive(Clone)]
547pub enum RenderNode {
548 Primitive(PrimitiveEntry),
550 DrawRun(DrawRunNode),
557 Layer(Box<LayerNode>),
559}
560
561#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
566pub struct DrawCommandId {
567 pub node_id: NodeId,
569 pub command_index: u32,
571 pub placement: DrawPlacement,
573}
574
575#[derive(Clone, Debug, PartialEq)]
577pub struct DrawRunNode {
578 pub phase: PrimitivePhase,
580 pub command: Option<DrawCommandId>,
583 pub recording: Rc<CommandRecording>,
589 pub segments: Range<u32>,
592 pub summary: DrawRunSummary,
595}
596
597pub type DrawRunSummary = RecordingSummary;
599
600impl DrawRunNode {
601 pub fn new(phase: PrimitivePhase, primitives: Vec<DrawPrimitive>) -> Self {
603 Self::for_command(phase, None, primitives)
604 }
605
606 pub fn for_command(
608 phase: PrimitivePhase,
609 command: Option<DrawCommandId>,
610 primitives: Vec<DrawPrimitive>,
611 ) -> Self {
612 let recording = CommandRecording::from_primitives(primitives);
613 let segments = recording.all_segments();
614 Self::for_command_shared(phase, command, Rc::new(recording), segments)
615 }
616
617 pub fn for_command_shared(
619 phase: PrimitivePhase,
620 command: Option<DrawCommandId>,
621 recording: Rc<CommandRecording>,
622 segments: Range<u32>,
623 ) -> Self {
624 let summary = recording.summary_in(&segments);
625 Self {
626 phase,
627 command,
628 recording,
629 segments,
630 summary,
631 }
632 }
633
634 pub fn primitives(&self) -> impl Iterator<Item = DrawPrimitive> + '_ {
636 self.recording.primitives(self.segments.clone())
637 }
638
639 pub fn coverage_rects(&self) -> impl Iterator<Item = Rect> + '_ {
641 self.recording.coverage_rects(self.segments.clone())
642 }
643
644 pub fn len(&self) -> usize {
646 self.recording.len_in(&self.segments)
647 }
648
649 pub fn is_empty(&self) -> bool {
651 self.recording.is_empty_in(&self.segments)
652 }
653}
654
655#[derive(Clone)]
657pub struct RenderGraph {
658 pub root: LayerNode,
660 pub update: u64,
662}
663
664impl RenderGraph {
665 pub fn new(root: LayerNode) -> Self {
667 Self { root, update: 0 }
668 }
669
670 pub fn node_count(&self) -> usize {
673 fn count_layer(layer: &LayerNode) -> usize {
674 1 + layer
675 .children
676 .iter()
677 .map(|child| match child {
678 RenderNode::Primitive(_) => 1,
679 RenderNode::DrawRun(run) => run.len(),
680 RenderNode::Layer(child_layer) => count_layer(child_layer),
681 })
682 .sum::<usize>()
683 }
684
685 count_layer(&self.root)
686 }
687
688 pub fn heap_bytes(&self) -> usize {
690 layer_heap_bytes(&self.root)
691 }
692
693 pub fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) {
695 fn collect(layer: &LayerNode, nodes: &mut HashSet<NodeId>) {
696 if let Some(node_id) = layer.node_id {
697 nodes.insert(node_id);
698 }
699 for child in &layer.children {
700 match child {
701 RenderNode::DrawRun(run) => {
702 if let Some(command) = run.command {
703 nodes.insert(command.node_id);
704 }
705 }
706 RenderNode::Layer(child) => collect(child, nodes),
707 RenderNode::Primitive(_) => {}
708 }
709 }
710 }
711
712 nodes.clear();
713 collect(&self.root, nodes);
714 }
715}
716
717fn layer_heap_bytes(layer: &LayerNode) -> usize {
718 size_of::<RenderNode>() * layer.children.capacity()
719 + layer
720 .graphics_layer
721 .0
722 .as_ref()
723 .map_or(0, |_| size_of::<GraphicsLayer>())
724 + layer
725 .children
726 .iter()
727 .map(render_node_heap_bytes)
728 .sum::<usize>()
729}
730
731fn render_node_heap_bytes(node: &RenderNode) -> usize {
732 match node {
733 RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
734 RenderNode::DrawRun(run) => {
735 run.recording.pod_heap_bytes()
736 + std::mem::size_of_val(run.recording.others())
737 + run
738 .recording
739 .others()
740 .iter()
741 .map(draw_primitive_heap_bytes)
742 .sum::<usize>()
743 }
744 RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
745 }
746}
747
748fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
749 match &entry.node {
750 PrimitiveNode::Draw(draw) => {
751 size_of::<DrawPrimitiveNode>() + draw_primitive_heap_bytes(&draw.primitive)
752 }
753 PrimitiveNode::Text(text) => {
754 size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
755 }
756 }
757}
758
759fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
760 match primitive {
761 DrawPrimitive::Content
762 | DrawPrimitive::Rect { .. }
763 | DrawPrimitive::RoundRect { .. }
764 | DrawPrimitive::Arc { .. }
765 | DrawPrimitive::Line { .. }
766 | DrawPrimitive::Trapezoid { .. } => 0,
767 DrawPrimitive::Blend { primitive, .. } => {
768 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
769 }
770 DrawPrimitive::Image { .. } => 0,
771 DrawPrimitive::Text(text) => {
772 size_of::<cranpose_ui_graphics::TextPrimitive>()
773 + text.text.len()
774 + text
775 .style
776 .font_family
777 .as_ref()
778 .map_or(0, std::string::String::capacity)
779 }
780 DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
781 }
782}
783
784fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
785 match shadow {
786 ShadowPrimitive::Drop { shape, .. } => {
787 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
788 }
789 ShadowPrimitive::Inner { fill, cutout, .. } => {
790 size_of::<DrawPrimitive>() * 2
791 + draw_primitive_heap_bytes(fill)
792 + draw_primitive_heap_bytes(cutout)
793 }
794 }
795}
796
797fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
798 text.text.capacity()
799 + text.span_styles.capacity() * size_of::<usize>() * 2
800 + text.paragraph_styles.capacity() * size_of::<usize>() * 2
801 + text.string_annotations.capacity() * size_of::<usize>() * 2
802 + text.link_annotations.capacity() * size_of::<usize>() * 2
803 + text
804 .string_annotations
805 .iter()
806 .map(|annotation| {
807 annotation.item.tag.capacity() + annotation.item.annotation.capacity()
808 })
809 .sum::<usize>()
810 + text
811 .link_annotations
812 .iter()
813 .map(|annotation| match &annotation.item {
814 cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
815 cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
816 })
817 .sum::<usize>()
818}
819
820fn inflate_rect(rect: Rect, by: f32) -> Rect {
821 Rect {
822 x: rect.x - by,
823 y: rect.y - by,
824 width: rect.width + by * 2.0,
825 height: rect.height + by * 2.0,
826 }
827}
828
829fn rect_within(inner: Rect, outer: Rect) -> bool {
830 inner.x >= outer.x
831 && inner.y >= outer.y
832 && inner.x + inner.width <= outer.x + outer.width
833 && inner.y + inner.height <= outer.y + outer.height
834}
835
836#[cfg(test)]
837#[path = "tests/graph_tests.rs"]
838mod tests;