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.render_text.span_styles(),
96 self.font_size,
97 )
98 })
99 }
100
101 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#[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 pub wraps: Option<NodeId>,
201 pub local_bounds: Rect,
204 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 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 pub has_origin_sinks: bool,
228 pub draws_within_bounds: bool,
233 pub isolation: IsolationReasons,
234 pub cache_policy: CachePolicy,
235 pub cache_hashes: Cell<Option<LayerRasterCacheHashes>>,
238 pub content_changes: ContentChanges,
240 pub children: Vec<RenderNode>,
241}
242
243#[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
298pub const CONTAINED_DRAW_SLACK: f32 = 1.0;
301
302impl LayerNode {
303 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 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 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 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 pub fn shadow_clip(&self) -> Option<Rect> {
392 self.clip_to_bounds.then_some(self.local_bounds)
393 }
394
395 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 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 pub(crate) fn forget_own_raster_cache_hashes(&mut self) {
462 self.cache_hashes.set(None);
463 }
464
465 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 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 DrawRun(DrawRunNode),
490 Layer(Box<LayerNode>),
491}
492
493#[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 pub command: Option<DrawCommandId>,
510 pub recording: Rc<CommandRecording>,
516 pub segments: Range<u32>,
519 pub summary: DrawRunSummary,
522}
523
524pub 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 pub fn primitives(&self) -> impl Iterator<Item = DrawPrimitive> + '_ {
560 self.recording.primitives(self.segments.clone())
561 }
562
563 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 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 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;