Skip to main content

cranpose_render_common/
scene_builder.rs

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