dynamic.rs raw

//! Dynamic regions: the runtime machinery behind structural `(if …)` and
//! keyed `(for …)` in component files.
//!
//! A region owns two hidden **marker** widgets (`Display::None`, so they
//! never affect layout, painting, or hit testing) that bracket its place
//! among its siblings: rows and branches mount between them, so a region can
//! grow, shrink, and reorder without disturbing anything around it. Both
//! compiler consumers — the proc-macro's generated code and the IR
//! interpreter — drive these same types, so structural reactivity cannot
//! drift between them.
//!
//! Two markers rather than one because a region does not occupy a single
//! sibling slot; it occupies a **run**, whose length changes as it reacts.
//! Bracketing it is what lets a region be a branch or a row of *another*
//! region: the enclosing one reads the run from the tree when it needs it,
//! and the answer is right however the inner region has changed since. A
//! transparent wrapper widget would be the other way to do it, and is not
//! available — taffy has no `Display::Contents`, so a wrapper would be a real
//! box that changes the layout its contents take part in.
//!
//! A region also owns the reactive scopes of the branches and rows it mounts,
//! so it has a lifetime rather than only a place: disposing the scope that
//! mounted a region disposes everything the region built. Both consumers drive
//! a region through the command queue, so a region can be gone by the time an
//! update reaches it — an enclosing region retired the row it is, or its
//! component was unmounted — and an update that arrives then does nothing,
//! exactly as a queued mutation of a removed widget does.

use guiduck_signals::{Scope, Signal};

use super::{Widget, WidgetId, WidgetTree};

/// What one branch or row occupies among its siblings.
///
/// An ordinary body is a single widget, however deep its own subtree — its
/// children are inside it, not beside it. A body that is *itself* a region is
/// a run, and is named by its bounds rather than its members precisely
/// because those members change: the inner region mounts and retires content
/// long after the outer one placed it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Extent {
    /// One widget.
    Single(WidgetId),
    /// A nested region: everything from its leading anchor to its trailing
    /// one, inclusive.
    Region { lead: WidgetId, trail: WidgetId },
}

impl Extent {
    /// The widgets this extent covers, in sibling order, read out of a
    /// snapshot of the parent's children.
    fn widgets(self, children: &[WidgetId]) -> Vec<WidgetId> {
        match self {
            Self::Single(id) => vec![id],
            Self::Region { lead, trail } => {
                let start = children.iter().position(|c| *c == lead);
                let end = children.iter().position(|c| *c == trail);
                match (start, end) {
                    // A region always brackets its own content, so `lead`
                    // precedes `trail`; the guard keeps a torn-down region
                    // from panicking during teardown.
                    (Some(start), Some(end)) if start <= end => children[start..=end].to_vec(),
                    _ => Vec::new(),
                }
            }
        }
    }

    /// The widgets this extent covers, looked up live.
    fn widgets_in(self, tree: &WidgetTree, parent: Option<WidgetId>) -> Vec<WidgetId> {
        match (self, parent) {
            (Self::Single(id), _) => vec![id],
            (Self::Region { .. }, Some(parent)) => self.widgets(tree.children(parent)),
            (Self::Region { .. }, None) => Vec::new(),
        }
    }

    /// Tear the body down: every widget of the run, not just its first.
    fn remove_from(self, tree: &mut WidgetTree, parent: Option<WidgetId>) {
        for id in self.widgets_in(tree, parent) {
            tree.remove(id);
        }
    }
}

/// Move a freshly-built body — appended at the end of `parent`'s children —
/// to `at`, keeping its widgets contiguous and in order.
///
/// Both consumers build a branch or row by appending and then placing it,
/// which is why this is public: it is the one definition of "put this body
/// where the region says", and a run has to move as a block.
pub fn place(tree: &mut WidgetTree, parent: Option<WidgetId>, extent: Extent, at: usize) {
    let Some(parent) = parent else {
        return;
    };
    for (offset, id) in extent
        .widgets_in(tree, Some(parent))
        .into_iter()
        .enumerate()
    {
        tree.move_child(parent, id, at + offset);
    }
}

/// What every dynamic region has: its place among its siblings, and
/// ownership of whatever it currently has mounted.
struct Region {
    parent: Option<WidgetId>,
    lead: WidgetId,
    trail: WidgetId,
    /// Owns the scopes of the branches and rows this region mounts.
    ///
    /// A region's content is built from the command queue, where no scope is
    /// entered — so without an owner of its own, every branch and row scope
    /// would be a child of the root and would outlive the region that made
    /// it. This scope is created where the region is, inside whatever scope
    /// is mounting it, so disposing that one disposes the region's content
    /// with it: teardown stays a scope cascade.
    scope: Scope,
}

impl Region {
    /// Anchor the region under `parent`, at the current end of its children.
    fn new(tree: &mut WidgetTree, parent: Option<WidgetId>) -> Self {
        // A control node cannot be a component's root (validation rejects
        // it), but headless markers keep this total.
        let lead = tree.insert(Marker, taffy::Style::default(), parent);
        let trail = tree.insert(Marker, taffy::Style::default(), parent);
        Self {
            parent,
            lead,
            trail,
            scope: Scope::new(),
        }
    }

    fn extent(&self) -> Extent {
        Extent::Region {
            lead: self.lead,
            trail: self.trail,
        }
    }

    /// Where new content mounts: immediately before the trailing anchor —
    /// or `None` if the region is no longer in the tree.
    ///
    /// A region is updated through the command queue, so it can be torn down
    /// between an effect queueing an update and the drain running it: an
    /// enclosing region retires the row it is, or its component is unmounted.
    /// Every other command target already answers that way — a `mutate` on a
    /// removed widget does nothing — and a region, which reads its own place
    /// out of the tree, has nothing to read.
    fn insert_index(&self, tree: &WidgetTree) -> Option<usize> {
        match self.parent {
            Some(parent) => tree.child_index(parent, self.trail),
            None => Some(0),
        }
    }

    /// Whether the region is still mounted.
    fn is_mounted(&self, tree: &WidgetTree) -> bool {
        self.insert_index(tree).is_some()
    }

    /// Run `f` with this region owning any scope it creates.
    fn run<R>(&self, f: impl FnOnce() -> R) -> R {
        self.scope.run(f)
    }
}

/// A plain value wearing a signal's `.get()` spelling, so generated key
/// expressions can evaluate loop variables before any row (and thus any row
/// signal) exists.
pub struct PlainValue<T: Clone>(pub T);

impl<T: Clone> PlainValue<T> {
    pub fn get(&self) -> T {
        self.0.clone()
    }
}

/// The invisible position anchor a dynamic region mounts before.
pub struct Marker;

impl Widget for Marker {
    fn adjust_style(&self, style: &mut taffy::Style) {
        style.display = taffy::Display::None;
    }

    fn type_name(&self) -> &'static str {
        "marker"
    }
}

/// A structural `if`: at most one mounted branch, swapped when the
/// condition flips. Branch contents live in their own reactive scope, so a
/// swap disposes the old branch's effects and signals with it.
pub struct Conditional {
    region: Region,
    /// The condition value last applied; `None` before the first `set`.
    applied: Option<bool>,
    /// The mounted branch, when the applied condition has content.
    current: Option<Branch>,
}

struct Branch {
    extent: Extent,
    scope: Scope,
}

impl Conditional {
    /// Create the region (and its markers) under `parent`, at the current
    /// end of its children — call in document order, like any insert.
    pub fn new(tree: &mut WidgetTree, parent: Option<WidgetId>) -> Self {
        Self {
            region: Region::new(tree, parent),
            applied: None,
            current: None,
        }
    }

    /// This region's own extent, for an enclosing region that is placing it
    /// as a branch or a row.
    pub fn extent(&self) -> Extent {
        self.region.extent()
    }

    /// Bring the region in line with `condition`. `build` mounts the
    /// branch's subtree for a given condition value — returning `None` when
    /// that branch has no content (an `if` without an `else`) — and runs
    /// inside a fresh scope owned by the branch.
    pub fn set(
        &mut self,
        tree: &mut WidgetTree,
        condition: bool,
        build: impl FnOnce(&mut WidgetTree, Option<WidgetId>, usize) -> Option<Extent>,
    ) {
        if self.applied == Some(condition) || !self.region.is_mounted(tree) {
            return;
        }
        if let Some(old) = self.current.take() {
            // Dispose first: the old branch's effects must not rebuild into
            // widgets that are on their way out.
            old.scope.dispose();
            old.extent.remove_from(tree, self.region.parent);
        }
        let scope = self.region.run(Scope::new);
        let at = self
            .region
            .insert_index(tree)
            .expect("the anchors outlive the region's own content");
        let extent = scope.run(|| build(tree, self.region.parent, at));
        self.applied = Some(condition);
        self.current = match extent {
            Some(extent) => Some(Branch { extent, scope }),
            None => {
                scope.dispose();
                None
            }
        };
    }
}

/// A keyed `for`: one mounted row per item, reconciled by key. Rows whose
/// key survives a change keep their widgets, scope, and state — their item
/// and index signals update in place; new keys mount, vanished keys
/// dispose, and reordered keys move.
pub struct KeyedList<K, T: 'static> {
    region: Region,
    rows: Vec<KeyedRow<K, T>>,
}

/// One live row.
pub struct KeyedRow<K, T: 'static> {
    key: K,
    extent: Extent,
    scope: Scope,
    /// The row's loop-variable signal; reused rows get updated values.
    pub item: Signal<T>,
    /// The row's position, as a signal (`:index i` in the file).
    pub index: Signal<i64>,
}

impl<K: PartialEq + Clone, T: Clone + PartialEq + 'static> KeyedList<K, T> {
    /// Create the region (and its markers) under `parent`, at the current
    /// end of its children.
    pub fn new(tree: &mut WidgetTree, parent: Option<WidgetId>) -> Self {
        Self {
            region: Region::new(tree, parent),
            rows: Vec::new(),
        }
    }

    /// This region's own extent, for an enclosing region that is placing it
    /// as a branch or a row.
    pub fn extent(&self) -> Extent {
        self.region.extent()
    }

    /// Update surviving rows' item and index signals against `items`.
    ///
    /// Called in the *effect* phase (structural work must wait for the
    /// command phase, but signal updates must not): effects reading a
    /// reused row's signals re-run in the same flush, so a reorder never
    /// shows a frame of moved rows wearing stale labels.
    pub fn sync_rows(&mut self, items: &[T], key_of: impl Fn(usize, &T) -> K) {
        for (position, item) in items.iter().enumerate() {
            let key = key_of(position, item);
            if let Some(row) = self.rows.iter().find(|row| row.key == key) {
                if row.item.get_untracked() != *item {
                    row.item.set(item.clone());
                }
                if row.index.get_untracked() != position as i64 {
                    row.index.set(position as i64);
                }
            }
        }
    }

    /// Bring the rows in line with `items`. `key_of` derives each item's
    /// identity; `build` mounts a new row's subtree (given the parent, the
    /// insertion index, and the row's item/index signals) inside a fresh
    /// row scope.
    pub fn reconcile(
        &mut self,
        tree: &mut WidgetTree,
        items: &[T],
        key_of: impl Fn(usize, &T) -> K,
        build: impl Fn(&mut WidgetTree, Option<WidgetId>, usize, Signal<T>, Signal<i64>) -> Extent,
    ) {
        if !self.region.is_mounted(tree) {
            return;
        }
        let keys: Vec<K> = items
            .iter()
            .enumerate()
            .map(|(i, item)| key_of(i, item))
            .collect();

        // Retire rows whose key vanished.
        let (kept, retired): (Vec<_>, Vec<_>) =
            self.rows.drain(..).partition(|row| keys.contains(&row.key));
        self.rows = kept;
        for row in retired {
            row.scope.dispose();
            row.extent.remove_from(tree, self.region.parent);
        }

        // Reuse or create, building the new row order.
        let mut new_rows: Vec<KeyedRow<K, T>> = Vec::with_capacity(items.len());
        for (position, (item, key)) in items.iter().zip(keys.iter()).enumerate() {
            match self.rows.iter().position(|row| row.key == *key) {
                Some(existing) => {
                    let row = self.rows.remove(existing);
                    if row.item.get_untracked() != *item {
                        row.item.set(item.clone());
                    }
                    if row.index.get_untracked() != position as i64 {
                        row.index.set(position as i64);
                    }
                    new_rows.push(row);
                }
                None => {
                    let scope = self.region.run(Scope::new);
                    let (extent, item_signal, index_signal) = scope.run(|| {
                        let item_signal = Signal::new(item.clone());
                        let index_signal = Signal::new(position as i64);
                        let at = self
                            .region
                            .insert_index(tree)
                            .expect("the anchors outlive the region's own rows");
                        let extent = build(tree, self.region.parent, at, item_signal, index_signal);
                        (extent, item_signal, index_signal)
                    });
                    new_rows.push(KeyedRow {
                        key: key.clone(),
                        extent,
                        scope,
                        item: item_signal,
                        index: index_signal,
                    });
                }
            }
        }
        self.rows = new_rows;

        // Reorder: settle each row at its final position, in row order,
        // ending immediately before the trailing marker.
        //
        // Runs are read from one snapshot taken before anything moves, so a
        // row whose body is a nested region contributes however many widgets
        // it currently has. Every move lands inside the window the rows
        // already occupy, so the marker does not shift under us.
        if let Some(parent) = self.region.parent {
            let children = tree.children(parent).to_vec();
            let runs: Vec<Vec<WidgetId>> = self
                .rows
                .iter()
                .map(|row| row.extent.widgets(&children))
                .collect();
            let marker_at = children
                .iter()
                .position(|c| *c == self.region.trail)
                .expect("the anchors outlive the region's own rows");
            let total: usize = runs.iter().map(Vec::len).sum();
            let mut at = marker_at - total;
            for id in runs.into_iter().flatten() {
                tree.move_child(parent, id, at);
                at += 1;
            }
        }
    }

    /// How many rows are currently mounted.
    pub fn len(&self) -> usize {
        self.rows.len()
    }

    pub fn is_empty(&self) -> bool {
        self.rows.is_empty()
    }
}

#[cfg(test)]
mod tests;