Skip to main content

cranpose_render_common/
scene_builder.rs

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