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},
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 fn draws_within(&self, bounds: Rect) -> bool {
86 self.text_style.span_style.shadow.is_none()
87 && self
88 .text
89 .span_styles
90 .iter()
91 .all(|span| span.item.shadow.is_none())
92 && !matches!(self.layout_options.overflow, TextOverflow::Visible)
93 && rect_within(self.rect, bounds)
94 }
95}
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
98pub enum PrimitivePhase {
99 BeforeChildren,
100 AfterChildren,
101}
102
103#[derive(Clone, Debug, PartialEq)]
104pub enum PrimitiveNode {
105 Draw(Box<DrawPrimitiveNode>),
106 Text(Box<TextPrimitiveNode>),
107}
108
109#[derive(Clone, Debug, PartialEq)]
110pub struct PrimitiveEntry {
111 pub phase: PrimitivePhase,
112 pub node: PrimitiveNode,
113}
114
115#[derive(Clone, Debug, Default)]
117pub struct LayerProperties(Option<Box<GraphicsLayer>>);
118
119impl LayerProperties {
120 pub(crate) fn replace(&mut self, value: Option<GraphicsLayer>) {
121 match (&mut self.0, value) {
122 (Some(current), Some(value)) => **current = value,
123 (storage, value) => *storage = value.map(Box::new),
124 }
125 }
126}
127
128impl Deref for LayerProperties {
129 type Target = GraphicsLayer;
130
131 fn deref(&self) -> &Self::Target {
132 self.0.as_deref().unwrap_or(&GraphicsLayer::DEFAULT)
133 }
134}
135
136impl DerefMut for LayerProperties {
137 fn deref_mut(&mut self) -> &mut Self::Target {
138 self.0.get_or_insert_with(Box::default)
139 }
140}
141
142impl From<GraphicsLayer> for LayerProperties {
143 fn from(value: GraphicsLayer) -> Self {
144 Self(Some(Box::new(value)))
145 }
146}
147
148impl From<Option<GraphicsLayer>> for LayerProperties {
149 fn from(value: Option<GraphicsLayer>) -> Self {
150 Self(value.map(Box::new))
151 }
152}
153
154impl PartialEq for LayerProperties {
155 fn eq(&self, other: &Self) -> bool {
156 **self == **other
157 }
158}
159
160#[derive(Clone)]
161pub struct LayerNode {
162 pub node_id: Option<NodeId>,
163 pub wraps: Option<NodeId>,
168 pub local_bounds: Rect,
171 pub node_bounds: Option<Rect>,
176 pub transform_to_parent: ProjectiveTransform,
177 pub content_offset: Point,
178 pub motion_context_animated: bool,
179 pub translated_content_context: bool,
180 pub translated_content_offset: Point,
181 pub origin_in_parent: Point,
186 pub graphics_layer: LayerProperties,
187 pub clip_to_bounds: bool,
188 pub shadow_clip: Option<Rect>,
189 pub hit_test: Option<HitTestNode>,
190 pub has_hit_targets: bool,
191 pub has_origin_sinks: bool,
196 pub draws_within_bounds: bool,
201 pub isolation: IsolationReasons,
202 pub cache_policy: CachePolicy,
203 pub cache_hashes: LayerRasterCacheHashes,
204 pub cache_hashes_valid: bool,
205 pub children: Vec<RenderNode>,
206}
207
208impl Default for LayerNode {
209 fn default() -> Self {
210 Self {
211 node_id: None,
212 wraps: None,
213 local_bounds: Rect {
214 x: 0.0,
215 y: 0.0,
216 width: 0.0,
217 height: 0.0,
218 },
219 node_bounds: None,
220 transform_to_parent: ProjectiveTransform::identity(),
221 content_offset: Point::default(),
222 motion_context_animated: false,
223 translated_content_context: false,
224 translated_content_offset: Point::default(),
225 origin_in_parent: Point::default(),
226 graphics_layer: LayerProperties::default(),
227 clip_to_bounds: false,
228 shadow_clip: None,
229 hit_test: None,
230 has_hit_targets: false,
231 has_origin_sinks: false,
232 draws_within_bounds: false,
233 isolation: IsolationReasons::default(),
234 cache_policy: CachePolicy::None,
235 cache_hashes: LayerRasterCacheHashes::default(),
236 cache_hashes_valid: false,
237 children: Vec::new(),
238 }
239 }
240}
241
242pub const CONTAINED_DRAW_SLACK: f32 = 1.0;
245
246impl LayerNode {
247 pub fn content_draws_within_bounds(&self) -> bool {
251 if self.visual_clip_rect().is_some() {
252 return true;
253 }
254 let bounds = inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK);
255 self.children.iter().all(|child| match child {
256 RenderNode::DrawRun(run) => run
257 .recording
258 .bounds()
259 .is_none_or(|drawn| rect_within(drawn, bounds)),
260 RenderNode::Primitive(entry) => match &entry.node {
261 PrimitiveNode::Text(text) => text.draws_within(bounds),
262 PrimitiveNode::Draw(_) => false,
263 },
264 RenderNode::Layer(layer) => layer.draws_within_parent(bounds),
265 })
266 }
267
268 pub(crate) fn refresh_child_facts(&mut self) {
269 self.has_hit_targets = self.hit_test.is_some()
270 || self.children.iter().any(|child| match child {
271 RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
272 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
273 });
274 self.draws_within_bounds = self.content_draws_within_bounds();
275 }
276
277 fn draws_within_parent(&self, bounds: Rect) -> bool {
281 self.draws_within_bounds
282 && self.graphics_layer.shadow_elevation <= 0.0
283 && self.effect().is_none()
284 && self.backdrop().is_none()
285 && rect_within(
286 quad_bounds(self.transform_to_parent.map_rect(self.local_bounds)),
287 bounds,
288 )
289 }
290
291 pub fn node_rect(&self) -> Rect {
293 self.node_bounds.unwrap_or(self.local_bounds)
294 }
295
296 pub fn clip_rect(&self) -> Option<Rect> {
297 (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
298 }
299
300 pub fn visual_clip_rect(&self) -> Option<Rect> {
305 self.clip_rect().or_else(|| {
306 (self.isolation.group_opacity || self.isolation.explicit_offscreen)
307 .then_some(self.local_bounds)
308 })
309 }
310
311 pub fn effect(&self) -> Option<&RenderEffect> {
312 self.graphics_layer.render_effect.as_ref()
313 }
314
315 pub fn backdrop(&self) -> Option<&RenderEffect> {
316 self.graphics_layer.backdrop_effect.as_ref()
317 }
318
319 pub fn opacity(&self) -> f32 {
320 self.graphics_layer.alpha
321 }
322
323 pub fn blend_mode(&self) -> BlendMode {
324 self.graphics_layer.blend_mode
325 }
326
327 pub fn color_filter(&self) -> Option<ColorFilter> {
328 self.graphics_layer.color_filter
329 }
330
331 pub fn target_content_hash(&self) -> u64 {
332 if self.cache_hashes_valid {
333 self.cache_hashes.target_content
334 } else {
335 crate::graph_hash::layer_raster_cache_hashes(self).target_content
336 }
337 }
338
339 pub fn motion_source_content_hash(&self) -> u64 {
340 crate::graph_hash::layer_motion_source_content_hash(self)
341 }
342
343 pub fn effect_hash(&self) -> u64 {
344 if self.cache_hashes_valid {
345 self.cache_hashes.effect
346 } else {
347 crate::graph_hash::layer_raster_cache_hashes(self).effect
348 }
349 }
350
351 pub fn recompute_raster_cache_hashes(&mut self) {
352 crate::graph_hash::recompute_layer_raster_cache_hashes(self);
353 }
354}
355
356#[derive(Clone)]
357pub enum RenderNode {
358 Primitive(PrimitiveEntry),
359 DrawRun(DrawRunNode),
366 Layer(Box<LayerNode>),
367}
368
369#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
378pub struct DrawCommandId {
379 pub node_id: NodeId,
380 pub command_index: u32,
381 pub placement: DrawPlacement,
382}
383
384#[derive(Clone, Debug, PartialEq)]
385pub struct DrawRunNode {
386 pub phase: PrimitivePhase,
387 pub command: Option<DrawCommandId>,
390 pub recording: Rc<CommandRecording>,
396 pub segments: Range<u32>,
399 pub summary: DrawRunSummary,
402}
403
404pub type DrawRunSummary = RecordingSummary;
406
407impl DrawRunNode {
408 pub fn new(phase: PrimitivePhase, primitives: Vec<DrawPrimitive>) -> Self {
409 Self::for_command(phase, None, primitives)
410 }
411
412 pub fn for_command(
413 phase: PrimitivePhase,
414 command: Option<DrawCommandId>,
415 primitives: Vec<DrawPrimitive>,
416 ) -> Self {
417 let recording = CommandRecording::from_primitives(primitives);
418 let segments = recording.all_segments();
419 Self::for_command_shared(phase, command, Rc::new(recording), segments)
420 }
421
422 pub fn for_command_shared(
423 phase: PrimitivePhase,
424 command: Option<DrawCommandId>,
425 recording: Rc<CommandRecording>,
426 segments: Range<u32>,
427 ) -> Self {
428 let summary = recording.summary_in(&segments);
429 Self {
430 phase,
431 command,
432 recording,
433 segments,
434 summary,
435 }
436 }
437
438 pub fn primitives(&self) -> impl Iterator<Item = DrawPrimitive> + '_ {
440 self.recording.primitives(self.segments.clone())
441 }
442
443 pub fn coverage_rects(&self) -> impl Iterator<Item = Rect> + '_ {
445 self.recording.coverage_rects(self.segments.clone())
446 }
447
448 pub fn len(&self) -> usize {
449 self.recording.len_in(&self.segments)
450 }
451
452 pub fn is_empty(&self) -> bool {
453 self.recording.is_empty_in(&self.segments)
454 }
455}
456
457#[derive(Clone)]
458pub struct RenderGraph {
459 pub root: LayerNode,
460}
461
462impl RenderGraph {
463 pub fn new(mut root: LayerNode) -> Self {
464 root.recompute_raster_cache_hashes();
465 Self { root }
466 }
467
468 pub fn node_count(&self) -> usize {
469 fn count_layer(layer: &LayerNode) -> usize {
470 1 + layer
471 .children
472 .iter()
473 .map(|child| match child {
474 RenderNode::Primitive(_) => 1,
475 RenderNode::DrawRun(run) => run.len(),
476 RenderNode::Layer(child_layer) => count_layer(child_layer),
477 })
478 .sum::<usize>()
479 }
480
481 count_layer(&self.root)
482 }
483
484 pub fn heap_bytes(&self) -> usize {
485 layer_heap_bytes(&self.root)
486 }
487
488 pub fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) {
490 fn collect(layer: &LayerNode, nodes: &mut HashSet<NodeId>) {
491 if let Some(node_id) = layer.node_id {
492 nodes.insert(node_id);
493 }
494 for child in &layer.children {
495 match child {
496 RenderNode::DrawRun(run) => {
497 if let Some(command) = run.command {
498 nodes.insert(command.node_id);
499 }
500 }
501 RenderNode::Layer(child) => collect(child, nodes),
502 RenderNode::Primitive(_) => {}
503 }
504 }
505 }
506
507 nodes.clear();
508 collect(&self.root, nodes);
509 }
510}
511
512fn layer_heap_bytes(layer: &LayerNode) -> usize {
513 size_of::<RenderNode>() * layer.children.capacity()
514 + layer
515 .graphics_layer
516 .0
517 .as_ref()
518 .map_or(0, |_| size_of::<GraphicsLayer>())
519 + layer
520 .children
521 .iter()
522 .map(render_node_heap_bytes)
523 .sum::<usize>()
524}
525
526fn render_node_heap_bytes(node: &RenderNode) -> usize {
527 match node {
528 RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
529 RenderNode::DrawRun(run) => {
530 run.recording.pod_heap_bytes()
531 + std::mem::size_of_val(run.recording.others())
532 + run
533 .recording
534 .others()
535 .iter()
536 .map(draw_primitive_heap_bytes)
537 .sum::<usize>()
538 }
539 RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
540 }
541}
542
543fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
544 match &entry.node {
545 PrimitiveNode::Draw(draw) => {
546 size_of::<DrawPrimitiveNode>() + draw_primitive_heap_bytes(&draw.primitive)
547 }
548 PrimitiveNode::Text(text) => {
549 size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
550 }
551 }
552}
553
554fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
555 match primitive {
556 DrawPrimitive::Content
557 | DrawPrimitive::Rect { .. }
558 | DrawPrimitive::RoundRect { .. }
559 | DrawPrimitive::Arc { .. }
560 | DrawPrimitive::Line { .. } => 0,
561 DrawPrimitive::Blend { primitive, .. } => {
562 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
563 }
564 DrawPrimitive::Image { .. } => 0,
565 DrawPrimitive::Text(text) => {
566 size_of::<cranpose_ui_graphics::TextPrimitive>()
567 + text.text.len()
568 + text
569 .style
570 .font_family
571 .as_ref()
572 .map_or(0, std::string::String::capacity)
573 }
574 DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
575 }
576}
577
578fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
579 match shadow {
580 ShadowPrimitive::Drop { shape, .. } => {
581 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
582 }
583 ShadowPrimitive::Inner { fill, cutout, .. } => {
584 size_of::<DrawPrimitive>() * 2
585 + draw_primitive_heap_bytes(fill)
586 + draw_primitive_heap_bytes(cutout)
587 }
588 }
589}
590
591fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
592 text.text.capacity()
593 + text.span_styles.capacity() * size_of::<usize>() * 2
594 + text.paragraph_styles.capacity() * size_of::<usize>() * 2
595 + text.string_annotations.capacity() * size_of::<usize>() * 2
596 + text.link_annotations.capacity() * size_of::<usize>() * 2
597 + text
598 .string_annotations
599 .iter()
600 .map(|annotation| {
601 annotation.item.tag.capacity() + annotation.item.annotation.capacity()
602 })
603 .sum::<usize>()
604 + text
605 .link_annotations
606 .iter()
607 .map(|annotation| match &annotation.item {
608 cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
609 cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
610 })
611 .sum::<usize>()
612}
613
614fn inflate_rect(rect: Rect, by: f32) -> Rect {
615 Rect {
616 x: rect.x - by,
617 y: rect.y - by,
618 width: rect.width + by * 2.0,
619 height: rect.height + by * 2.0,
620 }
621}
622
623fn rect_within(inner: Rect, outer: Rect) -> bool {
624 inner.x >= outer.x
625 && inner.y >= outer.y
626 && inner.x + inner.width <= outer.x + outer.width
627 && inner.y + inner.height <= outer.y + outer.height
628}
629
630#[cfg(test)]
631#[path = "tests/graph_tests.rs"]
632mod tests;