1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use crate::{
debug_scope_invalidation_sources, debug_scope_label, Command, Composer, ComposerCore,
DirtyBubble, NodeId, RecomposeScope,
};
use std::rc::Rc;
impl Composer {
pub(crate) fn recranpose_group(&self, scope: &RecomposeScope) {
struct RecomposeGuard {
composer: Composer,
scope: RecomposeScope,
}
impl Drop for RecomposeGuard {
fn drop(&mut self) {
self.composer
.close_current_group_body_for_scope(&self.scope);
self.composer
.with_slot_session_mut(|slots| slots.end_recompose());
log::trace!(
target: "cranpose::compose::recompose",
"scope_id={} label={:?} ended",
self.scope.id(),
debug_scope_label(self.scope.id()),
);
self.scope.mark_recomposed();
if let Err(err) = self.composer.flush_pending_commands_if_large() {
log::error!("mid-recomposition command flush failed: {err}");
}
}
}
// A prior callback in the same batch can detach this scope. Retained scopes
// keep their invalid bit so reactivation can schedule the deferred work.
if !scope.is_effectively_active() {
scope.defer_until_reactivated();
return;
}
// Check if scope is still invalid before recomposing.
// When parent and child scopes are both invalidated, the child may be
// visited (and marked recomposed) during parent's recomposition.
// Without this check, we'd recompose the child again with wrong parent_stack,
// causing nodes to get attached to root instead of their actual parent.
if !scope.is_invalid() {
scope.mark_recomposed();
return;
}
let started =
self.with_slot_session_mut(|slots| slots.begin_recompose_at_scope(scope.id()));
log::trace!(
target: "cranpose::compose::recompose",
"scope_id={} label={:?} started_at={started:?} sources={:?}",
scope.id(),
debug_scope_label(scope.id()),
debug_scope_invalidation_sources(scope.id()),
);
if started.is_some() {
let previous_hint = self
.core
.recranpose_parent_hint
.replace(scope.parent_hint());
struct HintGuard {
core: Rc<ComposerCore>,
previous: Option<NodeId>,
}
impl Drop for HintGuard {
fn drop(&mut self) {
self.core.recranpose_parent_hint.set(self.previous);
}
}
let _hint_guard = HintGuard {
core: self.clone_core(),
previous: previous_hint,
};
{
let mut stack = self.scope_stack();
stack.push(scope.clone());
}
let guard = RecomposeGuard {
composer: self.clone(),
scope: scope.clone(),
};
let saved_locals = self.current_local_stack();
{
let mut locals = self.local_stack();
*locals = scope.local_stack();
}
let callback_ran = self.observe_scope(scope, || scope.run_recompose(self));
log::trace!(
target: "cranpose::compose::recompose",
"scope_id={} label={:?} callback_ran={}",
scope.id(),
debug_scope_label(scope.id()),
callback_ran,
);
if !callback_ran {
if let Some(ancestor_scope) = scope.callback_promotion_target() {
ancestor_scope.invalidate();
// Keep this scope PENDING: the guard below clears its
// invalid flag, and the (arg-equal) ancestor visit would
// otherwise skip the group, silently swallowing the
// invalidation that got us here — mark_recomposed
// re-invalidates pending scopes, so the healing visit
// finds the scope invalid and re-runs its body. Only the
// ancestor path re-arms the callback; the root-render
// fallback must NOT spin (an unreachable scope would
// re-invalidate forever).
scope.request_pending_recompose();
} else {
self.request_root_render();
}
// Preserve all existing slot content until the promoted ancestor
// or the requested root render replays the subtree.
self.skip_current_group();
}
{
let mut locals = self.local_stack();
*locals = saved_locals;
}
drop(guard);
} else {
if let Some(ancestor_scope) = scope.callback_promotion_target() {
ancestor_scope.invalidate();
} else {
self.request_root_render();
}
if let Some(parent_hint) = scope.parent_hint().or_else(|| self.root()) {
self.commands_mut().push(Command::BubbleDirty {
node_id: parent_hint,
bubble: DirtyBubble::LAYOUT_AND_MEASURE,
});
}
scope.mark_recomposed();
}
}
}