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    graphics_layer: GraphicsLayer,
512}
513
514fn translated_container(
515    container: &LayerNode,
516    layout_state: &cranpose_ui::widgets::LayoutState,
517    modifier_slices: &ModifierNodeSlices,
518    inherited_motion_context_animated: bool,
519    wrapped: bool,
520) -> Result<TranslatedContainer, &'static str> {
521    if cranpose_core::env_flag!("CRANPOSE_DISABLE_SCROLL_TRANSLATE") {
522        return Err("fast path disabled by ablation switch");
523    }
524    let Some(node_id) = container.node_id else {
525        return Err("no node id");
526    };
527    if container
528        .children
529        .iter()
530        .any(|child| !matches!(child, RenderNode::Layer(_)))
531    {
532        return Err("container has own primitive children");
533    }
534    if !layout_state.is_placed()
535        || Rect::from_size(layout_state.size()) != container.node_rect()
536        || modifier_slices.layer_bounds(layout_state.size()) != container.local_bounds
537    {
538        return Err("container unplaced, resized or its layer moved");
539    }
540    let outer_count = modifier_slices.outer_draw_command_count();
541    if (outer_count > 0 && !wrapped)
542        || !modifier_slices.draw_commands()[outer_count..].is_empty()
543        || (inherited_motion_context_animated || modifier_slices.motion_context_animated())
544            != container.motion_context_animated
545        || modifier_slices.annotated_text().is_some()
546        || modifier_slices.translated_content_context() != container.translated_content_context
547    {
548        return Err("container draw/text/translated-context changed");
549    }
550    let clip_to_bounds = modifier_slices.clip_to_bounds();
551    if clip_to_bounds != container.clip_to_bounds {
552        return Err("container clip changed");
553    }
554    let graphics_layer = graphics_layer_with_shaped_clip(
555        modifier_slices.graphics_layer().unwrap_or_default(),
556        clip_to_bounds,
557        modifier_slices.corner_shape(),
558        container.local_bounds,
559    );
560    if graphics_layer != container.graphics_layer {
561        return Err("container graphics layer changed");
562    }
563    Ok(TranslatedContainer {
564        node_id,
565        graphics_layer,
566    })
567}
568
569struct TranslatedChildren {
570    placed_fresh: SmallVec<[(NodeId, cranpose_ui::widgets::LayoutState); 8]>,
571    children_unchanged: bool,
572    old_index_by_id: std::collections::HashMap<NodeId, usize>,
573}
574
575fn translated_children(
576    applier: &mut MemoryApplier,
577    container: &LayerNode,
578    dirty_nodes: &HashSet<NodeId>,
579    fresh_children: &[NodeId],
580) -> Result<TranslatedChildren, &'static str> {
581    let mut placed_fresh = SmallVec::<[_; 8]>::with_capacity(fresh_children.len());
582    for child_id in fresh_children {
583        let state = applier
584            .with_node::<LayoutNode, _>(*child_id, |node| node.layout_state())
585            .or_else(|_| {
586                applier.with_node::<SubcomposeLayoutNode, _>(*child_id, |node| node.layout_state())
587            });
588        let Ok(state) = state else {
589            continue;
590        };
591        if !state.is_placed() {
592            continue;
593        }
594        placed_fresh.push((*child_id, state));
595    }
596    let children_unchanged = container.children.len() == placed_fresh.len()
597        && container
598            .children
599            .iter()
600            .zip(&placed_fresh)
601            .all(|(child, (id, _))| {
602                matches!(child, RenderNode::Layer(layer) if layer_identity(layer) == Some(*id))
603            });
604    let old_index_by_id = if children_unchanged {
605        std::collections::HashMap::new()
606    } else {
607        let Some(index): Option<std::collections::HashMap<NodeId, usize>> = container
608            .children
609            .iter()
610            .enumerate()
611            .map(|(index, child)| match child {
612                RenderNode::Layer(layer) => layer_identity(layer).map(|id| (id, index)),
613                _ => None,
614            })
615            .collect()
616        else {
617            return Err("child without node id");
618        };
619        index
620    };
621    check_retained_children(
622        container,
623        dirty_nodes,
624        &placed_fresh,
625        children_unchanged,
626        &old_index_by_id,
627    )?;
628    Ok(TranslatedChildren {
629        placed_fresh,
630        children_unchanged,
631        old_index_by_id,
632    })
633}
634
635fn check_retained_children(
636    container: &LayerNode,
637    dirty_nodes: &HashSet<NodeId>,
638    placed_fresh: &[(NodeId, cranpose_ui::widgets::LayoutState)],
639    children_unchanged: bool,
640    old_index_by_id: &std::collections::HashMap<NodeId, usize>,
641) -> Result<(), &'static str> {
642    for (fresh_index, (child_id, state)) in placed_fresh.iter().enumerate() {
643        let old_index = if children_unchanged {
644            fresh_index
645        } else if let Some(index) = old_index_by_id.get(child_id) {
646            *index
647        } else {
648            continue;
649        };
650        let RenderNode::Layer(layer) = &container.children[old_index] else {
651            return Err("retained child slot is not a layer");
652        };
653        if dirty_nodes.contains(child_id) {
654            continue;
655        }
656        if layer.has_origin_sinks {
657            return Err("child subtree publishes window origins");
658        }
659        if Rect::from_size(state.size()) != layer.node_rect() {
660            return Err("child resized");
661        }
662    }
663    Ok(())
664}
665
666#[derive(Clone, Copy)]
667struct TranslateGeometry {
668    content_offset: Point,
669    layer_translation: Point,
670    window_origin: Point,
671    child_origin: Point,
672}
673
674impl TranslateGeometry {
675    fn new(
676        layout_state: &cranpose_ui::widgets::LayoutState,
677        graphics_layer: &GraphicsLayer,
678        parent_abs: AbsOrigin,
679    ) -> Self {
680        let content_offset = layout_state.content_offset;
681        let top_left = Point {
682            x: parent_abs.content_origin.x + layout_state.position().x,
683            y: parent_abs.content_origin.y + layout_state.position().y,
684        };
685        let layer_translation = Point {
686            x: parent_abs.layer_translation.x + graphics_layer.translation_x,
687            y: parent_abs.layer_translation.y + graphics_layer.translation_y,
688        };
689        Self {
690            content_offset,
691            layer_translation,
692            window_origin: Point {
693                x: top_left.x + layer_translation.x,
694                y: top_left.y + layer_translation.y,
695            },
696            child_origin: Point {
697                x: top_left.x + content_offset.x,
698                y: top_left.y + content_offset.y,
699            },
700        }
701    }
702}
703
704fn build_entering_children(
705    applier: &mut MemoryApplier,
706    container: &LayerNode,
707    placed_fresh: &[(NodeId, cranpose_ui::widgets::LayoutState)],
708    retained: (bool, &std::collections::HashMap<NodeId, usize>),
709    geometry: TranslateGeometry,
710    inherited: (bool, bool),
711) -> std::collections::HashMap<NodeId, LayerNode> {
712    let (children_unchanged, old_index_by_id) = retained;
713    let (child_inherited_translated_content_context, children_ancestor_hashed) = inherited;
714    let mut entering: std::collections::HashMap<NodeId, LayerNode> =
715        std::collections::HashMap::new();
716    for (child_id, _) in placed_fresh {
717        if children_unchanged || old_index_by_id.contains_key(child_id) {
718            continue;
719        }
720        let Some(mut lowered) = build_layer_node_from_applier_internal(
721            applier,
722            *child_id,
723            container.motion_context_animated,
724            child_inherited_translated_content_context,
725            Some(AbsOrigin {
726                content_origin: geometry.child_origin,
727                layer_translation: geometry.layer_translation,
728            }),
729        ) else {
730            continue;
731        };
732        if geometry.content_offset != Point::default() {
733            lowered.transform_to_parent =
734                lowered
735                    .transform_to_parent
736                    .then(ProjectiveTransform::translation(
737                        geometry.content_offset.x,
738                        geometry.content_offset.y,
739                    ));
740        }
741        crate::graph_hash::recompute_layer_raster_cache_hashes_under(
742            &mut lowered,
743            children_ancestor_hashed,
744        );
745        entering.insert(*child_id, lowered);
746    }
747    entering
748}
749
750fn apply_translated_container_state(
751    container: &mut LayerNode,
752    modifier_slices: &ModifierNodeSlices,
753    layout_state: &cranpose_ui::widgets::LayoutState,
754    graphics_layer: &GraphicsLayer,
755    parent_content_offset: Point,
756    geometry: TranslateGeometry,
757) {
758    let mut transform = layer_transform_to_parent(
759        container.local_bounds,
760        layout_state.position(),
761        graphics_layer,
762    );
763    if parent_content_offset != Point::default() {
764        transform = transform.then(ProjectiveTransform::translation(
765            parent_content_offset.x,
766            parent_content_offset.y,
767        ));
768    }
769    container.transform_to_parent = transform;
770    container.content_offset = geometry.content_offset;
771    if container.translated_content_context {
772        container.translated_content_offset = modifier_slices
773            .translated_content_offset()
774            .unwrap_or(geometry.content_offset);
775    }
776    if let Some(sink) = modifier_slices.text_window_origin() {
777        sink.set(geometry.window_origin);
778    }
779    if let Some(sink) = modifier_slices.viewport_window_rect() {
780        sink.set(Rect {
781            x: geometry.window_origin.x,
782            y: geometry.window_origin.y,
783            width: layout_state.size().width,
784            height: layout_state.size().height,
785        });
786    }
787    container.origin_in_parent = layout_state.position();
788}
789
790fn reconcile_translated_children(
791    container: &mut LayerNode,
792    dirty_nodes: &mut HashSet<NodeId>,
793    changed_nodes: &mut Vec<NodeId>,
794    placed_fresh: &[(NodeId, cranpose_ui::widgets::LayoutState)],
795    children_unchanged: bool,
796    entering: &mut std::collections::HashMap<NodeId, LayerNode>,
797    geometry: TranslateGeometry,
798) {
799    if children_unchanged {
800        for (child, (child_id, state)) in container.children.iter_mut().zip(placed_fresh) {
801            let RenderNode::Layer(layer) = child else {
802                unreachable!("retained child identities were checked");
803            };
804            if !dirty_nodes.contains(child_id) {
805                translate_retained_child(layer, state, geometry.content_offset);
806                changed_nodes.push(*child_id);
807            }
808        }
809        return;
810    }
811    let fresh_id_set: HashSet<NodeId> = placed_fresh.iter().map(|(id, _)| *id).collect();
812    let mut old_by_id: std::collections::HashMap<NodeId, Box<LayerNode>> =
813        std::collections::HashMap::new();
814    for child in container.children.drain(..) {
815        let RenderNode::Layer(layer) = child else {
816            continue;
817        };
818        let child_id = layer_identity(&layer).expect("checked above");
819        if fresh_id_set.contains(&child_id) {
820            old_by_id.insert(child_id, layer);
821        } else {
822            collect_layer_node_ids(&layer, changed_nodes);
823        }
824    }
825    let mut new_children = Vec::with_capacity(placed_fresh.len());
826    for (child_id, state) in placed_fresh {
827        if let Some(mut layer) = old_by_id.remove(child_id) {
828            if !dirty_nodes.contains(child_id) {
829                translate_retained_child(&mut layer, state, geometry.content_offset);
830                changed_nodes.push(*child_id);
831            }
832            new_children.push(RenderNode::Layer(layer));
833        } else if let Some(lowered) = entering.remove(child_id) {
834            dirty_nodes.remove(child_id);
835            remove_dirty_descendants(&lowered, dirty_nodes);
836            collect_layer_node_ids(&lowered, changed_nodes);
837            new_children.push(RenderNode::Layer(Box::new(lowered)));
838        }
839    }
840    container.children = new_children;
841}
842
843fn translate_layer_from_data(
844    applier: &mut MemoryApplier,
845    container: &mut LayerNode,
846    dirty_nodes: &mut HashSet<NodeId>,
847    changed_nodes: &mut Vec<NodeId>,
848    ancestors: TranslateAncestorContext,
849    data: SnapshotNodeData,
850    wrapped: bool,
851) -> bool {
852    let TranslateAncestorContext {
853        inherited_motion_context_animated,
854        ancestor_hashed: container_ancestor_hashed,
855        inherited_translated_content_context,
856        parent_content_offset,
857        parent_abs,
858    } = ancestors;
859    let SnapshotNodeData {
860        layout_state,
861        modifier_slices,
862        children: fresh_children,
863        window_root,
864    } = data;
865    let layout_state = if window_root {
866        layout_state.at_origin()
867    } else {
868        layout_state
869    };
870    let container_plan = match translated_container(
871        container,
872        &layout_state,
873        &modifier_slices,
874        inherited_motion_context_animated,
875        wrapped,
876    ) {
877        Ok(plan) => plan,
878        Err(reason) => return translate_bail(reason),
879    };
880    let TranslatedContainer {
881        node_id,
882        graphics_layer,
883    } = container_plan;
884    let child_plan = match translated_children(applier, container, dirty_nodes, &fresh_children) {
885        Ok(plan) => plan,
886        Err(reason) => return translate_bail(reason),
887    };
888    let TranslatedChildren {
889        placed_fresh,
890        children_unchanged,
891        old_index_by_id,
892    } = child_plan;
893
894    let geometry = TranslateGeometry::new(&layout_state, &graphics_layer, parent_abs);
895    let child_inherited_translated_content_context =
896        inherited_translated_content_context || container.translated_content_context;
897    let children_ancestor_hashed =
898        crate::graph_hash::layer_children_ancestor_hashed(container, container_ancestor_hashed);
899    let mut entering = build_entering_children(
900        applier,
901        container,
902        &placed_fresh,
903        (children_unchanged, &old_index_by_id),
904        geometry,
905        (
906            child_inherited_translated_content_context,
907            children_ancestor_hashed,
908        ),
909    );
910
911    apply_translated_container_state(
912        container,
913        &modifier_slices,
914        &layout_state,
915        &graphics_layer,
916        parent_content_offset,
917        geometry,
918    );
919
920    reconcile_translated_children(
921        container,
922        dirty_nodes,
923        changed_nodes,
924        &placed_fresh,
925        children_unchanged,
926        &mut entering,
927        geometry,
928    );
929    modifier_slices.publish_pointer_input_size(layout_state.size());
930    container.hit_test = hit_test_from_slices(&modifier_slices);
931
932    container.has_hit_targets = container.hit_test.is_some()
933        || container.children.iter().any(|child| match child {
934            RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
935            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
936        });
937    container.has_origin_sinks = modifier_slices_have_origin_sinks(&modifier_slices)
938        || container.children.iter().any(|child| match child {
939            RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
940            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
941        });
942
943    container.draws_within_bounds = container.content_draws_within_bounds();
944    crate::graph_hash::refresh_layer_own_raster_cache_hashes(container, container_ancestor_hashed);
945    changed_nodes.push(node_id);
946    true
947}
948
949fn translate_retained_child(
950    layer: &mut LayerNode,
951    state: &cranpose_ui::widgets::LayoutState,
952    content_offset: Point,
953) {
954    let mut child_transform =
955        layer_transform_to_parent(layer.local_bounds, state.position(), &layer.graphics_layer);
956    if content_offset != Point::default() {
957        child_transform = child_transform.then(ProjectiveTransform::translation(
958            content_offset.x,
959            content_offset.y,
960        ));
961    }
962    layer.transform_to_parent = child_transform;
963    layer.origin_in_parent = state.position();
964}
965
966fn layer_hit_graph_state_dirty(previous: &LayerNode, replacement: &LayerNode) -> bool {
967    if previous.hit_test.is_some() || replacement.hit_test.is_some() {
968        return true;
969    }
970
971    if !(previous.has_hit_targets || replacement.has_hit_targets) {
972        return false;
973    }
974
975    previous.has_hit_targets != replacement.has_hit_targets
976        || previous.local_bounds != replacement.local_bounds
977        || previous.node_bounds != replacement.node_bounds
978        || previous.transform_to_parent != replacement.transform_to_parent
979        || previous.clip_rect() != replacement.clip_rect()
980        || previous.graphics_layer.shape != replacement.graphics_layer.shape
981}
982
983fn collect_layer_node_ids(layer: &LayerNode, out: &mut Vec<NodeId>) {
984    if let Some(node_id) = layer.node_id {
985        out.push(node_id);
986    }
987    for child in &layer.children {
988        if let RenderNode::Layer(child_layer) = child {
989            collect_layer_node_ids(child_layer, out);
990        }
991    }
992}
993
994fn remove_dirty_descendants(layer: &LayerNode, dirty_nodes: &mut HashSet<NodeId>) {
995    for child in &layer.children {
996        let RenderNode::Layer(child_layer) = child else {
997            continue;
998        };
999        if let Some(node_id) = child_layer.node_id {
1000            dirty_nodes.remove(&node_id);
1001        }
1002        remove_dirty_descendants(child_layer, dirty_nodes);
1003    }
1004}
1005
1006fn build_layer_node(
1007    snapshot: BuildNodeSnapshot,
1008    _root_scale: f32,
1009    inherited_motion_context_animated: bool,
1010) -> LayerNode {
1011    build_layer_node_internal(snapshot, inherited_motion_context_animated, false)
1012}
1013
1014fn build_layer_node_internal(
1015    snapshot: BuildNodeSnapshot,
1016    inherited_motion_context_animated: bool,
1017    inherited_translated_content_context: bool,
1018) -> LayerNode {
1019    let BuildNodeSnapshot {
1020        node_id,
1021        placement,
1022        size,
1023        content_offset,
1024        slices,
1025        graphics_layer,
1026        children: child_snapshots,
1027    } = snapshot;
1028    let motion_context_animated = slices.motion_context_animated();
1029    let translated_content_context = slices.translated_content_context();
1030    let has_own_origin_sinks = modifier_slices_have_origin_sinks(&slices);
1031    let measured_text_layout = slices.measured_text_layout();
1032    let draw_commands = slices.draw_commands();
1033    let outer_draw_command_count = slices.outer_draw_command_count();
1034    let clip_to_bounds = slices.clip_to_bounds();
1035    let text_style = slices.text_style();
1036    let text_layout_options = slices.text_layout_options();
1037    let text_pan = slices.text_pan_resolver();
1038    let outer = outer_draws(node_id, draw_commands, outer_draw_command_count, size);
1039    let layer_draw_commands = &draw_commands[outer_draw_command_count..];
1040    let (local_bounds, node_bounds) = layer_and_node_bounds(&slices, size);
1041    let graphics_layer = graphics_layer.unwrap_or_default();
1042    let transform_to_parent = layer_transform_to_parent(local_bounds, placement, &graphics_layer);
1043    let isolation = isolation_reasons(&graphics_layer);
1044    let cache_policy = layer_cache_policy(&graphics_layer, isolation);
1045    let shadow_clip = clip_to_bounds.then_some(local_bounds);
1046    let hit_test = hit_test_from_slices(&slices);
1047
1048    let node_motion_context_animated = inherited_motion_context_animated || motion_context_animated;
1049    let child_translated_content_context =
1050        inherited_translated_content_context || translated_content_context;
1051
1052    let mut children = Vec::with_capacity(layer_node_capacity(
1053        layer_draw_commands,
1054        child_snapshots.len(),
1055        measured_text_layout.is_some(),
1056    ));
1057    append_draw_nodes(
1058        &mut children,
1059        node_id,
1060        layer_draw_commands,
1061        outer_draw_command_count,
1062        DrawPlacement::Behind,
1063        size,
1064        PrimitivePhase::BeforeChildren,
1065    );
1066    if let Some(text) = text_node_from_parts(TextNodeParts {
1067        node_id,
1068        text_rect: slices.text_content_rect(size),
1069        text_style,
1070        text_layout_options,
1071        text_pan,
1072        measured_layout: measured_text_layout,
1073    }) {
1074        children.push(RenderNode::Primitive(PrimitiveEntry {
1075            phase: PrimitivePhase::BeforeChildren,
1076            node: PrimitiveNode::Text(Box::new(text)),
1077        }));
1078    }
1079    let child_motion_context_animated = node_motion_context_animated;
1080    for child in child_snapshots {
1081        let mut child_layer = build_layer_node_internal(
1082            child,
1083            child_motion_context_animated,
1084            child_translated_content_context,
1085        );
1086        if content_offset != Point::default() {
1087            child_layer.transform_to_parent =
1088                child_layer
1089                    .transform_to_parent
1090                    .then(ProjectiveTransform::translation(
1091                        content_offset.x,
1092                        content_offset.y,
1093                    ));
1094        }
1095        children.push(RenderNode::Layer(Box::new(child_layer)));
1096    }
1097    append_draw_nodes(
1098        &mut children,
1099        node_id,
1100        layer_draw_commands,
1101        outer_draw_command_count,
1102        DrawPlacement::Overlay,
1103        size,
1104        PrimitivePhase::AfterChildren,
1105    );
1106    let has_hit_targets = hit_test.is_some()
1107        || children.iter().any(|child| match child {
1108            RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
1109            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1110        });
1111    let has_origin_sinks = has_own_origin_sinks
1112        || children.iter().any(|child| match child {
1113            RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
1114            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1115        });
1116
1117    let layer = LayerNode {
1118        node_id: Some(node_id),
1119        wraps: None,
1120        local_bounds,
1121        node_bounds,
1122        transform_to_parent,
1123        content_offset,
1124        motion_context_animated: node_motion_context_animated,
1125        translated_content_context,
1126        translated_content_offset: if translated_content_context {
1127            content_offset
1128        } else {
1129            Point::default()
1130        },
1131        origin_in_parent: placement,
1132        graphics_layer,
1133        clip_to_bounds,
1134        shadow_clip,
1135        hit_test,
1136        has_hit_targets,
1137        has_origin_sinks,
1138        draws_within_bounds: false,
1139        isolation,
1140        cache_policy,
1141        cache_hashes: LayerRasterCacheHashes::default(),
1142        cache_hashes_valid: false,
1143        children,
1144    };
1145    finish_layer(layer, placement, outer)
1146}
1147
1148#[derive(Clone, Copy)]
1149struct AbsOrigin {
1150    content_origin: Point,
1151    layer_translation: Point,
1152}
1153
1154impl AbsOrigin {
1155    const ROOT: AbsOrigin = AbsOrigin {
1156        content_origin: Point { x: 0.0, y: 0.0 },
1157        layer_translation: Point { x: 0.0, y: 0.0 },
1158    };
1159
1160    fn children_of(self, layer: &LayerNode) -> AbsOrigin {
1161        AbsOrigin {
1162            content_origin: Point {
1163                x: self.content_origin.x + layer.origin_in_parent.x + layer.content_offset.x,
1164                y: self.content_origin.y + layer.origin_in_parent.y + layer.content_offset.y,
1165            },
1166            layer_translation: Point {
1167                x: self.layer_translation.x + layer.graphics_layer.translation_x,
1168                y: self.layer_translation.y + layer.graphics_layer.translation_y,
1169            },
1170        }
1171    }
1172}
1173
1174fn build_layer_node_from_applier(
1175    applier: &mut MemoryApplier,
1176    node_id: NodeId,
1177    _root_scale: f32,
1178    inherited_motion_context_animated: bool,
1179) -> Option<LayerNode> {
1180    let mut data = snapshot_node_data(applier, node_id)?;
1181    if data.window_root {
1182        data.layout_state = data.layout_state.at_origin();
1183    }
1184    build_layer_node_from_data(
1185        applier,
1186        node_id,
1187        data,
1188        inherited_motion_context_animated,
1189        false,
1190        Some(AbsOrigin::ROOT),
1191    )
1192}
1193
1194fn snapshot_node_data(applier: &mut MemoryApplier, node_id: NodeId) -> Option<SnapshotNodeData> {
1195    if let Ok(data) = applier.with_node::<LayoutNode, _>(node_id, |node| {
1196        let state = node.layout_state();
1197        let mut children = SmallVec::new();
1198        node.collect_children_into(&mut children);
1199        let modifier_slices = node.modifier_slices_snapshot();
1200        SnapshotNodeData {
1201            layout_state: state,
1202            modifier_slices,
1203            children,
1204            window_root: node.is_window_root(),
1205        }
1206    }) {
1207        return Some(data);
1208    }
1209
1210    applier
1211        .with_node::<SubcomposeLayoutNode, _>(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: false,
1221            }
1222        })
1223        .ok()
1224}
1225
1226fn build_layer_node_from_applier_internal(
1227    applier: &mut MemoryApplier,
1228    node_id: NodeId,
1229    inherited_motion_context_animated: bool,
1230    inherited_translated_content_context: bool,
1231    parent_abs: Option<AbsOrigin>,
1232) -> Option<LayerNode> {
1233    let data = snapshot_node_data(applier, node_id)?;
1234    if data.window_root {
1235        return None;
1236    }
1237    build_layer_node_from_data(
1238        applier,
1239        node_id,
1240        data,
1241        inherited_motion_context_animated,
1242        inherited_translated_content_context,
1243        parent_abs,
1244    )
1245}
1246
1247fn hit_test_from_slices(slices: &Rc<ModifierNodeSlices>) -> Option<HitTestNode> {
1248    slices_hit_something(slices).then(|| HitTestNode {
1249        shape: None,
1250        handlers: Rc::clone(slices),
1251    })
1252}
1253
1254/// A node's layer bounds and, when they differ from it, its own rect (see
1255/// [`LayerNode::node_bounds`]).
1256fn layer_and_node_bounds(slices: &ModifierNodeSlices, size: Size) -> (Rect, Option<Rect>) {
1257    let node_bounds = Rect::from_size(size);
1258    let layer_bounds = slices.layer_bounds(size);
1259    (
1260        layer_bounds,
1261        (layer_bounds != node_bounds).then_some(node_bounds),
1262    )
1263}
1264
1265/// Whether a node's slices make it a hit target: a pointer input or a
1266/// pointer icon.
1267fn slices_hit_something(slices: &ModifierNodeSlices) -> bool {
1268    !slices.pointer_inputs().is_empty() || slices.pointer_icon().is_some()
1269}
1270
1271fn build_layer_node_from_data(
1272    applier: &mut MemoryApplier,
1273    node_id: NodeId,
1274    data: SnapshotNodeData,
1275    inherited_motion_context_animated: bool,
1276    inherited_translated_content_context: bool,
1277    parent_abs: Option<AbsOrigin>,
1278) -> Option<LayerNode> {
1279    note_layer_lowered();
1280    let SnapshotNodeData {
1281        layout_state,
1282        modifier_slices,
1283        children,
1284        window_root: _,
1285    } = data;
1286    if !layout_state.is_placed() {
1287        return None;
1288    }
1289
1290    let (local_bounds, node_bounds) = layer_and_node_bounds(&modifier_slices, layout_state.size());
1291    if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
1292        eprintln!(
1293            "[scene-update-diag] build layer node={node_id:?} size=({:.2},{:.2}) pos=({:.2},{:.2})",
1294            layout_state.size().width,
1295            layout_state.size().height,
1296            layout_state.position().x,
1297            layout_state.position().y,
1298        );
1299    }
1300    let clip_to_bounds = modifier_slices.clip_to_bounds();
1301    let graphics_layer = graphics_layer_with_shaped_clip(
1302        modifier_slices.graphics_layer().unwrap_or_default(),
1303        clip_to_bounds,
1304        modifier_slices.corner_shape(),
1305        local_bounds,
1306    );
1307    let transform_to_parent =
1308        layer_transform_to_parent(local_bounds, layout_state.position(), &graphics_layer);
1309    let isolation = isolation_reasons(&graphics_layer);
1310    let cache_policy = layer_cache_policy(&graphics_layer, isolation);
1311    let shadow_clip = clip_to_bounds.then_some(local_bounds);
1312    let hit_test = hit_test_from_slices(&modifier_slices);
1313
1314    modifier_slices.publish_pointer_input_size(layout_state.size());
1315
1316    let node_motion_context_animated =
1317        inherited_motion_context_animated || modifier_slices.motion_context_animated();
1318    let local_translated_content_context = modifier_slices.translated_content_context();
1319    let local_translated_content_offset = modifier_slices
1320        .translated_content_offset()
1321        .unwrap_or(layout_state.content_offset);
1322    let child_translated_content_context =
1323        inherited_translated_content_context || local_translated_content_context;
1324
1325    let this_abs = parent_abs.map(|parent| {
1326        let top_left = Point {
1327            x: parent.content_origin.x + layout_state.position().x,
1328            y: parent.content_origin.y + layout_state.position().y,
1329        };
1330        let layer_translation = Point {
1331            x: parent.layer_translation.x + graphics_layer.translation_x,
1332            y: parent.layer_translation.y + graphics_layer.translation_y,
1333        };
1334        (top_left, layer_translation)
1335    });
1336    if let Some((top_left, layer_translation)) = this_abs {
1337        let window_origin = Point {
1338            x: top_left.x + layer_translation.x,
1339            y: top_left.y + layer_translation.y,
1340        };
1341        if let Some(sink) = modifier_slices.text_window_origin() {
1342            sink.set(window_origin);
1343        }
1344        if let Some(sink) = modifier_slices.viewport_window_rect() {
1345            sink.set(Rect {
1346                x: window_origin.x,
1347                y: window_origin.y,
1348                width: layout_state.size().width,
1349                height: layout_state.size().height,
1350            });
1351        }
1352    }
1353    let child_abs = this_abs.map(|(top_left, layer_translation)| AbsOrigin {
1354        content_origin: Point {
1355            x: top_left.x + layout_state.content_offset.x,
1356            y: top_left.y + layout_state.content_offset.y,
1357        },
1358        layer_translation,
1359    });
1360
1361    let outer_draw_command_count = modifier_slices.outer_draw_command_count();
1362    let outer = outer_draws(
1363        node_id,
1364        modifier_slices.draw_commands(),
1365        outer_draw_command_count,
1366        layout_state.size(),
1367    );
1368    let layer_draw_commands = &modifier_slices.draw_commands()[outer_draw_command_count..];
1369    let mut render_children = Vec::with_capacity(layer_node_capacity(
1370        layer_draw_commands,
1371        children.len(),
1372        modifier_slices.annotated_text().is_some(),
1373    ));
1374    append_draw_nodes(
1375        &mut render_children,
1376        node_id,
1377        layer_draw_commands,
1378        outer_draw_command_count,
1379        DrawPlacement::Behind,
1380        layout_state.size(),
1381        PrimitivePhase::BeforeChildren,
1382    );
1383    if let Some(text) = text_node_from_parts(TextNodeParts {
1384        node_id,
1385        text_rect: modifier_slices.text_content_rect(layout_state.size()),
1386        text_style: modifier_slices.text_style(),
1387        text_layout_options: modifier_slices.text_layout_options(),
1388        text_pan: modifier_slices.text_pan_resolver(),
1389        measured_layout: modifier_slices.measured_text_layout(),
1390    }) {
1391        render_children.push(RenderNode::Primitive(PrimitiveEntry {
1392            phase: PrimitivePhase::BeforeChildren,
1393            node: PrimitiveNode::Text(Box::new(text)),
1394        }));
1395    }
1396    let child_motion_context_animated = node_motion_context_animated;
1397    for child_id in children {
1398        let Some(mut child_layer) = build_layer_node_from_applier_internal(
1399            applier,
1400            child_id,
1401            child_motion_context_animated,
1402            child_translated_content_context,
1403            child_abs,
1404        ) else {
1405            continue;
1406        };
1407        if layout_state.content_offset != Point::default() {
1408            child_layer.transform_to_parent =
1409                child_layer
1410                    .transform_to_parent
1411                    .then(ProjectiveTransform::translation(
1412                        layout_state.content_offset.x,
1413                        layout_state.content_offset.y,
1414                    ));
1415        }
1416        render_children.push(RenderNode::Layer(Box::new(child_layer)));
1417    }
1418    append_draw_nodes(
1419        &mut render_children,
1420        node_id,
1421        layer_draw_commands,
1422        outer_draw_command_count,
1423        DrawPlacement::Overlay,
1424        layout_state.size(),
1425        PrimitivePhase::AfterChildren,
1426    );
1427    let has_hit_targets = hit_test.is_some()
1428        || render_children.iter().any(|child| match child {
1429            RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
1430            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1431        });
1432    let has_origin_sinks = modifier_slices_have_origin_sinks(&modifier_slices)
1433        || render_children.iter().any(|child| match child {
1434            RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
1435            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1436        });
1437
1438    let layer = LayerNode {
1439        node_id: Some(node_id),
1440        wraps: None,
1441        local_bounds,
1442        node_bounds,
1443        transform_to_parent,
1444        content_offset: layout_state.content_offset,
1445        motion_context_animated: node_motion_context_animated,
1446        translated_content_context: local_translated_content_context,
1447        translated_content_offset: if local_translated_content_context {
1448            local_translated_content_offset
1449        } else {
1450            Point::default()
1451        },
1452        origin_in_parent: layout_state.position(),
1453        graphics_layer,
1454        clip_to_bounds,
1455        shadow_clip,
1456        hit_test,
1457        has_hit_targets,
1458        has_origin_sinks,
1459        draws_within_bounds: false,
1460        isolation,
1461        cache_policy,
1462        cache_hashes: LayerRasterCacheHashes::default(),
1463        cache_hashes_valid: false,
1464        children: render_children,
1465    };
1466    Some(finish_layer(layer, layout_state.position(), outer))
1467}
1468
1469struct RecorderSlot {
1470    generation: u64,
1471    handles: [Option<Rc<CommandRecording>>; 2],
1472}
1473
1474thread_local! {
1475    static COMMAND_RECORDINGS: std::cell::RefCell<
1476        std::collections::HashMap<DrawCommandId, RecorderSlot, cranpose_ui_graphics::FxBuildHasher>,
1477    > = std::cell::RefCell::new(std::collections::HashMap::default());
1478    static RECORDING_GENERATION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
1479}
1480
1481#[doc(hidden)]
1482pub fn clear_command_recordings_for_tests() {
1483    COMMAND_RECORDINGS.with(|map| map.borrow_mut().clear());
1484}
1485
1486fn bump_recording_generation() {
1487    let generation = RECORDING_GENERATION.with(|cell| {
1488        let next = cell.get().wrapping_add(1);
1489        cell.set(next);
1490        next
1491    });
1492    if generation.is_multiple_of(512) {
1493        COMMAND_RECORDINGS.with(|map| {
1494            map.borrow_mut()
1495                .retain(|_, slot| generation.wrapping_sub(slot.generation) <= 64);
1496        });
1497    }
1498}
1499
1500fn acquire_storage(id: DrawCommandId) -> CommandRecording {
1501    COMMAND_RECORDINGS.with(|map| {
1502        let mut map = map.borrow_mut();
1503        let Some(slot) = map.get_mut(&id) else {
1504            return CommandRecording::default();
1505        };
1506        for handle in &mut slot.handles {
1507            if handle
1508                .as_ref()
1509                .is_some_and(|shared| Rc::strong_count(shared) == 1)
1510            {
1511                let shared = handle.take().expect("checked some above");
1512                return Rc::try_unwrap(shared).expect("sole owner checked above");
1513            }
1514        }
1515        CommandRecording::default()
1516    })
1517}
1518
1519fn publish_recording(id: DrawCommandId, recording: CommandRecording) -> Rc<CommandRecording> {
1520    let shared = Rc::new(recording);
1521    COMMAND_RECORDINGS.with(|map| {
1522        let mut map = map.borrow_mut();
1523        let generation = RECORDING_GENERATION.with(Cell::get);
1524        let slot = map.entry(id).or_insert_with(|| RecorderSlot {
1525            generation,
1526            handles: [None, None],
1527        });
1528        slot.generation = generation;
1529        slot.handles[1] = slot.handles[0].take();
1530        slot.handles[0] = Some(shared.clone());
1531    });
1532    shared
1533}
1534
1535fn layer_node_capacity(commands: &[DrawCommand], children: usize, has_text: bool) -> usize {
1536    children
1537        + usize::from(has_text)
1538        + commands.len()
1539        + commands
1540            .iter()
1541            .filter(|command| matches!(command, DrawCommand::WithContent(_)))
1542            .count()
1543}
1544
1545fn draw_nodes(
1546    node_id: NodeId,
1547    commands: &[DrawCommand],
1548    first_command_index: usize,
1549    placement: DrawPlacement,
1550    size: Size,
1551    phase: PrimitivePhase,
1552) -> Vec<RenderNode> {
1553    let mut nodes = Vec::new();
1554    append_draw_nodes(
1555        &mut nodes,
1556        node_id,
1557        commands,
1558        first_command_index,
1559        placement,
1560        size,
1561        phase,
1562    );
1563    nodes
1564}
1565
1566fn append_draw_nodes(
1567    nodes: &mut Vec<RenderNode>,
1568    node_id: NodeId,
1569    commands: &[DrawCommand],
1570    first_command_index: usize,
1571    placement: DrawPlacement,
1572    size: Size,
1573    phase: PrimitivePhase,
1574) {
1575    for (command_index, command) in commands.iter().enumerate() {
1576        let id = DrawCommandId {
1577            node_id,
1578            command_index: (first_command_index + command_index) as u32,
1579            placement,
1580        };
1581        let Some((recording, segments)) =
1582            recording_for_placement_reusing(command, placement, size, || acquire_storage(id))
1583        else {
1584            retain_empty_draw_command(nodes, phase, id, placement, command);
1585            continue;
1586        };
1587        let shared = publish_recording(id, recording);
1588        if shared.is_empty_in(&segments) {
1589            retain_empty_draw_command(nodes, phase, id, placement, command);
1590            continue;
1591        }
1592        nodes.push(RenderNode::DrawRun(DrawRunNode::for_command_shared(
1593            phase,
1594            Some(id),
1595            shared,
1596            segments,
1597        )));
1598    }
1599}
1600
1601fn retain_empty_draw_command(
1602    nodes: &mut Vec<RenderNode>,
1603    phase: PrimitivePhase,
1604    id: DrawCommandId,
1605    placement: DrawPlacement,
1606    command: &DrawCommand,
1607) {
1608    if matches!(
1609        (placement, command),
1610        (DrawPlacement::Behind, DrawCommand::Behind(_))
1611            | (DrawPlacement::Overlay, DrawCommand::Overlay(_))
1612            | (_, DrawCommand::WithContent(_))
1613    ) {
1614        nodes.push(RenderNode::DrawRun(DrawRunNode::for_command(
1615            phase,
1616            Some(id),
1617            Vec::new(),
1618        )));
1619    }
1620}
1621
1622#[doc(hidden)]
1623pub fn draw_command_nodes_for_tests(
1624    node_id: NodeId,
1625    commands: &[DrawCommand],
1626    placement: DrawPlacement,
1627    size: Size,
1628    phase: PrimitivePhase,
1629) -> Vec<RenderNode> {
1630    bump_recording_generation();
1631    draw_nodes(node_id, commands, 0, placement, size, phase)
1632}
1633
1634struct OuterDraws {
1635    behind: Vec<RenderNode>,
1636    overlay: Vec<RenderNode>,
1637}
1638
1639fn outer_draws(
1640    node_id: NodeId,
1641    draw_commands: &[DrawCommand],
1642    outer_draw_command_count: usize,
1643    size: Size,
1644) -> Option<OuterDraws> {
1645    (outer_draw_command_count > 0).then(|| {
1646        let commands = &draw_commands[..outer_draw_command_count];
1647        OuterDraws {
1648            behind: draw_nodes(
1649                node_id,
1650                commands,
1651                0,
1652                DrawPlacement::Behind,
1653                size,
1654                PrimitivePhase::BeforeChildren,
1655            ),
1656            overlay: draw_nodes(
1657                node_id,
1658                commands,
1659                0,
1660                DrawPlacement::Overlay,
1661                size,
1662                PrimitivePhase::AfterChildren,
1663            ),
1664        }
1665    })
1666}
1667
1668fn finish_layer(mut layer: LayerNode, placement: Point, outer: Option<OuterDraws>) -> LayerNode {
1669    layer.draws_within_bounds = layer.content_draws_within_bounds();
1670    match outer {
1671        Some(outer) => wrap_layer_with_outer_draws(layer, placement, outer),
1672        None => layer,
1673    }
1674}
1675
1676fn wrap_layer_with_outer_draws(
1677    mut layer: LayerNode,
1678    placement: Point,
1679    outer: OuterDraws,
1680) -> LayerNode {
1681    layer.transform_to_parent =
1682        layer_transform_to_parent(layer.local_bounds, Point::default(), &layer.graphics_layer);
1683    layer.origin_in_parent = Point::default();
1684    // The outer draws sit on the node's rect, outside its layer.
1685    let node_rect = layer.node_rect();
1686    let wrapper = LayerNode {
1687        wraps: layer.node_id,
1688        local_bounds: node_rect,
1689        transform_to_parent: layer_transform_to_parent(
1690            node_rect,
1691            placement,
1692            &GraphicsLayer::default(),
1693        ),
1694        origin_in_parent: placement,
1695        motion_context_animated: layer.motion_context_animated,
1696        has_hit_targets: layer.has_hit_targets,
1697        has_origin_sinks: layer.has_origin_sinks,
1698        ..Default::default()
1699    };
1700    let mut children = outer.behind;
1701    children.push(RenderNode::Layer(Box::new(layer)));
1702    children.extend(outer.overlay);
1703    let mut wrapper = LayerNode {
1704        children,
1705        ..wrapper
1706    };
1707    wrapper.draws_within_bounds = wrapper.content_draws_within_bounds();
1708    wrapper
1709}
1710
1711fn layer_identity(layer: &LayerNode) -> Option<NodeId> {
1712    layer.node_id.or(layer.wraps)
1713}
1714
1715struct TextNodeParts<'a> {
1716    node_id: NodeId,
1717    /// Where the text node was placed in its layout node.
1718    text_rect: Rect,
1719    text_style: Option<&'a TextStyle>,
1720    text_layout_options: Option<TextLayoutOptions>,
1721    text_pan: Option<TextPanResolver>,
1722    measured_layout: Option<Rc<PreparedTextLayout>>,
1723}
1724
1725fn text_node_from_parts(parts: TextNodeParts<'_>) -> Option<TextPrimitiveNode> {
1726    let TextNodeParts {
1727        node_id,
1728        text_rect,
1729        text_style,
1730        text_layout_options,
1731        text_pan,
1732        measured_layout,
1733    } = parts;
1734    let prepared = measured_layout?;
1735    let default_text_style;
1736    let text_style = match text_style {
1737        Some(style) => style,
1738        None => {
1739            default_text_style = TextStyle::default();
1740            &default_text_style
1741        }
1742    };
1743    let options = text_layout_options.unwrap_or_default().normalized();
1744    let content_width = text_rect.width.max(0.0);
1745    if content_width <= 0.0 {
1746        return None;
1747    }
1748
1749    let pan_offset = text_pan
1750        .as_ref()
1751        .map_or(0.0, |resolve| resolve(content_width));
1752    let pans_horizontally = text_pan.is_some();
1753
1754    let visual_style = &prepared.visual_style;
1755    let measured_draw_width = prepared.metrics.width.max(0.0);
1756    let draw_width = if options.overflow == TextOverflow::Visible || pans_horizontally {
1757        measured_draw_width
1758    } else {
1759        measured_draw_width.min(content_width)
1760    };
1761    let alignment_offset = resolve_text_horizontal_offset(
1762        text_style,
1763        prepared.text.text.as_str(),
1764        content_width,
1765        prepared.metrics.width,
1766    );
1767    let rect = Rect {
1768        x: text_rect.x + alignment_offset - pan_offset,
1769        y: text_rect.y,
1770        width: draw_width,
1771        height: prepared.metrics.height,
1772    };
1773    let text_bounds = Rect {
1774        width: content_width,
1775        height: text_rect.height.max(0.0),
1776        ..text_rect
1777    };
1778    let font_size = visual_style.resolve_font_size(14.0);
1779    let expanded_bounds =
1780        expand_text_bounds_for_baseline_shift(text_bounds, visual_style, font_size);
1781    let clip = if options.overflow == TextOverflow::Visible && !pans_horizontally {
1782        None
1783    } else {
1784        Some(pad_clip_rect(expanded_bounds))
1785    };
1786
1787    Some(TextPrimitiveNode {
1788        node_id,
1789        rect,
1790        text: Rc::clone(&prepared.text),
1791        render_text: prepared.render_text(),
1792        text_style: std::sync::Arc::clone(visual_style),
1793        font_size,
1794        layout_options: options,
1795        clip,
1796    })
1797}
1798
1799fn layout_box_to_snapshot(node: &LayoutBox, parent: Option<&LayoutBox>) -> BuildNodeSnapshot {
1800    let placement = parent
1801        .map(|parent_box| Point {
1802            x: node.rect.x - parent_box.rect.x - parent_box.content_offset.x,
1803            y: node.rect.y - parent_box.rect.y - parent_box.content_offset.y,
1804        })
1805        .unwrap_or_default();
1806    let mut children = Vec::with_capacity(node.children.len());
1807    for child in &node.children {
1808        children.push(layout_box_to_snapshot(child, Some(node)));
1809    }
1810    let base_graphics_layer = node.node_data.modifier_slices.graphics_layer();
1811    let graphics_layer = graphics_layer_with_shaped_clip(
1812        base_graphics_layer.clone().unwrap_or_default(),
1813        node.node_data.modifier_slices.clip_to_bounds(),
1814        node.node_data.modifier_slices.corner_shape(),
1815        Rect {
1816            x: 0.0,
1817            y: 0.0,
1818            width: node.rect.width,
1819            height: node.rect.height,
1820        },
1821    );
1822    let has_graphics_layer =
1823        base_graphics_layer.is_some() || graphics_layer.render_effect.is_some();
1824
1825    BuildNodeSnapshot {
1826        node_id: node.node_id,
1827        placement,
1828        size: Size {
1829            width: node.rect.width,
1830            height: node.rect.height,
1831        },
1832        content_offset: node.content_offset,
1833        slices: Rc::clone(&node.node_data.modifier_slices),
1834        graphics_layer: has_graphics_layer.then_some(graphics_layer),
1835        children,
1836    }
1837}
1838
1839fn modifier_slices_have_origin_sinks(slices: &ModifierNodeSlices) -> bool {
1840    slices.text_window_origin().is_some() || slices.viewport_window_rect().is_some()
1841}
1842
1843fn graphics_layer_with_shaped_clip(
1844    mut graphics_layer: GraphicsLayer,
1845    clip_to_bounds: bool,
1846    corner_shape: Option<RoundedCornerShape>,
1847    local_bounds: Rect,
1848) -> GraphicsLayer {
1849    if !clip_to_bounds {
1850        return graphics_layer;
1851    }
1852
1853    let Some(corner_shape) = corner_shape else {
1854        return graphics_layer;
1855    };
1856    let radii = corner_shape.resolve(local_bounds.width, local_bounds.height);
1857    if radii.top_left <= f32::EPSILON
1858        && radii.top_right <= f32::EPSILON
1859        && radii.bottom_right <= f32::EPSILON
1860        && radii.bottom_left <= f32::EPSILON
1861    {
1862        return graphics_layer;
1863    }
1864
1865    if let Some(existing) = graphics_layer.render_effect.take() {
1866        let rounded_clip = rounded_corner_alpha_mask_effect(
1867            local_bounds.width,
1868            local_bounds.height,
1869            radii,
1870            ROUNDED_CLIP_EDGE_FEATHER,
1871        );
1872        graphics_layer.render_effect = Some(existing.then(rounded_clip));
1873    } else {
1874        graphics_layer.shape = LayerShape::Rounded(corner_shape);
1875        graphics_layer.clip = true;
1876    }
1877    graphics_layer
1878}
1879
1880fn layer_cache_policy(layer: &GraphicsLayer, isolation: IsolationReasons) -> CachePolicy {
1881    if isolation.has_any() || layer_scales_or_rotates(layer) {
1882        CachePolicy::Auto
1883    } else {
1884        CachePolicy::None
1885    }
1886}
1887
1888fn isolation_reasons(layer: &GraphicsLayer) -> IsolationReasons {
1889    IsolationReasons {
1890        explicit_offscreen: layer.compositing_strategy == CompositingStrategy::Offscreen,
1891        shape_clip: layer.clip && !matches!(layer.shape, LayerShape::Rectangle),
1892        effect: layer.render_effect.is_some(),
1893        backdrop: layer.backdrop_effect.is_some(),
1894        group_opacity: layer.compositing_strategy != CompositingStrategy::ModulateAlpha
1895            && layer.alpha < 1.0,
1896        blend_mode: layer.blend_mode != cranpose_ui::BlendMode::SrcOver,
1897    }
1898}
1899
1900fn pad_clip_rect(rect: Rect) -> Rect {
1901    Rect {
1902        x: rect.x - TEXT_CLIP_PAD,
1903        y: rect.y - TEXT_CLIP_PAD,
1904        width: (rect.width + TEXT_CLIP_PAD * 2.0).max(0.0),
1905        height: (rect.height + TEXT_CLIP_PAD * 2.0).max(0.0),
1906    }
1907}
1908
1909pub fn expand_text_bounds_for_baseline_shift(
1910    text_bounds: Rect,
1911    text_style: &TextStyle,
1912    font_size: f32,
1913) -> Rect {
1914    let baseline_shift_px = text_style
1915        .span_style
1916        .baseline_shift
1917        .filter(|shift| shift.is_specified())
1918        .map_or(0.0, |shift| -(shift.0 * font_size));
1919    if baseline_shift_px == 0.0 {
1920        return text_bounds;
1921    }
1922
1923    if baseline_shift_px < 0.0 {
1924        Rect {
1925            x: text_bounds.x,
1926            y: text_bounds.y + baseline_shift_px,
1927            width: text_bounds.width,
1928            height: (text_bounds.height - baseline_shift_px).max(0.0),
1929        }
1930    } else {
1931        Rect {
1932            x: text_bounds.x,
1933            y: text_bounds.y,
1934            width: text_bounds.width,
1935            height: (text_bounds.height + baseline_shift_px).max(0.0),
1936        }
1937    }
1938}
1939
1940fn resolve_text_horizontal_offset(
1941    text_style: &TextStyle,
1942    text: &str,
1943    content_width: f32,
1944    measured_width: f32,
1945) -> f32 {
1946    let remaining = (content_width - measured_width).max(0.0);
1947    remaining * cranpose_ui::text::text_align_fraction(text_style, text)
1948}
1949
1950#[cfg(test)]
1951#[path = "tests/scene_builder_tests.rs"]
1952mod tests;