Skip to main content

cranpose_render_common/
scene_builder.rs

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