Skip to main content

cranpose_render_common/
scene_builder.rs

1use std::{any::Any, rc::Rc};
2
3use cranpose_core::{MemoryApplier, NodeId, NodeMarks, collections::map::HashMap};
4use cranpose_ui::{
5    DrawCommand, LayoutBox, LayoutNode, ModifierNodeSlices, Point, PreparedTextLayout, Rect, Size,
6    SubcomposeLayoutNode, TextLayoutOptions, TextOverflow, TextPanResolver, text::TextStyle,
7};
8use cranpose_ui_graphics::{
9    CommandRecording, CompositingStrategy, GraphicsLayer, LayerShape, RoundedCornerShape,
10    rounded_corner_alpha_mask_effect,
11};
12
13use crate::{
14    SceneUpdates,
15    graph::{
16        CachePolicy, ContentChanges, DrawCommandId, DrawRunNode, HitTestNode, IsolationReasons,
17        LayerNode, PrimitiveEntry, PrimitiveNode, PrimitivePhase, ProjectiveTransform, RenderGraph,
18        RenderNode, TextPrimitiveNode,
19    },
20    layer_transform::{
21        layer_scales_or_rotates, layer_transform_to_parent, layer_transform_to_window,
22    },
23    style_shared::{DrawPlacement, recording_for_placement_reusing},
24    text_paint::{TextPaint, TextPaintCache},
25};
26
27const TEXT_CLIP_PAD: f32 = 1.0;
28const ROUNDED_CLIP_EDGE_FEATHER: f32 = 1.0;
29
30#[derive(Clone, Default)]
31struct BuildNodeSnapshot {
32    node_id: NodeId,
33    placement: Point,
34    size: Size,
35    content_offset: Point,
36    /// The node's modifier slices, shared: its draw commands, handlers and
37    /// text are read from them rather than copied out.
38    slices: Rc<ModifierNodeSlices>,
39    graphics_layer: Option<GraphicsLayer>,
40    children: Vec<Self>,
41}
42
43struct SnapshotNodeData<'a> {
44    layout_state: cranpose_ui::widgets::LayoutState,
45    modifier_slices: Rc<ModifierNodeSlices>,
46    children: &'a [NodeId],
47    window_root: bool,
48}
49
50/// Why a scoped scene update could not be applied, forcing the caller to throw
51/// the render graph away and build it again from the applier.
52///
53/// The reason has to travel with the outcome because the shell picks the
54/// scoped path from the shape of the dirty set, before this code runs, and
55/// logs that choice. A frame that chose the scoped path and then rebuilt the
56/// whole scene is the expensive case, and in the log it reads exactly like a
57/// cheap patch -- so the fallback says so itself.
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum GraphRebuildReason {
60    /// The root was dirty and its replacement layer could not be built.
61    RootLayerUnavailable,
62    /// A dirty subtree's replacement layer could not be built.
63    DirtyLayerUnavailable,
64    /// This many dirty nodes own no layer in the render graph, so the scoped
65    /// walk never reached them. A node enters the graph only as a `LayerNode`;
66    /// a dirty node that never produced one -- or whose layer left the graph
67    /// this frame -- cannot be patched in place.
68    UnmatchedDirtyNodes(usize),
69}
70
71/// The outcome of a scoped scene update.
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum GraphUpdate {
74    /// The dirty nodes were updated in place.
75    Patched,
76    /// The update could not be applied and the graph must be built again, for the given reason.
77    NeedsRebuild(GraphRebuildReason),
78}
79
80/// The outcome of a scoped update together with its effect on hit testing.
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82pub struct GraphUpdateReport {
83    /// Whether the graph was patched or must be rebuilt.
84    pub update: GraphUpdate,
85    /// Whether the update changed hit geometry or handlers, so the hit targets must be collected
86    /// again.
87    pub hit_graph_dirty: bool,
88}
89
90impl GraphUpdateReport {
91    /// Whether the graph was patched in place.
92    pub fn applied(self) -> bool {
93        matches!(self.update, GraphUpdate::Patched)
94    }
95
96    /// Why the graph must be rebuilt, or `None` when it was patched.
97    pub fn rebuild_reason(self) -> Option<GraphRebuildReason> {
98        match self.update {
99            GraphUpdate::Patched => None,
100            GraphUpdate::NeedsRebuild(reason) => Some(reason),
101        }
102    }
103}
104
105fn note_layer_lowered() {
106    #[cfg(test)]
107    crate::thread_state::with(|state| {
108        let count = &state.lowered_layer_count;
109        count.set(count.get() + 1);
110    });
111}
112
113#[cfg(test)]
114fn reset_lowered_layer_count() {
115    crate::thread_state::with(|state| state.lowered_layer_count.set(0));
116}
117
118#[cfg(test)]
119fn lowered_layer_count() -> usize {
120    crate::thread_state::with(|state| state.lowered_layer_count.get())
121}
122
123/// Builds the render graph of the layout box tree rooted at `root`.
124pub fn build_graph_from_layout_tree(root: &LayoutBox, _scale: f32) -> RenderGraph {
125    bump_recording_generation();
126    let root_snapshot = layout_box_to_snapshot(root, None);
127    let mut graph = RenderGraph::new(LayerNode::default());
128    write_snapshot_layer(root_snapshot, LowerContext::ROOT, &mut graph.root);
129    graph
130}
131
132/// Builds the render graph of the layout tree below `root` by reading nodes from `applier`.
133/// Returns `None` when `root` cannot be lowered to a layer.
134pub fn build_graph_from_applier(
135    applier: &MemoryApplier,
136    root: NodeId,
137    _scale: f32,
138) -> Option<RenderGraph> {
139    bump_recording_generation();
140    let mut graph = RenderGraph::new(LayerNode::default());
141    lower_root_into(applier, root, &mut graph.root).then_some(graph)
142}
143
144/// Builds `root`'s graph again, its layers taking the allocations of
145/// `previous`, the graph it replaces.
146pub fn rebuild_graph_from_applier(
147    applier: &MemoryApplier,
148    root: NodeId,
149    scale: f32,
150    previous: Option<RenderGraph>,
151) -> Option<RenderGraph> {
152    let graph = match previous {
153        Some(mut graph) => {
154            release_for_rebuild(&mut graph.root);
155            bump_recording_generation();
156            graph.update += 1;
157            graph.root.note_content_change(graph.update);
158            lower_root_into(applier, root, &mut graph.root).then_some(graph)
159        }
160        None => build_graph_from_applier(applier, root, scale),
161    };
162    crate::layer_recycling::release();
163    graph
164}
165
166/// Applies `updates` to `graph` in place. Returns `false` when the update could not be applied
167/// and the graph must be built again.
168pub fn update_graph_from_applier(
169    applier: &MemoryApplier,
170    graph: &mut RenderGraph,
171    updates: SceneUpdates<'_>,
172    scale: f32,
173) -> bool {
174    update_graph_from_applier_report(applier, graph, updates, scale).applied()
175}
176
177/// Like `update_graph_from_applier`, and also reports whether the hit targets must be collected
178/// again or why the graph must be rebuilt.
179pub fn update_graph_from_applier_report(
180    applier: &MemoryApplier,
181    graph: &mut RenderGraph,
182    updates: SceneUpdates<'_>,
183    _scale: f32,
184) -> GraphUpdateReport {
185    let report = update_graph_from_applier_report_inner(applier, graph, updates);
186    crate::layer_recycling::release();
187    if let GraphUpdate::NeedsRebuild(reason) = report.update
188        && cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG")
189    {
190        eprintln!(
191            "[scene-update-diag] scoped update abandoned, whole scene rebuilt: {reason:?} dirty={}",
192            updates.content.len() + updates.layers.len() + updates.moved.len()
193        );
194    }
195    report
196}
197
198#[derive(Clone, Copy)]
199enum NodeUpdate {
200    Content,
201    Layer,
202    /// The node moved in its parent and kept its size: its layer moves and
203    /// keeps what it drew.
204    Moved,
205    /// The node moved and its layer properties changed: its layer moves,
206    /// takes the new properties and keeps what it drew.
207    MovedLayer,
208}
209
210/// Each dirty node with the update it needs: content wins over the others,
211/// and a moved node whose layer properties changed too moves with them.
212fn dirty_node_updates(updates: SceneUpdates<'_>) -> HashMap<NodeId, NodeUpdate> {
213    let mut dirty = HashMap::with_capacity_and_hasher(
214        updates.moved.len() + updates.layers.len() + updates.content.len(),
215        Default::default(),
216    );
217    dirty.extend(updates.moved.iter().map(|&id| (id, NodeUpdate::Moved)));
218    for &id in updates.layers {
219        let kind = match dirty.get(&id) {
220            Some(NodeUpdate::Moved) => NodeUpdate::MovedLayer,
221            _ => NodeUpdate::Layer,
222        };
223        dirty.insert(id, kind);
224    }
225    dirty.extend(updates.content.iter().map(|&id| (id, NodeUpdate::Content)));
226    dirty
227}
228
229fn update_graph_from_applier_report_inner(
230    applier: &MemoryApplier,
231    graph: &mut RenderGraph,
232    updates: SceneUpdates<'_>,
233) -> GraphUpdateReport {
234    if updates.is_empty() {
235        return GraphUpdateReport {
236            update: GraphUpdate::Patched,
237            hit_graph_dirty: false,
238        };
239    }
240    bump_recording_generation();
241    graph.update += 1;
242    let update = graph.update;
243
244    if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
245        eprintln!("[scene-update-diag] dirty={updates:?}");
246    }
247
248    let mut remaining_dirty_nodes = dirty_node_updates(updates);
249    if let Some(root_id) = layer_identity(&graph.root)
250        && let Some(kind) = remaining_dirty_nodes.remove(&root_id)
251    {
252        if let Some(hit_graph_dirty) = try_update_retained_layer(
253            applier,
254            &mut graph.root,
255            &mut remaining_dirty_nodes,
256            kind,
257            true,
258            TranslateAncestorContext {
259                inherited_motion_context_animated: false,
260                update,
261                inherited_translated_content_context: false,
262                parent_content_offset: Point::default(),
263                parent_abs: AbsOrigin::ROOT,
264            },
265        ) {
266            if remaining_dirty_nodes.is_empty() {
267                return GraphUpdateReport {
268                    update: GraphUpdate::Patched,
269                    hit_graph_dirty,
270                };
271            }
272            let walked =
273                walk_dirty_layers(applier, &mut graph.root, &mut remaining_dirty_nodes, update);
274            return GraphUpdateReport {
275                update: classify_walk(applier, walked.is_some(), &mut remaining_dirty_nodes),
276                hit_graph_dirty: hit_graph_dirty || walked.is_none_or(|r| r.hit_graph_dirty),
277            };
278        }
279
280        let Some(hit_graph_dirty) = rebuild_tracking_hits(&mut graph.root, |root| {
281            release_for_rebuild(root);
282            lower_root_into(applier, root_id, root)
283        }) else {
284            return GraphUpdateReport {
285                update: GraphUpdate::NeedsRebuild(GraphRebuildReason::RootLayerUnavailable),
286                hit_graph_dirty: true,
287            };
288        };
289        graph.root.note_content_change(update);
290
291        return GraphUpdateReport {
292            update: GraphUpdate::Patched,
293            hit_graph_dirty,
294        };
295    }
296
297    let Some(report) =
298        walk_dirty_layers(applier, &mut graph.root, &mut remaining_dirty_nodes, update)
299    else {
300        return GraphUpdateReport {
301            update: GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable),
302            hit_graph_dirty: true,
303        };
304    };
305
306    match classify_walk(applier, true, &mut remaining_dirty_nodes) {
307        GraphUpdate::Patched => GraphUpdateReport {
308            update: GraphUpdate::Patched,
309            hit_graph_dirty: report.hit_graph_dirty,
310        },
311        update => GraphUpdateReport {
312            update,
313            hit_graph_dirty: true,
314        },
315    }
316}
317
318/// The update a walk made. A dirty node the walk did not find needs the whole
319/// scene built again, unless no layer draws it.
320fn classify_walk(
321    applier: &MemoryApplier,
322    walked: bool,
323    remaining_dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
324) -> GraphUpdate {
325    if !walked {
326        return GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable);
327    }
328    remaining_dirty_nodes.retain(|&node, _| is_drawn(applier, node));
329    if !remaining_dirty_nodes.is_empty() {
330        GraphUpdate::NeedsRebuild(GraphRebuildReason::UnmatchedDirtyNodes(
331            remaining_dirty_nodes.len(),
332        ))
333    } else {
334        GraphUpdate::Patched
335    }
336}
337
338/// Whether the scene draws `node`: each subcompose node above it draws the
339/// slot `node` is in. A slot a lazy list keeps for reuse, or composes before
340/// it shows it, stays in the applier's tree without being drawn, and its
341/// nodes still change.
342fn is_drawn(applier: &MemoryApplier, node: NodeId) -> bool {
343    let mut child = node;
344    let mut current = node;
345    while let Some(parent) = applier
346        .get_ref(current)
347        .ok()
348        .and_then(cranpose_core::Node::parent)
349    {
350        let Ok(parent_node) = applier.get_ref(parent) else {
351            return true;
352        };
353        current = parent;
354        if parent_node.is_virtual() {
355            continue;
356        }
357        let parent_node: &dyn Any = parent_node;
358        if let Some(subcompose) = parent_node.downcast_ref::<SubcomposeLayoutNode>()
359            && !subcompose.with_active_children(|children| children.contains(&child))
360        {
361            return false;
362        }
363        child = parent;
364    }
365    true
366}
367
368#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
369struct ReplaceDirtyLayersReport {
370    updated: bool,
371    hit_graph_dirty: bool,
372}
373
374/// The dirty nodes a scene update has yet to find, and every node above
375/// them in the applier: a layer is lowered from its node's subtree, so a
376/// layer whose node is in neither holds no dirty layer.
377struct DirtyWalk<'a> {
378    nodes: &'a mut HashMap<NodeId, NodeUpdate>,
379    ancestry: &'a NodeMarks,
380    applier: &'a MemoryApplier,
381}
382
383impl DirtyWalk<'_> {
384    fn reborrow(&mut self) -> DirtyWalk<'_> {
385        DirtyWalk {
386            nodes: self.nodes,
387            ancestry: self.ancestry,
388            applier: self.applier,
389        }
390    }
391
392    /// Whether the subtree of a layer with `identity` may hold a dirty
393    /// layer: always for a layer without a node.
394    fn may_hold(&self, identity: Option<NodeId>) -> bool {
395        identity.is_none_or(|id| self.ancestry.get(self.applier, id) == DIRTY_ANCESTOR)
396    }
397}
398
399const DIRTY_ANCESTOR: u8 = 1;
400
401fn walk_dirty_layers(
402    applier: &MemoryApplier,
403    root: &mut LayerNode,
404    dirty: &mut HashMap<NodeId, NodeUpdate>,
405    update: u64,
406) -> Option<ReplaceDirtyLayersReport> {
407    let inherited = root.translated_content_context;
408    let root_children = AbsOrigin::ROOT.children_of(root);
409    let mut ancestry = crate::thread_state::with(|state| state.dirty_ancestry.take());
410    mark_dirty_ancestry(applier, dirty, &mut ancestry);
411    let walked = replace_dirty_layers_from_applier(
412        applier,
413        root,
414        root_children,
415        DirtyWalk {
416            nodes: dirty,
417            ancestry: &ancestry,
418            applier,
419        },
420        inherited,
421        update,
422    );
423    crate::thread_state::with(|state| state.dirty_ancestry.set(ancestry));
424    walked
425}
426
427fn mark_dirty_ancestry(
428    applier: &MemoryApplier,
429    dirty: &HashMap<NodeId, NodeUpdate>,
430    ancestry: &mut NodeMarks,
431) {
432    ancestry.reset(applier);
433    for &node in dirty.keys() {
434        let mut current = node;
435        while let Some(parent) = applier
436            .get_ref(current)
437            .ok()
438            .and_then(cranpose_core::Node::parent)
439        {
440            if ancestry.get(applier, parent) == DIRTY_ANCESTOR {
441                break;
442            }
443            ancestry.set(applier, parent, DIRTY_ANCESTOR);
444            current = parent;
445        }
446    }
447}
448
449fn replace_dirty_layers_from_applier(
450    applier: &MemoryApplier,
451    parent: &mut LayerNode,
452    parent_children: AbsOrigin,
453    mut dirty: DirtyWalk<'_>,
454    inherited_translated_content_context: bool,
455    update: u64,
456) -> Option<ReplaceDirtyLayersReport> {
457    if dirty.nodes.is_empty() {
458        return Some(ReplaceDirtyLayersReport::default());
459    }
460
461    let child_inherited_translated_content_context =
462        inherited_translated_content_context || parent.translated_content_context;
463    let mut report = ReplaceDirtyLayersReport::default();
464
465    for child in &mut parent.children {
466        if dirty.nodes.is_empty() {
467            break;
468        }
469        let RenderNode::Layer(child_layer) = child else {
470            continue;
471        };
472
473        let identity = layer_identity(child_layer);
474        if let Some(kind) = identity.and_then(|id| dirty.nodes.remove(&id)) {
475            if let Some(hit_graph_dirty) = try_update_retained_layer(
476                applier,
477                child_layer,
478                dirty.nodes,
479                kind,
480                false,
481                TranslateAncestorContext {
482                    inherited_motion_context_animated: parent.motion_context_animated,
483                    update,
484                    inherited_translated_content_context:
485                        child_inherited_translated_content_context,
486                    parent_content_offset: parent.content_offset,
487                    parent_abs: parent_children,
488                },
489            ) {
490                report.hit_graph_dirty |= hit_graph_dirty;
491                report.updated = true;
492                let child_children = parent_children.children_of(child_layer);
493                let child_report = replace_dirty_layers_from_applier(
494                    applier,
495                    child_layer,
496                    child_children,
497                    dirty.reborrow(),
498                    child_inherited_translated_content_context,
499                    update,
500                )?;
501                report.hit_graph_dirty |= child_report.hit_graph_dirty;
502                continue;
503            }
504            let node_id = layer_identity(child_layer).expect("dirty layer must have a node id");
505
506            // Nodes that leave this subtree are gone from the scene: nothing
507            // remains to update for them, and a dirty one would otherwise go
508            // unmatched and rebuild the whole scene.
509            remove_dirty_descendants(child_layer, dirty.nodes);
510            let context = LowerContext {
511                inherited_motion_context_animated: parent.motion_context_animated,
512                inherited_translated_content_context: child_inherited_translated_content_context,
513                parent_abs: Some(parent_children),
514                parent_content_offset: parent.content_offset,
515            };
516            report.hit_graph_dirty |= rebuild_tracking_hits(child_layer, |layer| {
517                release_for_rebuild(layer);
518                read_placed_node_data(applier, node_id, false, |data| {
519                    write_node_layer(applier, node_id, data, context, layer);
520                })
521                .is_some()
522            })?;
523            remove_dirty_descendants(child_layer, dirty.nodes);
524
525            child_layer.note_content_change(update);
526            report.updated = true;
527            continue;
528        }
529
530        if !dirty.may_hold(identity) {
531            continue;
532        }
533        let child_children = parent_children.children_of(child_layer);
534        let child_report = replace_dirty_layers_from_applier(
535            applier,
536            child_layer,
537            child_children,
538            dirty.reborrow(),
539            child_inherited_translated_content_context,
540            update,
541        )?;
542        report.updated |= child_report.updated;
543        report.hit_graph_dirty |= child_report.hit_graph_dirty;
544    }
545
546    if report.updated {
547        parent.refresh_child_facts();
548        // A layer's own sinks are not kept apart from its children's, so a
549        // child that stopped publishing leaves the flag set, which only keeps
550        // the scroll fast path off; a child that started must set it.
551        parent.has_origin_sinks |= children_have_origin_sinks(&parent.children);
552        parent.note_content_change(update);
553    }
554
555    Some(report)
556}
557
558fn translate_bail(reason: &str) -> bool {
559    if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
560        eprintln!("[scene-update-diag] translate bail: {reason}");
561    }
562    false
563}
564
565#[derive(Clone, Copy)]
566struct TranslateAncestorContext {
567    inherited_motion_context_animated: bool,
568    /// The scene update in progress.
569    update: u64,
570    inherited_translated_content_context: bool,
571    parent_content_offset: Point,
572    parent_abs: AbsOrigin,
573}
574
575fn try_update_retained_layer(
576    applier: &MemoryApplier,
577    layer: &mut LayerNode,
578    dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
579    kind: NodeUpdate,
580    root: bool,
581    ancestors: TranslateAncestorContext,
582) -> Option<bool> {
583    match kind {
584        NodeUpdate::Moved if !root => {
585            move_retained_layer(applier, layer, ancestors.parent_content_offset).or_else(|| {
586                try_translate_scrolled_layer(applier, layer, dirty_nodes, ancestors).then_some(true)
587            })
588        }
589        NodeUpdate::Content | NodeUpdate::Moved => {
590            try_translate_scrolled_layer(applier, layer, dirty_nodes, ancestors).then_some(true)
591        }
592        NodeUpdate::Layer => update_layer_properties(applier, layer, root, false, ancestors),
593        NodeUpdate::MovedLayer => update_layer_properties(applier, layer, root, true, ancestors),
594    }
595}
596
597/// Moves the layer of a node that only moved in its parent, keeping what it
598/// drew, and returns whether hit geometry changed. `None` when the layer
599/// cannot keep its content: its size changed, or its subtree publishes
600/// window origins.
601fn move_retained_layer(
602    applier: &MemoryApplier,
603    layer: &mut LayerNode,
604    parent_content_offset: Point,
605) -> Option<bool> {
606    if layer.has_origin_sinks {
607        return None;
608    }
609    let state = placed_child_layout_state(applier, layer_identity(layer)?)?;
610    if Rect::from_size(state.size()) != layer.node_rect() {
611        return None;
612    }
613    let previous = HitGraphState::of(layer);
614    translate_retained_child(layer, &state, parent_content_offset);
615    Some(previous.dirty_against(&HitGraphState::of(layer)))
616}
617
618fn content_layer_mut(container: &mut LayerNode) -> Option<&mut LayerNode> {
619    let Some(node_id) = container.wraps else {
620        return Some(container);
621    };
622    container.children.iter_mut().find_map(|child| match child {
623        RenderNode::Layer(layer) if layer.node_id == Some(node_id) => Some(&mut **layer),
624        _ => None,
625    })
626}
627
628/// Whether `layer` still fits its node's layout, so new layer properties
629/// can update it in place: at `placement`, unless the node `moved`.
630fn retained_layer_matches_layout(
631    layer: &LayerNode,
632    data: &SnapshotNodeData<'_>,
633    placement: Point,
634    wrapped: bool,
635    moved: bool,
636) -> bool {
637    let state = &data.layout_state;
638    let slices = &data.modifier_slices;
639    (moved || state.position() == placement)
640        && Rect::from_size(state.size()) == layer.node_rect()
641        && slices.layer_bounds(state.size()) == layer.local_bounds
642        && state.content_offset() == layer.content_offset
643        && (slices.outer_draw_command_count() > 0) == wrapped
644}
645
646fn update_layer_properties(
647    applier: &MemoryApplier,
648    container: &mut LayerNode,
649    root: bool,
650    moved: bool,
651    ancestors: TranslateAncestorContext,
652) -> Option<bool> {
653    let node_id = layer_identity(container)?;
654    let wrapped = container.wraps.is_some();
655    // The wrapper of a node's outer draws holds its placement; a moved
656    // wrapped node is drawn again.
657    if moved && wrapped {
658        return None;
659    }
660    let placement = container.origin_in_parent;
661    let hit_graph_dirty = read_placed_node_data(applier, node_id, root, |data| {
662        let target = content_layer_mut(container)?;
663        let slices = &data.modifier_slices;
664        let state = &data.layout_state;
665        if !retained_layer_matches_layout(target, &data, placement, wrapped, moved) {
666            return None;
667        }
668        let context = LowerContext {
669            inherited_motion_context_animated: ancestors.inherited_motion_context_animated,
670            inherited_translated_content_context: ancestors.inherited_translated_content_context,
671            parent_abs: Some(ancestors.parent_abs),
672            parent_content_offset: ancestors.parent_content_offset,
673        };
674        let previous = HitGraphState::of(target);
675        let (head, child_context) = prepare_node_layer(node_id, slices, state, context);
676        assign_layer(target, head);
677        if wrapped {
678            target.transform_to_parent = layer_transform_to_parent(
679                target.local_bounds,
680                Point::default(),
681                &target.graphics_layer,
682            );
683            target.origin_in_parent = Point::default();
684        }
685        if target.has_origin_sinks
686            && let Some(child_abs) = child_context.parent_abs
687        {
688            for child in &target.children {
689                if let RenderNode::Layer(child) = child {
690                    publish_retained_window_geometry(applier, child, child_abs);
691                }
692            }
693        }
694        Some(previous.dirty_against(&HitGraphState::of(target)))
695    })??;
696    if wrapped {
697        container.refresh_child_facts();
698        container.has_origin_sinks = children_have_origin_sinks(&container.children);
699        container.forget_own_raster_cache_hashes();
700    }
701    Some(hit_graph_dirty)
702}
703
704fn publish_retained_window_geometry(
705    applier: &MemoryApplier,
706    layer: &LayerNode,
707    parent_abs: AbsOrigin,
708) {
709    if !layer.has_origin_sinks {
710        return;
711    }
712    let child_abs = parent_abs.children_of(layer);
713    if let Some(node_id) = layer.node_id {
714        read_node_data(applier, node_id, |data| {
715            data.modifier_slices.publish_window_geometry(
716                Point {
717                    x: parent_abs.content_origin.x + layer.origin_in_parent.x,
718                    y: parent_abs.content_origin.y + layer.origin_in_parent.y,
719                },
720                child_abs.window_transform,
721                data.layout_state.size(),
722            );
723        });
724    }
725    for child in &layer.children {
726        if let RenderNode::Layer(child) = child {
727            publish_retained_window_geometry(applier, child, child_abs);
728        }
729    }
730}
731
732fn try_translate_scrolled_layer(
733    applier: &MemoryApplier,
734    container: &mut LayerNode,
735    dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
736    ancestors: TranslateAncestorContext,
737) -> bool {
738    let Some(node_id) = layer_identity(container) else {
739        return translate_bail("no node id");
740    };
741    read_node_data(applier, node_id, |data| {
742        if container.wraps.is_none() {
743            return translate_layer_from_data(
744                applier,
745                container,
746                dirty_nodes,
747                ancestors,
748                data,
749                false,
750            );
751        }
752        let outer_count = data.modifier_slices.outer_draw_command_count();
753        if outer_count == 0 {
754            return translate_bail("outer draws removed");
755        }
756        let size = data.layout_state.size();
757        let placement = data.layout_state.position();
758        let slices = Rc::clone(&data.modifier_slices);
759        let Some(inner) = content_layer_mut(container) else {
760            return translate_bail("wrapped layer missing");
761        };
762        if !translate_layer_from_data(applier, inner, dirty_nodes, ancestors, data, true) {
763            return false;
764        }
765        let outer = outer_draws(node_id, slices.draw_commands(), outer_count, size)
766            .expect("outer command count is nonzero");
767        let layer =
768            take_wrapped_layer(container, node_id).expect("the wrapped layer was found above");
769        write_wrapper(
770            container,
771            layer,
772            placement,
773            outer,
774            ancestors.parent_content_offset,
775        );
776        container.note_content_change(ancestors.update);
777        true
778    })
779    .unwrap_or_else(|| translate_bail("container snapshot read failed"))
780}
781
782/// What a container keeps its children under: its node, its graphics layer
783/// and its bounds at its current size.
784struct TranslatedContainer {
785    node_id: NodeId,
786    graphics_layer: Option<GraphicsLayer>,
787    local_bounds: Rect,
788    node_bounds: Option<Rect>,
789}
790
791fn translated_container(
792    container: &LayerNode,
793    layout_state: &cranpose_ui::widgets::LayoutState,
794    modifier_slices: &ModifierNodeSlices,
795    inherited_motion_context_animated: bool,
796    wrapped: bool,
797) -> Result<TranslatedContainer, &'static str> {
798    if cranpose_core::env_flag!("CRANPOSE_DISABLE_SCROLL_TRANSLATE") {
799        return Err("fast path disabled by ablation switch");
800    }
801    let Some(node_id) = container.node_id else {
802        return Err("no node id");
803    };
804    if !layout_state.is_placed() {
805        return Err("container unplaced");
806    }
807    let outer_count = modifier_slices.outer_draw_command_count();
808    if (outer_count > 0 && !wrapped)
809        || (inherited_motion_context_animated || modifier_slices.motion_context_animated())
810            != container.motion_context_animated
811        || modifier_slices.translated_content_context() != container.translated_content_context
812    {
813        return Err("container outer draws or translated context changed");
814    }
815    let clip_to_bounds = modifier_slices.clip_to_bounds();
816    if clip_to_bounds != container.clip_to_bounds {
817        return Err("container clip changed");
818    }
819    // A resized container keeps its children: only its bounds and the clip
820    // shaped to them follow its size. Any other change to its graphics layer
821    // may change what its children inherit.
822    let shaped = |bounds: Rect| {
823        graphics_layer_with_shaped_clip(
824            modifier_slices.graphics_layer(),
825            clip_to_bounds,
826            modifier_slices.corner_shape(),
827            bounds,
828        )
829    };
830    if shaped(container.local_bounds)
831        .as_ref()
832        .unwrap_or(&GraphicsLayer::DEFAULT)
833        != &*container.graphics_layer
834    {
835        return Err("container graphics layer changed");
836    }
837    let (local_bounds, node_bounds) = layer_and_node_bounds(modifier_slices, layout_state.size());
838    Ok(TranslatedContainer {
839        node_id,
840        graphics_layer: shaped(local_bounds),
841        local_bounds,
842        node_bounds,
843    })
844}
845
846/// What a scroll step reconciles a container's children in, kept between
847/// steps so a step allocates nothing of its own. Every list is empty
848/// between steps, so it holds no layer.
849#[derive(Default)]
850pub(crate) struct TranslateScratch {
851    /// The placed children, in their new order.
852    placed_fresh: Vec<(NodeId, cranpose_ui::widgets::LayoutState)>,
853    old_index_by_id: HashMap<NodeId, usize>,
854    /// The container's previous children by their index, until each is
855    /// kept or recycled.
856    retained: Vec<Option<Box<LayerNode>>>,
857    /// The previous children that stay, in their new order; `None` where a
858    /// child enters.
859    kept: Vec<Option<Box<LayerNode>>>,
860    /// The children built for the step, in their new order.
861    entering: Vec<(NodeId, Box<LayerNode>)>,
862}
863
864/// Fills `scratch` with `container`'s placed children and the index of its
865/// previous ones, and returns whether they are the same children in the
866/// same order.
867fn translated_children(
868    applier: &MemoryApplier,
869    container: &LayerNode,
870    dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
871    fresh_children: &[NodeId],
872    scratch: &mut TranslateScratch,
873) -> Result<bool, &'static str> {
874    let placed_fresh = &mut scratch.placed_fresh;
875    placed_fresh.clear();
876    for child_id in fresh_children {
877        let Some(state) = placed_child_layout_state(applier, *child_id) else {
878            continue;
879        };
880        placed_fresh.push((*child_id, state));
881    }
882    let children_unchanged = container.children.len() == placed_fresh.len()
883        && container
884        .children
885        .iter()
886        .zip(placed_fresh.iter())
887        .all(|(child, (id, _))| {
888            matches!(child, RenderNode::Layer(layer) if layer_identity(layer) == Some(*id))
889        });
890    let old_index_by_id = &mut scratch.old_index_by_id;
891    old_index_by_id.clear();
892    if !children_unchanged {
893        for (index, child) in container.children.iter().enumerate() {
894            let RenderNode::Layer(layer) = child else {
895                return Err("child without node id");
896            };
897            let Some(id) = layer_identity(layer) else {
898                return Err("child without node id");
899            };
900            old_index_by_id.insert(id, index);
901        }
902    }
903    check_retained_children(
904        container,
905        dirty_nodes,
906        &scratch.placed_fresh,
907        children_unchanged,
908        &scratch.old_index_by_id,
909    )?;
910    Ok(children_unchanged)
911}
912
913/// Checks that every child kept as it is still fits its layer. A moved child
914/// that no longer fits is drawn again: it becomes a content update.
915fn check_retained_children(
916    container: &LayerNode,
917    dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
918    placed_fresh: &[(NodeId, cranpose_ui::widgets::LayoutState)],
919    children_unchanged: bool,
920    old_index_by_id: &HashMap<NodeId, usize>,
921) -> Result<(), &'static str> {
922    for (fresh_index, (child_id, state)) in placed_fresh.iter().enumerate() {
923        let old_index = if children_unchanged {
924            fresh_index
925        } else if let Some(index) = old_index_by_id.get(child_id) {
926            *index
927        } else {
928            continue;
929        };
930        let RenderNode::Layer(layer) = &container.children[old_index] else {
931            return Err("retained child slot is not a layer");
932        };
933        let fits = !layer.has_origin_sinks && Rect::from_size(state.size()) == layer.node_rect();
934        match dirty_nodes.get_mut(child_id) {
935            Some(NodeUpdate::Content | NodeUpdate::Layer) => {}
936            Some(kind @ (NodeUpdate::Moved | NodeUpdate::MovedLayer)) => {
937                if !fits {
938                    *kind = NodeUpdate::Content;
939                }
940            }
941            None if layer.has_origin_sinks => {
942                return Err("child subtree publishes window origins");
943            }
944            None if !fits => return Err("child resized"),
945            None => {}
946        }
947    }
948    Ok(())
949}
950
951#[derive(Clone, Copy)]
952struct TranslateGeometry {
953    content_offset: Point,
954    window_transform: ProjectiveTransform,
955    top_left: Point,
956    child_origin: Point,
957}
958
959impl TranslateGeometry {
960    fn new(
961        layout_state: &cranpose_ui::widgets::LayoutState,
962        graphics_layer: &GraphicsLayer,
963        local_bounds: Rect,
964        parent_abs: AbsOrigin,
965    ) -> Self {
966        let content_offset = layout_state.content_offset();
967        let top_left = Point {
968            x: parent_abs.content_origin.x + layout_state.position().x,
969            y: parent_abs.content_origin.y + layout_state.position().y,
970        };
971        let window_transform = layer_transform_to_window(
972            parent_abs.window_transform,
973            top_left,
974            local_bounds,
975            graphics_layer,
976        );
977        Self {
978            content_offset,
979            window_transform,
980            top_left,
981            child_origin: Point {
982                x: top_left.x + content_offset.x,
983                y: top_left.y + content_offset.y,
984            },
985        }
986    }
987
988    fn child_abs(self) -> AbsOrigin {
989        AbsOrigin {
990            content_origin: self.child_origin,
991            window_transform: self.window_transform,
992        }
993    }
994}
995
996/// Moves `container`'s previous children that stay into `scratch.kept`, in
997/// their new order, and the subtrees of those that leave into the layer
998/// pool, so the entering children are built in their allocations.
999/// Recycles the children that leave `container`. Their nodes leave the
1000/// dirty set with them: nothing remains to update for a node the scene no
1001/// longer shows, and one left dirty would go unmatched and rebuild the whole
1002/// scene.
1003fn recycle_leaving_children(
1004    container: &mut LayerNode,
1005    scratch: &mut TranslateScratch,
1006    dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
1007) {
1008    let TranslateScratch {
1009        placed_fresh,
1010        old_index_by_id,
1011        retained,
1012        kept,
1013        ..
1014    } = scratch;
1015    retained.clear();
1016    retained.extend(container.children.drain(..).map(|child| match child {
1017        RenderNode::Layer(layer) => Some(layer),
1018        RenderNode::Primitive(_) | RenderNode::DrawRun(_) => None,
1019    }));
1020    kept.clear();
1021    kept.extend(placed_fresh.iter().map(|(child_id, _)| {
1022        old_index_by_id
1023            .get(child_id)
1024            .and_then(|index| retained[*index].take())
1025    }));
1026    for leaving in retained.drain(..).flatten() {
1027        forget_dirty_subtree(&leaving, dirty_nodes);
1028        crate::layer_recycling::recycle(leaving);
1029    }
1030}
1031
1032/// Builds the children that enter `container`. They are built from the
1033/// applier's current state, so their nodes leave the dirty set: the walk
1034/// beneath would otherwise build each of them a second time.
1035fn build_entering_children(
1036    applier: &MemoryApplier,
1037    container: &LayerNode,
1038    scratch: &mut TranslateScratch,
1039    dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
1040    geometry: TranslateGeometry,
1041    inherited: (bool, u64),
1042) {
1043    let (child_inherited_translated_content_context, update) = inherited;
1044    let entering = &mut scratch.entering;
1045    entering.clear();
1046    for (child_id, _) in &scratch.placed_fresh {
1047        if scratch.old_index_by_id.contains_key(child_id) {
1048            continue;
1049        }
1050        let context = LowerContext {
1051            inherited_motion_context_animated: container.motion_context_animated,
1052            inherited_translated_content_context: child_inherited_translated_content_context,
1053            parent_abs: Some(geometry.child_abs()),
1054            parent_content_offset: geometry.content_offset,
1055        };
1056        let Some(mut lowered) = lower_child(applier, *child_id, context) else {
1057            continue;
1058        };
1059        lowered.note_content_change(update);
1060        forget_dirty_subtree(&lowered, dirty_nodes);
1061        entering.push((*child_id, lowered));
1062    }
1063}
1064
1065/// Records `container`'s own draws and text around the child layers its list
1066/// holds, in the order [`write_node_content`] writes a layer.
1067fn write_own_content_around_children(
1068    container: &mut LayerNode,
1069    node_id: NodeId,
1070    modifier_slices: &ModifierNodeSlices,
1071    size: Size,
1072) {
1073    if modifier_slices.draw_commands()[modifier_slices.outer_draw_command_count()..].is_empty()
1074        && !modifier_slices.has_text()
1075    {
1076        return;
1077    }
1078    let kept = container.children.len();
1079    write_node_content(
1080        &mut container.children,
1081        node_id,
1082        modifier_slices,
1083        size,
1084        (kept, 0),
1085        |_| {},
1086    );
1087}
1088
1089fn apply_translated_container_state(
1090    container: &mut LayerNode,
1091    modifier_slices: &ModifierNodeSlices,
1092    layout_state: &cranpose_ui::widgets::LayoutState,
1093    parent_content_offset: Point,
1094    geometry: TranslateGeometry,
1095) {
1096    container.transform_to_parent = placed_transform(
1097        container.local_bounds,
1098        layout_state.position(),
1099        &container.graphics_layer,
1100        parent_content_offset,
1101    );
1102    container.content_offset = geometry.content_offset;
1103    if container.translated_content_context {
1104        container.translated_content_offset = modifier_slices
1105            .translated_content_offset()
1106            .unwrap_or(geometry.content_offset);
1107    }
1108    modifier_slices.publish_window_geometry(
1109        geometry.top_left,
1110        geometry.window_transform,
1111        layout_state.size(),
1112    );
1113    container.origin_in_parent = layout_state.position();
1114}
1115
1116fn reconcile_translated_children(
1117    container: &mut LayerNode,
1118    dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
1119    scratch: &mut TranslateScratch,
1120    children_unchanged: bool,
1121    geometry: TranslateGeometry,
1122) {
1123    if children_unchanged {
1124        for (child, (child_id, state)) in container.children.iter_mut().zip(&scratch.placed_fresh) {
1125            let RenderNode::Layer(layer) = child else {
1126                unreachable!("retained child identities were checked");
1127            };
1128            if keeps_retained_child(dirty_nodes, *child_id) {
1129                translate_retained_child(layer, state, geometry.content_offset);
1130            }
1131        }
1132        return;
1133    }
1134    let mut entering = scratch.entering.drain(..).peekable();
1135    for ((child_id, state), kept) in scratch.placed_fresh.iter().zip(scratch.kept.drain(..)) {
1136        if let Some(mut layer) = kept {
1137            if keeps_retained_child(dirty_nodes, *child_id) {
1138                translate_retained_child(&mut layer, state, geometry.content_offset);
1139            }
1140            container.children.push(RenderNode::Layer(layer));
1141        } else if let Some((_, lowered)) = entering.next_if(|(id, _)| id == child_id) {
1142            dirty_nodes.remove(child_id);
1143            remove_dirty_descendants(&lowered, dirty_nodes);
1144
1145            container.children.push(RenderNode::Layer(lowered));
1146        }
1147    }
1148}
1149
1150fn translate_layer_from_data(
1151    applier: &MemoryApplier,
1152    container: &mut LayerNode,
1153    dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
1154    ancestors: TranslateAncestorContext,
1155    data: SnapshotNodeData<'_>,
1156    wrapped: bool,
1157) -> bool {
1158    let TranslateAncestorContext {
1159        inherited_motion_context_animated,
1160        update,
1161        inherited_translated_content_context,
1162        parent_content_offset,
1163        parent_abs,
1164    } = ancestors;
1165    let SnapshotNodeData {
1166        layout_state,
1167        modifier_slices,
1168        children: fresh_children,
1169        window_root,
1170    } = data;
1171    let layout_state = if window_root {
1172        layout_state.at_origin()
1173    } else {
1174        layout_state
1175    };
1176    let container_plan = match translated_container(
1177        container,
1178        &layout_state,
1179        &modifier_slices,
1180        inherited_motion_context_animated,
1181        wrapped,
1182    ) {
1183        Ok(plan) => plan,
1184        Err(reason) => return translate_bail(reason),
1185    };
1186    let TranslatedContainer {
1187        node_id,
1188        graphics_layer,
1189        local_bounds,
1190        node_bounds,
1191    } = container_plan;
1192    if container.local_bounds != local_bounds {
1193        container.local_bounds = local_bounds;
1194        container.graphics_layer.replace(graphics_layer);
1195    }
1196    container.node_bounds = node_bounds;
1197    // The container's own draws and text are recorded again around the
1198    // children it keeps: an unchanged draw reuses its recording, while
1199    // building the container anew lowered every child layer beneath it too.
1200    crate::layer_recycling::recycle_primitives(container);
1201    let mut scratch = crate::thread_state::with(|state| state.translate_scratch.take());
1202    let children_unchanged = match translated_children(
1203        applier,
1204        container,
1205        dirty_nodes,
1206        fresh_children,
1207        &mut scratch,
1208    ) {
1209        Ok(unchanged) => unchanged,
1210        Err(reason) => {
1211            crate::thread_state::with(|state| state.translate_scratch.set(scratch));
1212            return translate_bail(reason);
1213        }
1214    };
1215
1216    let geometry = TranslateGeometry::new(
1217        &layout_state,
1218        &container.graphics_layer,
1219        container.local_bounds,
1220        parent_abs,
1221    );
1222    let child_inherited_translated_content_context =
1223        inherited_translated_content_context || container.translated_content_context;
1224    if !children_unchanged {
1225        recycle_leaving_children(container, &mut scratch, dirty_nodes);
1226        build_entering_children(
1227            applier,
1228            container,
1229            &mut scratch,
1230            dirty_nodes,
1231            geometry,
1232            (child_inherited_translated_content_context, update),
1233        );
1234    }
1235
1236    apply_translated_container_state(
1237        container,
1238        &modifier_slices,
1239        &layout_state,
1240        parent_content_offset,
1241        geometry,
1242    );
1243
1244    reconcile_translated_children(
1245        container,
1246        dirty_nodes,
1247        &mut scratch,
1248        children_unchanged,
1249        geometry,
1250    );
1251    crate::thread_state::with(|state| state.translate_scratch.set(scratch));
1252    write_own_content_around_children(container, node_id, &modifier_slices, layout_state.size());
1253    container.hit_test = hit_test_from_slices(&modifier_slices);
1254
1255    container.has_origin_sinks = modifier_slices_have_origin_sinks(&modifier_slices)
1256        || children_have_origin_sinks(&container.children);
1257    container.refresh_child_facts();
1258    container.note_content_change(update);
1259
1260    true
1261}
1262
1263/// Whether a retained child keeps its layer as it is and only follows its
1264/// placement: it is not dirty, or it only moved, which this placement
1265/// settles.
1266fn keeps_retained_child(dirty_nodes: &mut HashMap<NodeId, NodeUpdate>, child_id: NodeId) -> bool {
1267    match dirty_nodes.get(&child_id) {
1268        None => true,
1269        Some(NodeUpdate::Moved) => {
1270            dirty_nodes.remove(&child_id);
1271            true
1272        }
1273        Some(NodeUpdate::Content | NodeUpdate::Layer | NodeUpdate::MovedLayer) => false,
1274    }
1275}
1276
1277fn translate_retained_child(
1278    layer: &mut LayerNode,
1279    state: &cranpose_ui::widgets::LayoutState,
1280    content_offset: Point,
1281) {
1282    layer.transform_to_parent = placed_transform(
1283        layer.local_bounds,
1284        state.position(),
1285        &layer.graphics_layer,
1286        content_offset,
1287    );
1288    layer.origin_in_parent = state.position();
1289}
1290
1291/// Rebuilds `layer` with `rebuild`, which reports whether it succeeded, and
1292/// returns whether the hit geometry of the layer or of a layer under it
1293/// changed; `None` when the rebuild failed. A rebuild takes in the moves and
1294/// layer changes of the nodes under the layer, which its own state does not
1295/// show: a sheet that slides inside a container that redraws keeps the
1296/// container's state.
1297fn rebuild_tracking_hits(
1298    layer: &mut LayerNode,
1299    rebuild: impl FnOnce(&mut LayerNode) -> bool,
1300) -> Option<bool> {
1301    let previous = HitGraphState::of(layer);
1302    let mut descendants = crate::thread_state::with(|state| state.hit_geometry_scratch.take());
1303    descendants.clear();
1304    visit_hit_layers(layer, &mut |child| {
1305        descendants.push(HitGraphState::of(child));
1306        true
1307    });
1308    let dirty = rebuild(layer).then(|| {
1309        let mut expected = descendants.iter();
1310        previous.dirty_against(&HitGraphState::of(layer))
1311            || !visit_hit_layers(layer, &mut |child| {
1312                expected.next() == Some(&HitGraphState::of(child))
1313            })
1314            || expected.next().is_some()
1315    });
1316    crate::thread_state::with(|state| state.hit_geometry_scratch.set(descendants));
1317    dirty
1318}
1319
1320/// Visits the layers under `layer` that are hit targets or hold one, in draw
1321/// order, while `visit` returns true; returns whether every visit did.
1322fn visit_hit_layers(layer: &LayerNode, visit: &mut impl FnMut(&LayerNode) -> bool) -> bool {
1323    for child in &layer.children {
1324        let RenderNode::Layer(child) = child else {
1325            continue;
1326        };
1327        if child.hit_test.is_none() && !child.has_hit_targets {
1328            continue;
1329        }
1330        if !visit(child) || !visit_hit_layers(child, visit) {
1331            return false;
1332        }
1333    }
1334    true
1335}
1336
1337#[derive(PartialEq)]
1338pub(crate) struct HitGraphState {
1339    hit_test: bool,
1340    has_hit_targets: bool,
1341    local_bounds: Rect,
1342    node_bounds: Option<Rect>,
1343    transform_to_parent: ProjectiveTransform,
1344    clip_rect: Option<Rect>,
1345    shape: LayerShape,
1346}
1347
1348impl HitGraphState {
1349    fn of(layer: &LayerNode) -> Self {
1350        Self {
1351            hit_test: layer.hit_test.is_some(),
1352            has_hit_targets: layer.has_hit_targets,
1353            local_bounds: layer.local_bounds,
1354            node_bounds: layer.node_bounds,
1355            transform_to_parent: layer.transform_to_parent,
1356            clip_rect: layer.clip_rect(),
1357            shape: layer.graphics_layer.shape,
1358        }
1359    }
1360
1361    fn dirty_against(&self, replacement: &Self) -> bool {
1362        self.hit_test
1363            || replacement.hit_test
1364            || ((self.has_hit_targets || replacement.has_hit_targets) && self != replacement)
1365    }
1366}
1367
1368fn release_for_rebuild(layer: &mut LayerNode) {
1369    layer.hit_test = None;
1370    crate::layer_recycling::recycle_children(layer);
1371}
1372
1373/// Takes `layer`'s node and every node beneath it out of `dirty_nodes`.
1374fn forget_dirty_subtree(layer: &LayerNode, dirty_nodes: &mut HashMap<NodeId, NodeUpdate>) {
1375    if let Some(node_id) = layer_identity(layer) {
1376        dirty_nodes.remove(&node_id);
1377    }
1378    remove_dirty_descendants(layer, dirty_nodes);
1379}
1380
1381fn remove_dirty_descendants(layer: &LayerNode, dirty_nodes: &mut HashMap<NodeId, NodeUpdate>) {
1382    for child in &layer.children {
1383        let RenderNode::Layer(child_layer) = child else {
1384            continue;
1385        };
1386        if let Some(node_id) = child_layer.node_id {
1387            dirty_nodes.remove(&node_id);
1388        }
1389        remove_dirty_descendants(child_layer, dirty_nodes);
1390    }
1391}
1392
1393#[derive(Clone, Copy)]
1394struct LowerContext {
1395    inherited_motion_context_animated: bool,
1396    inherited_translated_content_context: bool,
1397    parent_abs: Option<AbsOrigin>,
1398    parent_content_offset: Point,
1399}
1400
1401impl LowerContext {
1402    const ROOT: Self = Self {
1403        inherited_motion_context_animated: false,
1404        inherited_translated_content_context: false,
1405        parent_abs: Some(AbsOrigin::ROOT),
1406        parent_content_offset: Point { x: 0.0, y: 0.0 },
1407    };
1408
1409    fn for_children(
1410        self,
1411        slices: &ModifierNodeSlices,
1412        content_offset: Point,
1413        parent_abs: Option<AbsOrigin>,
1414    ) -> Self {
1415        Self {
1416            inherited_motion_context_animated: self.inherited_motion_context_animated
1417                || slices.motion_context_animated(),
1418            inherited_translated_content_context: self.inherited_translated_content_context
1419                || slices.translated_content_context(),
1420            parent_abs,
1421            parent_content_offset: content_offset,
1422        }
1423    }
1424}
1425
1426struct LayerHead {
1427    node_id: Option<NodeId>,
1428    wraps: Option<NodeId>,
1429    local_bounds: Rect,
1430    node_bounds: Option<Rect>,
1431    transform_to_parent: ProjectiveTransform,
1432    content_offset: Point,
1433    motion_context_animated: bool,
1434    translated_content_context: bool,
1435    translated_content_offset: Point,
1436    origin_in_parent: Point,
1437    graphics_layer: Option<GraphicsLayer>,
1438    clip_to_bounds: bool,
1439    hit_test: Option<HitTestNode>,
1440    has_origin_sinks: bool,
1441    isolation: IsolationReasons,
1442    cache_policy: CachePolicy,
1443}
1444
1445struct NodeFrame {
1446    local_bounds: Rect,
1447    node_bounds: Option<Rect>,
1448    placement: Point,
1449    content_offset: Point,
1450    translated_content_offset: Point,
1451}
1452
1453fn node_layer_head(
1454    node_id: NodeId,
1455    slices: &Rc<ModifierNodeSlices>,
1456    frame: NodeFrame,
1457    graphics_layer: Option<GraphicsLayer>,
1458    context: LowerContext,
1459) -> LayerHead {
1460    let clip_to_bounds = slices.clip_to_bounds();
1461    let translated_content_context = slices.translated_content_context();
1462    let properties = graphics_layer.as_ref().unwrap_or(&GraphicsLayer::DEFAULT);
1463    let isolation = isolation_reasons(properties);
1464    LayerHead {
1465        node_id: Some(node_id),
1466        wraps: None,
1467        local_bounds: frame.local_bounds,
1468        node_bounds: frame.node_bounds,
1469        transform_to_parent: placed_transform(
1470            frame.local_bounds,
1471            frame.placement,
1472            properties,
1473            context.parent_content_offset,
1474        ),
1475        content_offset: frame.content_offset,
1476        motion_context_animated: context.inherited_motion_context_animated
1477            || slices.motion_context_animated(),
1478        translated_content_context,
1479        translated_content_offset: if translated_content_context {
1480            frame.translated_content_offset
1481        } else {
1482            Point::default()
1483        },
1484        origin_in_parent: frame.placement,
1485        cache_policy: layer_cache_policy(properties, isolation),
1486        isolation,
1487        graphics_layer,
1488        clip_to_bounds,
1489        hit_test: hit_test_from_slices(slices),
1490        has_origin_sinks: modifier_slices_have_origin_sinks(slices),
1491    }
1492}
1493
1494fn assign_layer(layer: &mut LayerNode, head: LayerHead) {
1495    let LayerNode {
1496        node_id,
1497        wraps,
1498        local_bounds,
1499        node_bounds,
1500        transform_to_parent,
1501        content_offset,
1502        motion_context_animated,
1503        translated_content_context,
1504        translated_content_offset,
1505        origin_in_parent,
1506        graphics_layer,
1507        clip_to_bounds,
1508        hit_test,
1509        has_hit_targets,
1510        has_origin_sinks,
1511        draws_within_bounds,
1512        isolation,
1513        cache_policy,
1514        cache_hashes,
1515        content_changes,
1516        children,
1517    } = layer;
1518    if *node_id != head.node_id {
1519        *content_changes = ContentChanges::default();
1520    }
1521    *node_id = head.node_id;
1522    *wraps = head.wraps;
1523    *local_bounds = head.local_bounds;
1524    *node_bounds = head.node_bounds;
1525    *transform_to_parent = head.transform_to_parent;
1526    *content_offset = head.content_offset;
1527    *motion_context_animated = head.motion_context_animated;
1528    *translated_content_context = head.translated_content_context;
1529    *translated_content_offset = head.translated_content_offset;
1530    *origin_in_parent = head.origin_in_parent;
1531    graphics_layer.replace(head.graphics_layer);
1532    *clip_to_bounds = head.clip_to_bounds;
1533    *hit_test = head.hit_test;
1534    *has_hit_targets = false;
1535    *has_origin_sinks = head.has_origin_sinks || children_have_origin_sinks(children);
1536    *draws_within_bounds = false;
1537    *isolation = head.isolation;
1538    *cache_policy = head.cache_policy;
1539    cache_hashes.set(None);
1540    layer.refresh_child_facts();
1541}
1542
1543fn children_have_origin_sinks(children: &[RenderNode]) -> bool {
1544    children.iter().any(|child| match child {
1545        RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
1546        RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1547    })
1548}
1549
1550fn placed_transform(
1551    local_bounds: Rect,
1552    placement: Point,
1553    graphics_layer: &GraphicsLayer,
1554    parent_content_offset: Point,
1555) -> ProjectiveTransform {
1556    let transform = layer_transform_to_parent(local_bounds, placement, graphics_layer);
1557    if parent_content_offset == Point::default() {
1558        transform
1559    } else {
1560        transform.then(ProjectiveTransform::translation(
1561            parent_content_offset.x,
1562            parent_content_offset.y,
1563        ))
1564    }
1565}
1566
1567/// Writes a node's own draws and text around its child layers: `kept`
1568/// layers already end `list`, and `write_children` appends `written` more.
1569fn write_node_content(
1570    list: &mut Vec<RenderNode>,
1571    node_id: NodeId,
1572    slices: &ModifierNodeSlices,
1573    size: Size,
1574    (kept, written): (usize, usize),
1575    write_children: impl FnOnce(&mut Vec<RenderNode>),
1576) {
1577    let outer_count = slices.outer_draw_command_count();
1578    let commands = &slices.draw_commands()[outer_count..];
1579    crate::layer_recycling::reserve_nodes(
1580        list,
1581        layer_node_capacity(commands, written, slices.has_text()),
1582    );
1583    let first_kept = list.len() - kept;
1584    let first_draw = list.len();
1585    append_draw_nodes(
1586        list,
1587        node_id,
1588        commands,
1589        outer_count,
1590        DrawPass::Record(DrawPlacement::Behind),
1591        size,
1592        PrimitivePhase::BeforeChildren,
1593    );
1594    if let Some(text) = text_node_from_parts(TextNodeParts {
1595        node_id,
1596        text_rect: slices.text_content_rect(size),
1597        text_style: slices.text_style().as_deref(),
1598        text_layout_options: slices.text_layout_options(),
1599        text_pan: slices.text_pan_resolver(),
1600        measured_layout: slices.measured_text_layout(),
1601    }) {
1602        list.push(RenderNode::Primitive(PrimitiveEntry {
1603            phase: PrimitivePhase::BeforeChildren,
1604            node: PrimitiveNode::Text(crate::layer_recycling::boxed_text(text)),
1605        }));
1606    }
1607    // The kept layers go after the draws and text just written.
1608    list[first_kept..].rotate_left(kept);
1609    let first_draw = first_draw - kept;
1610    write_children(list);
1611    append_draw_nodes(
1612        list,
1613        node_id,
1614        commands,
1615        outer_count,
1616        DrawPass::OverlayFromOutput(first_draw),
1617        size,
1618        PrimitivePhase::AfterChildren,
1619    );
1620}
1621
1622fn write_layer_with_outer(
1623    target: &mut LayerNode,
1624    head: LayerHead,
1625    outer: Option<OuterDraws>,
1626    placement: Point,
1627    parent_content_offset: Point,
1628    write_children: impl FnOnce(&mut Vec<RenderNode>),
1629) {
1630    debug_assert!(target.children.is_empty(), "a layer is written emptied");
1631    let Some(outer) = outer else {
1632        write_children(&mut target.children);
1633        assign_layer(target, head);
1634        return;
1635    };
1636    let mut layer = crate::layer_recycling::layer_box();
1637    write_children(&mut layer.children);
1638    assign_layer(&mut layer, head);
1639    write_wrapper(target, layer, placement, outer, parent_content_offset);
1640}
1641
1642fn write_snapshot_layer(
1643    snapshot: BuildNodeSnapshot,
1644    context: LowerContext,
1645    target: &mut LayerNode,
1646) {
1647    let BuildNodeSnapshot {
1648        node_id,
1649        placement,
1650        size,
1651        content_offset,
1652        slices,
1653        graphics_layer,
1654        children,
1655    } = snapshot;
1656    let (local_bounds, node_bounds) = layer_and_node_bounds(&slices, size);
1657    let child_context = context.for_children(&slices, content_offset, None);
1658    let head = node_layer_head(
1659        node_id,
1660        &slices,
1661        NodeFrame {
1662            local_bounds,
1663            node_bounds,
1664            placement,
1665            content_offset,
1666            translated_content_offset: content_offset,
1667        },
1668        graphics_layer,
1669        context,
1670    );
1671    let outer = outer_draws(
1672        node_id,
1673        slices.draw_commands(),
1674        slices.outer_draw_command_count(),
1675        size,
1676    );
1677    write_layer_with_outer(
1678        target,
1679        head,
1680        outer,
1681        placement,
1682        context.parent_content_offset,
1683        |list| {
1684            write_node_content(list, node_id, &slices, size, (0, children.len()), |list| {
1685                for child in children {
1686                    let mut layer = crate::layer_recycling::layer_box();
1687                    write_snapshot_layer(child, child_context, &mut layer);
1688                    list.push(RenderNode::Layer(layer));
1689                }
1690            });
1691        },
1692    );
1693}
1694
1695#[derive(Clone, Copy)]
1696struct AbsOrigin {
1697    content_origin: Point,
1698    window_transform: ProjectiveTransform,
1699}
1700
1701impl AbsOrigin {
1702    const ROOT: AbsOrigin = AbsOrigin {
1703        content_origin: Point { x: 0.0, y: 0.0 },
1704        window_transform: ProjectiveTransform::identity(),
1705    };
1706
1707    fn children_of(self, layer: &LayerNode) -> AbsOrigin {
1708        let top_left = Point {
1709            x: self.content_origin.x + layer.origin_in_parent.x,
1710            y: self.content_origin.y + layer.origin_in_parent.y,
1711        };
1712        AbsOrigin {
1713            content_origin: Point {
1714                x: self.content_origin.x + layer.origin_in_parent.x + layer.content_offset.x,
1715                y: self.content_origin.y + layer.origin_in_parent.y + layer.content_offset.y,
1716            },
1717            window_transform: layer_transform_to_window(
1718                self.window_transform,
1719                top_left,
1720                layer.local_bounds,
1721                &layer.graphics_layer,
1722            ),
1723        }
1724    }
1725}
1726
1727fn read_placed_node_data<R>(
1728    applier: &MemoryApplier,
1729    node_id: NodeId,
1730    root: bool,
1731    read: impl FnOnce(SnapshotNodeData<'_>) -> R,
1732) -> Option<R> {
1733    read_node_data(applier, node_id, |mut data| {
1734        if data.window_root {
1735            if !root {
1736                return None;
1737            }
1738            data.layout_state = data.layout_state.at_origin();
1739        }
1740        note_layer_lowered();
1741        if !data.layout_state.is_placed() {
1742            return None;
1743        }
1744        Some(read(data))
1745    })?
1746}
1747
1748fn lower_root_into(applier: &MemoryApplier, node_id: NodeId, target: &mut LayerNode) -> bool {
1749    read_placed_node_data(applier, node_id, true, |data| {
1750        write_node_layer(applier, node_id, data, LowerContext::ROOT, target);
1751    })
1752    .is_some()
1753}
1754
1755fn lower_child(
1756    applier: &MemoryApplier,
1757    node_id: NodeId,
1758    context: LowerContext,
1759) -> Option<Box<LayerNode>> {
1760    read_placed_node_data(applier, node_id, false, |data| {
1761        let mut layer = crate::layer_recycling::layer_box();
1762        write_node_layer(applier, node_id, data, context, &mut layer);
1763        layer
1764    })
1765}
1766
1767fn placed_child_layout_state(
1768    applier: &MemoryApplier,
1769    node_id: NodeId,
1770) -> Option<cranpose_ui::widgets::LayoutState> {
1771    let node: &dyn Any = applier.get_ref(node_id).ok()?;
1772    let state = if let Some(node) = node.downcast_ref::<LayoutNode>() {
1773        if node.is_window_root() {
1774            return None;
1775        }
1776        node.layout_state()
1777    } else {
1778        node.downcast_ref::<SubcomposeLayoutNode>()?.layout_state()
1779    };
1780    state.is_placed().then_some(state)
1781}
1782
1783fn read_node_data<R>(
1784    applier: &MemoryApplier,
1785    node_id: NodeId,
1786    read: impl FnOnce(SnapshotNodeData<'_>) -> R,
1787) -> Option<R> {
1788    let node: &dyn Any = applier.get_ref(node_id).ok()?;
1789    if let Some(node) = node.downcast_ref::<LayoutNode>() {
1790        let z_order = z_ordered_children(applier, &node.children);
1791        return Some(read(SnapshotNodeData {
1792            layout_state: node.layout_state(),
1793            modifier_slices: node.modifier_slices_snapshot(),
1794            children: z_order.as_deref().unwrap_or(&node.children),
1795            window_root: node.is_window_root(),
1796        }));
1797    }
1798    let node = node.downcast_ref::<SubcomposeLayoutNode>()?;
1799    let layout_state = node.layout_state();
1800    let modifier_slices = node.modifier_slices_snapshot();
1801    Some(node.with_active_children(|children| {
1802        let z_order = z_ordered_children(applier, children);
1803        read(SnapshotNodeData {
1804            layout_state,
1805            modifier_slices,
1806            children: z_order.as_deref().unwrap_or(children),
1807            window_root: false,
1808        })
1809    }))
1810}
1811
1812/// `children` in the order they draw and take taps, when a `z_index`
1813/// modifier moves one of them: by z index, and in composition order among
1814/// equal ones, as Compose's `zSortedChildren`. `None` while the composition
1815/// order already is that order, which it always is while no `z_index`
1816/// modifier exists.
1817fn z_ordered_children(applier: &MemoryApplier, children: &[NodeId]) -> Option<Vec<NodeId>> {
1818    if children.len() < 2 || !cranpose_ui::z_index_in_use() {
1819        return None;
1820    }
1821    let mut previous = f32::NEG_INFINITY;
1822    let in_order = children.iter().all(|&child| {
1823        let z_index = node_z_index(applier, child);
1824        let ordered = z_index >= previous;
1825        previous = z_index;
1826        ordered
1827    });
1828    if in_order {
1829        return None;
1830    }
1831    let mut ordered: Vec<(f32, NodeId)> = children
1832        .iter()
1833        .map(|&child| (node_z_index(applier, child), child))
1834        .collect();
1835    ordered.sort_by(|a, b| a.0.total_cmp(&b.0));
1836    Some(ordered.into_iter().map(|(_, child)| child).collect())
1837}
1838
1839fn node_z_index(applier: &MemoryApplier, node_id: NodeId) -> f32 {
1840    let Ok(node) = applier.get_ref(node_id) else {
1841        return 0.0;
1842    };
1843    let node: &dyn Any = node;
1844    if let Some(node) = node.downcast_ref::<LayoutNode>() {
1845        return node.modifier_slices_snapshot().z_index();
1846    }
1847    node.downcast_ref::<SubcomposeLayoutNode>()
1848        .map_or(0.0, |node| node.modifier_slices_snapshot().z_index())
1849}
1850
1851fn hit_test_from_slices(slices: &Rc<ModifierNodeSlices>) -> Option<HitTestNode> {
1852    slices_hit_something(slices).then(|| HitTestNode {
1853        handlers: Rc::clone(slices),
1854    })
1855}
1856
1857/// A node's layer bounds and, when they differ from it, its own rect (see
1858/// [`LayerNode::node_bounds`]).
1859fn layer_and_node_bounds(slices: &ModifierNodeSlices, size: Size) -> (Rect, Option<Rect>) {
1860    let node_bounds = Rect::from_size(size);
1861    let layer_bounds = slices.layer_bounds(size);
1862    (
1863        layer_bounds,
1864        (layer_bounds != node_bounds).then_some(node_bounds),
1865    )
1866}
1867
1868/// Whether a node's slices make it a hit target: a pointer input or a
1869/// pointer icon.
1870fn slices_hit_something(slices: &ModifierNodeSlices) -> bool {
1871    !slices.pointer_inputs().is_empty() || slices.pointer_icon().is_some()
1872}
1873
1874fn prepare_node_layer(
1875    node_id: NodeId,
1876    slices: &Rc<ModifierNodeSlices>,
1877    layout_state: &cranpose_ui::widgets::LayoutState,
1878    context: LowerContext,
1879) -> (LayerHead, LowerContext) {
1880    let size = layout_state.size();
1881    let placement = layout_state.position();
1882    let (local_bounds, node_bounds) = layer_and_node_bounds(slices, size);
1883    let graphics_layer = graphics_layer_with_shaped_clip(
1884        slices.graphics_layer(),
1885        slices.clip_to_bounds(),
1886        slices.corner_shape(),
1887        local_bounds,
1888    );
1889    let geometry = context.parent_abs.map(|parent_abs| {
1890        TranslateGeometry::new(
1891            layout_state,
1892            graphics_layer.as_ref().unwrap_or(&GraphicsLayer::DEFAULT),
1893            local_bounds,
1894            parent_abs,
1895        )
1896    });
1897    if let Some(geometry) = geometry {
1898        slices.publish_window_geometry(geometry.top_left, geometry.window_transform, size);
1899    } else {
1900        slices.publish_pointer_input_size(size);
1901    }
1902    let content_offset = layout_state.content_offset();
1903    let child_context = context.for_children(
1904        slices,
1905        content_offset,
1906        geometry.map(TranslateGeometry::child_abs),
1907    );
1908    let head = node_layer_head(
1909        node_id,
1910        slices,
1911        NodeFrame {
1912            local_bounds,
1913            node_bounds,
1914            placement,
1915            content_offset,
1916            translated_content_offset: slices.translated_content_offset().unwrap_or(content_offset),
1917        },
1918        graphics_layer,
1919        context,
1920    );
1921    (head, child_context)
1922}
1923
1924fn write_node_layer(
1925    applier: &MemoryApplier,
1926    node_id: NodeId,
1927    data: SnapshotNodeData<'_>,
1928    context: LowerContext,
1929    target: &mut LayerNode,
1930) {
1931    let SnapshotNodeData {
1932        layout_state,
1933        modifier_slices: slices,
1934        children,
1935        window_root: _,
1936    } = data;
1937    let size = layout_state.size();
1938    let placement = layout_state.position();
1939    if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
1940        eprintln!(
1941            "[scene-update-diag] build layer node={node_id:?} size=({:.2},{:.2}) pos=({:.2},{:.2})",
1942            size.width, size.height, placement.x, placement.y,
1943        );
1944    }
1945    let (head, child_context) = prepare_node_layer(node_id, &slices, &layout_state, context);
1946    let outer = outer_draws(
1947        node_id,
1948        slices.draw_commands(),
1949        slices.outer_draw_command_count(),
1950        size,
1951    );
1952    write_layer_with_outer(
1953        target,
1954        head,
1955        outer,
1956        placement,
1957        context.parent_content_offset,
1958        |list| {
1959            write_node_content(list, node_id, &slices, size, (0, children.len()), |list| {
1960                for &child_id in children {
1961                    if let Some(child) = lower_child(applier, child_id, child_context) {
1962                        list.push(RenderNode::Layer(child));
1963                    }
1964                }
1965            });
1966        },
1967    );
1968}
1969
1970pub(crate) struct RecorderSlot {
1971    generation: u64,
1972    handles: [Option<Rc<CommandRecording>>; 3],
1973    /// The allocation of a handle whose recording `acquire_storage` took,
1974    /// which the next recording the slot publishes moves into.
1975    spare: Option<Rc<CommandRecording>>,
1976}
1977
1978#[derive(Clone, Copy, PartialEq, Eq, Hash)]
1979pub(crate) struct RecorderKey {
1980    node_id: NodeId,
1981    command_index: u32,
1982}
1983
1984impl From<DrawCommandId> for RecorderKey {
1985    fn from(id: DrawCommandId) -> Self {
1986        Self {
1987            node_id: id.node_id,
1988            command_index: id.command_index,
1989        }
1990    }
1991}
1992
1993#[doc(hidden)]
1994pub fn clear_command_recordings_for_tests() {
1995    crate::thread_state::with(|state| state.command_recordings.borrow_mut().clear());
1996}
1997
1998fn bump_recording_generation() {
1999    crate::thread_state::with(|state| {
2000        let generation = state.recording_generation.get().wrapping_add(1);
2001        state.recording_generation.set(generation);
2002        if generation.is_multiple_of(512) {
2003            state
2004                .command_recordings
2005                .borrow_mut()
2006                .retain(|_, slot| generation.wrapping_sub(slot.generation) <= 64);
2007        }
2008    });
2009}
2010
2011fn acquire_storage(id: DrawCommandId) -> CommandRecording {
2012    crate::thread_state::with(|state| {
2013        let mut map = state.command_recordings.borrow_mut();
2014        let Some(slot) = map.get_mut(&RecorderKey::from(id)) else {
2015            return CommandRecording::default();
2016        };
2017        for handle in &mut slot.handles {
2018            let Some(recording) = handle.as_mut().and_then(Rc::get_mut) else {
2019                continue;
2020            };
2021            let Some(storage) = recording.try_take_reusable() else {
2022                continue;
2023            };
2024            slot.spare = handle.take();
2025            return storage;
2026        }
2027        if slot.handles.iter().all(Option::is_some) {
2028            let storage = slot.handles[2]
2029                .as_mut()
2030                .and_then(Rc::get_mut)
2031                .map(std::mem::take);
2032            if let Some(storage) = storage {
2033                slot.spare = slot.handles[2].take();
2034                return storage;
2035            }
2036        }
2037        CommandRecording::default()
2038    })
2039}
2040
2041fn publish_recording(id: DrawCommandId, recording: CommandRecording) -> Rc<CommandRecording> {
2042    crate::thread_state::with(|state| {
2043        let mut map = state.command_recordings.borrow_mut();
2044        let generation = state.recording_generation.get();
2045        let slot = map
2046            .entry(RecorderKey::from(id))
2047            .or_insert_with(|| RecorderSlot {
2048                generation,
2049                handles: [None, None, None],
2050                spare: None,
2051            });
2052        let shared = match slot.spare.take() {
2053            Some(mut spare) => match Rc::get_mut(&mut spare) {
2054                Some(storage) => {
2055                    *storage = recording;
2056                    spare
2057                }
2058                None => Rc::new(recording),
2059            },
2060            None => Rc::new(recording),
2061        };
2062        slot.generation = generation;
2063        slot.handles[2] = slot.handles[1].take();
2064        slot.handles[1] = slot.handles[0].take();
2065        slot.handles[0] = Some(Rc::clone(&shared));
2066        shared
2067    })
2068}
2069
2070enum DrawPass<'a> {
2071    Record(DrawPlacement),
2072    OverlayFrom(std::slice::Iter<'a, RenderNode>),
2073    OverlayFromOutput(usize),
2074}
2075
2076impl DrawPass<'_> {
2077    fn placement(&self) -> DrawPlacement {
2078        match self {
2079            Self::Record(placement) => *placement,
2080            Self::OverlayFrom(_) | Self::OverlayFromOutput(_) => DrawPlacement::Overlay,
2081        }
2082    }
2083
2084    fn take_recording(
2085        &mut self,
2086        command: &DrawCommand,
2087        output: &[RenderNode],
2088    ) -> Option<Rc<CommandRecording>> {
2089        if matches!(command, DrawCommand::Overlay(_)) {
2090            return None;
2091        }
2092        let node = match self {
2093            Self::Record(_) => return None,
2094            Self::OverlayFrom(nodes) => nodes.next().expect("each behind command has a draw run"),
2095            Self::OverlayFromOutput(index) => {
2096                let node = &output[*index];
2097                *index += 1;
2098                node
2099            }
2100        };
2101        let RenderNode::DrawRun(run) = node else {
2102            unreachable!("behind commands produce draw runs");
2103        };
2104        matches!(command, DrawCommand::WithContent(_)).then(|| Rc::clone(&run.recording))
2105    }
2106}
2107
2108fn layer_node_capacity(commands: &[DrawCommand], children: usize, has_text: bool) -> usize {
2109    children
2110        + usize::from(has_text)
2111        + commands.len()
2112        + commands
2113            .iter()
2114            .filter(|command| matches!(command, DrawCommand::WithContent(_)))
2115            .count()
2116}
2117
2118fn draw_nodes(
2119    node_id: NodeId,
2120    commands: &[DrawCommand],
2121    first_command_index: usize,
2122    placement: DrawPlacement,
2123    size: Size,
2124    phase: PrimitivePhase,
2125) -> Vec<RenderNode> {
2126    let mut nodes = crate::layer_recycling::child_list(commands.len());
2127    append_draw_nodes(
2128        &mut nodes,
2129        node_id,
2130        commands,
2131        first_command_index,
2132        DrawPass::Record(placement),
2133        size,
2134        phase,
2135    );
2136    nodes
2137}
2138
2139fn append_draw_nodes(
2140    nodes: &mut Vec<RenderNode>,
2141    node_id: NodeId,
2142    commands: &[DrawCommand],
2143    first_command_index: usize,
2144    mut pass: DrawPass<'_>,
2145    size: Size,
2146    phase: PrimitivePhase,
2147) {
2148    let placement = pass.placement();
2149    for (command_index, command) in commands.iter().enumerate() {
2150        let id = DrawCommandId {
2151            node_id,
2152            command_index: (first_command_index + command_index) as u32,
2153            placement,
2154        };
2155        let (shared, segments) = if let Some(shared) = pass.take_recording(command, nodes) {
2156            let segments = shared.content_split(false);
2157            (shared, segments)
2158        } else {
2159            let Some((recording, segments)) =
2160                recording_for_placement_reusing(command, placement, size, || acquire_storage(id))
2161            else {
2162                continue;
2163            };
2164            (publish_recording(id, recording), segments)
2165        };
2166        nodes.push(RenderNode::DrawRun(DrawRunNode::for_command_shared(
2167            phase,
2168            Some(id),
2169            shared,
2170            segments,
2171        )));
2172    }
2173}
2174
2175#[doc(hidden)]
2176pub fn draw_command_nodes_for_tests(
2177    node_id: NodeId,
2178    commands: &[DrawCommand],
2179    placement: DrawPlacement,
2180    size: Size,
2181    phase: PrimitivePhase,
2182) -> Vec<RenderNode> {
2183    bump_recording_generation();
2184    draw_nodes(node_id, commands, 0, placement, size, phase)
2185}
2186
2187struct OuterDraws {
2188    behind: Vec<RenderNode>,
2189    overlay: Vec<RenderNode>,
2190}
2191
2192fn outer_draws(
2193    node_id: NodeId,
2194    draw_commands: &[DrawCommand],
2195    outer_draw_command_count: usize,
2196    size: Size,
2197) -> Option<OuterDraws> {
2198    (outer_draw_command_count > 0).then(|| {
2199        let commands = &draw_commands[..outer_draw_command_count];
2200        let behind = draw_nodes(
2201            node_id,
2202            commands,
2203            0,
2204            DrawPlacement::Behind,
2205            size,
2206            PrimitivePhase::BeforeChildren,
2207        );
2208        let mut overlay = crate::layer_recycling::child_list(commands.len());
2209        append_draw_nodes(
2210            &mut overlay,
2211            node_id,
2212            commands,
2213            0,
2214            DrawPass::OverlayFrom(behind.iter()),
2215            size,
2216            PrimitivePhase::AfterChildren,
2217        );
2218        OuterDraws { behind, overlay }
2219    })
2220}
2221
2222fn write_wrapper(
2223    wrapper: &mut LayerNode,
2224    mut layer: Box<LayerNode>,
2225    placement: Point,
2226    outer: OuterDraws,
2227    parent_content_offset: Point,
2228) {
2229    debug_assert!(wrapper.children.is_empty(), "a wrapper is written emptied");
2230    layer.transform_to_parent =
2231        layer_transform_to_parent(layer.local_bounds, Point::default(), &layer.graphics_layer);
2232    layer.origin_in_parent = Point::default();
2233    let node_rect = layer.node_rect();
2234    let graphics_layer = None;
2235    let head = LayerHead {
2236        node_id: None,
2237        wraps: layer.node_id,
2238        local_bounds: node_rect,
2239        node_bounds: None,
2240        transform_to_parent: placed_transform(
2241            node_rect,
2242            placement,
2243            &GraphicsLayer::DEFAULT,
2244            parent_content_offset,
2245        ),
2246        content_offset: Point::default(),
2247        motion_context_animated: layer.motion_context_animated,
2248        translated_content_context: false,
2249        translated_content_offset: Point::default(),
2250        origin_in_parent: placement,
2251        graphics_layer,
2252        clip_to_bounds: false,
2253        hit_test: None,
2254        has_origin_sinks: false,
2255        isolation: IsolationReasons::default(),
2256        cache_policy: CachePolicy::None,
2257    };
2258    let OuterDraws {
2259        mut behind,
2260        mut overlay,
2261    } = outer;
2262    let children = &mut wrapper.children;
2263    crate::layer_recycling::reserve_nodes(children, behind.len() + 1 + overlay.len());
2264    children.append(&mut behind);
2265    children.push(RenderNode::Layer(layer));
2266    children.append(&mut overlay);
2267    crate::layer_recycling::recycle_list(behind);
2268    crate::layer_recycling::recycle_list(overlay);
2269    assign_layer(wrapper, head);
2270}
2271
2272fn take_wrapped_layer(wrapper: &mut LayerNode, node_id: NodeId) -> Option<Box<LayerNode>> {
2273    let mut wrapped = None;
2274    for child in wrapper.children.drain(..) {
2275        if let RenderNode::Layer(layer) = child
2276            && layer.node_id == Some(node_id)
2277        {
2278            wrapped = Some(layer);
2279        }
2280    }
2281    wrapped
2282}
2283
2284fn layer_identity(layer: &LayerNode) -> Option<NodeId> {
2285    layer.node_id.or(layer.wraps)
2286}
2287
2288struct TextNodeParts<'a> {
2289    node_id: NodeId,
2290    /// Where the text node was placed in its layout node.
2291    text_rect: Rect,
2292    text_style: Option<&'a TextStyle>,
2293    text_layout_options: Option<TextLayoutOptions>,
2294    text_pan: Option<TextPanResolver>,
2295    measured_layout: Option<Rc<PreparedTextLayout>>,
2296}
2297
2298fn text_node_from_parts(parts: TextNodeParts<'_>) -> Option<TextPrimitiveNode> {
2299    let TextNodeParts {
2300        node_id,
2301        text_rect,
2302        text_style,
2303        text_layout_options,
2304        text_pan,
2305        measured_layout,
2306    } = parts;
2307    let prepared = measured_layout?;
2308    let default_text_style;
2309    let text_style = match text_style {
2310        Some(style) => style,
2311        None => {
2312            default_text_style = TextStyle::default();
2313            &default_text_style
2314        }
2315    };
2316    let options = text_layout_options.unwrap_or_default().normalized();
2317    let content_width = text_rect.width.max(0.0);
2318    if content_width <= 0.0 {
2319        return None;
2320    }
2321
2322    let pan_offset = text_pan
2323        .as_ref()
2324        .map_or(0.0, |resolve| resolve(content_width));
2325    let pans_horizontally = text_pan.is_some();
2326
2327    let visual_style = &prepared.visual_style;
2328    let measured_draw_width = prepared.metrics.width.max(0.0);
2329    let draw_width = if options.overflow == TextOverflow::Visible || pans_horizontally {
2330        measured_draw_width
2331    } else {
2332        measured_draw_width.min(content_width)
2333    };
2334    let alignment_offset = resolve_text_horizontal_offset(
2335        text_style,
2336        prepared.text.text.as_str(),
2337        content_width,
2338        prepared.metrics.width,
2339    );
2340    let rect = Rect {
2341        x: text_rect.x + alignment_offset - pan_offset,
2342        y: text_rect.y,
2343        width: draw_width,
2344        height: prepared.metrics.height,
2345    };
2346    let text_bounds = Rect {
2347        width: content_width,
2348        height: text_rect.height.max(0.0),
2349        ..text_rect
2350    };
2351    let font_size = visual_style.resolve_font_size(14.0);
2352    let expanded_bounds =
2353        expand_text_bounds_for_baseline_shift(text_bounds, visual_style, font_size);
2354    let clip = if options.overflow == TextOverflow::Visible && !pans_horizontally {
2355        None
2356    } else {
2357        Some(pad_clip_rect(expanded_bounds))
2358    };
2359
2360    Some(TextPrimitiveNode {
2361        node_id,
2362        rect,
2363        text: Rc::clone(&prepared.text),
2364        render_text: prepared.render_text(),
2365        text_style: std::sync::Arc::clone(visual_style),
2366        font_size,
2367        layout_options: options,
2368        clip,
2369        paint: TextPaintCache::holding(TextPaint::of(
2370            visual_style,
2371            prepared.visual_style_hash(),
2372            &prepared.text.span_styles,
2373            font_size,
2374            (prepared.render_text_hash(), prepared.text.text.is_empty()),
2375        )),
2376    })
2377}
2378
2379fn layout_box_to_snapshot(node: &LayoutBox, parent: Option<&LayoutBox>) -> BuildNodeSnapshot {
2380    let placement = parent
2381        .map(|parent_box| Point {
2382            x: node.rect.x - parent_box.rect.x - parent_box.content_offset.x,
2383            y: node.rect.y - parent_box.rect.y - parent_box.content_offset.y,
2384        })
2385        .unwrap_or_default();
2386    let mut children = Vec::with_capacity(node.children.len());
2387    for child in &node.children {
2388        children.push(layout_box_to_snapshot(child, Some(node)));
2389    }
2390    if cranpose_ui::z_index_in_use() {
2391        // Stable: equal z indices keep the composition order.
2392        children.sort_by(|a, b| a.slices.z_index().total_cmp(&b.slices.z_index()));
2393    }
2394    let base_graphics_layer = node.node_data.modifier_slices.graphics_layer();
2395    let has_graphics_layer = base_graphics_layer.is_some();
2396    let graphics_layer = graphics_layer_with_shaped_clip(
2397        base_graphics_layer,
2398        node.node_data.modifier_slices.clip_to_bounds(),
2399        node.node_data.modifier_slices.corner_shape(),
2400        Rect {
2401            x: 0.0,
2402            y: 0.0,
2403            width: node.rect.width,
2404            height: node.rect.height,
2405        },
2406    );
2407    let has_graphics_layer = has_graphics_layer
2408        || graphics_layer
2409            .as_ref()
2410            .is_some_and(|layer| layer.render_effect.is_some());
2411
2412    BuildNodeSnapshot {
2413        node_id: node.node_id,
2414        placement,
2415        size: Size {
2416            width: node.rect.width,
2417            height: node.rect.height,
2418        },
2419        content_offset: node.content_offset,
2420        slices: Rc::clone(&node.node_data.modifier_slices),
2421        graphics_layer: graphics_layer.filter(|_| has_graphics_layer),
2422        children,
2423    }
2424}
2425
2426fn modifier_slices_have_origin_sinks(slices: &ModifierNodeSlices) -> bool {
2427    slices.text_window().is_some() || slices.viewport_window_rect().is_some()
2428}
2429
2430fn graphics_layer_with_shaped_clip(
2431    mut graphics_layer: Option<GraphicsLayer>,
2432    clip_to_bounds: bool,
2433    corner_shape: Option<RoundedCornerShape>,
2434    local_bounds: Rect,
2435) -> Option<GraphicsLayer> {
2436    if !clip_to_bounds {
2437        return graphics_layer;
2438    }
2439
2440    let Some(corner_shape) = corner_shape else {
2441        return graphics_layer;
2442    };
2443    let radii = corner_shape.resolve(local_bounds.width, local_bounds.height);
2444    if radii.top_left <= f32::EPSILON
2445        && radii.top_right <= f32::EPSILON
2446        && radii.bottom_right <= f32::EPSILON
2447        && radii.bottom_left <= f32::EPSILON
2448    {
2449        return graphics_layer;
2450    }
2451
2452    let properties = graphics_layer.get_or_insert_default();
2453    if let Some(existing) = properties.render_effect.take() {
2454        let rounded_clip = rounded_corner_alpha_mask_effect(
2455            local_bounds.width,
2456            local_bounds.height,
2457            radii,
2458            ROUNDED_CLIP_EDGE_FEATHER,
2459        );
2460        properties.render_effect = Some(existing.then(rounded_clip));
2461    } else {
2462        properties.shape = LayerShape::Rounded(corner_shape);
2463        properties.clip = true;
2464    }
2465    graphics_layer
2466}
2467
2468fn layer_cache_policy(layer: &GraphicsLayer, isolation: IsolationReasons) -> CachePolicy {
2469    if isolation.has_any() || layer_scales_or_rotates(layer) {
2470        CachePolicy::Auto
2471    } else {
2472        CachePolicy::None
2473    }
2474}
2475
2476fn isolation_reasons(layer: &GraphicsLayer) -> IsolationReasons {
2477    IsolationReasons {
2478        explicit_offscreen: layer.compositing_strategy == CompositingStrategy::Offscreen,
2479        shape_clip: layer.clip && !matches!(layer.shape, LayerShape::Rectangle),
2480        effect: layer.render_effect.is_some(),
2481        backdrop: layer.backdrop_effect.is_some(),
2482        group_opacity: layer.compositing_strategy != CompositingStrategy::ModulateAlpha
2483            && layer.alpha < 1.0,
2484        blend_mode: layer.blend_mode != cranpose_ui::BlendMode::SrcOver,
2485    }
2486}
2487
2488fn pad_clip_rect(rect: Rect) -> Rect {
2489    Rect {
2490        x: rect.x - TEXT_CLIP_PAD,
2491        y: rect.y - TEXT_CLIP_PAD,
2492        width: (rect.width + TEXT_CLIP_PAD * 2.0).max(0.0),
2493        height: (rect.height + TEXT_CLIP_PAD * 2.0).max(0.0),
2494    }
2495}
2496
2497/// Grows `text_bounds` in the direction of the style's baseline shift, by the shift in pixels at
2498/// `font_size`, so a clip does not cut the shifted glyphs. Returns the bounds unchanged when
2499/// there is no baseline shift.
2500pub fn expand_text_bounds_for_baseline_shift(
2501    text_bounds: Rect,
2502    text_style: &TextStyle,
2503    font_size: f32,
2504) -> Rect {
2505    let baseline_shift_px = text_style
2506        .span_style
2507        .baseline_shift
2508        .filter(|shift| shift.is_specified())
2509        .map_or(0.0, |shift| -(shift.0 * font_size));
2510    if baseline_shift_px == 0.0 {
2511        return text_bounds;
2512    }
2513
2514    if baseline_shift_px < 0.0 {
2515        Rect {
2516            x: text_bounds.x,
2517            y: text_bounds.y + baseline_shift_px,
2518            width: text_bounds.width,
2519            height: (text_bounds.height - baseline_shift_px).max(0.0),
2520        }
2521    } else {
2522        Rect {
2523            x: text_bounds.x,
2524            y: text_bounds.y,
2525            width: text_bounds.width,
2526            height: (text_bounds.height + baseline_shift_px).max(0.0),
2527        }
2528    }
2529}
2530
2531fn resolve_text_horizontal_offset(
2532    text_style: &TextStyle,
2533    text: &str,
2534    content_width: f32,
2535    measured_width: f32,
2536) -> f32 {
2537    let remaining = (content_width - measured_width).max(0.0);
2538    if remaining == 0.0 {
2539        return 0.0;
2540    }
2541    remaining * cranpose_ui::text::text_align_fraction(text_style, text)
2542}
2543
2544#[cfg(test)]
2545#[path = "tests/scene_builder_tests.rs"]
2546mod tests;