1use std::{
2 mem::size_of,
3 ops::{Deref, DerefMut, Range},
4 rc::Rc,
5};
6
7use cranpose_core::{NodeId, collections::map::HashSet};
8use cranpose_ui::{
9 GraphicsLayer, ModifierNodeSlices, Point, Rect, RenderEffect, RoundedCornerShape,
10 TextLayoutOptions, TextOverflow, TextStyle,
11 text::{AnnotatedString, RenderString, SpanStyle, TextDrawStyle},
12};
13pub use cranpose_ui_graphics::transform::{ProjectiveTransform, quad_bounds};
14use cranpose_ui_graphics::{
15 BlendMode, ColorFilter, CommandRecording, DrawPrimitive, RecordingSummary, ShadowPrimitive,
16};
17
18use crate::{raster_cache::LayerRasterCacheHashes, style_shared::DrawPlacement};
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21pub struct IsolationReasons {
22 pub explicit_offscreen: bool,
23 pub shape_clip: bool,
24 pub effect: bool,
25 pub backdrop: bool,
26 pub group_opacity: bool,
27 pub blend_mode: bool,
28}
29
30impl IsolationReasons {
31 pub fn has_any(self) -> bool {
32 self.explicit_offscreen
33 || self.shape_clip
34 || self.effect
35 || self.backdrop
36 || self.group_opacity
37 || self.blend_mode
38 }
39}
40
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42pub enum CachePolicy {
43 #[default]
44 None,
45 Auto,
46}
47
48#[derive(Clone)]
49pub struct HitTestNode {
50 pub shape: Option<RoundedCornerShape>,
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 shadow_clip: Option<Rect>,
204 pub hit_test: Option<HitTestNode>,
205 pub has_hit_targets: bool,
206 pub has_origin_sinks: bool,
211 pub draws_within_bounds: bool,
216 pub isolation: IsolationReasons,
217 pub cache_policy: CachePolicy,
218 pub cache_hashes: LayerRasterCacheHashes,
219 pub cache_hashes_valid: bool,
220 pub children: Vec<RenderNode>,
221}
222
223impl Default for LayerNode {
224 fn default() -> Self {
225 Self {
226 node_id: None,
227 wraps: None,
228 local_bounds: Rect {
229 x: 0.0,
230 y: 0.0,
231 width: 0.0,
232 height: 0.0,
233 },
234 node_bounds: None,
235 transform_to_parent: ProjectiveTransform::identity(),
236 content_offset: Point::default(),
237 motion_context_animated: false,
238 translated_content_context: false,
239 translated_content_offset: Point::default(),
240 origin_in_parent: Point::default(),
241 graphics_layer: LayerProperties::default(),
242 clip_to_bounds: false,
243 shadow_clip: None,
244 hit_test: None,
245 has_hit_targets: false,
246 has_origin_sinks: false,
247 draws_within_bounds: false,
248 isolation: IsolationReasons::default(),
249 cache_policy: CachePolicy::None,
250 cache_hashes: LayerRasterCacheHashes::default(),
251 cache_hashes_valid: false,
252 children: Vec::new(),
253 }
254 }
255}
256
257pub const CONTAINED_DRAW_SLACK: f32 = 1.0;
260
261impl LayerNode {
262 pub fn content_draws_within_bounds(&self) -> bool {
266 if self.visual_clip_rect().is_some() {
267 return true;
268 }
269 let bounds = inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK);
270 self.children.iter().all(|child| match child {
271 RenderNode::DrawRun(run) => {
272 !run.summary.has_shadow
273 && run
274 .recording
275 .bounds()
276 .is_none_or(|drawn| rect_within(drawn, bounds))
277 }
278 RenderNode::Primitive(entry) => match &entry.node {
279 PrimitiveNode::Text(text) => text.draws_within(bounds),
280 PrimitiveNode::Draw(_) => false,
281 },
282 RenderNode::Layer(layer) => layer.draws_within_parent(bounds),
283 })
284 }
285
286 pub(crate) fn refresh_child_facts(&mut self) {
287 self.has_hit_targets = self.hit_test.is_some()
288 || self.children.iter().any(|child| match child {
289 RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
290 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
291 });
292 self.draws_within_bounds = self.content_draws_within_bounds();
293 }
294
295 fn draws_within_parent(&self, bounds: Rect) -> bool {
299 self.draws_within_bounds
300 && self.graphics_layer.shadow_elevation <= 0.0
301 && self.effect().is_none()
302 && self.backdrop().is_none()
303 && rect_within(
304 quad_bounds(
305 self.transform_to_parent
306 .map_rect(inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK)),
307 ),
308 bounds,
309 )
310 }
311
312 pub fn node_rect(&self) -> Rect {
314 self.node_bounds.unwrap_or(self.local_bounds)
315 }
316
317 pub fn clip_rect(&self) -> Option<Rect> {
318 (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
319 }
320
321 pub fn visual_clip_rect(&self) -> Option<Rect> {
326 self.clip_rect().or_else(|| {
327 (self.isolation.group_opacity || self.isolation.explicit_offscreen)
328 .then_some(self.local_bounds)
329 })
330 }
331
332 pub fn effect(&self) -> Option<&RenderEffect> {
333 self.graphics_layer.render_effect.as_ref()
334 }
335
336 pub fn backdrop(&self) -> Option<&RenderEffect> {
337 self.graphics_layer.backdrop_effect.as_ref()
338 }
339
340 pub fn opacity(&self) -> f32 {
341 self.graphics_layer.alpha
342 }
343
344 pub fn blend_mode(&self) -> BlendMode {
345 self.graphics_layer.blend_mode
346 }
347
348 pub fn color_filter(&self) -> Option<ColorFilter> {
349 self.graphics_layer.color_filter
350 }
351
352 pub fn target_content_hash(&self) -> u64 {
353 if self.cache_hashes_valid {
354 self.cache_hashes.target_content
355 } else {
356 crate::graph_hash::layer_raster_cache_hashes(self).target_content
357 }
358 }
359
360 pub fn motion_source_content_hash(&self) -> u64 {
361 crate::graph_hash::layer_motion_source_content_hash(self)
362 }
363
364 pub fn effect_hash(&self) -> u64 {
365 if self.cache_hashes_valid {
366 self.cache_hashes.effect
367 } else {
368 crate::graph_hash::layer_raster_cache_hashes(self).effect
369 }
370 }
371
372 pub fn recompute_raster_cache_hashes(&mut self) {
373 crate::graph_hash::recompute_layer_raster_cache_hashes(self);
374 }
375}
376
377#[derive(Clone)]
378pub enum RenderNode {
379 Primitive(PrimitiveEntry),
380 DrawRun(DrawRunNode),
387 Layer(Box<LayerNode>),
388}
389
390#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
399pub struct DrawCommandId {
400 pub node_id: NodeId,
401 pub command_index: u32,
402 pub placement: DrawPlacement,
403}
404
405#[derive(Clone, Debug, PartialEq)]
406pub struct DrawRunNode {
407 pub phase: PrimitivePhase,
408 pub command: Option<DrawCommandId>,
411 pub recording: Rc<CommandRecording>,
417 pub segments: Range<u32>,
420 pub summary: DrawRunSummary,
423}
424
425pub type DrawRunSummary = RecordingSummary;
427
428impl DrawRunNode {
429 pub fn new(phase: PrimitivePhase, primitives: Vec<DrawPrimitive>) -> Self {
430 Self::for_command(phase, None, primitives)
431 }
432
433 pub fn for_command(
434 phase: PrimitivePhase,
435 command: Option<DrawCommandId>,
436 primitives: Vec<DrawPrimitive>,
437 ) -> Self {
438 let recording = CommandRecording::from_primitives(primitives);
439 let segments = recording.all_segments();
440 Self::for_command_shared(phase, command, Rc::new(recording), segments)
441 }
442
443 pub fn for_command_shared(
444 phase: PrimitivePhase,
445 command: Option<DrawCommandId>,
446 recording: Rc<CommandRecording>,
447 segments: Range<u32>,
448 ) -> Self {
449 let summary = recording.summary_in(&segments);
450 Self {
451 phase,
452 command,
453 recording,
454 segments,
455 summary,
456 }
457 }
458
459 pub fn primitives(&self) -> impl Iterator<Item = DrawPrimitive> + '_ {
461 self.recording.primitives(self.segments.clone())
462 }
463
464 pub fn coverage_rects(&self) -> impl Iterator<Item = Rect> + '_ {
466 self.recording.coverage_rects(self.segments.clone())
467 }
468
469 pub fn len(&self) -> usize {
470 self.recording.len_in(&self.segments)
471 }
472
473 pub fn is_empty(&self) -> bool {
474 self.recording.is_empty_in(&self.segments)
475 }
476}
477
478#[derive(Clone)]
479pub struct RenderGraph {
480 pub root: LayerNode,
481}
482
483impl RenderGraph {
484 pub fn new(mut root: LayerNode) -> Self {
485 root.recompute_raster_cache_hashes();
486 Self { root }
487 }
488
489 pub fn node_count(&self) -> usize {
490 fn count_layer(layer: &LayerNode) -> usize {
491 1 + layer
492 .children
493 .iter()
494 .map(|child| match child {
495 RenderNode::Primitive(_) => 1,
496 RenderNode::DrawRun(run) => run.len(),
497 RenderNode::Layer(child_layer) => count_layer(child_layer),
498 })
499 .sum::<usize>()
500 }
501
502 count_layer(&self.root)
503 }
504
505 pub fn heap_bytes(&self) -> usize {
506 layer_heap_bytes(&self.root)
507 }
508
509 pub fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) {
511 fn collect(layer: &LayerNode, nodes: &mut HashSet<NodeId>) {
512 if let Some(node_id) = layer.node_id {
513 nodes.insert(node_id);
514 }
515 for child in &layer.children {
516 match child {
517 RenderNode::DrawRun(run) => {
518 if let Some(command) = run.command {
519 nodes.insert(command.node_id);
520 }
521 }
522 RenderNode::Layer(child) => collect(child, nodes),
523 RenderNode::Primitive(_) => {}
524 }
525 }
526 }
527
528 nodes.clear();
529 collect(&self.root, nodes);
530 }
531}
532
533fn layer_heap_bytes(layer: &LayerNode) -> usize {
534 size_of::<RenderNode>() * layer.children.capacity()
535 + layer
536 .graphics_layer
537 .0
538 .as_ref()
539 .map_or(0, |_| size_of::<GraphicsLayer>())
540 + layer
541 .children
542 .iter()
543 .map(render_node_heap_bytes)
544 .sum::<usize>()
545}
546
547fn render_node_heap_bytes(node: &RenderNode) -> usize {
548 match node {
549 RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
550 RenderNode::DrawRun(run) => {
551 run.recording.pod_heap_bytes()
552 + std::mem::size_of_val(run.recording.others())
553 + run
554 .recording
555 .others()
556 .iter()
557 .map(draw_primitive_heap_bytes)
558 .sum::<usize>()
559 }
560 RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
561 }
562}
563
564fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
565 match &entry.node {
566 PrimitiveNode::Draw(draw) => {
567 size_of::<DrawPrimitiveNode>() + draw_primitive_heap_bytes(&draw.primitive)
568 }
569 PrimitiveNode::Text(text) => {
570 size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
571 }
572 }
573}
574
575fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
576 match primitive {
577 DrawPrimitive::Content
578 | DrawPrimitive::Rect { .. }
579 | DrawPrimitive::RoundRect { .. }
580 | DrawPrimitive::Arc { .. }
581 | DrawPrimitive::Line { .. } => 0,
582 DrawPrimitive::Blend { primitive, .. } => {
583 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
584 }
585 DrawPrimitive::Image { .. } => 0,
586 DrawPrimitive::Text(text) => {
587 size_of::<cranpose_ui_graphics::TextPrimitive>()
588 + text.text.len()
589 + text
590 .style
591 .font_family
592 .as_ref()
593 .map_or(0, std::string::String::capacity)
594 }
595 DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
596 }
597}
598
599fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
600 match shadow {
601 ShadowPrimitive::Drop { shape, .. } => {
602 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
603 }
604 ShadowPrimitive::Inner { fill, cutout, .. } => {
605 size_of::<DrawPrimitive>() * 2
606 + draw_primitive_heap_bytes(fill)
607 + draw_primitive_heap_bytes(cutout)
608 }
609 }
610}
611
612fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
613 text.text.capacity()
614 + text.span_styles.capacity() * size_of::<usize>() * 2
615 + text.paragraph_styles.capacity() * size_of::<usize>() * 2
616 + text.string_annotations.capacity() * size_of::<usize>() * 2
617 + text.link_annotations.capacity() * size_of::<usize>() * 2
618 + text
619 .string_annotations
620 .iter()
621 .map(|annotation| {
622 annotation.item.tag.capacity() + annotation.item.annotation.capacity()
623 })
624 .sum::<usize>()
625 + text
626 .link_annotations
627 .iter()
628 .map(|annotation| match &annotation.item {
629 cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
630 cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
631 })
632 .sum::<usize>()
633}
634
635fn inflate_rect(rect: Rect, by: f32) -> Rect {
636 Rect {
637 x: rect.x - by,
638 y: rect.y - by,
639 width: rect.width + by * 2.0,
640 height: rect.height + by * 2.0,
641 }
642}
643
644fn rect_within(inner: Rect, outer: Rect) -> bool {
645 inner.x >= outer.x
646 && inner.y >= outer.y
647 && inner.x + inner.width <= outer.x + outer.width
648 && inner.y + inner.height <= outer.y + outer.height
649}
650
651#[cfg(test)]
652#[path = "tests/graph_tests.rs"]
653mod tests;