Skip to main content

cranpose_render_common/
scene_builder.rs

1use std::{cell::Cell, rc::Rc};
2
3use cranpose_core::{MemoryApplier, Node, NodeId, collections::map::HashSet};
4use cranpose_ui::{
5    DrawCommand, LayoutBox, LayoutNode, ModifierNodeSlices, Point, PreparedTextLayout, Rect,
6    ResolvedModifiers, Size, SubcomposeLayoutNode, TextLayoutOptions, TextOverflow,
7    TextPanResolver, text::TextStyle,
8};
9use cranpose_ui_graphics::{
10    CommandRecording, CompositingStrategy, GraphicsLayer, LayerShape, PointerIcon,
11    RoundedCornerShape, rounded_corner_alpha_mask_effect,
12};
13use smallvec::SmallVec;
14
15use crate::{
16    graph::{
17        CachePolicy, DrawCommandId, DrawRunNode, HitTestNode, IsolationReasons, LayerNode,
18        PrimitiveEntry, PrimitiveNode, PrimitivePhase, ProjectiveTransform, RenderGraph,
19        RenderNode, TextPrimitiveNode,
20    },
21    layer_transform::{layer_scales_or_rotates, layer_transform_to_parent},
22    raster_cache::LayerRasterCacheHashes,
23    style_shared::{DrawPlacement, recording_for_placement_reusing},
24};
25
26const TEXT_CLIP_PAD: f32 = 1.0;
27const ROUNDED_CLIP_EDGE_FEATHER: f32 = 1.0;
28
29#[derive(Clone, Default)]
30struct BuildNodeSnapshot {
31    node_id: NodeId,
32    placement: Point,
33    size: Size,
34    content_offset: Point,
35    motion_context_animated: bool,
36    translated_content_context: bool,
37    has_own_origin_sinks: bool,
38    measured_text_layout: Option<Rc<PreparedTextLayout>>,
39    resolved_modifiers: ResolvedModifiers,
40    draw_commands: Vec<DrawCommand>,
41    outer_draw_command_count: usize,
42    click_actions: Vec<Rc<dyn Fn(Point)>>,
43    pointer_inputs: Vec<Rc<dyn Fn(cranpose_foundation::PointerEvent)>>,
44    pointer_icon: Option<PointerIcon>,
45    clip_to_bounds: bool,
46    text_style: Option<TextStyle>,
47    text_layout_options: Option<TextLayoutOptions>,
48    text_pan: Option<TextPanResolver>,
49    graphics_layer: Option<GraphicsLayer>,
50    children: Vec<Self>,
51}
52
53struct SnapshotNodeData {
54    layout_state: cranpose_ui::widgets::LayoutState,
55    modifier_slices: Rc<ModifierNodeSlices>,
56    resolved_modifiers: ResolvedModifiers,
57    children: SmallVec<[NodeId; 8]>,
58    window_root: bool,
59}
60
61/// Why a scoped scene update could not be applied, forcing the caller to throw
62/// the render graph away and build it again from the applier.
63///
64/// The reason has to travel with the outcome because the shell picks the
65/// scoped path from the shape of the dirty set, before this code runs, and
66/// logs that choice. A frame that chose the scoped path and then rebuilt the
67/// whole scene is the expensive case, and in the log it reads exactly like a
68/// cheap patch -- so the fallback says so itself.
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum GraphRebuildReason {
71    /// The root was dirty and its replacement layer could not be built.
72    RootLayerUnavailable,
73    /// A dirty subtree's replacement layer could not be built.
74    DirtyLayerUnavailable,
75    /// This many dirty nodes own no layer in the render graph, so the scoped
76    /// walk never reached them. A node enters the graph only as a `LayerNode`;
77    /// a dirty node that never produced one -- or whose layer left the graph
78    /// this frame -- cannot be patched in place.
79    UnmatchedDirtyNodes(usize),
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum GraphUpdate {
84    Patched,
85    NeedsRebuild(GraphRebuildReason),
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub struct GraphUpdateReport {
90    pub update: GraphUpdate,
91    pub hit_graph_dirty: bool,
92}
93
94impl GraphUpdateReport {
95    pub fn applied(self) -> bool {
96        matches!(self.update, GraphUpdate::Patched)
97    }
98
99    pub fn rebuild_reason(self) -> Option<GraphRebuildReason> {
100        match self.update {
101            GraphUpdate::Patched => None,
102            GraphUpdate::NeedsRebuild(reason) => Some(reason),
103        }
104    }
105}
106
107#[cfg(test)]
108thread_local! {
109    static LOWERED_LAYER_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
110}
111
112fn note_layer_lowered() {
113    #[cfg(test)]
114    LOWERED_LAYER_COUNT.with(|count| count.set(count.get() + 1));
115}
116
117#[cfg(test)]
118fn reset_lowered_layer_count() {
119    LOWERED_LAYER_COUNT.with(|count| count.set(0));
120}
121
122#[cfg(test)]
123fn lowered_layer_count() -> usize {
124    LOWERED_LAYER_COUNT.with(Cell::get)
125}
126
127pub fn build_graph_from_layout_tree(root: &LayoutBox, scale: f32) -> RenderGraph {
128    bump_recording_generation();
129    let root_snapshot = layout_box_to_snapshot(root, None);
130    RenderGraph {
131        root: build_layer_node(root_snapshot, scale, false),
132    }
133}
134
135pub fn build_graph_from_applier(
136    applier: &mut MemoryApplier,
137    root: NodeId,
138    scale: f32,
139) -> Option<RenderGraph> {
140    bump_recording_generation();
141    Some(RenderGraph {
142        root: build_layer_node_from_applier(applier, root, scale, false)?,
143    })
144}
145
146pub fn update_graph_from_applier(
147    applier: &mut MemoryApplier,
148    graph: &mut RenderGraph,
149    dirty_nodes: &[NodeId],
150    scale: f32,
151) -> bool {
152    update_graph_from_applier_report(applier, graph, dirty_nodes, scale).applied()
153}
154
155pub fn update_graph_from_applier_report(
156    applier: &mut MemoryApplier,
157    graph: &mut RenderGraph,
158    dirty_nodes: &[NodeId],
159    scale: f32,
160) -> GraphUpdateReport {
161    let mut changed_nodes = Vec::new();
162    update_graph_from_applier_report_into(applier, graph, dirty_nodes, scale, &mut changed_nodes)
163}
164
165pub fn update_graph_from_applier_report_into(
166    applier: &mut MemoryApplier,
167    graph: &mut RenderGraph,
168    dirty_nodes: &[NodeId],
169    scale: f32,
170    changed_nodes: &mut Vec<NodeId>,
171) -> GraphUpdateReport {
172    let report = update_graph_from_applier_report_into_inner(
173        applier,
174        graph,
175        dirty_nodes,
176        scale,
177        changed_nodes,
178    );
179    if let GraphUpdate::NeedsRebuild(reason) = report.update
180        && cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG")
181    {
182        eprintln!(
183            "[scene-update-diag] scoped update abandoned, whole scene rebuilt: {reason:?} dirty={}",
184            dirty_nodes.len()
185        );
186    }
187    report
188}
189
190fn update_graph_from_applier_report_into_inner(
191    applier: &mut MemoryApplier,
192    graph: &mut RenderGraph,
193    dirty_nodes: &[NodeId],
194    scale: f32,
195    changed_nodes: &mut Vec<NodeId>,
196) -> GraphUpdateReport {
197    if dirty_nodes.is_empty() {
198        return GraphUpdateReport {
199            update: GraphUpdate::Patched,
200            hit_graph_dirty: false,
201        };
202    }
203    bump_recording_generation();
204
205    if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
206        eprintln!("[scene-update-diag] dirty={dirty_nodes:?}");
207    }
208
209    let mut remaining_dirty_nodes = dirty_nodes.iter().copied().collect::<HashSet<_>>();
210    if let Some(root_id) = layer_identity(&graph.root)
211        && remaining_dirty_nodes.contains(&root_id)
212    {
213        remaining_dirty_nodes.remove(&root_id);
214        if try_translate_scrolled_layer(
215            applier,
216            &mut graph.root,
217            &mut remaining_dirty_nodes,
218            changed_nodes,
219            TranslateAncestorContext {
220                inherited_motion_context_animated: false,
221                ancestor_hashed: false,
222                inherited_translated_content_context: false,
223                parent_content_offset: Point::default(),
224                parent_abs: AbsOrigin::ROOT,
225            },
226        ) {
227            if remaining_dirty_nodes.is_empty() {
228                return GraphUpdateReport {
229                    update: GraphUpdate::Patched,
230                    hit_graph_dirty: true,
231                };
232            }
233            let inherited = graph.root.translated_content_context;
234            let walked = replace_dirty_layers_from_applier(
235                applier,
236                &mut graph.root,
237                &mut remaining_dirty_nodes,
238                inherited,
239                false,
240                changed_nodes,
241            );
242            return GraphUpdateReport {
243                update: classify_walk(walked.is_some(), &remaining_dirty_nodes),
244                hit_graph_dirty: true,
245            };
246        }
247        let Some(root) = build_layer_node_from_applier(applier, root_id, scale, false) else {
248            return GraphUpdateReport {
249                update: GraphUpdate::NeedsRebuild(GraphRebuildReason::RootLayerUnavailable),
250                hit_graph_dirty: true,
251            };
252        };
253        let hit_graph_dirty = layer_hit_graph_state_dirty(&graph.root, &root);
254        collect_layer_node_ids(&graph.root, changed_nodes);
255        graph.root = root;
256        graph.root.recompute_raster_cache_hashes();
257        collect_layer_node_ids(&graph.root, changed_nodes);
258        return GraphUpdateReport {
259            update: GraphUpdate::Patched,
260            hit_graph_dirty,
261        };
262    }
263
264    let inherited_translated_content_context = graph.root.translated_content_context;
265    let Some(report) = replace_dirty_layers_from_applier(
266        applier,
267        &mut graph.root,
268        &mut remaining_dirty_nodes,
269        inherited_translated_content_context,
270        false,
271        changed_nodes,
272    ) else {
273        return GraphUpdateReport {
274            update: GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable),
275            hit_graph_dirty: true,
276        };
277    };
278
279    match classify_walk(true, &remaining_dirty_nodes) {
280        GraphUpdate::Patched => GraphUpdateReport {
281            update: GraphUpdate::Patched,
282            hit_graph_dirty: report.hit_graph_dirty,
283        },
284        update => GraphUpdateReport {
285            update,
286            hit_graph_dirty: true,
287        },
288    }
289}
290
291fn classify_walk(walked: bool, remaining_dirty_nodes: &HashSet<NodeId>) -> GraphUpdate {
292    if !walked {
293        GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable)
294    } else if !remaining_dirty_nodes.is_empty() {
295        GraphUpdate::NeedsRebuild(GraphRebuildReason::UnmatchedDirtyNodes(
296            remaining_dirty_nodes.len(),
297        ))
298    } else {
299        GraphUpdate::Patched
300    }
301}
302
303#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
304struct ReplaceDirtyLayersReport {
305    updated: bool,
306    hit_graph_dirty: bool,
307}
308
309fn replace_dirty_layers_from_applier(
310    applier: &mut MemoryApplier,
311    parent: &mut LayerNode,
312    dirty_nodes: &mut HashSet<NodeId>,
313    inherited_translated_content_context: bool,
314    ancestor_hashed: bool,
315    changed_nodes: &mut Vec<NodeId>,
316) -> Option<ReplaceDirtyLayersReport> {
317    if dirty_nodes.is_empty() {
318        return Some(ReplaceDirtyLayersReport::default());
319    }
320
321    let child_inherited_translated_content_context =
322        inherited_translated_content_context || parent.translated_content_context;
323    let child_ancestor_hashed =
324        crate::graph_hash::layer_children_ancestor_hashed(parent, ancestor_hashed);
325    let mut report = ReplaceDirtyLayersReport::default();
326
327    for child in &mut parent.children {
328        let RenderNode::Layer(child_layer) = child else {
329            continue;
330        };
331
332        if layer_identity(child_layer).is_some_and(|node_id| dirty_nodes.remove(&node_id)) {
333            if try_translate_scrolled_layer(
334                applier,
335                child_layer,
336                dirty_nodes,
337                changed_nodes,
338                TranslateAncestorContext {
339                    inherited_motion_context_animated: parent.motion_context_animated,
340                    ancestor_hashed: child_ancestor_hashed,
341                    inherited_translated_content_context:
342                        child_inherited_translated_content_context,
343                    parent_content_offset: parent.content_offset,
344                    parent_abs: AbsOrigin {
345                        content_origin: parent.scene_children_origin,
346                        layer_translation: parent.scene_children_layer_translation,
347                    },
348                },
349            ) {
350                report.hit_graph_dirty = true;
351                report.updated = true;
352                let child_report = replace_dirty_layers_from_applier(
353                    applier,
354                    child_layer,
355                    dirty_nodes,
356                    child_inherited_translated_content_context,
357                    child_ancestor_hashed,
358                    changed_nodes,
359                )?;
360                report.hit_graph_dirty |= child_report.hit_graph_dirty;
361                continue;
362            }
363            let mut replacement = build_layer_node_from_applier_internal(
364                applier,
365                layer_identity(child_layer).expect("dirty layer must have a node id"),
366                parent.motion_context_animated,
367                child_inherited_translated_content_context,
368                Some(AbsOrigin {
369                    content_origin: parent.scene_children_origin,
370                    layer_translation: parent.scene_children_layer_translation,
371                }),
372            )?;
373            if parent.content_offset != Point::default() {
374                replacement.transform_to_parent =
375                    replacement
376                        .transform_to_parent
377                        .then(ProjectiveTransform::translation(
378                            parent.content_offset.x,
379                            parent.content_offset.y,
380                        ));
381            }
382            report.hit_graph_dirty |= layer_hit_graph_state_dirty(child_layer, &replacement);
383            remove_dirty_descendants(&replacement, dirty_nodes);
384            collect_layer_node_ids(child_layer, changed_nodes);
385            **child_layer = replacement;
386            collect_layer_node_ids(child_layer, changed_nodes);
387            crate::graph_hash::recompute_layer_raster_cache_hashes_under(
388                child_layer,
389                child_ancestor_hashed,
390            );
391            report.updated = true;
392            continue;
393        }
394
395        let child_report = replace_dirty_layers_from_applier(
396            applier,
397            child_layer,
398            dirty_nodes,
399            child_inherited_translated_content_context,
400            child_ancestor_hashed,
401            changed_nodes,
402        )?;
403        report.updated |= child_report.updated;
404        report.hit_graph_dirty |= child_report.hit_graph_dirty;
405    }
406
407    if report.updated {
408        parent.has_hit_targets = parent.hit_test.is_some()
409            || parent.children.iter().any(|child| match child {
410                RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
411                RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
412            });
413        crate::graph_hash::refresh_layer_own_raster_cache_hashes(parent, ancestor_hashed);
414        if let Some(node_id) = parent.node_id {
415            changed_nodes.push(node_id);
416        }
417    }
418
419    Some(report)
420}
421
422fn translate_bail(reason: &str) -> bool {
423    if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
424        eprintln!("[scene-update-diag] translate bail: {reason}");
425    }
426    false
427}
428
429#[derive(Clone, Copy)]
430struct TranslateAncestorContext {
431    inherited_motion_context_animated: bool,
432    ancestor_hashed: bool,
433    inherited_translated_content_context: bool,
434    parent_content_offset: Point,
435    parent_abs: AbsOrigin,
436}
437
438fn try_translate_scrolled_layer(
439    applier: &mut MemoryApplier,
440    container: &mut LayerNode,
441    dirty_nodes: &mut HashSet<NodeId>,
442    changed_nodes: &mut Vec<NodeId>,
443    ancestors: TranslateAncestorContext,
444) -> bool {
445    let Some(node_id) = layer_identity(container) else {
446        return translate_bail("no node id");
447    };
448    let Some(data) = snapshot_node_data(applier, node_id) else {
449        return translate_bail("container snapshot read failed");
450    };
451    if container.wraps.is_none() {
452        return translate_layer_from_data(
453            applier,
454            container,
455            dirty_nodes,
456            changed_nodes,
457            ancestors,
458            data,
459            false,
460        );
461    }
462    let outer_count = data.modifier_slices.outer_draw_command_count();
463    if outer_count == 0 {
464        return translate_bail("outer draws removed");
465    }
466    let size = data.layout_state.size();
467    let placement = data.layout_state.position();
468    let slices = Rc::clone(&data.modifier_slices);
469    let inner_ancestors = TranslateAncestorContext {
470        ancestor_hashed: crate::graph_hash::layer_children_ancestor_hashed(
471            container,
472            ancestors.ancestor_hashed,
473        ),
474        ..ancestors
475    };
476    let Some(inner) = container.children.iter_mut().find_map(|child| match child {
477        RenderNode::Layer(layer) if layer.node_id == Some(node_id) => Some(layer),
478        _ => None,
479    }) else {
480        return translate_bail("wrapped layer missing");
481    };
482    if !translate_layer_from_data(
483        applier,
484        inner,
485        dirty_nodes,
486        changed_nodes,
487        inner_ancestors,
488        data,
489        true,
490    ) {
491        return false;
492    }
493    let layer = std::mem::take(inner.as_mut());
494    let outer = outer_draws(node_id, slices.draw_commands(), outer_count, size)
495        .expect("outer command count is nonzero");
496    *container = wrap_layer_with_outer_draws(layer, placement, outer);
497    if ancestors.parent_content_offset != Point::default() {
498        container.transform_to_parent =
499            container
500                .transform_to_parent
501                .then(ProjectiveTransform::translation(
502                    ancestors.parent_content_offset.x,
503                    ancestors.parent_content_offset.y,
504                ));
505    }
506    for child in &mut container.children {
507        if let RenderNode::Layer(layer) = child {
508            crate::graph_hash::refresh_layer_own_raster_cache_hashes(
509                layer,
510                inner_ancestors.ancestor_hashed,
511            );
512        }
513    }
514    crate::graph_hash::refresh_layer_own_raster_cache_hashes(container, ancestors.ancestor_hashed);
515    true
516}
517
518struct TranslatedContainer {
519    node_id: NodeId,
520    clip_to_bounds: bool,
521    graphics_layer: GraphicsLayer,
522}
523
524fn translated_container(
525    container: &LayerNode,
526    layout_state: &cranpose_ui::widgets::LayoutState,
527    modifier_slices: &ModifierNodeSlices,
528    inherited_motion_context_animated: bool,
529    wrapped: bool,
530) -> Result<TranslatedContainer, &'static str> {
531    if cranpose_core::env_flag!("CRANPOSE_DISABLE_SCROLL_TRANSLATE") {
532        return Err("fast path disabled by ablation switch");
533    }
534    let Some(node_id) = container.node_id else {
535        return Err("no node id");
536    };
537    if container
538        .children
539        .iter()
540        .any(|child| !matches!(child, RenderNode::Layer(_)))
541    {
542        return Err("container has own primitive children");
543    }
544    if !layout_state.is_placed()
545        || layout_state.size().width != container.local_bounds.width
546        || layout_state.size().height != container.local_bounds.height
547    {
548        return Err("container unplaced or resized");
549    }
550    let outer_count = modifier_slices.outer_draw_command_count();
551    if (outer_count > 0 && !wrapped)
552        || !modifier_slices.draw_commands()[outer_count..].is_empty()
553        || (inherited_motion_context_animated || modifier_slices.motion_context_animated())
554            != container.motion_context_animated
555        || modifier_slices.annotated_text().is_some()
556        || modifier_slices.translated_content_context() != container.translated_content_context
557    {
558        return Err("container draw/text/translated-context changed");
559    }
560    let clip_to_bounds = modifier_slices.clip_to_bounds();
561    if clip_to_bounds != container.clip_to_bounds {
562        return Err("container clip changed");
563    }
564    let graphics_layer = graphics_layer_with_shaped_clip(
565        modifier_slices.graphics_layer().unwrap_or_default(),
566        clip_to_bounds,
567        modifier_slices.corner_shape(),
568        container.local_bounds,
569    );
570    if graphics_layer != container.graphics_layer {
571        return Err("container graphics layer changed");
572    }
573    Ok(TranslatedContainer {
574        node_id,
575        clip_to_bounds,
576        graphics_layer,
577    })
578}
579
580struct TranslatedChildren {
581    placed_fresh: SmallVec<[(NodeId, cranpose_ui::widgets::LayoutState); 8]>,
582    children_unchanged: bool,
583    old_index_by_id: std::collections::HashMap<NodeId, usize>,
584}
585
586fn translated_children(
587    applier: &mut MemoryApplier,
588    container: &LayerNode,
589    dirty_nodes: &HashSet<NodeId>,
590    fresh_children: &[NodeId],
591) -> Result<TranslatedChildren, &'static str> {
592    let mut placed_fresh = SmallVec::<[_; 8]>::with_capacity(fresh_children.len());
593    for child_id in fresh_children {
594        let state = applier
595            .with_node::<LayoutNode, _>(*child_id, |node| node.layout_state())
596            .or_else(|_| {
597                applier.with_node::<SubcomposeLayoutNode, _>(*child_id, |node| node.layout_state())
598            });
599        let Ok(state) = state else {
600            continue;
601        };
602        if !state.is_placed() {
603            continue;
604        }
605        placed_fresh.push((*child_id, state));
606    }
607    let children_unchanged = container.children.len() == placed_fresh.len()
608        && container
609            .children
610            .iter()
611            .zip(&placed_fresh)
612            .all(|(child, (id, _))| {
613                matches!(child, RenderNode::Layer(layer) if layer_identity(layer) == Some(*id))
614            });
615    let old_index_by_id = if children_unchanged {
616        std::collections::HashMap::new()
617    } else {
618        let Some(index): Option<std::collections::HashMap<NodeId, usize>> = container
619            .children
620            .iter()
621            .enumerate()
622            .map(|(index, child)| match child {
623                RenderNode::Layer(layer) => layer_identity(layer).map(|id| (id, index)),
624                _ => None,
625            })
626            .collect()
627        else {
628            return Err("child without node id");
629        };
630        index
631    };
632    check_retained_children(
633        container,
634        dirty_nodes,
635        &placed_fresh,
636        children_unchanged,
637        &old_index_by_id,
638    )?;
639    Ok(TranslatedChildren {
640        placed_fresh,
641        children_unchanged,
642        old_index_by_id,
643    })
644}
645
646fn check_retained_children(
647    container: &LayerNode,
648    dirty_nodes: &HashSet<NodeId>,
649    placed_fresh: &[(NodeId, cranpose_ui::widgets::LayoutState)],
650    children_unchanged: bool,
651    old_index_by_id: &std::collections::HashMap<NodeId, usize>,
652) -> Result<(), &'static str> {
653    for (fresh_index, (child_id, state)) in placed_fresh.iter().enumerate() {
654        let old_index = if children_unchanged {
655            fresh_index
656        } else if let Some(index) = old_index_by_id.get(child_id) {
657            *index
658        } else {
659            continue;
660        };
661        let RenderNode::Layer(layer) = &container.children[old_index] else {
662            return Err("retained child slot is not a layer");
663        };
664        if dirty_nodes.contains(child_id) {
665            continue;
666        }
667        if layer.has_origin_sinks {
668            return Err("child subtree publishes window origins");
669        }
670        if state.size().width != layer.local_bounds.width
671            || state.size().height != layer.local_bounds.height
672        {
673            return Err("child resized");
674        }
675    }
676    Ok(())
677}
678
679#[derive(Clone, Copy)]
680struct TranslateGeometry {
681    content_offset: Point,
682    layer_translation: Point,
683    window_origin: Point,
684    child_origin: Point,
685    translation_delta: Point,
686}
687
688impl TranslateGeometry {
689    fn new(
690        container: &LayerNode,
691        layout_state: &cranpose_ui::widgets::LayoutState,
692        graphics_layer: &GraphicsLayer,
693        parent_abs: AbsOrigin,
694    ) -> Self {
695        let content_offset = layout_state.content_offset;
696        let top_left = Point {
697            x: parent_abs.content_origin.x + layout_state.position().x,
698            y: parent_abs.content_origin.y + layout_state.position().y,
699        };
700        let layer_translation = Point {
701            x: parent_abs.layer_translation.x + graphics_layer.translation_x,
702            y: parent_abs.layer_translation.y + graphics_layer.translation_y,
703        };
704        Self {
705            content_offset,
706            layer_translation,
707            window_origin: Point {
708                x: top_left.x + layer_translation.x,
709                y: top_left.y + layer_translation.y,
710            },
711            child_origin: Point {
712                x: top_left.x + content_offset.x,
713                y: top_left.y + content_offset.y,
714            },
715            translation_delta: Point {
716                x: layer_translation.x - container.scene_children_layer_translation.x,
717                y: layer_translation.y - container.scene_children_layer_translation.y,
718            },
719        }
720    }
721}
722
723fn build_entering_children(
724    applier: &mut MemoryApplier,
725    container: &LayerNode,
726    placed_fresh: &[(NodeId, cranpose_ui::widgets::LayoutState)],
727    retained: (bool, &std::collections::HashMap<NodeId, usize>),
728    geometry: TranslateGeometry,
729    inherited: (bool, bool),
730) -> std::collections::HashMap<NodeId, LayerNode> {
731    let (children_unchanged, old_index_by_id) = retained;
732    let (child_inherited_translated_content_context, children_ancestor_hashed) = inherited;
733    let mut entering: std::collections::HashMap<NodeId, LayerNode> =
734        std::collections::HashMap::new();
735    for (child_id, _) in placed_fresh {
736        if children_unchanged || old_index_by_id.contains_key(child_id) {
737            continue;
738        }
739        let Some(mut lowered) = build_layer_node_from_applier_internal(
740            applier,
741            *child_id,
742            container.motion_context_animated,
743            child_inherited_translated_content_context,
744            Some(AbsOrigin {
745                content_origin: geometry.child_origin,
746                layer_translation: geometry.layer_translation,
747            }),
748        ) else {
749            continue;
750        };
751        if geometry.content_offset != Point::default() {
752            lowered.transform_to_parent =
753                lowered
754                    .transform_to_parent
755                    .then(ProjectiveTransform::translation(
756                        geometry.content_offset.x,
757                        geometry.content_offset.y,
758                    ));
759        }
760        crate::graph_hash::recompute_layer_raster_cache_hashes_under(
761            &mut lowered,
762            children_ancestor_hashed,
763        );
764        entering.insert(*child_id, lowered);
765    }
766    entering
767}
768
769fn apply_translated_container_state(
770    container: &mut LayerNode,
771    modifier_slices: &ModifierNodeSlices,
772    layout_state: &cranpose_ui::widgets::LayoutState,
773    graphics_layer: &GraphicsLayer,
774    parent_content_offset: Point,
775    geometry: TranslateGeometry,
776) {
777    let mut transform = layer_transform_to_parent(
778        container.local_bounds,
779        layout_state.position(),
780        graphics_layer,
781    );
782    if parent_content_offset != Point::default() {
783        transform = transform.then(ProjectiveTransform::translation(
784            parent_content_offset.x,
785            parent_content_offset.y,
786        ));
787    }
788    container.transform_to_parent = transform;
789    container.content_offset = geometry.content_offset;
790    if container.translated_content_context {
791        container.translated_content_offset = modifier_slices
792            .translated_content_offset()
793            .unwrap_or(geometry.content_offset);
794    }
795    if let Some(sink) = modifier_slices.text_window_origin() {
796        sink.set(geometry.window_origin);
797    }
798    if let Some(sink) = modifier_slices.viewport_window_rect() {
799        sink.set(Rect {
800            x: geometry.window_origin.x,
801            y: geometry.window_origin.y,
802            width: layout_state.size().width,
803            height: layout_state.size().height,
804        });
805    }
806    container.scene_children_origin = geometry.child_origin;
807    container.scene_children_layer_translation = geometry.layer_translation;
808}
809
810fn reconcile_translated_children(
811    container: &mut LayerNode,
812    dirty_nodes: &mut HashSet<NodeId>,
813    changed_nodes: &mut Vec<NodeId>,
814    placed_fresh: &[(NodeId, cranpose_ui::widgets::LayoutState)],
815    children_unchanged: bool,
816    entering: &mut std::collections::HashMap<NodeId, LayerNode>,
817    geometry: TranslateGeometry,
818) {
819    if children_unchanged {
820        for (child, (child_id, state)) in container.children.iter_mut().zip(placed_fresh) {
821            let RenderNode::Layer(layer) = child else {
822                unreachable!("retained child identities were checked");
823            };
824            if !dirty_nodes.contains(child_id) {
825                translate_retained_child(
826                    layer,
827                    state,
828                    geometry.content_offset,
829                    geometry.child_origin,
830                    geometry.translation_delta,
831                );
832                changed_nodes.push(*child_id);
833            }
834        }
835        return;
836    }
837    let fresh_id_set: HashSet<NodeId> = placed_fresh.iter().map(|(id, _)| *id).collect();
838    let mut old_by_id: std::collections::HashMap<NodeId, Box<LayerNode>> =
839        std::collections::HashMap::new();
840    for child in container.children.drain(..) {
841        let RenderNode::Layer(layer) = child else {
842            continue;
843        };
844        let child_id = layer_identity(&layer).expect("checked above");
845        if fresh_id_set.contains(&child_id) {
846            old_by_id.insert(child_id, layer);
847        } else {
848            collect_layer_node_ids(&layer, changed_nodes);
849        }
850    }
851    let mut new_children = Vec::with_capacity(placed_fresh.len());
852    for (child_id, state) in placed_fresh {
853        if let Some(mut layer) = old_by_id.remove(child_id) {
854            if !dirty_nodes.contains(child_id) {
855                translate_retained_child(
856                    &mut layer,
857                    state,
858                    geometry.content_offset,
859                    geometry.child_origin,
860                    geometry.translation_delta,
861                );
862                changed_nodes.push(*child_id);
863            }
864            new_children.push(RenderNode::Layer(layer));
865        } else if let Some(lowered) = entering.remove(child_id) {
866            dirty_nodes.remove(child_id);
867            remove_dirty_descendants(&lowered, dirty_nodes);
868            collect_layer_node_ids(&lowered, changed_nodes);
869            new_children.push(RenderNode::Layer(Box::new(lowered)));
870        }
871    }
872    container.children = new_children;
873}
874
875fn translate_layer_from_data(
876    applier: &mut MemoryApplier,
877    container: &mut LayerNode,
878    dirty_nodes: &mut HashSet<NodeId>,
879    changed_nodes: &mut Vec<NodeId>,
880    ancestors: TranslateAncestorContext,
881    data: SnapshotNodeData,
882    wrapped: bool,
883) -> bool {
884    let TranslateAncestorContext {
885        inherited_motion_context_animated,
886        ancestor_hashed: container_ancestor_hashed,
887        inherited_translated_content_context,
888        parent_content_offset,
889        parent_abs,
890    } = ancestors;
891    let SnapshotNodeData {
892        layout_state,
893        modifier_slices,
894        resolved_modifiers: _,
895        children: fresh_children,
896        window_root,
897    } = data;
898    let layout_state = if window_root {
899        layout_state.at_origin()
900    } else {
901        layout_state
902    };
903    let container_plan = match translated_container(
904        container,
905        &layout_state,
906        &modifier_slices,
907        inherited_motion_context_animated,
908        wrapped,
909    ) {
910        Ok(plan) => plan,
911        Err(reason) => return translate_bail(reason),
912    };
913    let TranslatedContainer {
914        node_id,
915        clip_to_bounds,
916        graphics_layer,
917    } = container_plan;
918    let child_plan = match translated_children(applier, container, dirty_nodes, &fresh_children) {
919        Ok(plan) => plan,
920        Err(reason) => return translate_bail(reason),
921    };
922    let TranslatedChildren {
923        placed_fresh,
924        children_unchanged,
925        old_index_by_id,
926    } = child_plan;
927
928    let geometry = TranslateGeometry::new(container, &layout_state, &graphics_layer, parent_abs);
929    let child_inherited_translated_content_context =
930        inherited_translated_content_context || container.translated_content_context;
931    let children_ancestor_hashed =
932        crate::graph_hash::layer_children_ancestor_hashed(container, container_ancestor_hashed);
933    let mut entering = build_entering_children(
934        applier,
935        container,
936        &placed_fresh,
937        (children_unchanged, &old_index_by_id),
938        geometry,
939        (
940            child_inherited_translated_content_context,
941            children_ancestor_hashed,
942        ),
943    );
944
945    apply_translated_container_state(
946        container,
947        &modifier_slices,
948        &layout_state,
949        &graphics_layer,
950        parent_content_offset,
951        geometry,
952    );
953
954    reconcile_translated_children(
955        container,
956        dirty_nodes,
957        changed_nodes,
958        &placed_fresh,
959        children_unchanged,
960        &mut entering,
961        geometry,
962    );
963    modifier_slices.publish_pointer_input_size(layout_state.size());
964    container.hit_test = hit_test_from_slices(
965        &modifier_slices,
966        container.local_bounds,
967        clip_to_bounds || graphics_layer.clip,
968    );
969
970    container.has_hit_targets = container.hit_test.is_some()
971        || container.children.iter().any(|child| match child {
972            RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
973            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
974        });
975    container.has_origin_sinks = modifier_slices_have_origin_sinks(&modifier_slices)
976        || container.children.iter().any(|child| match child {
977            RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
978            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
979        });
980
981    crate::graph_hash::refresh_layer_own_raster_cache_hashes(container, container_ancestor_hashed);
982    changed_nodes.push(node_id);
983    true
984}
985
986fn translate_retained_child(
987    layer: &mut LayerNode,
988    state: &cranpose_ui::widgets::LayoutState,
989    content_offset: Point,
990    child_origin: Point,
991    translation_delta: Point,
992) {
993    let mut child_transform =
994        layer_transform_to_parent(layer.local_bounds, state.position(), &layer.graphics_layer);
995    if content_offset != Point::default() {
996        child_transform = child_transform.then(ProjectiveTransform::translation(
997            content_offset.x,
998            content_offset.y,
999        ));
1000    }
1001    layer.transform_to_parent = child_transform;
1002    let new_children_origin = Point {
1003        x: child_origin.x + state.position().x + layer.content_offset.x,
1004        y: child_origin.y + state.position().y + layer.content_offset.y,
1005    };
1006    let origin_delta = Point {
1007        x: new_children_origin.x - layer.scene_children_origin.x,
1008        y: new_children_origin.y - layer.scene_children_origin.y,
1009    };
1010    offset_scene_origins(layer, origin_delta, translation_delta);
1011}
1012
1013fn offset_scene_origins(layer: &mut LayerNode, origin_delta: Point, translation_delta: Point) {
1014    layer.scene_children_origin.x += origin_delta.x;
1015    layer.scene_children_origin.y += origin_delta.y;
1016    layer.scene_children_layer_translation.x += translation_delta.x;
1017    layer.scene_children_layer_translation.y += translation_delta.y;
1018    for child in &mut layer.children {
1019        if let RenderNode::Layer(child_layer) = child {
1020            offset_scene_origins(child_layer, origin_delta, translation_delta);
1021        }
1022    }
1023}
1024
1025fn layer_hit_graph_state_dirty(previous: &LayerNode, replacement: &LayerNode) -> bool {
1026    if previous.hit_test.is_some() || replacement.hit_test.is_some() {
1027        return true;
1028    }
1029
1030    if !(previous.has_hit_targets || replacement.has_hit_targets) {
1031        return false;
1032    }
1033
1034    previous.has_hit_targets != replacement.has_hit_targets
1035        || previous.local_bounds != replacement.local_bounds
1036        || previous.transform_to_parent != replacement.transform_to_parent
1037        || previous.clip_rect() != replacement.clip_rect()
1038        || previous.graphics_layer.shape != replacement.graphics_layer.shape
1039}
1040
1041fn collect_layer_node_ids(layer: &LayerNode, out: &mut Vec<NodeId>) {
1042    if let Some(node_id) = layer.node_id {
1043        out.push(node_id);
1044    }
1045    for child in &layer.children {
1046        if let RenderNode::Layer(child_layer) = child {
1047            collect_layer_node_ids(child_layer, out);
1048        }
1049    }
1050}
1051
1052fn remove_dirty_descendants(layer: &LayerNode, dirty_nodes: &mut HashSet<NodeId>) {
1053    for child in &layer.children {
1054        let RenderNode::Layer(child_layer) = child else {
1055            continue;
1056        };
1057        if let Some(node_id) = child_layer.node_id {
1058            dirty_nodes.remove(&node_id);
1059        }
1060        remove_dirty_descendants(child_layer, dirty_nodes);
1061    }
1062}
1063
1064fn build_layer_node(
1065    snapshot: BuildNodeSnapshot,
1066    _root_scale: f32,
1067    inherited_motion_context_animated: bool,
1068) -> LayerNode {
1069    build_layer_node_internal(snapshot, inherited_motion_context_animated, false)
1070}
1071
1072fn build_layer_node_internal(
1073    snapshot: BuildNodeSnapshot,
1074    inherited_motion_context_animated: bool,
1075    inherited_translated_content_context: bool,
1076) -> LayerNode {
1077    let BuildNodeSnapshot {
1078        node_id,
1079        placement,
1080        size,
1081        content_offset,
1082        motion_context_animated,
1083        translated_content_context,
1084        has_own_origin_sinks,
1085        measured_text_layout,
1086        resolved_modifiers,
1087        draw_commands,
1088        outer_draw_command_count,
1089        click_actions,
1090        pointer_inputs,
1091        pointer_icon,
1092        clip_to_bounds,
1093        text_style,
1094        text_layout_options,
1095        text_pan,
1096        graphics_layer,
1097        children: child_snapshots,
1098    } = snapshot;
1099    let outer = outer_draws(node_id, &draw_commands, outer_draw_command_count, size);
1100    let layer_draw_commands = &draw_commands[outer_draw_command_count..];
1101    let local_bounds = Rect {
1102        x: 0.0,
1103        y: 0.0,
1104        width: size.width,
1105        height: size.height,
1106    };
1107    let graphics_layer = graphics_layer.unwrap_or_default();
1108    let transform_to_parent = layer_transform_to_parent(local_bounds, placement, &graphics_layer);
1109    let isolation = isolation_reasons(&graphics_layer);
1110    let cache_policy = layer_cache_policy(&graphics_layer, isolation);
1111    let shadow_clip = clip_to_bounds.then_some(local_bounds);
1112    let hit_test = (!click_actions.is_empty()
1113        || !pointer_inputs.is_empty()
1114        || pointer_icon.is_some())
1115    .then(|| HitTestNode {
1116        shape: None,
1117        click_actions,
1118        pointer_inputs,
1119        pointer_icon,
1120        clip: (clip_to_bounds || graphics_layer.clip).then_some(local_bounds),
1121    });
1122
1123    let node_motion_context_animated = inherited_motion_context_animated || motion_context_animated;
1124    let child_translated_content_context =
1125        inherited_translated_content_context || translated_content_context;
1126
1127    let mut children = Vec::with_capacity(layer_node_capacity(
1128        layer_draw_commands,
1129        child_snapshots.len(),
1130        measured_text_layout.is_some(),
1131    ));
1132    append_draw_nodes(
1133        &mut children,
1134        node_id,
1135        layer_draw_commands,
1136        outer_draw_command_count,
1137        DrawPlacement::Behind,
1138        size,
1139        PrimitivePhase::BeforeChildren,
1140    );
1141    if let Some(text) = text_node_from_parts(TextNodeParts {
1142        node_id,
1143        local_bounds,
1144        resolved_modifiers: &resolved_modifiers,
1145        text_style: text_style.as_ref(),
1146        text_layout_options,
1147        text_pan,
1148        measured_layout: measured_text_layout,
1149    }) {
1150        children.push(RenderNode::Primitive(PrimitiveEntry {
1151            phase: PrimitivePhase::BeforeChildren,
1152            node: PrimitiveNode::Text(Box::new(text)),
1153        }));
1154    }
1155    let child_motion_context_animated = node_motion_context_animated;
1156    for child in child_snapshots {
1157        let mut child_layer = build_layer_node_internal(
1158            child,
1159            child_motion_context_animated,
1160            child_translated_content_context,
1161        );
1162        if content_offset != Point::default() {
1163            child_layer.transform_to_parent =
1164                child_layer
1165                    .transform_to_parent
1166                    .then(ProjectiveTransform::translation(
1167                        content_offset.x,
1168                        content_offset.y,
1169                    ));
1170        }
1171        children.push(RenderNode::Layer(Box::new(child_layer)));
1172    }
1173    append_draw_nodes(
1174        &mut children,
1175        node_id,
1176        layer_draw_commands,
1177        outer_draw_command_count,
1178        DrawPlacement::Overlay,
1179        size,
1180        PrimitivePhase::AfterChildren,
1181    );
1182    let has_hit_targets = hit_test.is_some()
1183        || children.iter().any(|child| match child {
1184            RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
1185            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1186        });
1187    let has_origin_sinks = has_own_origin_sinks
1188        || children.iter().any(|child| match child {
1189            RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
1190            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1191        });
1192
1193    let layer = LayerNode {
1194        node_id: Some(node_id),
1195        wraps: None,
1196        local_bounds,
1197        transform_to_parent,
1198        content_offset,
1199        motion_context_animated: node_motion_context_animated,
1200        translated_content_context,
1201        translated_content_offset: if translated_content_context {
1202            content_offset
1203        } else {
1204            Point::default()
1205        },
1206        scene_children_origin: Point::default(),
1207        scene_children_layer_translation: Point::default(),
1208        graphics_layer,
1209        clip_to_bounds,
1210        shadow_clip,
1211        hit_test,
1212        has_hit_targets,
1213        has_origin_sinks,
1214        isolation,
1215        cache_policy,
1216        cache_hashes: LayerRasterCacheHashes::default(),
1217        cache_hashes_valid: false,
1218        children,
1219    };
1220    finish_layer(layer, placement, outer)
1221}
1222
1223#[derive(Clone, Copy)]
1224struct AbsOrigin {
1225    content_origin: Point,
1226    layer_translation: Point,
1227}
1228
1229impl AbsOrigin {
1230    const ROOT: AbsOrigin = AbsOrigin {
1231        content_origin: Point { x: 0.0, y: 0.0 },
1232        layer_translation: Point { x: 0.0, y: 0.0 },
1233    };
1234}
1235
1236fn build_layer_node_from_applier(
1237    applier: &mut MemoryApplier,
1238    node_id: NodeId,
1239    _root_scale: f32,
1240    inherited_motion_context_animated: bool,
1241) -> Option<LayerNode> {
1242    let mut data = snapshot_node_data(applier, node_id)?;
1243    if data.window_root {
1244        data.layout_state = data.layout_state.at_origin();
1245    }
1246    build_layer_node_from_data(
1247        applier,
1248        node_id,
1249        data,
1250        inherited_motion_context_animated,
1251        false,
1252        Some(AbsOrigin::ROOT),
1253    )
1254}
1255
1256fn snapshot_node_data(applier: &mut MemoryApplier, node_id: NodeId) -> Option<SnapshotNodeData> {
1257    if let Ok(data) = applier.with_node::<LayoutNode, _>(node_id, |node| {
1258        let state = node.layout_state();
1259        let mut children = SmallVec::new();
1260        node.collect_children_into(&mut children);
1261        let modifier_slices = node.modifier_slices_snapshot();
1262        SnapshotNodeData {
1263            layout_state: state,
1264            modifier_slices,
1265            resolved_modifiers: node.resolved_modifiers(),
1266            children,
1267            window_root: node.is_window_root(),
1268        }
1269    }) {
1270        return Some(data);
1271    }
1272
1273    applier
1274        .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1275            let state = node.layout_state();
1276            let mut children = SmallVec::new();
1277            node.collect_children_into(&mut children);
1278            let modifier_slices = node.modifier_slices_snapshot();
1279            SnapshotNodeData {
1280                layout_state: state,
1281                modifier_slices,
1282                resolved_modifiers: node.resolved_modifiers(),
1283                children,
1284                window_root: false,
1285            }
1286        })
1287        .ok()
1288}
1289
1290fn build_layer_node_from_applier_internal(
1291    applier: &mut MemoryApplier,
1292    node_id: NodeId,
1293    inherited_motion_context_animated: bool,
1294    inherited_translated_content_context: bool,
1295    parent_abs: Option<AbsOrigin>,
1296) -> Option<LayerNode> {
1297    let data = snapshot_node_data(applier, node_id)?;
1298    if data.window_root {
1299        return None;
1300    }
1301    build_layer_node_from_data(
1302        applier,
1303        node_id,
1304        data,
1305        inherited_motion_context_animated,
1306        inherited_translated_content_context,
1307        parent_abs,
1308    )
1309}
1310
1311fn hit_test_from_slices(
1312    slices: &ModifierNodeSlices,
1313    bounds: Rect,
1314    clip: bool,
1315) -> Option<HitTestNode> {
1316    let click_actions = slices.click_handlers();
1317    let pointer_inputs = slices.pointer_inputs();
1318    let pointer_icon = slices.pointer_icon();
1319    (!click_actions.is_empty() || !pointer_inputs.is_empty() || pointer_icon.is_some()).then(|| {
1320        HitTestNode {
1321            shape: None,
1322            click_actions: click_actions.to_vec(),
1323            pointer_inputs: pointer_inputs.to_vec(),
1324            pointer_icon: pointer_icon.cloned(),
1325            clip: clip.then_some(bounds),
1326        }
1327    })
1328}
1329
1330fn build_layer_node_from_data(
1331    applier: &mut MemoryApplier,
1332    node_id: NodeId,
1333    data: SnapshotNodeData,
1334    inherited_motion_context_animated: bool,
1335    inherited_translated_content_context: bool,
1336    parent_abs: Option<AbsOrigin>,
1337) -> Option<LayerNode> {
1338    note_layer_lowered();
1339    let SnapshotNodeData {
1340        layout_state,
1341        modifier_slices,
1342        resolved_modifiers,
1343        children,
1344        window_root: _,
1345    } = data;
1346    if !layout_state.is_placed() {
1347        return None;
1348    }
1349
1350    let local_bounds = Rect {
1351        x: 0.0,
1352        y: 0.0,
1353        width: layout_state.size().width,
1354        height: layout_state.size().height,
1355    };
1356    if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
1357        eprintln!(
1358            "[scene-update-diag] build layer node={node_id:?} size=({:.2},{:.2}) pos=({:.2},{:.2})",
1359            layout_state.size().width,
1360            layout_state.size().height,
1361            layout_state.position().x,
1362            layout_state.position().y,
1363        );
1364    }
1365    let clip_to_bounds = modifier_slices.clip_to_bounds();
1366    let graphics_layer = graphics_layer_with_shaped_clip(
1367        modifier_slices.graphics_layer().unwrap_or_default(),
1368        clip_to_bounds,
1369        modifier_slices.corner_shape(),
1370        local_bounds,
1371    );
1372    let transform_to_parent =
1373        layer_transform_to_parent(local_bounds, layout_state.position(), &graphics_layer);
1374    let isolation = isolation_reasons(&graphics_layer);
1375    let cache_policy = layer_cache_policy(&graphics_layer, isolation);
1376    let shadow_clip = clip_to_bounds.then_some(local_bounds);
1377    let hit_test = hit_test_from_slices(
1378        &modifier_slices,
1379        local_bounds,
1380        clip_to_bounds || graphics_layer.clip,
1381    );
1382
1383    modifier_slices.publish_pointer_input_size(layout_state.size());
1384
1385    let node_motion_context_animated =
1386        inherited_motion_context_animated || modifier_slices.motion_context_animated();
1387    let local_translated_content_context = modifier_slices.translated_content_context();
1388    let local_translated_content_offset = modifier_slices
1389        .translated_content_offset()
1390        .unwrap_or(layout_state.content_offset);
1391    let child_translated_content_context =
1392        inherited_translated_content_context || local_translated_content_context;
1393
1394    let this_abs = parent_abs.map(|parent| {
1395        let top_left = Point {
1396            x: parent.content_origin.x + layout_state.position().x,
1397            y: parent.content_origin.y + layout_state.position().y,
1398        };
1399        let layer_translation = Point {
1400            x: parent.layer_translation.x + graphics_layer.translation_x,
1401            y: parent.layer_translation.y + graphics_layer.translation_y,
1402        };
1403        (top_left, layer_translation)
1404    });
1405    if let Some((top_left, layer_translation)) = this_abs {
1406        let window_origin = Point {
1407            x: top_left.x + layer_translation.x,
1408            y: top_left.y + layer_translation.y,
1409        };
1410        if let Some(sink) = modifier_slices.text_window_origin() {
1411            sink.set(window_origin);
1412        }
1413        if let Some(sink) = modifier_slices.viewport_window_rect() {
1414            sink.set(Rect {
1415                x: window_origin.x,
1416                y: window_origin.y,
1417                width: layout_state.size().width,
1418                height: layout_state.size().height,
1419            });
1420        }
1421    }
1422    let child_abs = this_abs.map(|(top_left, layer_translation)| AbsOrigin {
1423        content_origin: Point {
1424            x: top_left.x + layout_state.content_offset.x,
1425            y: top_left.y + layout_state.content_offset.y,
1426        },
1427        layer_translation,
1428    });
1429
1430    let outer_draw_command_count = modifier_slices.outer_draw_command_count();
1431    let outer = outer_draws(
1432        node_id,
1433        modifier_slices.draw_commands(),
1434        outer_draw_command_count,
1435        layout_state.size(),
1436    );
1437    let layer_draw_commands = &modifier_slices.draw_commands()[outer_draw_command_count..];
1438    let mut render_children = Vec::with_capacity(layer_node_capacity(
1439        layer_draw_commands,
1440        children.len(),
1441        modifier_slices.annotated_text().is_some(),
1442    ));
1443    append_draw_nodes(
1444        &mut render_children,
1445        node_id,
1446        layer_draw_commands,
1447        outer_draw_command_count,
1448        DrawPlacement::Behind,
1449        layout_state.size(),
1450        PrimitivePhase::BeforeChildren,
1451    );
1452    if let Some(text) = text_node_from_parts(TextNodeParts {
1453        node_id,
1454        local_bounds,
1455        resolved_modifiers: &resolved_modifiers,
1456        text_style: modifier_slices.text_style(),
1457        text_layout_options: modifier_slices.text_layout_options(),
1458        text_pan: modifier_slices.text_pan_resolver(),
1459        measured_layout: modifier_slices.measured_text_layout(),
1460    }) {
1461        render_children.push(RenderNode::Primitive(PrimitiveEntry {
1462            phase: PrimitivePhase::BeforeChildren,
1463            node: PrimitiveNode::Text(Box::new(text)),
1464        }));
1465    }
1466    let child_motion_context_animated = node_motion_context_animated;
1467    for child_id in children {
1468        let Some(mut child_layer) = build_layer_node_from_applier_internal(
1469            applier,
1470            child_id,
1471            child_motion_context_animated,
1472            child_translated_content_context,
1473            child_abs,
1474        ) else {
1475            continue;
1476        };
1477        if layout_state.content_offset != Point::default() {
1478            child_layer.transform_to_parent =
1479                child_layer
1480                    .transform_to_parent
1481                    .then(ProjectiveTransform::translation(
1482                        layout_state.content_offset.x,
1483                        layout_state.content_offset.y,
1484                    ));
1485        }
1486        render_children.push(RenderNode::Layer(Box::new(child_layer)));
1487    }
1488    append_draw_nodes(
1489        &mut render_children,
1490        node_id,
1491        layer_draw_commands,
1492        outer_draw_command_count,
1493        DrawPlacement::Overlay,
1494        layout_state.size(),
1495        PrimitivePhase::AfterChildren,
1496    );
1497    let has_hit_targets = hit_test.is_some()
1498        || render_children.iter().any(|child| match child {
1499            RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
1500            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1501        });
1502    let has_origin_sinks = modifier_slices_have_origin_sinks(&modifier_slices)
1503        || render_children.iter().any(|child| match child {
1504            RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
1505            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1506        });
1507
1508    let layer = LayerNode {
1509        node_id: Some(node_id),
1510        wraps: None,
1511        local_bounds,
1512        transform_to_parent,
1513        content_offset: layout_state.content_offset,
1514        motion_context_animated: node_motion_context_animated,
1515        translated_content_context: local_translated_content_context,
1516        translated_content_offset: if local_translated_content_context {
1517            local_translated_content_offset
1518        } else {
1519            Point::default()
1520        },
1521        scene_children_origin: child_abs.map(|c| c.content_origin).unwrap_or_default(),
1522        scene_children_layer_translation: child_abs
1523            .map(|c| c.layer_translation)
1524            .unwrap_or_default(),
1525        graphics_layer,
1526        clip_to_bounds,
1527        shadow_clip,
1528        hit_test,
1529        has_hit_targets,
1530        has_origin_sinks,
1531        isolation,
1532        cache_policy,
1533        cache_hashes: LayerRasterCacheHashes::default(),
1534        cache_hashes_valid: false,
1535        children: render_children,
1536    };
1537    Some(finish_layer(layer, layout_state.position(), outer))
1538}
1539
1540struct RecorderSlot {
1541    generation: u64,
1542    handles: [Option<Rc<CommandRecording>>; 2],
1543}
1544
1545thread_local! {
1546    static COMMAND_RECORDINGS: std::cell::RefCell<
1547        std::collections::HashMap<DrawCommandId, RecorderSlot, cranpose_ui_graphics::FxBuildHasher>,
1548    > = std::cell::RefCell::new(std::collections::HashMap::default());
1549    static RECORDING_GENERATION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
1550}
1551
1552#[doc(hidden)]
1553pub fn clear_command_recordings_for_tests() {
1554    COMMAND_RECORDINGS.with(|map| map.borrow_mut().clear());
1555}
1556
1557fn bump_recording_generation() {
1558    let generation = RECORDING_GENERATION.with(|cell| {
1559        let next = cell.get().wrapping_add(1);
1560        cell.set(next);
1561        next
1562    });
1563    if generation.is_multiple_of(512) {
1564        COMMAND_RECORDINGS.with(|map| {
1565            map.borrow_mut()
1566                .retain(|_, slot| generation.wrapping_sub(slot.generation) <= 64);
1567        });
1568    }
1569}
1570
1571fn acquire_storage(id: DrawCommandId) -> CommandRecording {
1572    COMMAND_RECORDINGS.with(|map| {
1573        let mut map = map.borrow_mut();
1574        let Some(slot) = map.get_mut(&id) else {
1575            return CommandRecording::default();
1576        };
1577        for handle in &mut slot.handles {
1578            if handle
1579                .as_ref()
1580                .is_some_and(|shared| Rc::strong_count(shared) == 1)
1581            {
1582                let shared = handle.take().expect("checked some above");
1583                return Rc::try_unwrap(shared).expect("sole owner checked above");
1584            }
1585        }
1586        CommandRecording::default()
1587    })
1588}
1589
1590fn publish_recording(id: DrawCommandId, recording: CommandRecording) -> Rc<CommandRecording> {
1591    let shared = Rc::new(recording);
1592    COMMAND_RECORDINGS.with(|map| {
1593        let mut map = map.borrow_mut();
1594        let generation = RECORDING_GENERATION.with(Cell::get);
1595        let slot = map.entry(id).or_insert_with(|| RecorderSlot {
1596            generation,
1597            handles: [None, None],
1598        });
1599        slot.generation = generation;
1600        slot.handles[1] = slot.handles[0].take();
1601        slot.handles[0] = Some(shared.clone());
1602    });
1603    shared
1604}
1605
1606fn layer_node_capacity(commands: &[DrawCommand], children: usize, has_text: bool) -> usize {
1607    children
1608        + usize::from(has_text)
1609        + commands.len()
1610        + commands
1611            .iter()
1612            .filter(|command| matches!(command, DrawCommand::WithContent(_)))
1613            .count()
1614}
1615
1616fn draw_nodes(
1617    node_id: NodeId,
1618    commands: &[DrawCommand],
1619    first_command_index: usize,
1620    placement: DrawPlacement,
1621    size: Size,
1622    phase: PrimitivePhase,
1623) -> Vec<RenderNode> {
1624    let mut nodes = Vec::new();
1625    append_draw_nodes(
1626        &mut nodes,
1627        node_id,
1628        commands,
1629        first_command_index,
1630        placement,
1631        size,
1632        phase,
1633    );
1634    nodes
1635}
1636
1637fn append_draw_nodes(
1638    nodes: &mut Vec<RenderNode>,
1639    node_id: NodeId,
1640    commands: &[DrawCommand],
1641    first_command_index: usize,
1642    placement: DrawPlacement,
1643    size: Size,
1644    phase: PrimitivePhase,
1645) {
1646    for (command_index, command) in commands.iter().enumerate() {
1647        let id = DrawCommandId {
1648            node_id,
1649            command_index: (first_command_index + command_index) as u32,
1650            placement,
1651        };
1652        let Some((recording, segments)) =
1653            recording_for_placement_reusing(command, placement, size, || acquire_storage(id))
1654        else {
1655            retain_empty_draw_command(nodes, phase, id, placement, command);
1656            continue;
1657        };
1658        let shared = publish_recording(id, recording);
1659        if shared.is_empty_in(&segments) {
1660            retain_empty_draw_command(nodes, phase, id, placement, command);
1661            continue;
1662        }
1663        nodes.push(RenderNode::DrawRun(DrawRunNode::for_command_shared(
1664            phase,
1665            Some(id),
1666            shared,
1667            segments,
1668        )));
1669    }
1670}
1671
1672fn retain_empty_draw_command(
1673    nodes: &mut Vec<RenderNode>,
1674    phase: PrimitivePhase,
1675    id: DrawCommandId,
1676    placement: DrawPlacement,
1677    command: &DrawCommand,
1678) {
1679    if matches!(
1680        (placement, command),
1681        (DrawPlacement::Behind, DrawCommand::Behind(_))
1682            | (DrawPlacement::Overlay, DrawCommand::Overlay(_))
1683            | (_, DrawCommand::WithContent(_))
1684    ) {
1685        nodes.push(RenderNode::DrawRun(DrawRunNode::for_command(
1686            phase,
1687            Some(id),
1688            Vec::new(),
1689        )));
1690    }
1691}
1692
1693#[doc(hidden)]
1694pub fn draw_command_nodes_for_tests(
1695    node_id: NodeId,
1696    commands: &[DrawCommand],
1697    placement: DrawPlacement,
1698    size: Size,
1699    phase: PrimitivePhase,
1700) -> Vec<RenderNode> {
1701    bump_recording_generation();
1702    draw_nodes(node_id, commands, 0, placement, size, phase)
1703}
1704
1705struct OuterDraws {
1706    behind: Vec<RenderNode>,
1707    overlay: Vec<RenderNode>,
1708}
1709
1710fn outer_draws(
1711    node_id: NodeId,
1712    draw_commands: &[DrawCommand],
1713    outer_draw_command_count: usize,
1714    size: Size,
1715) -> Option<OuterDraws> {
1716    (outer_draw_command_count > 0).then(|| {
1717        let commands = &draw_commands[..outer_draw_command_count];
1718        OuterDraws {
1719            behind: draw_nodes(
1720                node_id,
1721                commands,
1722                0,
1723                DrawPlacement::Behind,
1724                size,
1725                PrimitivePhase::BeforeChildren,
1726            ),
1727            overlay: draw_nodes(
1728                node_id,
1729                commands,
1730                0,
1731                DrawPlacement::Overlay,
1732                size,
1733                PrimitivePhase::AfterChildren,
1734            ),
1735        }
1736    })
1737}
1738
1739fn finish_layer(layer: LayerNode, placement: Point, outer: Option<OuterDraws>) -> LayerNode {
1740    match outer {
1741        Some(outer) => wrap_layer_with_outer_draws(layer, placement, outer),
1742        None => layer,
1743    }
1744}
1745
1746fn wrap_layer_with_outer_draws(
1747    mut layer: LayerNode,
1748    placement: Point,
1749    outer: OuterDraws,
1750) -> LayerNode {
1751    let local_bounds = layer.local_bounds;
1752    layer.transform_to_parent =
1753        layer_transform_to_parent(local_bounds, Point::default(), &layer.graphics_layer);
1754    let wrapper = LayerNode {
1755        wraps: layer.node_id,
1756        local_bounds,
1757        transform_to_parent: layer_transform_to_parent(
1758            local_bounds,
1759            placement,
1760            &GraphicsLayer::default(),
1761        ),
1762        scene_children_origin: Point {
1763            x: layer.scene_children_origin.x - layer.content_offset.x,
1764            y: layer.scene_children_origin.y - layer.content_offset.y,
1765        },
1766        scene_children_layer_translation: Point {
1767            x: layer.scene_children_layer_translation.x - layer.graphics_layer.translation_x,
1768            y: layer.scene_children_layer_translation.y - layer.graphics_layer.translation_y,
1769        },
1770        motion_context_animated: layer.motion_context_animated,
1771        has_hit_targets: layer.has_hit_targets,
1772        has_origin_sinks: layer.has_origin_sinks,
1773        ..Default::default()
1774    };
1775    let mut children = outer.behind;
1776    children.push(RenderNode::Layer(Box::new(layer)));
1777    children.extend(outer.overlay);
1778    LayerNode {
1779        children,
1780        ..wrapper
1781    }
1782}
1783
1784fn layer_identity(layer: &LayerNode) -> Option<NodeId> {
1785    layer.node_id.or(layer.wraps)
1786}
1787
1788struct TextNodeParts<'a> {
1789    node_id: NodeId,
1790    local_bounds: Rect,
1791    resolved_modifiers: &'a ResolvedModifiers,
1792    text_style: Option<&'a TextStyle>,
1793    text_layout_options: Option<TextLayoutOptions>,
1794    text_pan: Option<TextPanResolver>,
1795    measured_layout: Option<Rc<PreparedTextLayout>>,
1796}
1797
1798fn text_node_from_parts(parts: TextNodeParts<'_>) -> Option<TextPrimitiveNode> {
1799    let TextNodeParts {
1800        node_id,
1801        local_bounds,
1802        resolved_modifiers,
1803        text_style,
1804        text_layout_options,
1805        text_pan,
1806        measured_layout,
1807    } = parts;
1808    let prepared = measured_layout?;
1809    let default_text_style = TextStyle::default();
1810    let text_style = text_style.cloned().unwrap_or(default_text_style);
1811    let options = text_layout_options.unwrap_or_default().normalized();
1812    let padding = resolved_modifiers.padding();
1813    let content_width = (local_bounds.width - padding.left - padding.right).max(0.0);
1814    if content_width <= 0.0 {
1815        return None;
1816    }
1817
1818    let pan_offset = text_pan
1819        .as_ref()
1820        .map_or(0.0, |resolve| resolve(content_width));
1821    let pans_horizontally = text_pan.is_some();
1822
1823    let visual_style = prepared.visual_style.clone();
1824    let measured_draw_width = prepared.metrics.width.max(0.0);
1825    let draw_width = if options.overflow == TextOverflow::Visible || pans_horizontally {
1826        measured_draw_width
1827    } else {
1828        measured_draw_width.min(content_width)
1829    };
1830    let alignment_offset = resolve_text_horizontal_offset(
1831        &text_style,
1832        prepared.text.text.as_str(),
1833        content_width,
1834        prepared.metrics.width,
1835    );
1836    let rect = Rect {
1837        x: padding.left + alignment_offset - pan_offset,
1838        y: padding.top,
1839        width: draw_width,
1840        height: prepared.metrics.height,
1841    };
1842    let text_bounds = Rect {
1843        x: padding.left,
1844        y: padding.top,
1845        width: content_width,
1846        height: (local_bounds.height - padding.top - padding.bottom).max(0.0),
1847    };
1848    let font_size = visual_style.resolve_font_size(14.0);
1849    let expanded_bounds =
1850        expand_text_bounds_for_baseline_shift(text_bounds, &visual_style, font_size);
1851    let clip = if options.overflow == TextOverflow::Visible && !pans_horizontally {
1852        None
1853    } else {
1854        Some(pad_clip_rect(expanded_bounds))
1855    };
1856
1857    Some(TextPrimitiveNode {
1858        node_id,
1859        rect,
1860        text: Rc::clone(&prepared.text),
1861        text_style: visual_style,
1862        font_size,
1863        layout_options: options,
1864        clip,
1865    })
1866}
1867
1868fn layout_box_to_snapshot(node: &LayoutBox, parent: Option<&LayoutBox>) -> BuildNodeSnapshot {
1869    let placement = parent
1870        .map(|parent_box| Point {
1871            x: node.rect.x - parent_box.rect.x - parent_box.content_offset.x,
1872            y: node.rect.y - parent_box.rect.y - parent_box.content_offset.y,
1873        })
1874        .unwrap_or_default();
1875    let mut children = Vec::with_capacity(node.children.len());
1876    for child in &node.children {
1877        children.push(layout_box_to_snapshot(child, Some(node)));
1878    }
1879    let base_graphics_layer = node.node_data.modifier_slices.graphics_layer();
1880    let graphics_layer = graphics_layer_with_shaped_clip(
1881        base_graphics_layer.clone().unwrap_or_default(),
1882        node.node_data.modifier_slices.clip_to_bounds(),
1883        node.node_data.modifier_slices.corner_shape(),
1884        Rect {
1885            x: 0.0,
1886            y: 0.0,
1887            width: node.rect.width,
1888            height: node.rect.height,
1889        },
1890    );
1891    let has_graphics_layer =
1892        base_graphics_layer.is_some() || graphics_layer.render_effect.is_some();
1893
1894    BuildNodeSnapshot {
1895        node_id: node.node_id,
1896        placement,
1897        size: Size {
1898            width: node.rect.width,
1899            height: node.rect.height,
1900        },
1901        content_offset: node.content_offset,
1902        motion_context_animated: node.node_data.modifier_slices.motion_context_animated(),
1903        translated_content_context: node.node_data.modifier_slices.translated_content_context(),
1904        has_own_origin_sinks: modifier_slices_have_origin_sinks(&node.node_data.modifier_slices),
1905        measured_text_layout: node.node_data.modifier_slices.measured_text_layout(),
1906        resolved_modifiers: node.node_data.resolved_modifiers,
1907        draw_commands: node.node_data.modifier_slices.draw_commands().to_vec(),
1908        outer_draw_command_count: node.node_data.modifier_slices.outer_draw_command_count(),
1909        click_actions: node.node_data.modifier_slices.click_handlers().to_vec(),
1910        pointer_inputs: node.node_data.modifier_slices.pointer_inputs().to_vec(),
1911        pointer_icon: node.node_data.modifier_slices.pointer_icon().cloned(),
1912        clip_to_bounds: node.node_data.modifier_slices.clip_to_bounds(),
1913        text_style: node.node_data.modifier_slices.text_style().cloned(),
1914        text_layout_options: node.node_data.modifier_slices.text_layout_options(),
1915        text_pan: node.node_data.modifier_slices.text_pan_resolver(),
1916        graphics_layer: has_graphics_layer.then_some(graphics_layer),
1917        children,
1918    }
1919}
1920
1921fn modifier_slices_have_origin_sinks(slices: &ModifierNodeSlices) -> bool {
1922    slices.text_window_origin().is_some() || slices.viewport_window_rect().is_some()
1923}
1924
1925fn graphics_layer_with_shaped_clip(
1926    mut graphics_layer: GraphicsLayer,
1927    clip_to_bounds: bool,
1928    corner_shape: Option<RoundedCornerShape>,
1929    local_bounds: Rect,
1930) -> GraphicsLayer {
1931    if !clip_to_bounds {
1932        return graphics_layer;
1933    }
1934
1935    let Some(corner_shape) = corner_shape else {
1936        return graphics_layer;
1937    };
1938    let radii = corner_shape.resolve(local_bounds.width, local_bounds.height);
1939    if radii.top_left <= f32::EPSILON
1940        && radii.top_right <= f32::EPSILON
1941        && radii.bottom_right <= f32::EPSILON
1942        && radii.bottom_left <= f32::EPSILON
1943    {
1944        return graphics_layer;
1945    }
1946
1947    if let Some(existing) = graphics_layer.render_effect.take() {
1948        let rounded_clip = rounded_corner_alpha_mask_effect(
1949            local_bounds.width,
1950            local_bounds.height,
1951            radii,
1952            ROUNDED_CLIP_EDGE_FEATHER,
1953        );
1954        graphics_layer.render_effect = Some(existing.then(rounded_clip));
1955    } else {
1956        graphics_layer.shape = LayerShape::Rounded(corner_shape);
1957        graphics_layer.clip = true;
1958    }
1959    graphics_layer
1960}
1961
1962fn layer_cache_policy(layer: &GraphicsLayer, isolation: IsolationReasons) -> CachePolicy {
1963    if isolation.has_any() || layer_scales_or_rotates(layer) {
1964        CachePolicy::Auto
1965    } else {
1966        CachePolicy::None
1967    }
1968}
1969
1970fn isolation_reasons(layer: &GraphicsLayer) -> IsolationReasons {
1971    IsolationReasons {
1972        explicit_offscreen: layer.compositing_strategy == CompositingStrategy::Offscreen,
1973        shape_clip: layer.clip && !matches!(layer.shape, LayerShape::Rectangle),
1974        effect: layer.render_effect.is_some(),
1975        backdrop: layer.backdrop_effect.is_some(),
1976        group_opacity: layer.compositing_strategy != CompositingStrategy::ModulateAlpha
1977            && layer.alpha < 1.0,
1978        blend_mode: layer.blend_mode != cranpose_ui::BlendMode::SrcOver,
1979    }
1980}
1981
1982fn pad_clip_rect(rect: Rect) -> Rect {
1983    Rect {
1984        x: rect.x - TEXT_CLIP_PAD,
1985        y: rect.y - TEXT_CLIP_PAD,
1986        width: (rect.width + TEXT_CLIP_PAD * 2.0).max(0.0),
1987        height: (rect.height + TEXT_CLIP_PAD * 2.0).max(0.0),
1988    }
1989}
1990
1991pub fn expand_text_bounds_for_baseline_shift(
1992    text_bounds: Rect,
1993    text_style: &TextStyle,
1994    font_size: f32,
1995) -> Rect {
1996    let baseline_shift_px = text_style
1997        .span_style
1998        .baseline_shift
1999        .filter(|shift| shift.is_specified())
2000        .map_or(0.0, |shift| -(shift.0 * font_size));
2001    if baseline_shift_px == 0.0 {
2002        return text_bounds;
2003    }
2004
2005    if baseline_shift_px < 0.0 {
2006        Rect {
2007            x: text_bounds.x,
2008            y: text_bounds.y + baseline_shift_px,
2009            width: text_bounds.width,
2010            height: (text_bounds.height - baseline_shift_px).max(0.0),
2011        }
2012    } else {
2013        Rect {
2014            x: text_bounds.x,
2015            y: text_bounds.y,
2016            width: text_bounds.width,
2017            height: (text_bounds.height + baseline_shift_px).max(0.0),
2018        }
2019    }
2020}
2021
2022fn resolve_text_horizontal_offset(
2023    text_style: &TextStyle,
2024    text: &str,
2025    content_width: f32,
2026    measured_width: f32,
2027) -> f32 {
2028    let remaining = (content_width - measured_width).max(0.0);
2029    remaining * cranpose_ui::text::text_align_fraction(text_style, text)
2030}
2031
2032#[cfg(test)]
2033#[path = "tests/scene_builder_tests.rs"]
2034mod tests;