use rustc_hash::FxHashMap;
use std::cell::RefCell;
use std::hash::Hash;
use std::rc::Rc;
use crate::reactive::invalidation::suspend_widget_tracking;
use crate::reactive::{OwnerId, dispose_owner_now, with_owner};
use super::Widget;
use super::children::{ChildrenSource, DynItem, Leaving, OwnedWidget, SharedOwner};
pub struct StaticChild;
pub struct DynamicChild;
#[diagnostic::on_unimplemented(
message = "`.child()` takes a widget, an `Option` of one, or a reactive closure, and `{Self}` is none of them",
note = "reactive forms: `.child(move || widget)` or `.child(move || Option<widget>)` — the closure re-runs and replaces the child when a signal it reads changes",
note = "for reactive lists use `.children(...)` with a closure or `keyed(data, key, build)`"
)]
pub trait IntoChild<Marker = StaticChild> {
fn add_to_container(self, children_source: &mut ChildrenSource);
}
impl<W: Widget + 'static> IntoChild<StaticChild> for W {
fn add_to_container(self, children_source: &mut ChildrenSource) {
children_source.add_static(Box::new(self));
}
}
impl<W: Widget + 'static> IntoChild<StaticChild> for Option<W> {
fn add_to_container(self, children_source: &mut ChildrenSource) {
if let Some(widget) = self {
widget.add_to_container(children_source);
}
}
}
pub trait IntoDynChild {
fn into_dyn_child(self) -> Option<Box<dyn Widget>>;
}
impl<W: Widget + 'static> IntoDynChild for W {
fn into_dyn_child(self) -> Option<Box<dyn Widget>> {
Some(Box::new(self))
}
}
impl<W: Widget + 'static> IntoDynChild for Option<W> {
fn into_dyn_child(self) -> Option<Box<dyn Widget>> {
self.map(|w| Box::new(w) as Box<dyn Widget>)
}
}
impl<F, C> IntoChild<DynamicChild> for F
where
F: Fn() -> C + 'static,
C: IntoDynChild,
{
fn add_to_container(self, children_source: &mut ChildrenSource) {
struct State {
generation: u64,
pending: Option<(Box<dyn Widget>, OwnerId)>,
}
let state = Rc::new(RefCell::new(State {
generation: 0,
pending: None,
}));
let child_fn = Rc::new(self);
let items_fn = move |_: &Leaving| {
let mut st = state.borrow_mut();
if let Some((_, owner_id)) = st.pending.take() {
dispose_owner_now(owner_id);
}
let child_fn = Rc::clone(&child_fn);
let (built, owner_id) = with_owner(move || child_fn().into_dyn_child());
match built {
Some(widget) => {
st.generation += 1;
st.pending = Some((widget, owner_id));
let adopt_state = Rc::clone(&state);
vec![DynItem::new(st.generation, move || {
let (widget, owner_id) =
adopt_state.borrow_mut().pending.take().expect(
"reactive child factory ran without a freshly built widget",
);
OwnedWidget::new(widget, owner_id)
})]
}
None => {
dispose_owner_now(owner_id);
vec![]
}
}
};
children_source.add_dynamic(items_fn);
}
}
pub struct StaticChildren;
pub struct DynamicChildren;
pub struct ForwardedChildren;
#[diagnostic::on_unimplemented(
message = "`.children()` takes an iterator of widgets, a reactive closure returning one, `keyed(data, key, build)`, or the `ChildrenSource` a component was handed — and `{Self}` is none of those",
note = "`.children(move || widgets)` replaces all rows when a signal it reads changes; `keyed(data, key, build)` preserves per-row state via stable identity and rebuilds only rows whose item changed"
)]
pub trait IntoChildren<Marker = StaticChildren> {
fn add_to_container(self, children_source: &mut ChildrenSource);
}
impl<I, W> IntoChildren<StaticChildren> for I
where
I: IntoIterator<Item = W>,
W: Widget + 'static,
{
fn add_to_container(self, children_source: &mut ChildrenSource) {
for widget in self {
children_source.add_static(Box::new(widget));
}
}
}
impl IntoChildren<ForwardedChildren> for ChildrenSource {
fn add_to_container(self, children_source: &mut ChildrenSource) {
children_source.append(self);
}
}
impl<F, I, W> IntoChildren<DynamicChildren> for F
where
F: Fn() -> I + 'static,
I: IntoIterator<Item = W>,
W: Widget + 'static,
{
fn add_to_container(self, children_source: &mut ChildrenSource) {
struct State {
next_generation: u64,
pending: FxHashMap<u64, Box<dyn Widget>>,
}
let state = Rc::new(RefCell::new(State {
next_generation: 0,
pending: FxHashMap::default(),
}));
let list_fn = Rc::new(self);
let items_fn = move |_: &Leaving| {
let mut st = state.borrow_mut();
st.pending.clear();
let list_fn = Rc::clone(&list_fn);
let (widgets, owner_id) = with_owner(move || {
list_fn()
.into_iter()
.map(|w| Box::new(w) as Box<dyn Widget>)
.collect::<Vec<_>>()
});
if widgets.is_empty() {
dispose_owner_now(owner_id);
return vec![];
}
let shared = SharedOwner::new(owner_id);
let mut out = Vec::with_capacity(widgets.len());
for widget in widgets {
let generation = st.next_generation;
st.next_generation += 1;
st.pending.insert(generation, widget);
let adopt_state = Rc::clone(&state);
let shared = shared.clone();
out.push(DynItem::new(generation, move || {
let widget = adopt_state
.borrow_mut()
.pending
.remove(&generation)
.expect("reactive children factory ran without a freshly built row");
OwnedWidget::new_shared(widget, shared)
}));
}
out
};
children_source.add_dynamic(items_fn);
}
}
pub struct KeyedChildren<T, I, K, W> {
data: Box<dyn Fn() -> I>,
key: Box<dyn Fn(&T) -> K>,
build: Box<dyn Fn(T) -> W>,
}
pub fn keyed<T, I, K, W>(
data: impl Fn() -> I + 'static,
key: impl Fn(&T) -> K + 'static,
build: impl Fn(T) -> W + 'static,
) -> KeyedChildren<T, I, K, W>
where
T: Clone + PartialEq + 'static,
I: IntoIterator<Item = T>,
K: Hash + Eq + Clone + 'static,
W: Widget + 'static,
{
KeyedChildren {
data: Box::new(data),
key: Box::new(key),
build: Box::new(build),
}
}
impl<T, I, K, W> IntoChildren<DynamicChildren> for KeyedChildren<T, I, K, W>
where
T: Clone + PartialEq + 'static,
I: IntoIterator<Item = T> + 'static,
K: Hash + Eq + Clone + 'static,
W: Widget + 'static,
{
fn add_to_container(self, children_source: &mut ChildrenSource) {
add_keyed_children(children_source, self.data, self.key, self.build);
}
}
fn add_keyed_children<T, I, K, W>(
source: &mut ChildrenSource,
data_fn: impl Fn() -> I + 'static,
key_fn: impl Fn(&T) -> K + 'static,
build_fn: impl Fn(T) -> W + 'static,
) where
T: Clone + PartialEq + 'static,
I: IntoIterator<Item = T>,
K: Hash + Eq + Clone + 'static,
W: Widget + 'static,
{
struct Row<T> {
item: T,
generation: u64,
}
struct KeyedState<T, K> {
rows: FxHashMap<K, Row<T>>,
departed: FxHashMap<K, Row<T>>,
next_generation: u64,
pending: FxHashMap<u64, (Box<dyn Widget>, OwnerId)>,
}
let state = Rc::new(RefCell::new(KeyedState::<T, K> {
rows: FxHashMap::default(),
departed: FxHashMap::default(),
next_generation: 0,
pending: FxHashMap::default(),
}));
let items_fn = move |leaving: &Leaving| {
let items = data_fn(); let mut st = state.borrow_mut();
let st = &mut *st;
st.departed
.retain(|_, row| leaving.contains(row.generation));
for (_, (_, owner_id)) in st.pending.drain() {
dispose_owner_now(owner_id);
}
let mut seen = FxHashMap::default();
let mut out = Vec::new();
for (index, item) in items.into_iter().enumerate() {
let key = key_fn(&item);
if let Some(first) = seen.get(&key) {
log::warn!(
"keyed children: item {index} repeats the {} key of item {first}, skipping it",
std::any::type_name::<K>(),
);
continue;
}
let generation = match st.rows.get(&key) {
Some(row) if row.item == item => row.generation,
None if st.departed.get(&key).is_some_and(|row| row.item == item) => {
let row = st.departed.remove(&key).expect("just looked up");
let generation = row.generation;
st.rows.insert(key.clone(), row);
generation
}
_ => {
let generation = st.next_generation;
st.next_generation += 1;
let (widget, owner_id) = with_owner(|| {
suspend_widget_tracking(|| {
Box::new(build_fn(item.clone())) as Box<dyn Widget>
})
});
st.pending.insert(generation, (widget, owner_id));
st.rows.insert(key.clone(), Row { item, generation });
generation
}
};
seen.insert(key, index);
let adopt_state = Rc::clone(&state);
out.push(DynItem::new(generation, move || {
let (widget, owner_id) = adopt_state
.borrow_mut()
.pending
.remove(&generation)
.expect("keyed children factory ran without a freshly built widget");
OwnedWidget::new(widget, owner_id)
}));
}
st.departed
.extend(st.rows.extract_if(|key, _| !seen.contains_key(key)));
out
};
source.add_dynamic(items_fn);
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use std::rc::Rc;
use super::*;
use crate::layout::{Constraints, Size};
use crate::reactive::{create_signal, on_cleanup};
use crate::tree::LayoutCtx;
use crate::tree::{Tree, WidgetId};
use crate::widgets::{Container, container};
struct TestWidget;
impl Widget for TestWidget {
fn layout(&mut self, _: &mut LayoutCtx, _: Constraints) -> Size {
Size::zero()
}
fn paint(&self, _: &mut crate::renderer::PaintContext) {}
}
fn children_host() -> (Tree, ChildrenSource) {
let mut tree = Tree::new();
let parent = tree.register(Box::new(TestWidget));
let mut source = ChildrenSource::default();
source.set_container_id(parent);
(tree, source)
}
#[test]
fn an_optional_child_is_a_child() {
fn registered(container: Container) -> usize {
let mut tree = Tree::new();
let id = tree.register(Box::new(container));
tree.with_widget_mut(id, |w, id, t| w.register_children(t, id));
tree.get_children(id).len()
}
assert_eq!(
registered(container().child(Some(TestWidget))),
1,
"`Some(w)` is the widget"
);
assert_eq!(
registered(container().child(None::<TestWidget>)),
0,
"`None` is no child at all"
);
}
#[test]
fn colliding_keys_are_still_distinct_rows() {
#[derive(Clone, PartialEq, Eq)]
struct Collides(&'static str);
impl std::hash::Hash for Collides {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
0u8.hash(state);
}
}
let (mut tree, mut source) = children_host();
let items = create_signal(vec![Collides("a"), Collides("b")]);
IntoChildren::<DynamicChildren>::add_to_container(
keyed(
move || items.get(),
|c: &Collides| c.clone(),
|_| TestWidget,
),
&mut source,
);
let ids = source.reconcile_and_get(&mut tree).clone();
assert_eq!(ids.len(), 2, "two keys, two widgets");
items.set(vec![Collides("b"), Collides("a")]);
source.reconcile_with_tracking(&mut tree);
let reordered = source.reconcile_and_get(&mut tree).clone();
assert_eq!(reordered, vec![ids[1], ids[0]]);
}
#[test]
fn closure_child_replaces_on_tracked_change() {
let (mut tree, mut source) = children_host();
let sig = create_signal(1u64);
let unrelated = create_signal(0u64);
let builds = Rc::new(Cell::new(0usize));
let builds_counter = builds.clone();
let closure = move || {
sig.get();
builds_counter.set(builds_counter.get() + 1);
TestWidget
};
IntoChild::<DynamicChild>::add_to_container(closure, &mut source);
let first = source.reconcile_and_get(&mut tree).clone();
assert_eq!(first.len(), 1);
assert_eq!(builds.get(), 1);
unrelated.set(1);
source.reconcile_with_tracking(&mut tree);
assert_eq!(source.reconcile_and_get(&mut tree).clone(), first);
assert_eq!(builds.get(), 1);
sig.set(2);
source.reconcile_with_tracking(&mut tree);
let second = source.reconcile_and_get(&mut tree).clone();
assert_eq!(second.len(), 1);
assert_ne!(second[0], first[0]);
assert_eq!(builds.get(), 2);
sig.set(2);
source.reconcile_with_tracking(&mut tree);
assert_eq!(source.reconcile_and_get(&mut tree).clone(), second);
assert_eq!(builds.get(), 2);
}
#[test]
fn replaced_child_subtree_is_torn_down_synchronously() {
struct ParentWidget;
impl Widget for ParentWidget {
fn layout(&mut self, _: &mut LayoutCtx, _: Constraints) -> Size {
Size::zero()
}
fn paint(&self, _: &mut crate::renderer::PaintContext) {}
fn register_children(&mut self, tree: &mut Tree, id: WidgetId) {
let child = tree.register(Box::new(TestWidget));
tree.set_parent(child, id);
}
}
let (mut tree, mut source) = children_host();
let sig = create_signal(0u64);
let closure = move || {
sig.get();
ParentWidget
};
IntoChild::<DynamicChild>::add_to_container(closure, &mut source);
source.reconcile_and_get(&mut tree);
let count_after_build = tree.widget_count();
assert_eq!(count_after_build, 3);
sig.set(1);
source.reconcile_with_tracking(&mut tree);
assert_eq!(tree.widget_count(), count_after_build);
}
#[test]
fn segments_rerun_independently() {
let (mut tree, mut source) = children_host();
let sig_a = create_signal(0u64);
let sig_b = create_signal(0u64);
let closure_a = move || {
sig_a.get();
TestWidget
};
let closure_b = move || {
sig_b.get();
TestWidget
};
IntoChild::<DynamicChild>::add_to_container(closure_a, &mut source);
IntoChild::<DynamicChild>::add_to_container(closure_b, &mut source);
let first = source.reconcile_and_get(&mut tree).clone();
assert_eq!(first.len(), 2);
sig_a.set(1);
source.reconcile_with_tracking(&mut tree);
let second = source.reconcile_and_get(&mut tree).clone();
assert_ne!(second[0], first[0]);
assert_eq!(second[1], first[1]);
}
#[test]
fn closure_child_controls_presence() {
let (mut tree, mut source) = children_host();
let show = create_signal(false);
let closure = move || show.get().then_some(TestWidget);
IntoChild::<DynamicChild>::add_to_container(closure, &mut source);
assert!(source.reconcile_and_get(&mut tree).is_empty());
show.set(true);
source.reconcile_with_tracking(&mut tree);
assert_eq!(source.reconcile_and_get(&mut tree).len(), 1);
show.set(false);
source.reconcile_with_tracking(&mut tree);
assert!(source.reconcile_and_get(&mut tree).is_empty());
}
#[test]
fn closure_children_replace_all_rows_and_dispose_batch_owner() {
let (mut tree, mut source) = children_host();
let count = create_signal(2u64);
let cleanups = Rc::new(Cell::new(0usize));
let cleanups_counter = cleanups.clone();
let closure = move || {
let cleanups_counter = cleanups_counter.clone();
on_cleanup(move || cleanups_counter.set(cleanups_counter.get() + 1));
(0..count.get()).map(|_| TestWidget).collect::<Vec<_>>()
};
IntoChildren::<DynamicChildren>::add_to_container(closure, &mut source);
let first = source.reconcile_and_get(&mut tree).clone();
assert_eq!(first.len(), 2);
assert_eq!(cleanups.get(), 0);
count.set(3);
source.reconcile_with_tracking(&mut tree);
let second = source.reconcile_and_get(&mut tree).clone();
assert_eq!(second.len(), 3);
assert!(first.iter().all(|id| !second.contains(id)));
assert_eq!(cleanups.get(), 1);
}
#[test]
fn keyed_children_diff_by_identity_and_content() {
let (mut tree, mut source) = children_host();
let items = create_signal(vec![(1u64, "a".to_string()), (2, "b".to_string())]);
let builds = Rc::new(Cell::new(0usize));
let builds_counter = builds.clone();
keyed(
move || items.get(),
|(id, _)| *id,
move |_| {
builds_counter.set(builds_counter.get() + 1);
TestWidget
},
)
.add_to_container(&mut source);
let first = source.reconcile_and_get(&mut tree).clone();
assert_eq!(first.len(), 2);
assert_eq!(builds.get(), 2);
items.set(vec![(2, "b".to_string()), (1, "a".to_string())]);
source.reconcile_with_tracking(&mut tree);
let reordered = source.reconcile_and_get(&mut tree).clone();
assert_eq!(reordered, vec![first[1], first[0]]);
assert_eq!(builds.get(), 2);
items.set(vec![(2, "B!".to_string()), (1, "a".to_string())]);
source.reconcile_with_tracking(&mut tree);
let changed = source.reconcile_and_get(&mut tree).clone();
assert_eq!(changed.len(), 2);
assert_ne!(changed[0], reordered[0]); assert_eq!(changed[1], reordered[1]); assert_eq!(builds.get(), 3);
items.set(vec![(1, "a".to_string())]);
source.reconcile_with_tracking(&mut tree);
let removed = source.reconcile_and_get(&mut tree).clone();
assert_eq!(removed, vec![changed[1]]);
assert_eq!(builds.get(), 3);
}
#[test]
fn keyed_children_skip_duplicate_keys() {
let (mut tree, mut source) = children_host();
keyed(
move || vec![(7u64, "x"), (1, "a"), (7, "y"), (7, "z")],
|(id, _)| *id,
|_| TestWidget,
)
.add_to_container(&mut source);
assert_eq!(source.reconcile_and_get(&mut tree).len(), 2);
}
}