105 lines
3.5 KiB
Rust
105 lines
3.5 KiB
Rust
use iris_core::HasState;
|
|
use std::{
|
|
pin::Pin,
|
|
sync::{
|
|
Arc,
|
|
mpsc::{Receiver as SyncReceiver, Sender as SyncSender, channel as sync_channel},
|
|
},
|
|
};
|
|
use tokio::{
|
|
runtime::Runtime,
|
|
sync::mpsc::{
|
|
UnboundedReceiver as AsyncReceiver, UnboundedSender as AsyncSender,
|
|
unbounded_channel as async_channel,
|
|
},
|
|
};
|
|
|
|
/// What a completed task nudges when it wants its result drawn. Shared
|
|
/// between backends rather than typed as `winit::window::Window` directly:
|
|
/// android-view has no `Window` at all, and the redraw request there is a
|
|
/// JNI call (`View::post_frame_callback`) rather than a method call on a
|
|
/// value this crate owns. Each backend supplies its own implementation --
|
|
/// `desktop/render.rs` for winit, `android/render.rs` for android-view --
|
|
/// and this module never needs to know which one it is holding.
|
|
pub trait RequestRedraw: Send + Sync + 'static {
|
|
fn request_redraw(&self);
|
|
}
|
|
|
|
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
|
|
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
|
|
|
|
pub trait TaskUpdate<Rsc: HasState>: FnOnce(&mut Rsc::State, &mut Rsc) + Send {}
|
|
impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc> for F {}
|
|
|
|
pub struct Tasks<Rsc: HasState> {
|
|
start: AsyncSender<BoxTask>,
|
|
redraw: Arc<dyn RequestRedraw>,
|
|
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
|
|
}
|
|
|
|
pub struct TaskCtx<Rsc: HasState> {
|
|
send: TaskMsgSender<Rsc>,
|
|
}
|
|
|
|
impl<Rsc: HasState> TaskCtx<Rsc> {
|
|
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
|
|
let _ = self.send.send(Box::new(f));
|
|
}
|
|
}
|
|
impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
|
|
fn new(send: TaskMsgSender<Rsc>) -> Self {
|
|
Self { send }
|
|
}
|
|
}
|
|
|
|
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
|
|
|
|
impl<Rsc: HasState> Tasks<Rsc> {
|
|
pub fn init(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Rsc>) {
|
|
let (start, start_recv) = async_channel();
|
|
let (msgs, msgs_recv) = sync_channel();
|
|
std::thread::spawn(|| {
|
|
let rt = Runtime::new().unwrap();
|
|
rt.block_on(listen(start_recv))
|
|
});
|
|
(
|
|
Self {
|
|
start,
|
|
msg_send: msgs,
|
|
redraw,
|
|
},
|
|
msgs_recv,
|
|
)
|
|
}
|
|
|
|
/// The same redraw handle `spawn`'s wrapper calls once, after a whole
|
|
/// task's future completes -- exposed so a caller running its own
|
|
/// longer-lived loop *inside* a spawned task (a live SSE follow, here)
|
|
/// can ask for a frame after each `TaskCtx::update`, not just at the
|
|
/// end. Without this a caller has no way to get a redraw mid-stream,
|
|
/// which is exactly the gap `iris/desktop-app`'s `app.rs` module doc
|
|
/// names for why it uses winit's `Proxy` instead of `Tasks` -- Android
|
|
/// has no `Proxy`, so this is what closes the same gap there.
|
|
pub fn redraw_handle(&self) -> Arc<dyn RequestRedraw> {
|
|
self.redraw.clone()
|
|
}
|
|
|
|
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
|
|
where
|
|
F::CallOnceFuture: Send,
|
|
{
|
|
let send = self.msg_send.clone();
|
|
let redraw = self.redraw.clone();
|
|
let _ = self.start.send(Box::pin(async move {
|
|
task(TaskCtx::new(send)).await;
|
|
redraw.request_redraw();
|
|
}));
|
|
}
|
|
}
|
|
|
|
async fn listen(mut recv: AsyncReceiver<BoxTask>) {
|
|
while let Some(task) = recv.recv().await {
|
|
tokio::spawn(task);
|
|
}
|
|
}
|