1
0

app.rs 23 KB

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