lib.rs 17 KB

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