1use std::{cell::Cell, rc::Rc};
2
3use cranpose_core::{MemoryApplier, Node, NodeId, collections::map::HashSet};
4use cranpose_ui::{
5 DrawCommand, LayoutBox, LayoutNode, ModifierNodeSlices, Point, PreparedTextLayout, Rect, Size,
6 SubcomposeLayoutNode, TextLayoutOptions, TextOverflow, TextPanResolver, text::TextStyle,
7};
8use cranpose_ui_graphics::{
9 CommandRecording, CompositingStrategy, GraphicsLayer, LayerShape, RoundedCornerShape,
10 rounded_corner_alpha_mask_effect,
11};
12use smallvec::SmallVec;
13
14use crate::{
15 graph::{
16 CachePolicy, DrawCommandId, DrawRunNode, HitTestNode, IsolationReasons, LayerNode,
17 PrimitiveEntry, PrimitiveNode, PrimitivePhase, ProjectiveTransform, RenderGraph,
18 RenderNode, TextPrimitiveNode,
19 },
20 layer_transform::{layer_scales_or_rotates, layer_transform_to_parent},
21 raster_cache::LayerRasterCacheHashes,
22 style_shared::{DrawPlacement, recording_for_placement_reusing},
23};
24
25const TEXT_CLIP_PAD: f32 = 1.0;
26const ROUNDED_CLIP_EDGE_FEATHER: f32 = 1.0;
27
28#[derive(Clone, Default)]
29struct BuildNodeSnapshot {
30 node_id: NodeId,
31 placement: Point,
32 size: Size,
33 content_offset: Point,
34 slices: Rc<ModifierNodeSlices>,
37 graphics_layer: Option<GraphicsLayer>,
38 children: Vec<Self>,
39}
40
41struct SnapshotNodeData {
42 layout_state: cranpose_ui::widgets::LayoutState,
43 modifier_slices: Rc<ModifierNodeSlices>,
44 children: SmallVec<[NodeId; 8]>,
45 window_root: bool,
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum GraphRebuildReason {
58 RootLayerUnavailable,
60 DirtyLayerUnavailable,
62 UnmatchedDirtyNodes(usize),
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum GraphUpdate {
71 Patched,
72 NeedsRebuild(GraphRebuildReason),
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub struct GraphUpdateReport {
77 pub update: GraphUpdate,
78 pub hit_graph_dirty: bool,
79}
80
81impl GraphUpdateReport {
82 pub fn applied(self) -> bool {
83 matches!(self.update, GraphUpdate::Patched)
84 }
85
86 pub fn rebuild_reason(self) -> Option<GraphRebuildReason> {
87 match self.update {
88 GraphUpdate::Patched => None,
89 GraphUpdate::NeedsRebuild(reason) => Some(reason),
90 }
91 }
92}
93
94#[cfg(test)]
95thread_local! {
96 static LOWERED_LAYER_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
97}
98
99fn note_layer_lowered() {
100 #[cfg(test)]
101 LOWERED_LAYER_COUNT.with(|count| count.set(count.get() + 1));
102}
103
104#[cfg(test)]
105fn reset_lowered_layer_count() {
106 LOWERED_LAYER_COUNT.with(|count| count.set(0));
107}
108
109#[cfg(test)]
110fn lowered_layer_count() -> usize {
111 LOWERED_LAYER_COUNT.with(Cell::get)
112}
113
114pub fn build_graph_from_layout_tree(root: &LayoutBox, scale: f32) -> RenderGraph {
115 bump_recording_generation();
116 let root_snapshot = layout_box_to_snapshot(root, None);
117 RenderGraph {
118 root: build_layer_node(root_snapshot, scale, false),
119 }
120}
121
122pub fn build_graph_from_applier(
123 applier: &mut MemoryApplier,
124 root: NodeId,
125 scale: f32,
126) -> Option<RenderGraph> {
127 bump_recording_generation();
128 Some(RenderGraph {
129 root: build_layer_node_from_applier(applier, root, scale, false)?,
130 })
131}
132
133pub fn update_graph_from_applier(
134 applier: &mut MemoryApplier,
135 graph: &mut RenderGraph,
136 dirty_nodes: &[NodeId],
137 scale: f32,
138) -> bool {
139 update_graph_from_applier_report(applier, graph, dirty_nodes, scale).applied()
140}
141
142pub fn update_graph_from_applier_report(
143 applier: &mut MemoryApplier,
144 graph: &mut RenderGraph,
145 dirty_nodes: &[NodeId],
146 scale: f32,
147) -> GraphUpdateReport {
148 let mut changed_nodes = Vec::new();
149 update_graph_from_applier_report_into(applier, graph, dirty_nodes, scale, &mut changed_nodes)
150}
151
152pub fn update_graph_from_applier_report_into(
153 applier: &mut MemoryApplier,
154 graph: &mut RenderGraph,
155 dirty_nodes: &[NodeId],
156 scale: f32,
157 changed_nodes: &mut Vec<NodeId>,
158) -> GraphUpdateReport {
159 let report = update_graph_from_applier_report_into_inner(
160 applier,
161 graph,
162 dirty_nodes,
163 scale,
164 changed_nodes,
165 );
166 if let GraphUpdate::NeedsRebuild(reason) = report.update
167 && cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG")
168 {
169 eprintln!(
170 "[scene-update-diag] scoped update abandoned, whole scene rebuilt: {reason:?} dirty={}",
171 dirty_nodes.len()
172 );
173 }
174 report
175}
176
177fn update_graph_from_applier_report_into_inner(
178 applier: &mut MemoryApplier,
179 graph: &mut RenderGraph,
180 dirty_nodes: &[NodeId],
181 scale: f32,
182 changed_nodes: &mut Vec<NodeId>,
183) -> GraphUpdateReport {
184 if dirty_nodes.is_empty() {
185 return GraphUpdateReport {
186 update: GraphUpdate::Patched,
187 hit_graph_dirty: false,
188 };
189 }
190 bump_recording_generation();
191
192 if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
193 eprintln!("[scene-update-diag] dirty={dirty_nodes:?}");
194 }
195
196 let mut remaining_dirty_nodes = dirty_nodes.iter().copied().collect::<HashSet<_>>();
197 if let Some(root_id) = layer_identity(&graph.root)
198 && remaining_dirty_nodes.contains(&root_id)
199 {
200 remaining_dirty_nodes.remove(&root_id);
201 if try_translate_scrolled_layer(
202 applier,
203 &mut graph.root,
204 &mut remaining_dirty_nodes,
205 changed_nodes,
206 TranslateAncestorContext {
207 inherited_motion_context_animated: false,
208 ancestor_hashed: false,
209 inherited_translated_content_context: false,
210 parent_content_offset: Point::default(),
211 parent_abs: AbsOrigin::ROOT,
212 },
213 ) {
214 if remaining_dirty_nodes.is_empty() {
215 return GraphUpdateReport {
216 update: GraphUpdate::Patched,
217 hit_graph_dirty: true,
218 };
219 }
220 let inherited = graph.root.translated_content_context;
221 let root_children = AbsOrigin::ROOT.children_of(&graph.root);
222 let walked = replace_dirty_layers_from_applier(
223 applier,
224 &mut graph.root,
225 root_children,
226 &mut remaining_dirty_nodes,
227 inherited,
228 false,
229 changed_nodes,
230 );
231 return GraphUpdateReport {
232 update: classify_walk(walked.is_some(), &remaining_dirty_nodes),
233 hit_graph_dirty: true,
234 };
235 }
236 let Some(root) = build_layer_node_from_applier(applier, root_id, scale, false) else {
237 return GraphUpdateReport {
238 update: GraphUpdate::NeedsRebuild(GraphRebuildReason::RootLayerUnavailable),
239 hit_graph_dirty: true,
240 };
241 };
242 let hit_graph_dirty = layer_hit_graph_state_dirty(&graph.root, &root);
243 collect_layer_node_ids(&graph.root, changed_nodes);
244 graph.root = root;
245 graph.root.recompute_raster_cache_hashes();
246 collect_layer_node_ids(&graph.root, changed_nodes);
247 return GraphUpdateReport {
248 update: GraphUpdate::Patched,
249 hit_graph_dirty,
250 };
251 }
252
253 let inherited_translated_content_context = graph.root.translated_content_context;
254 let root_children = AbsOrigin::ROOT.children_of(&graph.root);
255 let Some(report) = replace_dirty_layers_from_applier(
256 applier,
257 &mut graph.root,
258 root_children,
259 &mut remaining_dirty_nodes,
260 inherited_translated_content_context,
261 false,
262 changed_nodes,
263 ) else {
264 return GraphUpdateReport {
265 update: GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable),
266 hit_graph_dirty: true,
267 };
268 };
269
270 match classify_walk(true, &remaining_dirty_nodes) {
271 GraphUpdate::Patched => GraphUpdateReport {
272 update: GraphUpdate::Patched,
273 hit_graph_dirty: report.hit_graph_dirty,
274 },
275 update => GraphUpdateReport {
276 update,
277 hit_graph_dirty: true,
278 },
279 }
280}
281
282fn classify_walk(walked: bool, remaining_dirty_nodes: &HashSet<NodeId>) -> GraphUpdate {
283 if !walked {
284 GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable)
285 } else if !remaining_dirty_nodes.is_empty() {
286 GraphUpdate::NeedsRebuild(GraphRebuildReason::UnmatchedDirtyNodes(
287 remaining_dirty_nodes.len(),
288 ))
289 } else {
290 GraphUpdate::Patched
291 }
292}
293
294#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
295struct ReplaceDirtyLayersReport {
296 updated: bool,
297 hit_graph_dirty: bool,
298}
299
300fn replace_dirty_layers_from_applier(
301 applier: &mut MemoryApplier,
302 parent: &mut LayerNode,
303 parent_children: AbsOrigin,
304 dirty_nodes: &mut HashSet<NodeId>,
305 inherited_translated_content_context: bool,
306 ancestor_hashed: bool,
307 changed_nodes: &mut Vec<NodeId>,
308) -> Option<ReplaceDirtyLayersReport> {
309 if dirty_nodes.is_empty() {
310 return Some(ReplaceDirtyLayersReport::default());
311 }
312
313 let child_inherited_translated_content_context =
314 inherited_translated_content_context || parent.translated_content_context;
315 let child_ancestor_hashed =
316 crate::graph_hash::layer_children_ancestor_hashed(parent, ancestor_hashed);
317 let mut report = ReplaceDirtyLayersReport::default();
318
319 for child in &mut parent.children {
320 let RenderNode::Layer(child_layer) = child else {
321 continue;
322 };
323
324 if layer_identity(child_layer).is_some_and(|node_id| dirty_nodes.remove(&node_id)) {
325 if try_translate_scrolled_layer(
326 applier,
327 child_layer,
328 dirty_nodes,
329 changed_nodes,
330 TranslateAncestorContext {
331 inherited_motion_context_animated: parent.motion_context_animated,
332 ancestor_hashed: child_ancestor_hashed,
333 inherited_translated_content_context:
334 child_inherited_translated_content_context,
335 parent_content_offset: parent.content_offset,
336 parent_abs: parent_children,
337 },
338 ) {
339 report.hit_graph_dirty = true;
340 report.updated = true;
341 let child_children = parent_children.children_of(child_layer);
342 let child_report = replace_dirty_layers_from_applier(
343 applier,
344 child_layer,
345 child_children,
346 dirty_nodes,
347 child_inherited_translated_content_context,
348 child_ancestor_hashed,
349 changed_nodes,
350 )?;
351 report.hit_graph_dirty |= child_report.hit_graph_dirty;
352 continue;
353 }
354 let mut replacement = build_layer_node_from_applier_internal(
355 applier,
356 layer_identity(child_layer).expect("dirty layer must have a node id"),
357 parent.motion_context_animated,
358 child_inherited_translated_content_context,
359 Some(parent_children),
360 )?;
361 if parent.content_offset != Point::default() {
362 replacement.transform_to_parent =
363 replacement
364 .transform_to_parent
365 .then(ProjectiveTransform::translation(
366 parent.content_offset.x,
367 parent.content_offset.y,
368 ));
369 }
370 report.hit_graph_dirty |= layer_hit_graph_state_dirty(child_layer, &replacement);
371 remove_dirty_descendants(&replacement, dirty_nodes);
372 collect_layer_node_ids(child_layer, changed_nodes);
373 **child_layer = replacement;
374 collect_layer_node_ids(child_layer, changed_nodes);
375 crate::graph_hash::recompute_layer_raster_cache_hashes_under(
376 child_layer,
377 child_ancestor_hashed,
378 );
379 report.updated = true;
380 continue;
381 }
382
383 let child_children = parent_children.children_of(child_layer);
384 let child_report = replace_dirty_layers_from_applier(
385 applier,
386 child_layer,
387 child_children,
388 dirty_nodes,
389 child_inherited_translated_content_context,
390 child_ancestor_hashed,
391 changed_nodes,
392 )?;
393 report.updated |= child_report.updated;
394 report.hit_graph_dirty |= child_report.hit_graph_dirty;
395 }
396
397 if report.updated {
398 parent.draws_within_bounds = parent.content_draws_within_bounds();
399 parent.has_hit_targets = parent.hit_test.is_some()
400 || parent.children.iter().any(|child| match child {
401 RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
402 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
403 });
404 crate::graph_hash::refresh_layer_own_raster_cache_hashes(parent, ancestor_hashed);
405 if let Some(node_id) = parent.node_id {
406 changed_nodes.push(node_id);
407 }
408 }
409
410 Some(report)
411}
412
413fn translate_bail(reason: &str) -> bool {
414 if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
415 eprintln!("[scene-update-diag] translate bail: {reason}");
416 }
417 false
418}
419
420#[derive(Clone, Copy)]
421struct TranslateAncestorContext {
422 inherited_motion_context_animated: bool,
423 ancestor_hashed: bool,
424 inherited_translated_content_context: bool,
425 parent_content_offset: Point,
426 parent_abs: AbsOrigin,
427}
428
429fn try_translate_scrolled_layer(
430 applier: &mut MemoryApplier,
431 container: &mut LayerNode,
432 dirty_nodes: &mut HashSet<NodeId>,
433 changed_nodes: &mut Vec<NodeId>,
434 ancestors: TranslateAncestorContext,
435) -> bool {
436 let Some(node_id) = layer_identity(container) else {
437 return translate_bail("no node id");
438 };
439 let Some(data) = snapshot_node_data(applier, node_id) else {
440 return translate_bail("container snapshot read failed");
441 };
442 if container.wraps.is_none() {
443 return translate_layer_from_data(
444 applier,
445 container,
446 dirty_nodes,
447 changed_nodes,
448 ancestors,
449 data,
450 false,
451 );
452 }
453 let outer_count = data.modifier_slices.outer_draw_command_count();
454 if outer_count == 0 {
455 return translate_bail("outer draws removed");
456 }
457 let size = data.layout_state.size();
458 let placement = data.layout_state.position();
459 let slices = Rc::clone(&data.modifier_slices);
460 let inner_ancestors = TranslateAncestorContext {
461 ancestor_hashed: crate::graph_hash::layer_children_ancestor_hashed(
462 container,
463 ancestors.ancestor_hashed,
464 ),
465 ..ancestors
466 };
467 let Some(inner) = container.children.iter_mut().find_map(|child| match child {
468 RenderNode::Layer(layer) if layer.node_id == Some(node_id) => Some(layer),
469 _ => None,
470 }) else {
471 return translate_bail("wrapped layer missing");
472 };
473 if !translate_layer_from_data(
474 applier,
475 inner,
476 dirty_nodes,
477 changed_nodes,
478 inner_ancestors,
479 data,
480 true,
481 ) {
482 return false;
483 }
484 let layer = std::mem::take(inner.as_mut());
485 let outer = outer_draws(node_id, slices.draw_commands(), outer_count, size)
486 .expect("outer command count is nonzero");
487 *container = wrap_layer_with_outer_draws(layer, placement, outer);
488 if ancestors.parent_content_offset != Point::default() {
489 container.transform_to_parent =
490 container
491 .transform_to_parent
492 .then(ProjectiveTransform::translation(
493 ancestors.parent_content_offset.x,
494 ancestors.parent_content_offset.y,
495 ));
496 }
497 for child in &mut container.children {
498 if let RenderNode::Layer(layer) = child {
499 crate::graph_hash::refresh_layer_own_raster_cache_hashes(
500 layer,
501 inner_ancestors.ancestor_hashed,
502 );
503 }
504 }
505 crate::graph_hash::refresh_layer_own_raster_cache_hashes(container, ancestors.ancestor_hashed);
506 true
507}
508
509struct TranslatedContainer {
510 node_id: NodeId,
511 graphics_layer: GraphicsLayer,
512}
513
514fn translated_container(
515 container: &LayerNode,
516 layout_state: &cranpose_ui::widgets::LayoutState,
517 modifier_slices: &ModifierNodeSlices,
518 inherited_motion_context_animated: bool,
519 wrapped: bool,
520) -> Result<TranslatedContainer, &'static str> {
521 if cranpose_core::env_flag!("CRANPOSE_DISABLE_SCROLL_TRANSLATE") {
522 return Err("fast path disabled by ablation switch");
523 }
524 let Some(node_id) = container.node_id else {
525 return Err("no node id");
526 };
527 if container
528 .children
529 .iter()
530 .any(|child| !matches!(child, RenderNode::Layer(_)))
531 {
532 return Err("container has own primitive children");
533 }
534 if !layout_state.is_placed()
535 || Rect::from_size(layout_state.size()) != container.node_rect()
536 || modifier_slices.layer_bounds(layout_state.size()) != container.local_bounds
537 {
538 return Err("container unplaced, resized or its layer moved");
539 }
540 let outer_count = modifier_slices.outer_draw_command_count();
541 if (outer_count > 0 && !wrapped)
542 || !modifier_slices.draw_commands()[outer_count..].is_empty()
543 || (inherited_motion_context_animated || modifier_slices.motion_context_animated())
544 != container.motion_context_animated
545 || modifier_slices.annotated_text().is_some()
546 || modifier_slices.translated_content_context() != container.translated_content_context
547 {
548 return Err("container draw/text/translated-context changed");
549 }
550 let clip_to_bounds = modifier_slices.clip_to_bounds();
551 if clip_to_bounds != container.clip_to_bounds {
552 return Err("container clip changed");
553 }
554 let graphics_layer = graphics_layer_with_shaped_clip(
555 modifier_slices.graphics_layer().unwrap_or_default(),
556 clip_to_bounds,
557 modifier_slices.corner_shape(),
558 container.local_bounds,
559 );
560 if graphics_layer != container.graphics_layer {
561 return Err("container graphics layer changed");
562 }
563 Ok(TranslatedContainer {
564 node_id,
565 graphics_layer,
566 })
567}
568
569struct TranslatedChildren {
570 placed_fresh: SmallVec<[(NodeId, cranpose_ui::widgets::LayoutState); 8]>,
571 children_unchanged: bool,
572 old_index_by_id: std::collections::HashMap<NodeId, usize>,
573}
574
575fn translated_children(
576 applier: &mut MemoryApplier,
577 container: &LayerNode,
578 dirty_nodes: &HashSet<NodeId>,
579 fresh_children: &[NodeId],
580) -> Result<TranslatedChildren, &'static str> {
581 let mut placed_fresh = SmallVec::<[_; 8]>::with_capacity(fresh_children.len());
582 for child_id in fresh_children {
583 let state = applier
584 .with_node::<LayoutNode, _>(*child_id, |node| node.layout_state())
585 .or_else(|_| {
586 applier.with_node::<SubcomposeLayoutNode, _>(*child_id, |node| node.layout_state())
587 });
588 let Ok(state) = state else {
589 continue;
590 };
591 if !state.is_placed() {
592 continue;
593 }
594 placed_fresh.push((*child_id, state));
595 }
596 let children_unchanged = container.children.len() == placed_fresh.len()
597 && container
598 .children
599 .iter()
600 .zip(&placed_fresh)
601 .all(|(child, (id, _))| {
602 matches!(child, RenderNode::Layer(layer) if layer_identity(layer) == Some(*id))
603 });
604 let old_index_by_id = if children_unchanged {
605 std::collections::HashMap::new()
606 } else {
607 let Some(index): Option<std::collections::HashMap<NodeId, usize>> = container
608 .children
609 .iter()
610 .enumerate()
611 .map(|(index, child)| match child {
612 RenderNode::Layer(layer) => layer_identity(layer).map(|id| (id, index)),
613 _ => None,
614 })
615 .collect()
616 else {
617 return Err("child without node id");
618 };
619 index
620 };
621 check_retained_children(
622 container,
623 dirty_nodes,
624 &placed_fresh,
625 children_unchanged,
626 &old_index_by_id,
627 )?;
628 Ok(TranslatedChildren {
629 placed_fresh,
630 children_unchanged,
631 old_index_by_id,
632 })
633}
634
635fn check_retained_children(
636 container: &LayerNode,
637 dirty_nodes: &HashSet<NodeId>,
638 placed_fresh: &[(NodeId, cranpose_ui::widgets::LayoutState)],
639 children_unchanged: bool,
640 old_index_by_id: &std::collections::HashMap<NodeId, usize>,
641) -> Result<(), &'static str> {
642 for (fresh_index, (child_id, state)) in placed_fresh.iter().enumerate() {
643 let old_index = if children_unchanged {
644 fresh_index
645 } else if let Some(index) = old_index_by_id.get(child_id) {
646 *index
647 } else {
648 continue;
649 };
650 let RenderNode::Layer(layer) = &container.children[old_index] else {
651 return Err("retained child slot is not a layer");
652 };
653 if dirty_nodes.contains(child_id) {
654 continue;
655 }
656 if layer.has_origin_sinks {
657 return Err("child subtree publishes window origins");
658 }
659 if Rect::from_size(state.size()) != layer.node_rect() {
660 return Err("child resized");
661 }
662 }
663 Ok(())
664}
665
666#[derive(Clone, Copy)]
667struct TranslateGeometry {
668 content_offset: Point,
669 layer_translation: Point,
670 window_origin: Point,
671 child_origin: Point,
672}
673
674impl TranslateGeometry {
675 fn new(
676 layout_state: &cranpose_ui::widgets::LayoutState,
677 graphics_layer: &GraphicsLayer,
678 parent_abs: AbsOrigin,
679 ) -> Self {
680 let content_offset = layout_state.content_offset;
681 let top_left = Point {
682 x: parent_abs.content_origin.x + layout_state.position().x,
683 y: parent_abs.content_origin.y + layout_state.position().y,
684 };
685 let layer_translation = Point {
686 x: parent_abs.layer_translation.x + graphics_layer.translation_x,
687 y: parent_abs.layer_translation.y + graphics_layer.translation_y,
688 };
689 Self {
690 content_offset,
691 layer_translation,
692 window_origin: Point {
693 x: top_left.x + layer_translation.x,
694 y: top_left.y + layer_translation.y,
695 },
696 child_origin: Point {
697 x: top_left.x + content_offset.x,
698 y: top_left.y + content_offset.y,
699 },
700 }
701 }
702}
703
704fn build_entering_children(
705 applier: &mut MemoryApplier,
706 container: &LayerNode,
707 placed_fresh: &[(NodeId, cranpose_ui::widgets::LayoutState)],
708 retained: (bool, &std::collections::HashMap<NodeId, usize>),
709 geometry: TranslateGeometry,
710 inherited: (bool, bool),
711) -> std::collections::HashMap<NodeId, LayerNode> {
712 let (children_unchanged, old_index_by_id) = retained;
713 let (child_inherited_translated_content_context, children_ancestor_hashed) = inherited;
714 let mut entering: std::collections::HashMap<NodeId, LayerNode> =
715 std::collections::HashMap::new();
716 for (child_id, _) in placed_fresh {
717 if children_unchanged || old_index_by_id.contains_key(child_id) {
718 continue;
719 }
720 let Some(mut lowered) = build_layer_node_from_applier_internal(
721 applier,
722 *child_id,
723 container.motion_context_animated,
724 child_inherited_translated_content_context,
725 Some(AbsOrigin {
726 content_origin: geometry.child_origin,
727 layer_translation: geometry.layer_translation,
728 }),
729 ) else {
730 continue;
731 };
732 if geometry.content_offset != Point::default() {
733 lowered.transform_to_parent =
734 lowered
735 .transform_to_parent
736 .then(ProjectiveTransform::translation(
737 geometry.content_offset.x,
738 geometry.content_offset.y,
739 ));
740 }
741 crate::graph_hash::recompute_layer_raster_cache_hashes_under(
742 &mut lowered,
743 children_ancestor_hashed,
744 );
745 entering.insert(*child_id, lowered);
746 }
747 entering
748}
749
750fn apply_translated_container_state(
751 container: &mut LayerNode,
752 modifier_slices: &ModifierNodeSlices,
753 layout_state: &cranpose_ui::widgets::LayoutState,
754 graphics_layer: &GraphicsLayer,
755 parent_content_offset: Point,
756 geometry: TranslateGeometry,
757) {
758 let mut transform = layer_transform_to_parent(
759 container.local_bounds,
760 layout_state.position(),
761 graphics_layer,
762 );
763 if parent_content_offset != Point::default() {
764 transform = transform.then(ProjectiveTransform::translation(
765 parent_content_offset.x,
766 parent_content_offset.y,
767 ));
768 }
769 container.transform_to_parent = transform;
770 container.content_offset = geometry.content_offset;
771 if container.translated_content_context {
772 container.translated_content_offset = modifier_slices
773 .translated_content_offset()
774 .unwrap_or(geometry.content_offset);
775 }
776 if let Some(sink) = modifier_slices.text_window_origin() {
777 sink.set(geometry.window_origin);
778 }
779 if let Some(sink) = modifier_slices.viewport_window_rect() {
780 sink.set(Rect {
781 x: geometry.window_origin.x,
782 y: geometry.window_origin.y,
783 width: layout_state.size().width,
784 height: layout_state.size().height,
785 });
786 }
787 container.origin_in_parent = layout_state.position();
788}
789
790fn reconcile_translated_children(
791 container: &mut LayerNode,
792 dirty_nodes: &mut HashSet<NodeId>,
793 changed_nodes: &mut Vec<NodeId>,
794 placed_fresh: &[(NodeId, cranpose_ui::widgets::LayoutState)],
795 children_unchanged: bool,
796 entering: &mut std::collections::HashMap<NodeId, LayerNode>,
797 geometry: TranslateGeometry,
798) {
799 if children_unchanged {
800 for (child, (child_id, state)) in container.children.iter_mut().zip(placed_fresh) {
801 let RenderNode::Layer(layer) = child else {
802 unreachable!("retained child identities were checked");
803 };
804 if !dirty_nodes.contains(child_id) {
805 translate_retained_child(layer, state, geometry.content_offset);
806 changed_nodes.push(*child_id);
807 }
808 }
809 return;
810 }
811 let fresh_id_set: HashSet<NodeId> = placed_fresh.iter().map(|(id, _)| *id).collect();
812 let mut old_by_id: std::collections::HashMap<NodeId, Box<LayerNode>> =
813 std::collections::HashMap::new();
814 for child in container.children.drain(..) {
815 let RenderNode::Layer(layer) = child else {
816 continue;
817 };
818 let child_id = layer_identity(&layer).expect("checked above");
819 if fresh_id_set.contains(&child_id) {
820 old_by_id.insert(child_id, layer);
821 } else {
822 collect_layer_node_ids(&layer, changed_nodes);
823 }
824 }
825 let mut new_children = Vec::with_capacity(placed_fresh.len());
826 for (child_id, state) in placed_fresh {
827 if let Some(mut layer) = old_by_id.remove(child_id) {
828 if !dirty_nodes.contains(child_id) {
829 translate_retained_child(&mut layer, state, geometry.content_offset);
830 changed_nodes.push(*child_id);
831 }
832 new_children.push(RenderNode::Layer(layer));
833 } else if let Some(lowered) = entering.remove(child_id) {
834 dirty_nodes.remove(child_id);
835 remove_dirty_descendants(&lowered, dirty_nodes);
836 collect_layer_node_ids(&lowered, changed_nodes);
837 new_children.push(RenderNode::Layer(Box::new(lowered)));
838 }
839 }
840 container.children = new_children;
841}
842
843fn translate_layer_from_data(
844 applier: &mut MemoryApplier,
845 container: &mut LayerNode,
846 dirty_nodes: &mut HashSet<NodeId>,
847 changed_nodes: &mut Vec<NodeId>,
848 ancestors: TranslateAncestorContext,
849 data: SnapshotNodeData,
850 wrapped: bool,
851) -> bool {
852 let TranslateAncestorContext {
853 inherited_motion_context_animated,
854 ancestor_hashed: container_ancestor_hashed,
855 inherited_translated_content_context,
856 parent_content_offset,
857 parent_abs,
858 } = ancestors;
859 let SnapshotNodeData {
860 layout_state,
861 modifier_slices,
862 children: fresh_children,
863 window_root,
864 } = data;
865 let layout_state = if window_root {
866 layout_state.at_origin()
867 } else {
868 layout_state
869 };
870 let container_plan = match translated_container(
871 container,
872 &layout_state,
873 &modifier_slices,
874 inherited_motion_context_animated,
875 wrapped,
876 ) {
877 Ok(plan) => plan,
878 Err(reason) => return translate_bail(reason),
879 };
880 let TranslatedContainer {
881 node_id,
882 graphics_layer,
883 } = container_plan;
884 let child_plan = match translated_children(applier, container, dirty_nodes, &fresh_children) {
885 Ok(plan) => plan,
886 Err(reason) => return translate_bail(reason),
887 };
888 let TranslatedChildren {
889 placed_fresh,
890 children_unchanged,
891 old_index_by_id,
892 } = child_plan;
893
894 let geometry = TranslateGeometry::new(&layout_state, &graphics_layer, parent_abs);
895 let child_inherited_translated_content_context =
896 inherited_translated_content_context || container.translated_content_context;
897 let children_ancestor_hashed =
898 crate::graph_hash::layer_children_ancestor_hashed(container, container_ancestor_hashed);
899 let mut entering = build_entering_children(
900 applier,
901 container,
902 &placed_fresh,
903 (children_unchanged, &old_index_by_id),
904 geometry,
905 (
906 child_inherited_translated_content_context,
907 children_ancestor_hashed,
908 ),
909 );
910
911 apply_translated_container_state(
912 container,
913 &modifier_slices,
914 &layout_state,
915 &graphics_layer,
916 parent_content_offset,
917 geometry,
918 );
919
920 reconcile_translated_children(
921 container,
922 dirty_nodes,
923 changed_nodes,
924 &placed_fresh,
925 children_unchanged,
926 &mut entering,
927 geometry,
928 );
929 modifier_slices.publish_pointer_input_size(layout_state.size());
930 container.hit_test = hit_test_from_slices(&modifier_slices);
931
932 container.has_hit_targets = container.hit_test.is_some()
933 || container.children.iter().any(|child| match child {
934 RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
935 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
936 });
937 container.has_origin_sinks = modifier_slices_have_origin_sinks(&modifier_slices)
938 || container.children.iter().any(|child| match child {
939 RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
940 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
941 });
942
943 container.draws_within_bounds = container.content_draws_within_bounds();
944 crate::graph_hash::refresh_layer_own_raster_cache_hashes(container, container_ancestor_hashed);
945 changed_nodes.push(node_id);
946 true
947}
948
949fn translate_retained_child(
950 layer: &mut LayerNode,
951 state: &cranpose_ui::widgets::LayoutState,
952 content_offset: Point,
953) {
954 let mut child_transform =
955 layer_transform_to_parent(layer.local_bounds, state.position(), &layer.graphics_layer);
956 if content_offset != Point::default() {
957 child_transform = child_transform.then(ProjectiveTransform::translation(
958 content_offset.x,
959 content_offset.y,
960 ));
961 }
962 layer.transform_to_parent = child_transform;
963 layer.origin_in_parent = state.position();
964}
965
966fn layer_hit_graph_state_dirty(previous: &LayerNode, replacement: &LayerNode) -> bool {
967 if previous.hit_test.is_some() || replacement.hit_test.is_some() {
968 return true;
969 }
970
971 if !(previous.has_hit_targets || replacement.has_hit_targets) {
972 return false;
973 }
974
975 previous.has_hit_targets != replacement.has_hit_targets
976 || previous.local_bounds != replacement.local_bounds
977 || previous.node_bounds != replacement.node_bounds
978 || previous.transform_to_parent != replacement.transform_to_parent
979 || previous.clip_rect() != replacement.clip_rect()
980 || previous.graphics_layer.shape != replacement.graphics_layer.shape
981}
982
983fn collect_layer_node_ids(layer: &LayerNode, out: &mut Vec<NodeId>) {
984 if let Some(node_id) = layer.node_id {
985 out.push(node_id);
986 }
987 for child in &layer.children {
988 if let RenderNode::Layer(child_layer) = child {
989 collect_layer_node_ids(child_layer, out);
990 }
991 }
992}
993
994fn remove_dirty_descendants(layer: &LayerNode, dirty_nodes: &mut HashSet<NodeId>) {
995 for child in &layer.children {
996 let RenderNode::Layer(child_layer) = child else {
997 continue;
998 };
999 if let Some(node_id) = child_layer.node_id {
1000 dirty_nodes.remove(&node_id);
1001 }
1002 remove_dirty_descendants(child_layer, dirty_nodes);
1003 }
1004}
1005
1006fn build_layer_node(
1007 snapshot: BuildNodeSnapshot,
1008 _root_scale: f32,
1009 inherited_motion_context_animated: bool,
1010) -> LayerNode {
1011 build_layer_node_internal(snapshot, inherited_motion_context_animated, false)
1012}
1013
1014fn build_layer_node_internal(
1015 snapshot: BuildNodeSnapshot,
1016 inherited_motion_context_animated: bool,
1017 inherited_translated_content_context: bool,
1018) -> LayerNode {
1019 let BuildNodeSnapshot {
1020 node_id,
1021 placement,
1022 size,
1023 content_offset,
1024 slices,
1025 graphics_layer,
1026 children: child_snapshots,
1027 } = snapshot;
1028 let motion_context_animated = slices.motion_context_animated();
1029 let translated_content_context = slices.translated_content_context();
1030 let has_own_origin_sinks = modifier_slices_have_origin_sinks(&slices);
1031 let measured_text_layout = slices.measured_text_layout();
1032 let draw_commands = slices.draw_commands();
1033 let outer_draw_command_count = slices.outer_draw_command_count();
1034 let clip_to_bounds = slices.clip_to_bounds();
1035 let text_style = slices.text_style();
1036 let text_layout_options = slices.text_layout_options();
1037 let text_pan = slices.text_pan_resolver();
1038 let outer = outer_draws(node_id, draw_commands, outer_draw_command_count, size);
1039 let layer_draw_commands = &draw_commands[outer_draw_command_count..];
1040 let (local_bounds, node_bounds) = layer_and_node_bounds(&slices, size);
1041 let graphics_layer = graphics_layer.unwrap_or_default();
1042 let transform_to_parent = layer_transform_to_parent(local_bounds, placement, &graphics_layer);
1043 let isolation = isolation_reasons(&graphics_layer);
1044 let cache_policy = layer_cache_policy(&graphics_layer, isolation);
1045 let shadow_clip = clip_to_bounds.then_some(local_bounds);
1046 let hit_test = hit_test_from_slices(&slices);
1047
1048 let node_motion_context_animated = inherited_motion_context_animated || motion_context_animated;
1049 let child_translated_content_context =
1050 inherited_translated_content_context || translated_content_context;
1051
1052 let mut children = Vec::with_capacity(layer_node_capacity(
1053 layer_draw_commands,
1054 child_snapshots.len(),
1055 measured_text_layout.is_some(),
1056 ));
1057 append_draw_nodes(
1058 &mut children,
1059 node_id,
1060 layer_draw_commands,
1061 outer_draw_command_count,
1062 DrawPlacement::Behind,
1063 size,
1064 PrimitivePhase::BeforeChildren,
1065 );
1066 if let Some(text) = text_node_from_parts(TextNodeParts {
1067 node_id,
1068 text_rect: slices.text_content_rect(size),
1069 text_style,
1070 text_layout_options,
1071 text_pan,
1072 measured_layout: measured_text_layout,
1073 }) {
1074 children.push(RenderNode::Primitive(PrimitiveEntry {
1075 phase: PrimitivePhase::BeforeChildren,
1076 node: PrimitiveNode::Text(Box::new(text)),
1077 }));
1078 }
1079 let child_motion_context_animated = node_motion_context_animated;
1080 for child in child_snapshots {
1081 let mut child_layer = build_layer_node_internal(
1082 child,
1083 child_motion_context_animated,
1084 child_translated_content_context,
1085 );
1086 if content_offset != Point::default() {
1087 child_layer.transform_to_parent =
1088 child_layer
1089 .transform_to_parent
1090 .then(ProjectiveTransform::translation(
1091 content_offset.x,
1092 content_offset.y,
1093 ));
1094 }
1095 children.push(RenderNode::Layer(Box::new(child_layer)));
1096 }
1097 append_draw_nodes(
1098 &mut children,
1099 node_id,
1100 layer_draw_commands,
1101 outer_draw_command_count,
1102 DrawPlacement::Overlay,
1103 size,
1104 PrimitivePhase::AfterChildren,
1105 );
1106 let has_hit_targets = hit_test.is_some()
1107 || children.iter().any(|child| match child {
1108 RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
1109 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1110 });
1111 let has_origin_sinks = has_own_origin_sinks
1112 || children.iter().any(|child| match child {
1113 RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
1114 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1115 });
1116
1117 let layer = LayerNode {
1118 node_id: Some(node_id),
1119 wraps: None,
1120 local_bounds,
1121 node_bounds,
1122 transform_to_parent,
1123 content_offset,
1124 motion_context_animated: node_motion_context_animated,
1125 translated_content_context,
1126 translated_content_offset: if translated_content_context {
1127 content_offset
1128 } else {
1129 Point::default()
1130 },
1131 origin_in_parent: placement,
1132 graphics_layer,
1133 clip_to_bounds,
1134 shadow_clip,
1135 hit_test,
1136 has_hit_targets,
1137 has_origin_sinks,
1138 draws_within_bounds: false,
1139 isolation,
1140 cache_policy,
1141 cache_hashes: LayerRasterCacheHashes::default(),
1142 cache_hashes_valid: false,
1143 children,
1144 };
1145 finish_layer(layer, placement, outer)
1146}
1147
1148#[derive(Clone, Copy)]
1149struct AbsOrigin {
1150 content_origin: Point,
1151 layer_translation: Point,
1152}
1153
1154impl AbsOrigin {
1155 const ROOT: AbsOrigin = AbsOrigin {
1156 content_origin: Point { x: 0.0, y: 0.0 },
1157 layer_translation: Point { x: 0.0, y: 0.0 },
1158 };
1159
1160 fn children_of(self, layer: &LayerNode) -> AbsOrigin {
1161 AbsOrigin {
1162 content_origin: Point {
1163 x: self.content_origin.x + layer.origin_in_parent.x + layer.content_offset.x,
1164 y: self.content_origin.y + layer.origin_in_parent.y + layer.content_offset.y,
1165 },
1166 layer_translation: Point {
1167 x: self.layer_translation.x + layer.graphics_layer.translation_x,
1168 y: self.layer_translation.y + layer.graphics_layer.translation_y,
1169 },
1170 }
1171 }
1172}
1173
1174fn build_layer_node_from_applier(
1175 applier: &mut MemoryApplier,
1176 node_id: NodeId,
1177 _root_scale: f32,
1178 inherited_motion_context_animated: bool,
1179) -> Option<LayerNode> {
1180 let mut data = snapshot_node_data(applier, node_id)?;
1181 if data.window_root {
1182 data.layout_state = data.layout_state.at_origin();
1183 }
1184 build_layer_node_from_data(
1185 applier,
1186 node_id,
1187 data,
1188 inherited_motion_context_animated,
1189 false,
1190 Some(AbsOrigin::ROOT),
1191 )
1192}
1193
1194fn snapshot_node_data(applier: &mut MemoryApplier, node_id: NodeId) -> Option<SnapshotNodeData> {
1195 if let Ok(data) = applier.with_node::<LayoutNode, _>(node_id, |node| {
1196 let state = node.layout_state();
1197 let mut children = SmallVec::new();
1198 node.collect_children_into(&mut children);
1199 let modifier_slices = node.modifier_slices_snapshot();
1200 SnapshotNodeData {
1201 layout_state: state,
1202 modifier_slices,
1203 children,
1204 window_root: node.is_window_root(),
1205 }
1206 }) {
1207 return Some(data);
1208 }
1209
1210 applier
1211 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1212 let state = node.layout_state();
1213 let mut children = SmallVec::new();
1214 node.collect_children_into(&mut children);
1215 let modifier_slices = node.modifier_slices_snapshot();
1216 SnapshotNodeData {
1217 layout_state: state,
1218 modifier_slices,
1219 children,
1220 window_root: false,
1221 }
1222 })
1223 .ok()
1224}
1225
1226fn build_layer_node_from_applier_internal(
1227 applier: &mut MemoryApplier,
1228 node_id: NodeId,
1229 inherited_motion_context_animated: bool,
1230 inherited_translated_content_context: bool,
1231 parent_abs: Option<AbsOrigin>,
1232) -> Option<LayerNode> {
1233 let data = snapshot_node_data(applier, node_id)?;
1234 if data.window_root {
1235 return None;
1236 }
1237 build_layer_node_from_data(
1238 applier,
1239 node_id,
1240 data,
1241 inherited_motion_context_animated,
1242 inherited_translated_content_context,
1243 parent_abs,
1244 )
1245}
1246
1247fn hit_test_from_slices(slices: &Rc<ModifierNodeSlices>) -> Option<HitTestNode> {
1248 slices_hit_something(slices).then(|| HitTestNode {
1249 shape: None,
1250 handlers: Rc::clone(slices),
1251 })
1252}
1253
1254fn layer_and_node_bounds(slices: &ModifierNodeSlices, size: Size) -> (Rect, Option<Rect>) {
1257 let node_bounds = Rect::from_size(size);
1258 let layer_bounds = slices.layer_bounds(size);
1259 (
1260 layer_bounds,
1261 (layer_bounds != node_bounds).then_some(node_bounds),
1262 )
1263}
1264
1265fn slices_hit_something(slices: &ModifierNodeSlices) -> bool {
1268 !slices.pointer_inputs().is_empty() || slices.pointer_icon().is_some()
1269}
1270
1271fn build_layer_node_from_data(
1272 applier: &mut MemoryApplier,
1273 node_id: NodeId,
1274 data: SnapshotNodeData,
1275 inherited_motion_context_animated: bool,
1276 inherited_translated_content_context: bool,
1277 parent_abs: Option<AbsOrigin>,
1278) -> Option<LayerNode> {
1279 note_layer_lowered();
1280 let SnapshotNodeData {
1281 layout_state,
1282 modifier_slices,
1283 children,
1284 window_root: _,
1285 } = data;
1286 if !layout_state.is_placed() {
1287 return None;
1288 }
1289
1290 let (local_bounds, node_bounds) = layer_and_node_bounds(&modifier_slices, layout_state.size());
1291 if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
1292 eprintln!(
1293 "[scene-update-diag] build layer node={node_id:?} size=({:.2},{:.2}) pos=({:.2},{:.2})",
1294 layout_state.size().width,
1295 layout_state.size().height,
1296 layout_state.position().x,
1297 layout_state.position().y,
1298 );
1299 }
1300 let clip_to_bounds = modifier_slices.clip_to_bounds();
1301 let graphics_layer = graphics_layer_with_shaped_clip(
1302 modifier_slices.graphics_layer().unwrap_or_default(),
1303 clip_to_bounds,
1304 modifier_slices.corner_shape(),
1305 local_bounds,
1306 );
1307 let transform_to_parent =
1308 layer_transform_to_parent(local_bounds, layout_state.position(), &graphics_layer);
1309 let isolation = isolation_reasons(&graphics_layer);
1310 let cache_policy = layer_cache_policy(&graphics_layer, isolation);
1311 let shadow_clip = clip_to_bounds.then_some(local_bounds);
1312 let hit_test = hit_test_from_slices(&modifier_slices);
1313
1314 modifier_slices.publish_pointer_input_size(layout_state.size());
1315
1316 let node_motion_context_animated =
1317 inherited_motion_context_animated || modifier_slices.motion_context_animated();
1318 let local_translated_content_context = modifier_slices.translated_content_context();
1319 let local_translated_content_offset = modifier_slices
1320 .translated_content_offset()
1321 .unwrap_or(layout_state.content_offset);
1322 let child_translated_content_context =
1323 inherited_translated_content_context || local_translated_content_context;
1324
1325 let this_abs = parent_abs.map(|parent| {
1326 let top_left = Point {
1327 x: parent.content_origin.x + layout_state.position().x,
1328 y: parent.content_origin.y + layout_state.position().y,
1329 };
1330 let layer_translation = Point {
1331 x: parent.layer_translation.x + graphics_layer.translation_x,
1332 y: parent.layer_translation.y + graphics_layer.translation_y,
1333 };
1334 (top_left, layer_translation)
1335 });
1336 if let Some((top_left, layer_translation)) = this_abs {
1337 let window_origin = Point {
1338 x: top_left.x + layer_translation.x,
1339 y: top_left.y + layer_translation.y,
1340 };
1341 if let Some(sink) = modifier_slices.text_window_origin() {
1342 sink.set(window_origin);
1343 }
1344 if let Some(sink) = modifier_slices.viewport_window_rect() {
1345 sink.set(Rect {
1346 x: window_origin.x,
1347 y: window_origin.y,
1348 width: layout_state.size().width,
1349 height: layout_state.size().height,
1350 });
1351 }
1352 }
1353 let child_abs = this_abs.map(|(top_left, layer_translation)| AbsOrigin {
1354 content_origin: Point {
1355 x: top_left.x + layout_state.content_offset.x,
1356 y: top_left.y + layout_state.content_offset.y,
1357 },
1358 layer_translation,
1359 });
1360
1361 let outer_draw_command_count = modifier_slices.outer_draw_command_count();
1362 let outer = outer_draws(
1363 node_id,
1364 modifier_slices.draw_commands(),
1365 outer_draw_command_count,
1366 layout_state.size(),
1367 );
1368 let layer_draw_commands = &modifier_slices.draw_commands()[outer_draw_command_count..];
1369 let mut render_children = Vec::with_capacity(layer_node_capacity(
1370 layer_draw_commands,
1371 children.len(),
1372 modifier_slices.annotated_text().is_some(),
1373 ));
1374 append_draw_nodes(
1375 &mut render_children,
1376 node_id,
1377 layer_draw_commands,
1378 outer_draw_command_count,
1379 DrawPlacement::Behind,
1380 layout_state.size(),
1381 PrimitivePhase::BeforeChildren,
1382 );
1383 if let Some(text) = text_node_from_parts(TextNodeParts {
1384 node_id,
1385 text_rect: modifier_slices.text_content_rect(layout_state.size()),
1386 text_style: modifier_slices.text_style(),
1387 text_layout_options: modifier_slices.text_layout_options(),
1388 text_pan: modifier_slices.text_pan_resolver(),
1389 measured_layout: modifier_slices.measured_text_layout(),
1390 }) {
1391 render_children.push(RenderNode::Primitive(PrimitiveEntry {
1392 phase: PrimitivePhase::BeforeChildren,
1393 node: PrimitiveNode::Text(Box::new(text)),
1394 }));
1395 }
1396 let child_motion_context_animated = node_motion_context_animated;
1397 for child_id in children {
1398 let Some(mut child_layer) = build_layer_node_from_applier_internal(
1399 applier,
1400 child_id,
1401 child_motion_context_animated,
1402 child_translated_content_context,
1403 child_abs,
1404 ) else {
1405 continue;
1406 };
1407 if layout_state.content_offset != Point::default() {
1408 child_layer.transform_to_parent =
1409 child_layer
1410 .transform_to_parent
1411 .then(ProjectiveTransform::translation(
1412 layout_state.content_offset.x,
1413 layout_state.content_offset.y,
1414 ));
1415 }
1416 render_children.push(RenderNode::Layer(Box::new(child_layer)));
1417 }
1418 append_draw_nodes(
1419 &mut render_children,
1420 node_id,
1421 layer_draw_commands,
1422 outer_draw_command_count,
1423 DrawPlacement::Overlay,
1424 layout_state.size(),
1425 PrimitivePhase::AfterChildren,
1426 );
1427 let has_hit_targets = hit_test.is_some()
1428 || render_children.iter().any(|child| match child {
1429 RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
1430 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1431 });
1432 let has_origin_sinks = modifier_slices_have_origin_sinks(&modifier_slices)
1433 || render_children.iter().any(|child| match child {
1434 RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
1435 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1436 });
1437
1438 let layer = LayerNode {
1439 node_id: Some(node_id),
1440 wraps: None,
1441 local_bounds,
1442 node_bounds,
1443 transform_to_parent,
1444 content_offset: layout_state.content_offset,
1445 motion_context_animated: node_motion_context_animated,
1446 translated_content_context: local_translated_content_context,
1447 translated_content_offset: if local_translated_content_context {
1448 local_translated_content_offset
1449 } else {
1450 Point::default()
1451 },
1452 origin_in_parent: layout_state.position(),
1453 graphics_layer,
1454 clip_to_bounds,
1455 shadow_clip,
1456 hit_test,
1457 has_hit_targets,
1458 has_origin_sinks,
1459 draws_within_bounds: false,
1460 isolation,
1461 cache_policy,
1462 cache_hashes: LayerRasterCacheHashes::default(),
1463 cache_hashes_valid: false,
1464 children: render_children,
1465 };
1466 Some(finish_layer(layer, layout_state.position(), outer))
1467}
1468
1469struct RecorderSlot {
1470 generation: u64,
1471 handles: [Option<Rc<CommandRecording>>; 2],
1472}
1473
1474thread_local! {
1475 static COMMAND_RECORDINGS: std::cell::RefCell<
1476 std::collections::HashMap<DrawCommandId, RecorderSlot, cranpose_ui_graphics::FxBuildHasher>,
1477 > = std::cell::RefCell::new(std::collections::HashMap::default());
1478 static RECORDING_GENERATION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
1479}
1480
1481#[doc(hidden)]
1482pub fn clear_command_recordings_for_tests() {
1483 COMMAND_RECORDINGS.with(|map| map.borrow_mut().clear());
1484}
1485
1486fn bump_recording_generation() {
1487 let generation = RECORDING_GENERATION.with(|cell| {
1488 let next = cell.get().wrapping_add(1);
1489 cell.set(next);
1490 next
1491 });
1492 if generation.is_multiple_of(512) {
1493 COMMAND_RECORDINGS.with(|map| {
1494 map.borrow_mut()
1495 .retain(|_, slot| generation.wrapping_sub(slot.generation) <= 64);
1496 });
1497 }
1498}
1499
1500fn acquire_storage(id: DrawCommandId) -> CommandRecording {
1501 COMMAND_RECORDINGS.with(|map| {
1502 let mut map = map.borrow_mut();
1503 let Some(slot) = map.get_mut(&id) else {
1504 return CommandRecording::default();
1505 };
1506 for handle in &mut slot.handles {
1507 if handle
1508 .as_ref()
1509 .is_some_and(|shared| Rc::strong_count(shared) == 1)
1510 {
1511 let shared = handle.take().expect("checked some above");
1512 return Rc::try_unwrap(shared).expect("sole owner checked above");
1513 }
1514 }
1515 CommandRecording::default()
1516 })
1517}
1518
1519fn publish_recording(id: DrawCommandId, recording: CommandRecording) -> Rc<CommandRecording> {
1520 let shared = Rc::new(recording);
1521 COMMAND_RECORDINGS.with(|map| {
1522 let mut map = map.borrow_mut();
1523 let generation = RECORDING_GENERATION.with(Cell::get);
1524 let slot = map.entry(id).or_insert_with(|| RecorderSlot {
1525 generation,
1526 handles: [None, None],
1527 });
1528 slot.generation = generation;
1529 slot.handles[1] = slot.handles[0].take();
1530 slot.handles[0] = Some(shared.clone());
1531 });
1532 shared
1533}
1534
1535fn layer_node_capacity(commands: &[DrawCommand], children: usize, has_text: bool) -> usize {
1536 children
1537 + usize::from(has_text)
1538 + commands.len()
1539 + commands
1540 .iter()
1541 .filter(|command| matches!(command, DrawCommand::WithContent(_)))
1542 .count()
1543}
1544
1545fn draw_nodes(
1546 node_id: NodeId,
1547 commands: &[DrawCommand],
1548 first_command_index: usize,
1549 placement: DrawPlacement,
1550 size: Size,
1551 phase: PrimitivePhase,
1552) -> Vec<RenderNode> {
1553 let mut nodes = Vec::new();
1554 append_draw_nodes(
1555 &mut nodes,
1556 node_id,
1557 commands,
1558 first_command_index,
1559 placement,
1560 size,
1561 phase,
1562 );
1563 nodes
1564}
1565
1566fn append_draw_nodes(
1567 nodes: &mut Vec<RenderNode>,
1568 node_id: NodeId,
1569 commands: &[DrawCommand],
1570 first_command_index: usize,
1571 placement: DrawPlacement,
1572 size: Size,
1573 phase: PrimitivePhase,
1574) {
1575 for (command_index, command) in commands.iter().enumerate() {
1576 let id = DrawCommandId {
1577 node_id,
1578 command_index: (first_command_index + command_index) as u32,
1579 placement,
1580 };
1581 let Some((recording, segments)) =
1582 recording_for_placement_reusing(command, placement, size, || acquire_storage(id))
1583 else {
1584 retain_empty_draw_command(nodes, phase, id, placement, command);
1585 continue;
1586 };
1587 let shared = publish_recording(id, recording);
1588 if shared.is_empty_in(&segments) {
1589 retain_empty_draw_command(nodes, phase, id, placement, command);
1590 continue;
1591 }
1592 nodes.push(RenderNode::DrawRun(DrawRunNode::for_command_shared(
1593 phase,
1594 Some(id),
1595 shared,
1596 segments,
1597 )));
1598 }
1599}
1600
1601fn retain_empty_draw_command(
1602 nodes: &mut Vec<RenderNode>,
1603 phase: PrimitivePhase,
1604 id: DrawCommandId,
1605 placement: DrawPlacement,
1606 command: &DrawCommand,
1607) {
1608 if matches!(
1609 (placement, command),
1610 (DrawPlacement::Behind, DrawCommand::Behind(_))
1611 | (DrawPlacement::Overlay, DrawCommand::Overlay(_))
1612 | (_, DrawCommand::WithContent(_))
1613 ) {
1614 nodes.push(RenderNode::DrawRun(DrawRunNode::for_command(
1615 phase,
1616 Some(id),
1617 Vec::new(),
1618 )));
1619 }
1620}
1621
1622#[doc(hidden)]
1623pub fn draw_command_nodes_for_tests(
1624 node_id: NodeId,
1625 commands: &[DrawCommand],
1626 placement: DrawPlacement,
1627 size: Size,
1628 phase: PrimitivePhase,
1629) -> Vec<RenderNode> {
1630 bump_recording_generation();
1631 draw_nodes(node_id, commands, 0, placement, size, phase)
1632}
1633
1634struct OuterDraws {
1635 behind: Vec<RenderNode>,
1636 overlay: Vec<RenderNode>,
1637}
1638
1639fn outer_draws(
1640 node_id: NodeId,
1641 draw_commands: &[DrawCommand],
1642 outer_draw_command_count: usize,
1643 size: Size,
1644) -> Option<OuterDraws> {
1645 (outer_draw_command_count > 0).then(|| {
1646 let commands = &draw_commands[..outer_draw_command_count];
1647 OuterDraws {
1648 behind: draw_nodes(
1649 node_id,
1650 commands,
1651 0,
1652 DrawPlacement::Behind,
1653 size,
1654 PrimitivePhase::BeforeChildren,
1655 ),
1656 overlay: draw_nodes(
1657 node_id,
1658 commands,
1659 0,
1660 DrawPlacement::Overlay,
1661 size,
1662 PrimitivePhase::AfterChildren,
1663 ),
1664 }
1665 })
1666}
1667
1668fn finish_layer(mut layer: LayerNode, placement: Point, outer: Option<OuterDraws>) -> LayerNode {
1669 layer.draws_within_bounds = layer.content_draws_within_bounds();
1670 match outer {
1671 Some(outer) => wrap_layer_with_outer_draws(layer, placement, outer),
1672 None => layer,
1673 }
1674}
1675
1676fn wrap_layer_with_outer_draws(
1677 mut layer: LayerNode,
1678 placement: Point,
1679 outer: OuterDraws,
1680) -> LayerNode {
1681 layer.transform_to_parent =
1682 layer_transform_to_parent(layer.local_bounds, Point::default(), &layer.graphics_layer);
1683 layer.origin_in_parent = Point::default();
1684 let node_rect = layer.node_rect();
1686 let wrapper = LayerNode {
1687 wraps: layer.node_id,
1688 local_bounds: node_rect,
1689 transform_to_parent: layer_transform_to_parent(
1690 node_rect,
1691 placement,
1692 &GraphicsLayer::default(),
1693 ),
1694 origin_in_parent: placement,
1695 motion_context_animated: layer.motion_context_animated,
1696 has_hit_targets: layer.has_hit_targets,
1697 has_origin_sinks: layer.has_origin_sinks,
1698 ..Default::default()
1699 };
1700 let mut children = outer.behind;
1701 children.push(RenderNode::Layer(Box::new(layer)));
1702 children.extend(outer.overlay);
1703 let mut wrapper = LayerNode {
1704 children,
1705 ..wrapper
1706 };
1707 wrapper.draws_within_bounds = wrapper.content_draws_within_bounds();
1708 wrapper
1709}
1710
1711fn layer_identity(layer: &LayerNode) -> Option<NodeId> {
1712 layer.node_id.or(layer.wraps)
1713}
1714
1715struct TextNodeParts<'a> {
1716 node_id: NodeId,
1717 text_rect: Rect,
1719 text_style: Option<&'a TextStyle>,
1720 text_layout_options: Option<TextLayoutOptions>,
1721 text_pan: Option<TextPanResolver>,
1722 measured_layout: Option<Rc<PreparedTextLayout>>,
1723}
1724
1725fn text_node_from_parts(parts: TextNodeParts<'_>) -> Option<TextPrimitiveNode> {
1726 let TextNodeParts {
1727 node_id,
1728 text_rect,
1729 text_style,
1730 text_layout_options,
1731 text_pan,
1732 measured_layout,
1733 } = parts;
1734 let prepared = measured_layout?;
1735 let default_text_style;
1736 let text_style = match text_style {
1737 Some(style) => style,
1738 None => {
1739 default_text_style = TextStyle::default();
1740 &default_text_style
1741 }
1742 };
1743 let options = text_layout_options.unwrap_or_default().normalized();
1744 let content_width = text_rect.width.max(0.0);
1745 if content_width <= 0.0 {
1746 return None;
1747 }
1748
1749 let pan_offset = text_pan
1750 .as_ref()
1751 .map_or(0.0, |resolve| resolve(content_width));
1752 let pans_horizontally = text_pan.is_some();
1753
1754 let visual_style = &prepared.visual_style;
1755 let measured_draw_width = prepared.metrics.width.max(0.0);
1756 let draw_width = if options.overflow == TextOverflow::Visible || pans_horizontally {
1757 measured_draw_width
1758 } else {
1759 measured_draw_width.min(content_width)
1760 };
1761 let alignment_offset = resolve_text_horizontal_offset(
1762 text_style,
1763 prepared.text.text.as_str(),
1764 content_width,
1765 prepared.metrics.width,
1766 );
1767 let rect = Rect {
1768 x: text_rect.x + alignment_offset - pan_offset,
1769 y: text_rect.y,
1770 width: draw_width,
1771 height: prepared.metrics.height,
1772 };
1773 let text_bounds = Rect {
1774 width: content_width,
1775 height: text_rect.height.max(0.0),
1776 ..text_rect
1777 };
1778 let font_size = visual_style.resolve_font_size(14.0);
1779 let expanded_bounds =
1780 expand_text_bounds_for_baseline_shift(text_bounds, visual_style, font_size);
1781 let clip = if options.overflow == TextOverflow::Visible && !pans_horizontally {
1782 None
1783 } else {
1784 Some(pad_clip_rect(expanded_bounds))
1785 };
1786
1787 Some(TextPrimitiveNode {
1788 node_id,
1789 rect,
1790 text: Rc::clone(&prepared.text),
1791 render_text: prepared.render_text(),
1792 text_style: std::sync::Arc::clone(visual_style),
1793 font_size,
1794 layout_options: options,
1795 clip,
1796 })
1797}
1798
1799fn layout_box_to_snapshot(node: &LayoutBox, parent: Option<&LayoutBox>) -> BuildNodeSnapshot {
1800 let placement = parent
1801 .map(|parent_box| Point {
1802 x: node.rect.x - parent_box.rect.x - parent_box.content_offset.x,
1803 y: node.rect.y - parent_box.rect.y - parent_box.content_offset.y,
1804 })
1805 .unwrap_or_default();
1806 let mut children = Vec::with_capacity(node.children.len());
1807 for child in &node.children {
1808 children.push(layout_box_to_snapshot(child, Some(node)));
1809 }
1810 let base_graphics_layer = node.node_data.modifier_slices.graphics_layer();
1811 let graphics_layer = graphics_layer_with_shaped_clip(
1812 base_graphics_layer.clone().unwrap_or_default(),
1813 node.node_data.modifier_slices.clip_to_bounds(),
1814 node.node_data.modifier_slices.corner_shape(),
1815 Rect {
1816 x: 0.0,
1817 y: 0.0,
1818 width: node.rect.width,
1819 height: node.rect.height,
1820 },
1821 );
1822 let has_graphics_layer =
1823 base_graphics_layer.is_some() || graphics_layer.render_effect.is_some();
1824
1825 BuildNodeSnapshot {
1826 node_id: node.node_id,
1827 placement,
1828 size: Size {
1829 width: node.rect.width,
1830 height: node.rect.height,
1831 },
1832 content_offset: node.content_offset,
1833 slices: Rc::clone(&node.node_data.modifier_slices),
1834 graphics_layer: has_graphics_layer.then_some(graphics_layer),
1835 children,
1836 }
1837}
1838
1839fn modifier_slices_have_origin_sinks(slices: &ModifierNodeSlices) -> bool {
1840 slices.text_window_origin().is_some() || slices.viewport_window_rect().is_some()
1841}
1842
1843fn graphics_layer_with_shaped_clip(
1844 mut graphics_layer: GraphicsLayer,
1845 clip_to_bounds: bool,
1846 corner_shape: Option<RoundedCornerShape>,
1847 local_bounds: Rect,
1848) -> GraphicsLayer {
1849 if !clip_to_bounds {
1850 return graphics_layer;
1851 }
1852
1853 let Some(corner_shape) = corner_shape else {
1854 return graphics_layer;
1855 };
1856 let radii = corner_shape.resolve(local_bounds.width, local_bounds.height);
1857 if radii.top_left <= f32::EPSILON
1858 && radii.top_right <= f32::EPSILON
1859 && radii.bottom_right <= f32::EPSILON
1860 && radii.bottom_left <= f32::EPSILON
1861 {
1862 return graphics_layer;
1863 }
1864
1865 if let Some(existing) = graphics_layer.render_effect.take() {
1866 let rounded_clip = rounded_corner_alpha_mask_effect(
1867 local_bounds.width,
1868 local_bounds.height,
1869 radii,
1870 ROUNDED_CLIP_EDGE_FEATHER,
1871 );
1872 graphics_layer.render_effect = Some(existing.then(rounded_clip));
1873 } else {
1874 graphics_layer.shape = LayerShape::Rounded(corner_shape);
1875 graphics_layer.clip = true;
1876 }
1877 graphics_layer
1878}
1879
1880fn layer_cache_policy(layer: &GraphicsLayer, isolation: IsolationReasons) -> CachePolicy {
1881 if isolation.has_any() || layer_scales_or_rotates(layer) {
1882 CachePolicy::Auto
1883 } else {
1884 CachePolicy::None
1885 }
1886}
1887
1888fn isolation_reasons(layer: &GraphicsLayer) -> IsolationReasons {
1889 IsolationReasons {
1890 explicit_offscreen: layer.compositing_strategy == CompositingStrategy::Offscreen,
1891 shape_clip: layer.clip && !matches!(layer.shape, LayerShape::Rectangle),
1892 effect: layer.render_effect.is_some(),
1893 backdrop: layer.backdrop_effect.is_some(),
1894 group_opacity: layer.compositing_strategy != CompositingStrategy::ModulateAlpha
1895 && layer.alpha < 1.0,
1896 blend_mode: layer.blend_mode != cranpose_ui::BlendMode::SrcOver,
1897 }
1898}
1899
1900fn pad_clip_rect(rect: Rect) -> Rect {
1901 Rect {
1902 x: rect.x - TEXT_CLIP_PAD,
1903 y: rect.y - TEXT_CLIP_PAD,
1904 width: (rect.width + TEXT_CLIP_PAD * 2.0).max(0.0),
1905 height: (rect.height + TEXT_CLIP_PAD * 2.0).max(0.0),
1906 }
1907}
1908
1909pub fn expand_text_bounds_for_baseline_shift(
1910 text_bounds: Rect,
1911 text_style: &TextStyle,
1912 font_size: f32,
1913) -> Rect {
1914 let baseline_shift_px = text_style
1915 .span_style
1916 .baseline_shift
1917 .filter(|shift| shift.is_specified())
1918 .map_or(0.0, |shift| -(shift.0 * font_size));
1919 if baseline_shift_px == 0.0 {
1920 return text_bounds;
1921 }
1922
1923 if baseline_shift_px < 0.0 {
1924 Rect {
1925 x: text_bounds.x,
1926 y: text_bounds.y + baseline_shift_px,
1927 width: text_bounds.width,
1928 height: (text_bounds.height - baseline_shift_px).max(0.0),
1929 }
1930 } else {
1931 Rect {
1932 x: text_bounds.x,
1933 y: text_bounds.y,
1934 width: text_bounds.width,
1935 height: (text_bounds.height + baseline_shift_px).max(0.0),
1936 }
1937 }
1938}
1939
1940fn resolve_text_horizontal_offset(
1941 text_style: &TextStyle,
1942 text: &str,
1943 content_width: f32,
1944 measured_width: f32,
1945) -> f32 {
1946 let remaining = (content_width - measured_width).max(0.0);
1947 remaining * cranpose_ui::text::text_align_fraction(text_style, text)
1948}
1949
1950#[cfg(test)]
1951#[path = "tests/scene_builder_tests.rs"]
1952mod tests;