//! 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 { 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) -> Vec { 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) { 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, 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, 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) -> 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 { 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(&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(pub T); impl PlainValue { 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, /// The mounted branch, when the applied condition has content. current: Option, } 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) -> 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, usize) -> Option, ) { 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 { region: Region, rows: Vec>, } /// One live row. pub struct KeyedRow { key: K, extent: Extent, scope: Scope, /// The row's loop-variable signal; reused rows get updated values. pub item: Signal, /// The row's position, as a signal (`:index i` in the file). pub index: Signal, } impl KeyedList { /// Create the region (and its markers) under `parent`, at the current /// end of its children. pub fn new(tree: &mut WidgetTree, parent: Option) -> 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, usize, Signal, Signal) -> Extent, ) { if !self.region.is_mounted(tree) { return; } let keys: Vec = 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> = 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> = 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;