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};
28
29const TEXT_CLIP_PAD: f32 = 1.0;
30const ROUNDED_CLIP_EDGE_FEATHER: f32 = 1.0;
31
32#[derive(Clone, Default)]
33struct BuildNodeSnapshot {
34 node_id: NodeId,
35 placement: Point,
36 size: Size,
37 content_offset: Point,
38 slices: Rc<ModifierNodeSlices>,
41 graphics_layer: Option<GraphicsLayer>,
42 children: Vec<Self>,
43}
44
45struct SnapshotNodeData<'a> {
46 layout_state: cranpose_ui::widgets::LayoutState,
47 modifier_slices: Rc<ModifierNodeSlices>,
48 children: &'a [NodeId],
49 window_root: bool,
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum GraphRebuildReason {
62 RootLayerUnavailable,
64 DirtyLayerUnavailable,
66 UnmatchedDirtyNodes(usize),
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub enum GraphUpdate {
75 Patched,
76 NeedsRebuild(GraphRebuildReason),
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct GraphUpdateReport {
81 pub update: GraphUpdate,
82 pub hit_graph_dirty: bool,
83}
84
85impl GraphUpdateReport {
86 pub fn applied(self) -> bool {
87 matches!(self.update, GraphUpdate::Patched)
88 }
89
90 pub fn rebuild_reason(self) -> Option<GraphRebuildReason> {
91 match self.update {
92 GraphUpdate::Patched => None,
93 GraphUpdate::NeedsRebuild(reason) => Some(reason),
94 }
95 }
96}
97
98#[cfg(test)]
99thread_local! {
100 static LOWERED_LAYER_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
101}
102
103fn note_layer_lowered() {
104 #[cfg(test)]
105 LOWERED_LAYER_COUNT.with(|count| count.set(count.get() + 1));
106}
107
108#[cfg(test)]
109fn reset_lowered_layer_count() {
110 LOWERED_LAYER_COUNT.with(|count| count.set(0));
111}
112
113#[cfg(test)]
114fn lowered_layer_count() -> usize {
115 LOWERED_LAYER_COUNT.with(Cell::get)
116}
117
118pub fn build_graph_from_layout_tree(root: &LayoutBox, _scale: f32) -> RenderGraph {
119 bump_recording_generation();
120 let root_snapshot = layout_box_to_snapshot(root, None);
121 let mut graph = RenderGraph::new(LayerNode::default());
122 write_snapshot_layer(root_snapshot, LowerContext::ROOT, &mut graph.root);
123 graph
124}
125
126pub fn build_graph_from_applier(
127 applier: &MemoryApplier,
128 root: NodeId,
129 _scale: f32,
130) -> Option<RenderGraph> {
131 bump_recording_generation();
132 let mut graph = RenderGraph::new(LayerNode::default());
133 lower_root_into(applier, root, &mut graph.root).then_some(graph)
134}
135
136pub fn rebuild_graph_from_applier(
139 applier: &MemoryApplier,
140 root: NodeId,
141 scale: f32,
142 previous: Option<RenderGraph>,
143) -> Option<RenderGraph> {
144 let graph = match previous {
145 Some(mut graph) => {
146 release_for_rebuild(&mut graph.root);
147 bump_recording_generation();
148 graph.update += 1;
149 graph.root.note_content_change(graph.update);
150 lower_root_into(applier, root, &mut graph.root).then_some(graph)
151 }
152 None => build_graph_from_applier(applier, root, scale),
153 };
154 crate::layer_recycling::release();
155 graph
156}
157
158pub fn update_graph_from_applier(
159 applier: &MemoryApplier,
160 graph: &mut RenderGraph,
161 updates: SceneUpdates<'_>,
162 scale: f32,
163) -> bool {
164 update_graph_from_applier_report(applier, graph, updates, scale).applied()
165}
166
167pub fn update_graph_from_applier_report(
168 applier: &MemoryApplier,
169 graph: &mut RenderGraph,
170 updates: SceneUpdates<'_>,
171 _scale: f32,
172) -> GraphUpdateReport {
173 let report = update_graph_from_applier_report_inner(applier, graph, updates);
174 crate::layer_recycling::release();
175 if let GraphUpdate::NeedsRebuild(reason) = report.update
176 && cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG")
177 {
178 eprintln!(
179 "[scene-update-diag] scoped update abandoned, whole scene rebuilt: {reason:?} dirty={}",
180 updates.content.len() + updates.layers.len() + updates.moved.len()
181 );
182 }
183 report
184}
185
186#[derive(Clone, Copy)]
187enum NodeUpdate {
188 Content,
189 Layer,
190 Moved,
193 MovedLayer,
196}
197
198fn dirty_node_updates(updates: SceneUpdates<'_>) -> HashMap<NodeId, NodeUpdate> {
201 let mut dirty = HashMap::with_capacity_and_hasher(
202 updates.moved.len() + updates.layers.len() + updates.content.len(),
203 Default::default(),
204 );
205 dirty.extend(updates.moved.iter().map(|&id| (id, NodeUpdate::Moved)));
206 for &id in updates.layers {
207 let kind = match dirty.get(&id) {
208 Some(NodeUpdate::Moved) => NodeUpdate::MovedLayer,
209 _ => NodeUpdate::Layer,
210 };
211 dirty.insert(id, kind);
212 }
213 dirty.extend(updates.content.iter().map(|&id| (id, NodeUpdate::Content)));
214 dirty
215}
216
217fn update_graph_from_applier_report_inner(
218 applier: &MemoryApplier,
219 graph: &mut RenderGraph,
220 updates: SceneUpdates<'_>,
221) -> GraphUpdateReport {
222 if updates.is_empty() {
223 return GraphUpdateReport {
224 update: GraphUpdate::Patched,
225 hit_graph_dirty: false,
226 };
227 }
228 bump_recording_generation();
229 graph.update += 1;
230 let update = graph.update;
231
232 if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
233 eprintln!("[scene-update-diag] dirty={updates:?}");
234 }
235
236 let mut remaining_dirty_nodes = dirty_node_updates(updates);
237 if let Some(root_id) = layer_identity(&graph.root)
238 && let Some(kind) = remaining_dirty_nodes.remove(&root_id)
239 {
240 if let Some(hit_graph_dirty) = try_update_retained_layer(
241 applier,
242 &mut graph.root,
243 &mut remaining_dirty_nodes,
244 kind,
245 true,
246 TranslateAncestorContext {
247 inherited_motion_context_animated: false,
248 update,
249 inherited_translated_content_context: false,
250 parent_content_offset: Point::default(),
251 parent_abs: AbsOrigin::ROOT,
252 },
253 ) {
254 if remaining_dirty_nodes.is_empty() {
255 return GraphUpdateReport {
256 update: GraphUpdate::Patched,
257 hit_graph_dirty,
258 };
259 }
260 let inherited = graph.root.translated_content_context;
261 let root_children = AbsOrigin::ROOT.children_of(&graph.root);
262 let ancestry = dirty_ancestry(applier, &remaining_dirty_nodes);
263 let walked = replace_dirty_layers_from_applier(
264 applier,
265 &mut graph.root,
266 root_children,
267 DirtyWalk {
268 nodes: &mut remaining_dirty_nodes,
269 ancestry: &ancestry,
270 },
271 inherited,
272 update,
273 );
274 return GraphUpdateReport {
275 update: classify_walk(applier, walked.is_some(), &mut remaining_dirty_nodes),
276 hit_graph_dirty: hit_graph_dirty || walked.is_none_or(|r| r.hit_graph_dirty),
277 };
278 }
279
280 let previous = HitGraphState::of(&graph.root);
281 release_for_rebuild(&mut graph.root);
282 if !lower_root_into(applier, root_id, &mut graph.root) {
283 return GraphUpdateReport {
284 update: GraphUpdate::NeedsRebuild(GraphRebuildReason::RootLayerUnavailable),
285 hit_graph_dirty: true,
286 };
287 }
288 let hit_graph_dirty = previous.dirty_against(&HitGraphState::of(&graph.root));
289 graph.root.note_content_change(update);
290
291 return GraphUpdateReport {
292 update: GraphUpdate::Patched,
293 hit_graph_dirty,
294 };
295 }
296
297 let inherited_translated_content_context = graph.root.translated_content_context;
298 let root_children = AbsOrigin::ROOT.children_of(&graph.root);
299 let ancestry = dirty_ancestry(applier, &remaining_dirty_nodes);
300 let Some(report) = replace_dirty_layers_from_applier(
301 applier,
302 &mut graph.root,
303 root_children,
304 DirtyWalk {
305 nodes: &mut remaining_dirty_nodes,
306 ancestry: &ancestry,
307 },
308 inherited_translated_content_context,
309 update,
310 ) else {
311 return GraphUpdateReport {
312 update: GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable),
313 hit_graph_dirty: true,
314 };
315 };
316
317 match classify_walk(applier, true, &mut remaining_dirty_nodes) {
318 GraphUpdate::Patched => GraphUpdateReport {
319 update: GraphUpdate::Patched,
320 hit_graph_dirty: report.hit_graph_dirty,
321 },
322 update => GraphUpdateReport {
323 update,
324 hit_graph_dirty: true,
325 },
326 }
327}
328
329fn classify_walk(
332 applier: &MemoryApplier,
333 walked: bool,
334 remaining_dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
335) -> GraphUpdate {
336 if !walked {
337 return GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable);
338 }
339 remaining_dirty_nodes.retain(|&node, _| is_drawn(applier, node));
340 if !remaining_dirty_nodes.is_empty() {
341 GraphUpdate::NeedsRebuild(GraphRebuildReason::UnmatchedDirtyNodes(
342 remaining_dirty_nodes.len(),
343 ))
344 } else {
345 GraphUpdate::Patched
346 }
347}
348
349fn is_drawn(applier: &MemoryApplier, node: NodeId) -> bool {
354 let mut child = node;
355 let mut current = node;
356 while let Some(parent) = applier
357 .get_ref(current)
358 .ok()
359 .and_then(cranpose_core::Node::parent)
360 {
361 let Ok(parent_node) = applier.get_ref(parent) else {
362 return true;
363 };
364 current = parent;
365 if parent_node.is_virtual() {
366 continue;
367 }
368 let parent_node: &dyn Any = parent_node;
369 if let Some(subcompose) = parent_node.downcast_ref::<SubcomposeLayoutNode>()
370 && !subcompose.with_active_children(|children| children.contains(&child))
371 {
372 return false;
373 }
374 child = parent;
375 }
376 true
377}
378
379#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
380struct ReplaceDirtyLayersReport {
381 updated: bool,
382 hit_graph_dirty: bool,
383}
384
385struct DirtyWalk<'a> {
389 nodes: &'a mut HashMap<NodeId, NodeUpdate>,
390 ancestry: &'a HashSet<NodeId>,
391}
392
393impl DirtyWalk<'_> {
394 fn reborrow(&mut self) -> DirtyWalk<'_> {
395 DirtyWalk {
396 nodes: self.nodes,
397 ancestry: self.ancestry,
398 }
399 }
400
401 fn may_hold(&self, identity: Option<NodeId>) -> bool {
404 identity.is_none_or(|id| self.ancestry.contains(&id))
405 }
406}
407
408fn dirty_ancestry(applier: &MemoryApplier, dirty: &HashMap<NodeId, NodeUpdate>) -> HashSet<NodeId> {
410 let mut ancestry = HashSet::default();
411 for &node in dirty.keys() {
412 let mut current = node;
413 while let Some(parent) = applier
414 .get_ref(current)
415 .ok()
416 .and_then(cranpose_core::Node::parent)
417 {
418 if !ancestry.insert(parent) {
419 break;
420 }
421 current = parent;
422 }
423 }
424 ancestry
425}
426
427fn replace_dirty_layers_from_applier(
428 applier: &MemoryApplier,
429 parent: &mut LayerNode,
430 parent_children: AbsOrigin,
431 mut dirty: DirtyWalk<'_>,
432 inherited_translated_content_context: bool,
433 update: u64,
434) -> Option<ReplaceDirtyLayersReport> {
435 if dirty.nodes.is_empty() {
436 return Some(ReplaceDirtyLayersReport::default());
437 }
438
439 let child_inherited_translated_content_context =
440 inherited_translated_content_context || parent.translated_content_context;
441 let mut report = ReplaceDirtyLayersReport::default();
442
443 for child in &mut parent.children {
444 if dirty.nodes.is_empty() {
445 break;
446 }
447 let RenderNode::Layer(child_layer) = child else {
448 continue;
449 };
450
451 let identity = layer_identity(child_layer);
452 if let Some(kind) = identity.and_then(|id| dirty.nodes.remove(&id)) {
453 if let Some(hit_graph_dirty) = try_update_retained_layer(
454 applier,
455 child_layer,
456 dirty.nodes,
457 kind,
458 false,
459 TranslateAncestorContext {
460 inherited_motion_context_animated: parent.motion_context_animated,
461 update,
462 inherited_translated_content_context:
463 child_inherited_translated_content_context,
464 parent_content_offset: parent.content_offset,
465 parent_abs: parent_children,
466 },
467 ) {
468 report.hit_graph_dirty |= hit_graph_dirty;
469 report.updated = true;
470 let child_children = parent_children.children_of(child_layer);
471 let child_report = replace_dirty_layers_from_applier(
472 applier,
473 child_layer,
474 child_children,
475 dirty.reborrow(),
476 child_inherited_translated_content_context,
477 update,
478 )?;
479 report.hit_graph_dirty |= child_report.hit_graph_dirty;
480 continue;
481 }
482 let node_id = layer_identity(child_layer).expect("dirty layer must have a node id");
483
484 let previous = HitGraphState::of(child_layer);
485 remove_dirty_descendants(child_layer, dirty.nodes);
489 release_for_rebuild(child_layer);
490 let context = LowerContext {
491 inherited_motion_context_animated: parent.motion_context_animated,
492 inherited_translated_content_context: child_inherited_translated_content_context,
493 parent_abs: Some(parent_children),
494 parent_content_offset: parent.content_offset,
495 };
496 read_placed_node_data(applier, node_id, false, |data| {
497 write_node_layer(applier, node_id, data, context, child_layer);
498 })?;
499 report.hit_graph_dirty |= previous.dirty_against(&HitGraphState::of(child_layer));
500 remove_dirty_descendants(child_layer, dirty.nodes);
501
502 child_layer.note_content_change(update);
503 report.updated = true;
504 continue;
505 }
506
507 if !dirty.may_hold(identity) {
508 continue;
509 }
510 let child_children = parent_children.children_of(child_layer);
511 let child_report = replace_dirty_layers_from_applier(
512 applier,
513 child_layer,
514 child_children,
515 dirty.reborrow(),
516 child_inherited_translated_content_context,
517 update,
518 )?;
519 report.updated |= child_report.updated;
520 report.hit_graph_dirty |= child_report.hit_graph_dirty;
521 }
522
523 if report.updated {
524 parent.refresh_child_facts();
525 parent.has_origin_sinks |= children_have_origin_sinks(&parent.children);
529 parent.note_content_change(update);
530 }
531
532 Some(report)
533}
534
535fn translate_bail(reason: &str) -> bool {
536 if cranpose_core::env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
537 eprintln!("[scene-update-diag] translate bail: {reason}");
538 }
539 false
540}
541
542#[derive(Clone, Copy)]
543struct TranslateAncestorContext {
544 inherited_motion_context_animated: bool,
545 update: u64,
547 inherited_translated_content_context: bool,
548 parent_content_offset: Point,
549 parent_abs: AbsOrigin,
550}
551
552fn try_update_retained_layer(
553 applier: &MemoryApplier,
554 layer: &mut LayerNode,
555 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
556 kind: NodeUpdate,
557 root: bool,
558 ancestors: TranslateAncestorContext,
559) -> Option<bool> {
560 match kind {
561 NodeUpdate::Moved if !root => {
562 move_retained_layer(applier, layer, ancestors.parent_content_offset).or_else(|| {
563 try_translate_scrolled_layer(applier, layer, dirty_nodes, ancestors).then_some(true)
564 })
565 }
566 NodeUpdate::Content | NodeUpdate::Moved => {
567 try_translate_scrolled_layer(applier, layer, dirty_nodes, ancestors).then_some(true)
568 }
569 NodeUpdate::Layer => update_layer_properties(applier, layer, root, false, ancestors),
570 NodeUpdate::MovedLayer => update_layer_properties(applier, layer, root, true, ancestors),
571 }
572}
573
574fn move_retained_layer(
579 applier: &MemoryApplier,
580 layer: &mut LayerNode,
581 parent_content_offset: Point,
582) -> Option<bool> {
583 if layer.has_origin_sinks {
584 return None;
585 }
586 let state = scene_layout_state(applier, layer_identity(layer)?)?;
587 if !state.is_placed() || Rect::from_size(state.size()) != layer.node_rect() {
588 return None;
589 }
590 let previous = HitGraphState::of(layer);
591 translate_retained_child(layer, &state, parent_content_offset);
592 Some(previous.dirty_against(&HitGraphState::of(layer)))
593}
594
595fn content_layer_mut(container: &mut LayerNode) -> Option<&mut LayerNode> {
596 let Some(node_id) = container.wraps else {
597 return Some(container);
598 };
599 container.children.iter_mut().find_map(|child| match child {
600 RenderNode::Layer(layer) if layer.node_id == Some(node_id) => Some(&mut **layer),
601 _ => None,
602 })
603}
604
605fn retained_layer_matches_layout(
608 layer: &LayerNode,
609 data: &SnapshotNodeData<'_>,
610 placement: Point,
611 wrapped: bool,
612 moved: bool,
613) -> bool {
614 let state = &data.layout_state;
615 let slices = &data.modifier_slices;
616 (moved || state.position() == placement)
617 && Rect::from_size(state.size()) == layer.node_rect()
618 && slices.layer_bounds(state.size()) == layer.local_bounds
619 && state.content_offset() == layer.content_offset
620 && (slices.outer_draw_command_count() > 0) == wrapped
621}
622
623fn update_layer_properties(
624 applier: &MemoryApplier,
625 container: &mut LayerNode,
626 root: bool,
627 moved: bool,
628 ancestors: TranslateAncestorContext,
629) -> Option<bool> {
630 let node_id = layer_identity(container)?;
631 let wrapped = container.wraps.is_some();
632 if moved && wrapped {
635 return None;
636 }
637 let placement = container.origin_in_parent;
638 let hit_graph_dirty = read_placed_node_data(applier, node_id, root, |data| {
639 let target = content_layer_mut(container)?;
640 let slices = &data.modifier_slices;
641 let state = &data.layout_state;
642 if !retained_layer_matches_layout(target, &data, placement, wrapped, moved) {
643 return None;
644 }
645 let context = LowerContext {
646 inherited_motion_context_animated: ancestors.inherited_motion_context_animated,
647 inherited_translated_content_context: ancestors.inherited_translated_content_context,
648 parent_abs: Some(ancestors.parent_abs),
649 parent_content_offset: ancestors.parent_content_offset,
650 };
651 let previous = HitGraphState::of(target);
652 let (head, child_context) = prepare_node_layer(node_id, slices, state, context);
653 assign_layer(target, head);
654 if wrapped {
655 target.transform_to_parent = layer_transform_to_parent(
656 target.local_bounds,
657 Point::default(),
658 &target.graphics_layer,
659 );
660 target.origin_in_parent = Point::default();
661 }
662 if target.has_origin_sinks
663 && let Some(child_abs) = child_context.parent_abs
664 {
665 for child in &target.children {
666 if let RenderNode::Layer(child) = child {
667 publish_retained_window_geometry(applier, child, child_abs);
668 }
669 }
670 }
671 Some(previous.dirty_against(&HitGraphState::of(target)))
672 })??;
673 if wrapped {
674 container.refresh_child_facts();
675 container.has_origin_sinks = children_have_origin_sinks(&container.children);
676 container.forget_own_raster_cache_hashes();
677 }
678 Some(hit_graph_dirty)
679}
680
681fn publish_retained_window_geometry(
682 applier: &MemoryApplier,
683 layer: &LayerNode,
684 parent_abs: AbsOrigin,
685) {
686 if !layer.has_origin_sinks {
687 return;
688 }
689 let child_abs = parent_abs.children_of(layer);
690 if let Some(node_id) = layer.node_id {
691 read_node_data(applier, node_id, |data| {
692 data.modifier_slices.publish_window_geometry(
693 Point {
694 x: parent_abs.content_origin.x + layer.origin_in_parent.x,
695 y: parent_abs.content_origin.y + layer.origin_in_parent.y,
696 },
697 child_abs.window_transform,
698 data.layout_state.size(),
699 );
700 });
701 }
702 for child in &layer.children {
703 if let RenderNode::Layer(child) = child {
704 publish_retained_window_geometry(applier, child, child_abs);
705 }
706 }
707}
708
709fn try_translate_scrolled_layer(
710 applier: &MemoryApplier,
711 container: &mut LayerNode,
712 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
713 ancestors: TranslateAncestorContext,
714) -> bool {
715 let Some(node_id) = layer_identity(container) else {
716 return translate_bail("no node id");
717 };
718 read_node_data(applier, node_id, |data| {
719 if container.wraps.is_none() {
720 return translate_layer_from_data(
721 applier,
722 container,
723 dirty_nodes,
724 ancestors,
725 data,
726 false,
727 );
728 }
729 let outer_count = data.modifier_slices.outer_draw_command_count();
730 if outer_count == 0 {
731 return translate_bail("outer draws removed");
732 }
733 let size = data.layout_state.size();
734 let placement = data.layout_state.position();
735 let slices = Rc::clone(&data.modifier_slices);
736 let Some(inner) = content_layer_mut(container) else {
737 return translate_bail("wrapped layer missing");
738 };
739 if !translate_layer_from_data(applier, inner, dirty_nodes, ancestors, data, true) {
740 return false;
741 }
742 let outer = outer_draws(node_id, slices.draw_commands(), outer_count, size)
743 .expect("outer command count is nonzero");
744 let layer =
745 take_wrapped_layer(container, node_id).expect("the wrapped layer was found above");
746 write_wrapper(
747 container,
748 layer,
749 placement,
750 outer,
751 ancestors.parent_content_offset,
752 );
753 container.note_content_change(ancestors.update);
754 true
755 })
756 .unwrap_or_else(|| translate_bail("container snapshot read failed"))
757}
758
759struct TranslatedContainer {
762 node_id: NodeId,
763 graphics_layer: Option<GraphicsLayer>,
764 local_bounds: Rect,
765 node_bounds: Option<Rect>,
766}
767
768fn translated_container(
769 container: &LayerNode,
770 layout_state: &cranpose_ui::widgets::LayoutState,
771 modifier_slices: &ModifierNodeSlices,
772 inherited_motion_context_animated: bool,
773 wrapped: bool,
774) -> Result<TranslatedContainer, &'static str> {
775 if cranpose_core::env_flag!("CRANPOSE_DISABLE_SCROLL_TRANSLATE") {
776 return Err("fast path disabled by ablation switch");
777 }
778 let Some(node_id) = container.node_id else {
779 return Err("no node id");
780 };
781 if !layout_state.is_placed() {
782 return Err("container unplaced");
783 }
784 let outer_count = modifier_slices.outer_draw_command_count();
785 if (outer_count > 0 && !wrapped)
786 || (inherited_motion_context_animated || modifier_slices.motion_context_animated())
787 != container.motion_context_animated
788 || modifier_slices.translated_content_context() != container.translated_content_context
789 {
790 return Err("container outer draws or translated context changed");
791 }
792 let clip_to_bounds = modifier_slices.clip_to_bounds();
793 if clip_to_bounds != container.clip_to_bounds {
794 return Err("container clip changed");
795 }
796 let shaped = |bounds: Rect| {
800 graphics_layer_with_shaped_clip(
801 modifier_slices.graphics_layer(),
802 clip_to_bounds,
803 modifier_slices.corner_shape(),
804 bounds,
805 )
806 };
807 if shaped(container.local_bounds)
808 .as_ref()
809 .unwrap_or(&GraphicsLayer::DEFAULT)
810 != &*container.graphics_layer
811 {
812 return Err("container graphics layer changed");
813 }
814 let (local_bounds, node_bounds) = layer_and_node_bounds(modifier_slices, layout_state.size());
815 Ok(TranslatedContainer {
816 node_id,
817 graphics_layer: shaped(local_bounds),
818 local_bounds,
819 node_bounds,
820 })
821}
822
823#[derive(Default)]
827struct TranslateScratch {
828 placed_fresh: Vec<(NodeId, cranpose_ui::widgets::LayoutState)>,
830 old_index_by_id: HashMap<NodeId, usize>,
831 retained: Vec<Option<Box<LayerNode>>>,
834 kept: Vec<Option<Box<LayerNode>>>,
837 entering: Vec<(NodeId, Box<LayerNode>)>,
839}
840
841thread_local! {
842 static TRANSLATE_SCRATCH: Cell<TranslateScratch> = Cell::new(TranslateScratch::default());
843}
844
845fn translated_children(
849 applier: &MemoryApplier,
850 container: &LayerNode,
851 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
852 fresh_children: &[NodeId],
853 scratch: &mut TranslateScratch,
854) -> Result<bool, &'static str> {
855 let placed_fresh = &mut scratch.placed_fresh;
856 placed_fresh.clear();
857 for child_id in fresh_children {
858 let Some(state) = scene_layout_state(applier, *child_id) else {
859 continue;
860 };
861 if !state.is_placed() {
862 continue;
863 }
864 placed_fresh.push((*child_id, state));
865 }
866 let children_unchanged = container.children.len() == placed_fresh.len()
867 && container
868 .children
869 .iter()
870 .zip(placed_fresh.iter())
871 .all(|(child, (id, _))| {
872 matches!(child, RenderNode::Layer(layer) if layer_identity(layer) == Some(*id))
873 });
874 let old_index_by_id = &mut scratch.old_index_by_id;
875 old_index_by_id.clear();
876 if !children_unchanged {
877 for (index, child) in container.children.iter().enumerate() {
878 let RenderNode::Layer(layer) = child else {
879 return Err("child without node id");
880 };
881 let Some(id) = layer_identity(layer) else {
882 return Err("child without node id");
883 };
884 old_index_by_id.insert(id, index);
885 }
886 }
887 check_retained_children(
888 container,
889 dirty_nodes,
890 &scratch.placed_fresh,
891 children_unchanged,
892 &scratch.old_index_by_id,
893 )?;
894 Ok(children_unchanged)
895}
896
897fn check_retained_children(
900 container: &LayerNode,
901 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
902 placed_fresh: &[(NodeId, cranpose_ui::widgets::LayoutState)],
903 children_unchanged: bool,
904 old_index_by_id: &HashMap<NodeId, usize>,
905) -> Result<(), &'static str> {
906 for (fresh_index, (child_id, state)) in placed_fresh.iter().enumerate() {
907 let old_index = if children_unchanged {
908 fresh_index
909 } else if let Some(index) = old_index_by_id.get(child_id) {
910 *index
911 } else {
912 continue;
913 };
914 let RenderNode::Layer(layer) = &container.children[old_index] else {
915 return Err("retained child slot is not a layer");
916 };
917 let fits = !layer.has_origin_sinks && Rect::from_size(state.size()) == layer.node_rect();
918 match dirty_nodes.get_mut(child_id) {
919 Some(NodeUpdate::Content | NodeUpdate::Layer) => {}
920 Some(kind @ (NodeUpdate::Moved | NodeUpdate::MovedLayer)) => {
921 if !fits {
922 *kind = NodeUpdate::Content;
923 }
924 }
925 None if layer.has_origin_sinks => {
926 return Err("child subtree publishes window origins");
927 }
928 None if !fits => return Err("child resized"),
929 None => {}
930 }
931 }
932 Ok(())
933}
934
935#[derive(Clone, Copy)]
936struct TranslateGeometry {
937 content_offset: Point,
938 window_transform: ProjectiveTransform,
939 top_left: Point,
940 child_origin: Point,
941}
942
943impl TranslateGeometry {
944 fn new(
945 layout_state: &cranpose_ui::widgets::LayoutState,
946 graphics_layer: &GraphicsLayer,
947 local_bounds: Rect,
948 parent_abs: AbsOrigin,
949 ) -> Self {
950 let content_offset = layout_state.content_offset();
951 let top_left = Point {
952 x: parent_abs.content_origin.x + layout_state.position().x,
953 y: parent_abs.content_origin.y + layout_state.position().y,
954 };
955 let window_transform = layer_transform_to_window(
956 parent_abs.window_transform,
957 top_left,
958 local_bounds,
959 graphics_layer,
960 );
961 Self {
962 content_offset,
963 window_transform,
964 top_left,
965 child_origin: Point {
966 x: top_left.x + content_offset.x,
967 y: top_left.y + content_offset.y,
968 },
969 }
970 }
971
972 fn child_abs(self) -> AbsOrigin {
973 AbsOrigin {
974 content_origin: self.child_origin,
975 window_transform: self.window_transform,
976 }
977 }
978}
979
980fn recycle_leaving_children(
988 container: &mut LayerNode,
989 scratch: &mut TranslateScratch,
990 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
991) {
992 let TranslateScratch {
993 placed_fresh,
994 old_index_by_id,
995 retained,
996 kept,
997 ..
998 } = scratch;
999 retained.clear();
1000 retained.extend(container.children.drain(..).map(|child| match child {
1001 RenderNode::Layer(layer) => Some(layer),
1002 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => None,
1003 }));
1004 kept.clear();
1005 kept.extend(placed_fresh.iter().map(|(child_id, _)| {
1006 old_index_by_id
1007 .get(child_id)
1008 .and_then(|index| retained[*index].take())
1009 }));
1010 for leaving in retained.drain(..).flatten() {
1011 forget_dirty_subtree(&leaving, dirty_nodes);
1012 crate::layer_recycling::recycle(leaving);
1013 }
1014}
1015
1016fn build_entering_children(
1020 applier: &MemoryApplier,
1021 container: &LayerNode,
1022 scratch: &mut TranslateScratch,
1023 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
1024 geometry: TranslateGeometry,
1025 inherited: (bool, u64),
1026) {
1027 let (child_inherited_translated_content_context, update) = inherited;
1028 let entering = &mut scratch.entering;
1029 entering.clear();
1030 for (child_id, _) in &scratch.placed_fresh {
1031 if scratch.old_index_by_id.contains_key(child_id) {
1032 continue;
1033 }
1034 let context = LowerContext {
1035 inherited_motion_context_animated: container.motion_context_animated,
1036 inherited_translated_content_context: child_inherited_translated_content_context,
1037 parent_abs: Some(geometry.child_abs()),
1038 parent_content_offset: geometry.content_offset,
1039 };
1040 let Some(mut lowered) = lower_child(applier, *child_id, context) else {
1041 continue;
1042 };
1043 lowered.note_content_change(update);
1044 forget_dirty_subtree(&lowered, dirty_nodes);
1045 entering.push((*child_id, lowered));
1046 }
1047}
1048
1049fn write_own_content_around_children(
1052 container: &mut LayerNode,
1053 node_id: NodeId,
1054 modifier_slices: &ModifierNodeSlices,
1055 size: Size,
1056) {
1057 if modifier_slices.draw_commands()[modifier_slices.outer_draw_command_count()..].is_empty()
1058 && modifier_slices.annotated_text().is_none()
1059 {
1060 return;
1061 }
1062 let mut layers = std::mem::replace(
1063 &mut container.children,
1064 crate::layer_recycling::child_list(0),
1065 );
1066 write_node_content(
1067 &mut container.children,
1068 node_id,
1069 modifier_slices,
1070 size,
1071 layers.len(),
1072 |list| list.append(&mut layers),
1073 );
1074 crate::layer_recycling::recycle_list(layers);
1075}
1076
1077fn apply_translated_container_state(
1078 container: &mut LayerNode,
1079 modifier_slices: &ModifierNodeSlices,
1080 layout_state: &cranpose_ui::widgets::LayoutState,
1081 parent_content_offset: Point,
1082 geometry: TranslateGeometry,
1083) {
1084 container.transform_to_parent = placed_transform(
1085 container.local_bounds,
1086 layout_state.position(),
1087 &container.graphics_layer,
1088 parent_content_offset,
1089 );
1090 container.content_offset = geometry.content_offset;
1091 if container.translated_content_context {
1092 container.translated_content_offset = modifier_slices
1093 .translated_content_offset()
1094 .unwrap_or(geometry.content_offset);
1095 }
1096 modifier_slices.publish_window_geometry(
1097 geometry.top_left,
1098 geometry.window_transform,
1099 layout_state.size(),
1100 );
1101 container.origin_in_parent = layout_state.position();
1102}
1103
1104fn reconcile_translated_children(
1105 container: &mut LayerNode,
1106 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
1107 scratch: &mut TranslateScratch,
1108 children_unchanged: bool,
1109 geometry: TranslateGeometry,
1110) {
1111 if children_unchanged {
1112 for (child, (child_id, state)) in container.children.iter_mut().zip(&scratch.placed_fresh) {
1113 let RenderNode::Layer(layer) = child else {
1114 unreachable!("retained child identities were checked");
1115 };
1116 if keeps_retained_child(dirty_nodes, *child_id) {
1117 translate_retained_child(layer, state, geometry.content_offset);
1118 }
1119 }
1120 return;
1121 }
1122 let mut entering = scratch.entering.drain(..).peekable();
1123 for ((child_id, state), kept) in scratch.placed_fresh.iter().zip(scratch.kept.drain(..)) {
1124 if let Some(mut layer) = kept {
1125 if keeps_retained_child(dirty_nodes, *child_id) {
1126 translate_retained_child(&mut layer, state, geometry.content_offset);
1127 }
1128 container.children.push(RenderNode::Layer(layer));
1129 } else if let Some((_, lowered)) = entering.next_if(|(id, _)| id == child_id) {
1130 dirty_nodes.remove(child_id);
1131 remove_dirty_descendants(&lowered, dirty_nodes);
1132
1133 container.children.push(RenderNode::Layer(lowered));
1134 }
1135 }
1136}
1137
1138fn translate_layer_from_data(
1139 applier: &MemoryApplier,
1140 container: &mut LayerNode,
1141 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
1142 ancestors: TranslateAncestorContext,
1143 data: SnapshotNodeData<'_>,
1144 wrapped: bool,
1145) -> bool {
1146 let TranslateAncestorContext {
1147 inherited_motion_context_animated,
1148 update,
1149 inherited_translated_content_context,
1150 parent_content_offset,
1151 parent_abs,
1152 } = ancestors;
1153 let SnapshotNodeData {
1154 layout_state,
1155 modifier_slices,
1156 children: fresh_children,
1157 window_root,
1158 } = data;
1159 let layout_state = if window_root {
1160 layout_state.at_origin()
1161 } else {
1162 layout_state
1163 };
1164 let container_plan = match translated_container(
1165 container,
1166 &layout_state,
1167 &modifier_slices,
1168 inherited_motion_context_animated,
1169 wrapped,
1170 ) {
1171 Ok(plan) => plan,
1172 Err(reason) => return translate_bail(reason),
1173 };
1174 let TranslatedContainer {
1175 node_id,
1176 graphics_layer,
1177 local_bounds,
1178 node_bounds,
1179 } = container_plan;
1180 if container.local_bounds != local_bounds {
1181 container.local_bounds = local_bounds;
1182 container.graphics_layer.replace(graphics_layer);
1183 }
1184 container.node_bounds = node_bounds;
1185 crate::layer_recycling::recycle_primitives(container);
1189 let mut scratch = TRANSLATE_SCRATCH.take();
1190 let children_unchanged = match translated_children(
1191 applier,
1192 container,
1193 dirty_nodes,
1194 fresh_children,
1195 &mut scratch,
1196 ) {
1197 Ok(unchanged) => unchanged,
1198 Err(reason) => {
1199 TRANSLATE_SCRATCH.set(scratch);
1200 return translate_bail(reason);
1201 }
1202 };
1203
1204 let geometry = TranslateGeometry::new(
1205 &layout_state,
1206 &container.graphics_layer,
1207 container.local_bounds,
1208 parent_abs,
1209 );
1210 let child_inherited_translated_content_context =
1211 inherited_translated_content_context || container.translated_content_context;
1212 if !children_unchanged {
1213 recycle_leaving_children(container, &mut scratch, dirty_nodes);
1214 build_entering_children(
1215 applier,
1216 container,
1217 &mut scratch,
1218 dirty_nodes,
1219 geometry,
1220 (child_inherited_translated_content_context, update),
1221 );
1222 }
1223
1224 apply_translated_container_state(
1225 container,
1226 &modifier_slices,
1227 &layout_state,
1228 parent_content_offset,
1229 geometry,
1230 );
1231
1232 reconcile_translated_children(
1233 container,
1234 dirty_nodes,
1235 &mut scratch,
1236 children_unchanged,
1237 geometry,
1238 );
1239 TRANSLATE_SCRATCH.set(scratch);
1240 write_own_content_around_children(container, node_id, &modifier_slices, layout_state.size());
1241 container.hit_test = hit_test_from_slices(&modifier_slices);
1242
1243 container.has_origin_sinks = modifier_slices_have_origin_sinks(&modifier_slices)
1244 || children_have_origin_sinks(&container.children);
1245 container.refresh_child_facts();
1246 container.note_content_change(update);
1247
1248 true
1249}
1250
1251fn keeps_retained_child(dirty_nodes: &mut HashMap<NodeId, NodeUpdate>, child_id: NodeId) -> bool {
1255 match dirty_nodes.get(&child_id) {
1256 None => true,
1257 Some(NodeUpdate::Moved) => {
1258 dirty_nodes.remove(&child_id);
1259 true
1260 }
1261 Some(NodeUpdate::Content | NodeUpdate::Layer | NodeUpdate::MovedLayer) => false,
1262 }
1263}
1264
1265fn translate_retained_child(
1266 layer: &mut LayerNode,
1267 state: &cranpose_ui::widgets::LayoutState,
1268 content_offset: Point,
1269) {
1270 layer.transform_to_parent = placed_transform(
1271 layer.local_bounds,
1272 state.position(),
1273 &layer.graphics_layer,
1274 content_offset,
1275 );
1276 layer.origin_in_parent = state.position();
1277}
1278
1279#[derive(PartialEq)]
1280struct HitGraphState {
1281 hit_test: bool,
1282 has_hit_targets: bool,
1283 local_bounds: Rect,
1284 node_bounds: Option<Rect>,
1285 transform_to_parent: ProjectiveTransform,
1286 clip_rect: Option<Rect>,
1287 shape: LayerShape,
1288}
1289
1290impl HitGraphState {
1291 fn of(layer: &LayerNode) -> Self {
1292 Self {
1293 hit_test: layer.hit_test.is_some(),
1294 has_hit_targets: layer.has_hit_targets,
1295 local_bounds: layer.local_bounds,
1296 node_bounds: layer.node_bounds,
1297 transform_to_parent: layer.transform_to_parent,
1298 clip_rect: layer.clip_rect(),
1299 shape: layer.graphics_layer.shape,
1300 }
1301 }
1302
1303 fn dirty_against(&self, replacement: &Self) -> bool {
1304 self.hit_test
1305 || replacement.hit_test
1306 || ((self.has_hit_targets || replacement.has_hit_targets) && self != replacement)
1307 }
1308}
1309
1310fn release_for_rebuild(layer: &mut LayerNode) {
1311 layer.hit_test = None;
1312 crate::layer_recycling::recycle_children(layer);
1313}
1314
1315fn forget_dirty_subtree(layer: &LayerNode, dirty_nodes: &mut HashMap<NodeId, NodeUpdate>) {
1317 if let Some(node_id) = layer_identity(layer) {
1318 dirty_nodes.remove(&node_id);
1319 }
1320 remove_dirty_descendants(layer, dirty_nodes);
1321}
1322
1323fn remove_dirty_descendants(layer: &LayerNode, dirty_nodes: &mut HashMap<NodeId, NodeUpdate>) {
1324 for child in &layer.children {
1325 let RenderNode::Layer(child_layer) = child else {
1326 continue;
1327 };
1328 if let Some(node_id) = child_layer.node_id {
1329 dirty_nodes.remove(&node_id);
1330 }
1331 remove_dirty_descendants(child_layer, dirty_nodes);
1332 }
1333}
1334
1335#[derive(Clone, Copy)]
1336struct LowerContext {
1337 inherited_motion_context_animated: bool,
1338 inherited_translated_content_context: bool,
1339 parent_abs: Option<AbsOrigin>,
1340 parent_content_offset: Point,
1341}
1342
1343impl LowerContext {
1344 const ROOT: Self = Self {
1345 inherited_motion_context_animated: false,
1346 inherited_translated_content_context: false,
1347 parent_abs: Some(AbsOrigin::ROOT),
1348 parent_content_offset: Point { x: 0.0, y: 0.0 },
1349 };
1350
1351 fn for_children(
1352 self,
1353 slices: &ModifierNodeSlices,
1354 content_offset: Point,
1355 parent_abs: Option<AbsOrigin>,
1356 ) -> Self {
1357 Self {
1358 inherited_motion_context_animated: self.inherited_motion_context_animated
1359 || slices.motion_context_animated(),
1360 inherited_translated_content_context: self.inherited_translated_content_context
1361 || slices.translated_content_context(),
1362 parent_abs,
1363 parent_content_offset: content_offset,
1364 }
1365 }
1366}
1367
1368struct LayerHead {
1369 node_id: Option<NodeId>,
1370 wraps: Option<NodeId>,
1371 local_bounds: Rect,
1372 node_bounds: Option<Rect>,
1373 transform_to_parent: ProjectiveTransform,
1374 content_offset: Point,
1375 motion_context_animated: bool,
1376 translated_content_context: bool,
1377 translated_content_offset: Point,
1378 origin_in_parent: Point,
1379 graphics_layer: Option<GraphicsLayer>,
1380 clip_to_bounds: bool,
1381 hit_test: Option<HitTestNode>,
1382 has_origin_sinks: bool,
1383 isolation: IsolationReasons,
1384 cache_policy: CachePolicy,
1385}
1386
1387struct NodeFrame {
1388 local_bounds: Rect,
1389 node_bounds: Option<Rect>,
1390 placement: Point,
1391 content_offset: Point,
1392 translated_content_offset: Point,
1393}
1394
1395fn node_layer_head(
1396 node_id: NodeId,
1397 slices: &Rc<ModifierNodeSlices>,
1398 frame: NodeFrame,
1399 graphics_layer: Option<GraphicsLayer>,
1400 context: LowerContext,
1401) -> LayerHead {
1402 let clip_to_bounds = slices.clip_to_bounds();
1403 let translated_content_context = slices.translated_content_context();
1404 let properties = graphics_layer.as_ref().unwrap_or(&GraphicsLayer::DEFAULT);
1405 let isolation = isolation_reasons(properties);
1406 LayerHead {
1407 node_id: Some(node_id),
1408 wraps: None,
1409 local_bounds: frame.local_bounds,
1410 node_bounds: frame.node_bounds,
1411 transform_to_parent: placed_transform(
1412 frame.local_bounds,
1413 frame.placement,
1414 properties,
1415 context.parent_content_offset,
1416 ),
1417 content_offset: frame.content_offset,
1418 motion_context_animated: context.inherited_motion_context_animated
1419 || slices.motion_context_animated(),
1420 translated_content_context,
1421 translated_content_offset: if translated_content_context {
1422 frame.translated_content_offset
1423 } else {
1424 Point::default()
1425 },
1426 origin_in_parent: frame.placement,
1427 cache_policy: layer_cache_policy(properties, isolation),
1428 isolation,
1429 graphics_layer,
1430 clip_to_bounds,
1431 hit_test: hit_test_from_slices(slices),
1432 has_origin_sinks: modifier_slices_have_origin_sinks(slices),
1433 }
1434}
1435
1436fn assign_layer(layer: &mut LayerNode, head: LayerHead) {
1437 let LayerNode {
1438 node_id,
1439 wraps,
1440 local_bounds,
1441 node_bounds,
1442 transform_to_parent,
1443 content_offset,
1444 motion_context_animated,
1445 translated_content_context,
1446 translated_content_offset,
1447 origin_in_parent,
1448 graphics_layer,
1449 clip_to_bounds,
1450 hit_test,
1451 has_hit_targets,
1452 has_origin_sinks,
1453 draws_within_bounds,
1454 isolation,
1455 cache_policy,
1456 cache_hashes,
1457 content_changes,
1458 children,
1459 } = layer;
1460 if *node_id != head.node_id {
1461 *content_changes = ContentChanges::default();
1462 }
1463 *node_id = head.node_id;
1464 *wraps = head.wraps;
1465 *local_bounds = head.local_bounds;
1466 *node_bounds = head.node_bounds;
1467 *transform_to_parent = head.transform_to_parent;
1468 *content_offset = head.content_offset;
1469 *motion_context_animated = head.motion_context_animated;
1470 *translated_content_context = head.translated_content_context;
1471 *translated_content_offset = head.translated_content_offset;
1472 *origin_in_parent = head.origin_in_parent;
1473 graphics_layer.replace(head.graphics_layer);
1474 *clip_to_bounds = head.clip_to_bounds;
1475 *hit_test = head.hit_test;
1476 *has_hit_targets = false;
1477 *has_origin_sinks = head.has_origin_sinks || children_have_origin_sinks(children);
1478 *draws_within_bounds = false;
1479 *isolation = head.isolation;
1480 *cache_policy = head.cache_policy;
1481 cache_hashes.set(None);
1482 layer.refresh_child_facts();
1483}
1484
1485fn children_have_origin_sinks(children: &[RenderNode]) -> bool {
1486 children.iter().any(|child| match child {
1487 RenderNode::Layer(child_layer) => child_layer.has_origin_sinks,
1488 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
1489 })
1490}
1491
1492fn placed_transform(
1493 local_bounds: Rect,
1494 placement: Point,
1495 graphics_layer: &GraphicsLayer,
1496 parent_content_offset: Point,
1497) -> ProjectiveTransform {
1498 let transform = layer_transform_to_parent(local_bounds, placement, graphics_layer);
1499 if parent_content_offset == Point::default() {
1500 transform
1501 } else {
1502 transform.then(ProjectiveTransform::translation(
1503 parent_content_offset.x,
1504 parent_content_offset.y,
1505 ))
1506 }
1507}
1508
1509fn write_node_content(
1510 list: &mut Vec<RenderNode>,
1511 node_id: NodeId,
1512 slices: &ModifierNodeSlices,
1513 size: Size,
1514 child_count: usize,
1515 write_children: impl FnOnce(&mut Vec<RenderNode>),
1516) {
1517 let outer_count = slices.outer_draw_command_count();
1518 let commands = &slices.draw_commands()[outer_count..];
1519 list.reserve(layer_node_capacity(
1520 commands,
1521 child_count,
1522 slices.annotated_text().is_some(),
1523 ));
1524 let first_draw = list.len();
1525 append_draw_nodes(
1526 list,
1527 node_id,
1528 commands,
1529 outer_count,
1530 DrawPass::Record(DrawPlacement::Behind),
1531 size,
1532 PrimitivePhase::BeforeChildren,
1533 );
1534 if let Some(text) = text_node_from_parts(TextNodeParts {
1535 node_id,
1536 text_rect: slices.text_content_rect(size),
1537 text_style: slices.text_style(),
1538 text_layout_options: slices.text_layout_options(),
1539 text_pan: slices.text_pan_resolver(),
1540 measured_layout: slices.measured_text_layout(),
1541 }) {
1542 list.push(RenderNode::Primitive(PrimitiveEntry {
1543 phase: PrimitivePhase::BeforeChildren,
1544 node: PrimitiveNode::Text(crate::layer_recycling::boxed_text(text)),
1545 }));
1546 }
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, 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 scene_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 if let Some(node) = node.downcast_ref::<LayoutNode>() {
1710 return Some(node.layout_state());
1711 }
1712 node.downcast_ref::<SubcomposeLayoutNode>()
1713 .map(SubcomposeLayoutNode::layout_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, 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 })
2270}
2271
2272fn layout_box_to_snapshot(node: &LayoutBox, parent: Option<&LayoutBox>) -> BuildNodeSnapshot {
2273 let placement = parent
2274 .map(|parent_box| Point {
2275 x: node.rect.x - parent_box.rect.x - parent_box.content_offset.x,
2276 y: node.rect.y - parent_box.rect.y - parent_box.content_offset.y,
2277 })
2278 .unwrap_or_default();
2279 let mut children = Vec::with_capacity(node.children.len());
2280 for child in &node.children {
2281 children.push(layout_box_to_snapshot(child, Some(node)));
2282 }
2283 let base_graphics_layer = node.node_data.modifier_slices.graphics_layer();
2284 let has_graphics_layer = base_graphics_layer.is_some();
2285 let graphics_layer = graphics_layer_with_shaped_clip(
2286 base_graphics_layer,
2287 node.node_data.modifier_slices.clip_to_bounds(),
2288 node.node_data.modifier_slices.corner_shape(),
2289 Rect {
2290 x: 0.0,
2291 y: 0.0,
2292 width: node.rect.width,
2293 height: node.rect.height,
2294 },
2295 );
2296 let has_graphics_layer = has_graphics_layer
2297 || graphics_layer
2298 .as_ref()
2299 .is_some_and(|layer| layer.render_effect.is_some());
2300
2301 BuildNodeSnapshot {
2302 node_id: node.node_id,
2303 placement,
2304 size: Size {
2305 width: node.rect.width,
2306 height: node.rect.height,
2307 },
2308 content_offset: node.content_offset,
2309 slices: Rc::clone(&node.node_data.modifier_slices),
2310 graphics_layer: graphics_layer.filter(|_| has_graphics_layer),
2311 children,
2312 }
2313}
2314
2315fn modifier_slices_have_origin_sinks(slices: &ModifierNodeSlices) -> bool {
2316 slices.text_window_transform().is_some() || slices.viewport_window_rect().is_some()
2317}
2318
2319fn graphics_layer_with_shaped_clip(
2320 mut graphics_layer: Option<GraphicsLayer>,
2321 clip_to_bounds: bool,
2322 corner_shape: Option<RoundedCornerShape>,
2323 local_bounds: Rect,
2324) -> Option<GraphicsLayer> {
2325 if !clip_to_bounds {
2326 return graphics_layer;
2327 }
2328
2329 let Some(corner_shape) = corner_shape else {
2330 return graphics_layer;
2331 };
2332 let radii = corner_shape.resolve(local_bounds.width, local_bounds.height);
2333 if radii.top_left <= f32::EPSILON
2334 && radii.top_right <= f32::EPSILON
2335 && radii.bottom_right <= f32::EPSILON
2336 && radii.bottom_left <= f32::EPSILON
2337 {
2338 return graphics_layer;
2339 }
2340
2341 let properties = graphics_layer.get_or_insert_default();
2342 if let Some(existing) = properties.render_effect.take() {
2343 let rounded_clip = rounded_corner_alpha_mask_effect(
2344 local_bounds.width,
2345 local_bounds.height,
2346 radii,
2347 ROUNDED_CLIP_EDGE_FEATHER,
2348 );
2349 properties.render_effect = Some(existing.then(rounded_clip));
2350 } else {
2351 properties.shape = LayerShape::Rounded(corner_shape);
2352 properties.clip = true;
2353 }
2354 graphics_layer
2355}
2356
2357fn layer_cache_policy(layer: &GraphicsLayer, isolation: IsolationReasons) -> CachePolicy {
2358 if isolation.has_any() || layer_scales_or_rotates(layer) {
2359 CachePolicy::Auto
2360 } else {
2361 CachePolicy::None
2362 }
2363}
2364
2365fn isolation_reasons(layer: &GraphicsLayer) -> IsolationReasons {
2366 IsolationReasons {
2367 explicit_offscreen: layer.compositing_strategy == CompositingStrategy::Offscreen,
2368 shape_clip: layer.clip && !matches!(layer.shape, LayerShape::Rectangle),
2369 effect: layer.render_effect.is_some(),
2370 backdrop: layer.backdrop_effect.is_some(),
2371 group_opacity: layer.compositing_strategy != CompositingStrategy::ModulateAlpha
2372 && layer.alpha < 1.0,
2373 blend_mode: layer.blend_mode != cranpose_ui::BlendMode::SrcOver,
2374 }
2375}
2376
2377fn pad_clip_rect(rect: Rect) -> Rect {
2378 Rect {
2379 x: rect.x - TEXT_CLIP_PAD,
2380 y: rect.y - TEXT_CLIP_PAD,
2381 width: (rect.width + TEXT_CLIP_PAD * 2.0).max(0.0),
2382 height: (rect.height + TEXT_CLIP_PAD * 2.0).max(0.0),
2383 }
2384}
2385
2386pub fn expand_text_bounds_for_baseline_shift(
2387 text_bounds: Rect,
2388 text_style: &TextStyle,
2389 font_size: f32,
2390) -> Rect {
2391 let baseline_shift_px = text_style
2392 .span_style
2393 .baseline_shift
2394 .filter(|shift| shift.is_specified())
2395 .map_or(0.0, |shift| -(shift.0 * font_size));
2396 if baseline_shift_px == 0.0 {
2397 return text_bounds;
2398 }
2399
2400 if baseline_shift_px < 0.0 {
2401 Rect {
2402 x: text_bounds.x,
2403 y: text_bounds.y + baseline_shift_px,
2404 width: text_bounds.width,
2405 height: (text_bounds.height - baseline_shift_px).max(0.0),
2406 }
2407 } else {
2408 Rect {
2409 x: text_bounds.x,
2410 y: text_bounds.y,
2411 width: text_bounds.width,
2412 height: (text_bounds.height + baseline_shift_px).max(0.0),
2413 }
2414 }
2415}
2416
2417fn resolve_text_horizontal_offset(
2418 text_style: &TextStyle,
2419 text: &str,
2420 content_width: f32,
2421 measured_width: f32,
2422) -> f32 {
2423 let remaining = (content_width - measured_width).max(0.0);
2424 if remaining == 0.0 {
2425 return 0.0;
2426 }
2427 remaining * cranpose_ui::text::text_align_fraction(text_style, text)
2428}
2429
2430#[cfg(test)]
2431#[path = "tests/scene_builder_tests.rs"]
2432mod tests;