1
0

protocol.rs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. use crate::document::NATIVE_EVAL_JS;
  2. use crate::{assets::*, webview::WebviewEdits};
  3. use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS;
  4. use dioxus_interpreter_js::NATIVE_JS;
  5. use wry::{
  6. http::{status::StatusCode, Request, Response},
  7. RequestAsyncResponder,
  8. };
  9. #[cfg(any(target_os = "android", target_os = "windows"))]
  10. const EDITS_PATH: &str = "http://dioxus.index.html/__edits";
  11. #[cfg(not(any(target_os = "android", target_os = "windows")))]
  12. const EDITS_PATH: &str = "dioxus://index.html/__edits";
  13. #[cfg(any(target_os = "android", target_os = "windows"))]
  14. const EVENTS_PATH: &str = "http://dioxus.index.html/__events";
  15. #[cfg(not(any(target_os = "android", target_os = "windows")))]
  16. const EVENTS_PATH: &str = "dioxus://index.html/__events";
  17. static DEFAULT_INDEX: &str = include_str!("./index.html");
  18. #[allow(clippy::too_many_arguments)] // just for now, should fix this eventually
  19. /// Handle a request from the webview
  20. ///
  21. /// - Tries to stream edits if they're requested.
  22. /// - If that doesn't match, tries a user provided asset handler
  23. /// - If that doesn't match, tries to serve a file from the filesystem
  24. pub(super) fn desktop_handler(
  25. request: Request<Vec<u8>>,
  26. asset_handlers: AssetHandlerRegistry,
  27. responder: RequestAsyncResponder,
  28. edit_state: &WebviewEdits,
  29. custom_head: Option<String>,
  30. custom_index: Option<String>,
  31. root_name: &str,
  32. headless: bool,
  33. ) {
  34. // Try to serve the index file first
  35. if let Some(index_bytes) =
  36. index_request(&request, custom_head, custom_index, root_name, headless)
  37. {
  38. return responder.respond(index_bytes);
  39. }
  40. // If the request is asking for edits (ie binary protocol streaming), do that
  41. let trimmed_uri = request.uri().path().trim_matches('/');
  42. if trimmed_uri == "__edits" {
  43. return edit_state.wry_queue.handle_request(responder);
  44. }
  45. // If the request is asking for an event response, do that
  46. if trimmed_uri == "__events" {
  47. return edit_state.handle_event(request, responder);
  48. }
  49. // todo: we want to move the custom assets onto a different protocol or something
  50. if let Some(name) = request.uri().path().split('/').nth(1) {
  51. if asset_handlers.has_handler(name) {
  52. let _name = name.to_string();
  53. return asset_handlers.handle_request(&_name, request, responder);
  54. }
  55. }
  56. match dioxus_asset_resolver::serve_asset_from_raw_path(request.uri().path()) {
  57. Ok(res) => responder.respond(res),
  58. Err(_e) => responder.respond(
  59. Response::builder()
  60. .status(StatusCode::INTERNAL_SERVER_ERROR)
  61. .body(String::from("Failed to serve asset").into_bytes())
  62. .unwrap(),
  63. ),
  64. }
  65. }
  66. /// Build the index.html file we use for bootstrapping a new app
  67. ///
  68. /// We use wry/webview by building a special index.html that forms a bridge between the webview and your rust code
  69. ///
  70. /// This is similar to tauri, except we give more power to your rust code and less power to your frontend code.
  71. /// This lets us skip a build/bundle step - your code just works - but limits how your Rust code can actually
  72. /// mess with UI elements. We make this decision since other renderers like LiveView are very separate and can
  73. /// never properly bridge the gap. Eventually of course, the idea is to build a custom CSS/HTML renderer where you
  74. /// *do* have native control over elements, but that still won't work with liveview.
  75. fn index_request(
  76. request: &Request<Vec<u8>>,
  77. custom_head: Option<String>,
  78. custom_index: Option<String>,
  79. root_name: &str,
  80. headless: bool,
  81. ) -> Option<Response<Vec<u8>>> {
  82. // If the request is for the root, we'll serve the index.html file.
  83. if request.uri().path() != "/" {
  84. return None;
  85. }
  86. // Load a custom index file if provided
  87. let mut index = custom_index.unwrap_or_else(|| DEFAULT_INDEX.to_string());
  88. // Insert a custom head if provided
  89. // We look just for the closing head tag. If a user provided a custom index with weird syntax, this might fail
  90. if let Some(head) = custom_head {
  91. index.insert_str(index.find("</head>").expect("Head element to exist"), &head);
  92. }
  93. // Inject our module loader by looking for a body tag
  94. // A failure mode here, obviously, is if the user provided a custom index without a body tag
  95. // Might want to document this
  96. index.insert_str(
  97. index.find("</body>").expect("Body element to exist"),
  98. &module_loader(root_name, headless),
  99. );
  100. Response::builder()
  101. .header("Content-Type", "text/html")
  102. .header("Access-Control-Allow-Origin", "*")
  103. .body(index.into())
  104. .ok()
  105. }
  106. /// Construct the inline script that boots up the page and bridges the webview with rust code.
  107. ///
  108. /// The arguments here:
  109. /// - root_name: the root element (by Id) that we stream edits into
  110. /// - headless: is this page being loaded but invisible? Important because not all windows are visible and the
  111. /// interpreter can't connect until the window is ready.
  112. fn module_loader(root_id: &str, headless: bool) -> String {
  113. format!(
  114. r#"
  115. <script type="module">
  116. // Bring the sledgehammer code
  117. {SLEDGEHAMMER_JS}
  118. // And then extend it with our native bindings
  119. {NATIVE_JS}
  120. // The native interpreter extends the sledgehammer interpreter with a few extra methods that we use for IPC
  121. window.interpreter = new NativeInterpreter("{EDITS_PATH}", "{EVENTS_PATH}");
  122. // Wait for the page to load before sending the initialize message
  123. window.onload = function() {{
  124. let root_element = window.document.getElementById("{root_id}");
  125. if (root_element != null) {{
  126. window.interpreter.initialize(root_element);
  127. window.ipc.postMessage(window.interpreter.serializeIpcMessage("initialize"));
  128. }}
  129. window.interpreter.waitForRequest({headless});
  130. }}
  131. </script>
  132. <script type="module">
  133. // Include the code for eval
  134. {NATIVE_EVAL_JS}
  135. </script>
  136. "#
  137. )
  138. }