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