From 43aca95e3c8bc4afe4874b24aea14df127980345 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Sat, 27 Jul 2024 17:50:37 +0200 Subject: [PATCH 1/2] xilem_web: Add a `await_once` view, and a simple example `suspense` showing it in action. --- Cargo.lock | 32 ++++++ Cargo.toml | 1 + xilem_web/src/concurrent/await_once.rs | 118 ++++++++++++++++++++ xilem_web/src/concurrent/mod.rs | 5 +- xilem_web/web_examples/fetch/index.html | 65 +++++------ xilem_web/web_examples/suspense/Cargo.toml | 16 +++ xilem_web/web_examples/suspense/index.html | 28 +++++ xilem_web/web_examples/suspense/src/main.rs | 35 ++++++ 8 files changed, 267 insertions(+), 33 deletions(-) create mode 100644 xilem_web/src/concurrent/await_once.rs create mode 100644 xilem_web/web_examples/suspense/Cargo.toml create mode 100644 xilem_web/web_examples/suspense/index.html create mode 100644 xilem_web/web_examples/suspense/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 4f8749684..f830a1fe8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1166,6 +1166,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.30" @@ -1322,6 +1331,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gloo-utils" version = "0.2.0" @@ -2920,6 +2941,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "suspense" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "gloo-timers", + "wasm-bindgen", + "web-sys", + "xilem_web", +] + [[package]] name = "svg_fmt" version = "0.4.3" diff --git a/Cargo.toml b/Cargo.toml index 35b33f432..c95baf346 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "xilem_web/web_examples/fetch", "xilem_web/web_examples/todomvc", "xilem_web/web_examples/mathml_svg", + "xilem_web/web_examples/suspense", "xilem_web/web_examples/svgtoy", ] diff --git a/xilem_web/src/concurrent/await_once.rs b/xilem_web/src/concurrent/await_once.rs new file mode 100644 index 000000000..9c4cc5aca --- /dev/null +++ b/xilem_web/src/concurrent/await_once.rs @@ -0,0 +1,118 @@ +// Copyright 2024 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::{future::Future, marker::PhantomData}; + +use wasm_bindgen::UnwrapThrowExt; +use wasm_bindgen_futures::spawn_local; +use xilem_core::{MessageResult, Mut, NoElement, View, ViewId}; + +use crate::{DynMessage, OptionalAction, ViewCtx}; + +/// Await a future returned by `init_future`, `callback` is called with the output of the future. `init_future` will only be invoked once. Use [`await_once`] for construction of this [`View`] +pub struct AwaitOnce { + init_future: InitFuture, + callback: Callback, + phantom: PhantomData (State, Action)>, +} + +/// Await a future returned by `init_future`, `callback` is called with the output of the future. `init_future` will only be invoked once. +/// +/// # Examples +/// +/// ``` +/// use xilem_web::{core::fork, concurrent::await_once, elements::html::div, interfaces::Element}; +/// +/// fn app_logic(state: &mut i32) -> impl Element { +/// fork( +/// div(*state), +/// await_once( +/// |_state: &mut i32| std::future::ready(42), +/// |state: &mut i32, meaning_of_life| *state = meaning_of_life, +/// ) +/// ) +/// } +/// ``` +pub fn await_once( + init_future: InitFuture, + callback: Callback, +) -> AwaitOnce +where + State: 'static, + Action: 'static, + FOut: std::fmt::Debug + 'static, + F: Future + 'static, + InitFuture: Fn(&mut State) -> F + 'static, + OA: OptionalAction + 'static, + Callback: Fn(&mut State, FOut) -> OA + 'static, +{ + AwaitOnce { + init_future, + callback, + phantom: PhantomData, + } +} + +pub struct AwaitOnceState { + future: Option, +} + +impl View + for AwaitOnce +where + State: 'static, + Action: 'static, + FOut: std::fmt::Debug + 'static, + F: Future + 'static, + InitFuture: Fn(&mut State) -> F + 'static, + OA: OptionalAction + 'static, + Callback: Fn(&mut State, FOut) -> OA + 'static, +{ + type Element = NoElement; + + type ViewState = AwaitOnceState; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let thunk = ctx.message_thunk(); + // we can't directly push the initial message, as this would be executed directly (not in the next microtick), which in turn means that the already mutably borrowed `App` would be borrowed again. + // So we have to delay this with a spawn_local + spawn_local(async move { thunk.push_message(None::) }); + (NoElement, AwaitOnceState { future: None }) + } + + fn rebuild<'el>( + &self, + _prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + (): Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + if let Some(future) = view_state.future.take() { + let thunk = ctx.message_thunk(); + spawn_local(async move { thunk.push_message(Some(future.await)) }); + } + } + + fn teardown(&self, _: &mut Self::ViewState, _: &mut ViewCtx, _: Mut<'_, Self::Element>) {} + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + assert!(id_path.is_empty()); // `debug_assert!` instead? to save some bytes in the release binary? + match *message.downcast().unwrap_throw() { + Some(future_output) => match (self.callback)(app_state, future_output).action() { + Some(action) => MessageResult::Action(action), + None => MessageResult::Nop, + }, + // Initial trigger in build, invoke the init_future and spawn it in `View::rebuild` + None => { + view_state.future = Some((self.init_future)(app_state)); + MessageResult::RequestRebuild + } + } + } +} diff --git a/xilem_web/src/concurrent/mod.rs b/xilem_web/src/concurrent/mod.rs index 7bfeb0b09..61d8b7057 100644 --- a/xilem_web/src/concurrent/mod.rs +++ b/xilem_web/src/concurrent/mod.rs @@ -3,6 +3,9 @@ //! Async views, allowing concurrent operations, like fetching data from a server -mod memoized_await; +mod await_once; +pub use await_once::{await_once, AwaitOnce}; +mod memoized_await; pub use memoized_await::{memoized_await, MemoizedAwait}; + diff --git a/xilem_web/web_examples/fetch/index.html b/xilem_web/web_examples/fetch/index.html index e76821ab1..d19abf576 100644 --- a/xilem_web/web_examples/fetch/index.html +++ b/xilem_web/web_examples/fetch/index.html @@ -3,43 +3,44 @@ - - + + diff --git a/xilem_web/web_examples/suspense/Cargo.toml b/xilem_web/web_examples/suspense/Cargo.toml new file mode 100644 index 000000000..369ec75b5 --- /dev/null +++ b/xilem_web/web_examples/suspense/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "suspense" +version = "0.1.0" +publish = false +license.workspace = true +edition.workspace = true + +[lints] +workspace = true + +[dependencies] +console_error_panic_hook = "0.1" +wasm-bindgen = "0.2.92" +web-sys = "0.3.69" +xilem_web = { path = "../.." } +gloo-timers = { version = "0.3.0", features = ["futures"] } diff --git a/xilem_web/web_examples/suspense/index.html b/xilem_web/web_examples/suspense/index.html new file mode 100644 index 000000000..dc633844f --- /dev/null +++ b/xilem_web/web_examples/suspense/index.html @@ -0,0 +1,28 @@ + + + + + Suspense | Xilem Web + + + + + + \ No newline at end of file diff --git a/xilem_web/web_examples/suspense/src/main.rs b/xilem_web/web_examples/suspense/src/main.rs new file mode 100644 index 000000000..2432a291e --- /dev/null +++ b/xilem_web/web_examples/suspense/src/main.rs @@ -0,0 +1,35 @@ +// Copyright 2023 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use gloo_timers::future::TimeoutFuture; +use xilem_web::{ + concurrent::await_once, + core::{fork, one_of::Either}, + document_body, + elements::html::{h1, p}, + interfaces::Element, + App, +}; + +fn app_logic(view_has_resolved: &mut bool) -> impl Element { + let view = if !*view_has_resolved { + Either::A(p("This will be replaced soon")) + } else { + Either::B(h1("The time has come for fanciness")) + }; + fork( + // note how the `Class` view is applied to either the p or the h1 element + view.class(view_has_resolved.then_some("blink")), + await_once( + |_| TimeoutFuture::new(5000), + |view_has_resolved: &mut bool, _| { + *view_has_resolved = true; + }, + ), + ) +} + +pub fn main() { + console_error_panic_hook::set_once(); + App::new(document_body(), false, app_logic).run(); +} From 74bbc35477c9b6717dc32c6b14310989c0dce2b2 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Sat, 27 Jul 2024 18:15:39 +0200 Subject: [PATCH 2/2] cargo fmt --- xilem_web/src/concurrent/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/xilem_web/src/concurrent/mod.rs b/xilem_web/src/concurrent/mod.rs index 61d8b7057..1a75dc521 100644 --- a/xilem_web/src/concurrent/mod.rs +++ b/xilem_web/src/concurrent/mod.rs @@ -8,4 +8,3 @@ pub use await_once::{await_once, AwaitOnce}; mod memoized_await; pub use memoized_await::{memoized_await, MemoizedAwait}; -