px_wsdom_core/interaction/
callback.rs

1/*!
2Interactivity for handling JS events.
3
4WSDOM is **one-way**, so JS code cannot directly call Rust code.
5Instead, we adopt a mechanism based on async.
6
7The [new_callback] function returns a Rust [Stream][futures_core::Stream] `s` *and*
8a JavaScript object `f` that can be called (i.e. a function/closure).
9Each time `f` is called like `f(arg)`,
10the Rust stream will yield the `arg` object.
11
12```rust
13# use wsdom_core::Browser;
14async fn example(browser: &Browser, button: wsdom::dom::HTMLButtonElement) {
15    let (mut stream, func) = wsdom::callback::new_callback::<wsdom::dom::MouseEvent>(&browser);
16    button.add_event_listener(&"click", &func, &wsdom::undefined());
17
18    use futures_util::StreamExt;
19    let _click_event: Option<wsdom::dom::MouseEvent> = stream.next().await;
20    println!("the button was clicked!");
21}
22```
23*/
24
25use core::{fmt::Write, marker::PhantomData, pin::Pin, task::Poll};
26
27use alloc::{borrow::ToOwned, boxed::Box, string::String};
28
29use crate::{
30    js::value::JsValue,
31    js_cast::JsCast,
32    link::{Browser, RetrievalState},
33    protocol::{DEL, GET, REP, SET},
34};
35
36/// Listens for JavaScript callbacks.
37///
38/// This implements the [Stream][futures_core::Stream] trait;
39/// the stream yields callback events.
40///
41/// The [new_callback] function creates a Callback; go see how it is used.
42pub struct Callback<E> {
43    arr_id: u64,
44    ret_id: u64,
45    browser: Browser,
46    consumed: usize,
47    _phantom: PhantomData<Pin<Box<E>>>,
48}
49
50impl<E: JsCast> futures_core::Stream for Callback<E> {
51    type Item = E;
52
53    fn poll_next(
54        self: Pin<&mut Self>,
55        cx: &mut core::task::Context<'_>,
56    ) -> Poll<Option<Self::Item>> {
57        let this = self.get_mut();
58        let mut link = this.browser.0.lock();
59        let ret_id = this.ret_id;
60        match link.retrievals.entry(ret_id) {
61           hashbrown::hash_map::Entry::Occupied(mut occ) => {
62                let state = occ.get_mut();
63
64                let new_waker = cx.waker();
65                if !state.waker.will_wake(new_waker) {
66                    state.waker = new_waker.to_owned();
67                }
68
69                if state.times > this.consumed {
70                    this.consumed += 1;
71                    let val_id = link.get_new_id();
72                    let arr_id = this.arr_id;
73                    writeln!(
74                        link.raw_commands_buf(),
75                        "{SET}({val_id}, {GET}({arr_id}).shift());"
76                    )
77                    .unwrap();
78                    link.wake_outgoing_lazy();
79                    Poll::Ready(Some(JsCast::unchecked_from_js(JsValue {
80                        id: val_id,
81                        browser: this.browser.to_owned(),
82                    })))
83                } else {
84                    Poll::Pending
85                }
86            }
87          hashbrown::hash_map::Entry::Vacant(vac) => {
88                vac.insert(RetrievalState {
89                    waker: cx.waker().to_owned(),
90                    last_value: String::new(),
91                    times: 0,
92                });
93                Poll::Pending
94            }
95        }
96    }
97}
98impl<E> Drop for Callback<E> {
99    fn drop(&mut self) {
100        let mut link = self.browser.0.lock();
101        let ret_id = self.ret_id;
102        link.retrievals.remove(&ret_id);
103        let arr_id = self.arr_id;
104        writeln!(link.raw_commands_buf(), "{DEL}({arr_id});").unwrap();
105    }
106}
107
108/// Create a new Callback and a corresponding JavaScript function.
109///
110/// The returned Callback object is a stream. Every time the returned function is called,
111/// the stream will yield the call argument as value.
112pub fn new_callback<E>(browser: &Browser) -> (Callback<E>, JsValue) {
113    let mut link = browser.0.lock();
114    let arr_id = link.get_new_id();
115    let ret_id = link.get_new_id();
116    let func_id = link.get_new_id();
117    let func = JsValue {
118        browser: browser.to_owned(),
119        id: func_id,
120    };
121    writeln!(link.raw_commands_buf(),
122"{SET}({arr_id}, []); {SET}({func_id}, function(e) {{ {GET}({arr_id}).push(e); {REP}({ret_id}, 0) }});").unwrap();
123    link.wake_outgoing_lazy();
124    let callback = Callback {
125        browser: browser.to_owned(),
126        ret_id,
127        arr_id,
128        consumed: 0,
129        _phantom: PhantomData,
130    };
131    (callback, func)
132}