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,
)
}
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,
{
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);
eff_version.update(|v| *v = v.wrapping_add(1));
});
Ok(Self {
node,
rect,
state,
version,
_effect,
})
}
}
fn reconcile<Item, KeyFn, B>(
state: &Rc<RefCell<ListState>>,
items: Vec<Item>,
keyer: &KeyFn,
build: &B,
) where
KeyFn: Fn(&Item, usize) -> u64,
B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError>,
{
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);
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"
);
}
}