1
0

lib.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. #![doc = include_str!("readme.md")]
  2. #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
  3. #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
  4. #![deny(missing_docs)]
  5. mod cfg;
  6. mod desktop_context;
  7. mod element;
  8. mod escape;
  9. mod eval;
  10. mod events;
  11. mod file_upload;
  12. mod protocol;
  13. mod query;
  14. mod shortcut;
  15. mod waker;
  16. mod webview;
  17. use crate::query::QueryResult;
  18. pub use cfg::Config;
  19. pub use desktop_context::{
  20. use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId,
  21. };
  22. use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
  23. use dioxus_core::*;
  24. use dioxus_html::MountedData;
  25. use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
  26. use element::DesktopElement;
  27. pub use eval::{use_eval, EvalResult};
  28. use futures_util::{pin_mut, FutureExt};
  29. use shortcut::ShortcutRegistry;
  30. pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
  31. use std::rc::Rc;
  32. use std::task::Waker;
  33. use std::{collections::HashMap, sync::Arc};
  34. pub use tao::dpi::{LogicalSize, PhysicalSize};
  35. use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
  36. pub use tao::window::WindowBuilder;
  37. use tao::{
  38. event::{Event, StartCause, WindowEvent},
  39. event_loop::{ControlFlow, EventLoop},
  40. };
  41. pub use wry;
  42. pub use wry::application as tao;
  43. use wry::webview::WebView;
  44. use wry::{application::window::WindowId, webview::WebContext};
  45. /// Launch the WebView and run the event loop.
  46. ///
  47. /// This function will start a multithreaded Tokio runtime as well the WebView event loop.
  48. ///
  49. /// ```rust, ignore
  50. /// use dioxus::prelude::*;
  51. ///
  52. /// fn main() {
  53. /// dioxus_desktop::launch(app);
  54. /// }
  55. ///
  56. /// fn app(cx: Scope) -> Element {
  57. /// cx.render(rsx!{
  58. /// h1 {"hello world!"}
  59. /// })
  60. /// }
  61. /// ```
  62. pub fn launch(root: Component) {
  63. launch_with_props(root, (), Config::default())
  64. }
  65. /// Launch the WebView and run the event loop, with configuration.
  66. ///
  67. /// This function will start a multithreaded Tokio runtime as well the WebView event loop.
  68. ///
  69. /// You can configure the WebView window with a configuration closure
  70. ///
  71. /// ```rust, ignore
  72. /// use dioxus::prelude::*;
  73. ///
  74. /// fn main() {
  75. /// dioxus_desktop::launch_cfg(app, |c| c.with_window(|w| w.with_title("My App")));
  76. /// }
  77. ///
  78. /// fn app(cx: Scope) -> Element {
  79. /// cx.render(rsx!{
  80. /// h1 {"hello world!"}
  81. /// })
  82. /// }
  83. /// ```
  84. pub fn launch_cfg(root: Component, config_builder: Config) {
  85. launch_with_props(root, (), config_builder)
  86. }
  87. /// Launch the WebView and run the event loop, with configuration and root props.
  88. ///
  89. /// This function will start a multithreaded Tokio runtime as well the WebView event loop. This will block the current thread.
  90. ///
  91. /// You can configure the WebView window with a configuration closure
  92. ///
  93. /// ```rust, ignore
  94. /// use dioxus::prelude::*;
  95. ///
  96. /// fn main() {
  97. /// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default());
  98. /// }
  99. ///
  100. /// struct AppProps {
  101. /// name: &'static str
  102. /// }
  103. ///
  104. /// fn app(cx: Scope<AppProps>) -> Element {
  105. /// cx.render(rsx!{
  106. /// h1 {"hello {cx.props.name}!"}
  107. /// })
  108. /// }
  109. /// ```
  110. pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config) {
  111. let event_loop = EventLoop::<UserWindowEvent>::with_user_event();
  112. let proxy = event_loop.create_proxy();
  113. // Intialize hot reloading if it is enabled
  114. #[cfg(all(feature = "hot-reload", debug_assertions))]
  115. {
  116. let proxy = proxy.clone();
  117. dioxus_hot_reload::connect(move |template| {
  118. let _ = proxy.send_event(UserWindowEvent(
  119. EventData::HotReloadEvent(template),
  120. unsafe { WindowId::dummy() },
  121. ));
  122. });
  123. }
  124. // We start the tokio runtime *on this thread*
  125. // Any future we poll later will use this runtime to spawn tasks and for IO
  126. let rt = tokio::runtime::Builder::new_multi_thread()
  127. .enable_all()
  128. .build()
  129. .unwrap();
  130. // We enter the runtime but we poll futures manually, circumventing the per-task runtime budget
  131. let _guard = rt.enter();
  132. // We only have one webview right now, but we'll have more later
  133. // Store them in a hashmap so we can remove them when they're closed
  134. let mut webviews = HashMap::<WindowId, WebviewHandler>::new();
  135. // We use this to allow dynamically adding and removing window event handlers
  136. let event_handlers = WindowEventHandlers::default();
  137. let queue = WebviewQueue::default();
  138. let shortcut_manager = ShortcutRegistry::new(&event_loop);
  139. // By default, we'll create a new window when the app starts
  140. queue.borrow_mut().push(create_new_window(
  141. cfg,
  142. &event_loop,
  143. &proxy,
  144. VirtualDom::new_with_props(root, props),
  145. &queue,
  146. &event_handlers,
  147. shortcut_manager.clone(),
  148. ));
  149. event_loop.run(move |window_event, event_loop, control_flow| {
  150. *control_flow = ControlFlow::Wait;
  151. event_handlers.apply_event(&window_event, event_loop);
  152. match window_event {
  153. Event::WindowEvent {
  154. event, window_id, ..
  155. } => match event {
  156. WindowEvent::CloseRequested => {
  157. webviews.remove(&window_id);
  158. if webviews.is_empty() {
  159. *control_flow = ControlFlow::Exit
  160. }
  161. }
  162. WindowEvent::Destroyed { .. } => {
  163. webviews.remove(&window_id);
  164. if webviews.is_empty() {
  165. *control_flow = ControlFlow::Exit;
  166. }
  167. }
  168. _ => {}
  169. },
  170. Event::NewEvents(StartCause::Init)
  171. | Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => {
  172. for handler in queue.borrow_mut().drain(..) {
  173. let id = handler.webview.window().id();
  174. webviews.insert(id, handler);
  175. _ = proxy.send_event(UserWindowEvent(EventData::Poll, id));
  176. }
  177. }
  178. Event::UserEvent(event) => match event.0 {
  179. #[cfg(all(feature = "hot-reload", debug_assertions))]
  180. EventData::HotReloadEvent(msg) => match msg {
  181. dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
  182. for webview in webviews.values_mut() {
  183. webview.dom.replace_template(template);
  184. poll_vdom(webview);
  185. }
  186. }
  187. dioxus_hot_reload::HotReloadMsg::Shutdown => {
  188. *control_flow = ControlFlow::Exit;
  189. }
  190. },
  191. EventData::CloseWindow => {
  192. webviews.remove(&event.1);
  193. if webviews.is_empty() {
  194. *control_flow = ControlFlow::Exit
  195. }
  196. }
  197. EventData::Poll => {
  198. if let Some(view) = webviews.get_mut(&event.1) {
  199. poll_vdom(view);
  200. }
  201. }
  202. EventData::Ipc(msg) if msg.method() == "user_event" => {
  203. let params = msg.params();
  204. let evt = match serde_json::from_value::<HtmlEvent>(params) {
  205. Ok(value) => value,
  206. Err(_) => return,
  207. };
  208. let HtmlEvent {
  209. element,
  210. name,
  211. bubbles,
  212. data,
  213. } = evt;
  214. let view = webviews.get_mut(&event.1).unwrap();
  215. // check for a mounted event placeholder and replace it with a desktop specific element
  216. let as_any = if let dioxus_html::EventData::Mounted = &data {
  217. let query = view
  218. .dom
  219. .base_scope()
  220. .consume_context::<DesktopContext>()
  221. .unwrap()
  222. .query;
  223. let element = DesktopElement::new(element, view.webview.clone(), query);
  224. Rc::new(MountedData::new(element))
  225. } else {
  226. data.into_any()
  227. };
  228. view.dom.handle_event(&name, as_any, element, bubbles);
  229. send_edits(view.dom.render_immediate(), &view.webview);
  230. }
  231. // When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query
  232. EventData::Ipc(msg) if msg.method() == "query" => {
  233. let params = msg.params();
  234. if let Ok(result) = serde_json::from_value::<QueryResult>(params) {
  235. let view = webviews.get(&event.1).unwrap();
  236. let query = view
  237. .dom
  238. .base_scope()
  239. .consume_context::<DesktopContext>()
  240. .unwrap()
  241. .query;
  242. query.send(result);
  243. }
  244. }
  245. EventData::Ipc(msg) if msg.method() == "initialize" => {
  246. let view = webviews.get_mut(&event.1).unwrap();
  247. send_edits(view.dom.rebuild(), &view.webview);
  248. }
  249. EventData::Ipc(msg) if msg.method() == "browser_open" => {
  250. if let Some(temp) = msg.params().as_object() {
  251. if temp.contains_key("href") {
  252. let open = webbrowser::open(temp["href"].as_str().unwrap());
  253. if let Err(e) = open {
  254. log::error!("Open Browser error: {:?}", e);
  255. }
  256. }
  257. }
  258. }
  259. EventData::Ipc(msg) if msg.method() == "file_diolog" => {
  260. if let Ok(file_diolog) =
  261. serde_json::from_value::<file_upload::FileDiologRequest>(msg.params())
  262. {
  263. let id = ElementId(file_diolog.target);
  264. let event_name = &file_diolog.event;
  265. let event_bubbles = file_diolog.bubbles;
  266. let files = file_upload::get_file_event(&file_diolog);
  267. let data = Rc::new(FormData {
  268. value: Default::default(),
  269. values: Default::default(),
  270. files: Some(Arc::new(NativeFileEngine::new(files))),
  271. });
  272. let view = webviews.get_mut(&event.1).unwrap();
  273. if event_name == "change&input" {
  274. view.dom
  275. .handle_event("input", data.clone(), id, event_bubbles);
  276. view.dom.handle_event("change", data, id, event_bubbles);
  277. } else {
  278. view.dom.handle_event(event_name, data, id, event_bubbles);
  279. }
  280. send_edits(view.dom.render_immediate(), &view.webview);
  281. }
  282. }
  283. _ => {}
  284. },
  285. Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id),
  286. _ => {}
  287. }
  288. })
  289. }
  290. fn create_new_window(
  291. mut cfg: Config,
  292. event_loop: &EventLoopWindowTarget<UserWindowEvent>,
  293. proxy: &EventLoopProxy<UserWindowEvent>,
  294. dom: VirtualDom,
  295. queue: &WebviewQueue,
  296. event_handlers: &WindowEventHandlers,
  297. shortcut_manager: ShortcutRegistry,
  298. ) -> WebviewHandler {
  299. let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone());
  300. dom.base_scope().provide_context(DesktopContext::new(
  301. webview.clone(),
  302. proxy.clone(),
  303. event_loop.clone(),
  304. queue.clone(),
  305. event_handlers.clone(),
  306. shortcut_manager,
  307. ));
  308. let id = webview.window().id();
  309. // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both
  310. WebviewHandler {
  311. webview,
  312. dom,
  313. waker: waker::tao_waker(proxy, id),
  314. web_context,
  315. }
  316. }
  317. struct WebviewHandler {
  318. dom: VirtualDom,
  319. webview: Rc<wry::webview::WebView>,
  320. waker: Waker,
  321. // This is nessisary because of a bug in wry. Wry assumes the webcontext is alive for the lifetime of the webview. We need to keep the webcontext alive, otherwise the webview will crash
  322. #[allow(dead_code)]
  323. web_context: WebContext,
  324. }
  325. /// Poll the virtualdom until it's pending
  326. ///
  327. /// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again
  328. ///
  329. /// All IO is done on the tokio runtime we started earlier
  330. fn poll_vdom(view: &mut WebviewHandler) {
  331. let mut cx = std::task::Context::from_waker(&view.waker);
  332. loop {
  333. {
  334. let fut = view.dom.wait_for_work();
  335. pin_mut!(fut);
  336. match fut.poll_unpin(&mut cx) {
  337. std::task::Poll::Ready(_) => {}
  338. std::task::Poll::Pending => break,
  339. }
  340. }
  341. send_edits(view.dom.render_immediate(), &view.webview);
  342. }
  343. }
  344. /// Send a list of mutations to the webview
  345. fn send_edits(edits: Mutations, webview: &WebView) {
  346. let serialized = serde_json::to_string(&edits).unwrap();
  347. // todo: use SSE and binary data to send the edits with lower overhead
  348. _ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})"));
  349. }