Simplify Iris app initialization and task updates
This commit is contained in:
1 parent
b5666cdef9
commit
166bac2a93
25 files changed
+319
-271
No files matched your search
@@ -28,8 +28,8 @@ pub struct Resources {
|
||||
}
|
||||
|
||||
impl AndroidResources<State> for Resources {
|
||||
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) {
|
||||
let (tasks, receiver) = Tasks::init(redraw);
|
||||
fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
|
||||
let (tasks, receiver) = Tasks::init(wake);
|
||||
(
|
||||
Self {
|
||||
ui: Ui::default(),
|
||||
|
||||
@@ -2,19 +2,11 @@ use iris::prelude::*;
|
||||
|
||||
#[path = "lib.rs"]
|
||||
mod app;
|
||||
use app::*;
|
||||
|
||||
#[derive(AndroidUiState)]
|
||||
struct State {
|
||||
ui_state: AndroidUiState,
|
||||
}
|
||||
|
||||
impl AndroidAppState for State {
|
||||
type Resources = StdRsc<Self>;
|
||||
}
|
||||
|
||||
#[iris::app_init]
|
||||
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
|
||||
let _ = build(rsc, &mut ui_state);
|
||||
State { ui_state }
|
||||
fn create(
|
||||
ui_state: &mut AndroidUiState,
|
||||
rsc: &mut StdRsc<AndroidUiState>,
|
||||
) {
|
||||
let _ = app::build(rsc, ui_state);
|
||||
}
|
||||
@@ -3,7 +3,6 @@ use winit::event::WindowEvent;
|
||||
|
||||
#[path = "lib.rs"]
|
||||
mod app;
|
||||
use app::*;
|
||||
|
||||
const SETTLE_FRAMES: usize = 4;
|
||||
const FRAMES: usize = 6;
|
||||
@@ -17,8 +16,8 @@ struct State {
|
||||
}
|
||||
|
||||
impl DesktopAppState for State {
|
||||
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
|
||||
let span = build(rsc, &mut ui_state);
|
||||
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self {
|
||||
let span = app::build(rsc, &mut ui_state);
|
||||
Self {
|
||||
ui_state,
|
||||
span,
|
||||
|
||||
@@ -2,19 +2,11 @@ use iris::prelude::*;
|
||||
|
||||
#[path = "lib.rs"]
|
||||
mod app;
|
||||
use app::*;
|
||||
|
||||
#[derive(AndroidUiState)]
|
||||
struct State {
|
||||
ui_state: AndroidUiState,
|
||||
}
|
||||
|
||||
impl AndroidAppState for State {
|
||||
type Resources = StdRsc<Self>;
|
||||
}
|
||||
|
||||
#[iris::app_init]
|
||||
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
|
||||
build(rsc, &mut ui_state);
|
||||
State { ui_state }
|
||||
fn create(
|
||||
ui_state: &mut AndroidUiState,
|
||||
rsc: &mut StdRsc<AndroidUiState>,
|
||||
) {
|
||||
app::build(rsc, ui_state);
|
||||
}
|
||||
@@ -3,24 +3,10 @@ use winit::{dpi::LogicalSize, window::WindowAttributes};
|
||||
|
||||
#[path = "lib.rs"]
|
||||
mod app;
|
||||
use app::*;
|
||||
|
||||
#[derive(DesktopUiState)]
|
||||
struct State {
|
||||
ui_state: DesktopUiState,
|
||||
}
|
||||
|
||||
impl DesktopAppState for State {
|
||||
fn window_attributes() -> WindowAttributes {
|
||||
WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0))
|
||||
}
|
||||
|
||||
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
|
||||
build(rsc, &mut ui_state);
|
||||
Self { ui_state }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
DesktopApp::<State>::run();
|
||||
let attributes = WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0));
|
||||
DesktopApp::<DesktopUiState>::run_with_attributes(attributes, |ui_state, rsc| {
|
||||
app::build(rsc, ui_state)
|
||||
});
|
||||
}
|
||||
@@ -2,19 +2,11 @@ use iris::prelude::*;
|
||||
|
||||
#[path = "lib.rs"]
|
||||
mod app;
|
||||
use app::*;
|
||||
|
||||
#[derive(AndroidUiState)]
|
||||
struct State {
|
||||
ui_state: AndroidUiState,
|
||||
}
|
||||
|
||||
impl AndroidAppState for State {
|
||||
type Resources = StdRsc<Self>;
|
||||
}
|
||||
|
||||
#[iris::app_init]
|
||||
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
|
||||
build(rsc, &mut ui_state);
|
||||
State { ui_state }
|
||||
fn create(
|
||||
ui_state: &mut AndroidUiState,
|
||||
rsc: &mut StdRsc<AndroidUiState>,
|
||||
) {
|
||||
app::build(rsc, ui_state);
|
||||
}
|
||||
@@ -2,20 +2,7 @@ use iris::prelude::*;
|
||||
|
||||
#[path = "lib.rs"]
|
||||
mod app;
|
||||
use app::*;
|
||||
|
||||
#[derive(DesktopUiState)]
|
||||
struct State {
|
||||
ui_state: DesktopUiState,
|
||||
}
|
||||
|
||||
impl DesktopAppState for State {
|
||||
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
|
||||
build(rsc, &mut ui_state);
|
||||
Self { ui_state }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
DesktopApp::<State>::run();
|
||||
DesktopApp::<DesktopUiState>::run_with(|ui_state, rsc| app::build(rsc, ui_state));
|
||||
}
|
||||
@@ -2,7 +2,6 @@ use iris::prelude::*;
|
||||
|
||||
#[path = "lib.rs"]
|
||||
mod app;
|
||||
use app::*;
|
||||
|
||||
#[derive(AndroidUiState)]
|
||||
struct Client {
|
||||
@@ -19,14 +18,14 @@ impl AndroidAppState for Client {
|
||||
.renderer
|
||||
.as_ref()
|
||||
.map_or(0, |renderer| renderer.ui.view_count());
|
||||
update_info(rsc, self.info, views);
|
||||
app::update_info(rsc, self.info, views);
|
||||
}
|
||||
}
|
||||
|
||||
#[iris::app_init]
|
||||
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Client>) -> Client {
|
||||
let widgets = build(rsc, &mut ui_state);
|
||||
update_info(rsc, widgets.info, 0);
|
||||
let widgets = app::build(rsc, &mut ui_state);
|
||||
app::update_info(rsc, widgets.info, 0);
|
||||
Client {
|
||||
ui_state,
|
||||
info: widgets.info,
|
||||
|
||||
@@ -3,7 +3,6 @@ use winit::event::WindowEvent;
|
||||
|
||||
#[path = "lib.rs"]
|
||||
mod app;
|
||||
use app::*;
|
||||
|
||||
#[derive(DesktopUiState)]
|
||||
struct Client {
|
||||
@@ -12,9 +11,9 @@ struct Client {
|
||||
}
|
||||
|
||||
impl DesktopAppState for Client {
|
||||
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
|
||||
let widgets = build(rsc, &mut ui_state);
|
||||
update_info(rsc, widgets.info, 0);
|
||||
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self {
|
||||
let widgets = app::build(rsc, &mut ui_state);
|
||||
app::update_info(rsc, widgets.info, 0);
|
||||
Self {
|
||||
ui_state,
|
||||
info: widgets.info,
|
||||
@@ -22,7 +21,7 @@ impl DesktopAppState for Client {
|
||||
}
|
||||
|
||||
fn window_event(&mut self, _: WindowEvent, rsc: &mut StdRsc<Self>) {
|
||||
update_info(rsc, self.info, self.ui_state.renderer.ui.view_count());
|
||||
app::update_info(rsc, self.info, self.ui_state.renderer.ui.view_count());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,11 @@ use iris::prelude::*;
|
||||
|
||||
#[path = "lib.rs"]
|
||||
mod app;
|
||||
use app::*;
|
||||
|
||||
#[derive(AndroidUiState)]
|
||||
struct State {
|
||||
ui_state: AndroidUiState,
|
||||
}
|
||||
|
||||
impl AndroidAppState for State {
|
||||
type Resources = StdRsc<Self>;
|
||||
}
|
||||
|
||||
#[iris::app_init]
|
||||
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
|
||||
build(rsc, &mut ui_state);
|
||||
State { ui_state }
|
||||
fn create(
|
||||
ui_state: &mut AndroidUiState,
|
||||
rsc: &mut StdRsc<AndroidUiState>,
|
||||
) {
|
||||
app::build(rsc, ui_state);
|
||||
}
|
||||
@@ -2,20 +2,7 @@ use iris::prelude::*;
|
||||
|
||||
#[path = "lib.rs"]
|
||||
mod app;
|
||||
use app::*;
|
||||
|
||||
#[derive(DesktopUiState)]
|
||||
struct State {
|
||||
ui_state: DesktopUiState,
|
||||
}
|
||||
|
||||
impl DesktopAppState for State {
|
||||
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
|
||||
build(rsc, &mut ui_state);
|
||||
Self { ui_state }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
DesktopApp::<State>::run();
|
||||
DesktopApp::<DesktopUiState>::run_with(|ui_state, rsc| app::build(rsc, ui_state));
|
||||
}
|
||||
@@ -6,7 +6,8 @@ where
|
||||
Rsc::State: FocusHost,
|
||||
{
|
||||
let rect = rect(PaintId::RED).add(rsc);
|
||||
rect.task_on(CursorSense::click(), async move |mut ctx| {
|
||||
rect.label("Toggle color")
|
||||
.task_on(CursorSense::click(), async move |mut ctx| {
|
||||
iris::task::sleep(Duration::from_secs(1)).await;
|
||||
ctx.update(move |_, rsc| {
|
||||
let rect = rect(rsc);
|
||||
|
||||
@@ -2,19 +2,11 @@ use iris::prelude::*;
|
||||
|
||||
#[path = "lib.rs"]
|
||||
mod app;
|
||||
use app::*;
|
||||
|
||||
#[derive(AndroidUiState)]
|
||||
struct State {
|
||||
ui_state: AndroidUiState,
|
||||
}
|
||||
|
||||
impl AndroidAppState for State {
|
||||
type Resources = StdRsc<Self>;
|
||||
}
|
||||
|
||||
#[iris::app_init]
|
||||
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
|
||||
build(rsc, &mut ui_state);
|
||||
State { ui_state }
|
||||
fn create(
|
||||
ui_state: &mut AndroidUiState,
|
||||
rsc: &mut StdRsc<AndroidUiState>,
|
||||
) {
|
||||
app::build(rsc, ui_state);
|
||||
}
|
||||
@@ -2,20 +2,7 @@ use iris::prelude::*;
|
||||
|
||||
#[path = "lib.rs"]
|
||||
mod app;
|
||||
use app::*;
|
||||
|
||||
#[derive(DesktopUiState)]
|
||||
struct State {
|
||||
ui_state: DesktopUiState,
|
||||
}
|
||||
|
||||
impl DesktopAppState for State {
|
||||
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
|
||||
build(rsc, &mut ui_state);
|
||||
Self { ui_state }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
DesktopApp::<State>::run();
|
||||
DesktopApp::<DesktopUiState>::run_with(|ui_state, rsc| app::build(rsc, ui_state));
|
||||
}
|
||||
+32
-13
@@ -9,13 +9,15 @@ use syn::{
|
||||
spanned::Spanned,
|
||||
};
|
||||
|
||||
/// Marks the factory called when Android creates an Iris view.
|
||||
/// Marks the initializer called when Android creates an Iris view.
|
||||
///
|
||||
/// An attribute is necessary here because the Android loader requires one
|
||||
/// exported `JNI_OnLoad` symbol and `android-view` requires a plain function
|
||||
/// pointer monomorphized for the returned application state. The generated
|
||||
/// linker and JNI glue is Android-gated; the annotated function therefore
|
||||
/// does not need its own `cfg` attribute.
|
||||
/// pointer monomorphized for the application state. A function returning a
|
||||
/// custom state remains its factory; a function with no return value receives
|
||||
/// `&mut AndroidUiState` and uses that state directly. The generated linker and
|
||||
/// JNI glue is Android-gated; the annotated function therefore does not need
|
||||
/// its own `cfg` attribute.
|
||||
#[proc_macro_attribute]
|
||||
pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
if !args.is_empty() {
|
||||
@@ -29,13 +31,9 @@ pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
|
||||
let function = parse_macro_input!(item as ItemFn);
|
||||
let name = &function.sig.ident;
|
||||
let ReturnType::Type(_, state) = &function.sig.output else {
|
||||
return Error::new(
|
||||
function.sig.output.span(),
|
||||
"an app_init function must return its application state",
|
||||
)
|
||||
.into_compile_error()
|
||||
.into();
|
||||
let (state, direct_initializer): (Type, bool) = match &function.sig.output {
|
||||
ReturnType::Default => (parse_quote!(::iris::android::AndroidUiState), true),
|
||||
ReturnType::Type(_, state) => ((**state).clone(), false),
|
||||
};
|
||||
if function.sig.inputs.len() != 2
|
||||
|| function
|
||||
@@ -46,7 +44,7 @@ pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
{
|
||||
return Error::new(
|
||||
function.sig.inputs.span(),
|
||||
"an app_init function takes AndroidUiState and &mut State::Resources",
|
||||
"an app_init function takes UI state and resources",
|
||||
)
|
||||
.into_compile_error()
|
||||
.into();
|
||||
@@ -64,6 +62,25 @@ pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
.into();
|
||||
}
|
||||
|
||||
let factory = if direct_initializer {
|
||||
quote! {
|
||||
fn init(
|
||||
mut ui_state: ::iris::android::AndroidUiState,
|
||||
rsc: &mut <#state as ::iris::android::AndroidAppState>::Resources,
|
||||
) -> #state {
|
||||
super::#name(&mut ui_state, rsc);
|
||||
ui_state
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let create = if direct_initializer {
|
||||
quote! { init }
|
||||
} else {
|
||||
quote! { super::#name }
|
||||
};
|
||||
|
||||
quote! {
|
||||
#[cfg(target_os = "android")]
|
||||
#function
|
||||
@@ -72,12 +89,14 @@ pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
mod __iris_android_app {
|
||||
use super::*;
|
||||
|
||||
#factory
|
||||
|
||||
extern "system" fn new_view_peer<'local>(
|
||||
env: ::iris::android::__private::JNIEnv<'local>,
|
||||
view: ::iris::android::__private::View<'local>,
|
||||
context: ::iris::android::__private::Context<'local>,
|
||||
) -> ::iris::android::__private::JLong {
|
||||
::iris::android::new_peer::<#state>(env, view, context, super::#name)
|
||||
::iris::android::new_peer::<#state>(env, view, context, #create)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
|
||||
@@ -25,31 +25,48 @@ application-id = "com.example.myapp"
|
||||
label = "My app"
|
||||
```
|
||||
|
||||
`#[iris::app_init]` marks the factory called when Android creates the Iris
|
||||
`#[iris::app_init]` marks the initializer called when Android creates the Iris
|
||||
view. The attribute supplies its own Android target gate and generates the JNI
|
||||
loader glue. The returned state chooses its resources through
|
||||
`AndroidAppState::Resources`; `StdRsc` is the standard bundle, not a
|
||||
requirement.
|
||||
loader glue. An application with no state beyond the UI state receives
|
||||
`AndroidUiState` directly by mutable reference:
|
||||
|
||||
```rust
|
||||
use iris::prelude::*;
|
||||
|
||||
#[derive(AndroidUiState)]
|
||||
struct State {
|
||||
ui_state: AndroidUiState,
|
||||
}
|
||||
|
||||
impl AndroidAppState for State {
|
||||
type Resources = StdRsc<Self>;
|
||||
}
|
||||
|
||||
#[iris::app_init]
|
||||
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
|
||||
rect(PaintId::RED).set_root(rsc, &mut ui_state);
|
||||
State { ui_state }
|
||||
fn create(
|
||||
ui_state: &mut AndroidUiState,
|
||||
rsc: &mut StdRsc<AndroidUiState>,
|
||||
) {
|
||||
rect(PaintId::RED).set_root(rsc, ui_state);
|
||||
}
|
||||
```
|
||||
|
||||
Desktop has the corresponding initializer form:
|
||||
|
||||
```rust
|
||||
DesktopApp::<DesktopUiState>::run_with(|ui_state, rsc| {
|
||||
rect(PaintId::RED).set_root(rsc, ui_state);
|
||||
});
|
||||
```
|
||||
|
||||
Background work updates either host through the same task context. An update
|
||||
wakes the UI thread; Iris schedules a frame automatically if the closure made
|
||||
the retained widget tree dirty:
|
||||
|
||||
```rust
|
||||
rsc.spawn_task(async move |mut ctx| {
|
||||
let text = load_text().await;
|
||||
ctx.update(move |_, rsc| label.edit(rsc).set(&text));
|
||||
});
|
||||
```
|
||||
|
||||
Applications do not need a winit event proxy or an explicit redraw request.
|
||||
|
||||
Applications with additional fields use their own state type. Its
|
||||
`AndroidAppState::Resources` associated type can also replace `StdRsc` with a
|
||||
custom resource bundle.
|
||||
|
||||
Install the Cargo subcommand from a checkout, then invoke it from the
|
||||
application's directory:
|
||||
|
||||
|
||||
@@ -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