Skip to main content

cranpose_render_common/
scene_builder.rs

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