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 children = self.state.borrow().children.clone();
318 dispatch_container_event(&mut children, event)
319 }
320
321 fn debug_name(&self) -> &'static str {
322 "ReactiveList"
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use crate::container::Container;
330 use crate::context::reset_layout_runtime;
331 use reactive_core::signal;
332
333 fn leaf() -> Result<Box<dyn LayoutItem>, LayoutError> {
334 Ok(Box::new(Container::new(
335 LayoutStyle::new().width(10.0).height(10.0),
336 vec![],
337 )?))
338 }
339
340 #[test]
342 fn builds_initial_items() {
343 reset_layout_runtime();
344 let items = signal(vec![1, 2, 3]);
345 let src = items.clone();
346 let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf(), 0.0).unwrap();
347 assert_eq!(list.state.borrow().children.len(), 3);
348 }
349
350 #[test]
352 fn reconcile_reuses_nodes_on_reorder_and_remove() {
353 reset_layout_runtime();
354 let items = signal(vec![1, 2, 3]);
355 let src = items.clone();
356 let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf(), 0.0).unwrap();
357 let v1: Vec<NodeId> = list
358 .state
359 .borrow()
360 .children
361 .iter()
362 .map(|c| c.node())
363 .collect();
364 assert_eq!(v1.len(), 3);
365
366 items.set(vec![3, 1]);
368
369 let st = list.state.borrow();
370 assert_eq!(st.children.len(), 2, "item 2 should be dropped");
371 let v2: Vec<NodeId> = st.children.iter().map(|c| c.node()).collect();
372 assert_eq!(v2[0], v1[2], "item 3 keeps its node, moved to front");
373 assert_eq!(v2[1], v1[0], "item 1 keeps its node");
374 }
375
376 #[test]
379 fn added_item_gets_laid_out_after_relayout() {
380 use crate::context::{compute_layout, relayout_if_dirty, track_layout};
381 use layout_core::AvailableSpace;
382
383 reset_layout_runtime();
384 let items = signal(vec![1i32, 2]);
385 let src = items.clone();
386 let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf(), 0.0).unwrap();
387 let list_node = list.layout_node();
388 compute_layout(
389 list_node,
390 AvailableSpace::Definite(200.0),
391 AvailableSpace::Definite(200.0),
392 )
393 .unwrap();
394 assert!(
395 track_layout(list.state.borrow().children[0].node())
396 .unwrap()
397 .get()
398 .height
399 > 0.0,
400 "initial items should be laid out"
401 );
402
403 items.set(vec![1, 2, 3]);
405 assert_eq!(list.state.borrow().children.len(), 3, "item added");
406
407 relayout_if_dirty();
409
410 let n2 = list.state.borrow().children[2].node();
411 assert!(
412 track_layout(n2).unwrap().get().height > 0.0,
413 "the newly added item must be laid out after relayout_if_dirty"
414 );
415 }
416
417 #[test]
419 fn reconcile_appends_new_item() {
420 reset_layout_runtime();
421 let items = signal(vec![1, 2]);
422 let src = items.clone();
423 let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf(), 0.0).unwrap();
424 let v1: Vec<NodeId> = list
425 .state
426 .borrow()
427 .children
428 .iter()
429 .map(|c| c.node())
430 .collect();
431
432 items.set(vec![1, 2, 3]);
433
434 let st = list.state.borrow();
435 assert_eq!(st.children.len(), 3);
436 let v2: Vec<NodeId> = st.children.iter().map(|c| c.node()).collect();
437 assert_eq!(&v2[..2], &v1[..], "existing items keep their nodes");
438 }
439
440 #[test]
443 fn with_gap_spaces_items_in_layout() {
444 use crate::context::compute_layout;
445 use layout_core::AvailableSpace;
446
447 reset_layout_runtime();
448 let items = signal(vec![1i32, 2]);
449 let src = items.clone();
450 let list = ReactiveList::new(move || src.get(), |n: &i32| *n, |_| leaf(), 8.0).unwrap();
451 let list_node = list.layout_node();
452 compute_layout(
453 list_node,
454 AvailableSpace::Definite(200.0),
455 AvailableSpace::Definite(200.0),
456 )
457 .unwrap();
458
459 let st = list.state.borrow();
460 let y0 = track_layout(st.children[0].node()).unwrap().get().y;
461 let y1 = track_layout(st.children[1].node()).unwrap().get().y;
462 assert_eq!(
463 y1 - y0,
464 18.0,
465 "each leaf is 10px tall; an 8px gap pushes the second item to 18px, not flush at 10px"
466 );
467 }
468
469 #[test]
472 fn positional_reuses_nodes_on_append() {
473 reset_layout_runtime();
474 let items = signal(vec![1, 2]);
475 let src = items.clone();
476 let list = ReactiveList::positional(move || src.get(), |_| leaf(), 0.0).unwrap();
477 let v1: Vec<NodeId> = list
478 .state
479 .borrow()
480 .children
481 .iter()
482 .map(|c| c.node())
483 .collect();
484
485 items.set(vec![1, 2, 3]);
486
487 let st = list.state.borrow();
488 assert_eq!(st.children.len(), 3);
489 let v2: Vec<NodeId> = st.children.iter().map(|c| c.node()).collect();
490 assert_eq!(
491 &v2[..2],
492 &v1[..],
493 "the first two positions keep their nodes"
494 );
495 }
496
497 #[test]
501 fn a_keyed_row_keeps_its_widget_and_still_sees_its_new_value() {
502 reset_layout_runtime();
503 reactive_core::reset_runtime();
504
505 #[derive(Clone)]
506 struct Row {
507 id: u32,
508 text: &'static str,
509 }
510
511 let rows = signal(vec![Row {
512 id: 1,
513 text: "before",
514 }]);
515 let seen = Rc::new(RefCell::new(Vec::<&'static str>::new()));
516 let builds = Rc::new(RefCell::new(0usize));
517
518 let (sink, counter) = (Rc::clone(&seen), Rc::clone(&builds));
519 let source = rows.clone();
520 let list = ReactiveList::keyed(
521 move || source.get(),
522 |row: &Row| row.id,
523 move |held: reactive_core::ReadSignal<Row>| {
524 *counter.borrow_mut() += 1;
525 let sink = Rc::clone(&sink);
526 let watch = effect(move || sink.borrow_mut().push(held.get().text));
528 Ok(Box::new(crate::Container::column(vec![])?.keeping(watch))
529 as Box<dyn LayoutItem>)
530 },
531 )
532 .unwrap();
533
534 let first = list.state.borrow().children[0].node();
535 rows.set(vec![Row {
536 id: 1,
537 text: "after",
538 }]);
539
540 assert_eq!(*builds.borrow(), 1, "the row was built once, not rebuilt");
541 assert_eq!(
542 list.state.borrow().children[0].node(),
543 first,
544 "and kept the very node it had"
545 );
546 assert_eq!(
547 *seen.borrow(),
548 vec!["before", "after"],
549 "while still seeing what it now says"
550 );
551 }
552
553 #[test]
558 fn a_child_may_remove_itself_from_inside_its_own_handler() {
559 use crate::context::compute_layout;
560 use crate::styled_container::StyledContainer;
561 use layout_core::AvailableSpace;
562 use platform_core::{Event, PointerButton, PointerSource};
563
564 reset_layout_runtime();
565 let items = signal(vec![1i32, 2, 3]);
566 let src = items.clone();
567 let pressed = items.clone();
568 let mut list = ReactiveList::new(
569 move || src.get(),
570 |n: &i32| *n,
571 move |n: i32| {
572 let items = pressed.clone();
573 Ok(Box::new(
574 StyledContainer::new(
575 LayoutStyle::new().width(50.0).height(20.0),
576 |_| Default::default(),
577 vec![],
578 )?
579 .on_press(move || {
580 items.set(items.peek().into_iter().filter(|x| *x != n).collect())
581 }),
582 ) as Box<dyn LayoutItem>)
583 },
584 0.0,
585 )
586 .unwrap();
587 compute_layout(
588 list.layout_node(),
589 AvailableSpace::Definite(200.0),
590 AvailableSpace::Definite(200.0),
591 )
592 .unwrap();
593
594 let at = |y: f64| (25.0, y);
596 let (x, y) = at(30.0);
597 list.on_event(&Event::PointerPressed {
598 x,
599 y,
600 button: PointerButton::Primary,
601 source: PointerSource::Mouse,
602 });
603 list.on_event(&Event::PointerReleased {
604 x,
605 y,
606 button: PointerButton::Primary,
607 source: PointerSource::Mouse,
608 });
609
610 assert_eq!(items.peek(), vec![1, 3]);
611 assert_eq!(list.state.borrow().children.len(), 2);
612 }
613}