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