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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum GraphRebuildReason {
72 RootLayerUnavailable,
74 DirtyLayerUnavailable,
76 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
2047pub 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
2096pub 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;