cranpose-ui 0.12.0

Composable widgets, text, layouts and input for Cranpose apps
Documentation
use std::{
    cell::RefCell,
    future::pending,
    sync::{
        Arc,
        atomic::{AtomicBool, Ordering},
        mpsc,
    },
};

use cranpose_core::{
    Composition, DisposableEffect, EventSender, LaunchedEffectAsync, MemoryApplier, location_key,
    ownedMutableStateOf, remember, rememberCoroutineScope, rememberEventStream,
    rememberMutableStateOf, schedule_frame,
};

thread_local! {
    static HELD_COMPOSITION: RefCell<Option<Composition<MemoryApplier>>> =
        const { RefCell::new(None) };
}

pub fn live_state() {
    let count = rememberMutableStateOf(|| 0u32);
    let holder = remember(|| ownedMutableStateOf(String::from("held")));
    rememberCoroutineScope().launch(pending());
    let _events = rememberEventStream((), |_: EventSender<u32>| ());
    LaunchedEffectAsync((), |_| std::boxed::Box::pin(pending()));
    DisposableEffect((), move |scope| {
        let _ = holder.with(|state| state.value());
        scope.on_dispose(move || {
            count.set(count.get() + 1);
            schedule_frame();
        })
    });
}

fn held_composition(disposed: &Arc<AtomicBool>) -> Composition<MemoryApplier> {
    let mut composition = Composition::new(MemoryApplier::new());
    composition
        .render(location_key(file!(), line!(), column!()), &mut || {
            live_state();
            let read = rememberMutableStateOf(|| 1u32);
            let disposed = Arc::clone(disposed);
            DisposableEffect((), move |scope| {
                scope.on_dispose(move || {
                    read.set(read.get() + 1);
                    disposed.store(read.get() == 2, Ordering::Release);
                })
            });
        })
        .expect("the composition renders");
    composition
}

pub fn exit_while_another_thread_composes(body: impl FnOnce() + Send + 'static) {
    let (composing, wait_for_composing) = mpsc::channel();
    let mut worker = Some(std::thread::spawn(move || {
        body();
        wait_for_composing
            .recv()
            .expect("the joining thread composes");
    }));
    let mut joining = Composition::new(MemoryApplier::new());
    joining
        .render(location_key(file!(), line!(), column!()), &mut || {
            if let Some(worker) = worker.take() {
                composing.send(()).expect("the worker waits");
                worker.join().expect("the thread exits without a panic");
            }
        })
        .expect("the joining composition renders");
}

pub fn hold_composition_at_thread_exit(before_hold: fn()) {
    let disposed = Arc::new(AtomicBool::new(false));
    let held_disposed = Arc::clone(&disposed);
    exit_while_another_thread_composes(move || {
        before_hold();
        let composition = held_composition(&held_disposed);
        HELD_COMPOSITION.with(|held| *held.borrow_mut() = Some(composition));
    });
    assert!(
        disposed.load(Ordering::Acquire),
        "thread exit runs the held composition's dispose callbacks, which read their own writes"
    );
}

pub fn touch_the_holder_first() {
    HELD_COMPOSITION.with(|held| assert!(held.borrow().is_none()));
}

pub fn touch_the_holder_after_another_runtime() {
    drop(Composition::new(MemoryApplier::new()));
    touch_the_holder_first();
}