use guiduck_scene::geom::Size; use taffy::prelude::{length, percent}; use super::*; use crate::text::TextContext; use crate::widget::Container; fn tree_with_root() -> (WidgetTree, WidgetId) { let mut tree = WidgetTree::with_text_context(TextContext::hermetic([])); let root = tree.insert( Container::new(), taffy::Style { size: taffy::Size { width: percent(1.0_f32), height: percent(1.0_f32), }, flex_direction: taffy::FlexDirection::Column, ..Default::default() }, None, ); (tree, root) } fn row_style() -> taffy::Style { taffy::Style { size: taffy::Size { width: percent(1.0_f32), height: length(20.0_f32), }, flex_shrink: 0.0, ..Default::default() } } fn build_row( tree: &mut WidgetTree, parent: Option, at: usize, _item: Signal, _index: Signal, ) -> Extent { Extent::Single(tree.insert_at(Container::new(), row_style(), parent.expect("parent"), at)) } /// The widget children of `parent` that are rows — everything between the /// region's leading and trailing markers. fn row_widgets(tree: &WidgetTree, parent: WidgetId) -> Vec { let children = tree.children(parent); children[1..children.len() - 1].to_vec() } #[test] fn keyed_reconcile_reuses_moves_and_disposes() { let (mut tree, root) = tree_with_root(); let mut list: KeyedList = KeyedList::new(&mut tree, Some(root)); let items: Vec = ["a", "b", "c"].map(String::from).into(); list.reconcile(&mut tree, &items, |_, item| item.clone(), build_row); tree.render_frame(Size::new(200.0, 200.0)); assert_eq!(list.len(), 3); let [a, b, c] = row_widgets(&tree, root)[..] else { panic!("three rows"); }; // Reorder plus removal plus insertion: c moves first, a survives, b // vanishes, d appears at the end. let items: Vec = ["c", "a", "d"].map(String::from).into(); list.reconcile(&mut tree, &items, |_, item| item.clone(), build_row); tree.render_frame(Size::new(200.0, 200.0)); let rows = row_widgets(&tree, root); assert_eq!(rows.len(), 3); assert_eq!(rows[0], c, "`c` kept its widget across the move"); assert_eq!(rows[1], a, "`a` kept its widget"); assert!(!tree.children(root).contains(&b), "`b` was removed"); assert_ne!(rows[2], b, "`d` is a fresh widget"); // Clearing removes everything but the marker. list.reconcile(&mut tree, &[], |_, item| item.clone(), build_row); tree.render_frame(Size::new(200.0, 200.0)); assert!(list.is_empty()); assert_eq!( tree.children(root).len(), 2, "only the region's own two markers remain" ); } #[test] fn reused_rows_receive_item_and_index_updates() { let (mut tree, root) = tree_with_root(); let mut list: KeyedList = KeyedList::new(&mut tree, Some(root)); let observed: std::rc::Rc>> = Default::default(); let items: Vec = ["x:1", "y:2"].map(String::from).into(); let key_of = |_: usize, item: &String| item.split(':').nth(1).unwrap().parse::().unwrap(); let build = |tree: &mut WidgetTree, parent: Option, at: usize, item: Signal, index: Signal| { let observed = observed.clone(); guiduck_signals::Effect::new(move || { observed.borrow_mut().push((index.get(), item.get())); }); Extent::Single(tree.insert_at(Container::new(), row_style(), parent.expect("parent"), at)) }; list.reconcile(&mut tree, &items, key_of, build); guiduck_signals::flush_effects(); assert_eq!( observed.borrow().as_slice(), [(0, "x:1".into()), (1, "y:2".into())] ); // Same keys, new values and order: rows are reused, their signals // update — no new effects appear. observed.borrow_mut().clear(); let items: Vec = ["Y:2", "X:1"].map(String::from).into(); list.reconcile(&mut tree, &items, key_of, build); guiduck_signals::flush_effects(); let mut seen = observed.borrow().clone(); seen.sort(); assert_eq!( seen.as_slice(), [(0, "Y:2".into()), (1, "X:1".into())], "existing rows saw the new values and positions" ); } #[test] fn conditional_swaps_branches_and_preserves_position() { let (mut tree, root) = tree_with_root(); let before = tree.insert(Container::new(), row_style(), Some(root)); let mut conditional = Conditional::new(&mut tree, Some(root)); let after = tree.insert(Container::new(), row_style(), Some(root)); let build_then = |tree: &mut WidgetTree, parent: Option, at: usize| { Some(Extent::Single(tree.insert_at( Container::new(), row_style(), parent.expect("parent"), at, ))) }; conditional.set(&mut tree, true, build_then); tree.render_frame(Size::new(200.0, 200.0)); let children = tree.children(root).to_vec(); assert_eq!(children.len(), 5, "before, lead, branch, trail, after"); assert_eq!(children[0], before); assert_eq!(children[4], after); let branch = children[2]; // Flipping to a contentless else removes the branch. conditional.set(&mut tree, false, |_, _, _| None); tree.render_frame(Size::new(200.0, 200.0)); assert!(!tree.children(root).contains(&branch)); assert_eq!(tree.children(root).len(), 4); // Same condition again: nothing rebuilds (the applied value is // remembered even for empty branches). conditional.set(&mut tree, false, |_, _, _| panic!("must not rebuild")); // And back: a fresh branch mounts in the same slot. conditional.set(&mut tree, true, build_then); tree.render_frame(Size::new(200.0, 200.0)); let children = tree.children(root); assert_eq!(children.len(), 5); assert_eq!(children[0], before); assert_eq!(children[4], after); } /// A region's content belongs to the scope that mounted the region. /// /// Content is built from the command queue, where no scope is entered, so /// without an owner of its own every row scope would be a child of the root /// and would outlive the region — and its effects would keep running against /// widgets that are gone. #[test] fn rows_die_with_the_scope_that_mounted_the_region() { let (mut tree, root) = tree_with_root(); let owner = Scope::new(); let mut list: KeyedList = owner.run(|| KeyedList::new(&mut tree, Some(root))); let items: Vec> = Default::default(); let items = std::cell::RefCell::new(items); let build = |tree: &mut WidgetTree, parent: Option, at: usize, item: Signal, index: Signal| { items.borrow_mut().push(item); build_row(tree, parent, at, item, index) }; // Reconciled outside `owner.run`, as the command drain does it. let values: Vec = ["a", "b"].map(String::from).into(); list.reconcile(&mut tree, &values, |_, item| item.clone(), build); assert!( items.borrow().iter().all(|item| item.is_alive()), "the rows are live while their region's owner is" ); owner.dispose(); assert!( items.borrow().iter().all(|item| !item.is_alive()), "and went with it, rather than outliving it in the root scope" ); } /// An update that arrives after the region was removed does nothing. /// /// Regions are updated through the command queue, so an enclosing region can /// retire this one — or its component can be unmounted — between the effect /// queueing an update and the drain running it. #[test] fn a_removed_region_ignores_an_update_queued_before_it_went() { let (mut tree, root) = tree_with_root(); let mut conditional = Conditional::new(&mut tree, Some(root)); let mut list: KeyedList = KeyedList::new(&mut tree, Some(root)); // Exactly what an enclosing region does when it retires a row. for region in [conditional.extent(), list.extent()] { let Extent::Region { lead, trail } = region else { panic!("a region's extent is a run"); }; tree.remove(lead); tree.remove(trail); } conditional.set(&mut tree, true, |_, _, _| { panic!("a removed region builds nothing") }); let values: Vec = ["a"].map(String::from).into(); list.reconcile( &mut tree, &values, |_, item| item.clone(), |_, _, _, _, _| panic!("a removed region builds nothing"), ); assert!(list.is_empty()); assert!(tree.children(root).is_empty(), "and mounted nothing"); } /// A region can be another region's row, which is what the two anchors buy: /// the outer list places, reorders, and retires a body whose widget count it /// does not know and which changes after placement. #[test] fn a_row_can_be_a_nested_region() { let (mut tree, root) = tree_with_root(); let before = tree.insert(Container::new(), row_style(), Some(root)); let mut list: KeyedList = KeyedList::new(&mut tree, Some(root)); let after = tree.insert(Container::new(), row_style(), Some(root)); // Each row is a `Conditional` that starts empty; the inner regions are // kept alive by the test, standing in for the row scope that holds them // in generated code. let inner: std::rc::Rc>> = Default::default(); let build = |tree: &mut WidgetTree, parent: Option, at: usize, _item: Signal, _index: Signal| { let region = Conditional::new(tree, parent); let extent = region.extent(); place(tree, parent, extent, at); inner.borrow_mut().push(region); extent }; let items: Vec = ["a", "b"].map(String::from).into(); list.reconcile(&mut tree, &items, |_, item| item.clone(), build); // before, list-lead, [a-lead, a-trail], [b-lead, b-trail], list-trail, after assert_eq!(tree.children(root).len(), 8); assert_eq!(tree.children(root)[0], before); assert_eq!(tree.children(root)[7], after); // The inner regions gain content *after* the outer list placed them — // the case a captured widget list could not survive. for region in inner.borrow_mut().iter_mut() { region.set(&mut tree, true, |tree, parent, at| { let id = tree.insert_at(Container::new(), row_style(), parent.expect("parent"), at); Some(Extent::Single(id)) }); } let children = tree.children(root).to_vec(); assert_eq!(children.len(), 10, "each row grew by one widget"); // Each row's content sits inside its own anchors, and the rows stay // contiguous between the list's. assert_eq!(children[0], before); assert_eq!(children[9], after); // Reordering moves whole runs: row `b` and its content go first, as a // block, and the surrounding siblings do not move. let row_a: Vec = children[2..5].to_vec(); let row_b: Vec = children[5..8].to_vec(); let items: Vec = ["b", "a"].map(String::from).into(); list.reconcile(&mut tree, &items, |_, item| item.clone(), build); let children = tree.children(root).to_vec(); assert_eq!(children.len(), 10, "a reorder mounts nothing new"); assert_eq!(children[0], before); assert_eq!(children[9], after); assert_eq!(children[2..5], row_b[..], "`b`'s whole run moved first"); assert_eq!(children[5..8], row_a[..], "`a`'s whole run followed"); // Retiring a row takes its whole run with it. let items: Vec = ["a"].map(String::from).into(); list.reconcile(&mut tree, &items, |_, item| item.clone(), build); let children = tree.children(root).to_vec(); assert_eq!(children.len(), 7, "`b`'s three widgets all went"); for id in row_b { assert!(!children.contains(&id), "no widget of `b` survived"); } }