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