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