From cc293b11702ab5038dc457a3ea68c04d5bcf8981 Mon Sep 17 00:00:00 2001 From: rkuklik <90713105+rkuklik@users.noreply.github.com> Date: Fri, 25 Aug 2023 17:41:16 +0200 Subject: [PATCH] feat: generic event handler types to make it easier to create collections of event handlers (#1444) --- leptos_dom/src/events/typed.rs | 426 +++++++++++++++++++++++++-------- leptos_dom/src/lib.rs | 2 +- 2 files changed, 325 insertions(+), 103 deletions(-) diff --git a/leptos_dom/src/events/typed.rs b/leptos_dom/src/events/typed.rs index e9fb8fd77..71feef646 100644 --- a/leptos_dom/src/events/typed.rs +++ b/leptos_dom/src/events/typed.rs @@ -31,7 +31,7 @@ pub trait EventDescriptor: Clone { /// Overrides the [`EventDescriptor::BUBBLES`] value to always return /// `false`, which forces the event to not be globally delegated. -#[derive(Clone)] +#[derive(Clone, Debug)] #[allow(non_camel_case_types)] pub struct undelegated(pub Ev); @@ -52,6 +52,7 @@ impl EventDescriptor for undelegated { } /// A custom event. +#[derive(Debug)] pub struct Custom { name: Cow<'static, str>, options: Option, @@ -125,34 +126,255 @@ impl Custom { } } +/// Type that can respond to DOM events +pub trait DOMEventResponder: Sized { + /// Adds handler to specified event + fn add( + self, + event: E, + handler: impl FnMut(E::EventType) + 'static, + ) -> Self; + /// Same as [add](DOMEventResponder::add), but with [`EventHandler`] + #[inline] + fn add_handler(self, handler: impl EventHandler) -> Self { + handler.attach(self) + } +} + +impl DOMEventResponder for crate::HtmlElement +where + T: crate::html::ElementDescriptor + 'static, +{ + #[inline(always)] + fn add( + self, + event: E, + handler: impl FnMut(E::EventType) + 'static, + ) -> Self { + self.on(event, handler) + } +} + +impl DOMEventResponder for crate::View { + #[inline(always)] + fn add( + self, + event: E, + handler: impl FnMut(E::EventType) + 'static, + ) -> Self { + self.on(event, handler) + } +} + +/// Type that can be used to handle DOM events +pub trait EventHandler { + /// Attaches event listener to any target that can respond to DOM events + fn attach(self, target: T) -> T; +} + +impl EventHandler for [T; N] +where + T: EventHandler, +{ + #[inline] + fn attach(self, target: R) -> R { + let mut target = target; + for item in self { + target = item.attach(target); + } + target + } +} + +impl EventHandler for Option +where + T: EventHandler, +{ + #[inline] + fn attach(self, target: R) -> R { + match self { + Some(event_handler) => event_handler.attach(target), + None => target, + } + } +} + +macro_rules! tc { + ($($ty:ident),*) => { + impl<$($ty),*> EventHandler for ($($ty,)*) + where + $($ty: EventHandler),* + { + #[inline] + fn attach(self, target: RES) -> RES { + ::paste::paste! { + let ( + $( + [<$ty:lower>],)* + ) = self; + $( + let target = [<$ty:lower>].attach(target); + )* + target + } + } + } + }; +} + +tc!(A); +tc!(A, B); +tc!(A, B, C); +tc!(A, B, C, D); +tc!(A, B, C, D, E); +tc!(A, B, C, D, E, F); +tc!(A, B, C, D, E, F, G); +tc!(A, B, C, D, E, F, G, H); +tc!(A, B, C, D, E, F, G, H, I); +tc!(A, B, C, D, E, F, G, H, I, J); +tc!(A, B, C, D, E, F, G, H, I, J, K); +tc!(A, B, C, D, E, F, G, H, I, J, K, L); +tc!(A, B, C, D, E, F, G, H, I, J, K, L, M); +tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N); +tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); +tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); +tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q); +tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R); +tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S); +tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T); +tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U); +tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V); +tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W); +tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X); +tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y); +#[rustfmt::skip] +tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z); + +macro_rules! collection_callback { + {$( + $collection:ident + ),* $(,)?} => { + $( + impl EventHandler for $collection + where + T: EventHandler + { + #[inline] + fn attach(self, target: R) -> R { + let mut target = target; + for item in self { + target = item.attach(target); + } + target + } + } + )* + }; +} + +use std::collections::{BTreeSet, BinaryHeap, HashSet, LinkedList, VecDeque}; + +collection_callback! { + Vec, + BTreeSet, + BinaryHeap, + HashSet, + LinkedList, + VecDeque, +} + macro_rules! generate_event_types { {$( $( #[$does_not_bubble:ident] )? - $event:ident : $web_sys_event:ident + $( $event:ident )+ : $web_event:ident ),* $(,)?} => { - - $( - #[doc = concat!("The `", stringify!($event), "` event, which receives [", stringify!($web_sys_event), "](web_sys::", stringify!($web_sys_event), ") as its argument.")] - #[derive(Copy, Clone)] + ::paste::paste! { + $( + #[doc = "The `" [< $($event)+ >] "` event, which receives [" $web_event "](web_sys::" $web_event ") as its argument."] + #[derive(Copy, Clone, Debug)] #[allow(non_camel_case_types)] - pub struct $event; + pub struct [<$( $event )+ >]; - impl EventDescriptor for $event { - type EventType = web_sys::$web_sys_event; + impl EventDescriptor for [< $($event)+ >] { + type EventType = web_sys::$web_event; #[inline(always)] fn name(&self) -> Cow<'static, str> { - stringify!($event).into() + stringify!([< $($event)+ >]).into() } #[inline(always)] fn event_delegation_key(&self) -> Cow<'static, str> { - concat!("$$$", stringify!($event)).into() + concat!("$$$", stringify!([< $($event)+ >])).into() } const BUBBLES: bool = true $(&& generate_event_types!($does_not_bubble))?; } - )* + )* + + /// An enum holding all basic event types with their respective handlers. + /// + /// It currently omits [`Custom`] and [`undelegated`] variants. + #[non_exhaustive] + pub enum GenericEventHandler { + $( + #[doc = "Variant mapping [`struct@" [< $($event)+ >] "`] to its event handler type."] + [< $($event:camel)+ >]([< $($event)+ >], Box), + )* + } + + impl ::std::fmt::Debug for GenericEventHandler { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match self { + $( + Self::[< $($event:camel)+ >](event, _) => f + .debug_tuple(stringify!([< $($event:camel)+ >])) + .field(&event) + .field(&::std::any::type_name::>()) + .finish(), + )* + } + } + } + + impl EventHandler for GenericEventHandler { + fn attach(self, target: T) -> T { + match self { + $( + Self::[< $($event:camel)+ >](event, handler) => target.add(event, handler), + )* + } + } + } + + $( + impl From<([< $($event)+ >], F)> for GenericEventHandler + where + F: FnMut($web_event) + 'static + { + fn from(value: ([< $($event)+ >], F)) -> Self { + Self::[< $($event:camel)+ >](value.0, Box::new(value.1)) + } + } + // NOTE: this could become legal in future and would save us from useless allocations + //impl From<([< $($event)+ >], Box)> for GenericEventHandler + //where + // F: FnMut($web_event) + 'static + //{ + // fn from(value: ([< $($event)+ >], Box)) -> Self { + // Self::[< $($event:camel)+ >](value.0, value.1) + // } + //} + impl EventHandler for ([< $($event)+ >], F) + where + F: FnMut($web_event) + 'static + { + fn attach(self, target: L) -> L { + target.add(self.0, self.1) + } + } + )* + } }; (does_not_bubble) => { false } @@ -163,36 +385,36 @@ generate_event_types! { // WindowEventHandlersEventMap // ========================================================= #[does_not_bubble] - afterprint: Event, + after print: Event, #[does_not_bubble] - beforeprint: Event, + before print: Event, #[does_not_bubble] - beforeunload: BeforeUnloadEvent, + before unload: BeforeUnloadEvent, #[does_not_bubble] - gamepadconnected: GamepadEvent, + gamepad connected: GamepadEvent, #[does_not_bubble] - gamepaddisconnected: GamepadEvent, - hashchange: HashChangeEvent, + gamepad disconnected: GamepadEvent, + hash change: HashChangeEvent, #[does_not_bubble] - languagechange: Event, + language change: Event, #[does_not_bubble] message: MessageEvent, #[does_not_bubble] - messageerror: MessageEvent, + message error: MessageEvent, #[does_not_bubble] offline: Event, #[does_not_bubble] online: Event, #[does_not_bubble] - pagehide: PageTransitionEvent, + page hide: PageTransitionEvent, #[does_not_bubble] - pageshow: PageTransitionEvent, - popstate: PopStateEvent, - rejectionhandled: PromiseRejectionEvent, + page show: PageTransitionEvent, + pop state: PopStateEvent, + rejection handled: PromiseRejectionEvent, #[does_not_bubble] storage: StorageEvent, #[does_not_bubble] - unhandledrejection: PromiseRejectionEvent, + unhandled rejection: PromiseRejectionEvent, #[does_not_bubble] unload: Event, @@ -201,38 +423,38 @@ generate_event_types! { // ========================================================= #[does_not_bubble] abort: UiEvent, - animationcancel: AnimationEvent, - animationend: AnimationEvent, - animationiteration: AnimationEvent, - animationstart: AnimationEvent, - auxclick: MouseEvent, - beforeinput: InputEvent, + animation cancel: AnimationEvent, + animation end: AnimationEvent, + animation iteration: AnimationEvent, + animation start: AnimationEvent, + aux click: MouseEvent, + before input: InputEvent, #[does_not_bubble] blur: FocusEvent, #[does_not_bubble] - canplay: Event, + can play: Event, #[does_not_bubble] - canplaythrough: Event, + can play through: Event, change: Event, click: MouseEvent, #[does_not_bubble] close: Event, - compositionend: CompositionEvent, - compositionstart: CompositionEvent, - compositionupdate: CompositionEvent, - contextmenu: MouseEvent, + composition end: CompositionEvent, + composition start: CompositionEvent, + composition update: CompositionEvent, + context menu: MouseEvent, #[does_not_bubble] - cuechange: Event, - dblclick: MouseEvent, + cue change: Event, + dbl click: MouseEvent, drag: DragEvent, - dragend: DragEvent, - dragenter: DragEvent, - dragleave: DragEvent, - dragover: DragEvent, - dragstart: DragEvent, + drag end: DragEvent, + drag enter: DragEvent, + drag leave: DragEvent, + drag over: DragEvent, + drag start: DragEvent, drop: DragEvent, #[does_not_bubble] - durationchange: Event, + duration change: Event, #[does_not_bubble] emptied: Event, #[does_not_bubble] @@ -242,110 +464,110 @@ generate_event_types! { #[does_not_bubble] focus: FocusEvent, #[does_not_bubble] - focusin: FocusEvent, + focus in: FocusEvent, #[does_not_bubble] - focusout: FocusEvent, - formdata: Event, // web_sys does not include `FormDataEvent` + focus out: FocusEvent, + form data: Event, // web_sys does not include `FormDataEvent` #[does_not_bubble] - gotpointercapture: PointerEvent, + got pointer capture: PointerEvent, input: Event, #[does_not_bubble] invalid: Event, - keydown: KeyboardEvent, - keypress: KeyboardEvent, - keyup: KeyboardEvent, + key down: KeyboardEvent, + key press: KeyboardEvent, + key up: KeyboardEvent, #[does_not_bubble] load: Event, #[does_not_bubble] - loadeddata: Event, + loaded data: Event, #[does_not_bubble] - loadedmetadata: Event, + loaded metadata: Event, #[does_not_bubble] - loadstart: Event, - lostpointercapture: PointerEvent, - mousedown: MouseEvent, + load start: Event, + lost pointer capture: PointerEvent, + mouse down: MouseEvent, #[does_not_bubble] - mouseenter: MouseEvent, + mouse enter: MouseEvent, #[does_not_bubble] - mouseleave: MouseEvent, - mousemove: MouseEvent, - mouseout: MouseEvent, - mouseover: MouseEvent, - mouseup: MouseEvent, + mouse leave: MouseEvent, + mouse move: MouseEvent, + mouse out: MouseEvent, + mouse over: MouseEvent, + mouse up: MouseEvent, #[does_not_bubble] pause: Event, #[does_not_bubble] play: Event, #[does_not_bubble] playing: Event, - pointercancel: PointerEvent, - pointerdown: PointerEvent, + pointer cancel: PointerEvent, + pointer down: PointerEvent, #[does_not_bubble] - pointerenter: PointerEvent, + pointer enter: PointerEvent, #[does_not_bubble] - pointerleave: PointerEvent, - pointermove: PointerEvent, - pointerout: PointerEvent, - pointerover: PointerEvent, - pointerup: PointerEvent, + pointer leave: PointerEvent, + pointer move: PointerEvent, + pointer out: PointerEvent, + pointer over: PointerEvent, + pointer up: PointerEvent, #[does_not_bubble] progress: ProgressEvent, #[does_not_bubble] - ratechange: Event, + rate change: Event, reset: Event, #[does_not_bubble] resize: UiEvent, #[does_not_bubble] scroll: Event, #[does_not_bubble] - scrollend: Event, - securitypolicyviolation: SecurityPolicyViolationEvent, + scroll end: Event, + security policy violation: SecurityPolicyViolationEvent, #[does_not_bubble] seeked: Event, #[does_not_bubble] seeking: Event, select: Event, #[does_not_bubble] - selectionchange: Event, - selectstart: Event, - slotchange: Event, + selection change: Event, + select start: Event, + slot change: Event, #[does_not_bubble] stalled: Event, submit: SubmitEvent, #[does_not_bubble] suspend: Event, #[does_not_bubble] - timeupdate: Event, + time update: Event, #[does_not_bubble] toggle: Event, - touchcancel: TouchEvent, - touchend: TouchEvent, - touchmove: TouchEvent, - touchstart: TouchEvent, - transitioncancel: TransitionEvent, - transitionend: TransitionEvent, - transitionrun: TransitionEvent, - transitionstart: TransitionEvent, + touch cancel: TouchEvent, + touch end: TouchEvent, + touch move: TouchEvent, + touch start: TouchEvent, + transition cancel: TransitionEvent, + transition end: TransitionEvent, + transition run: TransitionEvent, + transition start: TransitionEvent, #[does_not_bubble] - volumechange: Event, + volume change: Event, #[does_not_bubble] waiting: Event, - webkitanimationend: Event, - webkitanimationiteration: Event, - webkitanimationstart: Event, - webkittransitionend: Event, + webkit animation end: Event, + webkit animation iteration: Event, + webkit animation start: Event, + webkit transition end: Event, wheel: WheelEvent, // ========================================================= // WindowEventMap // ========================================================= - DOMContentLoaded: Event, + D O M Content Loaded: Event, // Hack for correct casing #[does_not_bubble] - devicemotion: DeviceMotionEvent, + device motion: DeviceMotionEvent, #[does_not_bubble] - deviceorientation: DeviceOrientationEvent, + device orientation: DeviceOrientationEvent, #[does_not_bubble] - orientationchange: Event, + orientation change: Event, // ========================================================= // DocumentAndElementEventHandlersEventMap @@ -357,13 +579,13 @@ generate_event_types! { // ========================================================= // DocumentEventMap // ========================================================= - fullscreenchange: Event, - fullscreenerror: Event, - pointerlockchange: Event, - pointerlockerror: Event, + fullscreen change: Event, + fullscreen error: Event, + pointer lock change: Event, + pointer lock error: Event, #[does_not_bubble] - readystatechange: Event, - visibilitychange: Event, + ready state change: Event, + visibility change: Event, } // Export `web_sys` event types @@ -371,7 +593,7 @@ pub use web_sys::{ AnimationEvent, BeforeUnloadEvent, CompositionEvent, CustomEvent, DeviceMotionEvent, DeviceOrientationEvent, DragEvent, ErrorEvent, Event, FocusEvent, GamepadEvent, HashChangeEvent, InputEvent, KeyboardEvent, - MouseEvent, PageTransitionEvent, PointerEvent, PopStateEvent, + MessageEvent, MouseEvent, PageTransitionEvent, PointerEvent, PopStateEvent, ProgressEvent, PromiseRejectionEvent, SecurityPolicyViolationEvent, StorageEvent, SubmitEvent, TouchEvent, TransitionEvent, UiEvent, WheelEvent, diff --git a/leptos_dom/src/lib.rs b/leptos_dom/src/lib.rs index 8517582fd..c3732582e 100644 --- a/leptos_dom/src/lib.rs +++ b/leptos_dom/src/lib.rs @@ -28,9 +28,9 @@ use cfg_if::cfg_if; pub use components::*; #[cfg(all(target_arch = "wasm32", feature = "web"))] pub use events::add_event_helper; -pub use events::typed as ev; #[cfg(all(target_arch = "wasm32", feature = "web"))] use events::{add_event_listener, add_event_listener_undelegated}; +pub use events::{typed as ev, typed::EventHandler}; pub use html::HtmlElement; use html::{AnyElement, ElementDescriptor}; pub use hydration::{HydrationCtx, HydrationKey};