webview.rs 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. use crate::{
  2. app::SharedContext, assets::AssetHandlerRegistry, edits::EditQueue, eval::DesktopEvalProvider,
  3. file_upload::NativeFileHover, ipc::UserWindowEvent, protocol, waker::tao_waker, Config,
  4. DesktopContext, DesktopService,
  5. };
  6. use dioxus_core::{ScopeId, VirtualDom};
  7. use dioxus_html::prelude::EvalProvider;
  8. use futures_util::{pin_mut, FutureExt};
  9. use std::{any::Any, rc::Rc, task::Waker};
  10. use wry::{RequestAsyncResponder, WebContext, WebViewBuilder};
  11. pub(crate) struct WebviewInstance {
  12. pub dom: VirtualDom,
  13. pub desktop_context: DesktopContext,
  14. pub waker: Waker,
  15. // Wry assumes the webcontext is alive for the lifetime of the webview.
  16. // We need to keep the webcontext alive, otherwise the webview will crash
  17. _web_context: WebContext,
  18. // Same with the menu.
  19. // Currently it's a box<dyn any> because 1) we don't touch it and 2) we support a number of platforms
  20. // like ios where muda does not give us a menu type. It sucks but alas.
  21. //
  22. // This would be a good thing for someone looking to contribute to fix.
  23. _menu: Option<Box<dyn Any>>,
  24. }
  25. impl WebviewInstance {
  26. pub(crate) fn new(
  27. mut cfg: Config,
  28. dom: VirtualDom,
  29. shared: Rc<SharedContext>,
  30. ) -> WebviewInstance {
  31. let mut window = cfg.window.clone();
  32. // tao makes small windows for some reason, make them bigger
  33. if cfg.window.window.inner_size.is_none() {
  34. window = window.with_inner_size(tao::dpi::LogicalSize::new(800.0, 600.0));
  35. }
  36. // We assume that if the icon is None in cfg, then the user just didnt set it
  37. if cfg.window.window.window_icon.is_none() {
  38. window = window.with_window_icon(Some(
  39. tao::window::Icon::from_rgba(
  40. include_bytes!("./assets/default_icon.bin").to_vec(),
  41. 460,
  42. 460,
  43. )
  44. .expect("image parse failed"),
  45. ));
  46. }
  47. let window = window.build(&shared.target).unwrap();
  48. let mut web_context = WebContext::new(cfg.data_dir.clone());
  49. let edit_queue = EditQueue::default();
  50. let file_hover = NativeFileHover::default();
  51. let asset_handlers = AssetHandlerRegistry::new(dom.runtime());
  52. let headless = !cfg.window.window.visible;
  53. // Rust :(
  54. let window_id = window.id();
  55. let custom_head = cfg.custom_head.clone();
  56. let index_file = cfg.custom_index.clone();
  57. let root_name = cfg.root_name.clone();
  58. let asset_handlers_ = asset_handlers.clone();
  59. let edit_queue_ = edit_queue.clone();
  60. let proxy_ = shared.proxy.clone();
  61. let file_hover_ = file_hover.clone();
  62. let request_handler = move |request, responder: RequestAsyncResponder| {
  63. // Try to serve the index file first
  64. let index_bytes = protocol::index_request(
  65. &request,
  66. custom_head.clone(),
  67. index_file.clone(),
  68. &root_name,
  69. headless,
  70. );
  71. // Otherwise, try to serve an asset, either from the user or the filesystem
  72. match index_bytes {
  73. Some(body) => responder.respond(body),
  74. None => protocol::desktop_handler(
  75. request,
  76. asset_handlers_.clone(),
  77. &edit_queue_,
  78. responder,
  79. ),
  80. }
  81. };
  82. let ipc_handler = move |payload: String| {
  83. // defer the event to the main thread
  84. if let Ok(msg) = serde_json::from_str(&payload) {
  85. _ = proxy_.send_event(UserWindowEvent::Ipc { id: window_id, msg });
  86. }
  87. };
  88. let file_drop_handler = move |evt| {
  89. // Update the most recent file drop event - when the event comes in from the webview we can use the
  90. // most recent event to build a new event with the files in it.
  91. file_hover_.set(evt);
  92. false
  93. };
  94. #[cfg(any(
  95. target_os = "windows",
  96. target_os = "macos",
  97. target_os = "ios",
  98. target_os = "android"
  99. ))]
  100. let mut webview = WebViewBuilder::new(&window);
  101. #[cfg(not(any(
  102. target_os = "windows",
  103. target_os = "macos",
  104. target_os = "ios",
  105. target_os = "android"
  106. )))]
  107. let mut webview = {
  108. use tao::platform::unix::WindowExtUnix;
  109. use wry::WebViewBuilderExtUnix;
  110. let vbox = window.default_vbox().unwrap();
  111. WebViewBuilder::new_gtk(vbox)
  112. };
  113. webview = webview
  114. .with_transparent(cfg.window.window.transparent)
  115. .with_url("dioxus://index.html/")
  116. .unwrap()
  117. .with_ipc_handler(ipc_handler)
  118. .with_navigation_handler(|var| var.contains("dioxus")) // prevent all navigations
  119. .with_asynchronous_custom_protocol(String::from("dioxus"), request_handler)
  120. .with_web_context(&mut web_context)
  121. .with_file_drop_handler(file_drop_handler);
  122. if let Some(color) = cfg.background_color {
  123. webview = webview.with_background_color(color);
  124. }
  125. for (name, handler) in cfg.protocols.drain(..) {
  126. webview = webview.with_custom_protocol(name, handler);
  127. }
  128. const INITIALIZATION_SCRIPT: &str = r#"
  129. if (document.addEventListener) {
  130. document.addEventListener('contextmenu', function(e) {
  131. e.preventDefault();
  132. }, false);
  133. } else {
  134. document.attachEvent('oncontextmenu', function() {
  135. window.event.returnValue = false;
  136. });
  137. }
  138. "#;
  139. if cfg.disable_context_menu {
  140. // in release mode, we don't want to show the dev tool or reload menus
  141. webview = webview.with_initialization_script(INITIALIZATION_SCRIPT)
  142. } else {
  143. // in debug, we are okay with the reload menu showing and dev tool
  144. webview = webview.with_devtools(true);
  145. }
  146. let webview = webview.build().unwrap();
  147. // TODO: allow users to specify their own menubars, again :/
  148. let menu = if cfg!(not(any(target_os = "android", target_os = "ios"))) {
  149. crate::menubar::build_menu(&window, cfg.enable_default_menu_bar)
  150. } else {
  151. None
  152. };
  153. let desktop_context = Rc::from(DesktopService::new(
  154. webview,
  155. window,
  156. shared.clone(),
  157. edit_queue,
  158. asset_handlers,
  159. file_hover,
  160. ));
  161. let provider: Rc<dyn EvalProvider> =
  162. Rc::new(DesktopEvalProvider::new(desktop_context.clone()));
  163. dom.in_runtime(|| {
  164. ScopeId::ROOT.provide_context(desktop_context.clone());
  165. ScopeId::ROOT.provide_context(provider);
  166. });
  167. WebviewInstance {
  168. waker: tao_waker(shared.proxy.clone(), desktop_context.window.id()),
  169. desktop_context,
  170. dom,
  171. _menu: menu,
  172. _web_context: web_context,
  173. }
  174. }
  175. pub fn poll_vdom(&mut self) {
  176. let mut cx = std::task::Context::from_waker(&self.waker);
  177. // Continously poll the virtualdom until it's pending
  178. // Wait for work will return Ready when it has edits to be sent to the webview
  179. // It will return Pending when it needs to be polled again - nothing is ready
  180. loop {
  181. {
  182. let fut = self.dom.wait_for_work();
  183. pin_mut!(fut);
  184. match fut.poll_unpin(&mut cx) {
  185. std::task::Poll::Ready(_) => {}
  186. std::task::Poll::Pending => return,
  187. }
  188. }
  189. self.dom
  190. .render_immediate(&mut *self.desktop_context.mutation_state.borrow_mut());
  191. self.desktop_context.send_edits();
  192. }
  193. }
  194. pub fn kick_stylsheets(&self) {
  195. // run eval in the webview to kick the stylesheets by appending a query string
  196. // we should do something less clunky than this
  197. _ = self.desktop_context
  198. .webview
  199. .evaluate_script("document.querySelectorAll('link[rel=\"stylesheet\"]').forEach((el) => el.href = el.href + \"?\" + Math.random());");
  200. }
  201. }