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
66pub 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#[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 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 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 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 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 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
135const 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
144struct 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 fn create(js: String) -> GenerationalBox<Box<dyn Evaluator>> {
154 let owner = UnsyncStorage::owner();
155
156 let channels = WebDioxusChannel::new(JSOwner::new(owner.clone()));
158
159 let weak_channels = channels.weak();
161
162 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 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 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 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}