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