cranpose 0.9.35

Build desktop, mobile and web apps with composable Rust functions
Documentation
use std::{cell::Cell, rc::Rc};

use cranpose::{
    native_view::{NativeView, NativeViewHost},
    prelude::*,
};
use cranpose_app_shell::{AppShell, default_root_key};
use cranpose_core::{MutableState, rememberMutableStateOf};
use cranpose_render_pixels::PixelsRenderer;

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

    use cranpose::{WebView, WebViewEvent};

    let host = NativeViewHost::default();
    let content_host = host.clone();
    let url = Rc::new(Cell::new(None::<MutableState<&'static str>>));
    let content_url = Rc::clone(&url);
    let events = Rc::new(RefCell::new(Vec::new()));
    let content_events = Rc::clone(&events);
    let mut shell = AppShell::new_with_size(
        PixelsRenderer::new(),
        default_root_key(),
        move || {
            let page = rememberMutableStateOf(|| "https://example.com");
            content_url.set(Some(page));
            let events = Rc::clone(&content_events);
            content_host.provide(|| {
                WebView(
                    page.value(),
                    Modifier::empty().size_points(200.0, 120.0),
                    move |event| {
                        events.borrow_mut().push(event);
                    },
                );
            });
        },
        (320, 240),
        (320.0, 240.0),
    );
    let initial = host.layout(shell.layout_tree());
    assert_eq!(initial.len(), 1);
    assert_eq!(initial[0].value, "https://example.com");
    assert!(host.dispatch(initial[0].id, "loaded:https://example.com"));
    assert_eq!(
        *events.borrow(),
        [WebViewEvent::Loaded("https://example.com".into())]
    );
    url.get()
        .expect("composed URL state")
        .set("https://example.org");
    shell.update();
    let updated = host.layout(shell.layout_tree());
    assert_eq!(updated[0].id, initial[0].id);
    assert_eq!(updated[0].value, "https://example.org");
    assert!(host.dispatch(updated[0].id, "error:offline"));
    assert_eq!(
        events.borrow().last(),
        Some(&WebViewEvent::Failed("offline".into()))
    );
    drop(shell);
    assert!(!host.dispatch(updated[0].id, "loaded:stale"));
}

#[test]
fn native_slots_follow_layout_updates_events_and_disposal() {
    let host = NativeViewHost::default();
    let content_host = host.clone();
    let controls = Rc::new(Cell::new(None::<MutableState<bool>>));
    let content_controls = Rc::clone(&controls);
    let mut shell = AppShell::new_with_size(
        PixelsRenderer::new(),
        default_root_key(),
        move || {
            let show = rememberMutableStateOf(|| true);
            let count = rememberMutableStateOf(|| 0);
            content_controls.set(Some(show));
            let host = content_host.clone();
            Column(
                Modifier::empty().padding(12.0),
                ColumnSpec::default().vertical_arrangement(LinearArrangement::spaced_by(8.0)),
                move || {
                    Spacer(Modifier::empty().height(20.0));
                    if show.value() {
                        NativeView(
                            host.clone(),
                            "web",
                            &format!("count={}", count.value()),
                            Modifier::empty().width(100.0).height(60.0),
                            move |event| {
                                if event == "increment" {
                                    count.set(count.value() + 1);
                                }
                            },
                        );
                    }
                },
            );
        },
        (320, 240),
        (320.0, 240.0),
    );
    let slots = host.layout(shell.layout_tree());
    assert_eq!(slots.len(), 1);
    assert_eq!(
        slots[0].bounds,
        Rect {
            x: 12.0,
            y: 40.0,
            width: 100.0,
            height: 60.0
        }
    );
    let id = slots[0].id;
    assert!(host.dispatch(id, "increment"));
    shell.update();
    let slots = host.layout(shell.layout_tree());
    assert_eq!(slots[0].id, id);
    assert_eq!(slots[0].value, "count=1");

    controls.get().expect("composed controls").set(false);
    shell.update();
    assert!(host.layout(shell.layout_tree()).is_empty());
    assert!(!host.dispatch(id, "increment"));

    controls.get().expect("composed controls").set(true);
    shell.update();
    let remounted = host.layout(shell.layout_tree());
    assert_eq!(remounted.len(), 1);
    assert_ne!(remounted[0].id, id);
    drop(shell);
    assert!(!host.dispatch(remounted[0].id, "increment"));
}

#[test]
fn hosts_do_not_share_native_events() {
    fn component(host: NativeViewHost) -> AppShell<PixelsRenderer> {
        AppShell::new_with_size(
            PixelsRenderer::new(),
            default_root_key(),
            move || {
                let count = rememberMutableStateOf(|| 0);
                NativeView(
                    host.clone(),
                    "web",
                    &count.value().to_string(),
                    Modifier::empty().width(100.0).height(60.0),
                    move |_| count.set(count.value() + 1),
                );
            },
            (320, 240),
            (320.0, 240.0),
        )
    }
    let first = NativeViewHost::default();
    let second = NativeViewHost::default();
    let mut a = component(first.clone());
    let mut b = component(second.clone());
    let id = first.layout(a.layout_tree())[0].id;
    assert!(first.dispatch(id, "increment"));
    a.update();
    b.update();
    assert_eq!(first.layout(a.layout_tree())[0].value, "1");
    assert_eq!(second.layout(b.layout_tree())[0].value, "0");
}

#[test]
fn changing_factory_retires_old_events() {
    let host = NativeViewHost::default();
    let content_host = host.clone();
    let kind = Rc::new(Cell::new(None::<MutableState<bool>>));
    let content_kind = Rc::clone(&kind);
    let received = Rc::new(Cell::new(0));
    let content_received = Rc::clone(&received);
    let mut shell = AppShell::new_with_size(
        PixelsRenderer::new(),
        default_root_key(),
        move || {
            let alternate = rememberMutableStateOf(|| false);
            content_kind.set(Some(alternate));
            let received = Rc::clone(&content_received);
            NativeView(
                content_host.clone(),
                if alternate.value() { "map" } else { "web" },
                "",
                Modifier::empty().width(100.0).height(60.0),
                move |_| received.set(received.get() + 1),
            );
        },
        (320, 240),
        (320.0, 240.0),
    );
    let old = host.layout(shell.layout_tree())[0].id;
    kind.get().expect("mounted").set(true);
    shell.update();
    let replacement = host.layout(shell.layout_tree());
    assert_eq!(replacement[0].kind, "map");
    assert!(!host.dispatch(old, "late completion from disposed WebView"));
    assert!(host.dispatch(replacement[0].id, "current event"));
    assert_eq!(received.get(), 1);
}