cranpose-app-shell 0.1.171

Application orchestration shell for Cranpose
Documentation
use std::cell::{Cell, RefCell};

use cranpose_core::{
    DisposableEffect, DisposableEffectResult, MutableState, location_key, mutableStateOf, remember,
};
use cranpose_foundation::lazy::rememberLazyListState;
use cranpose_macros::composable;
use cranpose_ui::{LazyColumn, LazyColumnSpec, Modifier, Text, TextStyle};

use super::*;

thread_local! {
    static SCREEN_SHOWN: RefCell<Option<MutableState<bool>>> = const { RefCell::new(None) };
    static DISPOSES: Cell<usize> = const { Cell::new(0) };
}

#[composable]
fn DisposalRoot() {
    let shown = remember(|| mutableStateOf(true)).with(|state| *state);
    SCREEN_SHOWN.with(|slot| *slot.borrow_mut() = Some(shown));
    if shown.get() {
        DisposalScreen();
    } else {
        OtherListScreen();
    }
}

#[composable]
fn OtherListScreen() {
    LazyColumn(
        Modifier::empty().fill_max_size(),
        rememberLazyListState(),
        LazyColumnSpec::new(),
        move |scope| {
            scope.items(20, move |index| {
                Text(
                    format!("Other {index}"),
                    Modifier::empty().height(40.0),
                    TextStyle::default(),
                );
            });
        },
    );
}

#[composable]
fn DisposalScreen() {
    let disposes = remember(|| mutableStateOf(0usize)).with(|state| *state);
    LazyColumn(
        Modifier::empty().fill_max_size(),
        rememberLazyListState(),
        LazyColumnSpec::new(),
        move |scope| {
            scope.items(20, move |index| DisposalItem(index, disposes));
        },
    );
}

#[composable]
fn DisposalItem(index: usize, disposes: MutableState<usize>) {
    DisposableEffect(index, move |_| {
        DisposableEffectResult::new(move || {
            disposes.update(|count| *count += 1);
            DISPOSES.with(|count| count.set(count.get() + 1));
        })
    });
    Text(
        format!("Item {index}"),
        Modifier::empty().height(40.0),
        TextStyle::default(),
    );
}

fn disposal_shell() -> AppShell<TestRenderer> {
    let root_key = location_key(file!(), line!(), column!());
    let mut shell = AppShell::new(TestRenderer::default(), root_key, DisposalRoot);
    shell.set_buffer_size(320, 240);
    shell.set_viewport(320.0, 240.0);
    shell.update();
    shell
}

#[test]
fn a_lazy_item_disposal_may_write_state_its_removed_screen_remembered() {
    let _guard = test_guard();
    let mut shell = disposal_shell();
    DISPOSES.with(|count| count.set(0));

    let shown = SCREEN_SHOWN
        .with(|slot| *slot.borrow())
        .expect("the root publishes its switch");
    for visible in [false, true, false, true, false] {
        shown.set(visible);
        for _ in 0..4 {
            shell.update();
        }
    }
    assert!(
        DISPOSES.with(Cell::get) > 0,
        "hiding the screen disposes its visible items"
    );
    drop(shell);
}

#[test]
fn a_lazy_item_disposal_may_write_state_at_final_teardown() {
    let _guard = test_guard();
    let shell = disposal_shell();
    DISPOSES.with(|count| count.set(0));
    drop(shell);
    assert!(
        DISPOSES.with(Cell::get) > 0,
        "dropping the app disposes the visible items"
    );
}

thread_local! {
    static SCROLL_LIST: RefCell<Option<cranpose_foundation::lazy::LazyListState>> = const { RefCell::new(None) };
}

#[composable]
fn SelfWritingList() {
    let list = rememberLazyListState();
    SCROLL_LIST.with(|slot| *slot.borrow_mut() = Some(list));
    LazyColumn(
        Modifier::empty().fill_max_size(),
        list,
        LazyColumnSpec::new(),
        |scope| {
            scope.items(200, SelfWritingItem);
        },
    );
}

#[composable]
fn SelfWritingItem(index: usize) {
    let own = remember(|| mutableStateOf(0usize)).with(|state| *state);
    DisposableEffect(index, move |_| {
        DisposableEffectResult::new(move || own.update(|count| *count += 1))
    });
    Text(
        format!("Row {index}"),
        Modifier::empty().height(40.0),
        TextStyle::default(),
    );
}

#[test]
fn an_item_scrolled_away_may_write_its_own_state_while_it_is_disposed() {
    let _guard = test_guard();
    let root_key = location_key(file!(), line!(), column!());
    let mut shell = AppShell::new(TestRenderer::default(), root_key, SelfWritingList);
    shell.set_buffer_size(320, 240);
    shell.set_viewport(320.0, 240.0);
    shell.update();
    let list = SCROLL_LIST
        .with(|slot| *slot.borrow())
        .expect("the list publishes its state");
    for index in [60, 120, 190, 0] {
        list.scroll_to_item(index, 0.0);
        for _ in 0..3 {
            shell.update();
        }
    }
    drop(shell);
}