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)
This commit is contained in:
Philipp Mildenberger 2024-08-05 15:36:18 +02:00 committed by GitHub
parent bb13f1a760
commit 99d61603d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 151 additions and 1 deletions

View File

@ -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<Callback, State, Action> {
ms: u32,
callback: Callback,
phantom: PhantomData<fn() -> (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<u32> {
/// 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 <https://developer.mozilla.org/en-US/docs/Web/API/setInterval#sect2> for more details.
pub fn interval<State, Action, OA, Callback>(
ms: u32,
callback: Callback,
) -> Interval<Callback, State, Action>
where
State: 'static,
Action: 'static,
OA: OptionalAction<Action> + '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<dyn FnMut()>,
interval_handle: i32,
}
fn start_interval(callback: &Closure<dyn FnMut()>, 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<Callback, State, Action> ViewMarker for Interval<Callback, State, Action> {}
impl<State, Action, Callback, OA> View<State, Action, ViewCtx, DynMessage>
for Interval<Callback, State, Action>
where
State: 'static,
Action: 'static,
OA: OptionalAction<Action> + '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<Action, DynMessage> {
debug_assert!(id_path.is_empty());
message.downcast::<()>().unwrap_throw();
match (self.callback)(app_state).action() {
Some(action) => MessageResult::Action(action),
None => MessageResult::Nop,
}
}
}

View File

@ -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};