Skip to main content

cranpose_render_common/
scene_builder.rs

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