1use std::cell::RefCell;
15use std::collections::HashMap;
16use std::collections::hash_map::DefaultHasher;
17use std::hash::{Hash, Hasher};
18use std::rc::Rc;
19
20use layout_core::{LayoutError, NodeId};
21use platform_core::Event;
22use reactive_core::{Effect, RwSignal, effect, signal};
23use ui_tree::{EventResult, RenderNode};
24
25use crate::context::{container_is_row, remove_node, set_children, set_leading_margin};
26use crate::layout_item::{Child, LayoutItem, make_child};
27use crate::pointer::dispatch_container_event;
28
29fn hash_key<K: Hash>(k: &K) -> u64 {
30 let mut h = DefaultHasher::new();
31 k.hash(&mut h);
32 h.finish()
33}
34
35pub enum ChildSlot {
38 Static(Box<dyn LayoutItem>),
39 Dynamic(FragmentSpec),
40}
41
42impl ChildSlot {
43 pub fn stat(item: Box<dyn LayoutItem>) -> Self {
46 ChildSlot::Static(item)
47 }
48}
49
50pub struct FragmentSpec {
55 install: Box<dyn FnOnce(Rc<RefCell<HostState>>, usize) -> Effect>,
56 gap: f32,
57}
58
59pub fn fragment<Item, Key, S, K, B>(source: S, key: K, build: B, gap: f32) -> ChildSlot
64where
65 Key: Hash + 'static,
66 Item: 'static,
67 S: Fn() -> Vec<Item> + 'static,
68 K: Fn(&Item) -> Key + 'static,
69 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
70{
71 make_fragment(
72 source,
73 move |item: &Item, _idx: usize| hash_key(&key(item)),
74 build,
75 gap,
76 )
77}
78
79pub fn fragment_positional<Item, S, B>(source: S, build: B, gap: f32) -> ChildSlot
81where
82 Item: 'static,
83 S: Fn() -> Vec<Item> + 'static,
84 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
85{
86 make_fragment(source, |_item: &Item, idx: usize| idx as u64, build, gap)
87}
88
89fn make_fragment<Item, S, B, KeyFn>(source: S, keyer: KeyFn, build: B, gap: f32) -> ChildSlot
90where
91 Item: 'static,
92 S: Fn() -> Vec<Item> + 'static,
93 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
94 KeyFn: Fn(&Item, usize) -> u64 + 'static,
95{
96 let install = Box::new(
97 move |state: Rc<RefCell<HostState>>, index: usize| -> Effect {
98 effect(move || {
100 let items = source();
101 reconcile_slot(&state, index, items, &keyer, &build);
102 })
103 },
104 );
105 ChildSlot::Dynamic(FragmentSpec { install, gap })
106}
107
108#[derive(Default)]
112struct DynState {
113 items: Vec<Child>,
114 keys: Vec<u64>,
115 gap: f32,
116 is_row: bool,
117}
118
119enum SlotState {
120 Static(Child),
121 Dynamic(DynState),
122}
123
124struct HostState {
128 node: NodeId,
129 slots: Vec<SlotState>,
130 version: RwSignal<u64>,
131}
132
133fn flatten_nodes(slots: &[SlotState]) -> Vec<NodeId> {
134 let mut nodes = Vec::new();
135 for slot in slots {
136 match slot {
137 SlotState::Static(child) => nodes.push(child.node()),
138 SlotState::Dynamic(dyn_state) => nodes.extend(dyn_state.items.iter().map(Child::node)),
139 }
140 }
141 nodes
142}
143
144fn collect_children(slots: &[SlotState]) -> Vec<Child> {
145 let mut out = Vec::new();
146 for slot in slots {
147 match slot {
148 SlotState::Static(child) => out.push(child.clone()),
149 SlotState::Dynamic(dyn_state) => out.extend(dyn_state.items.iter().cloned()),
150 }
151 }
152 out
153}
154
155fn reconcile_slot<Item, KeyFn, B>(
156 state: &Rc<RefCell<HostState>>,
157 index: usize,
158 items: Vec<Item>,
159 keyer: &KeyFn,
160 build: &B,
161) where
162 KeyFn: Fn(&Item, usize) -> u64,
163 B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError>,
164{
165 let mut st = state.borrow_mut();
166
167 let (old_items, old_keys) = match &mut st.slots[index] {
169 SlotState::Dynamic(dyn_state) => (
170 std::mem::take(&mut dyn_state.items),
171 std::mem::take(&mut dyn_state.keys),
172 ),
173 SlotState::Static(_) => unreachable!("a fragment slot is never static"),
174 };
175 let mut old: HashMap<u64, Child> = HashMap::new();
176 for (k, child) in old_keys.into_iter().zip(old_items) {
177 old.entry(k).or_insert(child);
178 }
179
180 let mut new_items: Vec<Child> = Vec::with_capacity(items.len());
181 let mut keys: Vec<u64> = Vec::with_capacity(items.len());
182 for (idx, item) in items.into_iter().enumerate() {
183 let k = keyer(&item, idx);
184 let child = match old.remove(&k) {
185 Some(existing) => existing,
186 None => make_child(build(item).expect("fragment item build")),
187 };
188 new_items.push(child);
189 keys.push(k);
190 }
191
192 let (gap, is_row, item_nodes) = if let SlotState::Dynamic(dyn_state) = &mut st.slots[index] {
195 let item_nodes: Vec<NodeId> = new_items.iter().map(Child::node).collect();
196 dyn_state.items = new_items;
197 dyn_state.keys = keys;
198 (dyn_state.gap, dyn_state.is_row, item_nodes)
199 } else {
200 (0.0, false, Vec::new())
201 };
202
203 let nodes = flatten_nodes(&st.slots);
205 let node = st.node;
206 let version = st.version.clone();
207 drop(st);
208
209 let _ = set_children(node, &nodes);
210 if gap != 0.0 {
214 for (i, &item) in item_nodes.iter().enumerate() {
215 set_leading_margin(item, is_row, if i == 0 { 0.0 } else { gap });
216 }
217 }
218 for (_, child) in old {
219 remove_node(child.node());
220 }
221 version.update(|v| *v = v.wrapping_add(1));
222}
223
224pub(crate) struct DynHost {
227 state: Rc<RefCell<HostState>>,
228 version: RwSignal<u64>,
229 _effects: Vec<Effect>,
230}
231
232impl DynHost {
233 pub(crate) fn build(node: NodeId, slots: Vec<ChildSlot>) -> Result<Self, LayoutError> {
236 let version = signal(0u64);
237 let state = Rc::new(RefCell::new(HostState {
238 node,
239 slots: Vec::with_capacity(slots.len()),
240 version: version.clone(),
241 }));
242
243 let host_is_row = container_is_row(node);
246
247 let mut specs: Vec<(usize, FragmentSpec)> = Vec::new();
250 {
251 let mut st = state.borrow_mut();
252 for slot in slots {
253 let index = st.slots.len();
254 match slot {
255 ChildSlot::Static(item) => st.slots.push(SlotState::Static(make_child(item))),
256 ChildSlot::Dynamic(spec) => {
257 st.slots.push(SlotState::Dynamic(DynState {
258 gap: spec.gap,
259 is_row: host_is_row,
260 ..Default::default()
261 }));
262 specs.push((index, spec));
263 }
264 }
265 }
266 }
267
268 let effects: Vec<Effect> = specs
269 .into_iter()
270 .map(|(index, spec)| (spec.install)(state.clone(), index))
271 .collect();
272
273 let nodes = flatten_nodes(&state.borrow().slots);
274 let _ = set_children(node, &nodes);
275
276 Ok(Self {
277 state,
278 version,
279 _effects: effects,
280 })
281 }
282
283 pub(crate) fn child_boundaries(&self) -> Vec<RenderNode> {
286 self.version.get();
287 let st = self.state.borrow();
288 let mut out = Vec::new();
289 for slot in &st.slots {
290 match slot {
291 SlotState::Static(child) => out.push(child.segment.boundary()),
292 SlotState::Dynamic(dyn_state) => {
293 out.extend(dyn_state.items.iter().map(|c| c.segment.boundary()))
294 }
295 }
296 }
297 out
298 }
299
300 pub(crate) fn dispatch(&self, event: &Event) -> EventResult {
303 let mut children = collect_children(&self.state.borrow().slots);
304 dispatch_container_event(&mut children, event)
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use crate::container::Container;
312 use crate::context::{compute_layout, reset_layout_runtime, track_layout};
313 use layout_core::{AvailableSpace, LayoutStyle};
314 use reactive_core::signal;
315 use ui_tree::Component;
316
317 fn leaf10() -> Box<dyn LayoutItem> {
318 Box::new(Container::new(LayoutStyle::new().width(10.0).height(10.0), vec![]).unwrap())
319 }
320
321 fn leaf10_node() -> (NodeId, Box<dyn LayoutItem>) {
323 let c = Container::new(LayoutStyle::new().width(10.0).height(10.0), vec![]).unwrap();
324 (c.layout_node(), Box::new(c))
325 }
326
327 fn group_len(node: &RenderNode) -> usize {
328 match node {
329 RenderNode::Group { children, .. } => children.len(),
330 _ => panic!("expected Group"),
331 }
332 }
333
334 #[test]
337 fn fragment_children_flatten_and_reconcile() {
338 reset_layout_runtime();
339 let items = signal(vec![1u32, 2, 3]);
340 let src = items.clone();
341 let container = Container::from_slots(
342 LayoutStyle::new().flex_row(),
343 vec![
344 ChildSlot::stat(leaf10()),
345 fragment(move || src.get(), |n: &u32| *n, |_n| Ok(leaf10()), 0.0),
346 ChildSlot::stat(leaf10()),
347 ],
348 )
349 .unwrap();
350
351 assert_eq!(group_len(&container.view()), 5, "2 static + 3 dynamic");
352
353 items.set(vec![9]);
355 assert_eq!(group_len(&container.view()), 3, "2 static + 1 dynamic");
356
357 items.set(vec![9, 8, 7, 6]);
358 assert_eq!(group_len(&container.view()), 6, "2 static + 4 dynamic");
359 }
360
361 #[test]
370 fn a_fragment_whose_node_is_gone_reconciles_into_nothing() {
371 reset_layout_runtime();
372 let items = signal(vec![1u32, 2, 3]);
373 let src = items.clone();
374 let container = Container::from_slots(
375 LayoutStyle::new().flex_row(),
376 vec![fragment(
377 move || src.get(),
378 |n: &u32| *n,
379 |_n| Ok(leaf10()),
380 0.0,
381 )],
382 )
383 .unwrap();
384 assert_eq!(group_len(&container.view()), 3);
385
386 crate::context::remove_node(container.layout_node());
388 items.set(vec![9]);
389 items.set(Vec::new());
390 }
391
392 #[test]
395 fn fragment_items_flow_in_host_direction_between_statics() {
396 reset_layout_runtime();
397 let (s0, static0) = leaf10_node();
398 let (s1, static1) = leaf10_node();
399 let built: Rc<RefCell<Vec<NodeId>>> = Rc::new(RefCell::new(Vec::new()));
400 let sink = built.clone();
401 let items = signal(vec![1u32, 2, 3]);
402 let src = items.clone();
403
404 let container = Container::from_slots(
405 LayoutStyle::new().flex_row(),
406 vec![
407 ChildSlot::stat(static0),
408 fragment(
409 move || src.get(),
410 |n: &u32| *n,
411 move |_n| {
412 let (node, item) = leaf10_node();
413 sink.borrow_mut().push(node);
414 Ok(item)
415 },
416 0.0,
417 ),
418 ChildSlot::stat(static1),
419 ],
420 )
421 .unwrap();
422
423 compute_layout(
424 container.layout_node(),
425 AvailableSpace::Definite(500.0),
426 AvailableSpace::Definite(50.0),
427 )
428 .unwrap();
429
430 let frag = built.borrow().clone();
431 assert_eq!(frag.len(), 3);
432 let x = |node: NodeId| track_layout(node).unwrap().get().x;
433 let xs = [x(s0), x(frag[0]), x(frag[1]), x(frag[2]), x(s1)];
435 for pair in xs.windows(2) {
436 assert!(
437 pair[1] > pair[0],
438 "children must advance along the row (got x sequence {xs:?})"
439 );
440 }
441 let y = |node: NodeId| track_layout(node).unwrap().get().y;
443 assert!((y(frag[0]) - y(s0)).abs() < 0.01 && (y(s1) - y(frag[2])).abs() < 0.01);
444 }
445
446 #[test]
450 fn pressing_a_fragment_chip_added_after_layout_fires_its_handler() {
451 use crate::context::{new_container, relayout_if_dirty};
452 use crate::styled_container::StyledContainer;
453 use layout_core::{AlignItems, JustifyContent};
454 use platform_core::{Event, PointerButton, PointerSource};
455 use renderer_core::RectStyle;
456
457 reset_layout_runtime();
458 let fired = Rc::new(std::cell::Cell::new(0i32));
459 let ids = signal(Vec::<i32>::new()); let src = ids.clone();
461 let sink = fired.clone();
462
463 let mut row = Container::from_slots(
465 LayoutStyle::new()
466 .flex_row()
467 .align_items(AlignItems::CENTER),
468 vec![fragment(
469 move || src.get(),
470 |id: &i32| *id as u64,
471 move |id| {
472 let sink = sink.clone();
473 let chip = StyledContainer::new(
474 LayoutStyle::new()
475 .flex_column()
476 .width(24.0)
477 .height(24.0)
478 .align_items(AlignItems::CENTER)
479 .justify_content(JustifyContent::CENTER),
480 move |_| RectStyle::default(),
481 vec![],
482 )?
483 .on_press(move || sink.set(id));
484 Ok(Box::new(chip) as Box<dyn LayoutItem>)
485 },
486 8.0,
487 )],
488 )
489 .unwrap();
490
491 let root = new_container(
492 LayoutStyle::new().flex_row().width(200.0).height(24.0),
493 &[row.layout_node()],
494 )
495 .unwrap();
496 compute_layout(
497 root,
498 AvailableSpace::Definite(200.0),
499 AvailableSpace::Definite(24.0),
500 )
501 .unwrap();
502
503 ids.set(vec![7, 8, 9]);
505 relayout_if_dirty();
506
507 let press = |x: f64, y: f64| Event::PointerPressed {
509 x,
510 y,
511 button: PointerButton::Primary,
512 source: PointerSource::Mouse,
513 };
514 let release = |x: f64, y: f64| Event::PointerReleased {
515 x,
516 y,
517 button: PointerButton::Primary,
518 source: PointerSource::Mouse,
519 };
520 row.on_event(&press(12.0, 12.0));
521 row.on_event(&release(12.0, 12.0));
522
523 assert_eq!(
524 fired.get(),
525 7,
526 "clicking the first workspace-style chip should fire its on_press"
527 );
528 }
529
530 #[test]
534 fn stretch_row_of_fragment_chips_fills_the_zone_height() {
535 use crate::context::{new_container, relayout_if_dirty};
536 use crate::styled_container::StyledContainer;
537 use layout_core::AlignItems;
538 use renderer_core::RectStyle;
539
540 reset_layout_runtime();
541 let built: Rc<RefCell<Vec<NodeId>>> = Rc::new(RefCell::new(Vec::new()));
542 let sink = built.clone();
543 let ids = signal(Vec::<i32>::new());
544 let src = ids.clone();
545
546 let row = Container::from_slots(
547 LayoutStyle::new()
548 .flex_row()
549 .align_items(AlignItems::STRETCH),
550 vec![fragment(
551 move || src.get(),
552 |id: &i32| *id as u64,
553 move |_id| {
554 let sink = sink.clone();
555 let chip = StyledContainer::new(
556 LayoutStyle::new().flex_column().padding_horizontal(10.0),
557 move |_| RectStyle::default(),
558 vec![Box::new(Container::new(
559 LayoutStyle::new().width(10.0).height(13.0),
560 vec![],
561 )?)],
562 )?;
563 let node = chip.layout_node();
564 sink.borrow_mut().push(node);
565 Ok(Box::new(chip) as Box<dyn LayoutItem>)
566 },
567 8.0,
568 )],
569 )
570 .unwrap();
571
572 let root = new_container(
573 LayoutStyle::new()
574 .flex_row()
575 .align_items(AlignItems::STRETCH)
576 .width(200.0)
577 .height(34.0),
578 &[row.layout_node()],
579 )
580 .unwrap();
581 compute_layout(
582 root,
583 AvailableSpace::Definite(200.0),
584 AvailableSpace::Definite(34.0),
585 )
586 .unwrap();
587
588 ids.set(vec![1, 2, 3]);
589 relayout_if_dirty();
590
591 let chips = built.borrow().clone();
592 assert_eq!(chips.len(), 3, "three chips built");
593 for node in chips {
594 let h = track_layout(node).unwrap().get().height;
595 assert!(
596 (h - 34.0).abs() < 0.01,
597 "each chip must stretch to the 34px zone height, got {h} (content height ~13 means a \
598 collapsing wrapper crept back in)"
599 );
600 }
601 }
602
603 #[test]
606 fn fragment_gap_spaces_items_along_the_row() {
607 reset_layout_runtime();
608 let built: Rc<RefCell<Vec<NodeId>>> = Rc::new(RefCell::new(Vec::new()));
609 let sink = built.clone();
610 let items = signal(vec![1u32, 2, 3]);
611 let src = items.clone();
612 let container = Container::from_slots(
613 LayoutStyle::new().flex_row(),
614 vec![fragment(
615 move || src.get(),
616 |n: &u32| *n,
617 move |_n| {
618 let (node, item) = leaf10_node();
619 sink.borrow_mut().push(node);
620 Ok(item)
621 },
622 8.0,
623 )],
624 )
625 .unwrap();
626 compute_layout(
627 container.layout_node(),
628 AvailableSpace::Definite(500.0),
629 AvailableSpace::Definite(50.0),
630 )
631 .unwrap();
632
633 let frag = built.borrow().clone();
634 assert_eq!(frag.len(), 3);
635 let x = |node: NodeId| track_layout(node).unwrap().get().x;
636 assert!(x(frag[0]).abs() < 0.01, "first item flush: {}", x(frag[0]));
637 assert!(
638 (x(frag[1]) - 18.0).abs() < 0.01,
639 "10px item + 8px gap → 18: {}",
640 x(frag[1])
641 );
642 assert!(
643 (x(frag[2]) - 36.0).abs() < 0.01,
644 "two 10px items + two 8px gaps → 36: {}",
645 x(frag[2])
646 );
647 }
648
649 #[test]
652 fn fragment_gap_reorder_moves_gap_off_the_new_first_item() {
653 reset_layout_runtime();
654 let built: Rc<RefCell<Vec<NodeId>>> = Rc::new(RefCell::new(Vec::new()));
655 let sink = built.clone();
656 let items = signal(vec![1u32, 2, 3]);
657 let src = items.clone();
658 let container = Container::from_slots(
659 LayoutStyle::new().flex_row(),
660 vec![fragment(
661 move || src.get(),
662 |n: &u32| *n,
663 move |_n| {
664 let (node, item) = leaf10_node();
665 sink.borrow_mut().push(node);
666 Ok(item)
667 },
668 8.0,
669 )],
670 )
671 .unwrap();
672 let root = container.layout_node();
673 let space = || {
674 (
675 AvailableSpace::Definite(500.0),
676 AvailableSpace::Definite(50.0),
677 )
678 };
679 compute_layout(root, space().0, space().1).unwrap();
680
681 let (n1, n3) = {
684 let b = built.borrow();
685 (b[0], b[2])
686 };
687 let x = |node: NodeId| track_layout(node).unwrap().get().x;
688 assert!(x(n1).abs() < 0.01, "key1 first, flush: {}", x(n1));
689 assert!((x(n3) - 36.0).abs() < 0.01, "key3 last, at 36: {}", x(n3));
690
691 items.set(vec![3, 1, 2]);
693 compute_layout(root, space().0, space().1).unwrap();
694 assert!(
695 x(n3).abs() < 0.01,
696 "reordered-to-front item drops its gap margin: {}",
697 x(n3)
698 );
699 assert!(
700 (x(n1) - 18.0).abs() < 0.01,
701 "former-first item now second, at 18: {}",
702 x(n1)
703 );
704 }
705}