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