use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::rc::Rc;
use layout_core::{LayoutError, NodeId};
use platform_core::Event;
use reactive_core::{Effect, RwSignal, effect, signal};
use ui_tree::{EventResult, RenderNode};
use crate::context::{container_is_row, remove_node, set_children, set_leading_margin};
use crate::layout_item::{Child, LayoutItem, make_child};
use crate::pointer::dispatch_container_event;
fn hash_key<K: Hash>(k: &K) -> u64 {
let mut h = DefaultHasher::new();
k.hash(&mut h);
h.finish()
}
pub enum ChildSlot {
Static(Box<dyn LayoutItem>),
Dynamic(FragmentSpec),
}
impl ChildSlot {
pub fn stat(item: Box<dyn LayoutItem>) -> Self {
ChildSlot::Static(item)
}
}
pub struct FragmentSpec {
install: Box<dyn FnOnce(Rc<RefCell<HostState>>, usize) -> Effect>,
gap: f32,
}
pub fn fragment<Item, Key, S, K, B>(source: S, key: K, build: B, gap: f32) -> ChildSlot
where
Key: Hash + 'static,
Item: 'static,
S: Fn() -> Vec<Item> + 'static,
K: Fn(&Item) -> Key + 'static,
B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
{
make_fragment(
source,
move |item: &Item, _idx: usize| hash_key(&key(item)),
build,
gap,
)
}
pub fn fragment_positional<Item, S, B>(source: S, build: B, gap: f32) -> ChildSlot
where
Item: 'static,
S: Fn() -> Vec<Item> + 'static,
B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
{
make_fragment(source, |_item: &Item, idx: usize| idx as u64, build, gap)
}
fn make_fragment<Item, S, B, KeyFn>(source: S, keyer: KeyFn, build: B, gap: f32) -> ChildSlot
where
Item: 'static,
S: Fn() -> Vec<Item> + 'static,
B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError> + 'static,
KeyFn: Fn(&Item, usize) -> u64 + 'static,
{
let install = Box::new(
move |state: Rc<RefCell<HostState>>, index: usize| -> Effect {
effect(move || {
let items = source();
reconcile_slot(&state, index, items, &keyer, &build);
})
},
);
ChildSlot::Dynamic(FragmentSpec { install, gap })
}
#[derive(Default)]
struct DynState {
items: Vec<Child>,
keys: Vec<u64>,
gap: f32,
is_row: bool,
}
enum SlotState {
Static(Child),
Dynamic(DynState),
}
struct HostState {
node: NodeId,
slots: Vec<SlotState>,
version: RwSignal<u64>,
}
fn flatten_nodes(slots: &[SlotState]) -> Vec<NodeId> {
let mut nodes = Vec::new();
for slot in slots {
match slot {
SlotState::Static(child) => nodes.push(child.node()),
SlotState::Dynamic(dyn_state) => nodes.extend(dyn_state.items.iter().map(Child::node)),
}
}
nodes
}
fn collect_children(slots: &[SlotState]) -> Vec<Child> {
let mut out = Vec::new();
for slot in slots {
match slot {
SlotState::Static(child) => out.push(child.clone()),
SlotState::Dynamic(dyn_state) => out.extend(dyn_state.items.iter().cloned()),
}
}
out
}
fn reconcile_slot<Item, KeyFn, B>(
state: &Rc<RefCell<HostState>>,
index: usize,
items: Vec<Item>,
keyer: &KeyFn,
build: &B,
) where
KeyFn: Fn(&Item, usize) -> u64,
B: Fn(Item) -> Result<Box<dyn LayoutItem>, LayoutError>,
{
let mut st = state.borrow_mut();
let (old_items, old_keys) = match &mut st.slots[index] {
SlotState::Dynamic(dyn_state) => (
std::mem::take(&mut dyn_state.items),
std::mem::take(&mut dyn_state.keys),
),
SlotState::Static(_) => unreachable!("a fragment slot is never static"),
};
let mut old: HashMap<u64, Child> = HashMap::new();
for (k, child) in old_keys.into_iter().zip(old_items) {
old.entry(k).or_insert(child);
}
let mut new_items: Vec<Child> = Vec::with_capacity(items.len());
let mut keys: Vec<u64> = Vec::with_capacity(items.len());
for (idx, item) in items.into_iter().enumerate() {
let k = keyer(&item, idx);
let child = match old.remove(&k) {
Some(existing) => existing,
None => make_child(build(item).expect("fragment item build")),
};
new_items.push(child);
keys.push(k);
}
let (gap, is_row, item_nodes) = if let SlotState::Dynamic(dyn_state) = &mut st.slots[index] {
let item_nodes: Vec<NodeId> = new_items.iter().map(Child::node).collect();
dyn_state.items = new_items;
dyn_state.keys = keys;
(dyn_state.gap, dyn_state.is_row, item_nodes)
} else {
(0.0, false, Vec::new())
};
let nodes = flatten_nodes(&st.slots);
let node = st.node;
let version = st.version.clone();
drop(st);
let _ = set_children(node, &nodes);
if gap != 0.0 {
for (i, &item) in item_nodes.iter().enumerate() {
set_leading_margin(item, is_row, if i == 0 { 0.0 } else { gap });
}
}
for (_, child) in old {
remove_node(child.node());
}
version.update(|v| *v = v.wrapping_add(1));
}
pub(crate) struct DynHost {
state: Rc<RefCell<HostState>>,
version: RwSignal<u64>,
_effects: Vec<Effect>,
}
impl DynHost {
pub(crate) fn build(node: NodeId, slots: Vec<ChildSlot>) -> Result<Self, LayoutError> {
let version = signal(0u64);
let state = Rc::new(RefCell::new(HostState {
node,
slots: Vec::with_capacity(slots.len()),
version: version.clone(),
}));
let host_is_row = container_is_row(node);
let mut specs: Vec<(usize, FragmentSpec)> = Vec::new();
{
let mut st = state.borrow_mut();
for slot in slots {
let index = st.slots.len();
match slot {
ChildSlot::Static(item) => st.slots.push(SlotState::Static(make_child(item))),
ChildSlot::Dynamic(spec) => {
st.slots.push(SlotState::Dynamic(DynState {
gap: spec.gap,
is_row: host_is_row,
..Default::default()
}));
specs.push((index, spec));
}
}
}
}
let effects: Vec<Effect> = specs
.into_iter()
.map(|(index, spec)| (spec.install)(state.clone(), index))
.collect();
let nodes = flatten_nodes(&state.borrow().slots);
let _ = set_children(node, &nodes);
Ok(Self {
state,
version,
_effects: effects,
})
}
pub(crate) fn child_boundaries(&self) -> Vec<RenderNode> {
self.version.get();
let st = self.state.borrow();
let mut out = Vec::new();
for slot in &st.slots {
match slot {
SlotState::Static(child) => out.push(child.segment.boundary()),
SlotState::Dynamic(dyn_state) => {
out.extend(dyn_state.items.iter().map(|c| c.segment.boundary()))
}
}
}
out
}
pub(crate) fn dispatch(&self, event: &Event) -> EventResult {
let mut children = collect_children(&self.state.borrow().slots);
dispatch_container_event(&mut children, event)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::container::Container;
use crate::context::{compute_layout, reset_layout_runtime, track_layout};
use layout_core::{AvailableSpace, LayoutStyle};
use reactive_core::signal;
use ui_tree::Component;
fn leaf10() -> Box<dyn LayoutItem> {
Box::new(Container::new(LayoutStyle::new().width(10.0).height(10.0), vec![]).unwrap())
}
fn leaf10_node() -> (NodeId, Box<dyn LayoutItem>) {
let c = Container::new(LayoutStyle::new().width(10.0).height(10.0), vec![]).unwrap();
(c.layout_node(), Box::new(c))
}
fn group_len(node: &RenderNode) -> usize {
match node {
RenderNode::Group { children, .. } => children.len(),
_ => panic!("expected Group"),
}
}
#[test]
fn fragment_children_flatten_and_reconcile() {
reset_layout_runtime();
let items = signal(vec![1u32, 2, 3]);
let src = items.clone();
let container = Container::from_slots(
LayoutStyle::new().flex_row(),
vec![
ChildSlot::stat(leaf10()),
fragment(move || src.get(), |n: &u32| *n, |_n| Ok(leaf10()), 0.0),
ChildSlot::stat(leaf10()),
],
)
.unwrap();
assert_eq!(group_len(&container.view()), 5, "2 static + 3 dynamic");
items.set(vec![9]);
assert_eq!(group_len(&container.view()), 3, "2 static + 1 dynamic");
items.set(vec![9, 8, 7, 6]);
assert_eq!(group_len(&container.view()), 6, "2 static + 4 dynamic");
}
#[test]
fn a_fragment_whose_node_is_gone_reconciles_into_nothing() {
reset_layout_runtime();
let items = signal(vec![1u32, 2, 3]);
let src = items.clone();
let container = Container::from_slots(
LayoutStyle::new().flex_row(),
vec![fragment(
move || src.get(),
|n: &u32| *n,
|_n| Ok(leaf10()),
0.0,
)],
)
.unwrap();
assert_eq!(group_len(&container.view()), 3);
crate::context::remove_node(container.layout_node());
items.set(vec![9]);
items.set(Vec::new());
}
#[test]
fn fragment_items_flow_in_host_direction_between_statics() {
reset_layout_runtime();
let (s0, static0) = leaf10_node();
let (s1, static1) = leaf10_node();
let built: Rc<RefCell<Vec<NodeId>>> = Rc::new(RefCell::new(Vec::new()));
let sink = built.clone();
let items = signal(vec![1u32, 2, 3]);
let src = items.clone();
let container = Container::from_slots(
LayoutStyle::new().flex_row(),
vec![
ChildSlot::stat(static0),
fragment(
move || src.get(),
|n: &u32| *n,
move |_n| {
let (node, item) = leaf10_node();
sink.borrow_mut().push(node);
Ok(item)
},
0.0,
),
ChildSlot::stat(static1),
],
)
.unwrap();
compute_layout(
container.layout_node(),
AvailableSpace::Definite(500.0),
AvailableSpace::Definite(50.0),
)
.unwrap();
let frag = built.borrow().clone();
assert_eq!(frag.len(), 3);
let x = |node: NodeId| track_layout(node).unwrap().get().x;
let xs = [x(s0), x(frag[0]), x(frag[1]), x(frag[2]), x(s1)];
for pair in xs.windows(2) {
assert!(
pair[1] > pair[0],
"children must advance along the row (got x sequence {xs:?})"
);
}
let y = |node: NodeId| track_layout(node).unwrap().get().y;
assert!((y(frag[0]) - y(s0)).abs() < 0.01 && (y(s1) - y(frag[2])).abs() < 0.01);
}
#[test]
fn pressing_a_fragment_chip_added_after_layout_fires_its_handler() {
use crate::context::{new_container, relayout_if_dirty};
use crate::styled_container::StyledContainer;
use layout_core::{AlignItems, JustifyContent};
use platform_core::{Event, PointerButton, PointerSource};
use renderer_core::RectStyle;
reset_layout_runtime();
let fired = Rc::new(std::cell::Cell::new(0i32));
let ids = signal(Vec::<i32>::new()); let src = ids.clone();
let sink = fired.clone();
let mut row = Container::from_slots(
LayoutStyle::new()
.flex_row()
.align_items(AlignItems::CENTER),
vec![fragment(
move || src.get(),
|id: &i32| *id as u64,
move |id| {
let sink = sink.clone();
let chip = StyledContainer::new(
LayoutStyle::new()
.flex_column()
.width(24.0)
.height(24.0)
.align_items(AlignItems::CENTER)
.justify_content(JustifyContent::CENTER),
move |_| RectStyle::default(),
vec![],
)?
.on_press(move || sink.set(id));
Ok(Box::new(chip) as Box<dyn LayoutItem>)
},
8.0,
)],
)
.unwrap();
let root = new_container(
LayoutStyle::new().flex_row().width(200.0).height(24.0),
&[row.layout_node()],
)
.unwrap();
compute_layout(
root,
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(24.0),
)
.unwrap();
ids.set(vec![7, 8, 9]);
relayout_if_dirty();
let press = |x: f64, y: f64| Event::PointerPressed {
x,
y,
button: PointerButton::Primary,
source: PointerSource::Mouse,
};
let release = |x: f64, y: f64| Event::PointerReleased {
x,
y,
button: PointerButton::Primary,
source: PointerSource::Mouse,
};
row.on_event(&press(12.0, 12.0));
row.on_event(&release(12.0, 12.0));
assert_eq!(
fired.get(),
7,
"clicking the first workspace-style chip should fire its on_press"
);
}
#[test]
fn stretch_row_of_fragment_chips_fills_the_zone_height() {
use crate::context::{new_container, relayout_if_dirty};
use crate::styled_container::StyledContainer;
use layout_core::AlignItems;
use renderer_core::RectStyle;
reset_layout_runtime();
let built: Rc<RefCell<Vec<NodeId>>> = Rc::new(RefCell::new(Vec::new()));
let sink = built.clone();
let ids = signal(Vec::<i32>::new());
let src = ids.clone();
let row = Container::from_slots(
LayoutStyle::new()
.flex_row()
.align_items(AlignItems::STRETCH),
vec![fragment(
move || src.get(),
|id: &i32| *id as u64,
move |_id| {
let sink = sink.clone();
let chip = StyledContainer::new(
LayoutStyle::new().flex_column().padding_horizontal(10.0),
move |_| RectStyle::default(),
vec![Box::new(Container::new(
LayoutStyle::new().width(10.0).height(13.0),
vec![],
)?)],
)?;
let node = chip.layout_node();
sink.borrow_mut().push(node);
Ok(Box::new(chip) as Box<dyn LayoutItem>)
},
8.0,
)],
)
.unwrap();
let root = new_container(
LayoutStyle::new()
.flex_row()
.align_items(AlignItems::STRETCH)
.width(200.0)
.height(34.0),
&[row.layout_node()],
)
.unwrap();
compute_layout(
root,
AvailableSpace::Definite(200.0),
AvailableSpace::Definite(34.0),
)
.unwrap();
ids.set(vec![1, 2, 3]);
relayout_if_dirty();
let chips = built.borrow().clone();
assert_eq!(chips.len(), 3, "three chips built");
for node in chips {
let h = track_layout(node).unwrap().get().height;
assert!(
(h - 34.0).abs() < 0.01,
"each chip must stretch to the 34px zone height, got {h} (content height ~13 means a \
collapsing wrapper crept back in)"
);
}
}
#[test]
fn fragment_gap_spaces_items_along_the_row() {
reset_layout_runtime();
let built: Rc<RefCell<Vec<NodeId>>> = Rc::new(RefCell::new(Vec::new()));
let sink = built.clone();
let items = signal(vec![1u32, 2, 3]);
let src = items.clone();
let container = Container::from_slots(
LayoutStyle::new().flex_row(),
vec![fragment(
move || src.get(),
|n: &u32| *n,
move |_n| {
let (node, item) = leaf10_node();
sink.borrow_mut().push(node);
Ok(item)
},
8.0,
)],
)
.unwrap();
compute_layout(
container.layout_node(),
AvailableSpace::Definite(500.0),
AvailableSpace::Definite(50.0),
)
.unwrap();
let frag = built.borrow().clone();
assert_eq!(frag.len(), 3);
let x = |node: NodeId| track_layout(node).unwrap().get().x;
assert!(x(frag[0]).abs() < 0.01, "first item flush: {}", x(frag[0]));
assert!(
(x(frag[1]) - 18.0).abs() < 0.01,
"10px item + 8px gap → 18: {}",
x(frag[1])
);
assert!(
(x(frag[2]) - 36.0).abs() < 0.01,
"two 10px items + two 8px gaps → 36: {}",
x(frag[2])
);
}
#[test]
fn fragment_gap_reorder_moves_gap_off_the_new_first_item() {
reset_layout_runtime();
let built: Rc<RefCell<Vec<NodeId>>> = Rc::new(RefCell::new(Vec::new()));
let sink = built.clone();
let items = signal(vec![1u32, 2, 3]);
let src = items.clone();
let container = Container::from_slots(
LayoutStyle::new().flex_row(),
vec![fragment(
move || src.get(),
|n: &u32| *n,
move |_n| {
let (node, item) = leaf10_node();
sink.borrow_mut().push(node);
Ok(item)
},
8.0,
)],
)
.unwrap();
let root = container.layout_node();
let space = || {
(
AvailableSpace::Definite(500.0),
AvailableSpace::Definite(50.0),
)
};
compute_layout(root, space().0, space().1).unwrap();
let (n1, n3) = {
let b = built.borrow();
(b[0], b[2])
};
let x = |node: NodeId| track_layout(node).unwrap().get().x;
assert!(x(n1).abs() < 0.01, "key1 first, flush: {}", x(n1));
assert!((x(n3) - 36.0).abs() < 0.01, "key3 last, at 36: {}", x(n3));
items.set(vec![3, 1, 2]);
compute_layout(root, space().0, space().1).unwrap();
assert!(
x(n3).abs() < 0.01,
"reordered-to-front item drops its gap margin: {}",
x(n3)
);
assert!(
(x(n1) - 18.0).abs() < 0.01,
"former-first item now second, at 18: {}",
x(n1)
);
}
}