1
0

document.rs 8.3 KB

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