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