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