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