1use std::{any::Any, rc::Rc};
2
3use cranpose_core::{MemoryApplier, NodeId, NodeMarks, collections::map::HashMap};
4use cranpose_ui::{
5 DrawCommand, LayoutBox, LayoutNode, ModifierNodeSlices, Point, PreparedTextLayout, Rect, Size,
6 SubcomposeLayoutNode, TextLayoutOptions, TextOverflow, TextPanResolver, text::TextStyle,
7};
8use cranpose_ui_graphics::{
9 CommandRecording, CompositingStrategy, GraphicsLayer, LayerShape, RoundedCornerShape,
10 rounded_corner_alpha_mask_effect,
11};
12
13use crate::{
14 SceneUpdates,
15 graph::{
16 CachePolicy, ContentChanges, DrawCommandId, DrawRunNode, HitTestNode, IsolationReasons,
17 LayerNode, PrimitiveEntry, PrimitiveNode, PrimitivePhase, ProjectiveTransform, RenderGraph,
18 RenderNode, TextPrimitiveNode,
19 },
20 layer_transform::{
21 layer_scales_or_rotates, layer_transform_to_parent, layer_transform_to_window,
22 },
23 style_shared::{DrawPlacement, recording_for_placement_reusing},
24 text_paint::{TextPaint, TextPaintCache},
25};
26
27const TEXT_CLIP_PAD: f32 = 1.0;
28const ROUNDED_CLIP_EDGE_FEATHER: f32 = 1.0;
29
30#[derive(Clone, Default)]
31struct BuildNodeSnapshot {
32 node_id: NodeId,
33 placement: Point,
34 size: Size,
35 content_offset: Point,
36 slices: Rc<ModifierNodeSlices>,
39 graphics_layer: Option<GraphicsLayer>,
40 children: Vec<Self>,
41}
42
43struct SnapshotNodeData<'a> {
44 layout_state: cranpose_ui::widgets::LayoutState,
45 modifier_slices: Rc<ModifierNodeSlices>,
46 children: &'a [NodeId],
47 window_root: bool,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum GraphRebuildReason {
60 RootLayerUnavailable,
62 DirtyLayerUnavailable,
64 UnmatchedDirtyNodes(usize),
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum GraphUpdate {
74 Patched,
76 NeedsRebuild(GraphRebuildReason),
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82pub struct GraphUpdateReport {
83 pub update: GraphUpdate,
85 pub hit_graph_dirty: bool,
88}
89
90impl GraphUpdateReport {
91 pub fn applied(self) -> bool {
93 matches!(self.update, GraphUpdate::Patched)
94 }
95
96 pub fn rebuild_reason(self) -> Option<GraphRebuildReason> {
98 match self.update {
99 GraphUpdate::Patched => None,
100 GraphUpdate::NeedsRebuild(reason) => Some(reason),
101 }
102 }
103}
104
105fn note_layer_lowered() {
106 #[cfg(test)]
107 crate::thread_state::with(|state| {
108 let count = &state.lowered_layer_count;
109 count.set(count.get() + 1);
110 });
111}
112
113#[cfg(test)]
114fn reset_lowered_layer_count() {
115 crate::thread_state::with(|state| state.lowered_layer_count.set(0));
116}
117
118#[cfg(test)]
119fn lowered_layer_count() -> usize {
120 crate::thread_state::with(|state| state.lowered_layer_count.get())
121}
122
123pub fn build_graph_from_layout_tree(root: &LayoutBox, _scale: f32) -> RenderGraph {
125 bump_recording_generation();
126 let root_snapshot = layout_box_to_snapshot(root, None);
127 let mut graph = RenderGraph::new(LayerNode::default());
128 write_snapshot_layer(root_snapshot, LowerContext::ROOT, &mut graph.root);
129 graph
130}
131
132pub fn build_graph_from_applier(
135 applier: &MemoryApplier,
136 root: NodeId,
137 _scale: f32,
138) -> Option<RenderGraph> {
139 bump_recording_generation();
140 let mut graph = RenderGraph::new(LayerNode::default());
141 lower_root_into(applier, root, &mut graph.root).then_some(graph)
142}
143
144pub fn rebuild_graph_from_applier(
147 applier: &MemoryApplier,
148 root: NodeId,
149 scale: f32,
150 previous: Option<RenderGraph>,
151) -> Option<RenderGraph> {
152 let graph = match previous {
153 Some(mut graph) => {
154 release_for_rebuild(&mut graph.root);
155 bump_recording_generation();
156 graph.update += 1;
157 graph.root.note_content_change(graph.update);
158 lower_root_into(applier, root, &mut graph.root).then_some(graph)
159 }
160 None => build_graph_from_applier(applier, root, scale),
161 };
162 crate::layer_recycling::release();
163 graph
164}
165
166pub fn update_graph_from_applier(
169 applier: &MemoryApplier,
170 graph: &mut RenderGraph,
171 updates: SceneUpdates<'_>,
172 scale: f32,
173) -> bool {
174 update_graph_from_applier_report(applier, graph, updates, scale).applied()
175}
176
177pub fn update_graph_from_applier_report(
180 applier: &MemoryApplier,
181 graph: &mut RenderGraph,
182 updates: SceneUpdates<'_>,
183 _scale: f32,
184) -> GraphUpdateReport {
185 let report = update_graph_from_applier_report_inner(applier, graph, updates);
186 crate::layer_recycling::release();
187 if let GraphUpdate::NeedsRebuild(reason) = report.update
188 && cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG")
189 {
190 eprintln!(
191 "[scene-update-diag] scoped update abandoned, whole scene rebuilt: {reason:?} dirty={}",
192 updates.content.len() + updates.layers.len() + updates.moved.len()
193 );
194 }
195 report
196}
197
198#[derive(Clone, Copy)]
199pub(crate) enum NodeUpdate {
200 Content,
201 Layer,
202 Moved,
205 MovedLayer,
208}
209
210fn dirty_node_updates(updates: SceneUpdates<'_>, dirty: &mut HashMap<NodeId, NodeUpdate>) {
214 dirty.clear();
215 dirty.reserve(updates.moved.len() + updates.layers.len() + updates.content.len());
216 dirty.extend(updates.moved.iter().map(|&id| (id, NodeUpdate::Moved)));
217 for &id in updates.layers {
218 let kind = match dirty.get(&id) {
219 Some(NodeUpdate::Moved) => NodeUpdate::MovedLayer,
220 _ => NodeUpdate::Layer,
221 };
222 dirty.insert(id, kind);
223 }
224 dirty.extend(updates.content.iter().map(|&id| (id, NodeUpdate::Content)));
225}
226
227fn update_graph_from_applier_report_inner(
228 applier: &MemoryApplier,
229 graph: &mut RenderGraph,
230 updates: SceneUpdates<'_>,
231) -> GraphUpdateReport {
232 if updates.is_empty() {
233 return GraphUpdateReport {
234 update: GraphUpdate::Patched,
235 hit_graph_dirty: false,
236 };
237 }
238 bump_recording_generation();
239 graph.update += 1;
240 let update = graph.update;
241
242 if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
243 eprintln!("[scene-update-diag] dirty={updates:?}");
244 }
245
246 let mut remaining_dirty_nodes =
248 crate::thread_state::with(|state| state.dirty_node_scratch.take()).unwrap_or_default();
249 dirty_node_updates(updates, &mut remaining_dirty_nodes);
250 let report = update_dirty_nodes(applier, graph, &mut remaining_dirty_nodes, update);
251 remaining_dirty_nodes.clear();
252 crate::thread_state::with(|state| state.dirty_node_scratch.set(Some(remaining_dirty_nodes)));
253 report
254}
255
256fn update_dirty_nodes(
259 applier: &MemoryApplier,
260 graph: &mut RenderGraph,
261 remaining_dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
262 update: u64,
263) -> GraphUpdateReport {
264 if let Some(root_id) = layer_identity(&graph.root)
265 && let Some(kind) = remaining_dirty_nodes.remove(&root_id)
266 {
267 if let Some(hit_graph_dirty) = try_update_retained_layer(
268 applier,
269 &mut graph.root,
270 remaining_dirty_nodes,
271 kind,
272 true,
273 TranslateAncestorContext {
274 inherited_motion_context_animated: false,
275 update,
276 inherited_translated_content_context: false,
277 parent_content_offset: Point::default(),
278 parent_abs: AbsOrigin::ROOT,
279 },
280 ) {
281 if remaining_dirty_nodes.is_empty() {
282 return GraphUpdateReport {
283 update: GraphUpdate::Patched,
284 hit_graph_dirty,
285 };
286 }
287 let walked = walk_dirty_layers(applier, &mut graph.root, remaining_dirty_nodes, update);
288 return GraphUpdateReport {
289 update: classify_walk(applier, walked.is_some(), remaining_dirty_nodes),
290 hit_graph_dirty: hit_graph_dirty || walked.is_none_or(|r| r.hit_graph_dirty),
291 };
292 }
293
294 let Some(hit_graph_dirty) = rebuild_tracking_hits(&mut graph.root, |root| {
295 release_for_rebuild(root);
296 lower_root_into(applier, root_id, root)
297 }) else {
298 return GraphUpdateReport {
299 update: GraphUpdate::NeedsRebuild(GraphRebuildReason::RootLayerUnavailable),
300 hit_graph_dirty: true,
301 };
302 };
303 graph.root.note_content_change(update);
304
305 return GraphUpdateReport {
306 update: GraphUpdate::Patched,
307 hit_graph_dirty,
308 };
309 }
310
311 let Some(report) = walk_dirty_layers(applier, &mut graph.root, remaining_dirty_nodes, update)
312 else {
313 return GraphUpdateReport {
314 update: GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable),
315 hit_graph_dirty: true,
316 };
317 };
318
319 match classify_walk(applier, true, remaining_dirty_nodes) {
320 GraphUpdate::Patched => GraphUpdateReport {
321 update: GraphUpdate::Patched,
322 hit_graph_dirty: report.hit_graph_dirty,
323 },
324 update => GraphUpdateReport {
325 update,
326 hit_graph_dirty: true,
327 },
328 }
329}
330
331fn classify_walk(
334 applier: &MemoryApplier,
335 walked: bool,
336 remaining_dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
337) -> GraphUpdate {
338 if !walked {
339 return GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable);
340 }
341 remaining_dirty_nodes.retain(|&node, _| is_drawn(applier, node));
342 if !remaining_dirty_nodes.is_empty() {
343 GraphUpdate::NeedsRebuild(GraphRebuildReason::UnmatchedDirtyNodes(
344 remaining_dirty_nodes.len(),
345 ))
346 } else {
347 GraphUpdate::Patched
348 }
349}
350
351fn is_drawn(applier: &MemoryApplier, node: NodeId) -> bool {
356 let mut child = node;
357 let mut current = node;
358 while let Some(parent) = applier
359 .get_ref(current)
360 .ok()
361 .and_then(cranpose_core::Node::parent)
362 {
363 let Ok(parent_node) = applier.get_ref(parent) else {
364 return true;
365 };
366 current = parent;
367 if parent_node.is_virtual() {
368 continue;
369 }
370 let parent_node: &dyn Any = parent_node;
371 if let Some(subcompose) = parent_node.downcast_ref::<SubcomposeLayoutNode>()
372 && !subcompose.with_active_children(|children| children.contains(&child))
373 {
374 return false;
375 }
376 child = parent;
377 }
378 true
379}
380
381#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
382struct ReplaceDirtyLayersReport {
383 updated: bool,
384 hit_graph_dirty: bool,
385}
386
387struct DirtyWalk<'a> {
391 nodes: &'a mut HashMap<NodeId, NodeUpdate>,
392 ancestry: &'a NodeMarks,
393 applier: &'a MemoryApplier,
394}
395
396impl DirtyWalk<'_> {
397 fn reborrow(&mut self) -> DirtyWalk<'_> {
398 DirtyWalk {
399 nodes: self.nodes,
400 ancestry: self.ancestry,
401 applier: self.applier,
402 }
403 }
404
405 fn may_hold(&self, identity: Option<NodeId>) -> bool {
408 identity.is_none_or(|id| self.ancestry.get(self.applier, id) == DIRTY_ANCESTOR)
409 }
410}
411
412const DIRTY_ANCESTOR: u8 = 1;
413
414fn walk_dirty_layers(
415 applier: &MemoryApplier,
416 root: &mut LayerNode,
417 dirty: &mut HashMap<NodeId, NodeUpdate>,
418 update: u64,
419) -> Option<ReplaceDirtyLayersReport> {
420 let inherited = root.translated_content_context;
421 let root_children = AbsOrigin::ROOT.children_of(root);
422 let mut ancestry = crate::thread_state::with(|state| state.dirty_ancestry.take());
423 mark_dirty_ancestry(applier, dirty, &mut ancestry);
424 let walked = replace_dirty_layers_from_applier(
425 applier,
426 root,
427 root_children,
428 DirtyWalk {
429 nodes: dirty,
430 ancestry: &ancestry,
431 applier,
432 },
433 inherited,
434 update,
435 );
436 crate::thread_state::with(|state| state.dirty_ancestry.set(ancestry));
437 walked
438}
439
440fn mark_dirty_ancestry(
441 applier: &MemoryApplier,
442 dirty: &HashMap<NodeId, NodeUpdate>,
443 ancestry: &mut NodeMarks,
444) {
445 ancestry.reset(applier);
446 for &node in dirty.keys() {
447 let mut current = node;
448 while let Some(parent) = applier
449 .get_ref(current)
450 .ok()
451 .and_then(cranpose_core::Node::parent)
452 {
453 if ancestry.get(applier, parent) == DIRTY_ANCESTOR {
454 break;
455 }
456 ancestry.set(applier, parent, DIRTY_ANCESTOR);
457 current = parent;
458 }
459 }
460}
461
462fn replace_dirty_layers_from_applier(
463 applier: &MemoryApplier,
464 parent: &mut LayerNode,
465 parent_children: AbsOrigin,
466 mut dirty: DirtyWalk<'_>,
467 inherited_translated_content_context: bool,
468 update: u64,
469) -> Option<ReplaceDirtyLayersReport> {
470 if dirty.nodes.is_empty() {
471 return Some(ReplaceDirtyLayersReport::default());
472 }
473
474 let child_inherited_translated_content_context =
475 inherited_translated_content_context || parent.translated_content_context;
476 let mut report = ReplaceDirtyLayersReport::default();
477
478 for child in &mut parent.children {
479 if dirty.nodes.is_empty() {
480 break;
481 }
482 let RenderNode::Layer(child_layer) = child else {
483 continue;
484 };
485
486 let identity = layer_identity(child_layer);
487 if let Some(kind) = identity.and_then(|id| dirty.nodes.remove(&id)) {
488 if let Some(hit_graph_dirty) = try_update_retained_layer(
489 applier,
490 child_layer,
491 dirty.nodes,
492 kind,
493 false,
494 TranslateAncestorContext {
495 inherited_motion_context_animated: parent.motion_context_animated,
496 update,
497 inherited_translated_content_context:
498 child_inherited_translated_content_context,
499 parent_content_offset: parent.content_offset,
500 parent_abs: parent_children,
501 },
502 ) {
503 report.hit_graph_dirty |= hit_graph_dirty;
504 report.updated = true;
505 let child_children = parent_children.children_of(child_layer);
506 let child_report = replace_dirty_layers_from_applier(
507 applier,
508 child_layer,
509 child_children,
510 dirty.reborrow(),
511 child_inherited_translated_content_context,
512 update,
513 )?;
514 report.hit_graph_dirty |= child_report.hit_graph_dirty;
515 continue;
516 }
517 let node_id = layer_identity(child_layer).expect("dirty layer must have a node id");
518
519 remove_dirty_descendants(child_layer, dirty.nodes);
523 let context = LowerContext {
524 inherited_motion_context_animated: parent.motion_context_animated,
525 inherited_translated_content_context: child_inherited_translated_content_context,
526 parent_abs: Some(parent_children),
527 parent_content_offset: parent.content_offset,
528 };
529 report.hit_graph_dirty |= rebuild_tracking_hits(child_layer, |layer| {
530 release_for_rebuild(layer);
531 read_placed_node_data(applier, node_id, false, |data| {
532 write_node_layer(applier, node_id, data, context, layer);
533 })
534 .is_some()
535 })?;
536 remove_dirty_descendants(child_layer, dirty.nodes);
537
538 child_layer.note_content_change(update);
539 report.updated = true;
540 continue;
541 }
542
543 if !dirty.may_hold(identity) {
544 continue;
545 }
546 let child_children = parent_children.children_of(child_layer);
547 let child_report = replace_dirty_layers_from_applier(
548 applier,
549 child_layer,
550 child_children,
551 dirty.reborrow(),
552 child_inherited_translated_content_context,
553 update,
554 )?;
555 report.updated |= child_report.updated;
556 report.hit_graph_dirty |= child_report.hit_graph_dirty;
557 }
558
559 if report.updated {
560 parent.refresh_child_facts();
561 parent.has_origin_sinks |= children_have_origin_sinks(&parent.children);
565 parent.note_content_change(update);
566 }
567
568 Some(report)
569}
570
571fn translate_bail(reason: &str) -> bool {
572 if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
573 eprintln!("[scene-update-diag] translate bail: {reason}");
574 }
575 false
576}
577
578#[derive(Clone, Copy)]
579struct TranslateAncestorContext {
580 inherited_motion_context_animated: bool,
581 update: u64,
583 inherited_translated_content_context: bool,
584 parent_content_offset: Point,
585 parent_abs: AbsOrigin,
586}
587
588fn try_update_retained_layer(
589 applier: &MemoryApplier,
590 layer: &mut LayerNode,
591 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
592 kind: NodeUpdate,
593 root: bool,
594 ancestors: TranslateAncestorContext,
595) -> Option<bool> {
596 match kind {
597 NodeUpdate::Moved if !root => {
598 move_retained_layer(applier, layer, ancestors.parent_content_offset).or_else(|| {
599 try_translate_scrolled_layer(applier, layer, dirty_nodes, ancestors).then_some(true)
600 })
601 }
602 NodeUpdate::Content | NodeUpdate::Moved => {
603 try_translate_scrolled_layer(applier, layer, dirty_nodes, ancestors).then_some(true)
604 }
605 NodeUpdate::Layer => update_layer_properties(applier, layer, root, false, ancestors),
606 NodeUpdate::MovedLayer => update_layer_properties(applier, layer, root, true, ancestors),
607 }
608}
609
610fn move_retained_layer(
615 applier: &MemoryApplier,
616 layer: &mut LayerNode,
617 parent_content_offset: Point,
618) -> Option<bool> {
619 if layer.has_origin_sinks {
620 return None;
621 }
622 let state = placed_child_layout_state(applier, layer_identity(layer)?)?;
623 if Rect::from_size(state.size()) != layer.node_rect() {
624 return None;
625 }
626 let previous = HitGraphState::of(layer);
627 translate_retained_child(layer, &state, parent_content_offset);
628 Some(previous.dirty_against(&HitGraphState::of(layer)))
629}
630
631fn content_layer_mut(container: &mut LayerNode) -> Option<&mut LayerNode> {
632 let Some(node_id) = container.wraps else {
633 return Some(container);
634 };
635 container.children.iter_mut().find_map(|child| match child {
636 RenderNode::Layer(layer) if layer.node_id == Some(node_id) => Some(&mut **layer),
637 _ => None,
638 })
639}
640
641fn retained_layer_matches_layout(
644 layer: &LayerNode,
645 data: &SnapshotNodeData<'_>,
646 placement: Point,
647 wrapped: bool,
648 moved: bool,
649) -> bool {
650 let state = &data.layout_state;
651 let slices = &data.modifier_slices;
652 (moved || state.position() == placement)
653 && Rect::from_size(state.size()) == layer.node_rect()
654 && slices.layer_bounds(state.size()) == layer.local_bounds
655 && state.content_offset() == layer.content_offset
656 && (slices.outer_draw_command_count() > 0) == wrapped
657}
658
659fn update_layer_properties(
660 applier: &MemoryApplier,
661 container: &mut LayerNode,
662 root: bool,
663 moved: bool,
664 ancestors: TranslateAncestorContext,
665) -> Option<bool> {
666 let node_id = layer_identity(container)?;
667 let wrapped = container.wraps.is_some();
668 if moved && wrapped {
671 return None;
672 }
673 let placement = container.origin_in_parent;
674 let hit_graph_dirty = read_placed_node_data(applier, node_id, root, |data| {
675 let target = content_layer_mut(container)?;
676 let slices = &data.modifier_slices;
677 let state = &data.layout_state;
678 if !retained_layer_matches_layout(target, &data, placement, wrapped, moved) {
679 return None;
680 }
681 let context = LowerContext {
682 inherited_motion_context_animated: ancestors.inherited_motion_context_animated,
683 inherited_translated_content_context: ancestors.inherited_translated_content_context,
684 parent_abs: Some(ancestors.parent_abs),
685 parent_content_offset: ancestors.parent_content_offset,
686 };
687 let previous = HitGraphState::of(target);
688 let (head, child_context) = prepare_node_layer(node_id, slices, state, context);
689 assign_layer(target, head);
690 if wrapped {
691 target.transform_to_parent = layer_transform_to_parent(
692 target.local_bounds,
693 Point::default(),
694 &target.graphics_layer,
695 );
696 target.origin_in_parent = Point::default();
697 }
698 if target.has_origin_sinks
699 && let Some(child_abs) = child_context.parent_abs
700 {
701 for child in &target.children {
702 if let RenderNode::Layer(child) = child {
703 publish_retained_window_geometry(applier, child, child_abs);
704 }
705 }
706 }
707 Some(previous.dirty_against(&HitGraphState::of(target)))
708 })??;
709 if wrapped {
710 container.refresh_child_facts();
711 container.has_origin_sinks = children_have_origin_sinks(&container.children);
712 container.forget_own_raster_cache_hashes();
713 }
714 Some(hit_graph_dirty)
715}
716
717fn publish_retained_window_geometry(
718 applier: &MemoryApplier,
719 layer: &LayerNode,
720 parent_abs: AbsOrigin,
721) {
722 if !layer.has_origin_sinks {
723 return;
724 }
725 let child_abs = parent_abs.children_of(layer);
726 if let Some(node_id) = layer.node_id {
727 read_node_data(applier, node_id, |data| {
728 data.modifier_slices.publish_window_geometry(
729 Point {
730 x: parent_abs.content_origin.x + layer.origin_in_parent.x,
731 y: parent_abs.content_origin.y + layer.origin_in_parent.y,
732 },
733 child_abs.window_transform,
734 data.layout_state.size(),
735 );
736 });
737 }
738 for child in &layer.children {
739 if let RenderNode::Layer(child) = child {
740 publish_retained_window_geometry(applier, child, child_abs);
741 }
742 }
743}
744
745fn try_translate_scrolled_layer(
746 applier: &MemoryApplier,
747 container: &mut LayerNode,
748 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
749 ancestors: TranslateAncestorContext,
750) -> bool {
751 let Some(node_id) = layer_identity(container) else {
752 return translate_bail("no node id");
753 };
754 read_node_data(applier, node_id, |data| {
755 if container.wraps.is_none() {
756 return translate_layer_from_data(
757 applier,
758 container,
759 dirty_nodes,
760 ancestors,
761 data,
762 false,
763 );
764 }
765 let outer_count = data.modifier_slices.outer_draw_command_count();
766 if outer_count == 0 {
767 return translate_bail("outer draws removed");
768 }
769 let size = data.layout_state.size();
770 let placement = data.layout_state.position();
771 let slices = Rc::clone(&data.modifier_slices);
772 let Some(inner) = content_layer_mut(container) else {
773 return translate_bail("wrapped layer missing");
774 };
775 if !translate_layer_from_data(applier, inner, dirty_nodes, ancestors, data, true) {
776 return false;
777 }
778 let outer = outer_draws(node_id, slices.draw_commands(), outer_count, size)
779 .expect("outer command count is nonzero");
780 let layer =
781 take_wrapped_layer(container, node_id).expect("the wrapped layer was found above");
782 write_wrapper(
783 container,
784 layer,
785 placement,
786 outer,
787 ancestors.parent_content_offset,
788 );
789 container.note_content_change(ancestors.update);
790 true
791 })
792 .unwrap_or_else(|| translate_bail("container snapshot read failed"))
793}
794
795struct TranslatedContainer {
798 node_id: NodeId,
799 graphics_layer: Option<GraphicsLayer>,
800 local_bounds: Rect,
801 node_bounds: Option<Rect>,
802}
803
804fn translated_container(
805 container: &LayerNode,
806 layout_state: &cranpose_ui::widgets::LayoutState,
807 modifier_slices: &ModifierNodeSlices,
808 inherited_motion_context_animated: bool,
809 wrapped: bool,
810) -> Result<TranslatedContainer, &'static str> {
811 if cranpose_core::env_flag!("CRANPOSE_DISABLE_SCROLL_TRANSLATE") {
812 return Err("fast path disabled by ablation switch");
813 }
814 let Some(node_id) = container.node_id else {
815 return Err("no node id");
816 };
817 if !layout_state.is_placed() {
818 return Err("container unplaced");
819 }
820 let outer_count = modifier_slices.outer_draw_command_count();
821 if (outer_count > 0 && !wrapped)
822 || (inherited_motion_context_animated || modifier_slices.motion_context_animated())
823 != container.motion_context_animated
824 || modifier_slices.translated_content_context() != container.translated_content_context
825 {
826 return Err("container outer draws or translated context changed");
827 }
828 let clip_to_bounds = modifier_slices.clip_to_bounds();
829 if clip_to_bounds != container.clip_to_bounds {
830 return Err("container clip changed");
831 }
832 let shaped = |bounds: Rect| {
836 let mut graphics_layer = modifier_slices.graphics_layer();
837 shape_clip(
838 &mut graphics_layer,
839 clip_to_bounds,
840 modifier_slices.corner_shape(),
841 bounds,
842 );
843 graphics_layer
844 };
845 let current = shaped(container.local_bounds);
846 if current.as_ref().unwrap_or(&GraphicsLayer::DEFAULT) != &*container.graphics_layer {
847 return Err("container graphics layer changed");
848 }
849 let (local_bounds, node_bounds) = layer_and_node_bounds(modifier_slices, layout_state.size());
850 let graphics_layer = if local_bounds == container.local_bounds {
852 current
853 } else {
854 shaped(local_bounds)
855 };
856 Ok(TranslatedContainer {
857 node_id,
858 graphics_layer,
859 local_bounds,
860 node_bounds,
861 })
862}
863
864#[derive(Default)]
868pub(crate) struct TranslateScratch {
869 placed_fresh: Vec<(NodeId, cranpose_ui::widgets::LayoutState)>,
871 old_index_by_id: HashMap<NodeId, usize>,
872 retained: Vec<Option<Box<LayerNode>>>,
875 kept: Vec<Option<Box<LayerNode>>>,
878 entering: Vec<(NodeId, Box<LayerNode>)>,
880}
881
882fn translated_children(
886 applier: &MemoryApplier,
887 container: &LayerNode,
888 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
889 fresh_children: &[NodeId],
890 scratch: &mut TranslateScratch,
891) -> Result<bool, &'static str> {
892 let placed_fresh = &mut scratch.placed_fresh;
893 placed_fresh.clear();
894 for child_id in fresh_children {
895 let Some(state) = placed_child_layout_state(applier, *child_id) else {
896 continue;
897 };
898 placed_fresh.push((*child_id, state));
899 }
900 let children_unchanged = container.children.len() == placed_fresh.len()
901 && container
902 .children
903 .iter()
904 .zip(placed_fresh.iter())
905 .all(|(child, (id, _))| {
906 matches!(child, RenderNode::Layer(layer) if layer_identity(layer) == Some(*id))
907 });
908 let old_index_by_id = &mut scratch.old_index_by_id;
909 old_index_by_id.clear();
910 if !children_unchanged {
911 for (index, child) in container.children.iter().enumerate() {
912 let RenderNode::Layer(layer) = child else {
913 return Err("child without node id");
914 };
915 let Some(id) = layer_identity(layer) else {
916 return Err("child without node id");
917 };
918 old_index_by_id.insert(id, index);
919 }
920 }
921 check_retained_children(
922 container,
923 dirty_nodes,
924 &scratch.placed_fresh,
925 children_unchanged,
926 &scratch.old_index_by_id,
927 )?;
928 Ok(children_unchanged)
929}
930
931fn check_retained_children(
934 container: &LayerNode,
935 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
936 placed_fresh: &[(NodeId, cranpose_ui::widgets::LayoutState)],
937 children_unchanged: bool,
938 old_index_by_id: &HashMap<NodeId, usize>,
939) -> Result<(), &'static str> {
940 for (fresh_index, (child_id, state)) in placed_fresh.iter().enumerate() {
941 let old_index = if children_unchanged {
942 fresh_index
943 } else if let Some(index) = old_index_by_id.get(child_id) {
944 *index
945 } else {
946 continue;
947 };
948 let RenderNode::Layer(layer) = &container.children[old_index] else {
949 return Err("retained child slot is not a layer");
950 };
951 let fits = !layer.has_origin_sinks && Rect::from_size(state.size()) == layer.node_rect();
952 match dirty_nodes.get_mut(child_id) {
953 Some(NodeUpdate::Content | NodeUpdate::Layer) => {}
954 Some(kind @ (NodeUpdate::Moved | NodeUpdate::MovedLayer)) => {
955 if !fits {
956 *kind = NodeUpdate::Content;
957 }
958 }
959 None if layer.has_origin_sinks => {
960 return Err("child subtree publishes window origins");
961 }
962 None if !fits => return Err("child resized"),
963 None => {}
964 }
965 }
966 Ok(())
967}
968
969#[derive(Clone, Copy)]
970struct TranslateGeometry {
971 content_offset: Point,
972 window_transform: ProjectiveTransform,
973 top_left: Point,
974 child_origin: Point,
975}
976
977impl TranslateGeometry {
978 fn new(
979 layout_state: &cranpose_ui::widgets::LayoutState,
980 graphics_layer: &GraphicsLayer,
981 local_bounds: Rect,
982 parent_abs: AbsOrigin,
983 ) -> Self {
984 let content_offset = layout_state.content_offset();
985 let top_left = Point {
986 x: parent_abs.content_origin.x + layout_state.position().x,
987 y: parent_abs.content_origin.y + layout_state.position().y,
988 };
989 let window_transform = layer_transform_to_window(
990 parent_abs.window_transform,
991 top_left,
992 local_bounds,
993 graphics_layer,
994 );
995 Self {
996 content_offset,
997 window_transform,
998 top_left,
999 child_origin: Point {
1000 x: top_left.x + content_offset.x,
1001 y: top_left.y + content_offset.y,
1002 },
1003 }
1004 }
1005
1006 fn child_abs(self) -> AbsOrigin {
1007 AbsOrigin {
1008 content_origin: self.child_origin,
1009 window_transform: self.window_transform,
1010 }
1011 }
1012}
1013
1014fn recycle_leaving_children(
1022 container: &mut LayerNode,
1023 scratch: &mut TranslateScratch,
1024 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
1025) {
1026 let TranslateScratch {
1027 placed_fresh,
1028 old_index_by_id,
1029 retained,
1030 kept,
1031 ..
1032 } = scratch;
1033 retained.clear();
1034 retained.extend(container.children.drain(..).map(|child| match child {
1035 RenderNode::Layer(layer) => Some(layer),
1036 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => None,
1037 }));
1038 kept.clear();
1039 kept.extend(placed_fresh.iter().map(|(child_id, _)| {
1040 old_index_by_id
1041 .get(child_id)
1042 .and_then(|index| retained[*index].take())
1043 }));
1044 for leaving in retained.drain(..).flatten() {
1045 forget_dirty_subtree(&leaving, dirty_nodes);
1046 crate::layer_recycling::recycle(leaving);
1047 }
1048}
1049
1050fn build_entering_children(
1054 applier: &MemoryApplier,
1055 container: &LayerNode,
1056 scratch: &mut TranslateScratch,
1057 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
1058 geometry: TranslateGeometry,
1059 inherited: (bool, u64),
1060) {
1061 let (child_inherited_translated_content_context, update) = inherited;
1062 let entering = &mut scratch.entering;
1063 entering.clear();
1064 for (child_id, _) in &scratch.placed_fresh {
1065 if scratch.old_index_by_id.contains_key(child_id) {
1066 continue;
1067 }
1068 let context = LowerContext {
1069 inherited_motion_context_animated: container.motion_context_animated,
1070 inherited_translated_content_context: child_inherited_translated_content_context,
1071 parent_abs: Some(geometry.child_abs()),
1072 parent_content_offset: geometry.content_offset,
1073 };
1074 let Some(mut lowered) = lower_child(applier, *child_id, context) else {
1075 continue;
1076 };
1077 lowered.note_content_change(update);
1078 forget_dirty_subtree(&lowered, dirty_nodes);
1079 entering.push((*child_id, lowered));
1080 }
1081}
1082
1083fn write_own_content_around_children(
1086 container: &mut LayerNode,
1087 node_id: NodeId,
1088 modifier_slices: &ModifierNodeSlices,
1089 size: Size,
1090) {
1091 if modifier_slices.draw_commands()[modifier_slices.outer_draw_command_count()..].is_empty()
1092 && !modifier_slices.has_text()
1093 {
1094 return;
1095 }
1096 let kept = container.children.len();
1097 write_node_content(
1098 &mut container.children,
1099 node_id,
1100 modifier_slices,
1101 size,
1102 (kept, 0),
1103 |_| {},
1104 );
1105}
1106
1107fn apply_translated_container_state(
1108 container: &mut LayerNode,
1109 modifier_slices: &ModifierNodeSlices,
1110 layout_state: &cranpose_ui::widgets::LayoutState,
1111 parent_content_offset: Point,
1112 geometry: TranslateGeometry,
1113) {
1114 container.transform_to_parent = placed_transform(
1115 container.local_bounds,
1116 layout_state.position(),
1117 &container.graphics_layer,
1118 parent_content_offset,
1119 );
1120 container.content_offset = geometry.content_offset;
1121 if container.translated_content_context {
1122 container.translated_content_offset = modifier_slices
1123 .translated_content_offset()
1124 .unwrap_or(geometry.content_offset);
1125 }
1126 modifier_slices.publish_window_geometry(
1127 geometry.top_left,
1128 geometry.window_transform,
1129 layout_state.size(),
1130 );
1131 container.origin_in_parent = layout_state.position();
1132}
1133
1134fn reconcile_translated_children(
1135 container: &mut LayerNode,
1136 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
1137 scratch: &mut TranslateScratch,
1138 children_unchanged: bool,
1139 geometry: TranslateGeometry,
1140) {
1141 if children_unchanged {
1142 for (child, (child_id, state)) in container.children.iter_mut().zip(&scratch.placed_fresh) {
1143 let RenderNode::Layer(layer) = child else {
1144 unreachable!("retained child identities were checked");
1145 };
1146 if keeps_retained_child(dirty_nodes, *child_id) {
1147 translate_retained_child(layer, state, geometry.content_offset);
1148 }
1149 }
1150 return;
1151 }
1152 let mut entering = scratch.entering.drain(..).peekable();
1153 for ((child_id, state), kept) in scratch.placed_fresh.iter().zip(scratch.kept.drain(..)) {
1154 if let Some(mut layer) = kept {
1155 if keeps_retained_child(dirty_nodes, *child_id) {
1156 translate_retained_child(&mut layer, state, geometry.content_offset);
1157 }
1158 container.children.push(RenderNode::Layer(layer));
1159 } else if let Some((_, lowered)) = entering.next_if(|(id, _)| id == child_id) {
1160 dirty_nodes.remove(child_id);
1161 remove_dirty_descendants(&lowered, dirty_nodes);
1162
1163 container.children.push(RenderNode::Layer(lowered));
1164 }
1165 }
1166}
1167
1168fn translate_layer_from_data(
1169 applier: &MemoryApplier,
1170 container: &mut LayerNode,
1171 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
1172 ancestors: TranslateAncestorContext,
1173 data: SnapshotNodeData<'_>,
1174 wrapped: bool,
1175) -> bool {
1176 let TranslateAncestorContext {
1177 inherited_motion_context_animated,
1178 update,
1179 inherited_translated_content_context,
1180 parent_content_offset,
1181 parent_abs,
1182 } = ancestors;
1183 let SnapshotNodeData {
1184 layout_state,
1185 modifier_slices,
1186 children: fresh_children,
1187 window_root,
1188 } = data;
1189 let layout_state = if window_root {
1190 layout_state.at_origin()
1191 } else {
1192 layout_state
1193 };
1194 let container_plan = match translated_container(
1195 container,
1196 &layout_state,
1197 &modifier_slices,
1198 inherited_motion_context_animated,
1199 wrapped,
1200 ) {
1201 Ok(plan) => plan,
1202 Err(reason) => return translate_bail(reason),
1203 };
1204 let TranslatedContainer {
1205 node_id,
1206 graphics_layer,
1207 local_bounds,
1208 node_bounds,
1209 } = container_plan;
1210 if container.local_bounds != local_bounds {
1211 container.local_bounds = local_bounds;
1212 container.graphics_layer.replace(graphics_layer);
1213 }
1214 container.node_bounds = node_bounds;
1215 crate::layer_recycling::recycle_primitives(container);
1219 let mut scratch = crate::thread_state::with(|state| state.translate_scratch.take());
1220 let children_unchanged = match translated_children(
1221 applier,
1222 container,
1223 dirty_nodes,
1224 fresh_children,
1225 &mut scratch,
1226 ) {
1227 Ok(unchanged) => unchanged,
1228 Err(reason) => {
1229 crate::thread_state::with(|state| state.translate_scratch.set(scratch));
1230 return translate_bail(reason);
1231 }
1232 };
1233
1234 let geometry = TranslateGeometry::new(
1235 &layout_state,
1236 &container.graphics_layer,
1237 container.local_bounds,
1238 parent_abs,
1239 );
1240 let child_inherited_translated_content_context =
1241 inherited_translated_content_context || container.translated_content_context;
1242 if !children_unchanged {
1243 recycle_leaving_children(container, &mut scratch, dirty_nodes);
1244 build_entering_children(
1245 applier,
1246 container,
1247 &mut scratch,
1248 dirty_nodes,
1249 geometry,
1250 (child_inherited_translated_content_context, update),
1251 );
1252 }
1253
1254 apply_translated_container_state(
1255 container,
1256 &modifier_slices,
1257 &layout_state,
1258 parent_content_offset,
1259 geometry,
1260 );
1261
1262 reconcile_translated_children(
1263 container,
1264 dirty_nodes,
1265 &mut scratch,
1266 children_unchanged,
1267 geometry,
1268 );
1269 crate::thread_state::with(|state| state.translate_scratch.set(scratch));
1270 write_own_content_around_children(container, node_id, &modifier_slices, layout_state.size());
1271 container.hit_test = hit_test_from_slices(&modifier_slices);
1272
1273 container.has_origin_sinks = modifier_slices_have_origin_sinks(&modifier_slices)
1274 || children_have_origin_sinks(&container.children);
1275 container.refresh_child_facts();
1276 container.note_content_change(update);
1277
1278 true
1279}
1280
1281fn keeps_retained_child(dirty_nodes: &mut HashMap<NodeId, NodeUpdate>, child_id: NodeId) -> bool {
1285 match dirty_nodes.get(&child_id) {
1286 None => true,
1287 Some(NodeUpdate::Moved) => {
1288 dirty_nodes.remove(&child_id);
1289 true
1290 }
1291 Some(NodeUpdate::Content | NodeUpdate::Layer | NodeUpdate::MovedLayer) => false,
1292 }
1293}
1294
1295fn translate_retained_child(
1296 layer: &mut LayerNode,
1297 state: &cranpose_ui::widgets::LayoutState,
1298 content_offset: Point,
1299) {
1300 layer.transform_to_parent = placed_transform(
1301 layer.local_bounds,
1302 state.position(),
1303 &layer.graphics_layer,
1304 content_offset,
1305 );
1306 layer.origin_in_parent = state.position();
1307}
1308
1309fn rebuild_tracking_hits(
1316 layer: &mut LayerNode,
1317 rebuild: impl FnOnce(&mut LayerNode) -> bool,
1318) -> Option<bool> {
1319 let previous = HitGraphState::of(layer);
1320 let mut descendants = crate::thread_state::with(|state| state.hit_geometry_scratch.take());
1321 descendants.clear();
1322 visit_hit_layers(layer, &mut |child| {
1323 descendants.push(HitGraphState::of(child));
1324 true
1325 });
1326 let dirty = rebuild(layer).then(|| {
1327 let mut expected = descendants.iter();
1328 previous.dirty_against(&HitGraphState::of(layer))
1329 || !visit_hit_layers(layer, &mut |child| {
1330 expected.next() == Some(&HitGraphState::of(child))
1331 })
1332 || expected.next().is_some()
1333 });
1334 crate::thread_state::with(|state| state.hit_geometry_scratch.set(descendants));
1335 dirty
1336}
1337
1338fn visit_hit_layers(layer: &LayerNode, visit: &mut impl FnMut(&LayerNode) -> bool) -> bool {
1341 for child in &layer.children {
1342 let RenderNode::Layer(child) = child else {
1343 continue;
1344 };
1345 if child.hit_test.is_none() && !child.has_hit_targets {
1346 continue;
1347 }
1348 if !visit(child) || !visit_hit_layers(child, visit) {
1349 return false;
1350 }
1351 }
1352 true
1353}
1354
1355#[derive(PartialEq)]
1356pub(crate) struct HitGraphState {
1357 hit_test: bool,
1358 has_hit_targets: bool,
1359 local_bounds: Rect,
1360 node_bounds: Option<Rect>,
1361 transform_to_parent: ProjectiveTransform,
1362 clip_rect: Option<Rect>,
1363 shape: LayerShape,
1364}
1365
1366impl HitGraphState {
1367 fn of(layer: &LayerNode) -> Self {
1368 Self {
1369 hit_test: layer.hit_test.is_some(),
1370 has_hit_targets: layer.has_hit_targets,
1371 local_bounds: layer.local_bounds,
1372 node_bounds: layer.node_bounds,
1373 transform_to_parent: layer.transform_to_parent,
1374 clip_rect: layer.clip_rect(),
1375 shape: layer.graphics_layer.shape,
1376 }
1377 }
1378
1379 fn dirty_against(&self, replacement: &Self) -> bool {
1380 self.hit_test
1381 || replacement.hit_test
1382 || ((self.has_hit_targets || replacement.has_hit_targets) && self != replacement)
1383 }
1384}
1385
1386fn release_for_rebuild(layer: &mut LayerNode) {
1387 layer.hit_test = None;
1388 crate::layer_recycling::recycle_children(layer);
1389}
1390
1391fn forget_dirty_subtree(layer: &LayerNode, dirty_nodes: &mut HashMap<NodeId, NodeUpdate>) {
1393 if let Some(node_id) = layer_identity(layer) {
1394 dirty_nodes.remove(&node_id);
1395 }
1396 remove_dirty_descendants(layer, dirty_nodes);
1397}
1398
1399fn remove_dirty_descendants(layer: &LayerNode, dirty_nodes: &mut HashMap<NodeId, NodeUpdate>) {
1400 for child in &layer.children {
1401 let RenderNode::Layer(child_layer) = child else {
1402 continue;
1403 };
1404 if let Some(node_id) = child_layer.node_id {
1405 dirty_nodes.remove(&node_id);
1406 }
1407 remove_dirty_descendants(child_layer, dirty_nodes);
1408 }
1409}
1410
1411#[derive(Clone, Copy)]
1412struct LowerContext {
1413 inherited_motion_context_animated: bool,
1414 inherited_translated_content_context: bool,
1415 parent_abs: Option<AbsOrigin>,
1416 parent_content_offset: Point,
1417}
1418
1419impl LowerContext {
1420 const ROOT: Self = Self {
1421 inherited_motion_context_animated: false,
1422 inherited_translated_content_context: false,
1423 parent_abs: Some(AbsOrigin::ROOT),
1424 parent_content_offset: Point { x: 0.0, y: 0.0 },
1425 };
1426
1427 fn for_children(
1428 self,
1429 slices: &ModifierNodeSlices,
1430 content_offset: Point,
1431 parent_abs: Option<AbsOrigin>,
1432 ) -> Self {
1433 Self {
1434 inherited_motion_context_animated: self.inherited_motion_context_animated
1435 || slices.motion_context_animated(),
1436 inherited_translated_content_context: self.inherited_translated_content_context
1437 || slices.translated_content_context(),
1438 parent_abs,
1439 parent_content_offset: content_offset,
1440 }
1441 }
1442}
1443
1444struct LayerHead {
1445 node_id: Option<NodeId>,
1446 wraps: Option<NodeId>,
1447 local_bounds: Rect,
1448 node_bounds: Option<Rect>,
1449 transform_to_parent: ProjectiveTransform,
1450 content_offset: Point,
1451 motion_context_animated: bool,
1452 translated_content_context: bool,
1453 translated_content_offset: Point,
1454 origin_in_parent: Point,
1455 graphics_layer: Option<GraphicsLayer>,
1456 clip_to_bounds: bool,
1457 hit_test: Option<HitTestNode>,
1458 has_origin_sinks: bool,
1459 isolation: IsolationReasons,
1460 cache_policy: CachePolicy,
1461}
1462
1463struct NodeFrame {
1464 local_bounds: Rect,
1465 node_bounds: Option<Rect>,
1466 placement: Point,
1467 content_offset: Point,
1468 translated_content_offset: Point,
1469}
1470
1471fn node_layer_head(
1472 node_id: NodeId,
1473 slices: &Rc<ModifierNodeSlices>,
1474 frame: NodeFrame,
1475 graphics_layer: Option<GraphicsLayer>,
1476 context: LowerContext,
1477) -> LayerHead {
1478 let clip_to_bounds = slices.clip_to_bounds();
1479 let translated_content_context = slices.translated_content_context();
1480 let properties = graphics_layer.as_ref().unwrap_or(&GraphicsLayer::DEFAULT);
1481 let isolation = isolation_reasons(properties);
1482 LayerHead {
1483 node_id: Some(node_id),
1484 wraps: None,
1485 local_bounds: frame.local_bounds,
1486 node_bounds: frame.node_bounds,
1487 transform_to_parent: placed_transform(
1488 frame.local_bounds,
1489 frame.placement,
1490 properties,
1491 context.parent_content_offset,
1492 ),
1493 content_offset: frame.content_offset,
1494 motion_context_animated: context.inherited_motion_context_animated
1495 || slices.motion_context_animated(),
1496 translated_content_context,
1497 translated_content_offset: if translated_content_context {
1498 frame.translated_content_offset
1499 } else {
1500 Point::default()
1501 },
1502 origin_in_parent: frame.placement,
1503 cache_policy: layer_cache_policy(properties, isolation),
1504 isolation,
1505 graphics_layer,
1506 clip_to_bounds,
1507 hit_test: hit_test_from_slices(slices),
1508 has_origin_sinks: modifier_slices_have_origin_sinks(slices),
1509 }
1510}
1511
1512fn assign_layer(layer: &mut LayerNode, head: LayerHead) {
1513 let LayerNode {
1514 node_id,
1515 wraps,
1516 local_bounds,
1517 node_bounds,
1518 transform_to_parent,
1519 content_offset,
1520 motion_context_animated,
1521 translated_content_context,
1522 translated_content_offset,
1523 origin_in_parent,
1524 graphics_layer,
1525 clip_to_bounds,
1526 hit_test,
1527 has_hit_targets,
1528 has_origin_sinks,
1529 draws_within_bounds,
1530 isolation,
1531 cache_policy,
1532 cache_hashes: _,
1533 content_changes,
1534 change: _,
1535 children,
1536 } = layer;
1537 if *node_id != head.node_id {
1538 *content_changes = ContentChanges::default();
1539 }
1540 *node_id = head.node_id;
1541 *wraps = head.wraps;
1542 *local_bounds = head.local_bounds;
1543 *node_bounds = head.node_bounds;
1544 *transform_to_parent = head.transform_to_parent;
1545 *content_offset = head.content_offset;
1546 *motion_context_animated = head.motion_context_animated;
1547 *translated_content_context = head.translated_content_context;
1548 *translated_content_offset = head.translated_content_offset;
1549 *origin_in_parent = head.origin_in_parent;
1550 graphics_layer.replace(head.graphics_layer);
1551 *clip_to_bounds = head.clip_to_bounds;
1552 *hit_test = head.hit_test;
1553 *has_hit_targets = false;
1554 *has_origin_sinks = head.has_origin_sinks || children_have_origin_sinks(children);
1555 *draws_within_bounds = false;
1556 *isolation = head.isolation;
1557 *cache_policy = head.cache_policy;
1558 layer.forget_own_raster_cache_hashes();
1559 layer.refresh_child_facts();
1560}
1561
1562fn children_have_origin_sinks(children: &[RenderNode]) -> bool {
1563 children.iter().any(|child| match child {
1564 RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
1565 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1566 })
1567}
1568
1569fn placed_transform(
1570 local_bounds: Rect,
1571 placement: Point,
1572 graphics_layer: &GraphicsLayer,
1573 parent_content_offset: Point,
1574) -> ProjectiveTransform {
1575 let transform = layer_transform_to_parent(local_bounds, placement, graphics_layer);
1576 if parent_content_offset == Point::default() {
1577 transform
1578 } else {
1579 transform.then(ProjectiveTransform::translation(
1580 parent_content_offset.x,
1581 parent_content_offset.y,
1582 ))
1583 }
1584}
1585
1586fn write_node_content(
1589 list: &mut Vec<RenderNode>,
1590 node_id: NodeId,
1591 slices: &ModifierNodeSlices,
1592 size: Size,
1593 (kept, written): (usize, usize),
1594 write_children: impl FnOnce(&mut Vec<RenderNode>),
1595) {
1596 let outer_count = slices.outer_draw_command_count();
1597 let commands = &slices.draw_commands()[outer_count..];
1598 crate::layer_recycling::reserve_nodes(
1599 list,
1600 layer_node_capacity(commands, written, slices.has_text()),
1601 );
1602 let first_kept = list.len() - kept;
1603 let first_draw = list.len();
1604 append_draw_nodes(
1605 list,
1606 node_id,
1607 commands,
1608 outer_count,
1609 DrawPass::Record(DrawPlacement::Behind),
1610 size,
1611 PrimitivePhase::BeforeChildren,
1612 );
1613 if let Some(text) = text_node_from_parts(TextNodeParts {
1614 node_id,
1615 text_rect: slices.text_content_rect(size),
1616 text_style: slices.text_style().as_deref(),
1617 text_layout_options: slices.text_layout_options(),
1618 text_pan: slices.text_pan_resolver(),
1619 measured_layout: slices.measured_text_layout(),
1620 }) {
1621 list.push(RenderNode::Primitive(PrimitiveEntry {
1622 phase: PrimitivePhase::BeforeChildren,
1623 node: PrimitiveNode::Text(crate::layer_recycling::boxed_text(text)),
1624 }));
1625 }
1626 list[first_kept..].rotate_left(kept);
1628 let first_draw = first_draw - kept;
1629 write_children(list);
1630 append_draw_nodes(
1631 list,
1632 node_id,
1633 commands,
1634 outer_count,
1635 DrawPass::OverlayFromOutput(first_draw),
1636 size,
1637 PrimitivePhase::AfterChildren,
1638 );
1639}
1640
1641fn write_layer_with_outer(
1642 target: &mut LayerNode,
1643 head: LayerHead,
1644 outer: Option<OuterDraws>,
1645 placement: Point,
1646 parent_content_offset: Point,
1647 write_children: impl FnOnce(&mut Vec<RenderNode>),
1648) {
1649 debug_assert!(target.children.is_empty(), "a layer is written emptied");
1650 let Some(outer) = outer else {
1651 write_children(&mut target.children);
1652 assign_layer(target, head);
1653 return;
1654 };
1655 let mut layer = crate::layer_recycling::layer_box();
1656 write_children(&mut layer.children);
1657 assign_layer(&mut layer, head);
1658 write_wrapper(target, layer, placement, outer, parent_content_offset);
1659}
1660
1661fn write_snapshot_layer(
1662 snapshot: BuildNodeSnapshot,
1663 context: LowerContext,
1664 target: &mut LayerNode,
1665) {
1666 let BuildNodeSnapshot {
1667 node_id,
1668 placement,
1669 size,
1670 content_offset,
1671 slices,
1672 graphics_layer,
1673 children,
1674 } = snapshot;
1675 let (local_bounds, node_bounds) = layer_and_node_bounds(&slices, size);
1676 let child_context = context.for_children(&slices, content_offset, None);
1677 let head = node_layer_head(
1678 node_id,
1679 &slices,
1680 NodeFrame {
1681 local_bounds,
1682 node_bounds,
1683 placement,
1684 content_offset,
1685 translated_content_offset: content_offset,
1686 },
1687 graphics_layer,
1688 context,
1689 );
1690 let outer = outer_draws(
1691 node_id,
1692 slices.draw_commands(),
1693 slices.outer_draw_command_count(),
1694 size,
1695 );
1696 write_layer_with_outer(
1697 target,
1698 head,
1699 outer,
1700 placement,
1701 context.parent_content_offset,
1702 |list| {
1703 write_node_content(list, node_id, &slices, size, (0, children.len()), |list| {
1704 for child in children {
1705 let mut layer = crate::layer_recycling::layer_box();
1706 write_snapshot_layer(child, child_context, &mut layer);
1707 list.push(RenderNode::Layer(layer));
1708 }
1709 });
1710 },
1711 );
1712}
1713
1714#[derive(Clone, Copy)]
1715struct AbsOrigin {
1716 content_origin: Point,
1717 window_transform: ProjectiveTransform,
1718}
1719
1720impl AbsOrigin {
1721 const ROOT: AbsOrigin = AbsOrigin {
1722 content_origin: Point { x: 0.0, y: 0.0 },
1723 window_transform: ProjectiveTransform::identity(),
1724 };
1725
1726 fn children_of(self, layer: &LayerNode) -> AbsOrigin {
1727 let top_left = Point {
1728 x: self.content_origin.x + layer.origin_in_parent.x,
1729 y: self.content_origin.y + layer.origin_in_parent.y,
1730 };
1731 AbsOrigin {
1732 content_origin: Point {
1733 x: self.content_origin.x + layer.origin_in_parent.x + layer.content_offset.x,
1734 y: self.content_origin.y + layer.origin_in_parent.y + layer.content_offset.y,
1735 },
1736 window_transform: layer_transform_to_window(
1737 self.window_transform,
1738 top_left,
1739 layer.local_bounds,
1740 &layer.graphics_layer,
1741 ),
1742 }
1743 }
1744}
1745
1746fn read_placed_node_data<R>(
1747 applier: &MemoryApplier,
1748 node_id: NodeId,
1749 root: bool,
1750 read: impl FnOnce(SnapshotNodeData<'_>) -> R,
1751) -> Option<R> {
1752 read_node_data(applier, node_id, |mut data| {
1753 if data.window_root {
1754 if !root {
1755 return None;
1756 }
1757 data.layout_state = data.layout_state.at_origin();
1758 }
1759 note_layer_lowered();
1760 if !data.layout_state.is_placed() {
1761 return None;
1762 }
1763 Some(read(data))
1764 })?
1765}
1766
1767fn lower_root_into(applier: &MemoryApplier, node_id: NodeId, target: &mut LayerNode) -> bool {
1768 read_placed_node_data(applier, node_id, true, |data| {
1769 write_node_layer(applier, node_id, data, LowerContext::ROOT, target);
1770 })
1771 .is_some()
1772}
1773
1774fn lower_child(
1775 applier: &MemoryApplier,
1776 node_id: NodeId,
1777 context: LowerContext,
1778) -> Option<Box<LayerNode>> {
1779 read_placed_node_data(applier, node_id, false, |data| {
1780 let mut layer = crate::layer_recycling::layer_box();
1781 write_node_layer(applier, node_id, data, context, &mut layer);
1782 layer
1783 })
1784}
1785
1786fn placed_child_layout_state(
1787 applier: &MemoryApplier,
1788 node_id: NodeId,
1789) -> Option<cranpose_ui::widgets::LayoutState> {
1790 let node: &dyn Any = applier.get_ref(node_id).ok()?;
1791 let state = if let Some(node) = node.downcast_ref::<LayoutNode>() {
1792 if node.is_window_root() {
1793 return None;
1794 }
1795 node.layout_state()
1796 } else {
1797 node.downcast_ref::<SubcomposeLayoutNode>()?.layout_state()
1798 };
1799 state.is_placed().then_some(state)
1800}
1801
1802fn read_node_data<R>(
1803 applier: &MemoryApplier,
1804 node_id: NodeId,
1805 read: impl FnOnce(SnapshotNodeData<'_>) -> R,
1806) -> Option<R> {
1807 let node: &dyn Any = applier.get_ref(node_id).ok()?;
1808 if let Some(node) = node.downcast_ref::<LayoutNode>() {
1809 let z_order = z_ordered_children(applier, &node.children);
1810 return Some(read(SnapshotNodeData {
1811 layout_state: node.layout_state(),
1812 modifier_slices: node.modifier_slices_snapshot(),
1813 children: z_order.as_deref().unwrap_or(&node.children),
1814 window_root: node.is_window_root(),
1815 }));
1816 }
1817 let node = node.downcast_ref::<SubcomposeLayoutNode>()?;
1818 let layout_state = node.layout_state();
1819 let modifier_slices = node.modifier_slices_snapshot();
1820 Some(node.with_active_children(|children| {
1821 let z_order = z_ordered_children(applier, children);
1822 read(SnapshotNodeData {
1823 layout_state,
1824 modifier_slices,
1825 children: z_order.as_deref().unwrap_or(children),
1826 window_root: false,
1827 })
1828 }))
1829}
1830
1831fn z_ordered_children(applier: &MemoryApplier, children: &[NodeId]) -> Option<Vec<NodeId>> {
1837 if children.len() < 2 || !cranpose_ui::z_index_in_use() {
1838 return None;
1839 }
1840 let mut previous = f32::NEG_INFINITY;
1841 let in_order = children.iter().all(|&child| {
1842 let z_index = node_z_index(applier, child);
1843 let ordered = z_index >= previous;
1844 previous = z_index;
1845 ordered
1846 });
1847 if in_order {
1848 return None;
1849 }
1850 let mut ordered: Vec<(f32, NodeId)> = children
1851 .iter()
1852 .map(|&child| (node_z_index(applier, child), child))
1853 .collect();
1854 ordered.sort_by(|a, b| a.0.total_cmp(&b.0));
1855 Some(ordered.into_iter().map(|(_, child)| child).collect())
1856}
1857
1858fn node_z_index(applier: &MemoryApplier, node_id: NodeId) -> f32 {
1859 let Ok(node) = applier.get_ref(node_id) else {
1860 return 0.0;
1861 };
1862 let node: &dyn Any = node;
1863 if let Some(node) = node.downcast_ref::<LayoutNode>() {
1864 return node.modifier_slices_snapshot().z_index();
1865 }
1866 node.downcast_ref::<SubcomposeLayoutNode>()
1867 .map_or(0.0, |node| node.modifier_slices_snapshot().z_index())
1868}
1869
1870fn hit_test_from_slices(slices: &Rc<ModifierNodeSlices>) -> Option<HitTestNode> {
1871 slices_hit_something(slices).then(|| HitTestNode {
1872 handlers: Rc::clone(slices),
1873 })
1874}
1875
1876fn layer_and_node_bounds(slices: &ModifierNodeSlices, size: Size) -> (Rect, Option<Rect>) {
1879 let node_bounds = Rect::from_size(size);
1880 let layer_bounds = slices.layer_bounds(size);
1881 (
1882 layer_bounds,
1883 (layer_bounds != node_bounds).then_some(node_bounds),
1884 )
1885}
1886
1887fn slices_hit_something(slices: &ModifierNodeSlices) -> bool {
1890 !slices.pointer_inputs().is_empty() || slices.pointer_icon().is_some()
1891}
1892
1893fn prepare_node_layer(
1894 node_id: NodeId,
1895 slices: &Rc<ModifierNodeSlices>,
1896 layout_state: &cranpose_ui::widgets::LayoutState,
1897 context: LowerContext,
1898) -> (LayerHead, LowerContext) {
1899 let size = layout_state.size();
1900 let placement = layout_state.position();
1901 let (local_bounds, node_bounds) = layer_and_node_bounds(slices, size);
1902 let mut graphics_layer = slices.graphics_layer();
1903 shape_clip(
1904 &mut graphics_layer,
1905 slices.clip_to_bounds(),
1906 slices.corner_shape(),
1907 local_bounds,
1908 );
1909 let geometry = context.parent_abs.map(|parent_abs| {
1910 TranslateGeometry::new(
1911 layout_state,
1912 graphics_layer.as_ref().unwrap_or(&GraphicsLayer::DEFAULT),
1913 local_bounds,
1914 parent_abs,
1915 )
1916 });
1917 if let Some(geometry) = geometry {
1918 slices.publish_window_geometry(geometry.top_left, geometry.window_transform, size);
1919 } else {
1920 slices.publish_pointer_input_size(size);
1921 }
1922 let content_offset = layout_state.content_offset();
1923 let child_context = context.for_children(
1924 slices,
1925 content_offset,
1926 geometry.map(TranslateGeometry::child_abs),
1927 );
1928 let head = node_layer_head(
1929 node_id,
1930 slices,
1931 NodeFrame {
1932 local_bounds,
1933 node_bounds,
1934 placement,
1935 content_offset,
1936 translated_content_offset: slices.translated_content_offset().unwrap_or(content_offset),
1937 },
1938 graphics_layer,
1939 context,
1940 );
1941 (head, child_context)
1942}
1943
1944fn write_node_layer(
1945 applier: &MemoryApplier,
1946 node_id: NodeId,
1947 data: SnapshotNodeData<'_>,
1948 context: LowerContext,
1949 target: &mut LayerNode,
1950) {
1951 let SnapshotNodeData {
1952 layout_state,
1953 modifier_slices: slices,
1954 children,
1955 window_root: _,
1956 } = data;
1957 let size = layout_state.size();
1958 let placement = layout_state.position();
1959 if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
1960 eprintln!(
1961 "[scene-update-diag] build layer node={node_id:?} size=({:.2},{:.2}) pos=({:.2},{:.2})",
1962 size.width, size.height, placement.x, placement.y,
1963 );
1964 }
1965 let (head, child_context) = prepare_node_layer(node_id, &slices, &layout_state, context);
1966 let outer = outer_draws(
1967 node_id,
1968 slices.draw_commands(),
1969 slices.outer_draw_command_count(),
1970 size,
1971 );
1972 write_layer_with_outer(
1973 target,
1974 head,
1975 outer,
1976 placement,
1977 context.parent_content_offset,
1978 |list| {
1979 write_node_content(list, node_id, &slices, size, (0, children.len()), |list| {
1980 for &child_id in children {
1981 if let Some(child) = lower_child(applier, child_id, child_context) {
1982 list.push(RenderNode::Layer(child));
1983 }
1984 }
1985 });
1986 },
1987 );
1988}
1989
1990pub(crate) struct RecorderSlot {
1991 generation: u64,
1992 handles: [Option<Rc<CommandRecording>>; 3],
1993 spare: Option<Rc<CommandRecording>>,
1996}
1997
1998#[derive(Clone, Copy, PartialEq, Eq, Hash)]
1999pub(crate) struct RecorderKey {
2000 node_id: NodeId,
2001 command_index: u32,
2002}
2003
2004impl From<DrawCommandId> for RecorderKey {
2005 fn from(id: DrawCommandId) -> Self {
2006 Self {
2007 node_id: id.node_id,
2008 command_index: id.command_index,
2009 }
2010 }
2011}
2012
2013#[doc(hidden)]
2014pub fn clear_command_recordings_for_tests() {
2015 crate::thread_state::with(|state| state.command_recordings.borrow_mut().clear());
2016}
2017
2018fn bump_recording_generation() {
2019 crate::thread_state::with(|state| {
2020 let generation = state.recording_generation.get().wrapping_add(1);
2021 state.recording_generation.set(generation);
2022 if generation.is_multiple_of(512) {
2023 state
2024 .command_recordings
2025 .borrow_mut()
2026 .retain(|_, slot| generation.wrapping_sub(slot.generation) <= 64);
2027 }
2028 });
2029}
2030
2031fn acquire_storage(id: DrawCommandId) -> CommandRecording {
2032 crate::thread_state::with(|state| {
2033 let mut map = state.command_recordings.borrow_mut();
2034 let Some(slot) = map.get_mut(&RecorderKey::from(id)) else {
2035 return CommandRecording::default();
2036 };
2037 for handle in &mut slot.handles {
2038 let Some(recording) = handle.as_mut().and_then(Rc::get_mut) else {
2039 continue;
2040 };
2041 let Some(storage) = recording.try_take_reusable() else {
2042 continue;
2043 };
2044 slot.spare = handle.take();
2045 return storage;
2046 }
2047 if slot.handles.iter().all(Option::is_some) {
2048 let storage = slot.handles[2]
2049 .as_mut()
2050 .and_then(Rc::get_mut)
2051 .map(std::mem::take);
2052 if let Some(storage) = storage {
2053 slot.spare = slot.handles[2].take();
2054 return storage;
2055 }
2056 }
2057 CommandRecording::default()
2058 })
2059}
2060
2061fn publish_recording(id: DrawCommandId, recording: CommandRecording) -> Rc<CommandRecording> {
2062 crate::thread_state::with(|state| {
2063 let mut map = state.command_recordings.borrow_mut();
2064 let generation = state.recording_generation.get();
2065 let slot = map
2066 .entry(RecorderKey::from(id))
2067 .or_insert_with(|| RecorderSlot {
2068 generation,
2069 handles: [None, None, None],
2070 spare: None,
2071 });
2072 let shared = match slot.spare.take() {
2073 Some(mut spare) => match Rc::get_mut(&mut spare) {
2074 Some(storage) => {
2075 *storage = recording;
2076 spare
2077 }
2078 None => Rc::new(recording),
2079 },
2080 None => Rc::new(recording),
2081 };
2082 slot.generation = generation;
2083 slot.handles[2] = slot.handles[1].take();
2084 slot.handles[1] = slot.handles[0].take();
2085 slot.handles[0] = Some(Rc::clone(&shared));
2086 shared
2087 })
2088}
2089
2090enum DrawPass<'a> {
2091 Record(DrawPlacement),
2092 OverlayFrom(std::slice::Iter<'a, RenderNode>),
2093 OverlayFromOutput(usize),
2094}
2095
2096impl DrawPass<'_> {
2097 fn placement(&self) -> DrawPlacement {
2098 match self {
2099 Self::Record(placement) => *placement,
2100 Self::OverlayFrom(_) | Self::OverlayFromOutput(_) => DrawPlacement::Overlay,
2101 }
2102 }
2103
2104 fn take_recording(
2105 &mut self,
2106 command: &DrawCommand,
2107 output: &[RenderNode],
2108 ) -> Option<Rc<CommandRecording>> {
2109 if matches!(command, DrawCommand::Overlay(_)) {
2110 return None;
2111 }
2112 let node = match self {
2113 Self::Record(_) => return None,
2114 Self::OverlayFrom(nodes) => nodes.next().expect("each behind command has a draw run"),
2115 Self::OverlayFromOutput(index) => {
2116 let node = &output[*index];
2117 *index += 1;
2118 node
2119 }
2120 };
2121 let RenderNode::DrawRun(run) = node else {
2122 unreachable!("behind commands produce draw runs");
2123 };
2124 matches!(command, DrawCommand::WithContent(_)).then(|| Rc::clone(&run.recording))
2125 }
2126}
2127
2128fn layer_node_capacity(commands: &[DrawCommand], children: usize, has_text: bool) -> usize {
2129 children
2130 + usize::from(has_text)
2131 + commands.len()
2132 + commands
2133 .iter()
2134 .filter(|command| matches!(command, DrawCommand::WithContent(_)))
2135 .count()
2136}
2137
2138fn draw_nodes(
2139 node_id: NodeId,
2140 commands: &[DrawCommand],
2141 first_command_index: usize,
2142 placement: DrawPlacement,
2143 size: Size,
2144 phase: PrimitivePhase,
2145) -> Vec<RenderNode> {
2146 let mut nodes = crate::layer_recycling::child_list(commands.len());
2147 append_draw_nodes(
2148 &mut nodes,
2149 node_id,
2150 commands,
2151 first_command_index,
2152 DrawPass::Record(placement),
2153 size,
2154 phase,
2155 );
2156 nodes
2157}
2158
2159fn append_draw_nodes(
2160 nodes: &mut Vec<RenderNode>,
2161 node_id: NodeId,
2162 commands: &[DrawCommand],
2163 first_command_index: usize,
2164 mut pass: DrawPass<'_>,
2165 size: Size,
2166 phase: PrimitivePhase,
2167) {
2168 let placement = pass.placement();
2169 for (command_index, command) in commands.iter().enumerate() {
2170 let id = DrawCommandId {
2171 node_id,
2172 command_index: (first_command_index + command_index) as u32,
2173 placement,
2174 };
2175 let (shared, segments) = if let Some(shared) = pass.take_recording(command, nodes) {
2176 let segments = shared.content_split(false);
2177 (shared, segments)
2178 } else {
2179 let Some((recording, segments)) =
2180 recording_for_placement_reusing(command, placement, size, || acquire_storage(id))
2181 else {
2182 continue;
2183 };
2184 (publish_recording(id, recording), segments)
2185 };
2186 nodes.push(RenderNode::DrawRun(DrawRunNode::for_command_shared(
2187 phase,
2188 Some(id),
2189 shared,
2190 segments,
2191 )));
2192 }
2193}
2194
2195#[doc(hidden)]
2196pub fn draw_command_nodes_for_tests(
2197 node_id: NodeId,
2198 commands: &[DrawCommand],
2199 placement: DrawPlacement,
2200 size: Size,
2201 phase: PrimitivePhase,
2202) -> Vec<RenderNode> {
2203 bump_recording_generation();
2204 draw_nodes(node_id, commands, 0, placement, size, phase)
2205}
2206
2207struct OuterDraws {
2208 behind: Vec<RenderNode>,
2209 overlay: Vec<RenderNode>,
2210}
2211
2212fn outer_draws(
2213 node_id: NodeId,
2214 draw_commands: &[DrawCommand],
2215 outer_draw_command_count: usize,
2216 size: Size,
2217) -> Option<OuterDraws> {
2218 (outer_draw_command_count > 0).then(|| {
2219 let commands = &draw_commands[..outer_draw_command_count];
2220 let behind = draw_nodes(
2221 node_id,
2222 commands,
2223 0,
2224 DrawPlacement::Behind,
2225 size,
2226 PrimitivePhase::BeforeChildren,
2227 );
2228 let mut overlay = crate::layer_recycling::child_list(commands.len());
2229 append_draw_nodes(
2230 &mut overlay,
2231 node_id,
2232 commands,
2233 0,
2234 DrawPass::OverlayFrom(behind.iter()),
2235 size,
2236 PrimitivePhase::AfterChildren,
2237 );
2238 OuterDraws { behind, overlay }
2239 })
2240}
2241
2242fn write_wrapper(
2243 wrapper: &mut LayerNode,
2244 mut layer: Box<LayerNode>,
2245 placement: Point,
2246 outer: OuterDraws,
2247 parent_content_offset: Point,
2248) {
2249 debug_assert!(wrapper.children.is_empty(), "a wrapper is written emptied");
2250 layer.transform_to_parent =
2251 layer_transform_to_parent(layer.local_bounds, Point::default(), &layer.graphics_layer);
2252 layer.origin_in_parent = Point::default();
2253 let node_rect = layer.node_rect();
2254 let graphics_layer = None;
2255 let head = LayerHead {
2256 node_id: None,
2257 wraps: layer.node_id,
2258 local_bounds: node_rect,
2259 node_bounds: None,
2260 transform_to_parent: placed_transform(
2261 node_rect,
2262 placement,
2263 &GraphicsLayer::DEFAULT,
2264 parent_content_offset,
2265 ),
2266 content_offset: Point::default(),
2267 motion_context_animated: layer.motion_context_animated,
2268 translated_content_context: false,
2269 translated_content_offset: Point::default(),
2270 origin_in_parent: placement,
2271 graphics_layer,
2272 clip_to_bounds: false,
2273 hit_test: None,
2274 has_origin_sinks: false,
2275 isolation: IsolationReasons::default(),
2276 cache_policy: CachePolicy::None,
2277 };
2278 let OuterDraws {
2279 mut behind,
2280 mut overlay,
2281 } = outer;
2282 let children = &mut wrapper.children;
2283 crate::layer_recycling::reserve_nodes(children, behind.len() + 1 + overlay.len());
2284 children.append(&mut behind);
2285 children.push(RenderNode::Layer(layer));
2286 children.append(&mut overlay);
2287 crate::layer_recycling::recycle_list(behind);
2288 crate::layer_recycling::recycle_list(overlay);
2289 assign_layer(wrapper, head);
2290}
2291
2292fn take_wrapped_layer(wrapper: &mut LayerNode, node_id: NodeId) -> Option<Box<LayerNode>> {
2293 let mut wrapped = None;
2294 for child in wrapper.children.drain(..) {
2295 if let RenderNode::Layer(layer) = child
2296 && layer.node_id == Some(node_id)
2297 {
2298 wrapped = Some(layer);
2299 }
2300 }
2301 wrapped
2302}
2303
2304fn layer_identity(layer: &LayerNode) -> Option<NodeId> {
2305 layer.node_id.or(layer.wraps)
2306}
2307
2308struct TextNodeParts<'a> {
2309 node_id: NodeId,
2310 text_rect: Rect,
2312 text_style: Option<&'a TextStyle>,
2313 text_layout_options: Option<TextLayoutOptions>,
2314 text_pan: Option<TextPanResolver>,
2315 measured_layout: Option<Rc<PreparedTextLayout>>,
2316}
2317
2318fn text_node_from_parts(parts: TextNodeParts<'_>) -> Option<TextPrimitiveNode> {
2319 let TextNodeParts {
2320 node_id,
2321 text_rect,
2322 text_style,
2323 text_layout_options,
2324 text_pan,
2325 measured_layout,
2326 } = parts;
2327 let prepared = measured_layout?;
2328 let default_text_style;
2329 let text_style = match text_style {
2330 Some(style) => style,
2331 None => {
2332 default_text_style = TextStyle::default();
2333 &default_text_style
2334 }
2335 };
2336 let options = text_layout_options.unwrap_or_default().normalized();
2337 let content_width = text_rect.width.max(0.0);
2338 if content_width <= 0.0 {
2339 return None;
2340 }
2341
2342 let pan_offset = text_pan
2343 .as_ref()
2344 .map_or(0.0, |resolve| resolve(content_width));
2345 let pans_horizontally = text_pan.is_some();
2346
2347 let visual_style = &prepared.visual_style;
2348 let measured_draw_width = prepared.metrics.width.max(0.0);
2349 let draw_width = if options.overflow == TextOverflow::Visible || pans_horizontally {
2350 measured_draw_width
2351 } else {
2352 measured_draw_width.min(content_width)
2353 };
2354 let alignment_offset = resolve_text_horizontal_offset(
2355 text_style,
2356 prepared.text.text.as_str(),
2357 content_width,
2358 prepared.metrics.width,
2359 );
2360 let rect = Rect {
2361 x: text_rect.x + alignment_offset - pan_offset,
2362 y: text_rect.y,
2363 width: draw_width,
2364 height: prepared.metrics.height,
2365 };
2366 let text_bounds = Rect {
2367 width: content_width,
2368 height: text_rect.height.max(0.0),
2369 ..text_rect
2370 };
2371 let font_size = visual_style.resolve_font_size(14.0);
2372 let expanded_bounds =
2373 expand_text_bounds_for_baseline_shift(text_bounds, visual_style, font_size);
2374 let clip = if options.overflow == TextOverflow::Visible && !pans_horizontally {
2375 None
2376 } else {
2377 Some(pad_clip_rect(expanded_bounds))
2378 };
2379
2380 Some(TextPrimitiveNode {
2381 node_id,
2382 rect,
2383 text: Rc::clone(&prepared.text),
2384 render_text: prepared.render_text(),
2385 text_style: std::sync::Arc::clone(visual_style),
2386 font_size,
2387 layout_options: options,
2388 clip,
2389 paint: TextPaintCache::holding(TextPaint::of(
2390 visual_style,
2391 prepared.visual_style_hash(),
2392 &prepared.text.span_styles,
2393 font_size,
2394 (prepared.render_text_hash(), prepared.text.text.is_empty()),
2395 )),
2396 })
2397}
2398
2399fn layout_box_to_snapshot(node: &LayoutBox, parent: Option<&LayoutBox>) -> BuildNodeSnapshot {
2400 let placement = parent
2401 .map(|parent_box| Point {
2402 x: node.rect.x - parent_box.rect.x - parent_box.content_offset.x,
2403 y: node.rect.y - parent_box.rect.y - parent_box.content_offset.y,
2404 })
2405 .unwrap_or_default();
2406 let mut children = Vec::with_capacity(node.children.len());
2407 for child in &node.children {
2408 children.push(layout_box_to_snapshot(child, Some(node)));
2409 }
2410 if cranpose_ui::z_index_in_use() {
2411 children.sort_by(|a, b| a.slices.z_index().total_cmp(&b.slices.z_index()));
2413 }
2414 let mut graphics_layer = node.node_data.modifier_slices.graphics_layer();
2415 let has_graphics_layer = graphics_layer.is_some();
2416 shape_clip(
2417 &mut graphics_layer,
2418 node.node_data.modifier_slices.clip_to_bounds(),
2419 node.node_data.modifier_slices.corner_shape(),
2420 Rect {
2421 x: 0.0,
2422 y: 0.0,
2423 width: node.rect.width,
2424 height: node.rect.height,
2425 },
2426 );
2427 let has_graphics_layer = has_graphics_layer
2428 || graphics_layer
2429 .as_ref()
2430 .is_some_and(|layer| layer.render_effect.is_some());
2431
2432 BuildNodeSnapshot {
2433 node_id: node.node_id,
2434 placement,
2435 size: Size {
2436 width: node.rect.width,
2437 height: node.rect.height,
2438 },
2439 content_offset: node.content_offset,
2440 slices: Rc::clone(&node.node_data.modifier_slices),
2441 graphics_layer: graphics_layer.filter(|_| has_graphics_layer),
2442 children,
2443 }
2444}
2445
2446fn modifier_slices_have_origin_sinks(slices: &ModifierNodeSlices) -> bool {
2447 slices.text_window().is_some() || slices.viewport_window_rect().is_some()
2448}
2449
2450fn shape_clip(
2454 graphics_layer: &mut Option<GraphicsLayer>,
2455 clip_to_bounds: bool,
2456 corner_shape: Option<RoundedCornerShape>,
2457 local_bounds: Rect,
2458) {
2459 if !clip_to_bounds {
2460 return;
2461 }
2462
2463 let Some(corner_shape) = corner_shape else {
2464 return;
2465 };
2466 let radii = corner_shape.resolve(local_bounds.width, local_bounds.height);
2467 if radii.top_left <= f32::EPSILON
2468 && radii.top_right <= f32::EPSILON
2469 && radii.bottom_right <= f32::EPSILON
2470 && radii.bottom_left <= f32::EPSILON
2471 {
2472 return;
2473 }
2474
2475 let properties = graphics_layer.get_or_insert_default();
2476 if let Some(existing) = properties.render_effect.take() {
2477 let rounded_clip = rounded_corner_alpha_mask_effect(
2478 local_bounds.width,
2479 local_bounds.height,
2480 radii,
2481 ROUNDED_CLIP_EDGE_FEATHER,
2482 );
2483 properties.render_effect = Some(existing.then(rounded_clip));
2484 } else {
2485 properties.shape = LayerShape::Rounded(corner_shape);
2486 properties.clip = true;
2487 }
2488}
2489
2490fn layer_cache_policy(layer: &GraphicsLayer, isolation: IsolationReasons) -> CachePolicy {
2491 if isolation.has_any() || layer_scales_or_rotates(layer) {
2492 CachePolicy::Auto
2493 } else {
2494 CachePolicy::None
2495 }
2496}
2497
2498fn isolation_reasons(layer: &GraphicsLayer) -> IsolationReasons {
2499 IsolationReasons {
2500 explicit_offscreen: layer.compositing_strategy == CompositingStrategy::Offscreen,
2501 shape_clip: layer.clip && !matches!(layer.shape, LayerShape::Rectangle),
2502 effect: layer.render_effect.is_some(),
2503 backdrop: layer.backdrop_effect.is_some(),
2504 group_opacity: layer.compositing_strategy != CompositingStrategy::ModulateAlpha
2505 && layer.alpha < 1.0,
2506 blend_mode: layer.blend_mode != cranpose_ui::BlendMode::SrcOver,
2507 }
2508}
2509
2510fn pad_clip_rect(rect: Rect) -> Rect {
2511 Rect {
2512 x: rect.x - TEXT_CLIP_PAD,
2513 y: rect.y - TEXT_CLIP_PAD,
2514 width: (rect.width + TEXT_CLIP_PAD * 2.0).max(0.0),
2515 height: (rect.height + TEXT_CLIP_PAD * 2.0).max(0.0),
2516 }
2517}
2518
2519pub fn expand_text_bounds_for_baseline_shift(
2523 text_bounds: Rect,
2524 text_style: &TextStyle,
2525 font_size: f32,
2526) -> Rect {
2527 let baseline_shift_px = text_style
2528 .span_style
2529 .baseline_shift
2530 .filter(|shift| shift.is_specified())
2531 .map_or(0.0, |shift| -(shift.0 * font_size));
2532 if baseline_shift_px == 0.0 {
2533 return text_bounds;
2534 }
2535
2536 if baseline_shift_px < 0.0 {
2537 Rect {
2538 x: text_bounds.x,
2539 y: text_bounds.y + baseline_shift_px,
2540 width: text_bounds.width,
2541 height: (text_bounds.height - baseline_shift_px).max(0.0),
2542 }
2543 } else {
2544 Rect {
2545 x: text_bounds.x,
2546 y: text_bounds.y,
2547 width: text_bounds.width,
2548 height: (text_bounds.height + baseline_shift_px).max(0.0),
2549 }
2550 }
2551}
2552
2553fn resolve_text_horizontal_offset(
2554 text_style: &TextStyle,
2555 text: &str,
2556 content_width: f32,
2557 measured_width: f32,
2558) -> f32 {
2559 let remaining = (content_width - measured_width).max(0.0);
2560 if remaining == 0.0 {
2561 return 0.0;
2562 }
2563 remaining * cranpose_ui::text::text_align_fraction(text_style, text)
2564}
2565
2566#[cfg(test)]
2567#[path = "tests/scene_builder_tests.rs"]
2568mod tests;