From b20b78862e9ced96bd691528bb41c071d761b769 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Sat, 20 Jul 2024 16:06:52 +0200 Subject: [PATCH 1/5] xilem_web: Abstract callback of OnEvent-like views away with an `EventHandler` --- xilem_web/src/event_handler.rs | 77 ++++++++++++++++ xilem_web/src/events.rs | 158 +++++++++++++++++++++------------ xilem_web/src/interfaces.rs | 4 +- xilem_web/src/lib.rs | 2 + 4 files changed, 181 insertions(+), 60 deletions(-) create mode 100644 xilem_web/src/event_handler.rs diff --git a/xilem_web/src/event_handler.rs b/xilem_web/src/event_handler.rs new file mode 100644 index 000000000..f2ec7b0a1 --- /dev/null +++ b/xilem_web/src/event_handler.rs @@ -0,0 +1,77 @@ +use xilem_core::{MessageResult, ViewPathTracker}; + +use crate::DynMessage; + +pub enum EventHandlerMessage { + Event(E), + Message(Message), +} + +pub trait EventHandler: + '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, + app_state: &mut State, + ) -> MessageResult>; +} + +// Because of intersecting trait impls with the blanket impl below, the following impl is unfortunately not possible: +// +// `impl 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 EventHandler + 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, + app_state: &mut State, + ) -> MessageResult> { + debug_assert!(id_path.is_empty()); + match message { + EventHandlerMessage::Event(event) => MessageResult::Action(self(app_state, event)), + EventHandlerMessage::Message(_) => unreachable!(), + } + } +} diff --git a/xilem_web/src/events.rs b/xilem_web/src/events.rs index 871b940f1..12e001274 100644 --- a/xilem_web/src/events.rs +++ b/xilem_web/src/events.rs @@ -6,27 +6,27 @@ use wasm_bindgen::{prelude::Closure, throw_str, JsCast, UnwrapThrowExt}; use web_sys::AddEventListenerOptions; use xilem_core::{MessageResult, Mut, View, ViewId, ViewPathTracker}; -use crate::{DynMessage, ElementAsRef, OptionalAction, ViewCtx}; +use crate::{DynMessage, ElementAsRef, EventHandler, EventHandlerMessage, OptionalAction, ViewCtx}; /// Wraps a [`View`] `V` and attaches an event listener. /// /// The event type `Event` should inherit from [`web_sys::Event`] #[derive(Clone, Debug)] -pub struct OnEvent { +pub struct OnEvent { pub(crate) element: V, pub(crate) event: Cow<'static, str>, pub(crate) capture: bool, pub(crate) passive: bool, - pub(crate) handler: Callback, + pub(crate) handler: Handler, #[allow(clippy::type_complexity)] - pub(crate) phantom_event_ty: PhantomData (State, Action, Event)>, + pub(crate) phantom_event_ty: PhantomData (State, Action, OA, Event)>, } -impl OnEvent +impl OnEvent where Event: JsCast + 'static, { - pub fn new(element: V, event: impl Into>, handler: Callback) -> Self { + pub fn new(element: V, event: impl Into>, handler: Handler) -> Self { OnEvent { element, event: event.into(), @@ -107,62 +107,75 @@ fn remove_event_listener( } /// State for the `OnEvent` view. -pub struct OnEventState { +pub struct OnEventState { #[allow(unused)] - child_state: S, + child_state: CS, + handler_state: HS, callback: Closure, } // These (boilerplatey) functions are there to reduce the boilerplate created by the macro-expansion below. -fn build_event_listener( +fn build_event_listener( element_view: &V, + event_handler: &Handler, event: &str, capture: bool, passive: bool, ctx: &mut ViewCtx, -) -> (V::Element, OnEventState) +) -> (V::Element, OnEventState) where State: 'static, Action: 'static, + OA: OptionalAction, V: View, V::Element: ElementAsRef, Event: JsCast + 'static + crate::Message, + Handler: EventHandler, { - // we use a placeholder id here, the id can never change, so we don't need to store it anywhere - ctx.with_id(ViewId::new(0), |ctx| { + let handler_state = ctx.with_id(ViewId::new(0), |ctx| event_handler.build(ctx)); + let (element, (child_state, callback)) = ctx.with_id(ViewId::new(1), |ctx| { let (element, child_state) = element_view.build(ctx); let callback = create_event_listener::(element.as_ref(), event, capture, passive, ctx); - let state = OnEventState { - child_state, - callback, - }; - (element, state) - }) + (element, (child_state, callback)) + }); + let state = OnEventState { + child_state, + handler_state, + callback, + }; + (element, state) } #[allow(clippy::too_many_arguments)] -fn rebuild_event_listener<'el, State, Action, V, Event>( +fn rebuild_event_listener<'el, State, Action, OA, Handler, V, Event>( element_view: &V, prev_element_view: &V, + event_handler: &Handler, + prev_event_handler: &Handler, element: Mut<'el, V::Element>, event: &str, capture: bool, passive: bool, prev_capture: bool, prev_passive: bool, - state: &mut OnEventState, + state: &mut OnEventState, ctx: &mut ViewCtx, ) -> Mut<'el, V::Element> where State: 'static, Action: 'static, + OA: OptionalAction, V: View, V::Element: ElementAsRef, Event: JsCast + 'static + crate::Message, + Handler: EventHandler, { ctx.with_id(ViewId::new(0), |ctx| { + event_handler.rebuild(prev_event_handler, &mut state.handler_state, ctx); + }); + ctx.with_id(ViewId::new(1), |ctx| { if prev_capture != capture || prev_passive != passive { remove_event_listener(element.as_ref(), event, &state.callback, prev_capture); @@ -173,33 +186,39 @@ where }) } -fn teardown_event_listener( +fn teardown_event_listener( element_view: &V, + event_handler: &Handler, element: Mut<'_, V::Element>, _event: &str, - state: &mut OnEventState, + state: &mut OnEventState, _capture: bool, ctx: &mut ViewCtx, ) where State: 'static, Action: 'static, + OA: OptionalAction, V: View, V::Element: ElementAsRef, + Handler: EventHandler, { + ctx.with_id(ViewId::new(0), |ctx| { + event_handler.teardown(&mut state.handler_state, ctx); + }); // TODO: is this really needed (as the element will be removed anyway)? // remove_event_listener(element.as_ref(), event, &state.callback, capture); - ctx.with_id(ViewId::new(0), |ctx| { + ctx.with_id(ViewId::new(1), |ctx| { element_view.teardown(&mut state.child_state, ctx, element); }); } -fn message_event_listener( +fn message_event_listener( element_view: &V, - state: &mut OnEventState, + handler: &Handler, + state: &mut OnEventState, id_path: &[ViewId], message: DynMessage, app_state: &mut State, - handler: &Callback, ) -> MessageResult where State: 'static, @@ -208,43 +227,58 @@ where V::Element: ElementAsRef, Event: JsCast + 'static + crate::Message, OA: OptionalAction, - Callback: Fn(&mut State, Event) -> OA + 'static, + Handler: EventHandler + 'static, { let Some((first, remainder)) = id_path.split_first() else { throw_str("Parent view of `OnEvent` sent outdated and/or incorrect empty view path"); }; - if first.routing_id() != 0 { - throw_str("Parent view of `OnEvent` sent outdated and/or incorrect empty view path"); - } - if remainder.is_empty() { - let event = message.downcast::().unwrap_throw(); - match (handler)(app_state, *event).action() { - Some(a) => MessageResult::Action(a), - None => MessageResult::Nop, + let handler_message = if first.routing_id() == 0 { + EventHandlerMessage::Message(message) + } else if first.routing_id() == 1 { + if remainder.is_empty() { + EventHandlerMessage::Event(*message.downcast::().unwrap_throw()) + } else { + return element_view.message(&mut state.child_state, remainder, message, app_state); } } else { - element_view.message(&mut state.child_state, remainder, message, app_state) + throw_str("Parent view of `OnEvent` sent outdated and/or incorrect empty view path"); + }; + + match handler + .message(&mut state.handler_state, &[], handler_message, app_state) + .map(|action| action.action()) + { + MessageResult::Action(Some(action)) => MessageResult::Action(action), + MessageResult::Nop | MessageResult::Action(None) => MessageResult::Nop, + MessageResult::RequestRebuild => MessageResult::RequestRebuild, + MessageResult::Stale(EventHandlerMessage::Event(event)) => { + MessageResult::Stale(Box::new(event)) + } + MessageResult::Stale(EventHandlerMessage::Message(message)) => { + MessageResult::Stale(message) + } } } -impl View - for OnEvent +impl View + for OnEvent where State: 'static, Action: 'static, - OA: OptionalAction, - Callback: Fn(&mut State, Event) -> OA + 'static, + OA: OptionalAction + 'static, + Handler: EventHandler, V: View, V::Element: ElementAsRef, Event: JsCast + 'static + crate::Message, { - type ViewState = OnEventState; + type ViewState = OnEventState; type Element = V::Element; fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { - build_event_listener::<_, _, _, Event>( + build_event_listener::<_, _, _, _, _, Event>( &self.element, + &self.handler, &self.event, self.capture, self.passive, @@ -259,8 +293,12 @@ where ctx: &mut ViewCtx, element: Mut<'el, Self::Element>, ) -> Mut<'el, Self::Element> { - // special case, where event name can change, so we can't reuse the rebuild_event_listener function above ctx.with_id(ViewId::new(0), |ctx| { + self.handler + .rebuild(&prev.handler, &mut view_state.handler_state, ctx); + }); + // special case, where event name can change, so we can't reuse the rebuild_event_listener function above + ctx.with_id(ViewId::new(1), |ctx| { if prev.capture != self.capture || prev.passive != self.passive || prev.event != self.event @@ -293,6 +331,7 @@ where ) { teardown_event_listener( &self.element, + &self.handler, element, &self.event, view_state, @@ -310,11 +349,11 @@ where ) -> MessageResult { message_event_listener( &self.element, + &self.handler, view_state, id_path, message, app_state, - &self.handler, ) } } @@ -322,16 +361,16 @@ where macro_rules! event_definitions { ($(($ty_name:ident, $event_name:literal, $web_sys_ty:ident)),*) => { $( - pub struct $ty_name { + pub struct $ty_name { pub(crate) element: V, pub(crate) capture: bool, pub(crate) passive: bool, - pub(crate) handler: Callback, - pub(crate) phantom_event_ty: PhantomData (State, Action)>, + pub(crate) handler: Handler, + pub(crate) phantom_event_ty: PhantomData (State, Action, OA)>, } - impl $ty_name { - pub fn new(element: V, handler: Callback) -> Self { + impl $ty_name { + pub fn new(element: V, handler: Handler) -> Self { Self { element, passive: true, @@ -365,23 +404,24 @@ macro_rules! event_definitions { } - impl View - for $ty_name + impl View + for $ty_name where State: 'static, Action: 'static, - OA: OptionalAction, - Callback: Fn(&mut State, web_sys::$web_sys_ty) -> OA + 'static, + OA: OptionalAction + 'static, + Handler: EventHandler, V: View, V::Element: ElementAsRef, { - type ViewState = OnEventState; + type ViewState = OnEventState; type Element = V::Element; fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { - build_event_listener::<_, _, _, web_sys::$web_sys_ty>( + build_event_listener::<_, _, _, _, _, web_sys::$web_sys_ty>( &self.element, + &self.handler, $event_name, self.capture, self.passive, @@ -396,9 +436,11 @@ macro_rules! event_definitions { ctx: &mut ViewCtx, element: Mut<'el, Self::Element>, ) -> Mut<'el, Self::Element> { - rebuild_event_listener::<_, _, _, web_sys::$web_sys_ty>( + rebuild_event_listener::<_, _, _, _, _, web_sys::$web_sys_ty>( &self.element, &prev.element, + &self.handler, + &prev.handler, element, $event_name, self.capture, @@ -416,7 +458,7 @@ macro_rules! event_definitions { ctx: &mut ViewCtx, element: Mut<'_, Self::Element>, ) { - teardown_event_listener(&self.element, element, $event_name, view_state, self.capture, ctx); + teardown_event_listener(&self.element, &self.handler, element, $event_name, view_state, self.capture, ctx); } fn message( @@ -426,7 +468,7 @@ macro_rules! event_definitions { message: crate::DynMessage, app_state: &mut State, ) -> MessageResult { - message_event_listener(&self.element, view_state, id_path, message, app_state, &self.handler) + message_event_listener(&self.element, &self.handler, view_state, id_path, message, app_state) } } )* diff --git a/xilem_web/src/interfaces.rs b/xilem_web/src/interfaces.rs index bff9d71bb..392da5d63 100644 --- a/xilem_web/src/interfaces.rs +++ b/xilem_web/src/interfaces.rs @@ -35,7 +35,7 @@ macro_rules! event_handler_mixin { fn $fn_name( self, handler: Callback, - ) -> events::$event_ty + ) -> events::$event_ty where Self: Sized, Self::Element: AsRef, @@ -115,7 +115,7 @@ pub trait Element: self, event: impl Into>, handler: Callback, - ) -> events::OnEvent + ) -> events::OnEvent where Self::Element: AsRef, Event: JsCast + 'static, diff --git a/xilem_web/src/lib.rs b/xilem_web/src/lib.rs index 9a4f0240f..19b3274d0 100644 --- a/xilem_web/src/lib.rs +++ b/xilem_web/src/lib.rs @@ -33,6 +33,7 @@ mod attribute_value; mod class; mod context; mod element_props; +mod event_handler; mod events; mod message; mod one_of; @@ -53,6 +54,7 @@ pub use attribute_value::{AttributeValue, IntoAttributeValue}; pub use class::{AsClassIter, Class, Classes, ElementWithClasses, WithClasses}; pub use context::ViewCtx; pub use element_props::ElementProps; +pub use event_handler::{EventHandler, EventHandlerMessage}; pub use message::{DynMessage, Message}; pub use optional_action::{Action, OptionalAction}; pub use pointer::{Pointer, PointerDetails, PointerMsg}; From 2081652a8b9faf6f570b47ea0fa1e640e8265fa6 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Sat, 20 Jul 2024 17:41:01 +0200 Subject: [PATCH 2/5] Add a `defer` `EventHandler` to xilem_web --- Cargo.lock | 1 + xilem_web/Cargo.toml | 1 + xilem_web/src/event_handler.rs | 89 +++++++++++++++++++++++++++++++++- xilem_web/src/events.rs | 5 +- xilem_web/src/lib.rs | 3 +- 5 files changed, 95 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 093cad070..a8969c819 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4092,6 +4092,7 @@ version = "0.1.0" dependencies = [ "peniko", "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", "xilem_core", ] diff --git a/xilem_web/Cargo.toml b/xilem_web/Cargo.toml index 075a2933a..00db6482e 100644 --- a/xilem_web/Cargo.toml +++ b/xilem_web/Cargo.toml @@ -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" diff --git a/xilem_web/src/event_handler.rs b/xilem_web/src/event_handler.rs index f2ec7b0a1..b38a7b1f2 100644 --- a/xilem_web/src/event_handler.rs +++ b/xilem_web/src/event_handler.rs @@ -1,6 +1,10 @@ +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::DynMessage; +use crate::{context::MessageThunk, DynMessage, Message, ViewCtx}; pub enum EventHandlerMessage { Event(E), @@ -75,3 +79,86 @@ where } } } + +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct DeferEventHandler { + #[allow(clippy::complexity)] + phantom: PhantomData (T, A, FO, F)>, + future_fn: FF, + callback_fn: CF, +} + +impl EventHandler + for DeferEventHandler +where + State: 'static, + Action: 'static, + Event: 'static, + FO: Message, + F: Future + 'static, + FF: Fn(&mut State, Event) -> F + 'static, + CF: Fn(&mut State, FO) + 'static, +{ + type State = Rc; + + 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, + app_state: &mut State, + ) -> MessageResult> { + 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) => { + (self.callback_fn)(app_state, *output.downcast::().unwrap_throw()); + MessageResult::RequestRebuild + } + } + } +} + +pub fn defer( + future_fn: FF, + callback_fn: CF, +) -> DeferEventHandler +where + State: 'static, + Action: 'static, + Event: 'static, + FO: Message, + F: Future + 'static, + FF: Fn(&mut State, Event) -> F + 'static, + CF: Fn(&mut State, FO) + 'static, +{ + DeferEventHandler { + phantom: PhantomData, + future_fn, + callback_fn, + } +} diff --git a/xilem_web/src/events.rs b/xilem_web/src/events.rs index 12e001274..7828314f0 100644 --- a/xilem_web/src/events.rs +++ b/xilem_web/src/events.rs @@ -6,7 +6,10 @@ use wasm_bindgen::{prelude::Closure, throw_str, JsCast, UnwrapThrowExt}; use web_sys::AddEventListenerOptions; use xilem_core::{MessageResult, Mut, View, ViewId, ViewPathTracker}; -use crate::{DynMessage, ElementAsRef, EventHandler, EventHandlerMessage, OptionalAction, ViewCtx}; +use crate::{ + event_handler::{EventHandler, EventHandlerMessage}, + DynMessage, ElementAsRef, OptionalAction, ViewCtx, +}; /// Wraps a [`View`] `V` and attaches an event listener. /// diff --git a/xilem_web/src/lib.rs b/xilem_web/src/lib.rs index 19b3274d0..2e554c371 100644 --- a/xilem_web/src/lib.rs +++ b/xilem_web/src/lib.rs @@ -33,7 +33,7 @@ mod attribute_value; mod class; mod context; mod element_props; -mod event_handler; +pub mod event_handler; mod events; mod message; mod one_of; @@ -54,7 +54,6 @@ pub use attribute_value::{AttributeValue, IntoAttributeValue}; pub use class::{AsClassIter, Class, Classes, ElementWithClasses, WithClasses}; pub use context::ViewCtx; pub use element_props::ElementProps; -pub use event_handler::{EventHandler, EventHandlerMessage}; pub use message::{DynMessage, Message}; pub use optional_action::{Action, OptionalAction}; pub use pointer::{Pointer, PointerDetails, PointerMsg}; From 178e2b97c7564fcbe23d1f82e5abe3d8ef635e73 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Sat, 20 Jul 2024 18:49:14 +0200 Subject: [PATCH 3/5] xilem_web: Make interfaces generic over `EventHandler` and 'fix' the defer handler, with requiring an action type for the callback --- xilem_web/src/event_handler.rs | 16 +++++++------- xilem_web/src/events.rs | 2 +- xilem_web/src/interfaces.rs | 25 +++++++++++----------- xilem_web/web_examples/elm/src/main.rs | 4 ++-- xilem_web/web_examples/svgtoy/src/main.rs | 4 ++-- xilem_web/web_examples/todomvc/src/main.rs | 10 ++++----- 6 files changed, 31 insertions(+), 30 deletions(-) diff --git a/xilem_web/src/event_handler.rs b/xilem_web/src/event_handler.rs index b38a7b1f2..2dbf82ae1 100644 --- a/xilem_web/src/event_handler.rs +++ b/xilem_web/src/event_handler.rs @@ -81,9 +81,9 @@ where } #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct DeferEventHandler { +pub struct DeferEventHandler { #[allow(clippy::complexity)] - phantom: PhantomData (T, A, FO, F)>, + phantom: PhantomData (State, Action, FO, F)>, future_fn: FF, callback_fn: CF, } @@ -97,7 +97,7 @@ where FO: Message, F: Future + 'static, FF: Fn(&mut State, Event) -> F + 'static, - CF: Fn(&mut State, FO) + 'static, + CF: Fn(&mut State, FO) -> Action + 'static, { type State = Rc; @@ -135,10 +135,10 @@ where spawn_local(async move { thunk.push_message(future.await) }); MessageResult::RequestRebuild } - EventHandlerMessage::Message(output) => { - (self.callback_fn)(app_state, *output.downcast::().unwrap_throw()); - MessageResult::RequestRebuild - } + EventHandlerMessage::Message(output) => MessageResult::Action((self.callback_fn)( + app_state, + *output.downcast::().unwrap_throw(), + )), } } } @@ -154,7 +154,7 @@ where FO: Message, F: Future + 'static, FF: Fn(&mut State, Event) -> F + 'static, - CF: Fn(&mut State, FO) + 'static, + CF: Fn(&mut State, FO) -> Action + 'static, { DeferEventHandler { phantom: PhantomData, diff --git a/xilem_web/src/events.rs b/xilem_web/src/events.rs index 7828314f0..5db11b0cf 100644 --- a/xilem_web/src/events.rs +++ b/xilem_web/src/events.rs @@ -15,7 +15,7 @@ use crate::{ /// /// The event type `Event` should inherit from [`web_sys::Event`] #[derive(Clone, Debug)] -pub struct OnEvent { +pub struct OnEvent OA> { pub(crate) element: V, pub(crate) event: Cow<'static, str>, pub(crate) capture: bool, diff --git a/xilem_web/src/interfaces.rs b/xilem_web/src/interfaces.rs index 392da5d63..fa5335928 100644 --- a/xilem_web/src/interfaces.rs +++ b/xilem_web/src/interfaces.rs @@ -16,9 +16,10 @@ use std::borrow::Cow; use crate::{ attribute::{Attr, WithAttributes}, class::{AsClassIter, Class, WithClasses}, + event_handler::EventHandler, events, style::{IntoStyles, Style, WithStyle}, - DomView, IntoAttributeValue, OptionalAction, Pointer, PointerMsg, + DomView, IntoAttributeValue, OptionalAction, Pointer, PointerMsg, ViewCtx, }; use wasm_bindgen::JsCast; @@ -32,15 +33,15 @@ macro_rules! event_handler_mixin { // We *could* add another parameter to the macro to fix this, or probably even not provide these events directly on the `Element` interface // /// // #[doc = concat!("See for more details")] - fn $fn_name( + fn $fn_name( self, - handler: Callback, - ) -> events::$event_ty + handler: Handler, + ) -> events::$event_ty where Self: Sized, Self::Element: AsRef, OA: OptionalAction, - Callback: Fn(&mut State, web_sys::$web_sys_event_type) -> OA, + Handler: EventHandler, { events::$event_ty::new(self, handler) } @@ -111,25 +112,25 @@ pub trait Element: /// div(()).on("custom-event", |state, event: web_sys::Event| {/* modify `state` */}) /// # } /// ``` - fn on( + fn on( self, event: impl Into>, - handler: Callback, - ) -> events::OnEvent + handler: Handler, + ) -> events::OnEvent where Self::Element: AsRef, Event: JsCast + 'static, OA: OptionalAction, - Callback: Fn(&mut State, Event) -> OA, + Handler: EventHandler, Self: Sized, { events::OnEvent::new(self, event, handler) } - fn pointer( + fn pointer( self, - handler: Callback, - ) -> Pointer { + handler: Handler, + ) -> Pointer { crate::pointer::pointer(self, handler) } diff --git a/xilem_web/web_examples/elm/src/main.rs b/xilem_web/web_examples/elm/src/main.rs index 116c5efcb..aabf8ce5b 100644 --- a/xilem_web/web_examples/elm/src/main.rs +++ b/xilem_web/web_examples/elm/src/main.rs @@ -45,8 +45,8 @@ fn app_logic(model: &mut Model) -> impl HtmlDivElement { fn counter_view(count: i32) -> impl HtmlDivElement { el::div(( el::label(format!("count: {count}")), - el::button("+").on_click(|_, _| Message::Increment), - el::button("-").on_click(|_, _| Message::Decrement), + el::button("+").on_click(|_: &mut _, _| Message::Increment), + el::button("-").on_click(|_: &mut _, _| Message::Decrement), )) } diff --git a/xilem_web/web_examples/svgtoy/src/main.rs b/xilem_web/web_examples/svgtoy/src/main.rs index 6b744651c..764ea53a3 100644 --- a/xilem_web/web_examples/svgtoy/src/main.rs +++ b/xilem_web/web_examples/svgtoy/src/main.rs @@ -58,7 +58,7 @@ fn app_logic(state: &mut AppState) -> impl DomView { .map(|i| Rect::from_origin_size((10.0 * i as f64, 150.0), (8.0, 8.0))) .collect::>(); svg(g(( - Rect::new(100.0, 100.0, 200.0, 200.0).on_click(|_, _| { + Rect::new(100.0, 100.0, 200.0, 200.0).on_click(|_: &mut _, _| { web_sys::console::log_1(&"app logic clicked".into()); }), Rect::new(210.0, 100.0, 310.0, 200.0) @@ -72,7 +72,7 @@ fn app_logic(state: &mut AppState) -> impl DomView { web_sys::console::log_1(&format!("pointer event {e:?}").into()); }), kurbo::Line::new((310.0, 210.0), (410.0, 310.0)), - kurbo::Circle::new((460.0, 260.0), 45.0).on_click(|_, _| { + kurbo::Circle::new((460.0, 260.0), 45.0).on_click(|_: &mut _, _| { web_sys::console::log_1(&"circle clicked".into()); }), ))) diff --git a/xilem_web/web_examples/todomvc/src/main.rs b/xilem_web/web_examples/todomvc/src/main.rs index fe18459d7..152b45a2b 100644 --- a/xilem_web/web_examples/todomvc/src/main.rs +++ b/xilem_web/web_examples/todomvc/src/main.rs @@ -45,7 +45,7 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { el::input(()) .attr("value", todo.title_editing.clone()) .class("edit") - .on_keydown(|state: &mut Todo, evt| { + .on_keydown(|state: &mut Todo, evt: web_sys::KeyboardEvent| { let key = evt.key(); if key == "Enter" { state.save_editing(); @@ -56,7 +56,7 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { None } }) - .on_input(|state: &mut Todo, evt| { + .on_input(|state: &mut Todo, evt: web_sys::Event| { // TODO There could/should be further checks, if this is indeed the right event (same DOM element) if let Some(element) = evt .target() @@ -67,7 +67,7 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { } }) .passive(true) - .on_blur(|_, _| TodoAction::CancelEditing), + .on_blur(|_: &mut _, _| TodoAction::CancelEditing), )) .class(todo.completed.then_some("completed")) .class(editing.then_some("editing")) @@ -184,12 +184,12 @@ fn app_logic(state: &mut AppState) -> impl DomView { el::header(( el::h1("TODOs"), input - .on_keydown(|state: &mut AppState, evt| { + .on_keydown(|state: &mut AppState, evt: web_sys::KeyboardEvent| { if evt.key() == "Enter" { state.create_todo(); } }) - .on_input(|state: &mut AppState, evt| { + .on_input(|state: &mut AppState, evt: web_sys::Event| { // TODO There could/should be further checks, if this is indeed the right event (same DOM element) if let Some(element) = evt .target() From e7a8e3c601e31e239868eb8e96f3ad61db6a9f31 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Sat, 20 Jul 2024 18:54:38 +0200 Subject: [PATCH 4/5] xilem_web: Add fetch example (modified version of #427) Co-authored-by: Markus Kohlhase --- Cargo.lock | 102 +++++++++++++++++++++ Cargo.toml | 1 + xilem_web/web_examples/fetch/Cargo.toml | 23 +++++ xilem_web/web_examples/fetch/index.html | 18 ++++ xilem_web/web_examples/fetch/src/main.rs | 112 +++++++++++++++++++++++ 5 files changed, 256 insertions(+) create mode 100644 xilem_web/web_examples/fetch/Cargo.toml create mode 100644 xilem_web/web_examples/fetch/index.html create mode 100644 xilem_web/web_examples/fetch/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index a8969c819..92f0478d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -186,6 +186,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + [[package]] name = "arrayref" version = "0.3.7" @@ -1080,6 +1086,24 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fetch" +version = "0.1.0" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "console_log", + "futures", + "gloo-net", + "log", + "serde", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "xilem_web", +] + [[package]] name = "flate2" version = "1.0.30" @@ -1170,12 +1194,48 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-intrusive" version = "0.5.0" @@ -1250,6 +1310,7 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1309,6 +1370,36 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "gloo-utils", + "http", + "js-sys", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "glow" version = "0.13.1" @@ -1435,6 +1526,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "icu_collections" version = "1.4.0" diff --git a/Cargo.toml b/Cargo.toml index dbf8200ab..c408ac592 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/xilem_web/web_examples/fetch/Cargo.toml b/xilem_web/web_examples/fetch/Cargo.toml new file mode 100644 index 000000000..9172ac1c1 --- /dev/null +++ b/xilem_web/web_examples/fetch/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "fetch" +version = "0.1.0" +publish = false +license.workspace = true +edition.workspace = true + +[lints] +workspace = true + +[dependencies] +anyhow = "1.0.86" +console_error_panic_hook = "0.1" +console_log = "1" +futures = "0.3.30" +gloo-net = { version = "0.5.0", default-features = false, features = ["http", "json", "serde"] } +log = "0.4" +serde = { version = "1", features = ["derive"] } +thiserror = "1" +web-sys = { version = "0.3.69", features = ["Event", "HtmlInputElement"] } +wasm-bindgen = "0.2.92" +wasm-bindgen-futures = "0.4.42" +xilem_web = { path = "../.." } diff --git a/xilem_web/web_examples/fetch/index.html b/xilem_web/web_examples/fetch/index.html new file mode 100644 index 000000000..271ad17fd --- /dev/null +++ b/xilem_web/web_examples/fetch/index.html @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/xilem_web/web_examples/fetch/src/main.rs b/xilem_web/web_examples/fetch/src/main.rs new file mode 100644 index 000000000..dc08a0921 --- /dev/null +++ b/xilem_web/web_examples/fetch/src/main.rs @@ -0,0 +1,112 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use wasm_bindgen::{JsCast, UnwrapThrowExt as _}; +use xilem_web::{ + document_body, + elements::html::*, + event_handler::defer, + interfaces::{Element, HtmlDivElement, HtmlImageElement}, + App, +}; + +use gloo_net::http::Request; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Cat { + pub url: String, +} + +#[derive(Error, Clone, Debug)] +pub enum CatError { + #[error("Please request more than zero cats.")] + NonZeroCats, +} + +pub type CatCount = usize; + +pub async fn fetch_cats(count: CatCount) -> anyhow::Result> { + log::debug!("Fetch {count} cats"); + if count < 1 { + return Err(CatError::NonZeroCats.into()); + } + let url = format!("https://api.thecatapi.com/v1/images/search?limit={count}",); + Ok(Request::get(&url) + .send() + .await? + .json::>() + .await? + .into_iter() + .take(count) + .collect()) +} + +#[derive(Default)] +struct AppState { + cat_count: usize, + cats: Vec, + error: Option, +} + +pub fn event_target_value(event: &T) -> String +where + T: JsCast, +{ + event + .unchecked_ref::() + .target() + .unwrap_throw() + .unchecked_into::() + .value() +} + +fn app_logic(state: &mut AppState) -> impl HtmlDivElement { + let cats = state + .cats + .iter() + .map(|cat| p(img(()).src(cat.url.clone()))) + .collect::>(); + div(( + label(( + "How many cats would you like?", + input(()) + .attr("type", "number") + .attr("value", state.cat_count.to_string()) + .on_input(defer( + |state: &mut AppState, ev: web_sys::Event| { + let count = event_target_value(&ev).parse::().unwrap_or(0); + state.cat_count = count; + state.cats.clear(); + fetch_cats(count) + }, + |state: &mut AppState, fetch_result| match fetch_result { + Ok(cats) => { + log::info!("Received {} cats", cats.len()); + state.cats = cats; + state.error = None; + } + Err(err) => { + log::warn!("Unable to fetch cats: {err:#}"); + state.error = Some(err.to_string()); + } + }, + )), + )), + state + .error + .as_ref() + .map(|err| div((h2("Error"), p(err.to_string()))).class("error")), + cats, + )) +} + +pub fn main() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + + log::info!("Start application"); + + App::new(document_body(), AppState::default(), app_logic).run(); +} From 8db04ab7949a6b430453688da4d50a3ad3a5e795 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Sat, 20 Jul 2024 19:44:59 +0200 Subject: [PATCH 5/5] Fix CI (tests, 'typo', license header) --- xilem_web/src/event_handler.rs | 29 ++++++++++++++++------------- xilem_web/src/interfaces.rs | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/xilem_web/src/event_handler.rs b/xilem_web/src/event_handler.rs index 2dbf82ae1..484901b22 100644 --- a/xilem_web/src/event_handler.rs +++ b/xilem_web/src/event_handler.rs @@ -1,3 +1,6 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + use std::{future::Future, marker::PhantomData, rc::Rc}; use wasm_bindgen::UnwrapThrowExt; @@ -81,23 +84,23 @@ where } #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct DeferEventHandler { +pub struct DeferEventHandler { #[allow(clippy::complexity)] - phantom: PhantomData (State, Action, FO, F)>, + phantom: PhantomData (State, Action, FOut, F)>, future_fn: FF, callback_fn: CF, } -impl EventHandler - for DeferEventHandler +impl EventHandler + for DeferEventHandler where State: 'static, Action: 'static, Event: 'static, - FO: Message, - F: Future + 'static, + FOut: Message, + F: Future + 'static, FF: Fn(&mut State, Event) -> F + 'static, - CF: Fn(&mut State, FO) -> Action + 'static, + CF: Fn(&mut State, FOut) -> Action + 'static, { type State = Rc; @@ -137,24 +140,24 @@ where } EventHandlerMessage::Message(output) => MessageResult::Action((self.callback_fn)( app_state, - *output.downcast::().unwrap_throw(), + *output.downcast::().unwrap_throw(), )), } } } -pub fn defer( +pub fn defer( future_fn: FF, callback_fn: CF, -) -> DeferEventHandler +) -> DeferEventHandler where State: 'static, Action: 'static, Event: 'static, - FO: Message, - F: Future + 'static, + FOut: Message, + F: Future + 'static, FF: Fn(&mut State, Event) -> F + 'static, - CF: Fn(&mut State, FO) -> Action + 'static, + CF: Fn(&mut State, FOut) -> Action + 'static, { DeferEventHandler { phantom: PhantomData, diff --git a/xilem_web/src/interfaces.rs b/xilem_web/src/interfaces.rs index fa5335928..057f667ae 100644 --- a/xilem_web/src/interfaces.rs +++ b/xilem_web/src/interfaces.rs @@ -109,7 +109,7 @@ pub trait Element: /// ``` /// use xilem_web::{interfaces::Element, elements::html::div}; /// # fn component() -> impl Element<()> { - /// div(()).on("custom-event", |state, event: web_sys::Event| {/* modify `state` */}) + /// div(()).on("custom-event", |state: &mut _, event: web_sys::Event| {/* modify `state` */}) /// # } /// ``` fn on(