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