1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::collections::hash_map::DefaultHasher;
4use std::hash::{Hash, Hasher};
5use std::rc::Rc;
6
7use geometry_core::Rect;
8use layout_core::{LayoutError, LayoutStyle, NodeId};
9use platform_core::Event;
10use reactive_core::{Effect, RwSignal, effect, signal};
11use ui_tree::{Component, EventResult, RenderNode};
12
13use crate::context::{new_container, remove_node, set_children, track_layout};
14use crate::layout_item::{Child, LayoutItem, TrackedChildren, make_child};
15use crate::pointer::dispatch_container_event;
16
17fn hash_key<K: Hash>(k: &K) -> u64 {
20 let mut h = DefaultHasher::new();
21 k.hash(&mut h);
22 h.finish()
23}
24
25struct ListState {
29 node: NodeId,
30 children: TrackedChildren,
31 keys: Vec<u64>,
32}
33
34pub struct ReactiveList {
40 node: NodeId,
41 rect: RwSignal<Rect>,
42 state: Rc<RefCell<ListState>>,
43 version: RwSignal<u64>,
45 _effect: Effect,
47}
48
49impl ReactiveList {
50 pub fn new<Item, Key, S, K, B>(source: S, key: K, build: B) -> Result<Self, LayoutError>
54 where
55 Key: Hash + 'static,
56 Item: 'static,
57 S: Fn() -> Vec<Item> + 'static,
58 K: Fn(&Item) -> Key + 'static,
59 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
60 {
61 Self::build(
62 LayoutStyle::new().flex_column(),
63 source,
64 build,
65 move |item: &Item, _idx: usize| hash_key(&key(item)),
66 )
67 }
68
69 pub fn with_gap<Item, Key, S, K, B>(
71 source: S,
72 key: K,
73 build: B,
74 gap: f32,
75 ) -> Result<Self, LayoutError>
76 where
77 Key: Hash + 'static,
78 Item: 'static,
79 S: Fn() -> Vec<Item> + 'static,
80 K: Fn(&Item) -> Key + 'static,
81 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
82 {
83 Self::build(
84 LayoutStyle::new().flex_column().gap(gap),
85 source,
86 build,
87 move |item: &Item, _idx: usize| hash_key(&key(item)),
88 )
89 }
90
91 pub fn with_style<Item, Key, S, K, B>(
95 container_style: LayoutStyle,
96 source: S,
97 key: K,
98 build: B,
99 ) -> Result<Self, LayoutError>
100 where
101 Key: Hash + 'static,
102 Item: 'static,
103 S: Fn() -> Vec<Item> + 'static,
104 K: Fn(&Item) -> Key + 'static,
105 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
106 {
107 Self::build(
108 container_style,
109 source,
110 build,
111 move |item: &Item, _idx: usize| hash_key(&key(item)),
112 )
113 }
114
115 pub fn positional<Item, S, B>(source: S, build: B) -> Result<Self, LayoutError>
120 where
121 Item: 'static,
122 S: Fn() -> Vec<Item> + 'static,
123 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
124 {
125 Self::build(
126 LayoutStyle::new().flex_column(),
127 source,
128 build,
129 |_item: &Item, idx: usize| idx as u64,
130 )
131 }
132
133 pub fn positional_with_gap<Item, S, B>(
135 source: S,
136 build: B,
137 gap: f32,
138 ) -> Result<Self, LayoutError>
139 where
140 Item: 'static,
141 S: Fn() -> Vec<Item> + 'static,
142 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
143 {
144 Self::build(
145 LayoutStyle::new().flex_column().gap(gap),
146 source,
147 build,
148 |_item: &Item, idx: usize| idx as u64,
149 )
150 }
151
152 pub fn keyed<Item, Key, S, K, B>(source: S, key: K, build: B) -> Result<Self, LayoutError>
163 where
164 Key: Hash + 'static,
165 Item: Clone + 'static,
166 S: Fn() -> Vec<Item> + 'static,
167 K: Fn(&Item) -> Key + 'static,
168 B: Fn(reactive_core::ReadSignal<Item>) -> Result<Box<dyn LayoutItem>, LayoutError>
169 + 'static,
170 {
171 let values: Rc<RefCell<HashMap<u64, RwSignal<Item>>>> =
175 Rc::new(RefCell::new(HashMap::new()));
176
177 let sync_values = Rc::clone(&values);
178 let sync = move |item: &Item, k: u64| {
179 let mut held = sync_values.borrow_mut();
180 match held.get(&k) {
181 Some(existing) => existing.set(item.clone()),
182 None => {
183 held.insert(k, signal(item.clone()));
184 }
185 }
186 };
187
188 let key = Rc::new(key);
189 let key_for_build = Rc::clone(&key);
190 let build_values = Rc::clone(&values);
191 Self::build_with_sync(
192 LayoutStyle::new().flex_column(),
193 source,
194 move |item: Item| {
195 let held = build_values
196 .borrow()
197 .get(&hash_key(&key_for_build(&item)))
198 .cloned()
199 .expect("sync inserts a handle for every item before build runs");
200 build(held.read_only())
201 },
202 move |item: &Item, _idx: usize| hash_key(&key(item)),
203 sync,
204 )
205 }
206
207 fn build<Item, S, B, KeyFn>(
210 container_style: LayoutStyle,
211 source: S,
212 build: B,
213 keyer: KeyFn,
214 ) -> Result<Self, LayoutError>
215 where
216 Item: 'static,
217 S: Fn() -> Vec<Item> + 'static,
218 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
219 KeyFn: Fn(&Item, usize) -> u64 + 'static,
220 {
221 Self::build_with_sync(container_style, source, build, keyer, |_, _| {})
222 }
223
224 fn build_with_sync<Item, S, B, KeyFn, Sync>(
225 container_style: LayoutStyle,
226 source: S,
227 build: B,
228 keyer: KeyFn,
229 sync: Sync,
230 ) -> Result<Self, LayoutError>
231 where
232 Item: 'static,
233 S: Fn() -> Vec<Item> + 'static,
234 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
235 KeyFn: Fn(&Item, usize) -> u64 + 'static,
236 Sync: Fn(&Item, u64) + 'static,
237 {
238 let node = new_container(container_style, &[])?;
239 let rect = track_layout(node).expect("list container is registered");
240 let state = Rc::new(RefCell::new(ListState {
241 node,
242 children: Vec::new(),
243 keys: Vec::new(),
244 }));
245 let version = signal(0u64);
246
247 let eff_state = Rc::clone(&state);
248 let eff_version = version.clone();
249 let _effect = effect(move || {
251 let items = source();
252 reconcile(&eff_state, items, &keyer, &build, &sync);
253 eff_version.update(|v| *v = v.wrapping_add(1));
254 });
255
256 Ok(Self {
257 node,
258 rect,
259 state,
260 version,
261 _effect,
262 })
263 }
264}
265
266fn reconcile<Item, KeyFn, B, Sync>(
267 state: &Rc<RefCell<ListState>>,
268 items: Vec<Item>,
269 keyer: &KeyFn,
270 build: &B,
271 sync: &Sync,
272) where
273 KeyFn: Fn(&Item, usize) -> u64,
274 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError>,
275 Sync: Fn(&Item, u64),
276{
277 let mut st = state.borrow_mut();
278 let container = st.node;
279
280 let old_keys = std::mem::take(&mut st.keys);
282 let old_children = std::mem::take(&mut st.children);
283 let mut old: HashMap<u64, Child> = HashMap::new();
284 for (k, child) in old_keys.into_iter().zip(old_children) {
285 old.entry(k).or_insert(child);
286 }
287
288 let mut children: TrackedChildren = Vec::with_capacity(items.len());
289 let mut keys: Vec<u64> = Vec::with_capacity(items.len());
290 let mut nodes: Vec<NodeId> = Vec::with_capacity(items.len());
291
292 for (idx, item) in items.into_iter().enumerate() {
293 let k = keyer(&item, idx);
294 sync(&item, k);
297 let child = match old.remove(&k) {
298 Some(existing) => existing,
299 None => make_child(build(item).expect("reactive list item build")),
300 };
301 nodes.push(child.node());
302 children.push(child);
303 keys.push(k);
304 }
305
306 st.children = children;
307 st.keys = keys;
308 drop(st);
309
310 let _ = set_children(container, &nodes);
313 for (_, child) in old {
314 remove_node(child.node());
315 }
316}
317
318impl LayoutItem for ReactiveList {
319 fn layout_node(&self) -> NodeId {
320 self.node
321 }
322}
323
324impl Component for ReactiveList {
325 fn view(&self) -> RenderNode {
326 self.version.get();
328 let _ = self.rect.get();
329 let st = self.state.borrow();
330 RenderNode::group(st.children.iter().map(|c| c.segment.boundary()))
331 }
332
333 fn on_event(&mut self, event: &Event) -> EventResult {
334 let mut st = self.state.borrow_mut();
335 dispatch_container_event(&mut st.children, event)
336 }
337
338 fn debug_name(&self) -> &'static str {
339 "ReactiveList"
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use crate::container::Container;
347 use crate::context::reset_layout_runtime;
348 use reactive_core::signal;
349
350 fn leaf() -> Result<Box<dyn LayoutItem>, LayoutError> {
351 Ok(Box::new(Container::new(
352 LayoutStyle::new().width(10.0).height(10.0),
353 vec![],
354 )?))
355 }
356
357 #[test]
359 fn builds_initial_items() {
360 reset_layout_runtime();
361 let items = signal(vec![1, 2, 3]);
362 let src = items.clone();
363 let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf()).unwrap();
364 assert_eq!(list.state.borrow().children.len(), 3);
365 }
366
367 #[test]
369 fn reconcile_reuses_nodes_on_reorder_and_remove() {
370 reset_layout_runtime();
371 let items = signal(vec![1, 2, 3]);
372 let src = items.clone();
373 let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf()).unwrap();
374 let v1: Vec<NodeId> = list
375 .state
376 .borrow()
377 .children
378 .iter()
379 .map(|c| c.node())
380 .collect();
381 assert_eq!(v1.len(), 3);
382
383 items.set(vec![3, 1]);
385
386 let st = list.state.borrow();
387 assert_eq!(st.children.len(), 2, "item 2 should be dropped");
388 let v2: Vec<NodeId> = st.children.iter().map(|c| c.node()).collect();
389 assert_eq!(v2[0], v1[2], "item 3 keeps its node, moved to front");
390 assert_eq!(v2[1], v1[0], "item 1 keeps its node");
391 }
392
393 #[test]
396 fn added_item_gets_laid_out_after_relayout() {
397 use crate::context::{compute_layout, relayout_if_dirty, track_layout};
398 use layout_core::AvailableSpace;
399
400 reset_layout_runtime();
401 let items = signal(vec![1i32, 2]);
402 let src = items.clone();
403 let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf()).unwrap();
404 let list_node = list.layout_node();
405 compute_layout(
406 list_node,
407 AvailableSpace::Definite(200.0),
408 AvailableSpace::Definite(200.0),
409 )
410 .unwrap();
411 assert!(
412 track_layout(list.state.borrow().children[0].node())
413 .unwrap()
414 .get()
415 .height
416 > 0.0,
417 "initial items should be laid out"
418 );
419
420 items.set(vec![1, 2, 3]);
422 assert_eq!(list.state.borrow().children.len(), 3, "item added");
423
424 relayout_if_dirty();
426
427 let n2 = list.state.borrow().children[2].node();
428 assert!(
429 track_layout(n2).unwrap().get().height > 0.0,
430 "the newly added item must be laid out after relayout_if_dirty"
431 );
432 }
433
434 #[test]
436 fn reconcile_appends_new_item() {
437 reset_layout_runtime();
438 let items = signal(vec![1, 2]);
439 let src = items.clone();
440 let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf()).unwrap();
441 let v1: Vec<NodeId> = list
442 .state
443 .borrow()
444 .children
445 .iter()
446 .map(|c| c.node())
447 .collect();
448
449 items.set(vec![1, 2, 3]);
450
451 let st = list.state.borrow();
452 assert_eq!(st.children.len(), 3);
453 let v2: Vec<NodeId> = st.children.iter().map(|c| c.node()).collect();
454 assert_eq!(&v2[..2], &v1[..], "existing items keep their nodes");
455 }
456
457 #[test]
460 fn with_gap_spaces_items_in_layout() {
461 use crate::context::compute_layout;
462 use layout_core::AvailableSpace;
463
464 reset_layout_runtime();
465 let items = signal(vec![1i32, 2]);
466 let src = items.clone();
467 let list =
468 ReactiveList::with_gap(move || src.get(), |n: &i32| *n, |_| leaf(), 8.0).unwrap();
469 let list_node = list.layout_node();
470 compute_layout(
471 list_node,
472 AvailableSpace::Definite(200.0),
473 AvailableSpace::Definite(200.0),
474 )
475 .unwrap();
476
477 let st = list.state.borrow();
478 let y0 = track_layout(st.children[0].node()).unwrap().get().y;
479 let y1 = track_layout(st.children[1].node()).unwrap().get().y;
480 assert_eq!(
481 y1 - y0,
482 18.0,
483 "each leaf is 10px tall; an 8px gap pushes the second item to 18px, not flush at 10px"
484 );
485 }
486
487 #[test]
490 fn positional_reuses_nodes_on_append() {
491 reset_layout_runtime();
492 let items = signal(vec![1, 2]);
493 let src = items.clone();
494 let list = ReactiveList::positional(move || src.get(), |_| leaf()).unwrap();
495 let v1: Vec<NodeId> = list
496 .state
497 .borrow()
498 .children
499 .iter()
500 .map(|c| c.node())
501 .collect();
502
503 items.set(vec![1, 2, 3]);
504
505 let st = list.state.borrow();
506 assert_eq!(st.children.len(), 3);
507 let v2: Vec<NodeId> = st.children.iter().map(|c| c.node()).collect();
508 assert_eq!(
509 &v2[..2],
510 &v1[..],
511 "the first two positions keep their nodes"
512 );
513 }
514
515 #[test]
519 fn a_keyed_row_keeps_its_widget_and_still_sees_its_new_value() {
520 reset_layout_runtime();
521 reactive_core::reset_runtime();
522
523 #[derive(Clone)]
524 struct Row {
525 id: u32,
526 text: &'static str,
527 }
528
529 let rows = signal(vec![Row {
530 id: 1,
531 text: "before",
532 }]);
533 let seen = Rc::new(RefCell::new(Vec::<&'static str>::new()));
534 let builds = Rc::new(RefCell::new(0usize));
535
536 let (sink, counter) = (Rc::clone(&seen), Rc::clone(&builds));
537 let source = rows.clone();
538 let list = ReactiveList::keyed(
539 move || source.get(),
540 |row: &Row| row.id,
541 move |held: reactive_core::ReadSignal<Row>| {
542 *counter.borrow_mut() += 1;
543 let sink = Rc::clone(&sink);
544 let watch = effect(move || sink.borrow_mut().push(held.get().text));
546 Ok(Box::new(crate::Container::column(vec![])?.keeping(watch))
547 as Box<dyn LayoutItem>)
548 },
549 )
550 .unwrap();
551
552 let first = list.state.borrow().children[0].node();
553 rows.set(vec![Row {
554 id: 1,
555 text: "after",
556 }]);
557
558 assert_eq!(*builds.borrow(), 1, "the row was built once, not rebuilt");
559 assert_eq!(
560 list.state.borrow().children[0].node(),
561 first,
562 "and kept the very node it had"
563 );
564 assert_eq!(
565 *seen.borrow(),
566 vec!["before", "after"],
567 "while still seeing what it now says"
568 );
569 }
570}