Skip to main content

cranpose_render_common/
scene_builder.rs

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    /// The node's modifier slices, shared: its draw commands, handlers and
38    /// text are read from them rather than copied out.
39    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/// Why a scoped scene update could not be applied, forcing the caller to throw
52/// the render graph away and build it again from the applier.
53///
54/// The reason has to travel with the outcome because the shell picks the
55/// scoped path from the shape of the dirty set, before this code runs, and
56/// logs that choice. A frame that chose the scoped path and then rebuilt the
57/// whole scene is the expensive case, and in the log it reads exactly like a
58/// cheap patch -- so the fallback says so itself.
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub enum GraphRebuildReason {
61    /// The root was dirty and its replacement layer could not be built.
62    RootLayerUnavailable,
63    /// A dirty subtree's replacement layer could not be built.
64    DirtyLayerUnavailable,
65    /// This many dirty nodes own no layer in the render graph, so the scoped
66    /// walk never reached them. A node enters the graph only as a `LayerNode`;
67    /// a dirty node that never produced one -- or whose layer left the graph
68    /// this frame -- cannot be patched in place.
69    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
136/// Builds `root`'s graph again, its layers taking the allocations of
137/// `previous`, the graph it replaces.
138pub 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/// What a scroll step reconciles a container's children in, kept between
592/// steps so a step allocates nothing of its own. Every list is empty
593/// between steps, so it holds no layer.
594#[derive(Default)]
595struct TranslateScratch {
596    /// The placed children, in their new order.
597    placed_fresh: Vec<(NodeId, cranpose_ui::widgets::LayoutState)>,
598    old_index_by_id: HashMap<NodeId, usize>,
599    /// The container's previous children by their index, until each is
600    /// kept or recycled.
601    retained: Vec<Option<Box<LayerNode>>>,
602    /// The previous children that stay, in their new order; `None` where a
603    /// child enters.
604    kept: Vec<Option<Box<LayerNode>>>,
605    /// The children built for the step, in their new order.
606    entering: Vec<(NodeId, Box<LayerNode>)>,
607}
608
609thread_local! {
610    static TRANSLATE_SCRATCH: Cell<TranslateScratch> = Cell::new(TranslateScratch::default());
611}
612
613/// Fills `scratch` with `container`'s placed children and the index of its
614/// previous ones, and returns whether they are the same children in the
615/// same order.
616fn 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
739/// Moves `container`'s previous children that stay into `scratch.kept`, in
740/// their new order, and the subtrees of those that leave into the layer
741/// pool, so the entering children are built in their allocations.
742fn 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
1309/// A node's layer bounds and, when they differ from it, its own rect (see
1310/// [`LayerNode::node_bounds`]).
1311fn 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
1320/// Whether a node's slices make it a hit target: a pointer input or a
1321/// pointer icon.
1322fn 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    /// The allocation of a handle whose recording `acquire_storage` took,
1530    /// which the next recording the slot publishes moves into.
1531    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    // The outer draws sit on the node's rect, outside its layer.
1754    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    /// Where the text node was placed in its layout node.
1792    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;