-
Notifications
You must be signed in to change notification settings - Fork 124
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
Philipp-M
wants to merge
6
commits into
linebender:main
Choose a base branch
from
Philipp-M:xilem_web-event-handler
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 5 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
b20b788
xilem_web: Abstract callback of OnEvent-like views away with an `Even…
Philipp-M 2081652
Add a `defer` `EventHandler` to xilem_web
Philipp-M 178e2b9
xilem_web: Make interfaces generic over `EventHandler` and 'fix' the …
Philipp-M e7a8e3c
xilem_web: Add fetch example (modified version of #427)
Philipp-M 8db04ab
Fix CI (tests, 'typo', license header)
Philipp-M c0adafd
Merge branch 'main' into xilem_web-event-handler
Philipp-M File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
CF: Fn(&mut State, FOut) -> Action + 'static, | ||
{ | ||
DeferEventHandler { | ||
phantom: PhantomData, | ||
future_fn, | ||
callback_fn, | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
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.
There was a problem hiding this comment.
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 aFuture
?There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes