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