cranpose-ui 0.1.86

UI primitives for Cranpose
Documentation
use super::*;
use cranpose_core::NodeId;
use cranpose_foundation::lazy::{remember_lazy_list_state, LazyListScope, LazyListState};
use cranpose_ui_graphics::Size as ViewportSize;
use std::cell::RefCell;
use std::rc::Rc;

const ROWS: usize = 40;
const ROW_HEIGHT: f32 = 48.0;
const VIEWPORT: ViewportSize = ViewportSize {
    width: 320.0,
    height: 200.0,
};

fn measure(composition: &mut TestComposition, root: NodeId) {
    let handle = composition.runtime_handle();
    let mut applier = composition.applier_mut();
    applier.set_runtime_handle(handle);
    measure_layout(&mut applier, root, VIEWPORT).expect("layout measurement");
    applier.clear_runtime_handle();
}

#[composable]
#[allow(non_snake_case)]
fn ParkedRow(index: usize, nested: bool) {
    let body = move || {
        if index == 0 {
            cranpose_core::LaunchedEffectAsync!(0u32, move |_scope| {
                Box::pin(async move {
                    std::future::pending::<()>().await;
                })
            });
        }
        Text(
            format!("row {index}"),
            Modifier::empty().height(ROW_HEIGHT),
            TextStyle::default(),
        );
    };
    match nested {
        true => {
            SwipeToDismiss(
                Modifier::empty().fill_max_width(),
                SwipeToDismissSpec::default(),
                || {},
                body,
            );
        }
        false => body(),
    }
}

fn task_counts_across_reuse(nested: bool, scroll_back: bool) -> Vec<usize> {
    let held: Rc<RefCell<Option<LazyListState>>> = Rc::new(RefCell::new(None));
    let keeper = Rc::clone(&held);
    let mut composition = run_test_composition(move || {
        let list_state = remember_lazy_list_state();
        *keeper.borrow_mut() = Some(list_state);
        LazyColumn(
            Modifier::empty().fill_max_size(),
            list_state,
            LazyColumnSpec::default(),
            move |scope| {
                scope.items(
                    ROWS,
                    Some(|index: usize| index as u64),
                    None::<fn(usize) -> u64>,
                    move |index| ParkedRow(index, nested),
                );
            },
        );
    });

    let root = composition.root().expect("root node");
    measure(&mut composition, root);
    composition.runtime_handle().drain_ui();
    let mut counts = vec![composition.runtime_handle().debug_stats().tasks_len];

    let list_state = (*held.borrow()).expect("list state");
    list_state.scroll_to_item(ROWS - 4, 0.0);
    measure(&mut composition, root);
    composition.runtime_handle().drain_ui();
    counts.push(composition.runtime_handle().debug_stats().tasks_len);

    if scroll_back {
        list_state.scroll_to_item(0, 0.0);
        measure(&mut composition, root);
        composition.runtime_handle().drain_ui();
        counts.push(composition.runtime_handle().debug_stats().tasks_len);
    }
    counts
}

#[test]
fn a_recycled_lazy_row_stops_its_parked_effect() {
    assert_eq!(
        task_counts_across_reuse(false, false),
        vec![1, 0],
        "the row's effect must run on screen and be cancelled off screen"
    );
}

#[test]
fn a_recycled_lazy_row_stops_the_effect_of_a_widget_one_subcompose_deeper() {
    assert_eq!(
        task_counts_across_reuse(true, false),
        vec![1, 0],
        "the SwipeToDismiss effect must run on screen and be cancelled off screen"
    );
}

#[test]
fn a_recycled_lazy_row_restarts_its_effect_when_it_returns() {
    assert_eq!(
        (
            task_counts_across_reuse(false, true),
            task_counts_across_reuse(true, true),
        ),
        (vec![1, 0, 1], vec![1, 0, 1]),
        "reusing a row must restart its nested effect exactly once"
    );
}