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