mogwai 0.7.6

The minimal, obvious, graphical, widget application interface.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
//! # Event Future API
//!
//! This module provides a future-based API for handling both DOM events and
//! arbitrary JavaScript callbacks. It allows for asynchronous event handling
//! by resolving futures when events occur.
//!
//! ## Two layers
//!
//! The module provides two levels of abstraction:
//!
//! - **[`EventListener`]** - a high-level wrapper for DOM events. It registers
//!   a callback on a [`web_sys::EventTarget`] via `addEventListener` and
//!   produces a future that resolves with the [`web_sys::Event`] when the event
//!   fires. When the last clone of the `EventListener` is dropped, the
//!   underlying DOM listener is automatically removed.
//!
//! - **[`Listener`] / [`Callback`]** - a low-level primitive for wrapping *any*
//!   JavaScript callback, not just DOM events. This is useful for integrating
//!   with JS libraries that use plain callback properties (e.g.
//!   `graph.onNodeAdded = fn`) instead of the standard `addEventListener` /
//!   `removeEventListener` API.
//!
//! ## When to use which
//!
//! Use [`EventListener`] when you have a DOM element and a standard event name
//! (`"click"`, `"input"`, `"keydown"`, etc.). Use [`Listener`] / [`Callback`]
//! when you're bridging a JS library that expects you to assign a function to
//! one of its object properties — the kind of API where there's no
//! `EventTarget` involved and "remove" means setting the property back to
//! `null`.
//!
//! ## How `Listener` / `Callback` works
//!
//! [`Listener::new`] takes a `convert` function that maps raw [`JsValue`]
//! arguments (received from JS) into a domain type `O`. It returns a pair:
//!
//! ```text
//!   ┌───────────┐         ┌──────────┐
//!   │ Callback  │  ──→    │ JS calls │   The Callback owns a wasm-bindgen
//!   │ (give to  │         │ the fn   │   Closure whose JS Function is what
//!   │  JS)      │         └────┬─────┘   you hand to JavaScript.
//!   └───────────┘              │
//!//!   ┌───────────┐         ┌──────────┐
//!   │ Listener  │  ←──    │ convert  │   The convert function turns the
//!   │ (call     │         │ JsValue→ │   raw args into O, then wakes all
//!   │  .next()) │         │   O      │   outstanding futures.
//!   └─────┬─────┘         └──────────┘
//!//!//!   ┌───────────────┐
//!   │ impl Future   │   Each .next() call returns a fresh future.
//!   │ Output = O    │   Multiple awaiters all resolve on the next
//!   └───────────────┘   callback invocation (fan-out).
//! ```
//!
//! The [`Callback`] must be kept alive for as long as JavaScript might invoke
//! it. Dropping all clones of the `Callback` invalidates the underlying
//! `Closure` - any subsequent call from JS will throw `"closure invoked
//! recursively or after being dropped"`.
//!
//! ## Example: wrapping a plain JS callback property
//!
//! ```ignore
//! use mogwai::web::event::{Callback, Listener};
//! use wasm_bindgen::JsValue;
//!
//! // Create a listener that converts a single JsValue argument into an f64.
//! let (callback, listener): (Callback<(JsValue,)>, Listener<(JsValue,), f64>) =
//!     Listener::new(|(v,): (JsValue,)| v.as_f64().unwrap_or(0.0));
//!
//! // Hand the callback's JS function to the library. The callback must
//! // stay alive for as long as the library might call it.
//! some_js_object.set_on_event(Some(callback.function()));
//!
//! // Await the next event. Each .next() call returns a fresh future.
//! let value: f64 = listener.next().await;
//! ```
use std::{
    borrow::Cow, cell::RefCell, marker::PhantomData, ops::DerefMut, pin::Pin, rc::Rc, task::Waker,
};

use wasm_bindgen::{UnwrapThrowExt, convert::FromWasmAbi};
use wasm_bindgen_futures::wasm_bindgen::{JsCast, JsValue, prelude::Closure};

use crate::Str;

/// Maps a Rust tuple type to the correct `Closure<dyn FnMut(...)>` arity.
///
/// This trait is the mechanism by which [`Listener::new`] can work with
/// JavaScript callbacks of any arity (0 through 8). Each tuple type —
/// `()`, `(A,)`, `(A, B)`, ..., `(A, B, C, D, E, F, G, H)` — has an
/// implementation that selects the matching `Closure<dyn FnMut()>`,
/// `Closure<dyn FnMut(A)>`, `Closure<dyn FnMut(A, B)>`, etc.
///
/// The implementations for arities 3–8 are generated by the
/// `impl_parameters_tuples!` macro. Users do not implement this trait
/// themselves.
///
/// This is for internal use.
pub trait Parameters {
    type Closure: AsRef<JsValue> + 'static;

    fn into_arity_closure(f: Box<dyn FnMut(Self)>) -> Self::Closure;
}

impl Parameters for () {
    type Closure = Closure<dyn FnMut()>;

    fn into_arity_closure(mut f: Box<dyn FnMut(Self)>) -> Self::Closure {
        Closure::wrap(Box::new(move || {
            f(());
        }))
    }
}

impl<A: FromWasmAbi + 'static> Parameters for (A,) {
    type Closure = Closure<dyn FnMut(A)>;

    fn into_arity_closure(mut f: Box<dyn FnMut(Self)>) -> Self::Closure {
        Closure::wrap(Box::new(move |a| {
            f((a,));
        }))
    }
}

use crate as mogwai;
mogwai_macros::impl_parameters_tuples!((A, B));
mogwai_macros::impl_parameters_tuples!((A, B, C));
mogwai_macros::impl_parameters_tuples!((A, B, C, D));
mogwai_macros::impl_parameters_tuples!((A, B, C, D, E));
mogwai_macros::impl_parameters_tuples!((A, B, C, D, E, F));
mogwai_macros::impl_parameters_tuples!((A, B, C, D, E, F, G));
mogwai_macros::impl_parameters_tuples!((A, B, C, D, E, F, G, H));

/// A JS-callable function backed by a Rust [`Closure`], used to trigger
/// a [`Listener`].
///
/// A `Callback` is created by [`Listener::new`] alongside its paired
/// `Listener`. The [`Callback::function`] method returns a reference to
/// the underlying `js_sys::Function` — this is what you hand to JavaScript
/// (e.g. by assigning it to a plain object property like
/// `graph.onNodeAdded = callback.function().clone()`, or by passing it to
/// a `wasm_bindgen` setter).
///
/// # Lifetime
///
/// The `Callback` owns the `wasm_bindgen::Closure` that backs the JS
/// function. The `Callback` (or at least one clone of it) **must remain
/// alive for as long as JavaScript might invoke it**. If all clones are
/// dropped while JS still holds a reference to the function and calls it,
/// the call will throw `"closure invoked recursively or after being
/// dropped"`.
///
/// `Callback` is `Clone` — cloning is cheap (a single `Rc` refcount bump)
/// and each clone keeps the underlying `Closure` alive.
#[repr(transparent)]
pub struct Callback<Params> {
    inner: Rc<Box<dyn std::any::Any>>,
    _phantom: PhantomData<Params>,
}

impl<Params> Clone for Callback<Params> {
    fn clone(&self) -> Self {
        Self {
            inner: self.inner.clone(),
            _phantom: self._phantom,
        }
    }
}

impl<P: Parameters> Callback<P> {
    /// Return a reference to the callback as a JavaScript function.
    ///
    /// This is the value you pass to JavaScript — for example, by assigning
    /// it to a plain object property or handing it to a `wasm_bindgen`
    /// setter binding.
    pub fn function(&self) -> &web_sys::js_sys::Function {
        let b = self.inner.as_ref();
        let closure: &P::Closure = b
            .downcast_ref()
            .expect_throw("must construct as Parameters");
        let jsval: &JsValue = closure.as_ref();
        jsval.unchecked_ref()
    }
}

/// The future returned by [`Listener::next`].
///
/// Each call to `Listener::next` clones the current occurrence (a cheap
/// `Rc` clone). When the paired [`Callback`] is invoked, the occurrence
/// is filled with the converted value and all outstanding wakers are
/// notified — so every `next()` future that was created before the
/// callback fires resolves simultaneously (fan-out). After the event,
/// `Listener::next` begins a fresh occurrence for the next event.
///
/// This type is an implementation detail of [`Listener`] and is not
/// meant to be constructed directly.
#[derive(Clone)]
struct FutureEventOccurrence<T> {
    value: Rc<RefCell<Option<T>>>,
    wakers: Rc<RefCell<Vec<Waker>>>,
}

impl<T> Default for FutureEventOccurrence<T> {
    fn default() -> Self {
        Self {
            value: Default::default(),
            wakers: Default::default(),
        }
    }
}

impl<T: Clone> std::future::Future for FutureEventOccurrence<T> {
    type Output = T;

    fn poll(
        self: Pin<&mut Self>,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Self::Output> {
        if let Some(event) = self.value.borrow().as_ref() {
            std::task::Poll::Ready(event.clone())
        } else {
            // Store the waker for later.
            self.wakers.borrow_mut().push(cx.waker().clone());
            std::task::Poll::Pending
        }
    }
}

/// A generic listener that provides futures resolving when a JS callback
/// is invoked.
///
/// A `Listener` is created by [`Listener::new`] alongside its paired
/// [`Callback`]. The type parameters are:
///
/// - `I` — the input tuple type, matching the JavaScript callback's arity. For
///   a JS function called with no arguments, use `()`. For one argument, use
///   `(JsValue,)`. For five arguments, use `(JsValue, JsValue, JsValue,
///   JsValue, JsValue)`. Each element must implement
///   [`wasm_bindgen::convert::FromWasmAbi`].
/// - `O` — the output type that futures resolve to. This is the return value of
///   the `convert` function passed to [`Listener::new`]. Must be `Clone +
///   'static`.
///
/// `Listener` is `Clone` — cloning produces an independent handle that
/// shares the same underlying signal, so clones can be held in different
/// places and all see the same events.
///
/// See the [module-level documentation](crate::web::event) for a usage
/// example and architectural overview.
pub struct Listener<I, O> {
    /// The paired [`Callback`], kept so `Drop` can check if this is the
    /// last clone before removing the JS-side listener.
    callback: Rc<RefCell<Option<Callback<I>>>>,
    /// The broadcast signal that notifies all `.await` points when the
    /// event occurs.
    event: Rc<RefCell<FutureEventOccurrence<O>>>,
}

impl<I, O> Clone for Listener<I, O> {
    fn clone(&self) -> Self {
        Self {
            callback: self.callback.clone(),
            event: self.event.clone(),
        }
    }
}

impl<I, O> Default for Listener<I, O> {
    fn default() -> Self {
        Self {
            callback: Default::default(),
            event: Default::default(),
        }
    }
}

impl<I: Parameters, O: Clone + 'static> Listener<I, O> {
    /// Create a new listener and callback pair.
    ///
    /// The `convert` function receives a tuple `I` of raw JS values (one
    /// per argument the JS callback was called with) and returns a value
    /// of type `O` that futures will resolve to.
    ///
    /// The input type `I` must be a tuple whose arity matches the JS
    /// callback's parameter count. Supported arities are 0 through 8:
    ///
    /// | JS arity | `I` type                              |
    /// |----------|---------------------------------------|
    /// | 0        | `()`                                  |
    /// | 1        | `(JsValue,)`                          |
    /// | 2        | `(JsValue, JsValue)`                  |
    /// | 5        | `(JsValue, JsValue, JsValue, JsValue, JsValue)` |
    ///
    /// If the JS callback is called with fewer arguments than the declared
    /// arity (common in JS), the missing arguments arrive as
    /// [`JsValue::UNDEFINED`].
    ///
    /// # Examples
    ///
    /// Wrapping a JS callback that takes a single argument:
    ///
    /// ```ignore
    /// use mogwai::web::event::Listener;
    /// use wasm_bindgen::JsValue;
    ///
    /// let (callback, listener) = Listener::new(|(v,): (JsValue,)| {
    ///     v.as_f64().unwrap_or(0.0)
    /// });
    ///
    /// // Give `callback.function()` to JavaScript. Keep `callback` alive
    /// // for as long as JS might call it.
    /// some_js_obj.set_on_event(Some(callback.function()));
    ///
    /// // Await the next invocation.
    /// let value: f64 = listener.next().await;
    /// ```
    ///
    /// Wrapping a zero-argument JS callback:
    ///
    /// ```ignore
    /// let (callback, listener) = Listener::new(|()| 42u32);
    /// // JS calls `callback.function()` with no arguments.
    /// // `listener.next().await` resolves to `42`.
    /// ```
    pub fn new(mut convert: impl FnMut(I) -> O + 'static) -> (Callback<I>, Self) {
        let event: Rc<RefCell<FutureEventOccurrence<_>>> = Default::default();
        let listener_event = event.clone();
        let convert_and_wake = Box::new(move |params: I| {
            let val = convert(params);
            // Swap out the current occurrence, fill it with the converted
            // value, and wake all pending awaiters. The `mem::take`
            // replaces the occurrence in `listener_event` with a fresh
            // empty one, so subsequent `next()` calls await the *next*
            // event rather than replaying this one.
            //
            // Awaiters that called `next()` before this point hold clones
            // of the old occurrence (cheap `Rc` clones). They'll observe
            // the filled value on their next poll and resolve.
            let current = std::mem::take(listener_event.borrow_mut().deref_mut());
            *current.value.borrow_mut() = Some(val);
            let wakers = std::mem::take(current.wakers.borrow_mut().deref_mut());
            for waker in wakers.into_iter() {
                waker.wake();
            }
            // `current` drops here — the only remaining references to it
            // are the clones held by outstanding futures.
        }) as Box<dyn FnMut(I)>;
        let closure = I::into_arity_closure(convert_and_wake);

        let callback = Callback {
            inner: Rc::new(Box::new(closure)),
            _phantom: PhantomData::<I>,
        };
        let listener = Self {
            callback: Rc::new(RefCell::new(Some(callback.clone()))),
            event,
        };
        (callback, listener)
    }

    /// Produces a future that resolves when the callback is next invoked.
    ///
    /// Each call returns a fresh future. If multiple futures are created
    /// before the next callback invocation, they all resolve simultaneously
    /// with the same value (fan-out). After an event resolves, a subsequent
    /// `next()` call awaits the *next* event, not a past one.
    pub fn next(&self) -> impl std::future::Future<Output = O> {
        self.event.borrow().clone()
    }
}

/// A convenience wrapper around [`Listener`] for DOM events.
///
/// `EventListener` registers a callback on a [`web_sys::EventTarget`] via
/// `addEventListener` and produces futures that resolve with the
/// [`web_sys::Event`] when the event fires. It is built on top of
/// [`Listener<(JsValue,), web_sys::Event>`] — the `EventListener::next`
/// method just delegates to `Listener::next`.
///
/// When the last clone of an `EventListener` is dropped, the underlying
/// DOM listener is removed via `removeEventListener`, preventing memory
/// leaks and stale callbacks.
///
/// For wrapping non-DOM JavaScript callbacks (plain object properties
/// like `graph.onNodeAdded = fn`), use [`Listener`] / [`Callback`]
/// directly.
#[derive(Clone)]
pub struct EventListener {
    /// The DOM target that the event listener is registered upon.
    target: web_sys::EventTarget,
    /// The name of the event being listened for (e.g. `"click"`).
    event_name: Str,
    /// The underlying [`Listener`] that drives [`EventListener::next`].
    listener: Listener<(JsValue,), web_sys::Event>,
}

impl Drop for EventListener {
    fn drop(&mut self) {
        // Only remove the DOM listener when the last clone is being
        // dropped. If other clones exist, they still need the callback
        // alive.
        if Rc::strong_count(&self.listener.callback) == 1
            && let Some(callback) = self.listener.callback.take()
        {
            self.target
                .remove_event_listener_with_callback(&self.event_name, callback.function())
                .unwrap();
        }
    }
}

impl EventListener {
    /// Create a new event listener.
    ///
    /// This registers `event_name` on `target` via `addEventListener`.
    ///
    /// Use [`EventListener::next`] to await an event occurrence.
    pub fn new(
        target: impl AsRef<web_sys::EventTarget>,
        event_name: impl Into<Cow<'static, str>>,
    ) -> Self {
        let (callback, listener) = Listener::new(|(val,): (JsValue,)| {
            // UNCHECKED: safe because this is an event callback, and events in JS are all
            // `Event`.
            let ev: web_sys::Event = val.unchecked_into();
            ev
        });

        let event_name = event_name.into();
        let target = target.as_ref().clone();
        target
            .add_event_listener_with_callback(&event_name, callback.function())
            .unwrap();

        Self {
            target,
            event_name,
            listener,
        }
    }

    /// Produces a future that resolves when the event occurs.
    ///
    /// Each call returns a fresh future. If multiple futures are created
    /// before the next event, they all resolve simultaneously (fan-out).
    /// After an event resolves, a subsequent `next()` call awaits the
    /// *next* event.
    pub fn next(&self) -> impl std::future::Future<Output = web_sys::Event> {
        self.listener.next()
    }
}

#[cfg(all(test, target_arch = "wasm32"))]
mod test {
    use super::*;
    use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};

    wasm_bindgen_test_configure!(run_in_browser);

    #[wasm_bindgen_test]
    async fn callback_zero_arity_resolves_future() {
        let (callback, listener) = Listener::new(|_: ()| {});

        wasm_bindgen_futures::spawn_local(async move {
            // Call the callback's JS function with no arguments.
            let f = callback.function().clone();
            f.call0(&JsValue::NULL).unwrap();
        });

        // The future should now be ready.
        let result = listener.next().await;
        assert_eq!(result, ());
    }

    /// 1-arity callback (the common case)
    #[wasm_bindgen_test]
    async fn callback_one_arity_resolves_with_value() {
        let (callback, listener) = Listener::new(|(v,): (JsValue,)| v.as_f64().unwrap_or(0.0));
        wasm_bindgen_futures::spawn_local(async move {
            let f = callback.function().clone();
            f.call1(&JsValue::NULL, &JsValue::from_f64(42.0)).unwrap();
        });
        let result = listener.next().await;
        assert_eq!(result, 42.0);
    }

    /// 2-arity callback (multi-arg, tests the tuple path)
    #[wasm_bindgen_test]
    async fn callback_two_arity_resolves_with_tuple() {
        let (callback, listener) = Listener::new(|(a, b): (JsValue, JsValue)| {
            (a.as_f64().unwrap_or(0.0), b.as_string().unwrap_or_default())
        });
        wasm_bindgen_futures::spawn_local(async move {
            let f = callback.function().clone();
            f.call2(
                &JsValue::NULL,
                &JsValue::from_f64(7.0),
                &JsValue::from_str("hello"),
            )
            .unwrap();
        });
        let (n, s) = listener.next().await;
        assert_eq!(n, 7.0);
        assert_eq!(s, "hello");
    }

    /// Multiple .next() calls all resolve on one callback invocation
    /// (fan-out)
    #[wasm_bindgen_test]
    async fn multiple_next_calls_all_resolve() {
        let (callback, listener) = Listener::new(|(v,): (JsValue,)| v.as_f64().unwrap_or(0.0));

        // Create two independent futures before firing.
        let fut1 = listener.next();
        let fut2 = listener.next();

        wasm_bindgen_futures::spawn_local(async move {
            let f = callback.function().clone();
            f.call1(&JsValue::NULL, &JsValue::from_f64(99.0)).unwrap();
        });

        // Both should resolve to the same value.
        assert_eq!(fut1.await, 99.0);
        assert_eq!(fut2.await, 99.0);
    }

    /// Sequential events — first .next() resolves, then a new .next()
    /// awaits // the next event
    #[wasm_bindgen_test]
    async fn sequential_events_resolve_in_order() {
        let (callback, listener) = Listener::new(|(v,): (JsValue,)| v.as_f64().unwrap_or(0.0));
        let f = callback.function();

        wasm_bindgen_futures::spawn_local({
            let f = f.clone();
            async move {
                f.call1(&JsValue::NULL, &JsValue::from_f64(1.0)).unwrap();
            }
        });
        assert_eq!(listener.next().await, 1.0);

        wasm_bindgen_futures::spawn_local({
            let f = f.clone();
            async move {
                f.call1(&JsValue::NULL, &JsValue::from_f64(2.0)).unwrap();
            }
        });
        assert_eq!(listener.next().await, 2.0);
    }

    /// EventListener still works (regression test for the existing
    /// DOM path)
    #[wasm_bindgen_test]
    async fn event_listener_resolves_on_dom_event() {
        use web_sys::HtmlElement;
        let el = mogwai::web::document()
            .create_element("button")
            .unwrap()
            .dyn_into::<HtmlElement>()
            .unwrap();
        let listener = EventListener::new(&el, "click");

        wasm_bindgen_futures::spawn_local(async move {
            // Dispatch a synthetic click.
            let event = web_sys::Event::new("click").unwrap();
            el.dispatch_event(&event).unwrap();
        });

        let ev = listener.next().await;
        assert_eq!(ev.type_(), "click");
    }
}