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