use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::rc::Rc;
use geometry_core::Rect;
use layout_core::{LayoutError, LayoutStyle, NodeId};
use platform_core::Event;
use reactive_core::{Effect, RwSignal, effect, signal};
use ui_tree::{Component, EventResult, RenderNode};
use crate::context::{new_container, remove_node, set_children, track_layout};
use crate::layout_item::{Child, LayoutItem, TrackedChildren, make_child};
use crate::pointer::dispatch_container_event;
fn hash_key<K: Hash>(k: &K) -> u64 {
let mut h = DefaultHasher::new();
k.hash(&mut h);
h.finish()
}
struct ListState {
node: NodeId,
children: TrackedChildren,
keys: Vec<u64>,
}
pub struct ReactiveList {
node: NodeId,
rect: RwSignal<Rect>,
state: Rc<RefCell<ListState>>,
version: RwSignal<u64>,
_effect: Effect,
}
impl ReactiveList {
pub fn new<Item, Key, S, K, B>(source: S, key: K, build: B) -> Result<Self, LayoutError>
where
Key: Hash + 'static,
Item: 'static,
S: Fn() -> Vec<Item> + 'static,
K: Fn(&Item) -> Key + 'static,
B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
{
Self::build(
LayoutStyle::new().flex_column(),
source,
build,
move |item: &Item, _idx: usize| hash_key(&key(item)),
)
}
pub fn with_gap<Item, Key, S, K, B>(
source: S,
key: K,
build: B,
gap: f32,
) -> Result<Self, LayoutError>
where
Key: Hash + 'static,
Item: 'static,
S: Fn() -> Vec<Item> + 'static,
K: Fn(&Item) -> Key + 'static,
B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
{
Self::build(
LayoutStyle::new().flex_column().gap(gap),
source,
build,
move |item: &Item, _idx: usize| hash_key(&key(item)),
)
}
pub fn with_style<Item, Key, S, K, B>(
container_style: LayoutStyle,
source: S,
key: K,
build: B,
) -> Result<Self, LayoutError>
where
Key: Hash + 'static,
Item: 'static,
S: Fn() -> Vec<Item> + 'static,
K: Fn(&Item) -> Key + 'static,
B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
{
Self::build(
container_style,
source,
build,
move |item: &Item, _idx: usize| hash_key(&key(item)),
)
}
pub fn positional<Item, S, B>(source: S, build: B) -> Result<Self, LayoutError>
where
Item: 'static,
S: Fn() -> Vec<Item> + 'static,
B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
{
Self::build(
LayoutStyle::new().flex_column(),
source,
build,
|_item: &Item, idx: usize| idx as u64,
)
}
pub fn positional_with_gap<Item, S, B>(
source: S,
build: B,
gap: f32,
) -> Result<Self, LayoutError>
where
Item: 'static,
S: Fn() -> Vec<Item> + 'static,
B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
{
Self::build(
LayoutStyle::new().flex_column().gap(gap),
source,
build,
|_item: &Item, idx: usize| idx as u64,
)
}
pub fn keyed<Item, Key, S, K, B>(source: S, key: K, build: B) -> Result<Self, LayoutError>
where
Key: Hash + 'static,
Item: Clone + 'static,
S: Fn() -> Vec<Item> + 'static,
K: Fn(&Item) -> Key + 'static,
B: Fn(reactive_core::ReadSignal<Item>) -> Result<Box<dyn LayoutItem>, LayoutError>
+ 'static,
{
let values: Rc<RefCell<HashMap<u64, RwSignal<Item>>>> =
Rc::new(RefCell::new(HashMap::new()));
let sync_values = Rc::clone(&values);
let sync = move |item: &Item, k: u64| {
let mut held = sync_values.borrow_mut();
match held.get(&k) {
Some(existing) => existing.set(item.clone()),
None => {
held.insert(k, signal(item.clone()));
}
}
};
let key = Rc::new(key);
let key_for_build = Rc::clone(&key);
let build_values = Rc::clone(&values);
Self::build_with_sync(
LayoutStyle::new().flex_column(),
source,
move |item: Item| {
let held = build_values
.borrow()
.get(&hash_key(&key_for_build(&item)))
.cloned()
.expect("sync inserts a handle for every item before build runs");
build(held.read_only())
},
move |item: &Item, _idx: usize| hash_key(&key(item)),
sync,
)
}
fn build<Item, S, B, KeyFn>(
container_style: LayoutStyle,
source: S,
build: B,
keyer: KeyFn,
) -> Result<Self, LayoutError>
where
Item: 'static,
S: Fn() -> Vec<Item> + 'static,
B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
KeyFn: Fn(&Item, usize) -> u64 + 'static,
{
Self::build_with_sync(container_style, source, build, keyer, |_, _| {})
}
fn build_with_sync<Item, S, B, KeyFn, Sync>(
container_style: LayoutStyle,
source: S,
build: B,
keyer: KeyFn,
sync: Sync,
) -> Result<Self, LayoutError>
where
Item: 'static,
S: Fn() -> Vec<Item> + 'static,
B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
KeyFn: Fn(&Item, usize) -> u64 + 'static,
Sync: Fn(&Item, u64) + 'static,
{
let node = new_container(container_style, &[])?;
let rect = track_layout(node).expect("list container is registered");
let state = Rc::new(RefCell::new(ListState {
node,
children: Vec::new(),
keys: Vec::new(),
}));
let version = signal(0u64);
let eff_state = Rc::clone(&state);
let eff_version = version.clone();
let _effect = effect(move || {
let items = source();
reconcile(&eff_state, items, &keyer, &build, &sync);
eff_version.update(|v| *v = v.wrapping_add(1));
});
Ok(Self {
node,
rect,
state,
version,
_effect,
})
}
}
fn reconcile<Item, KeyFn, B, Sync>(
state: &Rc<RefCell<ListState>>,
items: Vec<Item>,
keyer: &KeyFn,
build: &B,
sync: &Sync,
) where
KeyFn: Fn(&Item, usize) -> u64,
B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError>,
Sync: Fn(&Item, u64),
{
let mut st = state.borrow_mut();
let container = st.node;
let old_keys = std::mem::take(&mut st.keys);
let old_children = std::mem::take(&mut st.children);
let mut old: HashMap<u64, Child> = HashMap::new();
for (k, child) in old_keys.into_iter().zip(old_children) {
old.entry(k).or_insert(child);
}
let mut children: TrackedChildren = Vec::with_capacity(items.len());
let mut keys: Vec<u64> = Vec::with_capacity(items.len());
let mut nodes: Vec<NodeId> = Vec::with_capacity(items.len());
for (idx, item) in items.into_iter().enumerate() {
let k = keyer(&item, idx);
sync(&item, k);
let child = match old.remove(&k) {
Some(existing) => existing,
None => make_child(build(item).expect("reactive list item build")),
};
nodes.push(child.node());
children.push(child);
keys.push(k);
}
st.children = children;
st.keys = keys;
drop(st);
let _ = set_children(container, &nodes);
for (_, child) in old {
remove_node(child.node());
}
}
impl LayoutItem for ReactiveList {
fn layout_node(&self) -> NodeId {
self.node
}
}
impl Component for ReactiveList {
fn view(&self) -> RenderNode {
self.version.get();
let _ = self.rect.get();
let st = self.state.borrow();
RenderNode::group(st.children.iter().map(|c| c.segment.boundary()))
}
fn on_event(&mut self, event: &Event) -> EventResult {
let mut st = self.state.borrow_mut();
dispatch_container_event(&mut st.children, event)
}
fn debug_name(&self) -> &'static str {
"ReactiveList"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::container::Container;
use crate::context::reset_layout_runtime;
use reactive_core::signal;
fn leaf() -> Result<Box<dyn LayoutItem>, LayoutError> {
Ok(Box::new(Container::new(
LayoutStyle::new().width(10.0).height(10.0),
vec![],
)?))
}
#[test]
fn builds_initial_items() {
reset_layout_runtime();
let items = signal(vec![1, 2, 3]);
let src = items.clone();
let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf()).unwrap();
assert_eq!(list.state.borrow().children.len(), 3);
}
#[test]
fn reconcile_reuses_nodes_on_reorder_and_remove() {
reset_layout_runtime();
let items = signal(vec![1, 2, 3]);
let src = items.clone();
let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf()).unwrap();
let v1: Vec<NodeId> = list
.state
.borrow()
.children
.iter()
.map(|c| c.node())
.collect();
assert_eq!(v1.len(), 3);
items.set(vec![3, 1]);
let st = list.state.borrow();
assert_eq!(st.children.len(), 2, "item 2 should be dropped");
let v2: Vec<NodeId> = st.children.iter().map(|c| c.node()).collect();
assert_eq!(v2[0], v1[2], "item 3 keeps its node, moved to front");
assert_eq!(v2[1], v1[0], "item 1 keeps its node");
}
#[test]
fn added_item_gets_laid_out_after_relayout() {
use crate::context::{compute_layout, relayout_if_dirty, track_layout};
use layout_core::AvailableSpace;
reset_layout_runtime();
let items = signal(vec![1i32, 2]);
let src = items.clone();
let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf()).unwrap();
let list_node = list.layout_node();
compute_layout(
list_node,
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(200.0),
)
.unwrap();
assert!(
track_layout(list.state.borrow().children[0].node())
.unwrap()
.get()
.height
> 0.0,
"initial items should be laid out"
);
items.set(vec![1, 2, 3]);
assert_eq!(list.state.borrow().children.len(), 3, "item added");
relayout_if_dirty();
let n2 = list.state.borrow().children[2].node();
assert!(
track_layout(n2).unwrap().get().height > 0.0,
"the newly added item must be laid out after relayout_if_dirty"
);
}
#[test]
fn reconcile_appends_new_item() {
reset_layout_runtime();
let items = signal(vec![1, 2]);
let src = items.clone();
let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf()).unwrap();
let v1: Vec<NodeId> = list
.state
.borrow()
.children
.iter()
.map(|c| c.node())
.collect();
items.set(vec![1, 2, 3]);
let st = list.state.borrow();
assert_eq!(st.children.len(), 3);
let v2: Vec<NodeId> = st.children.iter().map(|c| c.node()).collect();
assert_eq!(&v2[..2], &v1[..], "existing items keep their nodes");
}
#[test]
fn with_gap_spaces_items_in_layout() {
use crate::context::compute_layout;
use layout_core::AvailableSpace;
reset_layout_runtime();
let items = signal(vec![1i32, 2]);
let src = items.clone();
let list =
ReactiveList::with_gap(move || src.get(), |n: &i32| *n, |_| leaf(), 8.0).unwrap();
let list_node = list.layout_node();
compute_layout(
list_node,
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(200.0),
)
.unwrap();
let st = list.state.borrow();
let y0 = track_layout(st.children[0].node()).unwrap().get().y;
let y1 = track_layout(st.children[1].node()).unwrap().get().y;
assert_eq!(
y1 - y0,
18.0,
"each leaf is 10px tall; an 8px gap pushes the second item to 18px, not flush at 10px"
);
}
#[test]
fn positional_reuses_nodes_on_append() {
reset_layout_runtime();
let items = signal(vec![1, 2]);
let src = items.clone();
let list = ReactiveList::positional(move || src.get(), |_| leaf()).unwrap();
let v1: Vec<NodeId> = list
.state
.borrow()
.children
.iter()
.map(|c| c.node())
.collect();
items.set(vec![1, 2, 3]);
let st = list.state.borrow();
assert_eq!(st.children.len(), 3);
let v2: Vec<NodeId> = st.children.iter().map(|c| c.node()).collect();
assert_eq!(
&v2[..2],
&v1[..],
"the first two positions keep their nodes"
);
}
#[test]
fn a_keyed_row_keeps_its_widget_and_still_sees_its_new_value() {
reset_layout_runtime();
reactive_core::reset_runtime();
#[derive(Clone)]
struct Row {
id: u32,
text: &'static str,
}
let rows = signal(vec![Row {
id: 1,
text: "before",
}]);
let seen = Rc::new(RefCell::new(Vec::<&'static str>::new()));
let builds = Rc::new(RefCell::new(0usize));
let (sink, counter) = (Rc::clone(&seen), Rc::clone(&builds));
let source = rows.clone();
let list = ReactiveList::keyed(
move || source.get(),
|row: &Row| row.id,
move |held: reactive_core::ReadSignal<Row>| {
*counter.borrow_mut() += 1;
let sink = Rc::clone(&sink);
let watch = effect(move || sink.borrow_mut().push(held.get().text));
Ok(Box::new(crate::Container::column(vec![])?.keeping(watch))
as Box<dyn LayoutItem>)
},
)
.unwrap();
let first = list.state.borrow().children[0].node();
rows.set(vec![Row {
id: 1,
text: "after",
}]);
assert_eq!(*builds.borrow(), 1, "the row was built once, not rebuilt");
assert_eq!(
list.state.borrow().children[0].node(),
first,
"and kept the very node it had"
);
assert_eq!(
*seen.borrow(),
vec!["before", "after"],
"while still seeing what it now says"
);
}
}