Simplify Iris app initialization and task updates
This commit is contained in:
1 parent
b5666cdef9
commit
166bac2a93
25 files changed
+330
-282
No files matched your search
@@ -1,4 +1,4 @@
|
||||
use crate::task::RequestRedraw;
|
||||
use crate::task::WakeTaskQueue;
|
||||
use android_view::{
|
||||
View,
|
||||
jni::{JavaVM, objects::GlobalRef},
|
||||
@@ -429,23 +429,23 @@ impl AndroidRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// `Tasks`' redraw handle on Android: a background task finishes on the
|
||||
/// tokio thread `Tasks::init` spawned, which is not attached to the JVM, so
|
||||
/// asking for a frame means attaching first. The global ref is what
|
||||
/// `Tasks`' UI-thread wake on Android. Updates can be submitted from the
|
||||
/// tokio runtime or an application-owned thread that is not attached to the
|
||||
/// JVM, so posting the callback means attaching first. The global ref is what
|
||||
/// survives past the JNI call that handed the `View` to us.
|
||||
pub struct AndroidRedrawHandle {
|
||||
pub struct AndroidTaskWake {
|
||||
vm: JavaVM,
|
||||
view: GlobalRef,
|
||||
}
|
||||
|
||||
impl AndroidRedrawHandle {
|
||||
impl AndroidTaskWake {
|
||||
pub fn new(vm: JavaVM, view: GlobalRef) -> Self {
|
||||
Self { vm, view }
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestRedraw for AndroidRedrawHandle {
|
||||
fn request_redraw(&self) {
|
||||
impl WakeTaskQueue for AndroidTaskWake {
|
||||
fn wake(&self) {
|
||||
let Ok(mut env) = self.vm.attach_current_thread() else {
|
||||
return;
|
||||
};
|
||||
|
||||
+31
-19
@@ -1,5 +1,5 @@
|
||||
use crate::prelude::*;
|
||||
use crate::task::RequestRedraw;
|
||||
use crate::task::WakeTaskQueue;
|
||||
use accesskit_android::Adapter as AccessAdapter;
|
||||
use android_view::{
|
||||
AccessibilityNodeInfo, AccessibilityNodeProvider, Bundle, CallbackCtx, Context,
|
||||
@@ -16,7 +16,7 @@ use std::{cell::RefCell, marker::Sized, rc::Rc, sync::Arc, time::Instant};
|
||||
use super::{
|
||||
access::{AndroidAccessSource, NullActionHandler, raise_if_enabled},
|
||||
insets::{Insets, Shared},
|
||||
render::{AndroidRedrawHandle, AndroidRenderer},
|
||||
render::{AndroidRenderer, AndroidTaskWake},
|
||||
};
|
||||
|
||||
/// Android host state. The renderer follows the `SurfaceView` lifecycle.
|
||||
@@ -100,6 +100,16 @@ pub trait HasAndroidUiState: Sized + 'static {
|
||||
fn android_state_mut(&mut self) -> &mut AndroidUiState;
|
||||
}
|
||||
|
||||
impl HasAndroidUiState for AndroidUiState {
|
||||
fn android_state(&self) -> &AndroidUiState {
|
||||
self
|
||||
}
|
||||
|
||||
fn android_state_mut(&mut self) -> &mut AndroidUiState {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Application state retained for the lifetime of one Android `View`.
|
||||
///
|
||||
/// [`StdRsc`] is the usual [`AndroidResources`] implementation, but the host only
|
||||
@@ -118,6 +128,10 @@ pub trait AndroidAppState: HasAndroidUiState {
|
||||
fn on_insets_changed(&mut self, rsc: &mut Self::Resources, insets: WindowInsets) {}
|
||||
}
|
||||
|
||||
impl AndroidAppState for AndroidUiState {
|
||||
type Resources = StdRsc<Self>;
|
||||
}
|
||||
|
||||
/// Resources the Android host needs to draw and dispatch application events.
|
||||
///
|
||||
/// This deliberately names capabilities rather than storage. Custom bundles
|
||||
@@ -128,12 +142,12 @@ pub trait AndroidResources<State>:
|
||||
where
|
||||
State: 'static,
|
||||
{
|
||||
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>);
|
||||
fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>);
|
||||
}
|
||||
|
||||
impl<State: 'static> AndroidResources<State> for StdRsc<State> {
|
||||
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) {
|
||||
StdRsc::new(redraw)
|
||||
fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
|
||||
StdRsc::new(wake)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,8 +237,12 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
||||
|
||||
self.update_ime_selection(ctx);
|
||||
|
||||
let ui_state = self.state.android_state_mut();
|
||||
ui_state.cursor.end_frame();
|
||||
self.state.android_state_mut().cursor.end_frame();
|
||||
self.request_frame_if_needed(ctx);
|
||||
}
|
||||
|
||||
fn request_frame_if_needed(&self, ctx: &mut CallbackCtx) {
|
||||
let ui_state = self.state.android_state();
|
||||
let render_state = self.rsc.ui().render_state();
|
||||
if render_state
|
||||
.get()
|
||||
@@ -720,18 +738,12 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
||||
self.render(ctx, now);
|
||||
}
|
||||
|
||||
/// Where `AndroidRedrawHandle::request_redraw` (`android/render.rs`)
|
||||
/// actually lands: `View.postDelayed`'s Runnable resolves to this, on
|
||||
/// the UI thread, which is what makes it safe to call from a background
|
||||
/// task's own thread when `post_frame_callback`'s `Choreographer`
|
||||
/// requirement (a `Looper` on the *calling* thread) is not. Same body
|
||||
/// as `do_frame` -- draining tasks and rendering immediately is a
|
||||
/// perfectly good answer to "a background fetch has new state," and
|
||||
/// avoids a second frame-scheduling path to keep in sync with the real
|
||||
/// one.
|
||||
/// Where `AndroidTaskWake::wake` lands on the UI thread. Applying an
|
||||
/// update and drawing it are deliberately separate: retained widget
|
||||
/// invalidation decides whether this wake needs a frame.
|
||||
fn delayed_callback(&mut self, ctx: &mut CallbackCtx) {
|
||||
self.drain_tasks();
|
||||
self.render(ctx, Instant::now());
|
||||
self.request_frame_if_needed(ctx);
|
||||
}
|
||||
|
||||
fn as_input_connection(&mut self) -> Option<&mut dyn InputConnection> {
|
||||
@@ -835,8 +847,8 @@ pub fn new_peer<'local, State: AndroidAppState>(
|
||||
log::info!("iris: new_peer content_scale={content_scale}");
|
||||
let vm = env.get_java_vm().unwrap();
|
||||
let global_view = env.new_global_ref(&view.0).unwrap();
|
||||
let redraw: Arc<dyn RequestRedraw> = Arc::new(AndroidRedrawHandle::new(vm, global_view));
|
||||
let (mut rsc, task_recv) = State::Resources::new(redraw);
|
||||
let wake: Arc<dyn WakeTaskQueue> = Arc::new(AndroidTaskWake::new(vm, global_view));
|
||||
let (mut rsc, task_recv) = State::Resources::new(wake);
|
||||
rsc.ui_mut().set_density(content_scale);
|
||||
let shared = Rc::new(RefCell::new(Shared::default()));
|
||||
let ui_state = AndroidUiState::new(shared.clone(), content_scale);
|
||||
|
||||
+18
-19
@@ -5,41 +5,40 @@ use winit::{
|
||||
window::WindowId,
|
||||
};
|
||||
|
||||
pub trait AppState {
|
||||
type Event: 'static;
|
||||
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self;
|
||||
pub trait AppState: 'static {
|
||||
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop);
|
||||
fn event(&mut self, event: Self::Event, event_loop: &ActiveEventLoop);
|
||||
fn tasks_ready(&mut self);
|
||||
fn exit(&mut self);
|
||||
|
||||
fn run()
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
App::<Self>::run();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct App<State: AppState> {
|
||||
state: Option<State>,
|
||||
proxy: EventLoopProxy<State::Event>,
|
||||
proxy: EventLoopProxy<()>,
|
||||
init: Option<Box<Init<State>>>,
|
||||
}
|
||||
|
||||
impl<State: AppState> App<State> {
|
||||
pub fn run() {
|
||||
type Init<State> = dyn FnOnce(&ActiveEventLoop, EventLoopProxy<()>) -> State;
|
||||
|
||||
impl<State: AppState + 'static> App<State> {
|
||||
pub fn run_with(init: impl FnOnce(&ActiveEventLoop, EventLoopProxy<()>) -> State + 'static) {
|
||||
super::logging::install(log::LevelFilter::Info);
|
||||
let event_loop = EventLoop::with_user_event().build().unwrap();
|
||||
let proxy = event_loop.create_proxy();
|
||||
event_loop
|
||||
.run_app(&mut App::<State> { state: None, proxy })
|
||||
.run_app(&mut App::<State> {
|
||||
state: None,
|
||||
proxy,
|
||||
init: Some(Box::new(init)),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: AppState> ApplicationHandler<State::Event> for App<State> {
|
||||
impl<State: AppState> ApplicationHandler<()> for App<State> {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.state.is_none() {
|
||||
let state = State::new(event_loop, self.proxy.clone());
|
||||
let init = self.init.take().unwrap();
|
||||
let state = init(event_loop, self.proxy.clone());
|
||||
self.state = Some(state);
|
||||
}
|
||||
}
|
||||
@@ -49,9 +48,9 @@ impl<State: AppState> ApplicationHandler<State::Event> for App<State> {
|
||||
state.window_event(event, event_loop);
|
||||
}
|
||||
|
||||
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: State::Event) {
|
||||
fn user_event(&mut self, _: &ActiveEventLoop, (): ()) {
|
||||
let state = self.state.as_mut().unwrap();
|
||||
state.event(event, event_loop);
|
||||
state.tasks_ready();
|
||||
}
|
||||
|
||||
fn exiting(&mut self, _: &ActiveEventLoop) {
|
||||
|
||||
+76
-14
@@ -16,11 +16,15 @@ mod platform;
|
||||
mod render;
|
||||
|
||||
pub use access::*;
|
||||
pub use app::*;
|
||||
use app::{App, AppState};
|
||||
pub use input::*;
|
||||
pub use render::*;
|
||||
|
||||
pub type Proxy<Event> = EventLoopProxy<Event>;
|
||||
impl WakeTaskQueue for EventLoopProxy<()> {
|
||||
fn wake(&self) {
|
||||
let _ = self.send_event(());
|
||||
}
|
||||
}
|
||||
|
||||
/// Physical pixels per dp. Layout and input stay in physical pixels; only
|
||||
/// `dp(...)` resolves through this scale.
|
||||
@@ -79,11 +83,18 @@ pub trait HasDesktopUiState: Sized + 'static {
|
||||
fn desktop_state_mut(&mut self) -> &mut DesktopUiState;
|
||||
}
|
||||
|
||||
impl HasDesktopUiState for DesktopUiState {
|
||||
fn desktop_state(&self) -> &DesktopUiState {
|
||||
self
|
||||
}
|
||||
|
||||
fn desktop_state_mut(&mut self) -> &mut DesktopUiState {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DesktopAppState: HasDesktopUiState {
|
||||
type Event = ();
|
||||
fn new(ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, proxy: Proxy<Self::Event>) -> Self;
|
||||
#[allow(unused_variables)]
|
||||
fn event(&mut self, event: Self::Event, rsc: &mut StdRsc<Self>) {}
|
||||
fn new(ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self;
|
||||
#[allow(unused_variables)]
|
||||
fn exit(&mut self, rsc: &mut StdRsc<Self>) {}
|
||||
#[allow(unused_variables)]
|
||||
@@ -93,18 +104,33 @@ pub trait DesktopAppState: HasDesktopUiState {
|
||||
}
|
||||
}
|
||||
|
||||
impl DesktopAppState for DesktopUiState {
|
||||
fn new(ui_state: DesktopUiState, _: &mut StdRsc<Self>) -> Self {
|
||||
ui_state
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DesktopApp<State: DesktopAppState> {
|
||||
rsc: StdRsc<State>,
|
||||
state: State,
|
||||
task_recv: TaskMsgReceiver<StdRsc<State>>,
|
||||
}
|
||||
|
||||
impl<State: DesktopAppState> AppState for DesktopApp<State> {
|
||||
type Event = State::Event;
|
||||
impl<State: DesktopAppState> DesktopApp<State> {
|
||||
pub fn run() {
|
||||
App::<Self>::run_with(|event_loop, proxy| {
|
||||
Self::new_with(event_loop, proxy, State::window_attributes(), State::new)
|
||||
});
|
||||
}
|
||||
|
||||
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
|
||||
fn new_with(
|
||||
event_loop: &ActiveEventLoop,
|
||||
proxy: EventLoopProxy<()>,
|
||||
attributes: WindowAttributes,
|
||||
init: impl FnOnce(DesktopUiState, &mut StdRsc<State>) -> State,
|
||||
) -> Self {
|
||||
let window = event_loop
|
||||
.create_window(State::window_attributes().with_visible(false))
|
||||
.create_window(attributes.with_visible(false))
|
||||
.unwrap();
|
||||
let access_adapter = accesskit_winit::Adapter::with_direct_handlers(
|
||||
event_loop,
|
||||
@@ -115,20 +141,56 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
|
||||
);
|
||||
window.set_visible(true);
|
||||
let desktop_state = DesktopUiState::new(window, access_adapter);
|
||||
let (mut rsc, task_recv) = StdRsc::new(desktop_state.window.clone());
|
||||
let (mut rsc, task_recv) = StdRsc::new(Arc::new(proxy));
|
||||
// Set before building widgets so the first text shape uses the right density.
|
||||
let scale = content_scale(desktop_state.window.as_ref());
|
||||
rsc.ui.set_density(scale);
|
||||
let state = State::new(desktop_state, &mut rsc, proxy);
|
||||
let state = init(desktop_state, &mut rsc);
|
||||
Self {
|
||||
rsc,
|
||||
state,
|
||||
task_recv,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
|
||||
self.state.event(event, &mut self.rsc);
|
||||
impl DesktopApp<DesktopUiState> {
|
||||
/// Runs an application whose only state is Iris's desktop UI state.
|
||||
pub fn run_with(init: impl FnOnce(&mut DesktopUiState, &mut StdRsc<DesktopUiState>) + 'static) {
|
||||
Self::run_with_attributes(DesktopUiState::window_attributes(), init);
|
||||
}
|
||||
|
||||
pub fn run_with_attributes(
|
||||
attributes: WindowAttributes,
|
||||
init: impl FnOnce(&mut DesktopUiState, &mut StdRsc<DesktopUiState>) + 'static,
|
||||
) {
|
||||
App::<Self>::run_with(move |event_loop, proxy| {
|
||||
Self::new_with(event_loop, proxy, attributes, move |mut ui_state, rsc| {
|
||||
init(&mut ui_state, rsc);
|
||||
ui_state
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: DesktopAppState> AppState for DesktopApp<State> {
|
||||
fn tasks_ready(&mut self) {
|
||||
let Self {
|
||||
rsc,
|
||||
state,
|
||||
task_recv,
|
||||
} = self;
|
||||
for update in task_recv.try_iter() {
|
||||
update(state, rsc);
|
||||
}
|
||||
let ui_state = state.desktop_state();
|
||||
let render_state = rsc.ui.render_state();
|
||||
if render_state
|
||||
.get()
|
||||
.needs_redraw(&ui_state.root, rsc.widgets())
|
||||
{
|
||||
ui_state.renderer.window().request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::task::RequestRedraw;
|
||||
use iris_core::{FrameParts, LinearRgba, Ui, UiRenderNode, util::Vec2};
|
||||
use pollster::FutureExt;
|
||||
use std::sync::Arc;
|
||||
@@ -8,12 +7,6 @@ use winit::{dpi::PhysicalSize, window::Window};
|
||||
|
||||
pub const CLEAR_COLOR: LinearRgba = LinearRgba::BLACK;
|
||||
|
||||
impl RequestRedraw for Window {
|
||||
fn request_redraw(&self) {
|
||||
Window::request_redraw(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UiRenderer {
|
||||
window: Arc<Window>,
|
||||
surface: Surface<'static>,
|
||||
|
||||
+2
-2
@@ -103,8 +103,8 @@ impl RedrawCounter {
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestRedraw for RedrawCounter {
|
||||
fn request_redraw(&self) {
|
||||
impl WakeTaskQueue for RedrawCounter {
|
||||
fn wake(&self) {
|
||||
self.0.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
#![feature(unboxed_closures)]
|
||||
#![feature(fn_traits)]
|
||||
// Only `desktop::DesktopAppState::Event`'s default uses this; unused (and
|
||||
// warned about) on the android target, which has no such default.
|
||||
#![cfg_attr(not(target_os = "android"), feature(associated_type_defaults))]
|
||||
#![feature(unsize)]
|
||||
#![feature(option_into_flat_iter)]
|
||||
#![feature(async_fn_traits)]
|
||||
|
||||
+2
-2
@@ -33,8 +33,8 @@ pub struct StdRsc<State: 'static> {
|
||||
}
|
||||
|
||||
impl<State> StdRsc<State> {
|
||||
pub(crate) fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) {
|
||||
let (tasks, receiver) = Tasks::init(redraw);
|
||||
pub(crate) fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
|
||||
let (tasks, receiver) = Tasks::init(wake);
|
||||
(
|
||||
Self {
|
||||
ui: Ui::default(),
|
||||
|
||||
+72
-28
@@ -19,15 +19,13 @@ pub async fn sleep(duration: std::time::Duration) {
|
||||
tokio::time::sleep(duration).await;
|
||||
}
|
||||
|
||||
/// 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);
|
||||
/// Wakes the platform UI thread so it can apply queued task updates.
|
||||
///
|
||||
/// Waking does not itself mean drawing. Once the updates have run, the host
|
||||
/// asks the retained UI tree whether anything visible became dirty and only
|
||||
/// then schedules a frame.
|
||||
pub trait WakeTaskQueue: Send + Sync + 'static {
|
||||
fn wake(&self);
|
||||
}
|
||||
|
||||
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
|
||||
@@ -38,29 +36,41 @@ impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc>
|
||||
|
||||
pub struct Tasks<Rsc: HasState> {
|
||||
start: AsyncSender<BoxTask>,
|
||||
redraw: Arc<dyn RequestRedraw>,
|
||||
wake: Arc<dyn WakeTaskQueue>,
|
||||
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
|
||||
}
|
||||
|
||||
pub struct TaskCtx<Rsc: HasState> {
|
||||
send: TaskMsgSender<Rsc>,
|
||||
wake: Arc<dyn WakeTaskQueue>,
|
||||
}
|
||||
|
||||
impl<Rsc: HasState> Clone for TaskCtx<Rsc> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
send: self.send.clone(),
|
||||
wake: self.wake.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rsc: HasState> TaskCtx<Rsc> {
|
||||
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
|
||||
let _ = self.send.send(Box::new(f));
|
||||
if self.send.send(Box::new(f)).is_ok() {
|
||||
self.wake.wake();
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
|
||||
fn new(send: TaskMsgSender<Rsc>) -> Self {
|
||||
Self { send }
|
||||
fn new(send: TaskMsgSender<Rsc>, wake: Arc<dyn WakeTaskQueue>) -> Self {
|
||||
Self { send, wake }
|
||||
}
|
||||
}
|
||||
|
||||
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
|
||||
|
||||
impl<Rsc: HasState> Tasks<Rsc> {
|
||||
pub fn init(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Rsc>) {
|
||||
pub fn init(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Rsc>) {
|
||||
let (start, start_recv) = async_channel();
|
||||
let (msgs, msgs_recv) = sync_channel();
|
||||
std::thread::spawn(|| {
|
||||
@@ -71,22 +81,16 @@ impl<Rsc: HasState> Tasks<Rsc> {
|
||||
Self {
|
||||
start,
|
||||
msg_send: msgs,
|
||||
redraw,
|
||||
wake,
|
||||
},
|
||||
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()
|
||||
/// A cloneable, platform-neutral way for an application-owned thread to
|
||||
/// submit work to the UI thread.
|
||||
pub fn context(&self) -> TaskCtx<Rsc> {
|
||||
TaskCtx::new(self.msg_send.clone(), self.wake.clone())
|
||||
}
|
||||
|
||||
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
|
||||
@@ -94,10 +98,9 @@ impl<Rsc: HasState> Tasks<Rsc> {
|
||||
F::CallOnceFuture: Send,
|
||||
{
|
||||
let send = self.msg_send.clone();
|
||||
let redraw = self.redraw.clone();
|
||||
let wake = self.wake.clone();
|
||||
let _ = self.start.send(Box::pin(async move {
|
||||
task(TaskCtx::new(send)).await;
|
||||
redraw.request_redraw();
|
||||
task(TaskCtx::new(send, wake)).await;
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -107,3 +110,44 @@ async fn listen(mut recv: AsyncReceiver<BoxTask>) {
|
||||
tokio::spawn(task);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::{
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
struct TestRsc;
|
||||
|
||||
impl HasState for TestRsc {
|
||||
type State = usize;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct WakeCounter(AtomicUsize);
|
||||
|
||||
impl WakeTaskQueue for WakeCounter {
|
||||
fn wake(&self) {
|
||||
self.0.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_update_wakes_the_ui_queue() {
|
||||
let wakes = Arc::new(WakeCounter::default());
|
||||
let (tasks, updates) = Tasks::<TestRsc>::init(wakes.clone());
|
||||
let mut ctx = tasks.context();
|
||||
|
||||
ctx.update(|state: &mut usize, _| *state += 1);
|
||||
ctx.update(|state: &mut usize, _| *state += 2);
|
||||
|
||||
assert_eq!(wakes.0.load(Ordering::Relaxed), 2);
|
||||
let mut state = 0;
|
||||
let mut rsc = TestRsc;
|
||||
updates.recv_timeout(Duration::from_secs(1)).unwrap()(&mut state, &mut rsc);
|
||||
updates.recv_timeout(Duration::from_secs(1)).unwrap()(&mut state, &mut rsc);
|
||||
assert_eq!(state, 3);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user