use std::cell::RefCell;
use std::collections::HashMap;
use std::hash::Hash;
use std::rc::Rc;
use crate::reactive::{effect, on_cleanup, Owner};
use super::handle::Element;
use super::list_provider::{NativeItemProvider, INVALID_ITEM_INDEX};
use super::renderer::{element_sign, install_list_native_item_provider, set_update_list_info};
struct Slot {
#[allow(dead_code)] element: Element,
owner: Owner,
}
pub fn list_mount<T, K, ItemsFn, KeyFn, BodyFn>(
handle: Element,
items: ItemsFn,
key: KeyFn,
body: BodyFn,
) where
T: Clone + 'static,
K: Eq + Hash + Clone + 'static,
ItemsFn: Fn() -> Vec<T> + 'static,
KeyFn: Fn(&T) -> K + 'static,
BodyFn: Fn(T) -> Element + 'static,
{
let _ = key;
let current_items: Rc<RefCell<Vec<T>>> = Rc::new(RefCell::new(Vec::new()));
let slots: Rc<RefCell<HashMap<i32, Slot>>> = Rc::new(RefCell::new(HashMap::new()));
let body = Rc::new(body);
{
let current_items = current_items.clone();
effect(move || {
let new_items = items();
let count = new_items.len() as i32;
*current_items.borrow_mut() = new_items;
set_update_list_info(handle, count);
});
}
let provider = NativeItemProvider {
component_at_index: {
let current_items = current_items.clone();
let slots = slots.clone();
let body = body.clone();
Box::new(move |index, _op_id, _reuse| {
let item = match current_items.borrow().get(index as usize) {
Some(t) => t.clone(),
None => return INVALID_ITEM_INDEX,
};
let owner = Owner::new(None);
let element = owner.with(|| (body)(item));
let sign = element_sign(element);
slots.borrow_mut().insert(sign, Slot { element, owner });
sign
})
},
enqueue_component: Some({
let slots = slots.clone();
Box::new(move |sign| {
if let Some(slot) = slots.borrow_mut().remove(&sign) {
slot.owner.dispose();
}
})
}),
};
install_list_native_item_provider(handle, provider);
on_cleanup(move || {
let mut slots = slots.borrow_mut();
for (_, slot) in slots.drain() {
slot.owner.dispose();
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::element::ElementTag;
use crate::reactive::flush;
use crate::view::renderer::{install_renderer, uninstall_renderer, DynRenderer};
use crate::view::{create_element_by_name, BindType};
#[derive(Default)]
struct CapturingRenderer {
next_id: u32,
last_count: Rc<RefCell<Option<i32>>>,
installed: Rc<RefCell<Option<NativeItemProvider>>>,
}
impl DynRenderer for CapturingRenderer {
fn create_element(&mut self, _tag: ElementTag) -> Element {
let id = self.next_id;
self.next_id += 1;
Element::from_raw(id)
}
fn create_element_by_name(&mut self, _tag: &str) -> Element {
let id = self.next_id;
self.next_id += 1;
Element::from_raw(id)
}
fn release_element(&mut self, _h: Element) {}
fn element_sign(&self, handle: Element) -> i32 {
handle.id() as i32
}
fn set_attribute(&mut self, _h: Element, _k: &str, _v: &str) {}
fn set_inline_styles(&mut self, _h: Element, _css: &str) {}
fn set_update_list_info(&mut self, _h: Element, count: i32) {
*self.last_count.borrow_mut() = Some(count);
}
fn install_list_native_item_provider(
&mut self,
_h: Element,
provider: NativeItemProvider,
) -> bool {
*self.installed.borrow_mut() = Some(provider);
true
}
fn append_child(&mut self, _p: Element, _c: Element) {}
fn remove_child(&mut self, _p: Element, _c: Element) {}
fn set_event_listener(
&mut self,
_h: Element,
_n: &str,
_bt: BindType,
_cb: Box<dyn Fn(crate::value::WhiskerValue) + 'static>,
) {
}
fn set_root(&mut self, _p: Element) {}
fn flush(&mut self) {}
}
fn with_capturing<R>(
f: impl FnOnce(Rc<RefCell<Option<i32>>>, Rc<RefCell<Option<NativeItemProvider>>>) -> R,
) -> R {
crate::reactive::__reset_for_tests();
let recorder = CapturingRenderer::default();
let last_count = recorder.last_count.clone();
let installed = recorder.installed.clone();
let prev = install_renderer(Box::new(recorder));
let r = f(last_count, installed);
uninstall_renderer(prev);
r
}
#[test]
fn list_mount_broadcasts_initial_count_and_installs_provider() {
with_capturing(|count, installed| {
let owner = Owner::new(None);
owner.with(|| {
let handle = create_element_by_name("list");
list_mount(
handle,
|| vec![10_i32, 20, 30],
|x| *x,
|_x| create_element_by_name("list-item"),
);
});
flush();
assert_eq!(*count.borrow(), Some(3));
assert!(installed.borrow().is_some());
owner.dispose();
});
}
#[test]
fn provider_component_at_index_returns_distinct_signs_and_runs_body() {
with_capturing(|_count, installed| {
let owner = Owner::new(None);
let body_calls = Rc::new(RefCell::new(Vec::<i32>::new()));
let bc = body_calls.clone();
owner.with(|| {
let handle = create_element_by_name("list");
list_mount(
handle,
|| vec![100_i32, 200, 300],
|x| *x,
move |x| {
bc.borrow_mut().push(x);
create_element_by_name("list-item")
},
);
});
flush();
let mut provider = installed.borrow_mut().take().expect("provider installed");
let s0 = (provider.component_at_index)(0, 0, false);
let s1 = (provider.component_at_index)(1, 0, false);
let s2 = (provider.component_at_index)(2, 0, false);
assert_ne!(s0, INVALID_ITEM_INDEX);
assert_ne!(s1, INVALID_ITEM_INDEX);
assert_ne!(s2, INVALID_ITEM_INDEX);
assert_ne!(s0, s1);
assert_ne!(s1, s2);
assert_eq!(*body_calls.borrow(), vec![100, 200, 300]);
owner.dispose();
});
}
#[test]
fn provider_out_of_range_index_returns_invalid() {
with_capturing(|_count, installed| {
let owner = Owner::new(None);
owner.with(|| {
let handle = create_element_by_name("list");
list_mount(
handle,
|| vec![1_i32, 2],
|x| *x,
|_x| create_element_by_name("list-item"),
);
});
flush();
let mut provider = installed.borrow_mut().take().expect("provider installed");
assert_eq!(
(provider.component_at_index)(5, 0, false),
INVALID_ITEM_INDEX
);
owner.dispose();
});
}
#[test]
fn enqueue_component_disposes_the_slot_owner() {
with_capturing(|_count, installed| {
let owner = Owner::new(None);
let drop_observed = Rc::new(RefCell::new(false));
owner.with(|| {
let handle = create_element_by_name("list");
let dr = drop_observed.clone();
list_mount(
handle,
|| vec![1_i32],
|x| *x,
move |_x| {
let dr = dr.clone();
crate::reactive::on_cleanup(move || {
*dr.borrow_mut() = true;
});
create_element_by_name("list-item")
},
);
});
flush();
let mut provider = installed.borrow_mut().take().expect("provider installed");
let sign = (provider.component_at_index)(0, 0, false);
assert_ne!(sign, INVALID_ITEM_INDEX);
assert!(!*drop_observed.borrow(), "before enqueue");
let enqueue = provider
.enqueue_component
.as_mut()
.expect("enqueue installed");
(enqueue)(sign);
assert!(
*drop_observed.borrow(),
"after enqueue, slot owner disposed"
);
owner.dispose();
});
}
#[test]
fn changing_items_rebroadcasts_count() {
with_capturing(|count, _installed| {
let owner = Owner::new(None);
let (items_read, items_write) = crate::reactive::signal(vec![1_i32, 2, 3]);
owner.with(|| {
let handle = create_element_by_name("list");
list_mount(
handle,
move || items_read.get(),
|x| *x,
|_x| create_element_by_name("list-item"),
);
});
flush();
assert_eq!(*count.borrow(), Some(3));
items_write.set(vec![1, 2, 3, 4, 5]);
flush();
assert_eq!(*count.borrow(), Some(5));
items_write.set(vec![]);
flush();
assert_eq!(*count.borrow(), Some(0));
owner.dispose();
});
}
#[test]
fn on_cleanup_disposes_remaining_slot_owners() {
with_capturing(|_count, installed| {
let outer = Owner::new(None);
let drops = Rc::new(RefCell::new(0_u32));
outer.with(|| {
let handle = create_element_by_name("list");
let drops = drops.clone();
list_mount(
handle,
|| vec![1_i32, 2, 3],
|x| *x,
move |_x| {
let d = drops.clone();
crate::reactive::on_cleanup(move || *d.borrow_mut() += 1);
create_element_by_name("list-item")
},
);
});
flush();
let mut provider = installed.borrow_mut().take().expect("provider installed");
let _ = (provider.component_at_index)(0, 0, false);
let _ = (provider.component_at_index)(1, 0, false);
assert_eq!(*drops.borrow(), 0);
drop(provider);
outer.dispose();
assert_eq!(
*drops.borrow(),
2,
"both live slots disposed on owner sweep"
);
});
}
}