dioxus_web/
document.rs

1use dioxus_core::prelude::queue_effect;
2use dioxus_core::ScopeId;
3use dioxus_document::{
4    create_element_in_head, Document, Eval, EvalError, Evaluator, LinkProps, MetaProps,
5    ScriptProps, StyleProps,
6};
7use dioxus_history::History;
8use futures_util::FutureExt;
9use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
10use js_sys::Function;
11use serde::Serialize;
12use serde_json::Value;
13use std::future::Future;
14use std::pin::Pin;
15use std::result;
16use std::{rc::Rc, str::FromStr};
17use wasm_bindgen::prelude::*;
18use wasm_bindgen_futures::JsFuture;
19
20use crate::history::WebHistory;
21
22#[wasm_bindgen::prelude::wasm_bindgen]
23pub struct JSOwner {
24    _owner: Box<dyn std::any::Any>,
25}
26
27impl JSOwner {
28    pub fn new(owner: impl std::any::Any) -> Self {
29        Self {
30            _owner: Box::new(owner),
31        }
32    }
33}
34
35#[wasm_bindgen::prelude::wasm_bindgen(module = "/src/js/eval.js")]
36extern "C" {
37    pub type WebDioxusChannel;
38
39    #[wasm_bindgen(constructor)]
40    pub fn new(owner: JSOwner) -> WebDioxusChannel;
41
42    #[wasm_bindgen(method, js_name = "rustSend")]
43    pub fn rust_send(this: &WebDioxusChannel, value: wasm_bindgen::JsValue);
44
45    #[wasm_bindgen(method, js_name = "rustRecv")]
46    pub async fn rust_recv(this: &WebDioxusChannel) -> wasm_bindgen::JsValue;
47
48    #[wasm_bindgen(method)]
49    pub fn send(this: &WebDioxusChannel, value: wasm_bindgen::JsValue);
50
51    #[wasm_bindgen(method)]
52    pub async fn recv(this: &WebDioxusChannel) -> wasm_bindgen::JsValue;
53
54    #[wasm_bindgen(method)]
55    pub fn weak(this: &WebDioxusChannel) -> WeakDioxusChannel;
56
57    pub type WeakDioxusChannel;
58
59    #[wasm_bindgen(method, js_name = "rustSend")]
60    pub fn rust_send(this: &WeakDioxusChannel, value: wasm_bindgen::JsValue);
61
62    #[wasm_bindgen(method, js_name = "rustRecv")]
63    pub async fn rust_recv(this: &WeakDioxusChannel) -> wasm_bindgen::JsValue;
64}
65
66/// Provides the Document through [`ScopeId::provide_context`].
67pub fn init_document() {
68    let provider: Rc<dyn Document> = Rc::new(WebDocument);
69    if ScopeId::ROOT.has_context::<Rc<dyn Document>>().is_none() {
70        ScopeId::ROOT.provide_context(provider);
71    }
72    let history_provider: Rc<dyn History> = Rc::new(WebHistory::default());
73    if ScopeId::ROOT.has_context::<Rc<dyn History>>().is_none() {
74        ScopeId::ROOT.provide_context(history_provider);
75    }
76}
77
78/// The web-target's document provider.
79#[derive(Clone)]
80pub struct WebDocument;
81impl Document for WebDocument {
82    fn eval(&self, js: String) -> Eval {
83        Eval::new(WebEvaluator::create(js))
84    }
85
86    /// Set the title of the document
87    fn set_title(&self, title: String) {
88        let myself = self.clone();
89        queue_effect(move || {
90            myself.eval(format!("document.title = {title:?};"));
91        });
92    }
93
94    /// Create a new meta tag in the head
95    fn create_meta(&self, props: MetaProps) {
96        let myself = self.clone();
97        queue_effect(move || {
98            myself.eval(create_element_in_head("meta", &props.attributes(), None));
99        });
100    }
101
102    /// Create a new script tag in the head
103    fn create_script(&self, props: ScriptProps) {
104        let myself = self.clone();
105        queue_effect(move || {
106            myself.eval(create_element_in_head(
107                "script",
108                &props.attributes(),
109                props.script_contents().ok(),
110            ));
111        });
112    }
113
114    /// Create a new style tag in the head
115    fn create_style(&self, props: StyleProps) {
116        let myself = self.clone();
117        queue_effect(move || {
118            myself.eval(create_element_in_head(
119                "style",
120                &props.attributes(),
121                props.style_contents().ok(),
122            ));
123        });
124    }
125
126    /// Create a new link tag in the head
127    fn create_link(&self, props: LinkProps) {
128        let myself = self.clone();
129        queue_effect(move || {
130            myself.eval(create_element_in_head("link", &props.attributes(), None));
131        });
132    }
133}
134
135/// Required to avoid blocking the Rust WASM thread.
136const PROMISE_WRAPPER: &str = r#"
137    return (async function(){
138        {JS_CODE}
139    })();
140"#;
141
142type NextPoll = Pin<Box<dyn Future<Output = Result<serde_json::Value, EvalError>>>>;
143
144/// Represents a web-target's JavaScript evaluator.
145struct WebEvaluator {
146    channels: WeakDioxusChannel,
147    next_future: Option<NextPoll>,
148    result: Pin<Box<dyn Future<Output = result::Result<Value, EvalError>>>>,
149}
150
151impl WebEvaluator {
152    /// Creates a new evaluator for web-based targets.
153    fn create(js: String) -> GenerationalBox<Box<dyn Evaluator>> {
154        let owner = UnsyncStorage::owner();
155
156        // add the drop handler to DioxusChannel so that it gets dropped when the channel is dropped in js
157        let channels = WebDioxusChannel::new(JSOwner::new(owner.clone()));
158
159        // The Rust side of the channel is a weak reference to the DioxusChannel
160        let weak_channels = channels.weak();
161
162        // Wrap the evaluated JS in a promise so that wasm can continue running (send/receive data from js)
163        let code = PROMISE_WRAPPER.replace("{JS_CODE}", &js);
164
165        let result = match Function::new_with_args("dioxus", &code).call1(&JsValue::NULL, &channels)
166        {
167            Ok(result) => {
168                let future = js_sys::Promise::resolve(&result);
169                let js_future = JsFuture::from(future);
170                Box::pin(async move {
171                    let result = js_future.await.map_err(|e| {
172                        EvalError::Communication(format!("Failed to await result - {:?}", e))
173                    })?;
174                    let stringified = js_sys::JSON::stringify(&result).map_err(|e| {
175                        EvalError::Communication(format!("Failed to stringify result - {:?}", e))
176                    })?;
177                    if !stringified.is_undefined() && stringified.is_valid_utf16() {
178                        let string: String = stringified.into();
179                        Value::from_str(&string).map_err(|e| {
180                            EvalError::Communication(format!("Failed to parse result - {}", e))
181                        })
182                    } else {
183                        Err(EvalError::Communication(
184                            "Failed to stringify result - undefined or not valid utf16".to_string(),
185                        ))
186                    }
187                })
188                    as Pin<Box<dyn Future<Output = result::Result<Value, EvalError>>>>
189            }
190            Err(err) => Box::pin(futures_util::future::ready(Err(EvalError::InvalidJs(
191                err.as_string().unwrap_or("unknown".to_string()),
192            )))),
193        };
194
195        owner.insert(Box::new(Self {
196            channels: weak_channels,
197            result,
198            next_future: None,
199        }) as Box<dyn Evaluator>)
200    }
201}
202
203impl Evaluator for WebEvaluator {
204    /// Runs the evaluated JavaScript.
205    fn poll_join(
206        &mut self,
207        cx: &mut std::task::Context<'_>,
208    ) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
209        self.result.poll_unpin(cx)
210    }
211
212    /// Sends a message to the evaluated JavaScript.
213    fn send(&self, data: serde_json::Value) -> Result<(), EvalError> {
214        let serializer = serde_wasm_bindgen::Serializer::json_compatible();
215
216        let data = match data.serialize(&serializer) {
217            Ok(d) => d,
218            Err(e) => return Err(EvalError::Communication(e.to_string())),
219        };
220
221        self.channels.rust_send(data);
222        Ok(())
223    }
224
225    /// Gets an UnboundedReceiver to receive messages from the evaluated JavaScript.
226    fn poll_recv(
227        &mut self,
228        context: &mut std::task::Context<'_>,
229    ) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
230        if self.next_future.is_none() {
231            let channels: WebDioxusChannel = self.channels.clone().into();
232            let pinned = Box::pin(async move {
233                let fut = channels.rust_recv();
234                let data = fut.await;
235                serde_wasm_bindgen::from_value::<serde_json::Value>(data)
236                    .map_err(|err| EvalError::Communication(err.to_string()))
237            });
238            self.next_future = Some(pinned);
239        }
240        let fut = self.next_future.as_mut().unwrap();
241        let mut pinned = std::pin::pin!(fut);
242        let result = pinned.as_mut().poll(context);
243        if result.is_ready() {
244            self.next_future = None;
245        }
246        result
247    }
248}