use std::cell::RefCell;
use std::collections::HashMap;
use std::hash::Hash;
use std::panic::{self, AssertUnwindSafe};
use std::rc::{Rc, Weak};
use std::time::Duration;
use crabshell_reactive::{Memo, Owner, Signal, untrack};
use crate::cx::Cx;
use crate::input::panic_message;
use crate::prop::Prop;
use crate::timer::Timer;
use crate::tree::{NodeId, Tree, borrow};
use crate::widgets::{Element, Stack, Widget};
pub(crate) struct KeyedSlot {
pub(crate) tree: Weak<RefCell<Tree>>,
pub(crate) container: NodeId,
pub(crate) group: usize,
pub(crate) cx: Cx,
pub(crate) path: Rc<str>,
}
pub(crate) type KeyedMount = Box<dyn FnOnce(KeyedSlot)>;
type Exit = Rc<dyn Fn() -> Duration>;
pub(crate) struct KeyedList {
pub(crate) exit: Option<Prop<Duration>>,
start: Box<dyn FnOnce(KeyedSlot, Option<Exit>)>,
}
impl KeyedList {
pub(crate) fn into_mount(self) -> KeyedMount {
let KeyedList { exit, start } = self;
Box::new(move |slot| start(slot, exit.map(Prop::shared)))
}
}
struct Entry<T> {
cx: Cx,
value: Signal<T>,
node: NodeId,
leaving: Option<Signal<bool>>,
exit: Option<(Owner, Timer)>,
}
fn build<W: Widget>(label: &str, view: impl FnOnce() -> W) -> Element {
match panic::catch_unwind(AssertUnwindSafe(|| Box::new(view()).into_element())) {
Ok(element) => element,
Err(payload) => {
tracing::error!(
"{label}: building an item panicked; it shows empty: {}",
panic_message(payload.as_ref())
);
Box::new(Stack::new()).into_element()
}
}
}
fn exit_duration(label: &str, exit: &dyn Fn() -> Duration) -> Duration {
match panic::catch_unwind(AssertUnwindSafe(|| untrack(exit))) {
Ok(duration) => duration,
Err(payload) => {
tracing::error!(
"{label}: its exit panicked; the child goes at once: {}",
panic_message(payload.as_ref())
);
Duration::ZERO
}
}
}
pub(crate) fn mount<T, K, W>(
mut items: impl FnMut() -> Vec<T> + 'static,
mut key: impl FnMut(&T) -> K + 'static,
mut view: impl FnMut(&Cx, Memo<T>) -> W + 'static,
) -> KeyedList
where
T: Clone + PartialEq + 'static,
K: Eq + Hash + 'static,
W: Widget,
{
let start = move |slot: KeyedSlot, exit: Option<Exit>| {
let label = format!("{}.children", slot.path);
let mut entries: HashMap<K, Entry<T>> = HashMap::new();
let mut order: Vec<NodeId> = Vec::new();
let mut warned = false;
let exit = exit.map(|exit| (exit, slot.cx.signal(())));
let cx = slot.cx.clone();
let name = label.clone();
cx.effect(move || {
if let Some((_, finished)) = &exit {
finished.get();
}
let list = items();
let mut next: Vec<(K, T)> = Vec::with_capacity(list.len());
for item in list {
let item_key = key(&item);
if next.iter().any(|(kept, _)| *kept == item_key) {
if !warned {
tracing::warn!("{label}: two items have the same key; showing the first");
warned = true;
}
continue;
}
next.push((item_key, item));
}
let mut old = std::mem::take(&mut entries);
let mut added = Vec::new();
let mut kept = Vec::with_capacity(next.len());
for (item_key, item) in next {
match old.remove(&item_key) {
Some(mut entry) => {
entry.value.set(item);
if let Some(leaving) = entry.leaving
&& leaving.get_untracked()
{
leaving.set(false);
if let Some((owner, _)) = entry.exit.take() {
owner.dispose();
}
}
kept.push((item_key, Some(entry)));
}
None => {
let cx = slot.cx.child();
let leaving = exit.as_ref().map(|_| cx.signal(false));
let cx = match leaving {
Some(leaving) => cx.in_item(leaving.read_only()),
None => cx,
};
let value = cx.signal(item);
let memo = cx.memo(move || value.get());
let element = untrack(|| build(&label, || view(&cx, memo)));
added.push((cx, value, leaving, element));
kept.push((item_key, None));
}
}
}
let mut removed = Vec::new();
let mut going = Vec::new();
for (item_key, mut entry) in old {
let (Some((duration, finished)), Some(leaving)) = (exit.as_ref(), entry.leaving) else {
removed.push(entry);
continue;
};
if !leaving.get_untracked() {
let duration = exit_duration(&label, duration.as_ref());
if duration.is_zero() {
removed.push(entry);
continue;
}
leaving.set(true);
let finished = *finished;
let scope = entry.cx.child();
let timer = scope.after(duration, move || {
finished.try_set(());
});
entry.exit = Some((scope.owner(), timer));
going.push((item_key, entry));
} else if entry
.exit
.is_some_and(|(_, timer)| untrack(|| timer.running()))
{
going.push((item_key, entry));
} else {
removed.push(entry);
}
}
let Some(tree) = slot.tree.upgrade() else {
return;
};
{
let mut tree = borrow(&tree);
let mut added = added.into_iter();
let mut nodes = Vec::with_capacity(kept.len());
for (item_key, entry) in kept {
let entry = match entry {
Some(entry) => entry,
None => {
let (cx, value, leaving, element) = added
.next()
.expect("invariant: every new key has a built element");
let node = tree.mount(element, &cx, slot.container);
Entry {
cx,
value,
node,
leaving,
exit: None,
}
}
};
nodes.push(entry.node);
entries.insert(item_key, entry);
}
let mut at = 0;
for node in &order {
if going.iter().any(|(_, entry)| entry.node == *node) {
nodes.insert(at, *node);
at += 1;
} else if let Some(index) = nodes.iter().position(|kept| kept == node) {
at = index + 1;
}
}
entries.extend(going);
for entry in &removed {
tree.unmount(entry.node);
}
if nodes != order {
tree.set_group(slot.container, slot.group, nodes.clone());
order = nodes;
}
}
for entry in removed {
entry.cx.owner().dispose();
}
})
.named(name);
};
KeyedList {
exit: None,
start: Box::new(start),
}
}