background.rs raw

//! Background work with a UI-thread continuation.
//!
//! [`run`] takes two closures: `work`, which runs on its own thread and may
//! block as long as it likes, and `then`, which receives `work`'s result on
//! the UI thread with the widget tree in hand. Anything that must not stall a
//! frame — a file chooser, a clipboard negotiation, file I/O against storage
//! that might be a dead network mount — goes through here.
//!
//! The continuation never leaves the UI thread: it closes over signal handles
//! and `Rc`s, and the signal runtime is thread-local by design. So it waits
//! here, by id, and only the result crosses between threads. The [`Waker`]
//! says a result has, and the shell hands it over in `poll`, on the right
//! thread with the tree in hand — an idle guiduck app renders no frames, so
//! without the wake the result would sit until the next input event happened
//! along.
//!
//! Each task gets its own thread. The callers this serves are rare,
//! human-scale events (a chooser closing, a paste arriving, an image
//! import); none of them warrant a pool.

use std::any::Any;
use std::cell::RefCell;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::sync::{Mutex, OnceLock};

use guiduck_core::WidgetTree;

use crate::Waker;

/// A finished task's result, on its way back to the UI thread: which
/// continuation it belongs to, and what the work produced.
type Answer = (u64, Box<dyn Any + Send>);

/// A continuation, boxed with the downcast that recovers its result's
/// concrete type. The pairing in [`run`] is what makes the downcast
/// infallible: one id, one result type.
type Continuation = Box<dyn FnOnce(&mut WidgetTree, Box<dyn Any + Send>)>;

fn channel_ends() -> &'static (Sender<Answer>, Mutex<Receiver<Answer>>) {
    static ENDS: OnceLock<(Sender<Answer>, Mutex<Receiver<Answer>>)> = OnceLock::new();
    ENDS.get_or_init(|| {
        let (sender, receiver) = channel();
        (sender, Mutex::new(receiver))
    })
}

thread_local! {
    /// Continuations awaiting their result. UI-thread-only, which is the point.
    static WAITING: RefCell<Vec<(u64, Continuation)>> = const { RefCell::new(Vec::new()) };
}

/// The handle that wakes the loop, learned once at startup.
fn waker() -> &'static Mutex<Option<Waker>> {
    static WAKER: OnceLock<Mutex<Option<Waker>>> = OnceLock::new();
    WAKER.get_or_init(|| Mutex::new(None))
}

/// The shell learned how to wake itself; remember it, so background work
/// needs no plumbing to report back.
pub(crate) fn set_waker(handle: Waker) {
    *waker().lock().expect("not poisoned") = Some(handle);
}

/// Run `work` on its own thread, then hand its result to `then` on the UI
/// thread. Returns immediately.
///
/// `then` runs during the shell's poll — between frames, never inside a
/// handler — so it may freely borrow what handlers borrow. If the work
/// thread dies (a panic in `work`), the continuation is simply never called:
/// its captures are dropped when the app shuts down.
pub fn run<T: Send + 'static>(
    work: impl FnOnce() -> T + Send + 'static,
    then: impl FnOnce(&mut WidgetTree, T) + 'static,
) {
    static NEXT: AtomicU64 = AtomicU64::new(0);
    let id = NEXT.fetch_add(1, Ordering::Relaxed);
    // The continuation stays on this thread; only its id goes with the work.
    WAITING.with_borrow_mut(|waiting| {
        waiting.push((
            id,
            Box::new(move |tree, result| {
                let result = result.downcast::<T>().expect("paired by id in run");
                then(tree, *result);
            }),
        ));
    });
    std::thread::spawn(move || {
        let result = work();
        let _ = channel_ends().0.send((id, Box::new(result)));
        // The result is across; now say so, or an idle window would sit on
        // it until the next input event happened along.
        if let Some(waker) = waker().lock().expect("not poisoned").as_ref() {
            waker.wake();
        }
    });
}

/// Hand every finished task's result to its continuation. Called from the
/// shell's `poll`, on the UI thread.
pub(crate) fn deliver(tree: &mut WidgetTree) {
    let answers: Vec<Answer> = {
        let receiver = channel_ends().1.lock().expect("not poisoned");
        receiver.try_iter().collect()
    };
    for (id, result) in answers {
        let continuation = WAITING.with_borrow_mut(|waiting| {
            waiting
                .iter()
                .position(|(waiting_id, _)| *waiting_id == id)
                .map(|index| waiting.remove(index).1)
        });
        if let Some(continuation) = continuation {
            continuation(tree, result);
        }
    }
}