tests.rs raw

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<WidgetId>,
    at: usize,
    _item: Signal<String>,
    _index: Signal<i64>,
) -> 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<WidgetId> {
    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<String, String> = KeyedList::new(&mut tree, Some(root));
    let items: Vec<String> = ["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<String> = ["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<i64, String> = KeyedList::new(&mut tree, Some(root));
    let observed: std::rc::Rc<std::cell::RefCell<Vec<(i64, String)>>> = Default::default();

    let items: Vec<String> = ["x:1", "y:2"].map(String::from).into();
    let key_of = |_: usize, item: &String| item.split(':').nth(1).unwrap().parse::<i64>().unwrap();
    let build = |tree: &mut WidgetTree,
                 parent: Option<WidgetId>,
                 at: usize,
                 item: Signal<String>,
                 index: Signal<i64>| {
        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<String> = ["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<WidgetId>, 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<String, String> = owner.run(|| KeyedList::new(&mut tree, Some(root)));

    let items: Vec<Signal<String>> = Default::default();
    let items = std::cell::RefCell::new(items);
    let build = |tree: &mut WidgetTree,
                 parent: Option<WidgetId>,
                 at: usize,
                 item: Signal<String>,
                 index: Signal<i64>| {
        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<String> = ["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<String, String> = 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<String> = ["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<String, String> = 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<std::cell::RefCell<Vec<Conditional>>> = Default::default();
    let build = |tree: &mut WidgetTree,
                 parent: Option<WidgetId>,
                 at: usize,
                 _item: Signal<String>,
                 _index: Signal<i64>| {
        let region = Conditional::new(tree, parent);
        let extent = region.extent();
        place(tree, parent, extent, at);
        inner.borrow_mut().push(region);
        extent
    };

    let items: Vec<String> = ["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<WidgetId> = children[2..5].to_vec();
    let row_b: Vec<WidgetId> = children[5..8].to_vec();
    let items: Vec<String> = ["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<String> = ["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");
    }
}