1use std::any::TypeId;
2
3use crate::{
4 Applier, ChildList, Command, CommandQueue, Composer, DirtyBubble, EmittedNode, MutableState,
5 Node, NodeError, NodeId, OwnedMutableState, ParentAttachMode, ParentFrame, debug_scope_label,
6 slot::NodeSlotUpdate,
7};
8
9impl Composer {
10 fn recorded_node_parent(&self, id: NodeId) -> Option<NodeId> {
11 let mut applier = self.borrow_applier();
12 applier.get_mut(id).ok().and_then(|node| node.parent())
13 }
14
15 fn queue_replaced_slot_node_removal(&self, old_id: NodeId, old_generation: u32) {
16 let current_generation = self.borrow_applier().node_generation(old_id);
17 if current_generation != old_generation {
18 log::trace!(
19 target: "cranpose::compose::emit",
20 "skipping stale replacement cleanup for node #{old_id} (slot_generation={old_generation} current_generation={current_generation})",
21 );
22 return;
23 }
24
25 log::trace!(
26 target: "cranpose::compose::emit",
27 "removing replaced node #{old_id} (generation={old_generation})",
28 );
29 self.commands_mut().push(Command::RemoveNode { id: old_id });
30 }
31
32 #[track_caller]
33 pub fn use_state<T: Clone + 'static>(&self, init: impl FnOnce() -> T) -> MutableState<T> {
34 let source = crate::caller_location_key();
35 let runtime = self.runtime_handle();
36 let state = self.with_slot_session_mut(|slots| {
37 slots.remember(source, || {
38 OwnedMutableState::with_runtime(init(), runtime.clone())
39 })
40 });
41 state.with(|state| state.handle())
42 }
43
44 fn emit_node_box<N: Node + 'static>(
45 &self,
46 source: crate::Key,
47 make_node: impl FnOnce(&mut dyn Applier) -> EmittedNode,
48 ) -> NodeId {
49 let adopted = {
50 let mut skip = 0;
51 loop {
52 let Some((id, slot_gen)) = self
53 .with_slot_session_mut(|slots| slots.peek_node_record_by_source(source, skip))
54 else {
55 break None;
56 };
57 let (type_ok, gen_ok) = {
58 let mut applier = self.borrow_applier();
59 let gen_ok = applier.node_generation(id) == slot_gen;
60 let type_ok = match applier.get_mut(id) {
61 Ok(node) => node.as_any_mut().downcast_ref::<N>().is_some(),
62 Err(_) => false,
63 };
64 (type_ok, gen_ok)
65 };
66 if type_ok && gen_ok {
67 let committed = self.with_slot_session_mut(|slots| {
68 slots.adopt_node_record_by_source(source, skip)
69 });
70 debug_assert_eq!(committed, Some((id, slot_gen)));
71 break Some((id, slot_gen));
72 }
73 skip += 1;
74 }
75 };
76
77 if let Some((id, slot_gen)) = adopted {
78 let scope_debug = self
79 .current_recompose_scope()
80 .map(|scope| (scope.id(), debug_scope_label(scope.id())))
81 .unwrap_or((0, None));
82 log::trace!(
83 target: "cranpose::compose::emit",
84 "reusing node #{id} as {} [scope_id={} scope_label={:?}]",
85 std::any::type_name::<N>(),
86 scope_debug.0,
87 scope_debug.1,
88 );
89 self.commands_mut().push(Command::update_node::<N>(id));
90 self.attach_to_parent(id);
91 let parent_id = self.recorded_node_parent(id);
92 let recorded = self.with_slot_session_mut(|slots| {
93 slots.record_node_with_parent(id, slot_gen, parent_id, source)
94 });
95 match recorded {
96 NodeSlotUpdate::Reused {
97 id: recorded_id,
98 generation,
99 } => {
100 debug_assert_eq!(recorded_id, id);
101 debug_assert_eq!(generation, slot_gen);
102 }
103 NodeSlotUpdate::Inserted { .. } => {
104 log::warn!(
105 target: "cranpose::compose::emit",
106 "slot writer inserted node #{id} while reusing the same node identity",
107 );
108 }
109 NodeSlotUpdate::Replaced {
110 old_id,
111 old_generation,
112 ..
113 } => {
114 log::warn!(
115 target: "cranpose::compose::emit",
116 "slot writer replaced node #{old_id} while reusing node #{id}",
117 );
118 self.queue_replaced_slot_node_removal(old_id, old_generation);
119 }
120 }
121 self.core.last_node_reused.set(Some(true));
122 return id;
123 }
124
125 let (id, generation) = {
126 let mut applier = self.borrow_applier();
127 let emitted = make_node(&mut *applier);
128 let id = match emitted {
129 EmittedNode::Fresh(node) => applier.create(node),
130 EmittedNode::Recycled(recycled) => {
131 let (stable_id, node, warm_origin) = recycled.into_parts();
132 let insertion = applier.insert_recycled_node_or_create(stable_id, node);
133 if let Some(error) = insertion.fallback_error.as_ref() {
134 log::warn!(
135 target: "cranpose::compose::emit",
136 "discarding stale recycled stable id #{stable_id}: {error}",
137 );
138 }
139 applier.set_recycled_node_origin(insertion.id, warm_origin);
140 insertion.id
141 }
142 };
143 let generation = applier.node_generation(id);
144 (id, generation)
145 };
146 let scope_debug = self
147 .current_recompose_scope()
148 .map(|scope| (scope.id(), debug_scope_label(scope.id())))
149 .unwrap_or((0, None));
150 log::trace!(
151 target: "cranpose::compose::emit",
152 "creating node #{} (gen={}) as {} [scope_id={} scope_label={:?}]",
153 id,
154 generation,
155 std::any::type_name::<N>(),
156 scope_debug.0,
157 scope_debug.1,
158 );
159 self.commands_mut().push(Command::MountNode { id });
160 self.attach_to_parent(id);
161 let parent_id = self.recorded_node_parent(id);
162 let recorded = self.with_slot_session_mut(|slots| {
163 slots.record_node_with_parent(id, generation, parent_id, source)
164 });
165 match recorded {
166 NodeSlotUpdate::Inserted {
167 id: recorded_id,
168 generation: recorded_generation,
169 } => {
170 debug_assert_eq!(recorded_id, id);
171 debug_assert_eq!(recorded_generation, generation);
172 }
173 NodeSlotUpdate::Replaced {
174 old_id,
175 old_generation,
176 new_id,
177 new_generation,
178 } => {
179 debug_assert_eq!(new_id, id);
180 debug_assert_eq!(new_generation, generation);
181 self.queue_replaced_slot_node_removal(old_id, old_generation);
182 }
183 NodeSlotUpdate::Reused { .. } => {
184 log::warn!(
185 target: "cranpose::compose::emit",
186 "slot writer reported reuse for newly emitted node #{id}",
187 );
188 }
189 }
190 self.core.last_node_reused.set(Some(false));
191 id
192 }
193
194 #[track_caller]
195 pub fn emit_node<N: Node + 'static>(&self, init: impl FnOnce() -> N) -> NodeId {
196 let source = crate::caller_location_key();
197 self.emit_node_box::<N>(source, |_| EmittedNode::Fresh(Box::new(init())))
198 }
199
200 #[track_caller]
201 pub fn emit_recyclable_node<N: Node + 'static>(
202 &self,
203 init: impl FnOnce() -> N,
204 reset: impl FnOnce(&mut N),
205 ) -> NodeId {
206 let source = crate::caller_location_key();
207 self.emit_node_box::<N>(source, |applier| {
208 let key = TypeId::of::<N>();
209 if let Some(mut recycled) = applier.take_recycled_node(key) {
210 if let Some(typed) = recycled.node_mut().as_any_mut().downcast_mut::<N>() {
211 reset(typed);
212 return EmittedNode::Recycled(recycled);
213 }
214 log::warn!(
215 target: "cranpose::compose::emit",
216 "discarding recycled node shell with mismatched type for {}",
217 std::any::type_name::<N>(),
218 );
219 }
220
221 let node = Box::new(init());
222 applier.record_fresh_recyclable_creation(key);
223 if let Some(shell) = node.rehouse_for_recycle() {
224 applier.seed_recycled_node_shell(key, node.recycle_pool_limit(), shell);
225 }
226 EmittedNode::Fresh(node)
227 })
228 }
229
230 fn attach_to_parent(&self, id: NodeId) {
231 self.attach_to_parent_with_mode(id, false);
232 }
233
234 fn advance_recompose_child_cursor(&self) -> Option<usize> {
235 let cursor = self.core.recompose_child_cursor.get()?;
236 self.core.recompose_child_cursor.set(Some(cursor + 1));
237 Some(cursor)
238 }
239
240 pub(crate) fn attach_to_parent_with_mode(
241 &self,
242 id: NodeId,
243 force_reparent_current_parent: bool,
244 ) {
245 let mut parent_stack = self.parent_stack();
246 if let Some(parent_id) = parent_stack.last().map(|frame| frame.id) {
247 let stale_root_parent = self.core.root.get() == Some(parent_id) && {
248 let mut applier = self.borrow_applier();
249 applier.get_mut(parent_id).is_err()
250 };
251 if stale_root_parent {
252 parent_stack.pop();
253 self.set_root(None);
254 } else {
255 let Some(frame) = parent_stack.last_mut() else {
256 return;
257 };
258 let attach_mode = frame.attach_mode;
259 if parent_id == id {
260 return;
261 }
262 if matches!(attach_mode, ParentAttachMode::DeferredSync) {
263 frame.new_children.push(id);
264 }
265 drop(parent_stack);
266
267 {
268 let mut applier = self.borrow_applier();
269 if let Ok(child_node) = applier.get_mut(id) {
270 let existing_parent = child_node.parent();
271 let should_set = if force_reparent_current_parent {
272 existing_parent != Some(parent_id)
273 } else {
274 match existing_parent {
275 None => true,
276 Some(existing) => {
277 let root_id = self.core.root.get();
278 parent_id != root_id.unwrap_or(0)
279 || existing == root_id.unwrap_or(0)
280 }
281 }
282 };
283 if should_set {
284 child_node.set_parent_for_bubbling(parent_id);
285 }
286 }
287 }
288 if matches!(attach_mode, ParentAttachMode::ImmediateAppend) {
289 self.commands_mut().push(Command::AttachChild {
290 parent_id,
291 child_id: id,
292 insert_index: None,
293 bubble: DirtyBubble::LAYOUT_AND_MEASURE,
294 });
295 }
296 return;
297 }
298 }
299 drop(parent_stack);
300
301 let in_subcompose = !self.subcompose_stack().is_empty();
302 if in_subcompose {
303 let has_parent = {
304 let mut applier = self.borrow_applier();
305 applier
306 .get_mut(id)
307 .map(|node| node.parent().is_some())
308 .unwrap_or(false)
309 };
310
311 if !has_parent {
312 let mut subcompose_stack = self.subcompose_stack();
313 if let Some(frame) = subcompose_stack.last_mut() {
314 frame.nodes.push(id);
315 }
316 }
317 return;
318 }
319
320 if let Some(parent_hint) = self.core.recompose_parent_hint.get() {
321 if parent_hint == id {
322 debug_assert_ne!(
323 parent_hint, id,
324 "a node cannot be attached as its own parent"
325 );
326 return;
327 }
328 let parent_status = {
329 let mut applier = self.borrow_applier();
330 applier
331 .get_mut(id)
332 .map(|node| node.parent())
333 .unwrap_or(None)
334 };
335 match parent_status {
336 Some(existing) if existing == parent_hint => {
337 self.advance_recompose_child_cursor();
338 }
339 None => {
340 let insert_index = self.advance_recompose_child_cursor();
341 self.commands_mut().push(Command::AttachChild {
342 parent_id: parent_hint,
343 child_id: id,
344 insert_index,
345 bubble: DirtyBubble::LAYOUT_AND_MEASURE,
346 });
347 }
348 Some(_) => {}
349 }
350 return;
351 }
352
353 let has_parent = {
354 let mut applier = self.borrow_applier();
355 applier
356 .get_mut(id)
357 .map(|node| node.parent().is_some())
358 .unwrap_or(false)
359 };
360 if has_parent {
361 return;
362 }
363
364 self.set_root(Some(id));
365 }
366
367 pub fn with_node_mut<N: Node + 'static, R>(
368 &self,
369 id: NodeId,
370 f: impl FnOnce(&mut N) -> R,
371 ) -> Result<R, NodeError> {
372 let mut applier = self.borrow_applier();
373 let node = applier.get_mut(id)?;
374 let typed = node
375 .as_any_mut()
376 .downcast_mut::<N>()
377 .ok_or(NodeError::TypeMismatch {
378 id,
379 expected: std::any::type_name::<N>(),
380 })?;
381 Ok(f(typed))
382 }
383
384 pub fn push_parent(&self, id: NodeId) {
385 let reused = self.core.last_node_reused.take().unwrap_or(true);
386 let in_subcompose = !self.core.subcompose_stack.borrow().is_empty();
387
388 let mut previous = ChildList::new();
389 if reused || in_subcompose {
390 previous.extend(self.get_node_children(id));
391 } else {
392 let existing_children = self.get_node_children(id);
393 if !existing_children.is_empty() {
394 previous.extend(existing_children);
395 }
396 }
397 let attach_mode = if in_subcompose || !previous.is_empty() {
398 ParentAttachMode::DeferredSync
399 } else {
400 ParentAttachMode::ImmediateAppend
401 };
402
403 self.parent_stack().push(ParentFrame {
404 id,
405 previous,
406 new_children: ChildList::new(),
407 new_children_membership: None,
408 attach_mode,
409 synthetic_root: false,
410 });
411 }
412
413 pub fn pop_parent(&self) {
414 let frame_opt = {
415 let mut stack = self.parent_stack();
416 stack.pop()
417 };
418 if let Some(frame) = frame_opt {
419 let ParentFrame {
420 id,
421 previous,
422 new_children,
423 new_children_membership: _new_children_membership,
424 attach_mode,
425 synthetic_root: _synthetic_root,
426 } = frame;
427
428 log::trace!(target: "cranpose::compose::parent", "pop_parent: node #{}", id);
429 log::trace!(
430 target: "cranpose::compose::parent",
431 "previous children: {:?}",
432 previous
433 );
434 log::trace!(
435 target: "cranpose::compose::parent",
436 "new children: {:?}",
437 new_children
438 );
439 if matches!(attach_mode, ParentAttachMode::DeferredSync) {
440 let _ = previous;
441 self.commands_mut().push(Command::SyncChildren {
442 parent_id: id,
443 expected_children: new_children,
444 });
445 }
446 }
447 }
448
449 pub(crate) fn take_commands(&self) -> CommandQueue {
450 std::mem::take(&mut *self.commands_mut())
451 }
452
453 pub fn apply_pending_commands(&self) -> Result<(), NodeError> {
458 let commands = self.take_commands();
459 let runtime_handle = self.runtime_handle();
460 let result = {
461 let mut applier = self.borrow_applier();
462 let mut result = commands.apply(&mut *applier);
463 if result.is_ok() {
464 for update in runtime_handle.take_updates() {
465 if let Err(err) = update.apply(&mut *applier) {
466 result = Err(err);
467 break;
468 }
469 }
470 }
471 result
472 };
473 if result.is_err() {
474 let host = self.active_slots_host();
475 if !host.has_active_pass() {
476 host.abandon_after_apply_failure();
477 }
478 }
479 result?;
480 runtime_handle.drain_ui();
481 Ok(())
482 }
483
484 pub fn register_side_effect(&self, effect: impl FnOnce() + 'static) {
485 self.side_effects_mut().push(Box::new(effect));
486 }
487
488 pub fn take_side_effects(&self) -> Vec<Box<dyn FnOnce()>> {
489 std::mem::take(&mut *self.side_effects_mut())
490 }
491
492 pub(crate) fn root(&self) -> Option<NodeId> {
493 self.core.root.get()
494 }
495
496 pub(crate) fn set_root(&self, node: Option<NodeId>) {
497 self.core.root.set(node);
498 }
499}