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 clip_to_bounds: bool,
512 graphics_layer: GraphicsLayer,
513}
514
515fn translated_container(
516 container: &LayerNode,
517 layout_state: &cranpose_ui::widgets::LayoutState,
518 modifier_slices: &ModifierNodeSlices,
519 inherited_motion_context_animated: bool,
520 wrapped: bool,
521) -> Result<TranslatedContainer, &'static str> {
522 if cranpose_core::env_flag!("CRANPOSE_DISABLE_SCROLL_TRANSLATE") {
523 return Err("fast path disabled by ablation switch");
524 }
525 let Some(node_id) = container.node_id else {
526 return Err("no node id");
527 };
528 if container
529 .children
530 .iter()
531 .any(|child| !matches!(child, RenderNode::Layer(_)))
532 {
533 return Err("container has own primitive children");
534 }
535 if !layout_state.is_placed()
536 || layout_state.size().width != container.local_bounds.width
537 || layout_state.size().height != container.local_bounds.height
538 {
539 return Err("container unplaced or resized");
540 }
541 let outer_count = modifier_slices.outer_draw_command_count();
542 if (outer_count > 0 && !wrapped)
543 || !modifier_slices.draw_commands()[outer_count..].is_empty()
544 || (inherited_motion_context_animated || modifier_slices.motion_context_animated())
545 != container.motion_context_animated
546 || modifier_slices.annotated_text().is_some()
547 || modifier_slices.translated_content_context() != container.translated_content_context
548 {
549 return Err("container draw/text/translated-context changed");
550 }
551 let clip_to_bounds = modifier_slices.clip_to_bounds();
552 if clip_to_bounds != container.clip_to_bounds {
553 return Err("container clip changed");
554 }
555 let graphics_layer = graphics_layer_with_shaped_clip(
556 modifier_slices.graphics_layer().unwrap_or_default(),
557 clip_to_bounds,
558 modifier_slices.corner_shape(),
559 container.local_bounds,
560 );
561 if graphics_layer != container.graphics_layer {
562 return Err("container graphics layer changed");
563 }
564 Ok(TranslatedContainer {
565 node_id,
566 clip_to_bounds,
567 graphics_layer,
568 })
569}
570
571struct TranslatedChildren {
572 placed_fresh: SmallVec<[(NodeId, cranpose_ui::widgets::LayoutState); 8]>,
573 children_unchanged: bool,
574 old_index_by_id: std::collections::HashMap<NodeId, usize>,
575}
576
577fn translated_children(
578 applier: &mut MemoryApplier,
579 container: &LayerNode,
580 dirty_nodes: &HashSet<NodeId>,
581 fresh_children: &[NodeId],
582) -> Result<TranslatedChildren, &'static str> {
583 let mut placed_fresh = SmallVec::<[_; 8]>::with_capacity(fresh_children.len());
584 for child_id in fresh_children {
585 let state = applier
586 .with_node::<LayoutNode, _>(*child_id, |node| node.layout_state())
587 .or_else(|_| {
588 applier.with_node::<SubcomposeLayoutNode, _>(*child_id, |node| node.layout_state())
589 });
590 let Ok(state) = state else {
591 continue;
592 };
593 if !state.is_placed() {
594 continue;
595 }
596 placed_fresh.push((*child_id, state));
597 }
598 let children_unchanged = container.children.len() == placed_fresh.len()
599 && container
600 .children
601 .iter()
602 .zip(&placed_fresh)
603 .all(|(child, (id, _))| {
604 matches!(child, RenderNode::Layer(layer) if layer_identity(layer) == Some(*id))
605 });
606 let old_index_by_id = if children_unchanged {
607 std::collections::HashMap::new()
608 } else {
609 let Some(index): Option<std::collections::HashMap<NodeId, usize>> = container
610 .children
611 .iter()
612 .enumerate()
613 .map(|(index, child)| match child {
614 RenderNode::Layer(layer) => layer_identity(layer).map(|id| (id, index)),
615 _ => None,
616 })
617 .collect()
618 else {
619 return Err("child without node id");
620 };
621 index
622 };
623 check_retained_children(
624 container,
625 dirty_nodes,
626 &placed_fresh,
627 children_unchanged,
628 &old_index_by_id,
629 )?;
630 Ok(TranslatedChildren {
631 placed_fresh,
632 children_unchanged,
633 old_index_by_id,
634 })
635}
636
637fn check_retained_children(
638 container: &LayerNode,
639 dirty_nodes: &HashSet<NodeId>,
640 placed_fresh: &[(NodeId, cranpose_ui::widgets::LayoutState)],
641 children_unchanged: bool,
642 old_index_by_id: &std::collections::HashMap<NodeId, usize>,
643) -> Result<(), &'static str> {
644 for (fresh_index, (child_id, state)) in placed_fresh.iter().enumerate() {
645 let old_index = if children_unchanged {
646 fresh_index
647 } else if let Some(index) = old_index_by_id.get(child_id) {
648 *index
649 } else {
650 continue;
651 };
652 let RenderNode::Layer(layer) = &container.children[old_index] else {
653 return Err("retained child slot is not a layer");
654 };
655 if dirty_nodes.contains(child_id) {
656 continue;
657 }
658 if layer.has_origin_sinks {
659 return Err("child subtree publishes window origins");
660 }
661 if state.size().width != layer.local_bounds.width
662 || state.size().height != layer.local_bounds.height
663 {
664 return Err("child resized");
665 }
666 }
667 Ok(())
668}
669
670#[derive(Clone, Copy)]
671struct TranslateGeometry {
672 content_offset: Point,
673 layer_translation: Point,
674 window_origin: Point,
675 child_origin: Point,
676}
677
678impl TranslateGeometry {
679 fn new(
680 layout_state: &cranpose_ui::widgets::LayoutState,
681 graphics_layer: &GraphicsLayer,
682 parent_abs: AbsOrigin,
683 ) -> Self {
684 let content_offset = layout_state.content_offset;
685 let top_left = Point {
686 x: parent_abs.content_origin.x + layout_state.position().x,
687 y: parent_abs.content_origin.y + layout_state.position().y,
688 };
689 let layer_translation = Point {
690 x: parent_abs.layer_translation.x + graphics_layer.translation_x,
691 y: parent_abs.layer_translation.y + graphics_layer.translation_y,
692 };
693 Self {
694 content_offset,
695 layer_translation,
696 window_origin: Point {
697 x: top_left.x + layer_translation.x,
698 y: top_left.y + layer_translation.y,
699 },
700 child_origin: Point {
701 x: top_left.x + content_offset.x,
702 y: top_left.y + content_offset.y,
703 },
704 }
705 }
706}
707
708fn build_entering_children(
709 applier: &mut MemoryApplier,
710 container: &LayerNode,
711 placed_fresh: &[(NodeId, cranpose_ui::widgets::LayoutState)],
712 retained: (bool, &std::collections::HashMap<NodeId, usize>),
713 geometry: TranslateGeometry,
714 inherited: (bool, bool),
715) -> std::collections::HashMap<NodeId, LayerNode> {
716 let (children_unchanged, old_index_by_id) = retained;
717 let (child_inherited_translated_content_context, children_ancestor_hashed) = inherited;
718 let mut entering: std::collections::HashMap<NodeId, LayerNode> =
719 std::collections::HashMap::new();
720 for (child_id, _) in placed_fresh {
721 if children_unchanged || old_index_by_id.contains_key(child_id) {
722 continue;
723 }
724 let Some(mut lowered) = build_layer_node_from_applier_internal(
725 applier,
726 *child_id,
727 container.motion_context_animated,
728 child_inherited_translated_content_context,
729 Some(AbsOrigin {
730 content_origin: geometry.child_origin,
731 layer_translation: geometry.layer_translation,
732 }),
733 ) else {
734 continue;
735 };
736 if geometry.content_offset != Point::default() {
737 lowered.transform_to_parent =
738 lowered
739 .transform_to_parent
740 .then(ProjectiveTransform::translation(
741 geometry.content_offset.x,
742 geometry.content_offset.y,
743 ));
744 }
745 crate::graph_hash::recompute_layer_raster_cache_hashes_under(
746 &mut lowered,
747 children_ancestor_hashed,
748 );
749 entering.insert(*child_id, lowered);
750 }
751 entering
752}
753
754fn apply_translated_container_state(
755 container: &mut LayerNode,
756 modifier_slices: &ModifierNodeSlices,
757 layout_state: &cranpose_ui::widgets::LayoutState,
758 graphics_layer: &GraphicsLayer,
759 parent_content_offset: Point,
760 geometry: TranslateGeometry,
761) {
762 let mut transform = layer_transform_to_parent(
763 container.local_bounds,
764 layout_state.position(),
765 graphics_layer,
766 );
767 if parent_content_offset != Point::default() {
768 transform = transform.then(ProjectiveTransform::translation(
769 parent_content_offset.x,
770 parent_content_offset.y,
771 ));
772 }
773 container.transform_to_parent = transform;
774 container.content_offset = geometry.content_offset;
775 if container.translated_content_context {
776 container.translated_content_offset = modifier_slices
777 .translated_content_offset()
778 .unwrap_or(geometry.content_offset);
779 }
780 if let Some(sink) = modifier_slices.text_window_origin() {
781 sink.set(geometry.window_origin);
782 }
783 if let Some(sink) = modifier_slices.viewport_window_rect() {
784 sink.set(Rect {
785 x: geometry.window_origin.x,
786 y: geometry.window_origin.y,
787 width: layout_state.size().width,
788 height: layout_state.size().height,
789 });
790 }
791 container.origin_in_parent = layout_state.position();
792}
793
794fn reconcile_translated_children(
795 container: &mut LayerNode,
796 dirty_nodes: &mut HashSet<NodeId>,
797 changed_nodes: &mut Vec<NodeId>,
798 placed_fresh: &[(NodeId, cranpose_ui::widgets::LayoutState)],
799 children_unchanged: bool,
800 entering: &mut std::collections::HashMap<NodeId, LayerNode>,
801 geometry: TranslateGeometry,
802) {
803 if children_unchanged {
804 for (child, (child_id, state)) in container.children.iter_mut().zip(placed_fresh) {
805 let RenderNode::Layer(layer) = child else {
806 unreachable!("retained child identities were checked");
807 };
808 if !dirty_nodes.contains(child_id) {
809 translate_retained_child(layer, state, geometry.content_offset);
810 changed_nodes.push(*child_id);
811 }
812 }
813 return;
814 }
815 let fresh_id_set: HashSet<NodeId> = placed_fresh.iter().map(|(id, _)| *id).collect();
816 let mut old_by_id: std::collections::HashMap<NodeId, Box<LayerNode>> =
817 std::collections::HashMap::new();
818 for child in container.children.drain(..) {
819 let RenderNode::Layer(layer) = child else {
820 continue;
821 };
822 let child_id = layer_identity(&layer).expect("checked above");
823 if fresh_id_set.contains(&child_id) {
824 old_by_id.insert(child_id, layer);
825 } else {
826 collect_layer_node_ids(&layer, changed_nodes);
827 }
828 }
829 let mut new_children = Vec::with_capacity(placed_fresh.len());
830 for (child_id, state) in placed_fresh {
831 if let Some(mut layer) = old_by_id.remove(child_id) {
832 if !dirty_nodes.contains(child_id) {
833 translate_retained_child(&mut layer, state, geometry.content_offset);
834 changed_nodes.push(*child_id);
835 }
836 new_children.push(RenderNode::Layer(layer));
837 } else if let Some(lowered) = entering.remove(child_id) {
838 dirty_nodes.remove(child_id);
839 remove_dirty_descendants(&lowered, dirty_nodes);
840 collect_layer_node_ids(&lowered, changed_nodes);
841 new_children.push(RenderNode::Layer(Box::new(lowered)));
842 }
843 }
844 container.children = new_children;
845}
846
847fn translate_layer_from_data(
848 applier: &mut MemoryApplier,
849 container: &mut LayerNode,
850 dirty_nodes: &mut HashSet<NodeId>,
851 changed_nodes: &mut Vec<NodeId>,
852 ancestors: TranslateAncestorContext,
853 data: SnapshotNodeData,
854 wrapped: bool,
855) -> bool {
856 let TranslateAncestorContext {
857 inherited_motion_context_animated,
858 ancestor_hashed: container_ancestor_hashed,
859 inherited_translated_content_context,
860 parent_content_offset,
861 parent_abs,
862 } = ancestors;
863 let SnapshotNodeData {
864 layout_state,
865 modifier_slices,
866 children: fresh_children,
867 window_root,
868 } = data;
869 let layout_state = if window_root {
870 layout_state.at_origin()
871 } else {
872 layout_state
873 };
874 let container_plan = match translated_container(
875 container,
876 &layout_state,
877 &modifier_slices,
878 inherited_motion_context_animated,
879 wrapped,
880 ) {
881 Ok(plan) => plan,
882 Err(reason) => return translate_bail(reason),
883 };
884 let TranslatedContainer {
885 node_id,
886 clip_to_bounds,
887 graphics_layer,
888 } = container_plan;
889 let child_plan = match translated_children(applier, container, dirty_nodes, &fresh_children) {
890 Ok(plan) => plan,
891 Err(reason) => return translate_bail(reason),
892 };
893 let TranslatedChildren {
894 placed_fresh,
895 children_unchanged,
896 old_index_by_id,
897 } = child_plan;
898
899 let geometry = TranslateGeometry::new(&layout_state, &graphics_layer, parent_abs);
900 let child_inherited_translated_content_context =
901 inherited_translated_content_context || container.translated_content_context;
902 let children_ancestor_hashed =
903 crate::graph_hash::layer_children_ancestor_hashed(container, container_ancestor_hashed);
904 let mut entering = build_entering_children(
905 applier,
906 container,
907 &placed_fresh,
908 (children_unchanged, &old_index_by_id),
909 geometry,
910 (
911 child_inherited_translated_content_context,
912 children_ancestor_hashed,
913 ),
914 );
915
916 apply_translated_container_state(
917 container,
918 &modifier_slices,
919 &layout_state,
920 &graphics_layer,
921 parent_content_offset,
922 geometry,
923 );
924
925 reconcile_translated_children(
926 container,
927 dirty_nodes,
928 changed_nodes,
929 &placed_fresh,
930 children_unchanged,
931 &mut entering,
932 geometry,
933 );
934 modifier_slices.publish_pointer_input_size(layout_state.size());
935 container.hit_test = hit_test_from_slices(
936 &modifier_slices,
937 container.local_bounds,
938 clip_to_bounds || graphics_layer.clip,
939 );
940
941 container.has_hit_targets = container.hit_test.is_some()
942 || container.children.iter().any(|child| match child {
943 RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
944 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
945 });
946 container.has_origin_sinks = modifier_slices_have_origin_sinks(&modifier_slices)
947 || container.children.iter().any(|child| match child {
948 RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
949 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
950 });
951
952 container.draws_within_bounds = container.content_draws_within_bounds();
953 crate::graph_hash::refresh_layer_own_raster_cache_hashes(container, container_ancestor_hashed);
954 changed_nodes.push(node_id);
955 true
956}
957
958fn translate_retained_child(
959 layer: &mut LayerNode,
960 state: &cranpose_ui::widgets::LayoutState,
961 content_offset: Point,
962) {
963 let mut child_transform =
964 layer_transform_to_parent(layer.local_bounds, state.position(), &layer.graphics_layer);
965 if content_offset != Point::default() {
966 child_transform = child_transform.then(ProjectiveTransform::translation(
967 content_offset.x,
968 content_offset.y,
969 ));
970 }
971 layer.transform_to_parent = child_transform;
972 layer.origin_in_parent = state.position();
973}
974
975fn layer_hit_graph_state_dirty(previous: &LayerNode, replacement: &LayerNode) -> bool {
976 if previous.hit_test.is_some() || replacement.hit_test.is_some() {
977 return true;
978 }
979
980 if !(previous.has_hit_targets || replacement.has_hit_targets) {
981 return false;
982 }
983
984 previous.has_hit_targets != replacement.has_hit_targets
985 || previous.local_bounds != replacement.local_bounds
986 || previous.transform_to_parent != replacement.transform_to_parent
987 || previous.clip_rect() != replacement.clip_rect()
988 || previous.graphics_layer.shape != replacement.graphics_layer.shape
989}
990
991fn collect_layer_node_ids(layer: &LayerNode, out: &mut Vec<NodeId>) {
992 if let Some(node_id) = layer.node_id {
993 out.push(node_id);
994 }
995 for child in &layer.children {
996 if let RenderNode::Layer(child_layer) = child {
997 collect_layer_node_ids(child_layer, out);
998 }
999 }
1000}
1001
1002fn remove_dirty_descendants(layer: &LayerNode, dirty_nodes: &mut HashSet<NodeId>) {
1003 for child in &layer.children {
1004 let RenderNode::Layer(child_layer) = child else {
1005 continue;
1006 };
1007 if let Some(node_id) = child_layer.node_id {
1008 dirty_nodes.remove(&node_id);
1009 }
1010 remove_dirty_descendants(child_layer, dirty_nodes);
1011 }
1012}
1013
1014fn build_layer_node(
1015 snapshot: BuildNodeSnapshot,
1016 _root_scale: f32,
1017 inherited_motion_context_animated: bool,
1018) -> LayerNode {
1019 build_layer_node_internal(snapshot, inherited_motion_context_animated, false)
1020}
1021
1022fn build_layer_node_internal(
1023 snapshot: BuildNodeSnapshot,
1024 inherited_motion_context_animated: bool,
1025 inherited_translated_content_context: bool,
1026) -> LayerNode {
1027 let BuildNodeSnapshot {
1028 node_id,
1029 placement,
1030 size,
1031 content_offset,
1032 slices,
1033 graphics_layer,
1034 children: child_snapshots,
1035 } = snapshot;
1036 let motion_context_animated = slices.motion_context_animated();
1037 let translated_content_context = slices.translated_content_context();
1038 let has_own_origin_sinks = modifier_slices_have_origin_sinks(&slices);
1039 let measured_text_layout = slices.measured_text_layout();
1040 let draw_commands = slices.draw_commands();
1041 let outer_draw_command_count = slices.outer_draw_command_count();
1042 let clip_to_bounds = slices.clip_to_bounds();
1043 let text_style = slices.text_style();
1044 let text_layout_options = slices.text_layout_options();
1045 let text_pan = slices.text_pan_resolver();
1046 let outer = outer_draws(node_id, draw_commands, outer_draw_command_count, size);
1047 let layer_draw_commands = &draw_commands[outer_draw_command_count..];
1048 let local_bounds = Rect {
1049 x: 0.0,
1050 y: 0.0,
1051 width: size.width,
1052 height: size.height,
1053 };
1054 let graphics_layer = graphics_layer.unwrap_or_default();
1055 let transform_to_parent = layer_transform_to_parent(local_bounds, placement, &graphics_layer);
1056 let isolation = isolation_reasons(&graphics_layer);
1057 let cache_policy = layer_cache_policy(&graphics_layer, isolation);
1058 let shadow_clip = clip_to_bounds.then_some(local_bounds);
1059 let hit_test = slices_hit_something(&slices).then(|| HitTestNode {
1060 shape: None,
1061 handlers: Rc::clone(&slices),
1062 clip: (clip_to_bounds || graphics_layer.clip).then_some(local_bounds),
1063 });
1064
1065 let node_motion_context_animated = inherited_motion_context_animated || motion_context_animated;
1066 let child_translated_content_context =
1067 inherited_translated_content_context || translated_content_context;
1068
1069 let mut children = Vec::with_capacity(layer_node_capacity(
1070 layer_draw_commands,
1071 child_snapshots.len(),
1072 measured_text_layout.is_some(),
1073 ));
1074 append_draw_nodes(
1075 &mut children,
1076 node_id,
1077 layer_draw_commands,
1078 outer_draw_command_count,
1079 DrawPlacement::Behind,
1080 size,
1081 PrimitivePhase::BeforeChildren,
1082 );
1083 if let Some(text) = text_node_from_parts(TextNodeParts {
1084 node_id,
1085 text_rect: slices.text_content_rect(size),
1086 text_style,
1087 text_layout_options,
1088 text_pan,
1089 measured_layout: measured_text_layout,
1090 }) {
1091 children.push(RenderNode::Primitive(PrimitiveEntry {
1092 phase: PrimitivePhase::BeforeChildren,
1093 node: PrimitiveNode::Text(Box::new(text)),
1094 }));
1095 }
1096 let child_motion_context_animated = node_motion_context_animated;
1097 for child in child_snapshots {
1098 let mut child_layer = build_layer_node_internal(
1099 child,
1100 child_motion_context_animated,
1101 child_translated_content_context,
1102 );
1103 if content_offset != Point::default() {
1104 child_layer.transform_to_parent =
1105 child_layer
1106 .transform_to_parent
1107 .then(ProjectiveTransform::translation(
1108 content_offset.x,
1109 content_offset.y,
1110 ));
1111 }
1112 children.push(RenderNode::Layer(Box::new(child_layer)));
1113 }
1114 append_draw_nodes(
1115 &mut children,
1116 node_id,
1117 layer_draw_commands,
1118 outer_draw_command_count,
1119 DrawPlacement::Overlay,
1120 size,
1121 PrimitivePhase::AfterChildren,
1122 );
1123 let has_hit_targets = hit_test.is_some()
1124 || children.iter().any(|child| match child {
1125 RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
1126 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1127 });
1128 let has_origin_sinks = has_own_origin_sinks
1129 || children.iter().any(|child| match child {
1130 RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
1131 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1132 });
1133
1134 let layer = LayerNode {
1135 node_id: Some(node_id),
1136 wraps: None,
1137 local_bounds,
1138 transform_to_parent,
1139 content_offset,
1140 motion_context_animated: node_motion_context_animated,
1141 translated_content_context,
1142 translated_content_offset: if translated_content_context {
1143 content_offset
1144 } else {
1145 Point::default()
1146 },
1147 origin_in_parent: placement,
1148 graphics_layer,
1149 clip_to_bounds,
1150 shadow_clip,
1151 hit_test,
1152 has_hit_targets,
1153 has_origin_sinks,
1154 draws_within_bounds: false,
1155 isolation,
1156 cache_policy,
1157 cache_hashes: LayerRasterCacheHashes::default(),
1158 cache_hashes_valid: false,
1159 children,
1160 };
1161 finish_layer(layer, placement, outer)
1162}
1163
1164#[derive(Clone, Copy)]
1165struct AbsOrigin {
1166 content_origin: Point,
1167 layer_translation: Point,
1168}
1169
1170impl AbsOrigin {
1171 const ROOT: AbsOrigin = AbsOrigin {
1172 content_origin: Point { x: 0.0, y: 0.0 },
1173 layer_translation: Point { x: 0.0, y: 0.0 },
1174 };
1175
1176 fn children_of(self, layer: &LayerNode) -> AbsOrigin {
1177 AbsOrigin {
1178 content_origin: Point {
1179 x: self.content_origin.x + layer.origin_in_parent.x + layer.content_offset.x,
1180 y: self.content_origin.y + layer.origin_in_parent.y + layer.content_offset.y,
1181 },
1182 layer_translation: Point {
1183 x: self.layer_translation.x + layer.graphics_layer.translation_x,
1184 y: self.layer_translation.y + layer.graphics_layer.translation_y,
1185 },
1186 }
1187 }
1188}
1189
1190fn build_layer_node_from_applier(
1191 applier: &mut MemoryApplier,
1192 node_id: NodeId,
1193 _root_scale: f32,
1194 inherited_motion_context_animated: bool,
1195) -> Option<LayerNode> {
1196 let mut data = snapshot_node_data(applier, node_id)?;
1197 if data.window_root {
1198 data.layout_state = data.layout_state.at_origin();
1199 }
1200 build_layer_node_from_data(
1201 applier,
1202 node_id,
1203 data,
1204 inherited_motion_context_animated,
1205 false,
1206 Some(AbsOrigin::ROOT),
1207 )
1208}
1209
1210fn snapshot_node_data(applier: &mut MemoryApplier, node_id: NodeId) -> Option<SnapshotNodeData> {
1211 if let Ok(data) = applier.with_node::<LayoutNode, _>(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: node.is_window_root(),
1221 }
1222 }) {
1223 return Some(data);
1224 }
1225
1226 applier
1227 .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1228 let state = node.layout_state();
1229 let mut children = SmallVec::new();
1230 node.collect_children_into(&mut children);
1231 let modifier_slices = node.modifier_slices_snapshot();
1232 SnapshotNodeData {
1233 layout_state: state,
1234 modifier_slices,
1235 children,
1236 window_root: false,
1237 }
1238 })
1239 .ok()
1240}
1241
1242fn build_layer_node_from_applier_internal(
1243 applier: &mut MemoryApplier,
1244 node_id: NodeId,
1245 inherited_motion_context_animated: bool,
1246 inherited_translated_content_context: bool,
1247 parent_abs: Option<AbsOrigin>,
1248) -> Option<LayerNode> {
1249 let data = snapshot_node_data(applier, node_id)?;
1250 if data.window_root {
1251 return None;
1252 }
1253 build_layer_node_from_data(
1254 applier,
1255 node_id,
1256 data,
1257 inherited_motion_context_animated,
1258 inherited_translated_content_context,
1259 parent_abs,
1260 )
1261}
1262
1263fn hit_test_from_slices(
1264 slices: &Rc<ModifierNodeSlices>,
1265 bounds: Rect,
1266 clip: bool,
1267) -> Option<HitTestNode> {
1268 slices_hit_something(slices).then(|| HitTestNode {
1269 shape: None,
1270 handlers: Rc::clone(slices),
1271 clip: clip.then_some(bounds),
1272 })
1273}
1274
1275fn slices_hit_something(slices: &ModifierNodeSlices) -> bool {
1278 !slices.pointer_inputs().is_empty() || slices.pointer_icon().is_some()
1279}
1280
1281fn build_layer_node_from_data(
1282 applier: &mut MemoryApplier,
1283 node_id: NodeId,
1284 data: SnapshotNodeData,
1285 inherited_motion_context_animated: bool,
1286 inherited_translated_content_context: bool,
1287 parent_abs: Option<AbsOrigin>,
1288) -> Option<LayerNode> {
1289 note_layer_lowered();
1290 let SnapshotNodeData {
1291 layout_state,
1292 modifier_slices,
1293 children,
1294 window_root: _,
1295 } = data;
1296 if !layout_state.is_placed() {
1297 return None;
1298 }
1299
1300 let local_bounds = Rect {
1301 x: 0.0,
1302 y: 0.0,
1303 width: layout_state.size().width,
1304 height: layout_state.size().height,
1305 };
1306 if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
1307 eprintln!(
1308 "[scene-update-diag] build layer node={node_id:?} size=({:.2},{:.2}) pos=({:.2},{:.2})",
1309 layout_state.size().width,
1310 layout_state.size().height,
1311 layout_state.position().x,
1312 layout_state.position().y,
1313 );
1314 }
1315 let clip_to_bounds = modifier_slices.clip_to_bounds();
1316 let graphics_layer = graphics_layer_with_shaped_clip(
1317 modifier_slices.graphics_layer().unwrap_or_default(),
1318 clip_to_bounds,
1319 modifier_slices.corner_shape(),
1320 local_bounds,
1321 );
1322 let transform_to_parent =
1323 layer_transform_to_parent(local_bounds, layout_state.position(), &graphics_layer);
1324 let isolation = isolation_reasons(&graphics_layer);
1325 let cache_policy = layer_cache_policy(&graphics_layer, isolation);
1326 let shadow_clip = clip_to_bounds.then_some(local_bounds);
1327 let hit_test = hit_test_from_slices(
1328 &modifier_slices,
1329 local_bounds,
1330 clip_to_bounds || graphics_layer.clip,
1331 );
1332
1333 modifier_slices.publish_pointer_input_size(layout_state.size());
1334
1335 let node_motion_context_animated =
1336 inherited_motion_context_animated || modifier_slices.motion_context_animated();
1337 let local_translated_content_context = modifier_slices.translated_content_context();
1338 let local_translated_content_offset = modifier_slices
1339 .translated_content_offset()
1340 .unwrap_or(layout_state.content_offset);
1341 let child_translated_content_context =
1342 inherited_translated_content_context || local_translated_content_context;
1343
1344 let this_abs = parent_abs.map(|parent| {
1345 let top_left = Point {
1346 x: parent.content_origin.x + layout_state.position().x,
1347 y: parent.content_origin.y + layout_state.position().y,
1348 };
1349 let layer_translation = Point {
1350 x: parent.layer_translation.x + graphics_layer.translation_x,
1351 y: parent.layer_translation.y + graphics_layer.translation_y,
1352 };
1353 (top_left, layer_translation)
1354 });
1355 if let Some((top_left, layer_translation)) = this_abs {
1356 let window_origin = Point {
1357 x: top_left.x + layer_translation.x,
1358 y: top_left.y + layer_translation.y,
1359 };
1360 if let Some(sink) = modifier_slices.text_window_origin() {
1361 sink.set(window_origin);
1362 }
1363 if let Some(sink) = modifier_slices.viewport_window_rect() {
1364 sink.set(Rect {
1365 x: window_origin.x,
1366 y: window_origin.y,
1367 width: layout_state.size().width,
1368 height: layout_state.size().height,
1369 });
1370 }
1371 }
1372 let child_abs = this_abs.map(|(top_left, layer_translation)| AbsOrigin {
1373 content_origin: Point {
1374 x: top_left.x + layout_state.content_offset.x,
1375 y: top_left.y + layout_state.content_offset.y,
1376 },
1377 layer_translation,
1378 });
1379
1380 let outer_draw_command_count = modifier_slices.outer_draw_command_count();
1381 let outer = outer_draws(
1382 node_id,
1383 modifier_slices.draw_commands(),
1384 outer_draw_command_count,
1385 layout_state.size(),
1386 );
1387 let layer_draw_commands = &modifier_slices.draw_commands()[outer_draw_command_count..];
1388 let mut render_children = Vec::with_capacity(layer_node_capacity(
1389 layer_draw_commands,
1390 children.len(),
1391 modifier_slices.annotated_text().is_some(),
1392 ));
1393 append_draw_nodes(
1394 &mut render_children,
1395 node_id,
1396 layer_draw_commands,
1397 outer_draw_command_count,
1398 DrawPlacement::Behind,
1399 layout_state.size(),
1400 PrimitivePhase::BeforeChildren,
1401 );
1402 if let Some(text) = text_node_from_parts(TextNodeParts {
1403 node_id,
1404 text_rect: modifier_slices.text_content_rect(layout_state.size()),
1405 text_style: modifier_slices.text_style(),
1406 text_layout_options: modifier_slices.text_layout_options(),
1407 text_pan: modifier_slices.text_pan_resolver(),
1408 measured_layout: modifier_slices.measured_text_layout(),
1409 }) {
1410 render_children.push(RenderNode::Primitive(PrimitiveEntry {
1411 phase: PrimitivePhase::BeforeChildren,
1412 node: PrimitiveNode::Text(Box::new(text)),
1413 }));
1414 }
1415 let child_motion_context_animated = node_motion_context_animated;
1416 for child_id in children {
1417 let Some(mut child_layer) = build_layer_node_from_applier_internal(
1418 applier,
1419 child_id,
1420 child_motion_context_animated,
1421 child_translated_content_context,
1422 child_abs,
1423 ) else {
1424 continue;
1425 };
1426 if layout_state.content_offset != Point::default() {
1427 child_layer.transform_to_parent =
1428 child_layer
1429 .transform_to_parent
1430 .then(ProjectiveTransform::translation(
1431 layout_state.content_offset.x,
1432 layout_state.content_offset.y,
1433 ));
1434 }
1435 render_children.push(RenderNode::Layer(Box::new(child_layer)));
1436 }
1437 append_draw_nodes(
1438 &mut render_children,
1439 node_id,
1440 layer_draw_commands,
1441 outer_draw_command_count,
1442 DrawPlacement::Overlay,
1443 layout_state.size(),
1444 PrimitivePhase::AfterChildren,
1445 );
1446 let has_hit_targets = hit_test.is_some()
1447 || render_children.iter().any(|child| match child {
1448 RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
1449 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1450 });
1451 let has_origin_sinks = modifier_slices_have_origin_sinks(&modifier_slices)
1452 || render_children.iter().any(|child| match child {
1453 RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
1454 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1455 });
1456
1457 let layer = LayerNode {
1458 node_id: Some(node_id),
1459 wraps: None,
1460 local_bounds,
1461 transform_to_parent,
1462 content_offset: layout_state.content_offset,
1463 motion_context_animated: node_motion_context_animated,
1464 translated_content_context: local_translated_content_context,
1465 translated_content_offset: if local_translated_content_context {
1466 local_translated_content_offset
1467 } else {
1468 Point::default()
1469 },
1470 origin_in_parent: layout_state.position(),
1471 graphics_layer,
1472 clip_to_bounds,
1473 shadow_clip,
1474 hit_test,
1475 has_hit_targets,
1476 has_origin_sinks,
1477 draws_within_bounds: false,
1478 isolation,
1479 cache_policy,
1480 cache_hashes: LayerRasterCacheHashes::default(),
1481 cache_hashes_valid: false,
1482 children: render_children,
1483 };
1484 Some(finish_layer(layer, layout_state.position(), outer))
1485}
1486
1487struct RecorderSlot {
1488 generation: u64,
1489 handles: [Option<Rc<CommandRecording>>; 2],
1490}
1491
1492thread_local! {
1493 static COMMAND_RECORDINGS: std::cell::RefCell<
1494 std::collections::HashMap<DrawCommandId, RecorderSlot, cranpose_ui_graphics::FxBuildHasher>,
1495 > = std::cell::RefCell::new(std::collections::HashMap::default());
1496 static RECORDING_GENERATION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
1497}
1498
1499#[doc(hidden)]
1500pub fn clear_command_recordings_for_tests() {
1501 COMMAND_RECORDINGS.with(|map| map.borrow_mut().clear());
1502}
1503
1504fn bump_recording_generation() {
1505 let generation = RECORDING_GENERATION.with(|cell| {
1506 let next = cell.get().wrapping_add(1);
1507 cell.set(next);
1508 next
1509 });
1510 if generation.is_multiple_of(512) {
1511 COMMAND_RECORDINGS.with(|map| {
1512 map.borrow_mut()
1513 .retain(|_, slot| generation.wrapping_sub(slot.generation) <= 64);
1514 });
1515 }
1516}
1517
1518fn acquire_storage(id: DrawCommandId) -> CommandRecording {
1519 COMMAND_RECORDINGS.with(|map| {
1520 let mut map = map.borrow_mut();
1521 let Some(slot) = map.get_mut(&id) else {
1522 return CommandRecording::default();
1523 };
1524 for handle in &mut slot.handles {
1525 if handle
1526 .as_ref()
1527 .is_some_and(|shared| Rc::strong_count(shared) == 1)
1528 {
1529 let shared = handle.take().expect("checked some above");
1530 return Rc::try_unwrap(shared).expect("sole owner checked above");
1531 }
1532 }
1533 CommandRecording::default()
1534 })
1535}
1536
1537fn publish_recording(id: DrawCommandId, recording: CommandRecording) -> Rc<CommandRecording> {
1538 let shared = Rc::new(recording);
1539 COMMAND_RECORDINGS.with(|map| {
1540 let mut map = map.borrow_mut();
1541 let generation = RECORDING_GENERATION.with(Cell::get);
1542 let slot = map.entry(id).or_insert_with(|| RecorderSlot {
1543 generation,
1544 handles: [None, None],
1545 });
1546 slot.generation = generation;
1547 slot.handles[1] = slot.handles[0].take();
1548 slot.handles[0] = Some(shared.clone());
1549 });
1550 shared
1551}
1552
1553fn layer_node_capacity(commands: &[DrawCommand], children: usize, has_text: bool) -> usize {
1554 children
1555 + usize::from(has_text)
1556 + commands.len()
1557 + commands
1558 .iter()
1559 .filter(|command| matches!(command, DrawCommand::WithContent(_)))
1560 .count()
1561}
1562
1563fn draw_nodes(
1564 node_id: NodeId,
1565 commands: &[DrawCommand],
1566 first_command_index: usize,
1567 placement: DrawPlacement,
1568 size: Size,
1569 phase: PrimitivePhase,
1570) -> Vec<RenderNode> {
1571 let mut nodes = Vec::new();
1572 append_draw_nodes(
1573 &mut nodes,
1574 node_id,
1575 commands,
1576 first_command_index,
1577 placement,
1578 size,
1579 phase,
1580 );
1581 nodes
1582}
1583
1584fn append_draw_nodes(
1585 nodes: &mut Vec<RenderNode>,
1586 node_id: NodeId,
1587 commands: &[DrawCommand],
1588 first_command_index: usize,
1589 placement: DrawPlacement,
1590 size: Size,
1591 phase: PrimitivePhase,
1592) {
1593 for (command_index, command) in commands.iter().enumerate() {
1594 let id = DrawCommandId {
1595 node_id,
1596 command_index: (first_command_index + command_index) as u32,
1597 placement,
1598 };
1599 let Some((recording, segments)) =
1600 recording_for_placement_reusing(command, placement, size, || acquire_storage(id))
1601 else {
1602 retain_empty_draw_command(nodes, phase, id, placement, command);
1603 continue;
1604 };
1605 let shared = publish_recording(id, recording);
1606 if shared.is_empty_in(&segments) {
1607 retain_empty_draw_command(nodes, phase, id, placement, command);
1608 continue;
1609 }
1610 nodes.push(RenderNode::DrawRun(DrawRunNode::for_command_shared(
1611 phase,
1612 Some(id),
1613 shared,
1614 segments,
1615 )));
1616 }
1617}
1618
1619fn retain_empty_draw_command(
1620 nodes: &mut Vec<RenderNode>,
1621 phase: PrimitivePhase,
1622 id: DrawCommandId,
1623 placement: DrawPlacement,
1624 command: &DrawCommand,
1625) {
1626 if matches!(
1627 (placement, command),
1628 (DrawPlacement::Behind, DrawCommand::Behind(_))
1629 | (DrawPlacement::Overlay, DrawCommand::Overlay(_))
1630 | (_, DrawCommand::WithContent(_))
1631 ) {
1632 nodes.push(RenderNode::DrawRun(DrawRunNode::for_command(
1633 phase,
1634 Some(id),
1635 Vec::new(),
1636 )));
1637 }
1638}
1639
1640#[doc(hidden)]
1641pub fn draw_command_nodes_for_tests(
1642 node_id: NodeId,
1643 commands: &[DrawCommand],
1644 placement: DrawPlacement,
1645 size: Size,
1646 phase: PrimitivePhase,
1647) -> Vec<RenderNode> {
1648 bump_recording_generation();
1649 draw_nodes(node_id, commands, 0, placement, size, phase)
1650}
1651
1652struct OuterDraws {
1653 behind: Vec<RenderNode>,
1654 overlay: Vec<RenderNode>,
1655}
1656
1657fn outer_draws(
1658 node_id: NodeId,
1659 draw_commands: &[DrawCommand],
1660 outer_draw_command_count: usize,
1661 size: Size,
1662) -> Option<OuterDraws> {
1663 (outer_draw_command_count > 0).then(|| {
1664 let commands = &draw_commands[..outer_draw_command_count];
1665 OuterDraws {
1666 behind: draw_nodes(
1667 node_id,
1668 commands,
1669 0,
1670 DrawPlacement::Behind,
1671 size,
1672 PrimitivePhase::BeforeChildren,
1673 ),
1674 overlay: draw_nodes(
1675 node_id,
1676 commands,
1677 0,
1678 DrawPlacement::Overlay,
1679 size,
1680 PrimitivePhase::AfterChildren,
1681 ),
1682 }
1683 })
1684}
1685
1686fn finish_layer(mut layer: LayerNode, placement: Point, outer: Option<OuterDraws>) -> LayerNode {
1687 layer.draws_within_bounds = layer.content_draws_within_bounds();
1688 match outer {
1689 Some(outer) => wrap_layer_with_outer_draws(layer, placement, outer),
1690 None => layer,
1691 }
1692}
1693
1694fn wrap_layer_with_outer_draws(
1695 mut layer: LayerNode,
1696 placement: Point,
1697 outer: OuterDraws,
1698) -> LayerNode {
1699 let local_bounds = layer.local_bounds;
1700 layer.transform_to_parent =
1701 layer_transform_to_parent(local_bounds, Point::default(), &layer.graphics_layer);
1702 layer.origin_in_parent = Point::default();
1703 let wrapper = LayerNode {
1704 wraps: layer.node_id,
1705 local_bounds,
1706 transform_to_parent: layer_transform_to_parent(
1707 local_bounds,
1708 placement,
1709 &GraphicsLayer::default(),
1710 ),
1711 origin_in_parent: placement,
1712 motion_context_animated: layer.motion_context_animated,
1713 has_hit_targets: layer.has_hit_targets,
1714 has_origin_sinks: layer.has_origin_sinks,
1715 ..Default::default()
1716 };
1717 let mut children = outer.behind;
1718 children.push(RenderNode::Layer(Box::new(layer)));
1719 children.extend(outer.overlay);
1720 let mut wrapper = LayerNode {
1721 children,
1722 ..wrapper
1723 };
1724 wrapper.draws_within_bounds = wrapper.content_draws_within_bounds();
1725 wrapper
1726}
1727
1728fn layer_identity(layer: &LayerNode) -> Option<NodeId> {
1729 layer.node_id.or(layer.wraps)
1730}
1731
1732struct TextNodeParts<'a> {
1733 node_id: NodeId,
1734 text_rect: Rect,
1736 text_style: Option<&'a TextStyle>,
1737 text_layout_options: Option<TextLayoutOptions>,
1738 text_pan: Option<TextPanResolver>,
1739 measured_layout: Option<Rc<PreparedTextLayout>>,
1740}
1741
1742fn text_node_from_parts(parts: TextNodeParts<'_>) -> Option<TextPrimitiveNode> {
1743 let TextNodeParts {
1744 node_id,
1745 text_rect,
1746 text_style,
1747 text_layout_options,
1748 text_pan,
1749 measured_layout,
1750 } = parts;
1751 let prepared = measured_layout?;
1752 let default_text_style = TextStyle::default();
1753 let text_style = text_style.cloned().unwrap_or(default_text_style);
1754 let options = text_layout_options.unwrap_or_default().normalized();
1755 let content_width = text_rect.width.max(0.0);
1756 if content_width <= 0.0 {
1757 return None;
1758 }
1759
1760 let pan_offset = text_pan
1761 .as_ref()
1762 .map_or(0.0, |resolve| resolve(content_width));
1763 let pans_horizontally = text_pan.is_some();
1764
1765 let visual_style = prepared.visual_style.clone();
1766 let measured_draw_width = prepared.metrics.width.max(0.0);
1767 let draw_width = if options.overflow == TextOverflow::Visible || pans_horizontally {
1768 measured_draw_width
1769 } else {
1770 measured_draw_width.min(content_width)
1771 };
1772 let alignment_offset = resolve_text_horizontal_offset(
1773 &text_style,
1774 prepared.text.text.as_str(),
1775 content_width,
1776 prepared.metrics.width,
1777 );
1778 let rect = Rect {
1779 x: text_rect.x + alignment_offset - pan_offset,
1780 y: text_rect.y,
1781 width: draw_width,
1782 height: prepared.metrics.height,
1783 };
1784 let text_bounds = Rect {
1785 width: content_width,
1786 height: text_rect.height.max(0.0),
1787 ..text_rect
1788 };
1789 let font_size = visual_style.resolve_font_size(14.0);
1790 let expanded_bounds =
1791 expand_text_bounds_for_baseline_shift(text_bounds, &visual_style, font_size);
1792 let clip = if options.overflow == TextOverflow::Visible && !pans_horizontally {
1793 None
1794 } else {
1795 Some(pad_clip_rect(expanded_bounds))
1796 };
1797
1798 Some(TextPrimitiveNode {
1799 node_id,
1800 rect,
1801 text: Rc::clone(&prepared.text),
1802 render_text: prepared.render_text(),
1803 text_style: std::sync::Arc::new(visual_style),
1804 font_size,
1805 layout_options: options,
1806 clip,
1807 })
1808}
1809
1810fn layout_box_to_snapshot(node: &LayoutBox, parent: Option<&LayoutBox>) -> BuildNodeSnapshot {
1811 let placement = parent
1812 .map(|parent_box| Point {
1813 x: node.rect.x - parent_box.rect.x - parent_box.content_offset.x,
1814 y: node.rect.y - parent_box.rect.y - parent_box.content_offset.y,
1815 })
1816 .unwrap_or_default();
1817 let mut children = Vec::with_capacity(node.children.len());
1818 for child in &node.children {
1819 children.push(layout_box_to_snapshot(child, Some(node)));
1820 }
1821 let base_graphics_layer = node.node_data.modifier_slices.graphics_layer();
1822 let graphics_layer = graphics_layer_with_shaped_clip(
1823 base_graphics_layer.clone().unwrap_or_default(),
1824 node.node_data.modifier_slices.clip_to_bounds(),
1825 node.node_data.modifier_slices.corner_shape(),
1826 Rect {
1827 x: 0.0,
1828 y: 0.0,
1829 width: node.rect.width,
1830 height: node.rect.height,
1831 },
1832 );
1833 let has_graphics_layer =
1834 base_graphics_layer.is_some() || graphics_layer.render_effect.is_some();
1835
1836 BuildNodeSnapshot {
1837 node_id: node.node_id,
1838 placement,
1839 size: Size {
1840 width: node.rect.width,
1841 height: node.rect.height,
1842 },
1843 content_offset: node.content_offset,
1844 slices: Rc::clone(&node.node_data.modifier_slices),
1845 graphics_layer: has_graphics_layer.then_some(graphics_layer),
1846 children,
1847 }
1848}
1849
1850fn modifier_slices_have_origin_sinks(slices: &ModifierNodeSlices) -> bool {
1851 slices.text_window_origin().is_some() || slices.viewport_window_rect().is_some()
1852}
1853
1854fn graphics_layer_with_shaped_clip(
1855 mut graphics_layer: GraphicsLayer,
1856 clip_to_bounds: bool,
1857 corner_shape: Option<RoundedCornerShape>,
1858 local_bounds: Rect,
1859) -> GraphicsLayer {
1860 if !clip_to_bounds {
1861 return graphics_layer;
1862 }
1863
1864 let Some(corner_shape) = corner_shape else {
1865 return graphics_layer;
1866 };
1867 let radii = corner_shape.resolve(local_bounds.width, local_bounds.height);
1868 if radii.top_left <= f32::EPSILON
1869 && radii.top_right <= f32::EPSILON
1870 && radii.bottom_right <= f32::EPSILON
1871 && radii.bottom_left <= f32::EPSILON
1872 {
1873 return graphics_layer;
1874 }
1875
1876 if let Some(existing) = graphics_layer.render_effect.take() {
1877 let rounded_clip = rounded_corner_alpha_mask_effect(
1878 local_bounds.width,
1879 local_bounds.height,
1880 radii,
1881 ROUNDED_CLIP_EDGE_FEATHER,
1882 );
1883 graphics_layer.render_effect = Some(existing.then(rounded_clip));
1884 } else {
1885 graphics_layer.shape = LayerShape::Rounded(corner_shape);
1886 graphics_layer.clip = true;
1887 }
1888 graphics_layer
1889}
1890
1891fn layer_cache_policy(layer: &GraphicsLayer, isolation: IsolationReasons) -> CachePolicy {
1892 if isolation.has_any() || layer_scales_or_rotates(layer) {
1893 CachePolicy::Auto
1894 } else {
1895 CachePolicy::None
1896 }
1897}
1898
1899fn isolation_reasons(layer: &GraphicsLayer) -> IsolationReasons {
1900 IsolationReasons {
1901 explicit_offscreen: layer.compositing_strategy == CompositingStrategy::Offscreen,
1902 shape_clip: layer.clip && !matches!(layer.shape, LayerShape::Rectangle),
1903 effect: layer.render_effect.is_some(),
1904 backdrop: layer.backdrop_effect.is_some(),
1905 group_opacity: layer.compositing_strategy != CompositingStrategy::ModulateAlpha
1906 && layer.alpha < 1.0,
1907 blend_mode: layer.blend_mode != cranpose_ui::BlendMode::SrcOver,
1908 }
1909}
1910
1911fn pad_clip_rect(rect: Rect) -> Rect {
1912 Rect {
1913 x: rect.x - TEXT_CLIP_PAD,
1914 y: rect.y - TEXT_CLIP_PAD,
1915 width: (rect.width + TEXT_CLIP_PAD * 2.0).max(0.0),
1916 height: (rect.height + TEXT_CLIP_PAD * 2.0).max(0.0),
1917 }
1918}
1919
1920pub fn expand_text_bounds_for_baseline_shift(
1921 text_bounds: Rect,
1922 text_style: &TextStyle,
1923 font_size: f32,
1924) -> Rect {
1925 let baseline_shift_px = text_style
1926 .span_style
1927 .baseline_shift
1928 .filter(|shift| shift.is_specified())
1929 .map_or(0.0, |shift| -(shift.0 * font_size));
1930 if baseline_shift_px == 0.0 {
1931 return text_bounds;
1932 }
1933
1934 if baseline_shift_px < 0.0 {
1935 Rect {
1936 x: text_bounds.x,
1937 y: text_bounds.y + baseline_shift_px,
1938 width: text_bounds.width,
1939 height: (text_bounds.height - baseline_shift_px).max(0.0),
1940 }
1941 } else {
1942 Rect {
1943 x: text_bounds.x,
1944 y: text_bounds.y,
1945 width: text_bounds.width,
1946 height: (text_bounds.height + baseline_shift_px).max(0.0),
1947 }
1948 }
1949}
1950
1951fn resolve_text_horizontal_offset(
1952 text_style: &TextStyle,
1953 text: &str,
1954 content_width: f32,
1955 measured_width: f32,
1956) -> f32 {
1957 let remaining = (content_width - measured_width).max(0.0);
1958 remaining * cranpose_ui::text::text_align_fraction(text_style, text)
1959}
1960
1961#[cfg(test)]
1962#[path = "tests/scene_builder_tests.rs"]
1963mod tests;