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