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