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 std::{fmt::Write, marker::PhantomData, pin::Pin, task::Poll};
26
27use crate::{
28    js::value::JsValue,
29    js_cast::JsCast,
30    link::{Browser, RetrievalState},
31    protocol::{DEL, GET, REP, SET},
32};
33
34/// Listens for JavaScript callbacks.
35///
36/// This implements the [Stream][futures_core::Stream] trait;
37/// the stream yields callback events.
38///
39/// The [new_callback] function creates a Callback; go see how it is used.
40pub struct Callback<E> {
41    arr_id: u64,
42    ret_id: u64,
43    browser: Browser,
44    consumed: usize,
45    _phantom: PhantomData<Pin<Box<E>>>,
46}
47
48impl<E: JsCast> futures_core::Stream for Callback<E> {
49    type Item = E;
50
51    fn poll_next(
52        self: Pin<&mut Self>,
53        cx: &mut std::task::Context<'_>,
54    ) -> Poll<Option<Self::Item>> {
55        let this = self.get_mut();
56        let mut link = this.browser.0.lock().unwrap();
57        let ret_id = this.ret_id;
58        match link.retrievals.entry(ret_id) {
59            std::collections::hash_map::Entry::Occupied(mut occ) => {
60                let state = occ.get_mut();
61
62                let new_waker = cx.waker();
63                if !state.waker.will_wake(new_waker) {
64                    state.waker = new_waker.to_owned();
65                }
66
67                if state.times > this.consumed {
68                    this.consumed += 1;
69                    let val_id = link.get_new_id();
70                    let arr_id = this.arr_id;
71                    writeln!(
72                        link.raw_commands_buf(),
73                        "{SET}({val_id}, {GET}({arr_id}).shift());"
74                    )
75                    .unwrap();
76                    link.wake_outgoing_lazy();
77                    Poll::Ready(Some(JsCast::unchecked_from_js(JsValue {
78                        id: val_id,
79                        browser: this.browser.to_owned(),
80                    })))
81                } else {
82                    Poll::Pending
83                }
84            }
85            std::collections::hash_map::Entry::Vacant(vac) => {
86                vac.insert(RetrievalState {
87                    waker: cx.waker().to_owned(),
88                    last_value: String::new(),
89                    times: 0,
90                });
91                Poll::Pending
92            }
93        }
94    }
95}
96impl<E> Drop for Callback<E> {
97    fn drop(&mut self) {
98        let mut link = self.browser.0.lock().unwrap();
99        let ret_id = self.ret_id;
100        link.retrievals.remove(&ret_id);
101        let arr_id = self.arr_id;
102        writeln!(link.raw_commands_buf(), "{DEL}({arr_id});").unwrap();
103    }
104}
105
106/// Create a new Callback and a corresponding JavaScript function.
107///
108/// The returned Callback object is a stream. Every time the returned function is called,
109/// the stream will yield the call argument as value.
110pub fn new_callback<E>(browser: &Browser) -> (Callback<E>, JsValue) {
111    let mut link = browser.0.lock().unwrap();
112    let arr_id = link.get_new_id();
113    let ret_id = link.get_new_id();
114    let func_id = link.get_new_id();
115    let func = JsValue {
116        browser: browser.to_owned(),
117        id: func_id,
118    };
119    writeln!(link.raw_commands_buf(),
120"{SET}({arr_id}, []); {SET}({func_id}, function(e) {{ {GET}({arr_id}).push(e); {REP}({ret_id}, 0) }});").unwrap();
121    link.wake_outgoing_lazy();
122    let callback = Callback {
123        browser: browser.to_owned(),
124        ret_id,
125        arr_id,
126        consumed: 0,
127        _phantom: PhantomData,
128    };
129    (callback, func)
130}