1use std::{mem::size_of, ops::Range, rc::Rc};
2
3use cranpose_core::{NodeId, collections::map::HashSet};
4use cranpose_ui::{
5 GraphicsLayer, ModifierNodeSlices, Point, Rect, RenderEffect, RoundedCornerShape,
6 TextLayoutOptions, TextOverflow, TextStyle,
7 text::{AnnotatedString, RenderString},
8};
9pub use cranpose_ui_graphics::transform::{ProjectiveTransform, quad_bounds};
10use cranpose_ui_graphics::{
11 BlendMode, ColorFilter, CommandRecording, DrawPrimitive, RecordingSummary, ShadowPrimitive,
12};
13
14use crate::{raster_cache::LayerRasterCacheHashes, style_shared::DrawPlacement};
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
17pub struct IsolationReasons {
18 pub explicit_offscreen: bool,
19 pub shape_clip: bool,
20 pub effect: bool,
21 pub backdrop: bool,
22 pub group_opacity: bool,
23 pub blend_mode: bool,
24}
25
26impl IsolationReasons {
27 pub fn has_any(self) -> bool {
28 self.explicit_offscreen
29 || self.shape_clip
30 || self.effect
31 || self.backdrop
32 || self.group_opacity
33 || self.blend_mode
34 }
35}
36
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub enum CachePolicy {
39 #[default]
40 None,
41 Auto,
42}
43
44#[derive(Clone)]
45pub struct HitTestNode {
46 pub shape: Option<RoundedCornerShape>,
47 pub handlers: Rc<ModifierNodeSlices>,
52}
53
54#[derive(Clone, Debug, PartialEq)]
55pub struct DrawPrimitiveNode {
56 pub primitive: DrawPrimitive,
57 pub clip: Option<Rect>,
58}
59
60#[derive(Clone, Debug, PartialEq)]
61pub struct TextPrimitiveNode {
62 pub node_id: NodeId,
63 pub rect: Rect,
64 pub text: Rc<AnnotatedString>,
67 pub render_text: std::sync::Arc<RenderString>,
70 pub text_style: std::sync::Arc<TextStyle>,
73 pub font_size: f32,
74 pub layout_options: TextLayoutOptions,
75 pub clip: Option<Rect>,
76}
77
78impl TextPrimitiveNode {
79 fn draws_within(&self, bounds: Rect) -> bool {
82 self.text_style.span_style.shadow.is_none()
83 && self
84 .text
85 .span_styles
86 .iter()
87 .all(|span| span.item.shadow.is_none())
88 && !matches!(self.layout_options.overflow, TextOverflow::Visible)
89 && rect_within(self.rect, bounds)
90 }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum PrimitivePhase {
95 BeforeChildren,
96 AfterChildren,
97}
98
99#[derive(Clone, Debug, PartialEq)]
100pub enum PrimitiveNode {
101 Draw(Box<DrawPrimitiveNode>),
102 Text(Box<TextPrimitiveNode>),
103}
104
105#[derive(Clone, Debug, PartialEq)]
106pub struct PrimitiveEntry {
107 pub phase: PrimitivePhase,
108 pub node: PrimitiveNode,
109}
110
111#[derive(Clone)]
112pub struct LayerNode {
113 pub node_id: Option<NodeId>,
114 pub wraps: Option<NodeId>,
119 pub local_bounds: Rect,
122 pub node_bounds: Option<Rect>,
127 pub transform_to_parent: ProjectiveTransform,
128 pub content_offset: Point,
129 pub motion_context_animated: bool,
130 pub translated_content_context: bool,
131 pub translated_content_offset: Point,
132 pub origin_in_parent: Point,
137 pub graphics_layer: GraphicsLayer,
138 pub clip_to_bounds: bool,
139 pub shadow_clip: Option<Rect>,
140 pub hit_test: Option<HitTestNode>,
141 pub has_hit_targets: bool,
142 pub has_origin_sinks: bool,
147 pub draws_within_bounds: bool,
152 pub isolation: IsolationReasons,
153 pub cache_policy: CachePolicy,
154 pub cache_hashes: LayerRasterCacheHashes,
155 pub cache_hashes_valid: bool,
156 pub children: Vec<RenderNode>,
157}
158
159impl Default for LayerNode {
160 fn default() -> Self {
161 Self {
162 node_id: None,
163 wraps: None,
164 local_bounds: Rect {
165 x: 0.0,
166 y: 0.0,
167 width: 0.0,
168 height: 0.0,
169 },
170 node_bounds: None,
171 transform_to_parent: ProjectiveTransform::identity(),
172 content_offset: Point::default(),
173 motion_context_animated: false,
174 translated_content_context: false,
175 translated_content_offset: Point::default(),
176 origin_in_parent: Point::default(),
177 graphics_layer: GraphicsLayer::default(),
178 clip_to_bounds: false,
179 shadow_clip: None,
180 hit_test: None,
181 has_hit_targets: false,
182 has_origin_sinks: false,
183 draws_within_bounds: false,
184 isolation: IsolationReasons::default(),
185 cache_policy: CachePolicy::None,
186 cache_hashes: LayerRasterCacheHashes::default(),
187 cache_hashes_valid: false,
188 children: Vec::new(),
189 }
190 }
191}
192
193pub const CONTAINED_DRAW_SLACK: f32 = 1.0;
196
197impl LayerNode {
198 pub fn content_draws_within_bounds(&self) -> bool {
202 if self.visual_clip_rect().is_some() {
203 return true;
204 }
205 let bounds = inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK);
206 self.children.iter().all(|child| match child {
207 RenderNode::DrawRun(run) => run
208 .recording
209 .bounds()
210 .is_none_or(|drawn| rect_within(drawn, bounds)),
211 RenderNode::Primitive(entry) => match &entry.node {
212 PrimitiveNode::Text(text) => text.draws_within(bounds),
213 PrimitiveNode::Draw(_) => false,
214 },
215 RenderNode::Layer(layer) => layer.draws_within_parent(bounds),
216 })
217 }
218
219 pub(crate) fn refresh_child_facts(&mut self) {
220 self.has_hit_targets = self.hit_test.is_some()
221 || self.children.iter().any(|child| match child {
222 RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
223 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
224 });
225 self.draws_within_bounds = self.content_draws_within_bounds();
226 }
227
228 fn draws_within_parent(&self, bounds: Rect) -> bool {
232 self.draws_within_bounds
233 && self.graphics_layer.shadow_elevation <= 0.0
234 && self.effect().is_none()
235 && self.backdrop().is_none()
236 && rect_within(
237 quad_bounds(self.transform_to_parent.map_rect(self.local_bounds)),
238 bounds,
239 )
240 }
241
242 pub fn node_rect(&self) -> Rect {
244 self.node_bounds.unwrap_or(self.local_bounds)
245 }
246
247 pub fn clip_rect(&self) -> Option<Rect> {
248 (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
249 }
250
251 pub fn visual_clip_rect(&self) -> Option<Rect> {
256 self.clip_rect().or_else(|| {
257 (self.isolation.group_opacity || self.isolation.explicit_offscreen)
258 .then_some(self.local_bounds)
259 })
260 }
261
262 pub fn effect(&self) -> Option<&RenderEffect> {
263 self.graphics_layer.render_effect.as_ref()
264 }
265
266 pub fn backdrop(&self) -> Option<&RenderEffect> {
267 self.graphics_layer.backdrop_effect.as_ref()
268 }
269
270 pub fn opacity(&self) -> f32 {
271 self.graphics_layer.alpha
272 }
273
274 pub fn blend_mode(&self) -> BlendMode {
275 self.graphics_layer.blend_mode
276 }
277
278 pub fn color_filter(&self) -> Option<ColorFilter> {
279 self.graphics_layer.color_filter
280 }
281
282 pub fn target_content_hash(&self) -> u64 {
283 if self.cache_hashes_valid {
284 self.cache_hashes.target_content
285 } else {
286 crate::graph_hash::layer_raster_cache_hashes(self).target_content
287 }
288 }
289
290 pub fn motion_source_content_hash(&self) -> u64 {
291 crate::graph_hash::layer_motion_source_content_hash(self)
292 }
293
294 pub fn effect_hash(&self) -> u64 {
295 if self.cache_hashes_valid {
296 self.cache_hashes.effect
297 } else {
298 crate::graph_hash::layer_raster_cache_hashes(self).effect
299 }
300 }
301
302 pub fn recompute_raster_cache_hashes(&mut self) {
303 crate::graph_hash::recompute_layer_raster_cache_hashes(self);
304 }
305}
306
307#[derive(Clone)]
308pub enum RenderNode {
309 Primitive(PrimitiveEntry),
310 DrawRun(DrawRunNode),
317 Layer(Box<LayerNode>),
318}
319
320#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
329pub struct DrawCommandId {
330 pub node_id: NodeId,
331 pub command_index: u32,
332 pub placement: DrawPlacement,
333}
334
335#[derive(Clone, Debug, PartialEq)]
336pub struct DrawRunNode {
337 pub phase: PrimitivePhase,
338 pub command: Option<DrawCommandId>,
341 pub recording: Rc<CommandRecording>,
347 pub segments: Range<u32>,
350 pub summary: DrawRunSummary,
353}
354
355pub type DrawRunSummary = RecordingSummary;
357
358impl DrawRunNode {
359 pub fn new(phase: PrimitivePhase, primitives: Vec<DrawPrimitive>) -> Self {
360 Self::for_command(phase, None, primitives)
361 }
362
363 pub fn for_command(
364 phase: PrimitivePhase,
365 command: Option<DrawCommandId>,
366 primitives: Vec<DrawPrimitive>,
367 ) -> Self {
368 let recording = CommandRecording::from_primitives(primitives);
369 let segments = recording.all_segments();
370 Self::for_command_shared(phase, command, Rc::new(recording), segments)
371 }
372
373 pub fn for_command_shared(
374 phase: PrimitivePhase,
375 command: Option<DrawCommandId>,
376 recording: Rc<CommandRecording>,
377 segments: Range<u32>,
378 ) -> Self {
379 let summary = recording.summary_in(&segments);
380 Self {
381 phase,
382 command,
383 recording,
384 segments,
385 summary,
386 }
387 }
388
389 pub fn primitives(&self) -> impl Iterator<Item = DrawPrimitive> + '_ {
391 self.recording.primitives(self.segments.clone())
392 }
393
394 pub fn coverage_rects(&self) -> impl Iterator<Item = Rect> + '_ {
396 self.recording.coverage_rects(self.segments.clone())
397 }
398
399 pub fn len(&self) -> usize {
400 self.recording.len_in(&self.segments)
401 }
402
403 pub fn is_empty(&self) -> bool {
404 self.recording.is_empty_in(&self.segments)
405 }
406}
407
408#[derive(Clone)]
409pub struct RenderGraph {
410 pub root: LayerNode,
411}
412
413impl RenderGraph {
414 pub fn new(mut root: LayerNode) -> Self {
415 root.recompute_raster_cache_hashes();
416 Self { root }
417 }
418
419 pub fn node_count(&self) -> usize {
420 fn count_layer(layer: &LayerNode) -> usize {
421 1 + layer
422 .children
423 .iter()
424 .map(|child| match child {
425 RenderNode::Primitive(_) => 1,
426 RenderNode::DrawRun(run) => run.len(),
427 RenderNode::Layer(child_layer) => count_layer(child_layer),
428 })
429 .sum::<usize>()
430 }
431
432 count_layer(&self.root)
433 }
434
435 pub fn heap_bytes(&self) -> usize {
436 layer_heap_bytes(&self.root)
437 }
438
439 pub fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) {
441 fn collect(layer: &LayerNode, nodes: &mut HashSet<NodeId>) {
442 if let Some(node_id) = layer.node_id {
443 nodes.insert(node_id);
444 }
445 for child in &layer.children {
446 match child {
447 RenderNode::DrawRun(run) => {
448 if let Some(command) = run.command {
449 nodes.insert(command.node_id);
450 }
451 }
452 RenderNode::Layer(child) => collect(child, nodes),
453 RenderNode::Primitive(_) => {}
454 }
455 }
456 }
457
458 nodes.clear();
459 collect(&self.root, nodes);
460 }
461}
462
463fn layer_heap_bytes(layer: &LayerNode) -> usize {
464 size_of::<RenderNode>() * layer.children.capacity()
465 + layer
466 .children
467 .iter()
468 .map(render_node_heap_bytes)
469 .sum::<usize>()
470}
471
472fn render_node_heap_bytes(node: &RenderNode) -> usize {
473 match node {
474 RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
475 RenderNode::DrawRun(run) => {
476 run.recording.pod_heap_bytes()
477 + std::mem::size_of_val(run.recording.others())
478 + run
479 .recording
480 .others()
481 .iter()
482 .map(draw_primitive_heap_bytes)
483 .sum::<usize>()
484 }
485 RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
486 }
487}
488
489fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
490 match &entry.node {
491 PrimitiveNode::Draw(draw) => {
492 size_of::<DrawPrimitiveNode>() + draw_primitive_heap_bytes(&draw.primitive)
493 }
494 PrimitiveNode::Text(text) => {
495 size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
496 }
497 }
498}
499
500fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
501 match primitive {
502 DrawPrimitive::Content
503 | DrawPrimitive::Rect { .. }
504 | DrawPrimitive::RoundRect { .. }
505 | DrawPrimitive::Arc { .. }
506 | DrawPrimitive::Line { .. } => 0,
507 DrawPrimitive::Blend { primitive, .. } => {
508 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
509 }
510 DrawPrimitive::Image { .. } => 0,
511 DrawPrimitive::Text(text) => {
512 size_of::<cranpose_ui_graphics::TextPrimitive>()
513 + text.text.len()
514 + text
515 .style
516 .font_family
517 .as_ref()
518 .map_or(0, std::string::String::capacity)
519 }
520 DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
521 }
522}
523
524fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
525 match shadow {
526 ShadowPrimitive::Drop { shape, .. } => {
527 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
528 }
529 ShadowPrimitive::Inner { fill, cutout, .. } => {
530 size_of::<DrawPrimitive>() * 2
531 + draw_primitive_heap_bytes(fill)
532 + draw_primitive_heap_bytes(cutout)
533 }
534 }
535}
536
537fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
538 text.text.capacity()
539 + text.span_styles.capacity() * size_of::<usize>() * 2
540 + text.paragraph_styles.capacity() * size_of::<usize>() * 2
541 + text.string_annotations.capacity() * size_of::<usize>() * 2
542 + text.link_annotations.capacity() * size_of::<usize>() * 2
543 + text
544 .string_annotations
545 .iter()
546 .map(|annotation| {
547 annotation.item.tag.capacity() + annotation.item.annotation.capacity()
548 })
549 .sum::<usize>()
550 + text
551 .link_annotations
552 .iter()
553 .map(|annotation| match &annotation.item {
554 cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
555 cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
556 })
557 .sum::<usize>()
558}
559
560fn inflate_rect(rect: Rect, by: f32) -> Rect {
561 Rect {
562 x: rect.x - by,
563 y: rect.y - by,
564 width: rect.width + by * 2.0,
565 height: rect.height + by * 2.0,
566 }
567}
568
569fn rect_within(inner: Rect, outer: Rect) -> bool {
570 inner.x >= outer.x
571 && inner.y >= outer.y
572 && inner.x + inner.width <= outer.x + outer.width
573 && inner.y + inner.height <= outer.y + outer.height
574}
575
576#[cfg(test)]
577#[path = "tests/graph_tests.rs"]
578mod tests;