cranpose 0.11.0

Build desktop, mobile and web apps with composable Rust functions
Documentation
use std::{
    cell::Cell,
    rc::Rc,
    sync::{
        Arc,
        atomic::{AtomicBool, Ordering},
    },
    time::{Duration, Instant},
};

use cranpose::{
    framebuffer::{Framebuffer, PixelRect},
    prelude::*,
};
use cranpose_animation::{
    AnimationSpec, RepeatMode, StartOffset, infiniteRepeatable, rememberInfiniteTransition,
};

const RED: Color = Color(1.0, 0.0, 0.0, 1.0);
const GREEN: Color = Color(0.0, 1.0, 0.0, 1.0);

fn center(framebuffer: &mut Framebuffer) -> [u8; 4] {
    framebuffer.changed_areas(&mut Vec::new());
    let area = PixelRect {
        x: framebuffer.width() / 2,
        y: framebuffer.height() / 2,
        width: 1,
        height: 1,
    };
    let mut pixel = [0; 4];
    framebuffer.draw_area(area, &mut pixel);
    pixel
}

#[test]
fn the_host_theme_reaches_content() {
    for (theme, dark) in [(SystemTheme::Dark, true), (SystemTheme::Light, false)] {
        let seen = Rc::new(Cell::new(None));
        let sink = Rc::clone(&seen);
        let mut framebuffer = Framebuffer::new(40, 30, 1.0, theme, &[], move || {
            sink.set(Some(isSystemInDarkTheme()));
        })
        .expect("valid surface");
        framebuffer.set_active(true);
        framebuffer.tick(1);
        assert_eq!(seen.get(), Some(dark));
    }
}

#[test]
fn a_still_picture_lets_the_host_sleep() {
    let mut framebuffer = Framebuffer::new(40, 30, 1.0, SystemTheme::Light, &[], || {
        Spacer(Modifier::empty().fill_max_size().background(RED));
    })
    .expect("valid surface");
    framebuffer.set_active(true);
    assert!(framebuffer.tick(1));
    assert_eq!(center(&mut framebuffer), [255, 0, 0, 255]);
    let schedule = framebuffer.frame_schedule();
    assert!(!schedule.needs_update);
    assert!(!schedule.needs_frame);
    assert_eq!(schedule.next_deadline, None);
    assert!(!framebuffer.tick(16_000_001));
    let mut areas = Vec::new();
    framebuffer.changed_areas(&mut areas);
    assert!(
        areas.is_empty(),
        "a still picture changes nothing: {areas:?}"
    );
}

#[test]
fn an_animation_asks_for_the_next_frame() {
    let mut framebuffer = Framebuffer::new(40, 30, 1.0, SystemTheme::Light, &[], || {
        let transition = rememberInfiniteTransition("sweep");
        let spec = infiniteRepeatable(
            AnimationSpec::linear(100),
            RepeatMode::Restart,
            StartOffset::default(),
        );
        let fraction = transition.animateFloat(0.0, 1.0, spec, "fraction").value();
        Spacer(
            Modifier::empty()
                .fill_max_width_fraction(fraction)
                .fill_max_height()
                .background(RED),
        );
    })
    .expect("valid surface");
    framebuffer.set_active(true);
    framebuffer.tick(1);
    let schedule = framebuffer.frame_schedule();
    assert!(schedule.needs_frame || schedule.needs_update);
    assert!(framebuffer.tick(60_000_000));
}

#[test]
fn a_delay_wakes_a_sleeping_host_and_its_change_presents() {
    let mut framebuffer = Framebuffer::new(40, 30, 1.0, SystemTheme::Light, &[], || {
        let done = cranpose_core::rememberMutableStateOf(|| false);
        cranpose_core::LaunchedEffectAsync((), move |_| {
            std::boxed::Box::pin(async move {
                cranpose_core::delay(Duration::from_millis(20)).await;
                done.set(true);
            })
        });
        let color = if done.get() { GREEN } else { RED };
        Spacer(Modifier::empty().fill_max_size().background(color));
    })
    .expect("valid surface");
    let woken = Arc::new(AtomicBool::new(false));
    let sink = Arc::clone(&woken);
    framebuffer.set_frame_waker(move || sink.store(true, Ordering::Release));
    framebuffer.set_active(true);
    framebuffer.tick(1);
    assert_eq!(center(&mut framebuffer), [255, 0, 0, 255]);
    let schedule = framebuffer.frame_schedule();
    assert!(!schedule.needs_frame);

    let deadline = Instant::now() + Duration::from_secs(5);
    let mut frame_time = 1;
    while center(&mut framebuffer) != [0, 255, 0, 255] {
        assert!(Instant::now() < deadline, "the delay never woke the host");
        if woken.swap(false, Ordering::AcqRel) {
            framebuffer.run_pending_tasks();
            frame_time += 16_000_000;
            framebuffer.tick(frame_time);
        } else {
            std::thread::sleep(Duration::from_millis(1));
        }
    }
}