webview.rs 9.3 KB

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