app.rs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. use crate::{
  2. config::{Config, WindowCloseBehaviour},
  3. event_handlers::WindowEventHandlers,
  4. file_upload::{DesktopFileUploadForm, FileDialogRequest, NativeFileEngine},
  5. ipc::{IpcMessage, UserWindowEvent},
  6. query::QueryResult,
  7. shortcut::ShortcutRegistry,
  8. webview::WebviewInstance,
  9. };
  10. use dioxus_core::{ElementId, VirtualDom};
  11. use dioxus_html::PlatformEventData;
  12. use std::{
  13. any::Any,
  14. cell::{Cell, RefCell},
  15. collections::HashMap,
  16. rc::Rc,
  17. sync::Arc,
  18. };
  19. use tao::{
  20. event::Event,
  21. event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget},
  22. window::WindowId,
  23. };
  24. /// The single top-level object that manages all the running windows, assets, shortcuts, etc
  25. pub(crate) struct App {
  26. // move the props into a cell so we can pop it out later to create the first window
  27. // iOS panics if we create a window before the event loop is started, so we toss them into a cell
  28. pub(crate) unmounted_dom: Cell<Option<VirtualDom>>,
  29. pub(crate) cfg: Cell<Option<Config>>,
  30. // Stuff we need mutable access to
  31. pub(crate) control_flow: ControlFlow,
  32. pub(crate) is_visible_before_start: bool,
  33. pub(crate) window_behavior: WindowCloseBehaviour,
  34. pub(crate) webviews: HashMap<WindowId, WebviewInstance>,
  35. pub(crate) float_all: bool,
  36. pub(crate) show_devtools: bool,
  37. /// This single blob of state is shared between all the windows so they have access to the runtime state
  38. ///
  39. /// This includes stuff like the event handlers, shortcuts, etc as well as ways to modify *other* windows
  40. pub(crate) shared: Rc<SharedContext>,
  41. }
  42. /// A bundle of state shared between all the windows, providing a way for us to communicate with running webview.
  43. pub(crate) struct SharedContext {
  44. pub(crate) event_handlers: WindowEventHandlers,
  45. pub(crate) pending_webviews: RefCell<Vec<WebviewInstance>>,
  46. pub(crate) shortcut_manager: ShortcutRegistry,
  47. pub(crate) proxy: EventLoopProxy<UserWindowEvent>,
  48. pub(crate) target: EventLoopWindowTarget<UserWindowEvent>,
  49. }
  50. impl App {
  51. pub fn new(cfg: Config, virtual_dom: VirtualDom) -> (EventLoop<UserWindowEvent>, Self) {
  52. let event_loop = EventLoopBuilder::<UserWindowEvent>::with_user_event().build();
  53. let app = Self {
  54. window_behavior: cfg.last_window_close_behavior,
  55. is_visible_before_start: true,
  56. webviews: HashMap::new(),
  57. control_flow: ControlFlow::Wait,
  58. unmounted_dom: Cell::new(Some(virtual_dom)),
  59. float_all: !cfg!(debug_assertions),
  60. show_devtools: false,
  61. cfg: Cell::new(Some(cfg)),
  62. shared: Rc::new(SharedContext {
  63. event_handlers: WindowEventHandlers::default(),
  64. pending_webviews: Default::default(),
  65. shortcut_manager: ShortcutRegistry::new(),
  66. proxy: event_loop.create_proxy(),
  67. target: event_loop.clone(),
  68. }),
  69. };
  70. // Set the event converter
  71. dioxus_html::set_event_converter(Box::new(crate::events::SerializedHtmlEventConverter));
  72. // Wire up the global hotkey handler
  73. #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
  74. app.set_global_hotkey_handler();
  75. // Wire up the menubar receiver - this way any component can key into the menubar actions
  76. #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
  77. app.set_menubar_receiver();
  78. // Allow hotreloading to work - but only in debug mode
  79. #[cfg(all(feature = "devtools", debug_assertions))]
  80. app.connect_hotreload();
  81. #[cfg(debug_assertions)]
  82. #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
  83. app.connect_preserve_window_state_handler();
  84. (event_loop, app)
  85. }
  86. pub fn tick(&mut self, window_event: &Event<'_, UserWindowEvent>) {
  87. self.control_flow = ControlFlow::Wait;
  88. self.shared
  89. .event_handlers
  90. .apply_event(window_event, &self.shared.target);
  91. }
  92. #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
  93. pub fn handle_global_hotkey(&self, event: global_hotkey::GlobalHotKeyEvent) {
  94. self.shared.shortcut_manager.call_handlers(event);
  95. }
  96. #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
  97. pub fn handle_menu_event(&mut self, event: muda::MenuEvent) {
  98. match event.id().0.as_str() {
  99. "dioxus-float-top" => {
  100. for webview in self.webviews.values() {
  101. webview
  102. .desktop_context
  103. .window
  104. .set_always_on_top(self.float_all);
  105. }
  106. self.float_all = !self.float_all;
  107. }
  108. "dioxus-toggle-dev-tools" => {
  109. self.show_devtools = !self.show_devtools;
  110. for webview in self.webviews.values() {
  111. let wv = &webview.desktop_context.webview;
  112. if self.show_devtools {
  113. wv.open_devtools();
  114. } else {
  115. wv.close_devtools();
  116. }
  117. }
  118. }
  119. _ => (),
  120. }
  121. }
  122. #[cfg(all(feature = "devtools", debug_assertions))]
  123. pub fn connect_hotreload(&self) {
  124. if let Some(endpoint) = dioxus_cli_config::devserver_ws_endpoint() {
  125. let proxy = self.shared.proxy.clone();
  126. dioxus_devtools::connect(endpoint, move |msg| {
  127. _ = proxy.send_event(UserWindowEvent::HotReloadEvent(msg));
  128. })
  129. }
  130. }
  131. pub fn handle_new_window(&mut self) {
  132. for handler in self.shared.pending_webviews.borrow_mut().drain(..) {
  133. let id = handler.desktop_context.window.id();
  134. self.webviews.insert(id, handler);
  135. _ = self.shared.proxy.send_event(UserWindowEvent::Poll(id));
  136. }
  137. }
  138. pub fn handle_close_requested(&mut self, id: WindowId) {
  139. use WindowCloseBehaviour::*;
  140. match self.window_behavior {
  141. LastWindowExitsApp => {
  142. #[cfg(debug_assertions)]
  143. self.persist_window_state();
  144. self.webviews.remove(&id);
  145. if self.webviews.is_empty() {
  146. self.control_flow = ControlFlow::Exit
  147. }
  148. }
  149. LastWindowHides => {
  150. let Some(webview) = self.webviews.get(&id) else {
  151. return;
  152. };
  153. hide_app_window(&webview.desktop_context.webview);
  154. }
  155. CloseWindow => {
  156. self.webviews.remove(&id);
  157. }
  158. }
  159. }
  160. pub fn window_destroyed(&mut self, id: WindowId) {
  161. self.webviews.remove(&id);
  162. if matches!(
  163. self.window_behavior,
  164. WindowCloseBehaviour::LastWindowExitsApp
  165. ) && self.webviews.is_empty()
  166. {
  167. self.control_flow = ControlFlow::Exit
  168. }
  169. }
  170. pub fn handle_start_cause_init(&mut self) {
  171. let virtual_dom = self.unmounted_dom.take().unwrap();
  172. let mut cfg = self.cfg.take().unwrap();
  173. self.is_visible_before_start = cfg.window.window.visible;
  174. cfg.window = cfg.window.with_visible(false);
  175. let webview = WebviewInstance::new(cfg, virtual_dom, self.shared.clone());
  176. // And then attempt to resume from state
  177. #[cfg(debug_assertions)]
  178. self.resume_from_state(&webview);
  179. let id = webview.desktop_context.window.id();
  180. self.webviews.insert(id, webview);
  181. }
  182. pub fn handle_browser_open(&mut self, msg: IpcMessage) {
  183. if let Some(temp) = msg.params().as_object() {
  184. if temp.contains_key("href") {
  185. let open = webbrowser::open(temp["href"].as_str().unwrap());
  186. if let Err(e) = open {
  187. tracing::error!("Open Browser error: {:?}", e);
  188. }
  189. }
  190. }
  191. }
  192. /// The webview is finally loaded
  193. ///
  194. /// Let's rebuild it and then start polling it
  195. pub fn handle_initialize_msg(&mut self, id: WindowId) {
  196. let view = self.webviews.get_mut(&id).unwrap();
  197. view.dom
  198. .rebuild(&mut *view.edits.wry_queue.mutation_state_mut());
  199. view.edits.wry_queue.send_edits();
  200. view.desktop_context
  201. .window
  202. .set_visible(self.is_visible_before_start);
  203. _ = self.shared.proxy.send_event(UserWindowEvent::Poll(id));
  204. }
  205. /// Todo: maybe we should poll the virtualdom asking if it has any final actions to apply before closing the webview
  206. ///
  207. /// Technically you can handle this with the use_window_event hook
  208. pub fn handle_close_msg(&mut self, id: WindowId) {
  209. self.webviews.remove(&id);
  210. if self.webviews.is_empty() {
  211. self.control_flow = ControlFlow::Exit
  212. }
  213. }
  214. pub fn handle_query_msg(&mut self, msg: IpcMessage, id: WindowId) {
  215. let Ok(result) = serde_json::from_value::<QueryResult>(msg.params()) else {
  216. return;
  217. };
  218. let Some(view) = self.webviews.get(&id) else {
  219. return;
  220. };
  221. view.desktop_context.query.send(result);
  222. }
  223. #[cfg(all(feature = "devtools", debug_assertions))]
  224. pub fn handle_hot_reload_msg(&mut self, msg: dioxus_devtools::DevserverMsg) {
  225. use dioxus_devtools::DevserverMsg;
  226. match msg {
  227. DevserverMsg::HotReload(hr_msg) => {
  228. for webview in self.webviews.values_mut() {
  229. dioxus_devtools::apply_changes(&webview.dom, &hr_msg);
  230. webview.poll_vdom();
  231. }
  232. if !hr_msg.assets.is_empty() {
  233. for webview in self.webviews.values_mut() {
  234. webview.kick_stylsheets();
  235. }
  236. }
  237. }
  238. DevserverMsg::FullReloadCommand
  239. | DevserverMsg::FullReloadStart
  240. | DevserverMsg::FullReloadFailed => {
  241. // usually only web gets this message - what are we supposed to do?
  242. // Maybe we could just binary patch ourselves in place without losing window state?
  243. }
  244. DevserverMsg::Shutdown => {
  245. self.control_flow = ControlFlow::Exit;
  246. }
  247. }
  248. }
  249. pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) {
  250. let Ok(file_dialog) = serde_json::from_value::<FileDialogRequest>(msg.params()) else {
  251. return;
  252. };
  253. let id = ElementId(file_dialog.target);
  254. let event_name = &file_dialog.event;
  255. let event_bubbles = file_dialog.bubbles;
  256. let files = file_dialog.get_file_event();
  257. let as_any = Box::new(DesktopFileUploadForm {
  258. files: Arc::new(NativeFileEngine::new(files)),
  259. });
  260. let data = Rc::new(PlatformEventData::new(as_any));
  261. let view = self.webviews.get_mut(&window).unwrap();
  262. let event = dioxus_core::Event::new(data as Rc<dyn Any>, event_bubbles);
  263. let runtime = view.dom.runtime();
  264. if event_name == "change&input" {
  265. runtime.handle_event("input", event.clone(), id);
  266. runtime.handle_event("change", event, id);
  267. } else {
  268. runtime.handle_event(event_name, event, id);
  269. }
  270. }
  271. /// Poll the virtualdom until it's pending
  272. ///
  273. /// 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
  274. ///
  275. /// All IO is done on the tokio runtime we started earlier
  276. pub fn poll_vdom(&mut self, id: WindowId) {
  277. let Some(view) = self.webviews.get_mut(&id) else {
  278. return;
  279. };
  280. view.poll_vdom();
  281. }
  282. #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
  283. fn set_global_hotkey_handler(&self) {
  284. let receiver = self.shared.proxy.clone();
  285. // The event loop becomes the hotkey receiver
  286. // This means we don't need to poll the receiver on every tick - we just get the events as they come in
  287. // This is a bit more efficient than the previous implementation, but if someone else sets a handler, the
  288. // receiver will become inert.
  289. global_hotkey::GlobalHotKeyEvent::set_event_handler(Some(move |t| {
  290. // todo: should we unset the event handler when the app shuts down?
  291. _ = receiver.send_event(UserWindowEvent::GlobalHotKeyEvent(t));
  292. }));
  293. }
  294. #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
  295. fn set_menubar_receiver(&self) {
  296. let receiver = self.shared.proxy.clone();
  297. // The event loop becomes the menu receiver
  298. // This means we don't need to poll the receiver on every tick - we just get the events as they come in
  299. // This is a bit more efficient than the previous implementation, but if someone else sets a handler, the
  300. // receiver will become inert.
  301. muda::MenuEvent::set_event_handler(Some(move |t| {
  302. // todo: should we unset the event handler when the app shuts down?
  303. _ = receiver.send_event(UserWindowEvent::MudaMenuEvent(t));
  304. }));
  305. }
  306. /// Do our best to preserve state about the window when the event loop is destroyed
  307. ///
  308. /// This will attempt to save the window position, size, and monitor into the environment before
  309. /// closing. This way, when the app is restarted, it can attempt to restore the window to the same
  310. /// position and size it was in before, making a better DX.
  311. pub(crate) fn handle_loop_destroyed(&self) {
  312. #[cfg(debug_assertions)]
  313. self.persist_window_state();
  314. }
  315. #[cfg(debug_assertions)]
  316. fn persist_window_state(&self) {
  317. if let Some(webview) = self.webviews.values().next() {
  318. let window = &webview.desktop_context.window;
  319. let monitor = window.current_monitor().unwrap();
  320. let position = window.outer_position().unwrap();
  321. let size = window.outer_size();
  322. let x = position.x;
  323. let y = position.y;
  324. // This is to work around a bug in how tao handles inner_size on macOS
  325. // We *want* to use inner_size, but that's currently broken, so we use outer_size instead and then an adjustment
  326. //
  327. // https://github.com/tauri-apps/tao/issues/889
  328. let adjustment = match window.is_decorated() {
  329. true if cfg!(target_os = "macos") => 56,
  330. _ => 0,
  331. };
  332. let state = PreservedWindowState {
  333. x,
  334. y,
  335. width: size.width.max(200),
  336. height: size.height.saturating_sub(adjustment).max(200),
  337. monitor: monitor.name().unwrap().to_string(),
  338. };
  339. // Yes... I know... we're loading a file that might not be ours... but it's a debug feature
  340. if let Ok(state) = serde_json::to_string(&state) {
  341. _ = std::fs::write(restore_file(), state);
  342. }
  343. }
  344. }
  345. // Write this to the target dir so we can pick back up
  346. #[cfg(debug_assertions)]
  347. fn resume_from_state(&mut self, webview: &WebviewInstance) {
  348. if let Ok(state) = std::fs::read_to_string(restore_file()) {
  349. if let Ok(state) = serde_json::from_str::<PreservedWindowState>(&state) {
  350. let window = &webview.desktop_context.window;
  351. let position = (state.x, state.y);
  352. let size = (state.width, state.height);
  353. window.set_outer_position(tao::dpi::PhysicalPosition::new(position.0, position.1));
  354. window.set_inner_size(tao::dpi::PhysicalSize::new(size.0, size.1));
  355. }
  356. }
  357. }
  358. /// Wire up a receiver to sigkill that lets us preserve the window state
  359. /// Whenever sigkill is sent, we shut down the app and save the window state
  360. #[cfg(debug_assertions)]
  361. fn connect_preserve_window_state_handler(&self) {
  362. // TODO: make this work on windows
  363. #[cfg(unix)]
  364. {
  365. // Wire up the trap
  366. let target = self.shared.proxy.clone();
  367. std::thread::spawn(move || {
  368. use signal_hook::consts::{SIGINT, SIGTERM};
  369. let sigkill = signal_hook::iterator::Signals::new([SIGTERM, SIGINT]);
  370. if let Ok(mut sigkill) = sigkill {
  371. for _ in sigkill.forever() {
  372. if target.send_event(UserWindowEvent::Shutdown).is_err() {
  373. std::process::exit(0);
  374. }
  375. // give it a moment for the event to be processed
  376. std::thread::sleep(std::time::Duration::from_secs(1));
  377. }
  378. }
  379. });
  380. }
  381. }
  382. }
  383. #[derive(Debug, serde::Serialize, serde::Deserialize)]
  384. struct PreservedWindowState {
  385. x: i32,
  386. y: i32,
  387. width: u32,
  388. height: u32,
  389. monitor: String,
  390. }
  391. /// Different hide implementations per platform
  392. #[allow(unused)]
  393. pub fn hide_app_window(window: &wry::WebView) {
  394. #[cfg(target_os = "windows")]
  395. {
  396. use tao::platform::windows::WindowExtWindows;
  397. window.set_visible(false);
  398. }
  399. #[cfg(target_os = "linux")]
  400. {
  401. use tao::platform::unix::WindowExtUnix;
  402. window.set_visible(false);
  403. }
  404. #[cfg(target_os = "macos")]
  405. {
  406. // window.set_visible(false); has the wrong behaviour on macOS
  407. // It will hide the window but not show it again when the user switches
  408. // back to the app. `NSApplication::hide:` has the correct behaviour
  409. use objc::runtime::Object;
  410. use objc::{msg_send, sel, sel_impl};
  411. objc::rc::autoreleasepool(|| unsafe {
  412. let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication];
  413. let nil = std::ptr::null_mut::<Object>();
  414. let _: () = msg_send![app, hide: nil];
  415. });
  416. }
  417. }
  418. /// Return the location of a tempfile with our window state in it such that we can restore it later
  419. #[cfg(debug_assertions)]
  420. fn restore_file() -> std::path::PathBuf {
  421. /// Get the name of the program or default to "dioxus" so we can hash it
  422. fn get_prog_name_or_default() -> Option<String> {
  423. Some(
  424. std::env::current_exe()
  425. .ok()?
  426. .file_name()?
  427. .to_str()?
  428. .to_string(),
  429. )
  430. }
  431. let name = get_prog_name_or_default().unwrap_or_else(|| "dioxus".to_string());
  432. let hashed_id = name.chars().map(|c| c as usize).sum::<usize>();
  433. let mut path = std::env::temp_dir();
  434. path.push(format!("{}-window-state.json", hashed_id));
  435. path
  436. }