Skip to content

Commit

Permalink
xilem_web: Add interval View, which somewhat reflects `setInterva…
Browse files Browse the repository at this point in the history
…l` (#486)

Simple but convenient `View`, e.g. for some kind of ticker. (Maybe needs
an additional example)
  • Loading branch information
Philipp-M authored Aug 5, 2024
1 parent bb13f1a commit 99d6160
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 1 deletion.
148 changes: 148 additions & 0 deletions xilem_web/src/concurrent/interval.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
4 changes: 3 additions & 1 deletion xilem_web/src/concurrent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

0 comments on commit 99d6160

Please sign in to comment.