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