From 99d61603d26a020d5d5f75f3113a0e1f1b5ac5e2 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Mon, 5 Aug 2024 15:36:18 +0200 Subject: [PATCH] xilem_web: Add `interval` `View`, which somewhat reflects `setInterval` (#486) Simple but convenient `View`, e.g. for some kind of ticker. (Maybe needs an additional example) --- xilem_web/src/concurrent/interval.rs | 148 +++++++++++++++++++++++++++ xilem_web/src/concurrent/mod.rs | 4 +- 2 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 xilem_web/src/concurrent/interval.rs diff --git a/xilem_web/src/concurrent/interval.rs b/xilem_web/src/concurrent/interval.rs new file mode 100644 index 000000000..8b1161581 --- /dev/null +++ b/xilem_web/src/concurrent/interval.rs @@ -0,0 +1,148 @@ +// Copyright 2024 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::marker::PhantomData; + +use wasm_bindgen::{closure::Closure, JsCast, UnwrapThrowExt}; +use xilem_core::{MessageResult, Mut, NoElement, View, ViewId, ViewMarker}; + +use crate::{DynMessage, OptionalAction, ViewCtx}; + +/// Start an interval which invokes `callback` every `ms` milliseconds +pub struct Interval { + ms: u32, + callback: Callback, + phantom: PhantomData (State, Action)>, +} + +/// Start an interval which invokes `callback` every `ms` milliseconds +/// +/// Currently, when `ms` changes, the previous interval is cleared, and starts with the new interval. +/// This default behavior may change in the future, and may even be configurable. +/// +/// # Examples +/// +/// ``` +/// use xilem_web::{core::fork, concurrent::interval, elements::html::div, interfaces::Element}; +/// +/// fn timer(seconds: &mut u32) -> impl Element { +/// fork( +/// div(format!("{seconds} seconds have passed, since creating this view")), +/// interval( +/// 1000, // in ms, when this changes, the interval is reset +/// |seconds: &mut u32| *seconds += 1, +/// ) +/// ) +/// } +/// ``` +/// +/// # Panics +/// +/// While `ms` is a `u32`, `setInterval` actually requires this to be a `i32`, so values above `2147483647` lead to a panic. +/// See for more details. +pub fn interval( + ms: u32, + callback: Callback, +) -> Interval +where + State: 'static, + Action: 'static, + OA: OptionalAction + 'static, + Callback: Fn(&mut State) -> OA + 'static, +{ + Interval { + ms, + callback, + phantom: PhantomData, + } +} + +pub struct IntervalState { + // Closures are retained so they can be called by environment + interval_fn: Closure, + interval_handle: i32, +} + +fn start_interval(callback: &Closure, ms: u32) -> i32 { + web_sys::window() + .unwrap_throw() + .set_interval_with_callback_and_timeout_and_arguments_0( + callback.as_ref().unchecked_ref(), + ms.try_into().expect_throw( + "`setInterval` requires this to be an `i32`,\ + which is why values above `2147483647` are not possible,\ + see https://developer.mozilla.org/en-US/docs/Web/API/setInterval#sect2 \ + for more details", + ), + ) + .unwrap_throw() +} + +fn clear_interval(handle: i32) { + web_sys::window() + .unwrap_throw() + .clear_interval_with_handle(handle); +} + +impl ViewMarker for Interval {} + +impl View + for Interval +where + State: 'static, + Action: 'static, + OA: OptionalAction + 'static, + Callback: Fn(&mut State) -> OA + 'static, +{ + type Element = NoElement; + + type ViewState = IntervalState; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let thunk = ctx.message_thunk(); + let interval_fn = Closure::new(move || thunk.push_message(())); + let state = IntervalState { + interval_handle: start_interval(&interval_fn, self.ms), + interval_fn, + }; + + (NoElement, state) + } + + fn rebuild<'el>( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + _: &mut ViewCtx, + (): Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + if prev.ms != self.ms { + clear_interval(view_state.interval_handle); + view_state.interval_handle = start_interval(&view_state.interval_fn, self.ms); + } + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + _: &mut ViewCtx, + _: Mut<'_, Self::Element>, + ) { + clear_interval(view_state.interval_handle); + } + + fn message( + &self, + _: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + debug_assert!(id_path.is_empty()); + message.downcast::<()>().unwrap_throw(); + match (self.callback)(app_state).action() { + Some(action) => MessageResult::Action(action), + None => MessageResult::Nop, + } + } +} diff --git a/xilem_web/src/concurrent/mod.rs b/xilem_web/src/concurrent/mod.rs index 7bfeb0b09..84fa07493 100644 --- a/xilem_web/src/concurrent/mod.rs +++ b/xilem_web/src/concurrent/mod.rs @@ -3,6 +3,8 @@ //! Async views, allowing concurrent operations, like fetching data from a server -mod memoized_await; +mod interval; +pub use interval::{interval, Interval}; +mod memoized_await; pub use memoized_await::{memoized_await, MemoizedAwait};