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, Rect, ResolvedModifiers, Size,
6    SubcomposeLayoutNode, TextLayoutOptions, TextOverflow, TextPanResolver, prepare_text_layout,
7    text::{AnnotatedString, TextAlign, TextStyle, resolve_text_direction},
8};
9use cranpose_ui_graphics::{
10    CommandRecording, CompositingStrategy, GraphicsLayer, LayerShape, PointerIcon,
11    RoundedCornerShape, rounded_corner_alpha_mask_effect,
12};
13use smallvec::SmallVec;
14
15use crate::{
16    graph::{
17        CachePolicy, DrawCommandId, DrawRunNode, HitTestNode, IsolationReasons, LayerNode,
18        PrimitiveEntry, PrimitiveNode, PrimitivePhase, ProjectiveTransform, RenderGraph,
19        RenderNode, TextPrimitiveNode,
20    },
21    layer_transform::layer_transform_to_parent,
22    raster_cache::LayerRasterCacheHashes,
23    style_shared::{DrawPlacement, recording_for_placement_reusing},
24};
25
26const TEXT_CLIP_PAD: f32 = 1.0;
27const ROUNDED_CLIP_EDGE_FEATHER: f32 = 1.0;
28
29#[derive(Clone, Default)]
30struct BuildNodeSnapshot {
31    node_id: NodeId,
32    placement: Point,
33    size: Size,
34    content_offset: Point,
35    motion_context_animated: bool,
36    translated_content_context: bool,
37    has_own_origin_sinks: bool,
38    measured_max_width: Option<f32>,
39    resolved_modifiers: ResolvedModifiers,
40    draw_commands: Vec<DrawCommand>,
41    outer_draw_command_count: usize,
42    click_actions: Vec<Rc<dyn Fn(Point)>>,
43    pointer_inputs: Vec<Rc<dyn Fn(cranpose_foundation::PointerEvent)>>,
44    pointer_icon: Option<PointerIcon>,
45    clip_to_bounds: bool,
46    annotated_text: Option<AnnotatedString>,
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_max_width,
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        annotated_text,
1095        text_style,
1096        text_layout_options,
1097        text_pan,
1098        graphics_layer,
1099        children: child_snapshots,
1100    } = snapshot;
1101    let outer = outer_draws(node_id, &draw_commands, outer_draw_command_count, size);
1102    let layer_draw_commands = &draw_commands[outer_draw_command_count..];
1103    let local_bounds = Rect {
1104        x: 0.0,
1105        y: 0.0,
1106        width: size.width,
1107        height: size.height,
1108    };
1109    let graphics_layer = graphics_layer.unwrap_or_default();
1110    let transform_to_parent = layer_transform_to_parent(local_bounds, placement, &graphics_layer);
1111    let isolation = isolation_reasons(&graphics_layer);
1112    let cache_policy = if isolation.has_any() {
1113        CachePolicy::Auto
1114    } else {
1115        CachePolicy::None
1116    };
1117    let shadow_clip = clip_to_bounds.then_some(local_bounds);
1118    let hit_test = (!click_actions.is_empty()
1119        || !pointer_inputs.is_empty()
1120        || pointer_icon.is_some())
1121    .then(|| HitTestNode {
1122        shape: None,
1123        click_actions,
1124        pointer_inputs,
1125        pointer_icon,
1126        clip: (clip_to_bounds || graphics_layer.clip).then_some(local_bounds),
1127    });
1128
1129    let node_motion_context_animated = inherited_motion_context_animated || motion_context_animated;
1130    let child_translated_content_context =
1131        inherited_translated_content_context || translated_content_context;
1132
1133    let mut children = Vec::with_capacity(layer_node_capacity(
1134        layer_draw_commands,
1135        child_snapshots.len(),
1136        annotated_text.is_some(),
1137    ));
1138    append_draw_nodes(
1139        &mut children,
1140        node_id,
1141        layer_draw_commands,
1142        outer_draw_command_count,
1143        DrawPlacement::Behind,
1144        size,
1145        PrimitivePhase::BeforeChildren,
1146    );
1147    if let Some(text) = text_node_from_parts(TextNodeParts {
1148        node_id,
1149        local_bounds,
1150        measured_max_width,
1151        resolved_modifiers: &resolved_modifiers,
1152        annotated_text: annotated_text.as_ref(),
1153        text_style: text_style.as_ref(),
1154        text_layout_options,
1155        text_pan,
1156        modifier_slices: None,
1157    }) {
1158        children.push(RenderNode::Primitive(PrimitiveEntry {
1159            phase: PrimitivePhase::BeforeChildren,
1160            node: PrimitiveNode::Text(Box::new(text)),
1161        }));
1162    }
1163    let child_motion_context_animated = node_motion_context_animated;
1164    for child in child_snapshots {
1165        let mut child_layer = build_layer_node_internal(
1166            child,
1167            child_motion_context_animated,
1168            child_translated_content_context,
1169        );
1170        if content_offset != Point::default() {
1171            child_layer.transform_to_parent =
1172                child_layer
1173                    .transform_to_parent
1174                    .then(ProjectiveTransform::translation(
1175                        content_offset.x,
1176                        content_offset.y,
1177                    ));
1178        }
1179        children.push(RenderNode::Layer(Box::new(child_layer)));
1180    }
1181    append_draw_nodes(
1182        &mut children,
1183        node_id,
1184        layer_draw_commands,
1185        outer_draw_command_count,
1186        DrawPlacement::Overlay,
1187        size,
1188        PrimitivePhase::AfterChildren,
1189    );
1190    let has_hit_targets = hit_test.is_some()
1191        || children.iter().any(|child| match child {
1192            RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
1193            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1194        });
1195    let has_origin_sinks = has_own_origin_sinks
1196        || children.iter().any(|child| match child {
1197            RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
1198            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1199        });
1200
1201    let layer = LayerNode {
1202        node_id: Some(node_id),
1203        wraps: None,
1204        local_bounds,
1205        transform_to_parent,
1206        content_offset,
1207        motion_context_animated: node_motion_context_animated,
1208        translated_content_context,
1209        translated_content_offset: if translated_content_context {
1210            content_offset
1211        } else {
1212            Point::default()
1213        },
1214        scene_children_origin: Point::default(),
1215        scene_children_layer_translation: Point::default(),
1216        graphics_layer,
1217        clip_to_bounds,
1218        shadow_clip,
1219        hit_test,
1220        has_hit_targets,
1221        has_origin_sinks,
1222        isolation,
1223        cache_policy,
1224        cache_hashes: LayerRasterCacheHashes::default(),
1225        cache_hashes_valid: false,
1226        children,
1227    };
1228    finish_layer(layer, placement, outer)
1229}
1230
1231#[derive(Clone, Copy)]
1232struct AbsOrigin {
1233    content_origin: Point,
1234    layer_translation: Point,
1235}
1236
1237impl AbsOrigin {
1238    const ROOT: AbsOrigin = AbsOrigin {
1239        content_origin: Point { x: 0.0, y: 0.0 },
1240        layer_translation: Point { x: 0.0, y: 0.0 },
1241    };
1242}
1243
1244fn build_layer_node_from_applier(
1245    applier: &mut MemoryApplier,
1246    node_id: NodeId,
1247    _root_scale: f32,
1248    inherited_motion_context_animated: bool,
1249) -> Option<LayerNode> {
1250    let mut data = snapshot_node_data(applier, node_id)?;
1251    if data.window_root {
1252        data.layout_state = data.layout_state.at_origin();
1253    }
1254    build_layer_node_from_data(
1255        applier,
1256        node_id,
1257        data,
1258        inherited_motion_context_animated,
1259        false,
1260        Some(AbsOrigin::ROOT),
1261    )
1262}
1263
1264fn snapshot_node_data(applier: &mut MemoryApplier, node_id: NodeId) -> Option<SnapshotNodeData> {
1265    if let Ok(data) = applier.with_node::<LayoutNode, _>(node_id, |node| {
1266        let state = node.layout_state();
1267        let mut children = SmallVec::new();
1268        node.collect_children_into(&mut children);
1269        let modifier_slices = node.modifier_slices_snapshot();
1270        SnapshotNodeData {
1271            layout_state: state,
1272            modifier_slices,
1273            resolved_modifiers: node.resolved_modifiers(),
1274            children,
1275            window_root: node.is_window_root(),
1276        }
1277    }) {
1278        return Some(data);
1279    }
1280
1281    applier
1282        .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
1283            let state = node.layout_state();
1284            let mut children = SmallVec::new();
1285            node.collect_children_into(&mut children);
1286            let modifier_slices = node.modifier_slices_snapshot();
1287            SnapshotNodeData {
1288                layout_state: state,
1289                modifier_slices,
1290                resolved_modifiers: node.resolved_modifiers(),
1291                children,
1292                window_root: false,
1293            }
1294        })
1295        .ok()
1296}
1297
1298fn build_layer_node_from_applier_internal(
1299    applier: &mut MemoryApplier,
1300    node_id: NodeId,
1301    inherited_motion_context_animated: bool,
1302    inherited_translated_content_context: bool,
1303    parent_abs: Option<AbsOrigin>,
1304) -> Option<LayerNode> {
1305    let data = snapshot_node_data(applier, node_id)?;
1306    if data.window_root {
1307        return None;
1308    }
1309    build_layer_node_from_data(
1310        applier,
1311        node_id,
1312        data,
1313        inherited_motion_context_animated,
1314        inherited_translated_content_context,
1315        parent_abs,
1316    )
1317}
1318
1319fn hit_test_from_slices(
1320    slices: &ModifierNodeSlices,
1321    bounds: Rect,
1322    clip: bool,
1323) -> Option<HitTestNode> {
1324    let click_actions = slices.click_handlers();
1325    let pointer_inputs = slices.pointer_inputs();
1326    let pointer_icon = slices.pointer_icon();
1327    (!click_actions.is_empty() || !pointer_inputs.is_empty() || pointer_icon.is_some()).then(|| {
1328        HitTestNode {
1329            shape: None,
1330            click_actions: click_actions.to_vec(),
1331            pointer_inputs: pointer_inputs.to_vec(),
1332            pointer_icon: pointer_icon.cloned(),
1333            clip: clip.then_some(bounds),
1334        }
1335    })
1336}
1337
1338fn build_layer_node_from_data(
1339    applier: &mut MemoryApplier,
1340    node_id: NodeId,
1341    data: SnapshotNodeData,
1342    inherited_motion_context_animated: bool,
1343    inherited_translated_content_context: bool,
1344    parent_abs: Option<AbsOrigin>,
1345) -> Option<LayerNode> {
1346    note_layer_lowered();
1347    let SnapshotNodeData {
1348        layout_state,
1349        modifier_slices,
1350        resolved_modifiers,
1351        children,
1352        window_root: _,
1353    } = data;
1354    if !layout_state.is_placed() {
1355        return None;
1356    }
1357
1358    let local_bounds = Rect {
1359        x: 0.0,
1360        y: 0.0,
1361        width: layout_state.size().width,
1362        height: layout_state.size().height,
1363    };
1364    if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
1365        eprintln!(
1366            "[scene-update-diag] build layer node={node_id:?} size=({:.2},{:.2}) pos=({:.2},{:.2})",
1367            layout_state.size().width,
1368            layout_state.size().height,
1369            layout_state.position().x,
1370            layout_state.position().y,
1371        );
1372    }
1373    let clip_to_bounds = modifier_slices.clip_to_bounds();
1374    let graphics_layer = graphics_layer_with_shaped_clip(
1375        modifier_slices.graphics_layer().unwrap_or_default(),
1376        clip_to_bounds,
1377        modifier_slices.corner_shape(),
1378        local_bounds,
1379    );
1380    let transform_to_parent =
1381        layer_transform_to_parent(local_bounds, layout_state.position(), &graphics_layer);
1382    let isolation = isolation_reasons(&graphics_layer);
1383    let cache_policy = if isolation.has_any() {
1384        CachePolicy::Auto
1385    } else {
1386        CachePolicy::None
1387    };
1388    let shadow_clip = clip_to_bounds.then_some(local_bounds);
1389    let hit_test = hit_test_from_slices(
1390        &modifier_slices,
1391        local_bounds,
1392        clip_to_bounds || graphics_layer.clip,
1393    );
1394
1395    modifier_slices.publish_pointer_input_size(layout_state.size());
1396
1397    let node_motion_context_animated =
1398        inherited_motion_context_animated || modifier_slices.motion_context_animated();
1399    let local_translated_content_context = modifier_slices.translated_content_context();
1400    let local_translated_content_offset = modifier_slices
1401        .translated_content_offset()
1402        .unwrap_or(layout_state.content_offset);
1403    let child_translated_content_context =
1404        inherited_translated_content_context || local_translated_content_context;
1405
1406    let this_abs = parent_abs.map(|parent| {
1407        let top_left = Point {
1408            x: parent.content_origin.x + layout_state.position().x,
1409            y: parent.content_origin.y + layout_state.position().y,
1410        };
1411        let layer_translation = Point {
1412            x: parent.layer_translation.x + graphics_layer.translation_x,
1413            y: parent.layer_translation.y + graphics_layer.translation_y,
1414        };
1415        (top_left, layer_translation)
1416    });
1417    if let Some((top_left, layer_translation)) = this_abs {
1418        let window_origin = Point {
1419            x: top_left.x + layer_translation.x,
1420            y: top_left.y + layer_translation.y,
1421        };
1422        if let Some(sink) = modifier_slices.text_field_window_origin() {
1423            sink.set(window_origin);
1424        }
1425        if let Some(sink) = modifier_slices.viewport_window_rect() {
1426            sink.set(Rect {
1427                x: window_origin.x,
1428                y: window_origin.y,
1429                width: layout_state.size().width,
1430                height: layout_state.size().height,
1431            });
1432        }
1433    }
1434    let child_abs = this_abs.map(|(top_left, layer_translation)| AbsOrigin {
1435        content_origin: Point {
1436            x: top_left.x + layout_state.content_offset.x,
1437            y: top_left.y + layout_state.content_offset.y,
1438        },
1439        layer_translation,
1440    });
1441
1442    let outer_draw_command_count = modifier_slices.outer_draw_command_count();
1443    let outer = outer_draws(
1444        node_id,
1445        modifier_slices.draw_commands(),
1446        outer_draw_command_count,
1447        layout_state.size(),
1448    );
1449    let layer_draw_commands = &modifier_slices.draw_commands()[outer_draw_command_count..];
1450    let mut render_children = Vec::with_capacity(layer_node_capacity(
1451        layer_draw_commands,
1452        children.len(),
1453        modifier_slices.annotated_text().is_some(),
1454    ));
1455    append_draw_nodes(
1456        &mut render_children,
1457        node_id,
1458        layer_draw_commands,
1459        outer_draw_command_count,
1460        DrawPlacement::Behind,
1461        layout_state.size(),
1462        PrimitivePhase::BeforeChildren,
1463    );
1464    if let Some(text) = text_node_from_parts(TextNodeParts {
1465        node_id,
1466        local_bounds,
1467        measured_max_width: layout_state
1468            .measurement_constraints
1469            .max_width
1470            .is_finite()
1471            .then_some(layout_state.measurement_constraints.max_width),
1472        resolved_modifiers: &resolved_modifiers,
1473        annotated_text: modifier_slices.annotated_text(),
1474        text_style: modifier_slices.text_style(),
1475        text_layout_options: modifier_slices.text_layout_options(),
1476        text_pan: modifier_slices.text_pan_resolver(),
1477        modifier_slices: Some(modifier_slices.as_ref()),
1478    }) {
1479        render_children.push(RenderNode::Primitive(PrimitiveEntry {
1480            phase: PrimitivePhase::BeforeChildren,
1481            node: PrimitiveNode::Text(Box::new(text)),
1482        }));
1483    }
1484    let child_motion_context_animated = node_motion_context_animated;
1485    for child_id in children {
1486        let Some(mut child_layer) = build_layer_node_from_applier_internal(
1487            applier,
1488            child_id,
1489            child_motion_context_animated,
1490            child_translated_content_context,
1491            child_abs,
1492        ) else {
1493            continue;
1494        };
1495        if layout_state.content_offset != Point::default() {
1496            child_layer.transform_to_parent =
1497                child_layer
1498                    .transform_to_parent
1499                    .then(ProjectiveTransform::translation(
1500                        layout_state.content_offset.x,
1501                        layout_state.content_offset.y,
1502                    ));
1503        }
1504        render_children.push(RenderNode::Layer(Box::new(child_layer)));
1505    }
1506    append_draw_nodes(
1507        &mut render_children,
1508        node_id,
1509        layer_draw_commands,
1510        outer_draw_command_count,
1511        DrawPlacement::Overlay,
1512        layout_state.size(),
1513        PrimitivePhase::AfterChildren,
1514    );
1515    let has_hit_targets = hit_test.is_some()
1516        || render_children.iter().any(|child| match child {
1517            RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
1518            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1519        });
1520    let has_origin_sinks = modifier_slices_have_origin_sinks(&modifier_slices)
1521        || render_children.iter().any(|child| match child {
1522            RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
1523            RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1524        });
1525
1526    let layer = LayerNode {
1527        node_id: Some(node_id),
1528        wraps: None,
1529        local_bounds,
1530        transform_to_parent,
1531        content_offset: layout_state.content_offset,
1532        motion_context_animated: node_motion_context_animated,
1533        translated_content_context: local_translated_content_context,
1534        translated_content_offset: if local_translated_content_context {
1535            local_translated_content_offset
1536        } else {
1537            Point::default()
1538        },
1539        scene_children_origin: child_abs.map(|c| c.content_origin).unwrap_or_default(),
1540        scene_children_layer_translation: child_abs
1541            .map(|c| c.layer_translation)
1542            .unwrap_or_default(),
1543        graphics_layer,
1544        clip_to_bounds,
1545        shadow_clip,
1546        hit_test,
1547        has_hit_targets,
1548        has_origin_sinks,
1549        isolation,
1550        cache_policy,
1551        cache_hashes: LayerRasterCacheHashes::default(),
1552        cache_hashes_valid: false,
1553        children: render_children,
1554    };
1555    Some(finish_layer(layer, layout_state.position(), outer))
1556}
1557
1558struct RecorderSlot {
1559    generation: u64,
1560    handles: [Option<Rc<CommandRecording>>; 2],
1561}
1562
1563thread_local! {
1564    static COMMAND_RECORDINGS: std::cell::RefCell<
1565        std::collections::HashMap<DrawCommandId, RecorderSlot, cranpose_ui_graphics::FxBuildHasher>,
1566    > = std::cell::RefCell::new(std::collections::HashMap::default());
1567    static RECORDING_GENERATION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
1568}
1569
1570#[doc(hidden)]
1571pub fn clear_command_recordings_for_tests() {
1572    COMMAND_RECORDINGS.with(|map| map.borrow_mut().clear());
1573}
1574
1575fn bump_recording_generation() {
1576    let generation = RECORDING_GENERATION.with(|cell| {
1577        let next = cell.get().wrapping_add(1);
1578        cell.set(next);
1579        next
1580    });
1581    if generation.is_multiple_of(512) {
1582        COMMAND_RECORDINGS.with(|map| {
1583            map.borrow_mut()
1584                .retain(|_, slot| generation.wrapping_sub(slot.generation) <= 64);
1585        });
1586    }
1587}
1588
1589fn acquire_storage(id: DrawCommandId) -> CommandRecording {
1590    COMMAND_RECORDINGS.with(|map| {
1591        let mut map = map.borrow_mut();
1592        let Some(slot) = map.get_mut(&id) else {
1593            return CommandRecording::default();
1594        };
1595        for handle in &mut slot.handles {
1596            if handle
1597                .as_ref()
1598                .is_some_and(|shared| Rc::strong_count(shared) == 1)
1599            {
1600                let shared = handle.take().expect("checked some above");
1601                return Rc::try_unwrap(shared).expect("sole owner checked above");
1602            }
1603        }
1604        CommandRecording::default()
1605    })
1606}
1607
1608fn publish_recording(id: DrawCommandId, recording: CommandRecording) -> Rc<CommandRecording> {
1609    let shared = Rc::new(recording);
1610    COMMAND_RECORDINGS.with(|map| {
1611        let mut map = map.borrow_mut();
1612        let generation = RECORDING_GENERATION.with(Cell::get);
1613        let slot = map.entry(id).or_insert_with(|| RecorderSlot {
1614            generation,
1615            handles: [None, None],
1616        });
1617        slot.generation = generation;
1618        slot.handles[1] = slot.handles[0].take();
1619        slot.handles[0] = Some(shared.clone());
1620    });
1621    shared
1622}
1623
1624fn layer_node_capacity(commands: &[DrawCommand], children: usize, has_text: bool) -> usize {
1625    children
1626        + usize::from(has_text)
1627        + commands.len()
1628        + commands
1629            .iter()
1630            .filter(|command| matches!(command, DrawCommand::WithContent(_)))
1631            .count()
1632}
1633
1634fn draw_nodes(
1635    node_id: NodeId,
1636    commands: &[DrawCommand],
1637    first_command_index: usize,
1638    placement: DrawPlacement,
1639    size: Size,
1640    phase: PrimitivePhase,
1641) -> Vec<RenderNode> {
1642    let mut nodes = Vec::new();
1643    append_draw_nodes(
1644        &mut nodes,
1645        node_id,
1646        commands,
1647        first_command_index,
1648        placement,
1649        size,
1650        phase,
1651    );
1652    nodes
1653}
1654
1655fn append_draw_nodes(
1656    nodes: &mut Vec<RenderNode>,
1657    node_id: NodeId,
1658    commands: &[DrawCommand],
1659    first_command_index: usize,
1660    placement: DrawPlacement,
1661    size: Size,
1662    phase: PrimitivePhase,
1663) {
1664    for (command_index, command) in commands.iter().enumerate() {
1665        let id = DrawCommandId {
1666            node_id,
1667            command_index: (first_command_index + command_index) as u32,
1668            placement,
1669        };
1670        let Some((recording, segments)) =
1671            recording_for_placement_reusing(command, placement, size, || acquire_storage(id))
1672        else {
1673            retain_empty_draw_command(nodes, phase, id, placement, command);
1674            continue;
1675        };
1676        let shared = publish_recording(id, recording);
1677        if shared.is_empty_in(&segments) {
1678            retain_empty_draw_command(nodes, phase, id, placement, command);
1679            continue;
1680        }
1681        nodes.push(RenderNode::DrawRun(DrawRunNode::for_command_shared(
1682            phase,
1683            Some(id),
1684            shared,
1685            segments,
1686        )));
1687    }
1688}
1689
1690fn retain_empty_draw_command(
1691    nodes: &mut Vec<RenderNode>,
1692    phase: PrimitivePhase,
1693    id: DrawCommandId,
1694    placement: DrawPlacement,
1695    command: &DrawCommand,
1696) {
1697    if matches!(
1698        (placement, command),
1699        (DrawPlacement::Behind, DrawCommand::Behind(_))
1700            | (DrawPlacement::Overlay, DrawCommand::Overlay(_))
1701            | (_, DrawCommand::WithContent(_))
1702    ) {
1703        nodes.push(RenderNode::DrawRun(DrawRunNode::for_command(
1704            phase,
1705            Some(id),
1706            Vec::new(),
1707        )));
1708    }
1709}
1710
1711#[doc(hidden)]
1712pub fn draw_command_nodes_for_tests(
1713    node_id: NodeId,
1714    commands: &[DrawCommand],
1715    placement: DrawPlacement,
1716    size: Size,
1717    phase: PrimitivePhase,
1718) -> Vec<RenderNode> {
1719    bump_recording_generation();
1720    draw_nodes(node_id, commands, 0, placement, size, phase)
1721}
1722
1723struct OuterDraws {
1724    behind: Vec<RenderNode>,
1725    overlay: Vec<RenderNode>,
1726}
1727
1728fn outer_draws(
1729    node_id: NodeId,
1730    draw_commands: &[DrawCommand],
1731    outer_draw_command_count: usize,
1732    size: Size,
1733) -> Option<OuterDraws> {
1734    (outer_draw_command_count > 0).then(|| {
1735        let commands = &draw_commands[..outer_draw_command_count];
1736        OuterDraws {
1737            behind: draw_nodes(
1738                node_id,
1739                commands,
1740                0,
1741                DrawPlacement::Behind,
1742                size,
1743                PrimitivePhase::BeforeChildren,
1744            ),
1745            overlay: draw_nodes(
1746                node_id,
1747                commands,
1748                0,
1749                DrawPlacement::Overlay,
1750                size,
1751                PrimitivePhase::AfterChildren,
1752            ),
1753        }
1754    })
1755}
1756
1757fn finish_layer(layer: LayerNode, placement: Point, outer: Option<OuterDraws>) -> LayerNode {
1758    match outer {
1759        Some(outer) => wrap_layer_with_outer_draws(layer, placement, outer),
1760        None => layer,
1761    }
1762}
1763
1764fn wrap_layer_with_outer_draws(
1765    mut layer: LayerNode,
1766    placement: Point,
1767    outer: OuterDraws,
1768) -> LayerNode {
1769    let local_bounds = layer.local_bounds;
1770    layer.transform_to_parent =
1771        layer_transform_to_parent(local_bounds, Point::default(), &layer.graphics_layer);
1772    let wrapper = LayerNode {
1773        wraps: layer.node_id,
1774        local_bounds,
1775        transform_to_parent: layer_transform_to_parent(
1776            local_bounds,
1777            placement,
1778            &GraphicsLayer::default(),
1779        ),
1780        scene_children_origin: Point {
1781            x: layer.scene_children_origin.x - layer.content_offset.x,
1782            y: layer.scene_children_origin.y - layer.content_offset.y,
1783        },
1784        scene_children_layer_translation: Point {
1785            x: layer.scene_children_layer_translation.x - layer.graphics_layer.translation_x,
1786            y: layer.scene_children_layer_translation.y - layer.graphics_layer.translation_y,
1787        },
1788        motion_context_animated: layer.motion_context_animated,
1789        has_hit_targets: layer.has_hit_targets,
1790        has_origin_sinks: layer.has_origin_sinks,
1791        ..Default::default()
1792    };
1793    let mut children = outer.behind;
1794    children.push(RenderNode::Layer(Box::new(layer)));
1795    children.extend(outer.overlay);
1796    LayerNode {
1797        children,
1798        ..wrapper
1799    }
1800}
1801
1802fn layer_identity(layer: &LayerNode) -> Option<NodeId> {
1803    layer.node_id.or(layer.wraps)
1804}
1805
1806struct TextNodeParts<'a> {
1807    node_id: NodeId,
1808    local_bounds: Rect,
1809    measured_max_width: Option<f32>,
1810    resolved_modifiers: &'a ResolvedModifiers,
1811    annotated_text: Option<&'a AnnotatedString>,
1812    text_style: Option<&'a TextStyle>,
1813    text_layout_options: Option<TextLayoutOptions>,
1814    text_pan: Option<TextPanResolver>,
1815    modifier_slices: Option<&'a ModifierNodeSlices>,
1816}
1817
1818fn text_node_from_parts(parts: TextNodeParts<'_>) -> Option<TextPrimitiveNode> {
1819    let TextNodeParts {
1820        node_id,
1821        local_bounds,
1822        measured_max_width,
1823        resolved_modifiers,
1824        annotated_text,
1825        text_style,
1826        text_layout_options,
1827        text_pan,
1828        modifier_slices,
1829    } = parts;
1830    let value = annotated_text?;
1831    let default_text_style = TextStyle::default();
1832    let text_style = text_style.cloned().unwrap_or(default_text_style);
1833    let options = text_layout_options.unwrap_or_default().normalized();
1834    let padding = resolved_modifiers.padding();
1835    let content_width = (local_bounds.width - padding.left - padding.right).max(0.0);
1836    if content_width <= 0.0 {
1837        return None;
1838    }
1839
1840    let pan_offset = text_pan
1841        .as_ref()
1842        .map_or(0.0, |resolve| resolve(content_width));
1843    let pans_horizontally = text_pan.is_some();
1844
1845    let max_width = if pans_horizontally {
1846        None
1847    } else {
1848        let measure_width =
1849            resolve_text_measure_width(content_width, padding, measured_max_width, options);
1850        Some(measure_width).filter(|width| width.is_finite() && *width > 0.0)
1851    };
1852    let prepared = modifier_slices
1853        .and_then(|slices| slices.prepare_text_layout(max_width))
1854        .unwrap_or_else(|| prepare_text_layout(value, &text_style, options, max_width));
1855    let visual_style = prepared.visual_style.clone();
1856    let measured_draw_width = prepared.metrics.width.max(0.0);
1857    let draw_width = if options.overflow == TextOverflow::Visible || pans_horizontally {
1858        measured_draw_width
1859    } else {
1860        measured_draw_width.min(content_width)
1861    };
1862    let alignment_offset = resolve_text_horizontal_offset(
1863        &text_style,
1864        prepared.text.text.as_str(),
1865        content_width,
1866        prepared.metrics.width,
1867    );
1868    let rect = Rect {
1869        x: padding.left + alignment_offset - pan_offset,
1870        y: padding.top,
1871        width: draw_width,
1872        height: prepared.metrics.height,
1873    };
1874    let text_bounds = Rect {
1875        x: padding.left,
1876        y: padding.top,
1877        width: content_width,
1878        height: (local_bounds.height - padding.top - padding.bottom).max(0.0),
1879    };
1880    let font_size = visual_style.resolve_font_size(14.0);
1881    let expanded_bounds =
1882        expand_text_bounds_for_baseline_shift(text_bounds, &visual_style, font_size);
1883    let clip = if options.overflow == TextOverflow::Visible && !pans_horizontally {
1884        None
1885    } else {
1886        Some(pad_clip_rect(expanded_bounds))
1887    };
1888
1889    Some(TextPrimitiveNode {
1890        node_id,
1891        rect,
1892        text: prepared.text,
1893        text_style: visual_style,
1894        font_size,
1895        layout_options: options,
1896        clip,
1897    })
1898}
1899
1900fn layout_box_to_snapshot(node: &LayoutBox, parent: Option<&LayoutBox>) -> BuildNodeSnapshot {
1901    let placement = parent
1902        .map(|parent_box| Point {
1903            x: node.rect.x - parent_box.rect.x - parent_box.content_offset.x,
1904            y: node.rect.y - parent_box.rect.y - parent_box.content_offset.y,
1905        })
1906        .unwrap_or_default();
1907    let mut children = Vec::with_capacity(node.children.len());
1908    for child in &node.children {
1909        children.push(layout_box_to_snapshot(child, Some(node)));
1910    }
1911    let base_graphics_layer = node.node_data.modifier_slices.graphics_layer();
1912    let graphics_layer = graphics_layer_with_shaped_clip(
1913        base_graphics_layer.clone().unwrap_or_default(),
1914        node.node_data.modifier_slices.clip_to_bounds(),
1915        node.node_data.modifier_slices.corner_shape(),
1916        Rect {
1917            x: 0.0,
1918            y: 0.0,
1919            width: node.rect.width,
1920            height: node.rect.height,
1921        },
1922    );
1923    let has_graphics_layer =
1924        base_graphics_layer.is_some() || graphics_layer.render_effect.is_some();
1925
1926    BuildNodeSnapshot {
1927        node_id: node.node_id,
1928        placement,
1929        size: Size {
1930            width: node.rect.width,
1931            height: node.rect.height,
1932        },
1933        content_offset: node.content_offset,
1934        motion_context_animated: node.node_data.modifier_slices.motion_context_animated(),
1935        translated_content_context: node.node_data.modifier_slices.translated_content_context(),
1936        has_own_origin_sinks: modifier_slices_have_origin_sinks(&node.node_data.modifier_slices),
1937        measured_max_width: None,
1938        resolved_modifiers: node.node_data.resolved_modifiers,
1939        draw_commands: node.node_data.modifier_slices.draw_commands().to_vec(),
1940        outer_draw_command_count: node.node_data.modifier_slices.outer_draw_command_count(),
1941        click_actions: node.node_data.modifier_slices.click_handlers().to_vec(),
1942        pointer_inputs: node.node_data.modifier_slices.pointer_inputs().to_vec(),
1943        pointer_icon: node.node_data.modifier_slices.pointer_icon().cloned(),
1944        clip_to_bounds: node.node_data.modifier_slices.clip_to_bounds(),
1945        annotated_text: node.node_data.modifier_slices.annotated_string(),
1946        text_style: node.node_data.modifier_slices.text_style().cloned(),
1947        text_layout_options: node.node_data.modifier_slices.text_layout_options(),
1948        text_pan: node.node_data.modifier_slices.text_pan_resolver(),
1949        graphics_layer: has_graphics_layer.then_some(graphics_layer),
1950        children,
1951    }
1952}
1953
1954fn modifier_slices_have_origin_sinks(slices: &ModifierNodeSlices) -> bool {
1955    slices.text_field_window_origin().is_some() || slices.viewport_window_rect().is_some()
1956}
1957
1958fn graphics_layer_with_shaped_clip(
1959    mut graphics_layer: GraphicsLayer,
1960    clip_to_bounds: bool,
1961    corner_shape: Option<RoundedCornerShape>,
1962    local_bounds: Rect,
1963) -> GraphicsLayer {
1964    if !clip_to_bounds {
1965        return graphics_layer;
1966    }
1967
1968    let Some(corner_shape) = corner_shape else {
1969        return graphics_layer;
1970    };
1971    let radii = corner_shape.resolve(local_bounds.width, local_bounds.height);
1972    if radii.top_left <= f32::EPSILON
1973        && radii.top_right <= f32::EPSILON
1974        && radii.bottom_right <= f32::EPSILON
1975        && radii.bottom_left <= f32::EPSILON
1976    {
1977        return graphics_layer;
1978    }
1979
1980    if let Some(existing) = graphics_layer.render_effect.take() {
1981        let rounded_clip = rounded_corner_alpha_mask_effect(
1982            local_bounds.width,
1983            local_bounds.height,
1984            radii,
1985            ROUNDED_CLIP_EDGE_FEATHER,
1986        );
1987        graphics_layer.render_effect = Some(existing.then(rounded_clip));
1988    } else {
1989        graphics_layer.shape = LayerShape::Rounded(corner_shape);
1990        graphics_layer.clip = true;
1991    }
1992    graphics_layer
1993}
1994
1995fn isolation_reasons(layer: &GraphicsLayer) -> IsolationReasons {
1996    IsolationReasons {
1997        explicit_offscreen: layer.compositing_strategy == CompositingStrategy::Offscreen,
1998        shape_clip: layer.clip && !matches!(layer.shape, LayerShape::Rectangle),
1999        effect: layer.render_effect.is_some(),
2000        backdrop: layer.backdrop_effect.is_some(),
2001        group_opacity: layer.compositing_strategy != CompositingStrategy::ModulateAlpha
2002            && layer.alpha < 1.0,
2003        blend_mode: layer.blend_mode != cranpose_ui::BlendMode::SrcOver,
2004    }
2005}
2006
2007fn pad_clip_rect(rect: Rect) -> Rect {
2008    Rect {
2009        x: rect.x - TEXT_CLIP_PAD,
2010        y: rect.y - TEXT_CLIP_PAD,
2011        width: (rect.width + TEXT_CLIP_PAD * 2.0).max(0.0),
2012        height: (rect.height + TEXT_CLIP_PAD * 2.0).max(0.0),
2013    }
2014}
2015
2016pub fn expand_text_bounds_for_baseline_shift(
2017    text_bounds: Rect,
2018    text_style: &TextStyle,
2019    font_size: f32,
2020) -> Rect {
2021    let baseline_shift_px = text_style
2022        .span_style
2023        .baseline_shift
2024        .filter(|shift| shift.is_specified())
2025        .map_or(0.0, |shift| -(shift.0 * font_size));
2026    if baseline_shift_px == 0.0 {
2027        return text_bounds;
2028    }
2029
2030    if baseline_shift_px < 0.0 {
2031        Rect {
2032            x: text_bounds.x,
2033            y: text_bounds.y + baseline_shift_px,
2034            width: text_bounds.width,
2035            height: (text_bounds.height - baseline_shift_px).max(0.0),
2036        }
2037    } else {
2038        Rect {
2039            x: text_bounds.x,
2040            y: text_bounds.y,
2041            width: text_bounds.width,
2042            height: (text_bounds.height + baseline_shift_px).max(0.0),
2043        }
2044    }
2045}
2046
2047/// The width the paint pass must lay this paragraph out at.
2048///
2049/// **It is the width LAYOUT wrapped at, not the width the node ended up.** A
2050/// `Text` without `fill_max_width` is placed at its own `metrics.width` — the
2051/// widest line it produced — which is by construction NARROWER than the
2052/// constraint it wrapped under. Re-wrapping at that narrower width is not the
2053/// no-op it looks like: the widest line is the one that exactly fills the
2054/// limit, so measuring it against itself puts its last word over the edge and
2055/// the paragraph gains a line. Measured against the real font backend
2056/// (`SoftwareTextMeasurer`, the one the wgpu renderer installs), that fires on
2057/// 46% of multi-line paragraphs — the block then paints a line taller than the
2058/// box layout reserved for it, its last line is clipped away, and every
2059/// following sibling has been placed as if that line did not exist.
2060///
2061/// So an unlimited soft-wrapping clip paragraph keeps the measurement width
2062/// even when the node came out narrower — `may_expand_to_avoid_synthetic_wrap`.
2063/// The modes that deliberately re-fit (no soft wrap, a finite `max_lines`, or
2064/// an ellipsis budget) still take the node's own width, because for those the
2065/// node width IS the fitting constraint.
2066///
2067/// This is the shared implementation. It exists because the wgpu and pixels
2068/// pipelines each grew a private copy WITH this rule and its contract tests,
2069/// while the scene builder — the copy that the retained render graph actually
2070/// runs — kept a plain `available.min(content_width)`. The two private copies
2071/// were reachable only from their own tests. One function now, so the tests
2072/// guard the code that runs.
2073pub fn resolve_text_measure_width(
2074    content_width: f32,
2075    padding: cranpose_ui::EdgeInsets,
2076    measured_max_width: Option<f32>,
2077    options: TextLayoutOptions,
2078) -> f32 {
2079    let width = content_width.max(0.0);
2080    if let Some(max_width) = measured_max_width.filter(|w| w.is_finite() && *w > 0.0) {
2081        let measured_content_width = (max_width - padding.left - padding.right).max(0.0);
2082        if measured_content_width <= width {
2083            return measured_content_width;
2084        }
2085
2086        let may_expand_to_avoid_synthetic_wrap = options.soft_wrap
2087            && options.max_lines == usize::MAX
2088            && options.overflow == TextOverflow::Clip;
2089        if may_expand_to_avoid_synthetic_wrap {
2090            return measured_content_width;
2091        }
2092    }
2093    width
2094}
2095
2096/// How much of the slack a `TextAlign` puts *before* the text: 0 at the start
2097/// edge, 0.5 centred, 1 at the end edge.
2098///
2099/// Split out because the same fraction has to be applied twice and by two
2100/// different pieces of code. Compose aligns a paragraph **line by line** —
2101/// `TextAlign.Center` centres each line in the paragraph's width, it does not
2102/// centre the paragraph's box in its parent — so the block offset computed
2103/// here and the per-line offset the rasteriser applies inside the block are
2104/// two halves of one rule. They telescope: block at `(box - block) * f`, line
2105/// at `(block - line) * f`, which sums to `(box - line) * f`, exactly the
2106/// offset Compose gives that line. Getting one without the other leaves every
2107/// wrapped continuation line start-aligned under a centred first line.
2108pub fn text_align_fraction(text_style: &TextStyle, text: &str) -> f32 {
2109    let paragraph_style = &text_style.paragraph_style;
2110    let direction = resolve_text_direction(text, Some(paragraph_style.text_direction));
2111    let rtl = direction == cranpose_ui::text::ResolvedTextDirection::Rtl;
2112    match paragraph_style.text_align {
2113        TextAlign::Center => 0.5,
2114        TextAlign::End | TextAlign::Right => 1.0,
2115        TextAlign::Start | TextAlign::Left | TextAlign::Justify | TextAlign::Unspecified => {
2116            if rtl {
2117                1.0
2118            } else {
2119                0.0
2120            }
2121        }
2122    }
2123}
2124
2125fn resolve_text_horizontal_offset(
2126    text_style: &TextStyle,
2127    text: &str,
2128    content_width: f32,
2129    measured_width: f32,
2130) -> f32 {
2131    let remaining = (content_width - measured_width).max(0.0);
2132    remaining * text_align_fraction(text_style, text)
2133}
2134
2135#[cfg(test)]
2136#[path = "tests/scene_builder_tests.rs"]
2137mod tests;