diff --git a/Cargo.lock b/Cargo.lock index 7c47cdd21..ca9411112 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -388,6 +388,17 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "async_repeat" +version = "0.0.0" +dependencies = [ + "console_error_panic_hook", + "console_log", + "gloo-timers", + "log", + "xilem_web", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -1166,6 +1177,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 +1342,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" diff --git a/Cargo.toml b/Cargo.toml index 3bee915d1..067ff4d18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "masonry", "xilem_web", + "xilem_web/web_examples/async_repeat", "xilem_web/web_examples/counter", "xilem_web/web_examples/counter_custom_element", "xilem_web/web_examples/elm", diff --git a/xilem_web/src/concurrent/async_repeat.rs b/xilem_web/src/concurrent/async_repeat.rs new file mode 100644 index 000000000..3ede6ab4b --- /dev/null +++ b/xilem_web/src/concurrent/async_repeat.rs @@ -0,0 +1,111 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::{future::Future, marker::PhantomData}; + +use xilem_core::{DynMessage, Message, MessageResult, NoElement, View, ViewMarker}; + +use crate::{context::MessageThunk, ViewCtx}; + +pub fn async_repeat( + future_future: F, + on_event: H, +) -> AsyncRepeat +where + F: Fn(MessageThunk) -> Fut + 'static, + Fut: Future + 'static, + H: Fn(&mut State, M) -> Action + 'static, + M: Message + 'static, +{ + const { + assert!( + core::mem::size_of::() == 0, + "`async_repeat` will not be ran again when its captured variables are updated.\n\ + To ignore this warning, use `async_repeat_raw`." + ); + }; + AsyncRepeat { + future_future, + on_event, + message: PhantomData, + } +} + +pub fn async_repeat_raw( + future_future: F, + on_event: H, +) -> AsyncRepeat +where + F: Fn(MessageThunk) -> Fut + 'static, + Fut: Future + 'static, + H: Fn(&mut State, M) -> Action + 'static, + M: Message + 'static, +{ + AsyncRepeat { + future_future, + on_event, + message: PhantomData, + } +} + +pub struct AsyncRepeat { + future_future: F, + on_event: H, + message: PhantomData M>, +} + +impl ViewMarker for AsyncRepeat {} + +impl View for AsyncRepeat +where + State: 'static, + Action: 'static, + F: Fn(MessageThunk) -> Fut + 'static, + Fut: Future + 'static, + H: Fn(&mut State, M) -> Action + 'static, + M: Message + 'static, +{ + type Element = NoElement; + + type ViewState = (); + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let thunk = ctx.message_thunk(); + wasm_bindgen_futures::spawn_local((self.future_future)(thunk)); + (NoElement, ()) + } + + fn rebuild<'el>( + &self, + _: &Self, + (): &mut Self::ViewState, + _: &mut ViewCtx, + (): xilem_core::Mut<'el, Self::Element>, + ) -> xilem_core::Mut<'el, Self::Element> { + // Nothing to do + } + + fn teardown( + &self, + (): &mut Self::ViewState, + _: &mut ViewCtx, + _: xilem_core::Mut<'_, Self::Element>, + ) { + // Nothing to do + } + + fn message( + &self, + _: &mut Self::ViewState, + id_path: &[xilem_core::ViewId], + message: DynMessage, + app_state: &mut State, + ) -> xilem_core::MessageResult { + debug_assert!( + id_path.is_empty(), + "id path should be empty in AsyncRepeat::message" + ); + let message = message.downcast::().unwrap(); + MessageResult::Action((self.on_event)(app_state, *message)) + } +} diff --git a/xilem_web/src/concurrent/mod.rs b/xilem_web/src/concurrent/mod.rs index 7bfeb0b09..1cd7046e1 100644 --- a/xilem_web/src/concurrent/mod.rs +++ b/xilem_web/src/concurrent/mod.rs @@ -3,6 +3,10 @@ //! Async views, allowing concurrent operations, like fetching data from a server +mod async_repeat; mod memoized_await; -pub use memoized_await::{memoized_await, MemoizedAwait}; +pub use self::{ + async_repeat::{async_repeat, async_repeat_raw, AsyncRepeat}, + memoized_await::{memoized_await, MemoizedAwait}, +}; diff --git a/xilem_web/web_examples/async_repeat/Cargo.toml b/xilem_web/web_examples/async_repeat/Cargo.toml new file mode 100644 index 000000000..0f7599ee9 --- /dev/null +++ b/xilem_web/web_examples/async_repeat/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "async_repeat" +version = "0.0.0" # not versioned +publish = false +license.workspace = true +edition.workspace = true + +[lints] +workspace = true + +[dependencies] +console_error_panic_hook = "0.1.7" +console_log = { version = "1.0.0", features = ["color"] } +gloo-timers = { version = "0.3.0", features = ["futures"] } +log = "0.4.22" +xilem_web = { path = "../.." } diff --git a/xilem_web/web_examples/async_repeat/index.html b/xilem_web/web_examples/async_repeat/index.html new file mode 100644 index 000000000..9a250aa88 --- /dev/null +++ b/xilem_web/web_examples/async_repeat/index.html @@ -0,0 +1,7 @@ + + + + Async repeat example | Xilem Web + + + diff --git a/xilem_web/web_examples/async_repeat/src/main.rs b/xilem_web/web_examples/async_repeat/src/main.rs new file mode 100644 index 000000000..08d5da416 --- /dev/null +++ b/xilem_web/web_examples/async_repeat/src/main.rs @@ -0,0 +1,43 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use gloo_timers::future::TimeoutFuture; +use xilem_web::{ + concurrent::async_repeat_raw, core::fork, document_body, elements::html, interfaces::Element, + App, +}; + +#[derive(Default)] +struct AppState { + ping_count: usize, +} + +#[derive(Debug)] +enum Message { + Ping, +} + +fn app_logic(state: &mut AppState) -> impl Element { + let task = async_repeat_raw( + |thunk| async move { + loop { + TimeoutFuture::new(1_000).await; + thunk.push_message(Message::Ping); + } + }, + |state: &mut AppState, message: Message| match message { + Message::Ping => { + state.ping_count += 1; + } + }, + ); + + fork(html::div(format!("Ping count: {}", state.ping_count)), task) +} + +pub fn main() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + log::info!("Start web application"); + App::new(document_body(), AppState::default(), app_logic).run(); +}