1use std::{any::Any, rc::Rc};
2
3use cranpose_core::{MemoryApplier, NodeId, collections::map::HashMap};
4use cranpose_ui::{LayoutBox, ModifierNodeSlices, Point, Rect, Size, SubcomposeLayoutNode};
5use cranpose_ui_graphics::GraphicsLayer;
6
7use crate::{
8 NodeUpdate, SceneUpdates,
9 graph::{LayerNode, ProjectiveTransform, RenderGraph, RenderNode},
10 layer_transform::layer_transform_to_parent,
11};
12
13mod dirty_walk;
14pub(crate) use dirty_walk::{DirtyAncestry, RetainedPath};
15use dirty_walk::{update_along_retained_path, walk_dirty_layers};
16mod draw_recording;
17use draw_recording::{
18 DrawPass, DrawTarget, append_draw_nodes, bump_recording_generation, draw_nodes,
19 layer_node_capacity,
20};
21pub(crate) use draw_recording::{RecorderKey, RecorderSlot};
22pub use draw_recording::{clear_command_recordings_for_tests, draw_command_nodes_for_tests};
23mod graph_changes;
24use graph_changes::GraphChanges;
25mod hit_graph;
26pub(crate) use hit_graph::HitGraphState;
27use hit_graph::rebuild_tracking_hits;
28mod inline_leaf;
29use inline_leaf::{lower_child, update_dirty_inline};
30mod layer_shape;
31use layer_shape::shaped_graphics_layer;
32mod text_node;
33mod translate_geometry;
34use text_node::{TextNodeParts, text_node_from_parts, translate_unwrapped};
35use translate_geometry::{KeptChild, TranslateGeometry, move_kept_child, translate_retained_child};
36mod node_layer;
37use node_layer::{
38 AbsOrigin, LayerHead, LowerContext, PlacedGeometry, assign_layer, children_have_origin_sinks,
39 hit_test_from_slices, layer_and_node_bounds, layer_identity, layout_box_to_snapshot,
40 lower_root_into, modifier_slices_have_origin_sinks, outer_draws, placed_child_geometry,
41 placed_transform, prepare_node_layer, read_node_data, read_placed_node_data,
42 slices_hit_something, take_wrapped_layer, write_node_content, write_node_layer,
43 write_snapshot_layer, write_wrapper,
44};
45mod translate_layer;
46pub(crate) use translate_layer::TranslateScratch;
47use translate_layer::{
48 TranslatedContainer, translate_tracking_hits, translated_container,
49 write_own_content_around_children,
50};
51
52#[derive(Clone, Default)]
53struct BuildNodeSnapshot {
54 node_id: NodeId,
55 placement: Point,
56 size: Size,
57 content_offset: Point,
58 slices: Rc<ModifierNodeSlices>,
61 graphics_layer: Option<GraphicsLayer>,
62 children: Vec<Self>,
63}
64
65struct SnapshotNodeData<'a> {
66 layout_state: cranpose_ui::widgets::LayoutState,
67 modifier_slices: Rc<ModifierNodeSlices>,
68 children: &'a [NodeId],
69 window_root: bool,
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub enum GraphRebuildReason {
82 RootLayerUnavailable,
84 DirtyLayerUnavailable,
86 UnmatchedDirtyNodes(usize),
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum GraphUpdate {
96 Patched,
98 NeedsRebuild(GraphRebuildReason),
100}
101
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub struct GraphUpdateReport {
105 pub update: GraphUpdate,
107 pub hit_graph_dirty: bool,
110 pub layers_moved: bool,
113}
114
115impl GraphUpdateReport {
116 fn patched(changes: GraphChanges) -> Self {
117 Self {
118 update: GraphUpdate::Patched,
119 hit_graph_dirty: changes.contains(GraphChanges::Hits),
120 layers_moved: changes.contains(GraphChanges::Moves),
121 }
122 }
123
124 fn needs_rebuild(reason: GraphRebuildReason) -> Self {
125 Self {
126 update: GraphUpdate::NeedsRebuild(reason),
127 hit_graph_dirty: true,
128 layers_moved: true,
129 }
130 }
131
132 pub fn applied(self) -> bool {
134 matches!(self.update, GraphUpdate::Patched)
135 }
136
137 pub fn rebuild_reason(self) -> Option<GraphRebuildReason> {
139 match self.update {
140 GraphUpdate::Patched => None,
141 GraphUpdate::NeedsRebuild(reason) => Some(reason),
142 }
143 }
144}
145
146fn note_layer_lowered() {
147 #[cfg(test)]
148 crate::thread_state::with(|state| {
149 let count = &state.lowered_layer_count;
150 count.set(count.get() + 1);
151 });
152}
153
154#[cfg(test)]
155fn reset_lowered_layer_count() {
156 crate::thread_state::with(|state| state.lowered_layer_count.set(0));
157}
158
159#[cfg(test)]
160fn lowered_layer_count() -> usize {
161 crate::thread_state::with(|state| state.lowered_layer_count.get())
162}
163
164pub fn build_graph_from_layout_tree(root: &LayoutBox, _scale: f32) -> RenderGraph {
166 bump_recording_generation();
167 let root_snapshot = layout_box_to_snapshot(root, None);
168 let mut graph = RenderGraph::new(LayerNode::default());
169 write_snapshot_layer(root_snapshot, LowerContext::ROOT, &mut graph.root);
170 graph
171}
172
173pub fn build_graph_from_applier(
176 applier: &MemoryApplier,
177 root: NodeId,
178 _scale: f32,
179) -> Option<RenderGraph> {
180 bump_recording_generation();
181 let mut graph = RenderGraph::new(LayerNode::default());
182 lower_root_into(applier, root, &mut graph.root).then_some(graph)
183}
184
185pub fn rebuild_graph_from_applier(
188 applier: &MemoryApplier,
189 root: NodeId,
190 scale: f32,
191 previous: Option<RenderGraph>,
192) -> Option<RenderGraph> {
193 let graph = match previous {
194 Some(mut graph) => {
195 release_for_rebuild(&mut graph.root);
196 bump_recording_generation();
197 graph.retained_path = None;
198 graph.update += 1;
199 graph.root.note_content_change(graph.update);
200 lower_root_into(applier, root, &mut graph.root).then_some(graph)
201 }
202 None => build_graph_from_applier(applier, root, scale),
203 };
204 crate::layer_recycling::release();
205 graph
206}
207
208pub fn update_graph_from_applier(
211 applier: &MemoryApplier,
212 graph: &mut RenderGraph,
213 updates: SceneUpdates<'_>,
214 scale: f32,
215) -> bool {
216 update_graph_from_applier_report(applier, graph, updates, scale).applied()
217}
218
219#[expect(clippy::print_stderr, reason = "CRANPOSE_SCENE_UPDATE_DIAG asks")]
222pub fn update_graph_from_applier_report(
223 applier: &MemoryApplier,
224 graph: &mut RenderGraph,
225 updates: SceneUpdates<'_>,
226 _scale: f32,
227) -> GraphUpdateReport {
228 let report = update_graph_from_applier_report_inner(applier, graph, updates);
229 crate::layer_recycling::release();
230 if let GraphUpdate::NeedsRebuild(reason) = report.update
231 && cranpose_core::DIAGNOSTICS.scene_update_diag.is_set()
232 {
233 eprintln!(
234 "[scene-update-diag] scoped update abandoned, whole scene rebuilt: {reason:?} dirty={}",
235 updates.len()
236 );
237 }
238 report
239}
240
241fn dirty_node_updates(
246 updates: SceneUpdates<'_>,
247 dirty: &mut HashMap<NodeId, NodeUpdate>,
248) -> GraphChanges {
249 dirty.clear();
250 dirty.reserve(updates.len());
251 let mut changes = GraphChanges::default();
252 for &(id, update) in updates {
253 dirty
254 .entry(id)
255 .and_modify(|kind| {
256 let merged = kind.merge(update);
257 if merged == NodeUpdate::Content && (changes_layer(*kind) || changes_layer(update))
258 {
259 changes = GraphChanges::Moves;
260 }
261 *kind = merged;
262 })
263 .or_insert(update);
264 }
265 changes
266}
267
268fn changes_layer(update: NodeUpdate) -> bool {
269 matches!(update, NodeUpdate::Layer | NodeUpdate::MovedLayer)
270}
271
272#[expect(clippy::print_stderr, reason = "CRANPOSE_SCENE_UPDATE_DIAG asks")]
273fn update_graph_from_applier_report_inner(
274 applier: &MemoryApplier,
275 graph: &mut RenderGraph,
276 updates: SceneUpdates<'_>,
277) -> GraphUpdateReport {
278 if updates.is_empty() {
279 return GraphUpdateReport::patched(GraphChanges::default());
280 }
281 bump_recording_generation();
282 graph.update += 1;
283 let update = graph.update;
284
285 if cranpose_core::DIAGNOSTICS.scene_update_diag.is_set() {
286 eprintln!("[scene-update-diag] dirty={updates:?}");
287 }
288
289 let mut remaining_dirty_nodes =
291 crate::thread_state::with(|state| state.dirty_node_scratch.take()).unwrap_or_default();
292 let merged = dirty_node_updates(updates, &mut remaining_dirty_nodes);
293 let mut report = update_dirty_nodes(applier, graph, &mut remaining_dirty_nodes, update);
294 report.layers_moved |= merged.contains(GraphChanges::Moves);
295 remaining_dirty_nodes.clear();
296 crate::thread_state::with(|state| state.dirty_node_scratch.set(Some(remaining_dirty_nodes)));
297 report
298}
299
300fn update_dirty_nodes(
303 applier: &MemoryApplier,
304 graph: &mut RenderGraph,
305 remaining_dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
306 update: u64,
307) -> GraphUpdateReport {
308 if let Some(root_id) = layer_identity(&graph.root)
309 && let Some(kind) = remaining_dirty_nodes.remove(&root_id)
310 {
311 if let Some(mut changes) = try_update_retained_layer(
312 applier,
313 &mut graph.root,
314 remaining_dirty_nodes,
315 kind,
316 Level::Root,
317 TranslateAncestorContext {
318 inherited_motion_context_animated: false,
319 update,
320 inherited_translated_content_context: false,
321 parent_content_offset: Point::default(),
322 parent_abs: AbsOrigin::ROOT,
323 },
324 ) {
325 if remaining_dirty_nodes.is_empty() {
326 return GraphUpdateReport::patched(changes);
327 }
328 let walked = walk_dirty_layers(applier, graph, remaining_dirty_nodes, update)
329 .map(|(report, _)| report);
330 changes |= walked.map_or(GraphChanges::HitsAndMoves, |report| report.changes);
331 return GraphUpdateReport {
332 update: classify_walk(applier, walked.is_some(), remaining_dirty_nodes),
333 ..GraphUpdateReport::patched(changes)
334 };
335 }
336
337 let Some(hit_graph_dirty) = rebuild_tracking_hits(&mut graph.root, |root| {
338 release_for_rebuild(root);
339 lower_root_into(applier, root_id, root)
340 }) else {
341 return GraphUpdateReport::needs_rebuild(GraphRebuildReason::RootLayerUnavailable);
342 };
343 graph.root.note_content_change(update);
344
345 return GraphUpdateReport::patched(GraphChanges::hits(hit_graph_dirty));
346 }
347
348 if let Some(path) = graph.retained_path.take()
351 && let Some(report) = update_along_retained_path(
352 applier,
353 &mut graph.root,
354 &path,
355 remaining_dirty_nodes,
356 update,
357 )
358 {
359 if report.is_some() {
360 graph.retained_path = Some(RetainedPath { update, ..path });
361 }
362 return match report {
363 Some(report) => GraphUpdateReport::patched(report.changes),
364 None => GraphUpdateReport::needs_rebuild(GraphRebuildReason::DirtyLayerUnavailable),
365 };
366 }
367
368 let Some((report, path)) = walk_dirty_layers(applier, graph, remaining_dirty_nodes, update)
369 else {
370 return GraphUpdateReport::needs_rebuild(GraphRebuildReason::DirtyLayerUnavailable);
371 };
372 graph.retained_path = path;
373
374 match classify_walk(applier, true, remaining_dirty_nodes) {
375 GraphUpdate::Patched => GraphUpdateReport::patched(report.changes),
376 GraphUpdate::NeedsRebuild(reason) => GraphUpdateReport::needs_rebuild(reason),
377 }
378}
379
380fn classify_walk(
383 applier: &MemoryApplier,
384 walked: bool,
385 remaining_dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
386) -> GraphUpdate {
387 if !walked {
388 return GraphUpdate::NeedsRebuild(GraphRebuildReason::DirtyLayerUnavailable);
389 }
390 remaining_dirty_nodes.retain(|&node, _| is_drawn(applier, node));
391 if !remaining_dirty_nodes.is_empty() {
392 GraphUpdate::NeedsRebuild(GraphRebuildReason::UnmatchedDirtyNodes(
393 remaining_dirty_nodes.len(),
394 ))
395 } else {
396 GraphUpdate::Patched
397 }
398}
399
400fn is_drawn(applier: &MemoryApplier, node: NodeId) -> bool {
405 let mut child = node;
406 let mut current = node;
407 while let Some(parent) = applier
408 .get_ref(current)
409 .ok()
410 .and_then(cranpose_core::Node::parent)
411 {
412 let Ok(parent_node) = applier.get_ref(parent) else {
413 return true;
414 };
415 current = parent;
416 if parent_node.is_virtual() {
417 continue;
418 }
419 let parent_node: &dyn Any = parent_node;
420 if let Some(subcompose) = parent_node.downcast_ref::<SubcomposeLayoutNode>()
421 && !subcompose.with_active_children(|children| children.contains(&child))
422 {
423 return false;
424 }
425 child = parent;
426 }
427 true
428}
429
430#[expect(clippy::print_stderr, reason = "CRANPOSE_SCENE_UPDATE_DIAG asks")]
431fn translate_bail(reason: &str) -> bool {
432 if cranpose_core::DIAGNOSTICS.scene_update_diag.is_set() {
433 eprintln!("[scene-update-diag] translate bail: {reason}");
434 }
435 false
436}
437
438#[derive(Clone, Copy)]
439struct TranslateAncestorContext {
440 inherited_motion_context_animated: bool,
441 update: u64,
443 inherited_translated_content_context: bool,
444 parent_content_offset: Point,
445 parent_abs: AbsOrigin,
446}
447
448#[derive(Clone, Copy, PartialEq, Eq)]
451enum Level {
452 Root,
453 Child,
454}
455
456#[derive(Clone, Copy, PartialEq, Eq)]
458enum Position {
459 Kept,
460 Moved,
461}
462
463#[derive(Clone, Copy, PartialEq, Eq)]
466enum Wrapping {
467 Wrapped,
468 Bare,
469}
470
471fn try_update_retained_layer(
472 applier: &MemoryApplier,
473 layer: &mut LayerNode,
474 dirty_nodes: &mut HashMap<NodeId, NodeUpdate>,
475 kind: NodeUpdate,
476 level: Level,
477 ancestors: TranslateAncestorContext,
478) -> Option<GraphChanges> {
479 match kind {
480 NodeUpdate::Moved if level == Level::Child => {
481 move_retained_layer(applier, layer, ancestors)
482 .or_else(|| translate_tracking_hits(applier, layer, dirty_nodes, ancestors))
483 }
484 NodeUpdate::Draws if redraw_retained_layer(applier, layer, level, ancestors) => {
485 Some(GraphChanges::default())
486 }
487 NodeUpdate::Content | NodeUpdate::Draws | NodeUpdate::Moved => {
490 translate_tracking_hits(applier, layer, dirty_nodes, ancestors)
491 }
492 NodeUpdate::Layer => {
493 update_layer_properties(applier, layer, level, Position::Kept, ancestors)
494 }
495 NodeUpdate::MovedLayer => {
496 update_layer_properties(applier, layer, level, Position::Moved, ancestors)
497 }
498 }
499}
500
501fn redraw_retained_layer(
506 applier: &MemoryApplier,
507 container: &mut LayerNode,
508 level: Level,
509 ancestors: TranslateAncestorContext,
510) -> bool {
511 if container.wraps.is_some() {
513 return false;
514 }
515 let Some(node_id) = container.node_id else {
516 return false;
517 };
518 read_placed_node_data(applier, node_id, level, |data| {
519 let placement = container.origin_in_parent;
520 let kept = Position::Kept;
521 if !retained_layer_matches_layout(container, &data, placement, Wrapping::Bare, kept) {
522 return false;
523 }
524 let Ok(plan) = translated_container(
525 container,
526 &data.layout_state,
527 &data.modifier_slices,
528 ancestors,
529 Wrapping::Bare,
530 ) else {
531 return false;
532 };
533 container.node_bounds = plan.node_bounds;
534 crate::layer_recycling::recycle_primitives(container);
535 write_own_content_around_children(
536 container,
537 node_id,
538 &data.modifier_slices,
539 data.layout_state.size(),
540 );
541 container.refresh_child_facts();
542 container.note_content_change(ancestors.update);
543 true
544 })
545 .unwrap_or(false)
546}
547
548fn move_retained_layer(
553 applier: &MemoryApplier,
554 layer: &mut LayerNode,
555 ancestors: TranslateAncestorContext,
556) -> Option<GraphChanges> {
557 let placed = placed_child_geometry(applier, layer_identity(layer)?)?;
558 if Rect::from_size(placed.size) != layer.node_rect() {
559 return None;
560 }
561 let previous = HitGraphState::of(layer);
562 translate_retained_child(layer, placed.position, ancestors.parent_content_offset);
563 if layer.has_origin_sinks {
564 publish_retained_window_geometry(applier, layer, ancestors.parent_abs);
565 }
566 Some(previous.changes_against(&HitGraphState::of(layer)))
567}
568
569fn content_layer_mut(container: &mut LayerNode) -> Option<&mut LayerNode> {
570 let Some(node_id) = container.wraps else {
571 return Some(container);
572 };
573 container.children.iter_mut().find_map(|child| match child {
574 RenderNode::Layer(layer) if layer.node_id == Some(node_id) => Some(&mut **layer),
575 _ => None,
576 })
577}
578
579fn retained_layer_matches_layout(
582 layer: &LayerNode,
583 data: &SnapshotNodeData<'_>,
584 placement: Point,
585 wrapping: Wrapping,
586 position: Position,
587) -> bool {
588 let state = &data.layout_state;
589 let slices = &data.modifier_slices;
590 (position == Position::Moved || state.position() == placement)
591 && Rect::from_size(state.size()) == layer.node_rect()
592 && slices.layer_bounds(state.size()) == layer.local_bounds
593 && state.content_offset() == layer.content_offset
594 && (slices.outer_draw_command_count() > 0) == (wrapping == Wrapping::Wrapped)
595}
596
597fn update_layer_properties(
598 applier: &MemoryApplier,
599 container: &mut LayerNode,
600 level: Level,
601 position: Position,
602 ancestors: TranslateAncestorContext,
603) -> Option<GraphChanges> {
604 let node_id = layer_identity(container)?;
605 let wrapping = if container.wraps.is_some() {
606 Wrapping::Wrapped
607 } else {
608 Wrapping::Bare
609 };
610 let wrapped = wrapping == Wrapping::Wrapped;
611 if position == Position::Moved && wrapped {
614 return None;
615 }
616 let placement = container.origin_in_parent;
617 let changes = read_placed_node_data(applier, node_id, level, |data| {
618 let target = content_layer_mut(container)?;
619 let slices = &data.modifier_slices;
620 let state = &data.layout_state;
621 if !retained_layer_matches_layout(target, &data, placement, wrapping, position) {
622 return None;
623 }
624 let context = LowerContext::beneath(ancestors);
625 let previous = HitGraphState::of(target);
626 let (head, child_context) = prepare_node_layer(node_id, slices, state, context);
627 assign_layer(target, head);
628 if wrapped {
629 target.transform_to_parent = layer_transform_to_parent(
630 target.local_bounds,
631 Point::default(),
632 &target.graphics_layer,
633 );
634 target.origin_in_parent = Point::default();
635 }
636 if target.has_origin_sinks
637 && let Some(child_abs) = child_context.parent_abs
638 {
639 for child in &target.children {
640 if let RenderNode::Layer(child) = child {
641 publish_retained_window_geometry(applier, child, child_abs);
642 }
643 }
644 }
645 Some(previous.changes_against(&HitGraphState::of(target)))
646 })??;
647 if wrapped {
648 container.refresh_child_facts();
649 container.has_origin_sinks = children_have_origin_sinks(&container.children);
650 container.forget_own_raster_cache_hashes();
651 }
652 Some(changes)
653}
654
655pub(crate) fn publish_space(applier: &MemoryApplier, graph: &RenderGraph) {
660 if !crate::thread_state::with(|state| state.space_changed.replace(false)) {
661 return;
662 }
663 publish_space_now(applier, graph);
664}
665
666#[cold]
667#[inline(never)]
668fn publish_space_now(applier: &MemoryApplier, graph: &RenderGraph) {
669 let root = ProjectiveTransform::identity();
670 crate::space::for_each_window_eye(&graph.root, root, &mut |tree, view, to_window| {
671 if let Some(eye) = &tree.cells[0].scene.eye {
672 eye.world.rebuild(|world| view.build_world(tree, world));
673 }
674 for seen in view
675 .surfaces
676 .iter()
677 .filter(|seen| seen.layer.has_origin_sinks)
678 {
679 if let Some(content) = view.content_to_window(seen, to_window) {
680 publish_content_geometry(applier, seen.layer, content);
681 }
682 }
683 });
684}
685
686fn publish_content_geometry(
690 applier: &MemoryApplier,
691 layer: &LayerNode,
692 to_window: ProjectiveTransform,
693) {
694 if let Some(node_id) = layer.node_id {
695 read_node_data(applier, node_id, |data| {
696 data.modifier_slices.publish_window_geometry(
697 Point::default(),
698 to_window,
699 data.layout_state.size(),
700 );
701 });
702 }
703 for child in &layer.children {
704 if let RenderNode::Layer(child) = child
705 && child.has_origin_sinks
706 && child.graphics_layer.space.is_none()
707 {
708 publish_content_geometry(applier, child, child.transform_to_parent.then(to_window));
709 }
710 }
711}
712
713fn publish_retained_window_geometry(
714 applier: &MemoryApplier,
715 layer: &LayerNode,
716 parent_abs: AbsOrigin,
717) {
718 if !layer.has_origin_sinks {
719 return;
720 }
721 let child_abs = parent_abs.children_of(layer);
722 if let Some(node_id) = layer.node_id {
723 read_node_data(applier, node_id, |data| {
724 data.modifier_slices.publish_window_geometry(
725 Point {
726 x: parent_abs.content_origin.x + layer.origin_in_parent.x,
727 y: parent_abs.content_origin.y + layer.origin_in_parent.y,
728 },
729 child_abs.window_transform,
730 data.layout_state.size(),
731 );
732 });
733 }
734 for child in &layer.children {
735 if let RenderNode::Layer(child) = child {
736 publish_retained_window_geometry(applier, child, child_abs);
737 }
738 }
739}
740
741#[cold]
745#[inline(never)]
746fn note_space_change() {
747 crate::thread_state::with(|state| state.space_changed.set(true));
748}
749
750fn release_for_rebuild(layer: &mut LayerNode) {
751 layer.hit_test = None;
752 crate::layer_recycling::recycle_children(layer);
753}
754
755fn forget_dirty_subtree(layer: &LayerNode, dirty_nodes: &mut HashMap<NodeId, NodeUpdate>) {
757 if let Some(node_id) = layer_identity(layer) {
758 dirty_nodes.remove(&node_id);
759 }
760 remove_dirty_descendants(layer, dirty_nodes);
761}
762
763fn forget_dirty_node(node: &RenderNode, dirty_nodes: &mut HashMap<NodeId, NodeUpdate>) {
766 match node {
767 RenderNode::Layer(layer) => forget_dirty_subtree(layer, dirty_nodes),
768 RenderNode::Inline(inline) => {
769 dirty_nodes.remove(&inline.node_id);
770 }
771 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => {}
772 }
773}
774
775fn remove_dirty_descendants(layer: &LayerNode, dirty_nodes: &mut HashMap<NodeId, NodeUpdate>) {
776 for child in &layer.children {
777 match child {
778 RenderNode::Layer(child_layer) => {
779 if let Some(node_id) = child_layer.node_id {
780 dirty_nodes.remove(&node_id);
781 }
782 remove_dirty_descendants(child_layer, dirty_nodes);
783 }
784 RenderNode::Inline(inline) => {
785 dirty_nodes.remove(&inline.node_id);
786 }
787 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => {}
788 }
789 }
790}
791
792fn child_identity(node: &RenderNode) -> Option<NodeId> {
795 match node {
796 RenderNode::Layer(layer) => layer_identity(layer),
797 RenderNode::Inline(inline) => Some(inline.node_id),
798 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => None,
799 }
800}
801
802#[cfg(test)]
803#[path = "tests/scene_builder_tests.rs"]
804mod tests;