lib.rs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  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. #[cfg(any(target_os = "ios", target_os = "android"))]
  18. mod mobile_shortcut;
  19. use crate::query::QueryResult;
  20. pub use cfg::{Config, WindowCloseBehaviour};
  21. pub use desktop_context::DesktopContext;
  22. pub use desktop_context::{
  23. use_window, use_wry_event_handler, DesktopService, WryEventHandler, WryEventHandlerId,
  24. };
  25. use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
  26. use dioxus_core::*;
  27. use dioxus_html::MountedData;
  28. use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
  29. use element::DesktopElement;
  30. use eval::init_eval;
  31. use futures_util::{pin_mut, FutureExt};
  32. use shortcut::ShortcutRegistry;
  33. pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
  34. use std::cell::Cell;
  35. use std::rc::Rc;
  36. use std::task::Waker;
  37. use std::{collections::HashMap, sync::Arc};
  38. pub use tao::dpi::{LogicalSize, PhysicalSize};
  39. use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
  40. pub use tao::window::WindowBuilder;
  41. use tao::{
  42. event::{Event, StartCause, WindowEvent},
  43. event_loop::{ControlFlow, EventLoop},
  44. };
  45. pub use wry;
  46. pub use wry::application as tao;
  47. use wry::webview::WebView;
  48. use wry::{application::window::WindowId, webview::WebContext};
  49. /// Launch the WebView and run the event loop.
  50. ///
  51. /// This function will start a multithreaded Tokio runtime as well the WebView event loop.
  52. ///
  53. /// ```rust, ignore
  54. /// use dioxus::prelude::*;
  55. ///
  56. /// fn main() {
  57. /// dioxus_desktop::launch(app);
  58. /// }
  59. ///
  60. /// fn app(cx: Scope) -> Element {
  61. /// cx.render(rsx!{
  62. /// h1 {"hello world!"}
  63. /// })
  64. /// }
  65. /// ```
  66. pub fn launch(root: Component) {
  67. launch_with_props(root, (), Config::default())
  68. }
  69. /// Launch the WebView and run the event loop, with configuration.
  70. ///
  71. /// This function will start a multithreaded Tokio runtime as well the WebView event loop.
  72. ///
  73. /// You can configure the WebView window with a configuration closure
  74. ///
  75. /// ```rust, ignore
  76. /// use dioxus::prelude::*;
  77. ///
  78. /// fn main() {
  79. /// dioxus_desktop::launch_cfg(app, |c| c.with_window(|w| w.with_title("My App")));
  80. /// }
  81. ///
  82. /// fn app(cx: Scope) -> Element {
  83. /// cx.render(rsx!{
  84. /// h1 {"hello world!"}
  85. /// })
  86. /// }
  87. /// ```
  88. pub fn launch_cfg(root: Component, config_builder: Config) {
  89. launch_with_props(root, (), config_builder)
  90. }
  91. /// Launch the WebView and run the event loop, with configuration and root props.
  92. ///
  93. /// This function will start a multithreaded Tokio runtime as well the WebView event loop. This will block the current thread.
  94. ///
  95. /// You can configure the WebView window with a configuration closure
  96. ///
  97. /// ```rust, ignore
  98. /// use dioxus::prelude::*;
  99. ///
  100. /// fn main() {
  101. /// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default());
  102. /// }
  103. ///
  104. /// struct AppProps {
  105. /// name: &'static str
  106. /// }
  107. ///
  108. /// fn app(cx: Scope<AppProps>) -> Element {
  109. /// cx.render(rsx!{
  110. /// h1 {"hello {cx.props.name}!"}
  111. /// })
  112. /// }
  113. /// ```
  114. pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config) {
  115. let event_loop = EventLoop::<UserWindowEvent>::with_user_event();
  116. let proxy = event_loop.create_proxy();
  117. let window_behaviour = cfg.last_window_close_behaviour;
  118. // Intialize hot reloading if it is enabled
  119. #[cfg(all(feature = "hot-reload", debug_assertions))]
  120. dioxus_hot_reload::connect({
  121. let proxy = proxy.clone();
  122. move |template| {
  123. let _ = proxy.send_event(UserWindowEvent(
  124. EventData::HotReloadEvent(template),
  125. unsafe { WindowId::dummy() },
  126. ));
  127. }
  128. });
  129. // We start the tokio runtime *on this thread*
  130. // Any future we poll later will use this runtime to spawn tasks and for IO
  131. let rt = tokio::runtime::Builder::new_multi_thread()
  132. .enable_all()
  133. .build()
  134. .unwrap();
  135. // We enter the runtime but we poll futures manually, circumventing the per-task runtime budget
  136. let _guard = rt.enter();
  137. // We only have one webview right now, but we'll have more later
  138. // Store them in a hashmap so we can remove them when they're closed
  139. let mut webviews = HashMap::<WindowId, WebviewHandler>::new();
  140. // We use this to allow dynamically adding and removing window event handlers
  141. let event_handlers = WindowEventHandlers::default();
  142. let queue = WebviewQueue::default();
  143. let shortcut_manager = ShortcutRegistry::new(&event_loop);
  144. // move the props into a cell so we can pop it out later to create the first window
  145. // iOS panics if we create a window before the event loop is started
  146. let props = Rc::new(Cell::new(Some(props)));
  147. let cfg = Rc::new(Cell::new(Some(cfg)));
  148. let mut is_visible_before_start = true;
  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 => match window_behaviour {
  157. cfg::WindowCloseBehaviour::LastWindowExitsApp => {
  158. webviews.remove(&window_id);
  159. if webviews.is_empty() {
  160. *control_flow = ControlFlow::Exit
  161. }
  162. }
  163. cfg::WindowCloseBehaviour::LastWindowHides => {
  164. let Some(webview) = webviews.get(&window_id) else {
  165. return;
  166. };
  167. hide_app_window(&webview.desktop_context.webview);
  168. }
  169. cfg::WindowCloseBehaviour::CloseWindow => {
  170. webviews.remove(&window_id);
  171. }
  172. },
  173. WindowEvent::Destroyed { .. } => {
  174. webviews.remove(&window_id);
  175. if matches!(
  176. window_behaviour,
  177. cfg::WindowCloseBehaviour::LastWindowExitsApp
  178. ) && webviews.is_empty()
  179. {
  180. *control_flow = ControlFlow::Exit
  181. }
  182. }
  183. _ => {}
  184. },
  185. Event::NewEvents(StartCause::Init) => {
  186. let props = props.take().unwrap();
  187. let cfg = cfg.take().unwrap();
  188. // Create a dom
  189. let dom = VirtualDom::new_with_props(root, props);
  190. is_visible_before_start = cfg.window.window.visible;
  191. let handler = create_new_window(
  192. cfg,
  193. event_loop,
  194. &proxy,
  195. dom,
  196. &queue,
  197. &event_handlers,
  198. shortcut_manager.clone(),
  199. );
  200. let id = handler.desktop_context.webview.window().id();
  201. webviews.insert(id, handler);
  202. _ = proxy.send_event(UserWindowEvent(EventData::Poll, id));
  203. }
  204. Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => {
  205. for handler in queue.borrow_mut().drain(..) {
  206. let id = handler.desktop_context.webview.window().id();
  207. webviews.insert(id, handler);
  208. _ = proxy.send_event(UserWindowEvent(EventData::Poll, id));
  209. }
  210. }
  211. Event::UserEvent(event) => match event.0 {
  212. #[cfg(all(feature = "hot-reload", debug_assertions))]
  213. EventData::HotReloadEvent(msg) => match msg {
  214. dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
  215. for webview in webviews.values_mut() {
  216. webview.dom.replace_template(template);
  217. poll_vdom(webview);
  218. }
  219. }
  220. dioxus_hot_reload::HotReloadMsg::Shutdown => {
  221. *control_flow = ControlFlow::Exit;
  222. }
  223. },
  224. EventData::CloseWindow => {
  225. webviews.remove(&event.1);
  226. if webviews.is_empty() {
  227. *control_flow = ControlFlow::Exit
  228. }
  229. }
  230. EventData::Poll => {
  231. if let Some(view) = webviews.get_mut(&event.1) {
  232. poll_vdom(view);
  233. }
  234. }
  235. EventData::Ipc(msg) if msg.method() == "user_event" => {
  236. let params = msg.params();
  237. let evt = match serde_json::from_value::<HtmlEvent>(params) {
  238. Ok(value) => value,
  239. Err(_) => return,
  240. };
  241. let HtmlEvent {
  242. element,
  243. name,
  244. bubbles,
  245. data,
  246. } = evt;
  247. let view = webviews.get_mut(&event.1).unwrap();
  248. // check for a mounted event placeholder and replace it with a desktop specific element
  249. let as_any = if let dioxus_html::EventData::Mounted = &data {
  250. let query = view
  251. .dom
  252. .base_scope()
  253. .consume_context::<DesktopContext>()
  254. .unwrap()
  255. .query
  256. .clone();
  257. let element =
  258. DesktopElement::new(element, view.desktop_context.clone(), query);
  259. Rc::new(MountedData::new(element))
  260. } else {
  261. data.into_any()
  262. };
  263. view.dom.handle_event(&name, as_any, element, bubbles);
  264. send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
  265. }
  266. // 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
  267. EventData::Ipc(msg) if msg.method() == "query" => {
  268. let params = msg.params();
  269. if let Ok(result) = serde_json::from_value::<QueryResult>(params) {
  270. let view = webviews.get(&event.1).unwrap();
  271. let query = view
  272. .dom
  273. .base_scope()
  274. .consume_context::<DesktopContext>()
  275. .unwrap()
  276. .query
  277. .clone();
  278. query.send(result);
  279. }
  280. }
  281. EventData::Ipc(msg) if msg.method() == "initialize" => {
  282. let view = webviews.get_mut(&event.1).unwrap();
  283. send_edits(view.dom.rebuild(), &view.desktop_context.webview);
  284. view.desktop_context
  285. .webview
  286. .window()
  287. .set_visible(is_visible_before_start);
  288. }
  289. EventData::Ipc(msg) if msg.method() == "browser_open" => {
  290. if let Some(temp) = msg.params().as_object() {
  291. if temp.contains_key("href") {
  292. let open = webbrowser::open(temp["href"].as_str().unwrap());
  293. if let Err(e) = open {
  294. tracing::error!("Open Browser error: {:?}", e);
  295. }
  296. }
  297. }
  298. }
  299. EventData::Ipc(msg) if msg.method() == "file_diolog" => {
  300. if let Ok(file_diolog) =
  301. serde_json::from_value::<file_upload::FileDialogRequest>(msg.params())
  302. {
  303. let id = ElementId(file_diolog.target);
  304. let event_name = &file_diolog.event;
  305. let event_bubbles = file_diolog.bubbles;
  306. let files = file_upload::get_file_event(&file_diolog);
  307. let data = Rc::new(FormData {
  308. value: Default::default(),
  309. values: Default::default(),
  310. files: Some(Arc::new(NativeFileEngine::new(files))),
  311. });
  312. let view = webviews.get_mut(&event.1).unwrap();
  313. if event_name == "change&input" {
  314. view.dom
  315. .handle_event("input", data.clone(), id, event_bubbles);
  316. view.dom.handle_event("change", data, id, event_bubbles);
  317. } else {
  318. view.dom.handle_event(event_name, data, id, event_bubbles);
  319. }
  320. send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
  321. }
  322. }
  323. _ => {}
  324. },
  325. Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id),
  326. _ => {}
  327. }
  328. })
  329. }
  330. fn create_new_window(
  331. mut cfg: Config,
  332. event_loop: &EventLoopWindowTarget<UserWindowEvent>,
  333. proxy: &EventLoopProxy<UserWindowEvent>,
  334. dom: VirtualDom,
  335. queue: &WebviewQueue,
  336. event_handlers: &WindowEventHandlers,
  337. shortcut_manager: ShortcutRegistry,
  338. ) -> WebviewHandler {
  339. let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone());
  340. let desktop_context = Rc::from(DesktopService::new(
  341. webview,
  342. proxy.clone(),
  343. event_loop.clone(),
  344. queue.clone(),
  345. event_handlers.clone(),
  346. shortcut_manager,
  347. ));
  348. let cx = dom.base_scope();
  349. cx.provide_context(desktop_context.clone());
  350. // Init eval
  351. init_eval(cx);
  352. WebviewHandler {
  353. // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both
  354. waker: waker::tao_waker(proxy, desktop_context.webview.window().id()),
  355. desktop_context,
  356. dom,
  357. _web_context: web_context,
  358. }
  359. }
  360. struct WebviewHandler {
  361. dom: VirtualDom,
  362. desktop_context: DesktopContext,
  363. waker: Waker,
  364. // Wry assumes the webcontext is alive for the lifetime of the webview.
  365. // We need to keep the webcontext alive, otherwise the webview will crash
  366. _web_context: WebContext,
  367. }
  368. /// Poll the virtualdom until it's pending
  369. ///
  370. /// 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
  371. ///
  372. /// All IO is done on the tokio runtime we started earlier
  373. fn poll_vdom(view: &mut WebviewHandler) {
  374. let mut cx = std::task::Context::from_waker(&view.waker);
  375. loop {
  376. {
  377. let fut = view.dom.wait_for_work();
  378. pin_mut!(fut);
  379. match fut.poll_unpin(&mut cx) {
  380. std::task::Poll::Ready(_) => {}
  381. std::task::Poll::Pending => break,
  382. }
  383. }
  384. send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
  385. }
  386. }
  387. /// Send a list of mutations to the webview
  388. fn send_edits(edits: Mutations, webview: &WebView) {
  389. let serialized = serde_json::to_string(&edits).unwrap();
  390. // todo: use SSE and binary data to send the edits with lower overhead
  391. _ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})"));
  392. }
  393. /// Different hide implementations per platform
  394. #[allow(unused)]
  395. fn hide_app_window(webview: &WebView) {
  396. #[cfg(target_os = "windows")]
  397. {
  398. use wry::application::platform::windows::WindowExtWindows;
  399. webview.window().set_visible(false);
  400. webview.window().set_skip_taskbar(true);
  401. }
  402. #[cfg(target_os = "linux")]
  403. {
  404. use wry::application::platform::unix::WindowExtUnix;
  405. webview.window().set_visible(false);
  406. }
  407. #[cfg(target_os = "macos")]
  408. {
  409. // webview.window().set_visible(false); has the wrong behaviour on macOS
  410. // It will hide the window but not show it again when the user switches
  411. // back to the app. `NSApplication::hide:` has the correct behaviour
  412. use objc::runtime::Object;
  413. use objc::{msg_send, sel, sel_impl};
  414. objc::rc::autoreleasepool(|| unsafe {
  415. let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication];
  416. let nil = std::ptr::null_mut::<Object>();
  417. let _: () = msg_send![app, hide: nil];
  418. });
  419. }
  420. }