Skip to main content

xilem_web/
events.rs

1// Copyright 2023 the Xilem Authors
2// SPDX-License-Identifier: Apache-2.0
3
4use std::borrow::Cow;
5use std::marker::PhantomData;
6
7use wasm_bindgen::prelude::Closure;
8use wasm_bindgen::{JsCast, UnwrapThrowExt, throw_str};
9use web_sys::{AddEventListenerOptions, js_sys};
10
11use crate::core::anymore::AnyDebug;
12use crate::core::{MessageContext, MessageResult, Mut, View, ViewId, ViewMarker, ViewPathTracker};
13use crate::{DomView, OptionalAction, ViewCtx};
14
15/// Use a distinctive number here, to be able to catch bugs.
16/// In case the generational-id view path in `View::Message` lead to a wrong view
17const ON_EVENT_VIEW_ID: ViewId = ViewId::new(0x2357_1113);
18
19/// Wraps a [`View`] `V` and attaches an event listener.
20///
21/// The event type `Event` should inherit from [`web_sys::Event`]
22#[derive(Clone, Debug)]
23pub struct OnEvent<V, State, Action, Event, Callback> {
24    pub(crate) dom_view: V,
25    pub(crate) event: Cow<'static, str>,
26    pub(crate) capture: bool,
27    pub(crate) passive: bool,
28    pub(crate) handler: Callback,
29    pub(crate) phantom_event_ty: PhantomData<fn() -> (State, Action, Event)>,
30}
31
32impl<V, State, Action, Event, Callback> OnEvent<V, State, Action, Event, Callback>
33where
34    Event: JsCast + 'static,
35{
36    pub fn new(dom_view: V, event: impl Into<Cow<'static, str>>, handler: Callback) -> Self {
37        Self {
38            dom_view,
39            event: event.into(),
40            passive: true,
41            capture: false,
42            handler,
43            phantom_event_ty: PhantomData,
44        }
45    }
46
47    /// Whether the event handler should be passive. (default = `true`)
48    ///
49    /// Passive event handlers can't prevent the browser's default action from
50    /// running (otherwise possible with `event.prevent_default()`), which
51    /// restricts what they can be used for, but reduces overhead.
52    pub fn passive(mut self, value: bool) -> Self {
53        self.passive = value;
54        self
55    }
56
57    /// Whether the event handler should capture the event *before* being dispatched to any `EventTarget` beneath it in the DOM tree. (default = `false`)
58    ///
59    /// Events that are bubbling upward through the tree will not trigger a listener designated to use capture.
60    /// Event bubbling and capturing are two ways of propagating events that occur in an element that is nested within another element,
61    /// when both elements have registered a handle for that event.
62    /// The event propagation mode determines the order in which elements receive the event.
63    // TODO use similar Nomenclature as gloo (Phase::Bubble/Phase::Capture)?
64    pub fn capture(mut self, value: bool) -> Self {
65        self.capture = value;
66        self
67    }
68}
69
70fn create_event_listener<Event: JsCast + AnyDebug>(
71    target: &web_sys::EventTarget,
72    event: &str,
73    // TODO options
74    capture: bool,
75    passive: bool,
76    ctx: &mut ViewCtx,
77) -> Closure<dyn FnMut(web_sys::Event)> {
78    let thunk = ctx.message_thunk();
79    let callback = Closure::new(move |event: web_sys::Event| {
80        let event = event.unchecked_into::<Event>();
81        thunk.push_message(event);
82    });
83
84    let options = AddEventListenerOptions::new();
85    options.set_capture(capture);
86    options.set_passive(passive);
87
88    target
89        .add_event_listener_with_callback_and_add_event_listener_options(
90            event,
91            callback.as_ref().unchecked_ref(),
92            &options,
93        )
94        .unwrap_throw();
95    callback
96}
97
98fn remove_event_listener(
99    target: &web_sys::EventTarget,
100    event: &str,
101    callback: &Closure<dyn FnMut(web_sys::Event)>,
102    is_capture: bool,
103) {
104    target
105        .remove_event_listener_with_callback_and_bool(
106            event,
107            callback.as_ref().unchecked_ref(),
108            is_capture,
109        )
110        .unwrap_throw();
111}
112
113mod hidden {
114    use wasm_bindgen::prelude::Closure;
115    #[expect(
116        unnameable_types,
117        reason = "Implementation detail, public because of trait visibility rules"
118    )]
119    /// State for the `OnEvent` view.
120    pub struct OnEventState<S> {
121        pub(crate) child_state: S,
122        pub(crate) callback: Closure<dyn FnMut(web_sys::Event)>,
123    }
124}
125
126use hidden::OnEventState;
127
128// These (boilerplatey) functions are there to reduce the boilerplate created by the macro-expansion below.
129
130fn build_event_listener<State, Action, V, Event>(
131    element_view: &V,
132    event: &str,
133    capture: bool,
134    passive: bool,
135    ctx: &mut ViewCtx,
136    app_state: &mut State,
137) -> (V::Element, OnEventState<V::ViewState>)
138where
139    State: 'static,
140    Action: 'static,
141    V: DomView<State, Action>,
142    Event: JsCast + 'static + AnyDebug,
143{
144    // we use a placeholder id here, the id can never change, so we don't need to store it anywhere
145    ctx.with_id(ON_EVENT_VIEW_ID, |ctx| {
146        let (element, child_state) = element_view.build(ctx, app_state);
147        let callback =
148            create_event_listener::<Event>(element.as_ref(), event, capture, passive, ctx);
149        let state = OnEventState {
150            child_state,
151            callback,
152        };
153        (element, state)
154    })
155}
156
157fn rebuild_event_listener<State, Action, V, Event>(
158    element_view: &V,
159    prev_element_view: &V,
160    mut element: Mut<'_, V::Element>,
161    event: &str,
162    capture: bool,
163    passive: bool,
164    prev_capture: bool,
165    prev_passive: bool,
166    state: &mut OnEventState<V::ViewState>,
167    ctx: &mut ViewCtx,
168    app_state: &mut State,
169) where
170    State: 'static,
171    Action: 'static,
172    V: DomView<State, Action>,
173    Event: JsCast + 'static + AnyDebug,
174{
175    ctx.with_id(ON_EVENT_VIEW_ID, |ctx| {
176        element_view.rebuild(
177            prev_element_view,
178            &mut state.child_state,
179            ctx,
180            element.reborrow_mut(),
181            app_state,
182        );
183        let was_created = element.flags.was_created();
184        let needs_update = prev_capture != capture || prev_passive != passive || was_created;
185        if !needs_update {
186            return;
187        }
188        if !was_created {
189            remove_event_listener(element.as_ref(), event, &state.callback, prev_capture);
190        }
191        state.callback =
192            create_event_listener::<Event>(element.as_ref(), event, capture, passive, ctx);
193    });
194}
195
196fn teardown_event_listener<State, Action, V>(
197    element_view: &V,
198    element: Mut<'_, V::Element>,
199    _event: &str,
200    state: &mut OnEventState<V::ViewState>,
201    _capture: bool,
202    ctx: &mut ViewCtx,
203) where
204    State: 'static,
205    Action: 'static,
206    V: DomView<State, Action>,
207{
208    // TODO: is this really needed (as the element will be removed anyway)?
209    // remove_event_listener(element.as_ref(), event, &state.callback, capture);
210    ctx.with_id(ON_EVENT_VIEW_ID, |ctx| {
211        element_view.teardown(&mut state.child_state, ctx, element);
212    });
213}
214
215fn message_event_listener<State, Action, V, Event, OA, Callback>(
216    element_view: &V,
217    state: &mut OnEventState<V::ViewState>,
218    message: &mut MessageContext,
219    element: Mut<'_, V::Element>,
220    app_state: &mut State,
221    handler: &Callback,
222) -> MessageResult<Action>
223where
224    State: 'static,
225    Action: 'static,
226    V: DomView<State, Action>,
227    Event: JsCast + 'static + AnyDebug,
228    OA: OptionalAction<Action>,
229    Callback: Fn(&mut State, Event) -> OA + 'static,
230{
231    let Some(first) = message.take_first() else {
232        throw_str("Parent view of `OnEvent` sent outdated and/or incorrect empty view path");
233    };
234    if first != ON_EVENT_VIEW_ID {
235        throw_str("Parent view of `OnEvent` sent outdated and/or incorrect empty view path");
236    }
237    if message.remaining_path().is_empty() {
238        let event = message.take_message::<Event>().unwrap_throw();
239        match (handler)(app_state, *event).action() {
240            Some(a) => MessageResult::Action(a),
241            None => MessageResult::Nop,
242        }
243    } else {
244        element_view.message(&mut state.child_state, message, element, app_state)
245    }
246}
247
248impl<V, State, Action, Event, Callback> ViewMarker for OnEvent<V, State, Action, Event, Callback> {}
249impl<V, State, Action, Event, Callback, OA> View<State, Action, ViewCtx>
250    for OnEvent<V, State, Action, Event, Callback>
251where
252    State: 'static,
253    Action: 'static,
254    V: DomView<State, Action>,
255    OA: OptionalAction<Action>,
256    Callback: Fn(&mut State, Event) -> OA + 'static,
257    Event: JsCast + 'static + AnyDebug,
258{
259    type ViewState = OnEventState<V::ViewState>;
260
261    type Element = V::Element;
262
263    fn build(&self, ctx: &mut ViewCtx, app_state: &mut State) -> (Self::Element, Self::ViewState) {
264        build_event_listener::<_, _, _, Event>(
265            &self.dom_view,
266            &self.event,
267            self.capture,
268            self.passive,
269            ctx,
270            app_state,
271        )
272    }
273
274    fn rebuild(
275        &self,
276        prev: &Self,
277        view_state: &mut Self::ViewState,
278        ctx: &mut ViewCtx,
279        mut element: Mut<'_, Self::Element>,
280        app_state: &mut State,
281    ) {
282        // special case, where event name can change, so we can't reuse the rebuild_event_listener function above
283        ctx.with_id(ON_EVENT_VIEW_ID, |ctx| {
284            self.dom_view.rebuild(
285                &prev.dom_view,
286                &mut view_state.child_state,
287                ctx,
288                element.reborrow_mut(),
289                app_state,
290            );
291
292            let was_created = element.flags.was_created();
293            let needs_update = prev.capture != self.capture
294                || prev.passive != self.passive
295                || prev.event != self.event
296                || was_created;
297            if !needs_update {
298                return;
299            }
300            if !was_created {
301                remove_event_listener(
302                    element.as_ref(),
303                    &prev.event,
304                    &view_state.callback,
305                    prev.capture,
306                );
307            }
308
309            view_state.callback = create_event_listener::<Event>(
310                element.as_ref(),
311                &self.event,
312                self.capture,
313                self.passive,
314                ctx,
315            );
316        });
317    }
318
319    fn teardown(
320        &self,
321        view_state: &mut Self::ViewState,
322        ctx: &mut ViewCtx,
323        element: Mut<'_, Self::Element>,
324    ) {
325        teardown_event_listener(
326            &self.dom_view,
327            element,
328            &self.event,
329            view_state,
330            self.capture,
331            ctx,
332        );
333    }
334
335    fn message(
336        &self,
337        view_state: &mut Self::ViewState,
338        message: &mut MessageContext,
339        element: Mut<'_, Self::Element>,
340        app_state: &mut State,
341    ) -> MessageResult<Action> {
342        message_event_listener(
343            &self.dom_view,
344            view_state,
345            message,
346            element,
347            app_state,
348            &self.handler,
349        )
350    }
351}
352
353macro_rules! event_definitions {
354    ($(($ty_name:ident, $event_name:literal, $web_sys_ty:ident)),*) => {
355        $(
356        pub struct $ty_name<V, State, Action, Callback> {
357            pub(crate) dom_view: V,
358            pub(crate) capture: bool,
359            pub(crate) passive: bool,
360            pub(crate) handler: Callback,
361            pub(crate) phantom_event_ty: PhantomData<fn() -> (State, Action)>,
362        }
363
364        impl<V, State, Action, Callback> ViewMarker for $ty_name<V, State, Action, Callback> {}
365        impl<V, State, Action, Callback> $ty_name<V, State, Action, Callback> {
366            pub fn new(dom_view: V, handler: Callback) -> Self {
367                Self {
368                    dom_view,
369                    passive: true,
370                    capture: false,
371                    handler,
372                    phantom_event_ty: PhantomData,
373                }
374            }
375
376            /// Whether the event handler should be passive. (default = `true`)
377            ///
378            /// Passive event handlers can't prevent the browser's default action from
379            /// running (otherwise possible with `event.prevent_default()`), which
380            /// restricts what they can be used for, but reduces overhead.
381            pub fn passive(mut self, value: bool) -> Self {
382                self.passive = value;
383                self
384            }
385
386            /// Whether the event handler should capture the event *before* being dispatched to any `EventTarget` beneath it in the DOM tree. (default = `false`)
387            ///
388            /// Events that are bubbling upward through the tree will not trigger a listener designated to use capture.
389            /// Event bubbling and capturing are two ways of propagating events that occur in an element that is nested within another element,
390            /// when both elements have registered a handle for that event.
391            /// The event propagation mode determines the order in which elements receive the event.
392            // TODO use similar Nomenclature as gloo (Phase::Bubble/Phase::Capture)?
393            pub fn capture(mut self, value: bool) -> Self {
394                self.capture = value;
395                self
396            }
397        }
398
399
400        impl<V, State, Action, Callback, OA> View<State, Action, ViewCtx>
401            for $ty_name<V, State, Action, Callback>
402        where
403            State: 'static,
404            Action: 'static,
405            V: DomView<State, Action>,
406            OA: OptionalAction<Action> + 'static,
407            Callback: Fn(&mut State, web_sys::$web_sys_ty) -> OA + 'static,
408        {
409            type ViewState = OnEventState<V::ViewState>;
410
411            type Element = V::Element;
412
413            fn build(&self, ctx: &mut ViewCtx, app_state: &mut State) -> (Self::Element, Self::ViewState) {
414                build_event_listener::<_, _, _, web_sys::$web_sys_ty>(
415                    &self.dom_view,
416                    $event_name,
417                    self.capture,
418                    self.passive,
419                    ctx,
420                    app_state
421                )
422            }
423
424            fn rebuild(
425                &self,
426                prev: &Self,
427                view_state: &mut Self::ViewState,
428                ctx: &mut ViewCtx,
429                element: Mut<'_, Self::Element>,
430                app_state: &mut State
431            ) {
432                rebuild_event_listener::<_, _, _, web_sys::$web_sys_ty>(
433                    &self.dom_view,
434                    &prev.dom_view,
435                    element,
436                    $event_name,
437                    self.capture,
438                    self.passive,
439                    prev.capture,
440                    prev.passive,
441                    view_state,
442                    ctx,
443                    app_state
444                );
445            }
446
447            fn teardown(
448                &self,
449                view_state: &mut Self::ViewState,
450                ctx: &mut ViewCtx,
451                element: Mut<'_, Self::Element>,
452            ) {
453                teardown_event_listener(&self.dom_view, element, $event_name, view_state, self.capture, ctx);
454            }
455
456            fn message(
457                &self,
458                view_state: &mut Self::ViewState,
459                 message: &mut MessageContext,
460                 element: Mut<'_, Self::Element>,
461                app_state: &mut State,
462            ) -> MessageResult<Action> {
463                message_event_listener(&self.dom_view, view_state, message, element, app_state, &self.handler)
464            }
465        }
466        )*
467    };
468}
469
470event_definitions!(
471    (OnAbort, "abort", Event),
472    (OnAuxClick, "auxclick", PointerEvent),
473    (OnBeforeInput, "beforeinput", InputEvent),
474    (OnBeforeMatch, "beforematch", Event),
475    (OnBeforeToggle, "beforetoggle", Event),
476    (OnBlur, "blur", FocusEvent),
477    (OnCancel, "cancel", Event),
478    (OnCanPlay, "canplay", Event),
479    (OnCanPlayThrough, "canplaythrough", Event),
480    (OnChange, "change", Event),
481    (OnClick, "click", PointerEvent),
482    (OnClose, "close", Event),
483    (OnContextLost, "contextlost", Event),
484    (OnContextMenu, "contextmenu", PointerEvent),
485    (OnContextRestored, "contextrestored", Event),
486    (OnCopy, "copy", Event),
487    (OnCueChange, "cuechange", Event),
488    (OnCut, "cut", Event),
489    (OnDblClick, "dblclick", MouseEvent),
490    (OnDrag, "drag", Event),
491    (OnDragEnd, "dragend", Event),
492    (OnDragEnter, "dragenter", Event),
493    (OnDragLeave, "dragleave", Event),
494    (OnDragOver, "dragover", Event),
495    (OnDragStart, "dragstart", Event),
496    (OnDrop, "drop", Event),
497    (OnDurationChange, "durationchange", Event),
498    (OnEmptied, "emptied", Event),
499    (OnEnded, "ended", Event),
500    (OnError, "error", Event),
501    (OnFocus, "focus", FocusEvent),
502    (OnFocusIn, "focusin", FocusEvent),
503    (OnFocusOut, "focusout", FocusEvent),
504    (OnFormData, "formdata", Event),
505    (OnInput, "input", Event),
506    (OnInvalid, "invalid", Event),
507    (OnKeyDown, "keydown", KeyboardEvent),
508    (OnKeyUp, "keyup", KeyboardEvent),
509    (OnLoad, "load", Event),
510    (OnLoadedData, "loadeddata", Event),
511    (OnLoadedMetadata, "loadedmetadata", Event),
512    (OnLoadStart, "loadstart", Event),
513    (OnMouseDown, "mousedown", MouseEvent),
514    (OnMouseEnter, "mouseenter", MouseEvent),
515    (OnMouseLeave, "mouseleave", MouseEvent),
516    (OnMouseMove, "mousemove", MouseEvent),
517    (OnMouseOut, "mouseout", MouseEvent),
518    (OnMouseOver, "mouseover", MouseEvent),
519    (OnMouseUp, "mouseup", MouseEvent),
520    (OnPaste, "paste", Event),
521    (OnPause, "pause", Event),
522    (OnPlay, "play", Event),
523    (OnPlaying, "playing", Event),
524    (OnPointerCancel, "pointercancel", PointerEvent),
525    (OnPointerDown, "pointerdown", PointerEvent),
526    (OnPointerEnter, "pointerenter", PointerEvent),
527    (OnPointerLeave, "pointerleave", PointerEvent),
528    (OnPointerMove, "pointermove", PointerEvent),
529    (OnPointerOut, "pointerout", PointerEvent),
530    (OnPointerOver, "pointerover", PointerEvent),
531    (OnPointerRawUpdate, "pointerrawupdate", PointerEvent),
532    (OnPointerUp, "pointerup", PointerEvent),
533    (OnProgress, "progress", Event),
534    (OnRateChange, "ratechange", Event),
535    (OnReset, "reset", Event),
536    (OnScroll, "scroll", Event),
537    (OnScrollEnd, "scrollend", Event),
538    (OnSecurityPolicyViolation, "securitypolicyviolation", Event),
539    (OnSeeked, "seeked", Event),
540    (OnSeeking, "seeking", Event),
541    (OnSelect, "select", Event),
542    (OnSlotChange, "slotchange", Event),
543    (OnStalled, "stalled", Event),
544    (OnSubmit, "submit", Event),
545    (OnSuspend, "suspend", Event),
546    (OnTimeUpdate, "timeupdate", Event),
547    (OnToggle, "toggle", Event),
548    (OnVolumeChange, "volumechange", Event),
549    (OnWaiting, "waiting", Event),
550    (OnWheel, "wheel", WheelEvent)
551);
552
553pub struct OnResize<V, State, Action, Callback> {
554    pub(crate) dom_view: V,
555    pub(crate) handler: Callback,
556    pub(crate) phantom_event_ty: PhantomData<fn() -> (State, Action)>,
557}
558
559pub struct OnResizeState<VState> {
560    child_state: VState,
561    #[expect(
562        dead_code,
563        reason = "Closures are retained so they can be called by environment"
564    )]
565    callback: Closure<dyn FnMut(js_sys::Array)>,
566    observer: web_sys::ResizeObserver,
567}
568
569impl<V, State, Action, Callback> ViewMarker for OnResize<V, State, Action, Callback> {}
570impl<State, Action, OA, Callback, V: View<State, Action, ViewCtx>> View<State, Action, ViewCtx>
571    for OnResize<V, State, Action, Callback>
572where
573    State: 'static,
574    Action: 'static,
575    OA: OptionalAction<Action>,
576    Callback: Fn(&mut State, web_sys::ResizeObserverEntry) -> OA + 'static,
577    V: DomView<State, Action, DomNode: AsRef<web_sys::Element>>,
578{
579    type Element = V::Element;
580
581    type ViewState = OnResizeState<V::ViewState>;
582
583    fn build(&self, ctx: &mut ViewCtx, app_state: &mut State) -> (Self::Element, Self::ViewState) {
584        ctx.with_id(ON_EVENT_VIEW_ID, |ctx| {
585            let thunk = ctx.message_thunk();
586            let callback = Closure::new(move |entries: js_sys::Array| {
587                let entry: web_sys::ResizeObserverEntry = entries.at(0).unchecked_into();
588                thunk.push_message(entry);
589            });
590
591            let observer =
592                web_sys::ResizeObserver::new(callback.as_ref().unchecked_ref()).unwrap_throw();
593            let (element, child_state) = self.dom_view.build(ctx, app_state);
594            observer.observe(element.as_ref());
595
596            let state = OnResizeState {
597                child_state,
598                callback,
599                observer,
600            };
601
602            (element, state)
603        })
604    }
605
606    fn rebuild(
607        &self,
608        prev: &Self,
609        view_state: &mut Self::ViewState,
610        ctx: &mut ViewCtx,
611        mut element: Mut<'_, Self::Element>,
612        app_state: &mut State,
613    ) {
614        ctx.with_id(ON_EVENT_VIEW_ID, |ctx| {
615            self.dom_view.rebuild(
616                &prev.dom_view,
617                &mut view_state.child_state,
618                ctx,
619                element.reborrow_mut(),
620                app_state,
621            );
622            if element.flags.was_created() {
623                view_state.observer.disconnect();
624                view_state.observer.observe(element.as_ref());
625            }
626        });
627    }
628
629    fn teardown(
630        &self,
631        view_state: &mut Self::ViewState,
632        ctx: &mut ViewCtx,
633        element: Mut<'_, Self::Element>,
634    ) {
635        ctx.with_id(ON_EVENT_VIEW_ID, |ctx| {
636            view_state.observer.disconnect();
637            self.dom_view
638                .teardown(&mut view_state.child_state, ctx, element);
639        });
640    }
641
642    fn message(
643        &self,
644        view_state: &mut Self::ViewState,
645        message: &mut MessageContext,
646        element: Mut<'_, Self::Element>,
647        app_state: &mut State,
648    ) -> MessageResult<Action> {
649        let Some(first) = message.take_first() else {
650            throw_str("Parent view of `OnResize` sent outdated and/or incorrect empty view path");
651        };
652        if first != ON_EVENT_VIEW_ID {
653            throw_str("Parent view of `OnResize` sent outdated and/or incorrect empty view path");
654        }
655        if message.remaining_path().is_empty() {
656            let event = message
657                .take_message::<web_sys::ResizeObserverEntry>()
658                .unwrap_throw();
659            match (self.handler)(app_state, *event).action() {
660                Some(a) => MessageResult::Action(a),
661                None => MessageResult::Nop,
662            }
663        } else {
664            self.dom_view
665                .message(&mut view_state.child_state, message, element, app_state)
666        }
667    }
668}