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