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