Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

xilem_web: Add EventHandler trait and an additional defer event handler, shown in a new fetch example #440

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"xilem_web/web_examples/counter",
"xilem_web/web_examples/counter_custom_element",
"xilem_web/web_examples/elm",
"xilem_web/web_examples/fetch",
"xilem_web/web_examples/todomvc",
"xilem_web/web_examples/mathml_svg",
"xilem_web/web_examples/svgtoy",
Expand Down
1 change: 1 addition & 0 deletions xilem_web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ workspace = true
xilem_core = { workspace = true, features = ["kurbo"] }
peniko.workspace = true
wasm-bindgen = "0.2.92"
wasm-bindgen-futures = "0.4.42"

[dependencies.web-sys]
version = "0.3.69"
Expand Down
167 changes: 167 additions & 0 deletions xilem_web/src/event_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0

use std::{future::Future, marker::PhantomData, rc::Rc};

use wasm_bindgen::UnwrapThrowExt;
use wasm_bindgen_futures::spawn_local;
use xilem_core::{MessageResult, ViewPathTracker};

use crate::{context::MessageThunk, DynMessage, Message, ViewCtx};

pub enum EventHandlerMessage<E, Message = DynMessage> {
Event(E),
Message(Message),
}

pub trait EventHandler<Event, State, Action, Context: ViewPathTracker, Message = DynMessage>:
'static
{
/// State that is used over the lifetime of the retained representation of the event handler.
///
/// This often means routing information for messages to child event handlers or state for async handlers,
type State;

/// Init and create the corresponding state.
fn build(&self, ctx: &mut Context) -> Self::State;

/// Update handler state based on the difference between `self` and `prev`.
fn rebuild(&self, prev: &Self, event_handler_state: &mut Self::State, ctx: &mut Context);

/// Cleanup the handler, when it's being removed from the tree.
///
/// The main use-cases of this method are to:
/// - Cancel any async tasks
/// - Clean up any book-keeping set-up in `build` and `rebuild`
// TODO: Should this take ownership of the `EventHandlerState`
// We have chosen not to because it makes swapping versions more awkward
fn teardown(&self, event_handler_state: &mut Self::State, ctx: &mut Context);

/// Route `message` to `id_path`, if that is still a valid path.
fn message(
&self,
event_handler_state: &mut Self::State,
id_path: &[xilem_core::ViewId],
message: EventHandlerMessage<Event, Message>,
app_state: &mut State,
) -> MessageResult<Action, EventHandlerMessage<Event, Message>>;
}

// Because of intersecting trait impls with the blanket impl below, the following impl is unfortunately not possible:
//
// `impl<State, Action, F: Fn(&mut State) -> Action> EventHandler<(), State, Action, ViewCtx> for F {}`
//
// A workaround for this would be to "hardcode" event types, instead of using a blanket impl.
// This is fortunately not a big issue in xilem_web, because there's AFAIK always an event payload (i.e. something different than `()`)

impl<State, Action, Event, Context, Message, F> EventHandler<Event, State, Action, Context, Message>
for F
where
Context: ViewPathTracker,
F: Fn(&mut State, Event) -> Action + 'static,
{
type State = ();

fn build(&self, _ctx: &mut Context) -> Self::State {}

fn rebuild(&self, _prev: &Self, _event_handler_state: &mut Self::State, _ctx: &mut Context) {}

fn teardown(&self, _event_handler_state: &mut Self::State, _ctx: &mut Context) {}

fn message(
&self,
_event_handler_state: &mut Self::State,
id_path: &[xilem_core::ViewId],
message: EventHandlerMessage<Event, Message>,
app_state: &mut State,
) -> MessageResult<Action, EventHandlerMessage<Event, Message>> {
debug_assert!(id_path.is_empty());
match message {
EventHandlerMessage::Event(event) => MessageResult::Action(self(app_state, event)),
EventHandlerMessage::Message(_) => unreachable!(),
}
}
}

#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DeferEventHandler<State, Action, FOut, F, FF, CF> {
#[allow(clippy::complexity)]
phantom: PhantomData<fn() -> (State, Action, FOut, F)>,
future_fn: FF,
callback_fn: CF,
}

impl<State, Action, Event, FOut, F, FF, CF> EventHandler<Event, State, Action, ViewCtx>
for DeferEventHandler<State, Action, FOut, F, FF, CF>
where
State: 'static,
Action: 'static,
Event: 'static,
FOut: Message,
F: Future<Output = FOut> + 'static,
FF: Fn(&mut State, Event) -> F + 'static,
CF: Fn(&mut State, FOut) -> Action + 'static,
{
type State = Rc<MessageThunk>;

fn build(&self, ctx: &mut ViewCtx) -> Self::State {
Rc::new(ctx.message_thunk())
}

fn rebuild(&self, _prev: &Self, _event_handler_state: &mut Self::State, _ctx: &mut ViewCtx) {}

fn teardown(&self, _event_handler_state: &mut Self::State, _ctx: &mut ViewCtx) {}

fn message(
&self,
message_thunk: &mut Self::State,
id_path: &[xilem_core::ViewId],
message: EventHandlerMessage<Event>,
app_state: &mut State,
) -> MessageResult<Action, EventHandlerMessage<Event>> {
debug_assert!(id_path.is_empty());
match message {
EventHandlerMessage::Event(event) => {
let future = (self.future_fn)(app_state, event);
let thunk = Rc::clone(message_thunk);
// TODO currently, multiple events could trigger this, while the (old) future is still not resolved
// This may be intended, but can also lead to surprising behavior.
// We could add an atomic boolean, to avoid this, i.e. either block a new future,
// or even queue it after the first future being resolved, there may also be other possible desired behaviors
// This could also be made configurable, e.g. via the builder pattern, like this:
// ```
// defer(...)
// .block() // block new events triggering that future
// .once() // allow this event to trigger the future only once.
// .queue() // queue additional triggered futures
// ```
spawn_local(async move { thunk.push_message(future.await) });
MessageResult::RequestRebuild
}
EventHandlerMessage::Message(output) => MessageResult::Action((self.callback_fn)(
app_state,
*output.downcast::<FOut>().unwrap_throw(),
)),
}
}
}

pub fn defer<State, Action, Event, FOut, F, FF, CF>(
future_fn: FF,
callback_fn: CF,
) -> DeferEventHandler<State, Action, FOut, F, FF, CF>
where
State: 'static,
Action: 'static,
Event: 'static,
FOut: Message,
F: Future<Output = FOut> + 'static,
FF: Fn(&mut State, Event) -> F + 'static,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could also be

Suggested change
FF: Fn(&mut State, Event) -> F + 'static,
FF: Fn(&mut State, Event) -> Option<F> + 'static,

which would give the user more control, if they want to handle the event or not, thus maybe make the blocking/once stuff above a non issue, as the user could handle this with more control.

I think the parallel/serial(queue) thing explained in the comment above may still be useful though for configuration.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why would you call defer if you do not want to return a Future?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... it would be like this, right?

|state, ev|{
  if state.value == some_value {
    None
  } else {
    Some(async { /* */ })
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

CF: Fn(&mut State, FOut) -> Action + 'static,
{
DeferEventHandler {
phantom: PhantomData,
future_fn,
callback_fn,
}
}
Loading