//! 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); /// 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)>; fn channel_ends() -> &'static (Sender, Mutex>) { static ENDS: OnceLock<(Sender, Mutex>)> = 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> = const { RefCell::new(Vec::new()) }; } /// The handle that wakes the loop, learned once at startup. fn waker() -> &'static Mutex> { static WAKER: OnceLock>> = 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( 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::().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 = { 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); } } }