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 as_row(self) -> Self {
57 crate::context::set_container_row(self.node);
58 self
59 }
60
61 pub fn new<Item, Key, S, K, B>(
65 source: S,
66 key: K,
67 build: B,
68 gap: f32,
69 ) -> Result<Self, LayoutError>
70 where
71 Key: Hash + 'static,
72 Item: 'static,
73 S: Fn() -> Vec<Item> + 'static,
74 K: Fn(&Item) -> Key + 'static,
75 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
76 {
77 Self::build(
78 LayoutStyle::new().flex_column().gap(gap),
79 source,
80 build,
81 move |item: &Item, _idx: usize| hash_key(&key(item)),
82 )
83 }
84
85 pub fn with_style<Item, Key, S, K, B>(
89 container_style: LayoutStyle,
90 source: S,
91 key: K,
92 build: B,
93 ) -> Result<Self, LayoutError>
94 where
95 Key: Hash + 'static,
96 Item: 'static,
97 S: Fn() -> Vec<Item> + 'static,
98 K: Fn(&Item) -> Key + 'static,
99 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
100 {
101 Self::build(
102 container_style,
103 source,
104 build,
105 move |item: &Item, _idx: usize| hash_key(&key(item)),
106 )
107 }
108
109 pub fn positional<Item, S, B>(source: S, build: B, gap: f32) -> Result<Self, LayoutError>
114 where
115 Item: 'static,
116 S: Fn() -> Vec<Item> + 'static,
117 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
118 {
119 Self::build(
120 LayoutStyle::new().flex_column().gap(gap),
121 source,
122 build,
123 |_item: &Item, idx: usize| idx as u64,
124 )
125 }
126
127 pub fn keyed<Item, Key, S, K, B>(source: S, key: K, build: B) -> Result<Self, LayoutError>
138 where
139 Key: Hash + 'static,
140 Item: Clone + 'static,
141 S: Fn() -> Vec<Item> + 'static,
142 K: Fn(&Item) -> Key + 'static,
143 B: Fn(reactive_core::ReadSignal<Item>) -> Result<Box<dyn LayoutItem>, LayoutError>
144 + 'static,
145 {
146 let values: Rc<RefCell<HashMap<u64, RwSignal<Item>>>> =
150 Rc::new(RefCell::new(HashMap::new()));
151
152 let sync_values = Rc::clone(&values);
153 let sync = move |item: &Item, k: u64| {
154 let mut held = sync_values.borrow_mut();
155 match held.get(&k) {
156 Some(existing) => existing.set(item.clone()),
157 None => {
158 held.insert(k, signal(item.clone()));
159 }
160 }
161 };
162
163 let key = Rc::new(key);
164 let key_for_build = Rc::clone(&key);
165 let build_values = Rc::clone(&values);
166 Self::build_with_sync(
167 LayoutStyle::new().flex_column(),
168 source,
169 move |item: Item| {
170 let held = build_values
171 .borrow()
172 .get(&hash_key(&key_for_build(&item)))
173 .cloned()
174 .expect("sync inserts a handle for every item before build runs");
175 build(held.read_only())
176 },
177 move |item: &Item, _idx: usize| hash_key(&key(item)),
178 sync,
179 )
180 }
181
182 fn build<Item, S, B, KeyFn>(
185 container_style: LayoutStyle,
186 source: S,
187 build: B,
188 keyer: KeyFn,
189 ) -> Result<Self, LayoutError>
190 where
191 Item: 'static,
192 S: Fn() -> Vec<Item> + 'static,
193 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
194 KeyFn: Fn(&Item, usize) -> u64 + 'static,
195 {
196 Self::build_with_sync(container_style, source, build, keyer, |_, _| {})
197 }
198
199 fn build_with_sync<Item, S, B, KeyFn, Sync>(
200 container_style: LayoutStyle,
201 source: S,
202 build: B,
203 keyer: KeyFn,
204 sync: Sync,
205 ) -> Result<Self, LayoutError>
206 where
207 Item: 'static,
208 S: Fn() -> Vec<Item> + 'static,
209 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
210 KeyFn: Fn(&Item, usize) -> u64 + 'static,
211 Sync: Fn(&Item, u64) + 'static,
212 {
213 let node = new_container(container_style, &[])?;
214 let rect = track_layout(node).expect("list container is registered");
215 let state = Rc::new(RefCell::new(ListState {
216 node,
217 children: Vec::new(),
218 keys: Vec::new(),
219 }));
220 let version = signal(0u64);
221
222 let eff_state = Rc::clone(&state);
223 let eff_version = version.clone();
224 let _effect = effect(move || {
226 let items = source();
227 reconcile(&eff_state, items, &keyer, &build, &sync);
228 eff_version.update(|v| *v = v.wrapping_add(1));
229 });
230
231 Ok(Self {
232 node,
233 rect,
234 state,
235 version,
236 _effect,
237 })
238 }
239}
240
241fn reconcile<Item, KeyFn, B, Sync>(
242 state: &Rc<RefCell<ListState>>,
243 items: Vec<Item>,
244 keyer: &KeyFn,
245 build: &B,
246 sync: &Sync,
247) where
248 KeyFn: Fn(&Item, usize) -> u64,
249 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError>,
250 Sync: Fn(&Item, u64),
251{
252 let mut st = state.borrow_mut();
253 let container = st.node;
254
255 let old_keys = std::mem::take(&mut st.keys);
257 let old_children = std::mem::take(&mut st.children);
258 let mut old: HashMap<u64, Child> = HashMap::new();
259 for (k, child) in old_keys.into_iter().zip(old_children) {
260 old.entry(k).or_insert(child);
261 }
262
263 let mut children: TrackedChildren = Vec::with_capacity(items.len());
264 let mut keys: Vec<u64> = Vec::with_capacity(items.len());
265 let mut nodes: Vec<NodeId> = Vec::with_capacity(items.len());
266
267 for (idx, item) in items.into_iter().enumerate() {
268 let k = keyer(&item, idx);
269 sync(&item, k);
272 let child = match old.remove(&k) {
273 Some(existing) => existing,
274 None => make_child(build(item).expect("reactive list item build")),
275 };
276 nodes.push(child.node());
277 children.push(child);
278 keys.push(k);
279 }
280
281 st.children = children;
282 st.keys = keys;
283 drop(st);
284
285 let _ = set_children(container, &nodes);
288 for (_, child) in old {
289 remove_node(child.node());
290 }
291}
292
293impl LayoutItem for ReactiveList {
294 fn layout_node(&self) -> NodeId {
295 self.node
296 }
297}
298
299impl Component for ReactiveList {
300 fn view(&self) -> RenderNode {
301 self.version.get();
303 let _ = self.rect.get();
304 let st = self.state.borrow();
305 RenderNode::group(st.children.iter().map(|c| c.segment.boundary()))
306 }
307
308 fn on_event(&mut self, event: &Event) -> EventResult {
309 let mut st = self.state.borrow_mut();
310 dispatch_container_event(&mut st.children, event)
311 }
312
313 fn debug_name(&self) -> &'static str {
314 "ReactiveList"
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 use crate::container::Container;
322 use crate::context::reset_layout_runtime;
323 use reactive_core::signal;
324
325 fn leaf() -> Result<Box<dyn LayoutItem>, LayoutError> {
326 Ok(Box::new(Container::new(
327 LayoutStyle::new().width(10.0).height(10.0),
328 vec![],
329 )?))
330 }
331
332 #[test]
334 fn builds_initial_items() {
335 reset_layout_runtime();
336 let items = signal(vec![1, 2, 3]);
337 let src = items.clone();
338 let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf(), 0.0).unwrap();
339 assert_eq!(list.state.borrow().children.len(), 3);
340 }
341
342 #[test]
344 fn reconcile_reuses_nodes_on_reorder_and_remove() {
345 reset_layout_runtime();
346 let items = signal(vec![1, 2, 3]);
347 let src = items.clone();
348 let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf(), 0.0).unwrap();
349 let v1: Vec<NodeId> = list
350 .state
351 .borrow()
352 .children
353 .iter()
354 .map(|c| c.node())
355 .collect();
356 assert_eq!(v1.len(), 3);
357
358 items.set(vec![3, 1]);
360
361 let st = list.state.borrow();
362 assert_eq!(st.children.len(), 2, "item 2 should be dropped");
363 let v2: Vec<NodeId> = st.children.iter().map(|c| c.node()).collect();
364 assert_eq!(v2[0], v1[2], "item 3 keeps its node, moved to front");
365 assert_eq!(v2[1], v1[0], "item 1 keeps its node");
366 }
367
368 #[test]
371 fn added_item_gets_laid_out_after_relayout() {
372 use crate::context::{compute_layout, relayout_if_dirty, track_layout};
373 use layout_core::AvailableSpace;
374
375 reset_layout_runtime();
376 let items = signal(vec![1i32, 2]);
377 let src = items.clone();
378 let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf(), 0.0).unwrap();
379 let list_node = list.layout_node();
380 compute_layout(
381 list_node,
382 AvailableSpace::Definite(200.0),
383 AvailableSpace::Definite(200.0),
384 )
385 .unwrap();
386 assert!(
387 track_layout(list.state.borrow().children[0].node())
388 .unwrap()
389 .get()
390 .height
391 > 0.0,
392 "initial items should be laid out"
393 );
394
395 items.set(vec![1, 2, 3]);
397 assert_eq!(list.state.borrow().children.len(), 3, "item added");
398
399 relayout_if_dirty();
401
402 let n2 = list.state.borrow().children[2].node();
403 assert!(
404 track_layout(n2).unwrap().get().height > 0.0,
405 "the newly added item must be laid out after relayout_if_dirty"
406 );
407 }
408
409 #[test]
411 fn reconcile_appends_new_item() {
412 reset_layout_runtime();
413 let items = signal(vec![1, 2]);
414 let src = items.clone();
415 let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf(), 0.0).unwrap();
416 let v1: Vec<NodeId> = list
417 .state
418 .borrow()
419 .children
420 .iter()
421 .map(|c| c.node())
422 .collect();
423
424 items.set(vec![1, 2, 3]);
425
426 let st = list.state.borrow();
427 assert_eq!(st.children.len(), 3);
428 let v2: Vec<NodeId> = st.children.iter().map(|c| c.node()).collect();
429 assert_eq!(&v2[..2], &v1[..], "existing items keep their nodes");
430 }
431
432 #[test]
435 fn with_gap_spaces_items_in_layout() {
436 use crate::context::compute_layout;
437 use layout_core::AvailableSpace;
438
439 reset_layout_runtime();
440 let items = signal(vec![1i32, 2]);
441 let src = items.clone();
442 let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf(), 8.0).unwrap();
443 let list_node = list.layout_node();
444 compute_layout(
445 list_node,
446 AvailableSpace::Definite(200.0),
447 AvailableSpace::Definite(200.0),
448 )
449 .unwrap();
450
451 let st = list.state.borrow();
452 let y0 = track_layout(st.children[0].node()).unwrap().get().y;
453 let y1 = track_layout(st.children[1].node()).unwrap().get().y;
454 assert_eq!(
455 y1 - y0,
456 18.0,
457 "each leaf is 10px tall; an 8px gap pushes the second item to 18px, not flush at 10px"
458 );
459 }
460
461 #[test]
464 fn positional_reuses_nodes_on_append() {
465 reset_layout_runtime();
466 let items = signal(vec![1, 2]);
467 let src = items.clone();
468 let list = ReactiveList::positional(move || src.get(), |_| leaf(), 0.0).unwrap();
469 let v1: Vec<NodeId> = list
470 .state
471 .borrow()
472 .children
473 .iter()
474 .map(|c| c.node())
475 .collect();
476
477 items.set(vec![1, 2, 3]);
478
479 let st = list.state.borrow();
480 assert_eq!(st.children.len(), 3);
481 let v2: Vec<NodeId> = st.children.iter().map(|c| c.node()).collect();
482 assert_eq!(
483 &v2[..2],
484 &v1[..],
485 "the first two positions keep their nodes"
486 );
487 }
488
489 #[test]
493 fn a_keyed_row_keeps_its_widget_and_still_sees_its_new_value() {
494 reset_layout_runtime();
495 reactive_core::reset_runtime();
496
497 #[derive(Clone)]
498 struct Row {
499 id: u32,
500 text: &'static str,
501 }
502
503 let rows = signal(vec![Row {
504 id: 1,
505 text: "before",
506 }]);
507 let seen = Rc::new(RefCell::new(Vec::<&'static str>::new()));
508 let builds = Rc::new(RefCell::new(0usize));
509
510 let (sink, counter) = (Rc::clone(&seen), Rc::clone(&builds));
511 let source = rows.clone();
512 let list = ReactiveList::keyed(
513 move || source.get(),
514 |row: &Row| row.id,
515 move |held: reactive_core::ReadSignal<Row>| {
516 *counter.borrow_mut() += 1;
517 let sink = Rc::clone(&sink);
518 let watch = effect(move || sink.borrow_mut().push(held.get().text));
520 Ok(Box::new(crate::Container::column(vec![])?.keeping(watch))
521 as Box<dyn LayoutItem>)
522 },
523 )
524 .unwrap();
525
526 let first = list.state.borrow().children[0].node();
527 rows.set(vec![Row {
528 id: 1,
529 text: "after",
530 }]);
531
532 assert_eq!(*builds.borrow(), 1, "the row was built once, not rebuilt");
533 assert_eq!(
534 list.state.borrow().children[0].node(),
535 first,
536 "and kept the very node it had"
537 );
538 assert_eq!(
539 *seen.borrow(),
540 vec!["before", "after"],
541 "while still seeing what it now says"
542 );
543 }
544}