cranpose-ui 0.17.2

Composable widgets, text, layouts and input for Cranpose apps
Documentation
use cranpose_foundation::PointerEvent;

use super::*;
use crate::modifier::collect_slices_from_modifier;

#[test]
fn refreshing_popup_content_does_not_invalidate_the_popup_list() {
    let _app_context = crate::render_state::app_context_test_scope();
    let mut composition = cranpose_core::Composition::new(cranpose_core::MemoryApplier::new());
    composition
        .render(
            cranpose_core::location_key(file!(), line!(), column!()),
            || {
                let registry = PopupRegistry::hosted();
                let id = registry.allocate_id();
                registry.upsert(
                    id,
                    Point::default(),
                    Rc::new(|| {}),
                    Some(Rc::new(|| {})),
                    CapturedCompositionContext::default(),
                );
                let revision = registry.inner.revision.as_ref().expect("hosted revision");
                let before = revision.get_non_reactive();
                let calls = Rc::new(Cell::new(0));
                let content_calls = calls.clone();
                let dismiss_calls = calls.clone();
                registry.upsert(
                    id,
                    Point::new(10.0, 20.0),
                    Rc::new(move || content_calls.set(content_calls.get() + 1)),
                    Some(Rc::new(move || dismiss_calls.set(dismiss_calls.get() + 10))),
                    CapturedCompositionContext::default(),
                );
                assert_eq!(
                    revision.get_non_reactive(),
                    before,
                    "only the changed popup needs recomposition"
                );
                let entries = registry.snapshot();
                assert_eq!(entries.len(), 1);
                let updated = entries[0].data.get_non_reactive();
                assert_eq!(updated.position, Point::new(10.0, 20.0));
                (updated.content)();
                assert_eq!(calls.get(), 1);
                (updated.on_dismiss.expect("updated dismiss callback"))();
                assert_eq!(calls.get(), 11);
                registry.upsert(
                    id,
                    Point::default(),
                    Rc::new(|| {}),
                    None,
                    CapturedCompositionContext::default(),
                );
                assert_eq!(revision.get_non_reactive(), before + 1);
                registry.remove(id);
                assert_eq!(revision.get_non_reactive(), before + 2);
            },
        )
        .expect("composition");
}

#[test]
fn non_dismissable_exit_frame_has_no_modal_scrim_callback() {
    let callback: Rc<dyn Fn()> = Rc::new(|| {});
    assert!(popup_dismiss_callback(false, Rc::clone(&callback)).is_none());
    assert!(popup_dismiss_callback(true, callback).is_some());
}

#[test]
fn dismiss_scrim_consumes_the_whole_tap_before_dismissing() {
    let _app_context = crate::render_state::app_context_test_scope();
    let dismissed = Rc::new(Cell::new(false));
    let action: Rc<dyn Fn()> = {
        let dismissed = Rc::clone(&dismissed);
        Rc::new(move || dismissed.set(true))
    };
    let modifier = popup_scrim_pointer_input(7, action);
    let slices = collect_slices_from_modifier(&modifier);
    assert_eq!(slices.pointer_inputs().len(), 1);
    let handler = slices.pointer_inputs()[0].clone();

    let event = |kind| PointerEvent::new(kind, Point::new(12.0, 18.0), Point::new(12.0, 18.0));
    let down = event(PointerEventKind::Down);
    handler(down.clone());
    assert!(
        down.is_consumed(),
        "covered controls must never receive Down"
    );
    assert!(!dismissed.get(), "dismissal fires on release");

    let up = event(PointerEventKind::Up);
    handler(up.clone());
    assert!(up.is_consumed(), "the release stays inside the scrim");
    assert!(
        dismissed.get(),
        "a completed outside tap dismisses the popup"
    );
    for terminal in [PointerEventKind::Up, PointerEventKind::Cancel] {
        dismissed.set(false);
        handler(event(PointerEventKind::Down));
        let handled = event(terminal);
        handled.consume();
        handler(handled);
        assert!(
            !dismissed.get(),
            "a handled or cancelled press stays inside the popup"
        );
    }
}

#[test]
fn popup_content_reads_the_composition_locals_of_its_call_site() {
    use std::cell::RefCell;

    use cranpose_core::{
        Composition, MemoryApplier, MutableState, compositionLocalOf, location_key, mutableStateOf,
    };

    let _app_context = crate::render_state::app_context_test_scope();
    let theme = compositionLocalOf(|| "default");
    let shown: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
    let chosen: Rc<RefCell<Option<MutableState<&'static str>>>> = Rc::new(RefCell::new(None));
    let mut content = {
        let (shown, chosen) = (Rc::clone(&shown), Rc::clone(&chosen));
        move || {
            let (theme, shown, chosen) = (theme.clone(), Rc::clone(&shown), Rc::clone(&chosen));
            PopupHost(move || {
                let value = remember(|| mutableStateOf("night")).with(|state| *state);
                *chosen.borrow_mut() = Some(value);
                let (reader, shown) = (theme.clone(), Rc::clone(&shown));
                CompositionLocalProvider([theme.provides(value.value())], move || {
                    let (reader, shown) = (reader.clone(), Rc::clone(&shown));
                    Popup(
                        Rect::from_origin_size(Point::default(), cranpose_ui_graphics::Size::ZERO),
                        Point::default(),
                        move || {
                            shown.borrow_mut().push(reader.current());
                        },
                    );
                });
            });
        }
    };
    let mut composition = Composition::new(MemoryApplier::new());
    let key = location_key(file!(), line!(), column!());
    let mut settle = move |composition: &mut Composition<MemoryApplier>| {
        composition.render(key, &mut content).expect("render");
        for _ in 0..8 {
            if !composition.should_render() {
                break;
            }
            composition.reconcile(key, &mut content).expect("reconcile");
        }
    };
    settle(&mut composition);
    assert_eq!(
        shown.borrow().last(),
        Some(&"night"),
        "the popup reads its call site's value"
    );

    let value = chosen.borrow().expect("the provided value's state");
    value.set("day");
    settle(&mut composition);
    assert_eq!(
        shown.borrow().last(),
        Some(&"day"),
        "a new value reaches the popup"
    );
}