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);
}
}
}