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