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;