Skip to main content

cranpose_render_common/
scene_builder.rs

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