1use 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
15const ON_EVENT_VIEW_ID: ViewId = ViewId::new(0x2357_1113);
18
19#[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 pub fn passive(mut self, value: bool) -> Self {
53 self.passive = value;
54 self
55 }
56
57 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 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 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
128fn 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 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 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 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 pub fn passive(mut self, value: bool) -> Self {
382 self.passive = value;
383 self
384 }
385
386 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}