cranpose-core 0.9.16

Composition runtime and reactive state for Cranpose
Documentation
use crate::{
    Command, Composer, ComposerCore, DirtyBubble, NodeId, RecomposeScope,
    debug_scope_invalidation_sources, debug_scope_label,
};

#[derive(Clone, Copy)]
pub(crate) enum RecomposeChildCursor {
    Unknown,
    After { first: NodeId, placed: usize },
    At(usize),
}

impl RecomposeChildCursor {
    fn at_first_root(parent_hint: Option<NodeId>, first_root: Option<NodeId>) -> Self {
        match (parent_hint, first_root) {
            (Some(_), Some(first)) => Self::After { first, placed: 0 },
            _ => Self::Unknown,
        }
    }
}

impl Composer {
    fn begin_scope_group(&self, scope: &RecomposeScope) -> Option<Option<NodeId>> {
        let started = self.with_slot_session_mut(|slots| {
            slots
                .begin_recompose_at_scope(scope)
                .map(|group| (group, slots.open_group_first_root_node()))
        });
        log::trace!(
            target: "cranpose::compose::recompose",
            "scope_id={} label={:?} started_at={:?} sources={:?}",
            scope.id(),
            debug_scope_label(scope.id()),
            started.map(|(group, _)| group),
            debug_scope_invalidation_sources(scope.id()),
        );
        started.map(|(_, first_root)| first_root)
    }

    pub(crate) fn resolve_recompose_child_cursor(&self) -> RecomposeChildCursor {
        let cursor = self.core.recompose_child_cursor.get();
        let RecomposeChildCursor::After { first, placed } = cursor else {
            return cursor;
        };
        let index = self.core.recompose_parent_hint.get().and_then(|parent| {
            let mut applier = self.borrow_applier();
            applier.get_mut(parent).ok()?.owned_child_index(first)
        });
        let resolved = index.map_or(RecomposeChildCursor::Unknown, |index| {
            RecomposeChildCursor::At(index + placed)
        });
        self.core.recompose_child_cursor.set(resolved);
        resolved
    }

    pub(crate) fn recompose_group(&self, scope: &RecomposeScope) {
        struct RecomposeGuard<'a> {
            composer: &'a Composer,
            scope: &'a RecomposeScope,
        }

        impl Drop for RecomposeGuard<'_> {
            fn drop(&mut self) {
                self.composer.close_current_group_body_for_scope(self.scope);
                #[expect(
                    clippy::redundant_closure_for_method_calls,
                    reason = "the method path is not general over the session lifetime"
                )]
                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}");
                }
            }
        }

        if !scope.is_effectively_active() {
            scope.defer_until_reactivated();
            return;
        }

        if !scope.is_invalid() {
            scope.mark_recomposed();
            return;
        }
        if let Some(first_root) = self.begin_scope_group(scope) {
            let parent_hint = scope.parent_hint();
            let previous_cursor = self.resolve_recompose_child_cursor();
            let cursor = RecomposeChildCursor::at_first_root(parent_hint, first_root);
            let previous_hint = self.core.recompose_parent_hint.replace(parent_hint);
            self.core.recompose_child_cursor.set(cursor);
            struct HintGuard<'a> {
                core: &'a ComposerCore,
                previous: Option<NodeId>,
                previous_cursor: RecomposeChildCursor,
            }
            impl Drop for HintGuard<'_> {
                fn drop(&mut self) {
                    self.core.recompose_parent_hint.set(self.previous);
                    self.core.recompose_child_cursor.set(self.previous_cursor);
                }
            }
            let _hint_guard = HintGuard {
                core: &self.core,
                previous: previous_hint,
                previous_cursor,
            };
            {
                let mut stack = self.scope_stack();
                stack.push(scope.clone());
            }
            let guard = RecomposeGuard {
                composer: self,
                scope,
            };
            let saved_locals = std::mem::replace(&mut *self.local_stack(), scope.local_stack());
            let callback_ran = 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();
                    scope.request_pending_recompose();
                } else {
                    self.request_root_render();
                }
                self.skip_current_group();
            }
            *self.local_stack() = 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();
        }
    }
}