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 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 pub text: Rc<AnnotatedString>,
75 pub render_text: std::sync::Arc<RenderString>,
78 pub text_style: std::sync::Arc<TextStyle>,
81 pub font_size: f32,
82 pub layout_options: TextLayoutOptions,
83 pub clip: Option<Rect>,
84 pub paint: TextPaintCache,
87}
88
89impl TextPrimitiveNode {
90 pub fn paint(&self) -> TextPaint {
92 self.paint.get_or(|| {
93 TextPaint::of(
94 &self.text_style,
95 self.text_style.render_hash(),
96 self.render_text.span_styles(),
97 self.font_size,
98 )
99 })
100 }
101
102 pub fn draw_bounds(&self) -> Option<Rect> {
105 if let Some(clip) = self.clip {
106 return Some(clip);
107 }
108 let style = &self.text_style.span_style;
109 let stroked =
110 |style: &SpanStyle| matches!(style.draw_style, Some(TextDrawStyle::Stroke { .. }));
111 (!matches!(self.layout_options.overflow, TextOverflow::Visible)
112 && style.shadow.is_none()
113 && style
114 .baseline_shift
115 .is_none_or(|shift| !shift.is_specified() || shift.0 == 0.0)
116 && !stroked(style)
117 && self
118 .text
119 .span_styles
120 .iter()
121 .all(|span| !stroked(&span.item)))
122 .then_some(self.rect)
123 }
124
125 fn draws_within(&self, bounds: Rect) -> bool {
126 self.draw_bounds()
127 .is_some_and(|drawn| rect_within(drawn, bounds))
128 }
129}
130
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132pub enum PrimitivePhase {
133 BeforeChildren,
134 AfterChildren,
135}
136
137#[derive(Clone, Debug, PartialEq)]
138pub enum PrimitiveNode {
139 Draw(Box<DrawPrimitiveNode>),
140 Text(Box<TextPrimitiveNode>),
141}
142
143#[derive(Clone, Debug, PartialEq)]
144pub struct PrimitiveEntry {
145 pub phase: PrimitivePhase,
146 pub node: PrimitiveNode,
147}
148
149#[derive(Clone, Debug, Default)]
151pub struct LayerProperties(Option<Box<GraphicsLayer>>);
152
153impl LayerProperties {
154 pub(crate) fn replace(&mut self, value: Option<GraphicsLayer>) {
155 match (&mut self.0, value) {
156 (Some(current), Some(value)) => **current = value,
157 (storage, value) => *storage = value.map(Box::new),
158 }
159 }
160}
161
162impl Deref for LayerProperties {
163 type Target = GraphicsLayer;
164
165 fn deref(&self) -> &Self::Target {
166 self.0.as_deref().unwrap_or(&GraphicsLayer::DEFAULT)
167 }
168}
169
170impl DerefMut for LayerProperties {
171 fn deref_mut(&mut self) -> &mut Self::Target {
172 self.0.get_or_insert_with(Box::default)
173 }
174}
175
176impl From<GraphicsLayer> for LayerProperties {
177 fn from(value: GraphicsLayer) -> Self {
178 Self(Some(Box::new(value)))
179 }
180}
181
182impl From<Option<GraphicsLayer>> for LayerProperties {
183 fn from(value: Option<GraphicsLayer>) -> Self {
184 Self(value.map(Box::new))
185 }
186}
187
188impl PartialEq for LayerProperties {
189 fn eq(&self, other: &Self) -> bool {
190 **self == **other
191 }
192}
193
194#[derive(Clone)]
195pub struct LayerNode {
196 pub node_id: Option<NodeId>,
197 pub wraps: Option<NodeId>,
202 pub local_bounds: Rect,
205 pub node_bounds: Option<Rect>,
210 pub transform_to_parent: ProjectiveTransform,
211 pub content_offset: Point,
212 pub motion_context_animated: bool,
213 pub translated_content_context: bool,
214 pub translated_content_offset: Point,
215 pub origin_in_parent: Point,
220 pub graphics_layer: LayerProperties,
221 pub clip_to_bounds: bool,
222 pub hit_test: Option<HitTestNode>,
223 pub has_hit_targets: bool,
224 pub has_origin_sinks: bool,
229 pub draws_within_bounds: bool,
234 pub isolation: IsolationReasons,
235 pub cache_policy: CachePolicy,
236 pub cache_hashes: Cell<Option<LayerRasterCacheHashes>>,
239 pub content_changes: ContentChanges,
241 pub children: Vec<RenderNode>,
242}
243
244#[derive(Clone, Copy, Debug, Default)]
247pub struct ContentChanges {
248 last_update: u64,
249 run: u32,
250}
251
252impl ContentChanges {
253 fn note(&mut self, update: u64) {
254 if self.run > 0 && self.last_update == update {
255 return;
256 }
257 self.run = if self.run > 0 && self.last_update + 1 == update {
258 self.run + 1
259 } else {
260 1
261 };
262 self.last_update = update;
263 }
264}
265
266impl Default for LayerNode {
267 fn default() -> Self {
268 Self {
269 node_id: None,
270 wraps: None,
271 local_bounds: Rect {
272 x: 0.0,
273 y: 0.0,
274 width: 0.0,
275 height: 0.0,
276 },
277 node_bounds: None,
278 transform_to_parent: ProjectiveTransform::identity(),
279 content_offset: Point::default(),
280 motion_context_animated: false,
281 translated_content_context: false,
282 translated_content_offset: Point::default(),
283 origin_in_parent: Point::default(),
284 graphics_layer: LayerProperties::default(),
285 clip_to_bounds: false,
286 hit_test: None,
287 has_hit_targets: false,
288 has_origin_sinks: false,
289 draws_within_bounds: false,
290 isolation: IsolationReasons::default(),
291 cache_policy: CachePolicy::None,
292 cache_hashes: Cell::new(None),
293 content_changes: ContentChanges::default(),
294 children: Vec::new(),
295 }
296 }
297}
298
299pub const CONTAINED_DRAW_SLACK: f32 = 1.0;
302
303impl LayerNode {
304 pub fn painted_text(&self) -> Vec<String> {
306 let mut out = Vec::new();
307 self.collect_painted_text(&mut out);
308 out
309 }
310
311 fn collect_painted_text(&self, out: &mut Vec<String>) {
312 for child in &self.children {
313 match child {
314 RenderNode::Primitive(primitive) => {
315 if let PrimitiveNode::Text(text) = &primitive.node {
316 out.push(text.text.text.clone());
317 }
318 }
319 RenderNode::Layer(child) => child.collect_painted_text(out),
320 RenderNode::DrawRun(run) => {
321 for primitive in run.primitives() {
322 if let DrawPrimitive::Text(text) = primitive {
323 out.push(text.text.to_string());
324 }
325 }
326 }
327 }
328 }
329 }
330
331 pub fn content_draws_within_bounds(&self) -> bool {
335 if self.visual_clip_rect().is_some() {
336 return true;
337 }
338 let bounds = inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK);
339 self.children.iter().all(|child| match child {
340 RenderNode::DrawRun(run) => {
341 !run.summary.has_shadow
342 && run
343 .recording
344 .bounds()
345 .is_none_or(|drawn| rect_within(drawn, bounds))
346 }
347 RenderNode::Primitive(entry) => match &entry.node {
348 PrimitiveNode::Text(text) => text.draws_within(bounds),
349 PrimitiveNode::Draw(_) => false,
350 },
351 RenderNode::Layer(layer) => layer.draws_within_parent(bounds),
352 })
353 }
354
355 pub(crate) fn refresh_child_facts(&mut self) {
356 self.has_hit_targets = self.hit_test.is_some()
357 || self.children.iter().any(|child| match child {
358 RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
359 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
360 });
361 self.draws_within_bounds = self.content_draws_within_bounds();
362 }
363
364 fn draws_within_parent(&self, bounds: Rect) -> bool {
368 self.draws_within_bounds
369 && self.graphics_layer.shadow_elevation <= 0.0
370 && self.effect().is_none()
371 && self.backdrop().is_none()
372 && rect_within(
373 quad_bounds(
374 self.transform_to_parent
375 .map_rect(inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK)),
376 ),
377 bounds,
378 )
379 }
380
381 pub fn node_rect(&self) -> Rect {
383 self.node_bounds.unwrap_or(self.local_bounds)
384 }
385
386 pub fn clip_rect(&self) -> Option<Rect> {
387 (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
388 }
389
390 pub fn shadow_clip(&self) -> Option<Rect> {
393 self.clip_to_bounds.then_some(self.local_bounds)
394 }
395
396 pub fn visual_clip_rect(&self) -> Option<Rect> {
401 self.clip_rect().or_else(|| {
402 (self.isolation.group_opacity || self.isolation.explicit_offscreen)
403 .then_some(self.local_bounds)
404 })
405 }
406
407 pub fn effect(&self) -> Option<&RenderEffect> {
408 self.graphics_layer.render_effect.as_ref()
409 }
410
411 pub fn backdrop(&self) -> Option<&RenderEffect> {
412 self.graphics_layer.backdrop_effect.as_ref()
413 }
414
415 pub fn opacity(&self) -> f32 {
416 self.graphics_layer.alpha
417 }
418
419 pub fn blend_mode(&self) -> BlendMode {
420 self.graphics_layer.blend_mode
421 }
422
423 pub fn color_filter(&self) -> Option<ColorFilter> {
424 self.graphics_layer.color_filter
425 }
426
427 pub fn target_content_hash(&self) -> u64 {
428 self.raster_cache_hashes().target_content
429 }
430
431 pub fn motion_source_content_hash(&self) -> u64 {
432 crate::graph_hash::layer_motion_source_content_hash(self)
433 }
434
435 pub fn effect_hash(&self) -> u64 {
436 self.raster_cache_hashes().effect
437 }
438
439 fn raster_cache_hashes(&self) -> LayerRasterCacheHashes {
440 if let Some(hashes) = self.cache_hashes.get() {
441 return hashes;
442 }
443 let hashes = crate::graph_hash::layer_raster_cache_hashes(self);
444 self.cache_hashes.set(Some(hashes));
445 hashes
446 }
447
448 pub fn forget_raster_cache_hashes(&mut self) {
452 self.cache_hashes.set(None);
453 for child in &mut self.children {
454 if let RenderNode::Layer(child) = child {
455 child.forget_raster_cache_hashes();
456 }
457 }
458 }
459
460 pub(crate) fn forget_own_raster_cache_hashes(&mut self) {
463 self.cache_hashes.set(None);
464 }
465
466 pub(crate) fn note_content_change(&mut self, update: u64) {
469 self.cache_hashes.set(None);
470 self.content_changes.note(update);
471 }
472
473 pub fn content_churns(&self, update: u64) -> bool {
477 self.content_changes.run >= 2 && self.content_changes.last_update == update
478 }
479}
480
481#[derive(Clone)]
482pub enum RenderNode {
483 Primitive(PrimitiveEntry),
484 DrawRun(DrawRunNode),
491 Layer(Box<LayerNode>),
492}
493
494#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
499pub struct DrawCommandId {
500 pub node_id: NodeId,
501 pub command_index: u32,
502 pub placement: DrawPlacement,
503}
504
505#[derive(Clone, Debug, PartialEq)]
506pub struct DrawRunNode {
507 pub phase: PrimitivePhase,
508 pub command: Option<DrawCommandId>,
511 pub recording: Rc<CommandRecording>,
517 pub segments: Range<u32>,
520 pub summary: DrawRunSummary,
523}
524
525pub type DrawRunSummary = RecordingSummary;
527
528impl DrawRunNode {
529 pub fn new(phase: PrimitivePhase, primitives: Vec<DrawPrimitive>) -> Self {
530 Self::for_command(phase, None, primitives)
531 }
532
533 pub fn for_command(
534 phase: PrimitivePhase,
535 command: Option<DrawCommandId>,
536 primitives: Vec<DrawPrimitive>,
537 ) -> Self {
538 let recording = CommandRecording::from_primitives(primitives);
539 let segments = recording.all_segments();
540 Self::for_command_shared(phase, command, Rc::new(recording), segments)
541 }
542
543 pub fn for_command_shared(
544 phase: PrimitivePhase,
545 command: Option<DrawCommandId>,
546 recording: Rc<CommandRecording>,
547 segments: Range<u32>,
548 ) -> Self {
549 let summary = recording.summary_in(&segments);
550 Self {
551 phase,
552 command,
553 recording,
554 segments,
555 summary,
556 }
557 }
558
559 pub fn primitives(&self) -> impl Iterator<Item = DrawPrimitive> + '_ {
561 self.recording.primitives(self.segments.clone())
562 }
563
564 pub fn coverage_rects(&self) -> impl Iterator<Item = Rect> + '_ {
566 self.recording.coverage_rects(self.segments.clone())
567 }
568
569 pub fn len(&self) -> usize {
570 self.recording.len_in(&self.segments)
571 }
572
573 pub fn is_empty(&self) -> bool {
574 self.recording.is_empty_in(&self.segments)
575 }
576}
577
578#[derive(Clone)]
579pub struct RenderGraph {
580 pub root: LayerNode,
581 pub update: u64,
583}
584
585impl RenderGraph {
586 pub fn new(root: LayerNode) -> Self {
587 Self { root, update: 0 }
588 }
589
590 pub fn node_count(&self) -> usize {
591 fn count_layer(layer: &LayerNode) -> usize {
592 1 + layer
593 .children
594 .iter()
595 .map(|child| match child {
596 RenderNode::Primitive(_) => 1,
597 RenderNode::DrawRun(run) => run.len(),
598 RenderNode::Layer(child_layer) => count_layer(child_layer),
599 })
600 .sum::<usize>()
601 }
602
603 count_layer(&self.root)
604 }
605
606 pub fn heap_bytes(&self) -> usize {
607 layer_heap_bytes(&self.root)
608 }
609
610 pub fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) {
612 fn collect(layer: &LayerNode, nodes: &mut HashSet<NodeId>) {
613 if let Some(node_id) = layer.node_id {
614 nodes.insert(node_id);
615 }
616 for child in &layer.children {
617 match child {
618 RenderNode::DrawRun(run) => {
619 if let Some(command) = run.command {
620 nodes.insert(command.node_id);
621 }
622 }
623 RenderNode::Layer(child) => collect(child, nodes),
624 RenderNode::Primitive(_) => {}
625 }
626 }
627 }
628
629 nodes.clear();
630 collect(&self.root, nodes);
631 }
632}
633
634fn layer_heap_bytes(layer: &LayerNode) -> usize {
635 size_of::<RenderNode>() * layer.children.capacity()
636 + layer
637 .graphics_layer
638 .0
639 .as_ref()
640 .map_or(0, |_| size_of::<GraphicsLayer>())
641 + layer
642 .children
643 .iter()
644 .map(render_node_heap_bytes)
645 .sum::<usize>()
646}
647
648fn render_node_heap_bytes(node: &RenderNode) -> usize {
649 match node {
650 RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
651 RenderNode::DrawRun(run) => {
652 run.recording.pod_heap_bytes()
653 + std::mem::size_of_val(run.recording.others())
654 + run
655 .recording
656 .others()
657 .iter()
658 .map(draw_primitive_heap_bytes)
659 .sum::<usize>()
660 }
661 RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
662 }
663}
664
665fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
666 match &entry.node {
667 PrimitiveNode::Draw(draw) => {
668 size_of::<DrawPrimitiveNode>() + draw_primitive_heap_bytes(&draw.primitive)
669 }
670 PrimitiveNode::Text(text) => {
671 size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
672 }
673 }
674}
675
676fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
677 match primitive {
678 DrawPrimitive::Content
679 | DrawPrimitive::Rect { .. }
680 | DrawPrimitive::RoundRect { .. }
681 | DrawPrimitive::Arc { .. }
682 | DrawPrimitive::Line { .. }
683 | DrawPrimitive::Trapezoid { .. } => 0,
684 DrawPrimitive::Blend { primitive, .. } => {
685 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
686 }
687 DrawPrimitive::Image { .. } => 0,
688 DrawPrimitive::Text(text) => {
689 size_of::<cranpose_ui_graphics::TextPrimitive>()
690 + text.text.len()
691 + text
692 .style
693 .font_family
694 .as_ref()
695 .map_or(0, std::string::String::capacity)
696 }
697 DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
698 }
699}
700
701fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
702 match shadow {
703 ShadowPrimitive::Drop { shape, .. } => {
704 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
705 }
706 ShadowPrimitive::Inner { fill, cutout, .. } => {
707 size_of::<DrawPrimitive>() * 2
708 + draw_primitive_heap_bytes(fill)
709 + draw_primitive_heap_bytes(cutout)
710 }
711 }
712}
713
714fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
715 text.text.capacity()
716 + text.span_styles.capacity() * size_of::<usize>() * 2
717 + text.paragraph_styles.capacity() * size_of::<usize>() * 2
718 + text.string_annotations.capacity() * size_of::<usize>() * 2
719 + text.link_annotations.capacity() * size_of::<usize>() * 2
720 + text
721 .string_annotations
722 .iter()
723 .map(|annotation| {
724 annotation.item.tag.capacity() + annotation.item.annotation.capacity()
725 })
726 .sum::<usize>()
727 + text
728 .link_annotations
729 .iter()
730 .map(|annotation| match &annotation.item {
731 cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
732 cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
733 })
734 .sum::<usize>()
735}
736
737fn inflate_rect(rect: Rect, by: f32) -> Rect {
738 Rect {
739 x: rect.x - by,
740 y: rect.y - by,
741 width: rect.width + by * 2.0,
742 height: rect.height + by * 2.0,
743 }
744}
745
746fn rect_within(inner: Rect, outer: Rect) -> bool {
747 inner.x >= outer.x
748 && inner.y >= outer.y
749 && inner.x + inner.width <= outer.x + outer.width
750 && inner.y + inner.height <= outer.y + outer.height
751}
752
753#[cfg(test)]
754#[path = "tests/graph_tests.rs"]
755mod tests;