1use 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};
10use dioxus_core::{ElementId, VirtualDom};
11use dioxus_html::PlatformEventData;
12use std::{
13 any::Any,
14 cell::{Cell, RefCell},
15 collections::HashMap,
16 rc::Rc,
17 sync::Arc,
18};
19use tao::{
20 dpi::PhysicalSize,
21 event::Event,
22 event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget},
23 window::{Window, WindowId},
24};
25
26pub(crate) struct App {
28 pub(crate) unmounted_dom: Cell<Option<VirtualDom>>,
31 pub(crate) cfg: Cell<Option<Config>>,
32
33 pub(crate) control_flow: ControlFlow,
35 pub(crate) is_visible_before_start: bool,
36 pub(crate) window_behavior: WindowCloseBehaviour,
37 pub(crate) webviews: HashMap<WindowId, WebviewInstance>,
38 pub(crate) float_all: bool,
39 pub(crate) show_devtools: bool,
40
41 pub(crate) shared: Rc<SharedContext>,
45}
46
47pub(crate) struct SharedContext {
49 pub(crate) event_handlers: WindowEventHandlers,
50 pub(crate) pending_webviews: RefCell<Vec<WebviewInstance>>,
51 pub(crate) shortcut_manager: ShortcutRegistry,
52 pub(crate) proxy: EventLoopProxy<UserWindowEvent>,
53 pub(crate) target: EventLoopWindowTarget<UserWindowEvent>,
54}
55
56impl App {
57 pub fn new(mut cfg: Config, virtual_dom: VirtualDom) -> (EventLoop<UserWindowEvent>, Self) {
58 let event_loop = cfg
59 .event_loop
60 .take()
61 .unwrap_or_else(|| EventLoopBuilder::<UserWindowEvent>::with_user_event().build());
62
63 let app = Self {
64 window_behavior: cfg.last_window_close_behavior,
65 is_visible_before_start: true,
66 webviews: HashMap::new(),
67 control_flow: ControlFlow::Wait,
68 unmounted_dom: Cell::new(Some(virtual_dom)),
69 float_all: false,
70 show_devtools: false,
71 cfg: Cell::new(Some(cfg)),
72 shared: Rc::new(SharedContext {
73 event_handlers: WindowEventHandlers::default(),
74 pending_webviews: Default::default(),
75 shortcut_manager: ShortcutRegistry::new(),
76 proxy: event_loop.create_proxy(),
77 target: event_loop.clone(),
78 }),
79 };
80
81 dioxus_html::set_event_converter(Box::new(crate::events::SerializedHtmlEventConverter));
83
84 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
86 app.set_global_hotkey_handler();
87
88 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
90 app.set_menubar_receiver();
91
92 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
94 app.set_tray_icon_receiver();
95
96 #[cfg(all(feature = "devtools", debug_assertions))]
98 app.connect_hotreload();
99
100 #[cfg(debug_assertions)]
101 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
102 app.connect_preserve_window_state_handler();
103
104 (event_loop, app)
105 }
106
107 pub fn tick(&mut self, window_event: &Event<'_, UserWindowEvent>) {
108 self.control_flow = ControlFlow::Wait;
109 self.shared
110 .event_handlers
111 .apply_event(window_event, &self.shared.target);
112 }
113
114 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
115 pub fn handle_global_hotkey(&self, event: global_hotkey::GlobalHotKeyEvent) {
116 self.shared.shortcut_manager.call_handlers(event);
117 }
118
119 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
120 pub fn handle_menu_event(&mut self, event: muda::MenuEvent) {
121 match event.id().0.as_str() {
122 "dioxus-float-top" => {
123 for webview in self.webviews.values() {
124 webview
125 .desktop_context
126 .window
127 .set_always_on_top(self.float_all);
128 }
129 self.float_all = !self.float_all;
130 }
131 "dioxus-toggle-dev-tools" => {
132 self.show_devtools = !self.show_devtools;
133 for webview in self.webviews.values() {
134 let wv = &webview.desktop_context.webview;
135 if self.show_devtools {
136 wv.open_devtools();
137 } else {
138 wv.close_devtools();
139 }
140 }
141 }
142 _ => (),
143 }
144 }
145 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
146 pub fn handle_tray_menu_event(&mut self, event: tray_icon::menu::MenuEvent) {
147 _ = event;
148 }
149
150 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
151 pub fn handle_tray_icon_event(&mut self, event: tray_icon::TrayIconEvent) {
152 if let tray_icon::TrayIconEvent::Click {
153 id: _,
154 position: _,
155 rect: _,
156 button,
157 button_state: _,
158 } = event
159 {
160 if button == tray_icon::MouseButton::Left {
161 for webview in self.webviews.values() {
162 webview.desktop_context.window.set_visible(true);
163 webview.desktop_context.window.set_focus();
164 }
165 }
166 }
167 }
168
169 #[cfg(all(feature = "devtools", debug_assertions))]
170 pub fn connect_hotreload(&self) {
171 if let Some(endpoint) = dioxus_cli_config::devserver_ws_endpoint() {
172 let proxy = self.shared.proxy.clone();
173 dioxus_devtools::connect(endpoint, move |msg| {
174 _ = proxy.send_event(UserWindowEvent::HotReloadEvent(msg));
175 })
176 }
177 }
178
179 pub fn handle_new_window(&mut self) {
180 for handler in self.shared.pending_webviews.borrow_mut().drain(..) {
181 let id = handler.desktop_context.window.id();
182 self.webviews.insert(id, handler);
183 _ = self.shared.proxy.send_event(UserWindowEvent::Poll(id));
184 }
185 }
186
187 pub fn handle_close_requested(&mut self, id: WindowId) {
188 use WindowCloseBehaviour::*;
189
190 match self.window_behavior {
191 LastWindowExitsApp => {
192 #[cfg(debug_assertions)]
193 self.persist_window_state();
194
195 self.webviews.remove(&id);
196 if self.webviews.is_empty() {
197 self.control_flow = ControlFlow::Exit
198 }
199 }
200
201 LastWindowHides if self.webviews.len() > 1 => {
202 self.webviews.remove(&id);
203 }
204
205 LastWindowHides => {
206 if let Some(webview) = self.webviews.get(&id) {
207 hide_last_window(&webview.desktop_context.window);
208 }
209 }
210
211 CloseWindow => {
212 self.webviews.remove(&id);
213 }
214 }
215 }
216
217 pub fn window_destroyed(&mut self, id: WindowId) {
218 self.webviews.remove(&id);
219
220 if matches!(
221 self.window_behavior,
222 WindowCloseBehaviour::LastWindowExitsApp
223 ) && self.webviews.is_empty()
224 {
225 self.control_flow = ControlFlow::Exit
226 }
227 }
228
229 pub fn resize_window(&self, id: WindowId, size: PhysicalSize<u32>) {
230 if let Some(webview) = self.webviews.get(&id) {
234 use wry::Rect;
235
236 _ = webview.desktop_context.webview.set_bounds(Rect {
237 position: wry::dpi::Position::Logical(wry::dpi::LogicalPosition::new(0.0, 0.0)),
238 size: wry::dpi::Size::Physical(wry::dpi::PhysicalSize::new(
239 size.width,
240 size.height,
241 )),
242 });
243 }
244 }
245
246 pub fn handle_start_cause_init(&mut self) {
247 let virtual_dom = self
248 .unmounted_dom
249 .take()
250 .expect("Virtualdom should be set before initialization");
251 let mut cfg = self
252 .cfg
253 .take()
254 .expect("Config should be set before initialization");
255
256 self.is_visible_before_start = cfg.window.window.visible;
257 cfg.window = cfg.window.with_visible(false);
258 let explicit_window_size = cfg.window.window.inner_size;
259 let explicit_window_position = cfg.window.window.position;
260
261 let webview = WebviewInstance::new(cfg, virtual_dom, self.shared.clone());
262
263 self.resume_from_state(&webview, explicit_window_size, explicit_window_position);
265
266 let id = webview.desktop_context.window.id();
267 self.webviews.insert(id, webview);
268 }
269
270 pub fn handle_browser_open(&mut self, msg: IpcMessage) {
271 if let Some(temp) = msg.params().as_object() {
272 if temp.contains_key("href") {
273 if let Some(href) = temp.get("href").and_then(|v| v.as_str()) {
274 if let Err(e) = webbrowser::open(href) {
275 tracing::error!("Open Browser error: {:?}", e);
276 }
277 }
278 }
279 }
280 }
281
282 pub fn handle_initialize_msg(&mut self, id: WindowId) {
286 let view = self.webviews.get_mut(&id).unwrap();
287
288 view.edits
289 .wry_queue
290 .with_mutation_state_mut(|f| view.dom.rebuild(f));
291
292 view.edits.wry_queue.send_edits();
293
294 view.desktop_context
295 .window
296 .set_visible(self.is_visible_before_start);
297
298 _ = self.shared.proxy.send_event(UserWindowEvent::Poll(id));
299 }
300
301 pub fn handle_close_msg(&mut self, id: WindowId) {
305 self.webviews.remove(&id);
306 if self.webviews.is_empty() {
307 self.control_flow = ControlFlow::Exit
308 }
309 }
310
311 pub fn handle_query_msg(&mut self, msg: IpcMessage, id: WindowId) {
312 let Ok(result) = serde_json::from_value::<QueryResult>(msg.params()) else {
313 return;
314 };
315
316 let Some(view) = self.webviews.get(&id) else {
317 return;
318 };
319
320 view.desktop_context.query.send(result);
321 }
322
323 #[cfg(all(feature = "devtools", debug_assertions))]
324 pub fn handle_hot_reload_msg(&mut self, msg: dioxus_devtools::DevserverMsg) {
325 use dioxus_devtools::DevserverMsg;
326
327 match msg {
328 DevserverMsg::HotReload(hr_msg) => {
329 for webview in self.webviews.values_mut() {
330 dioxus_devtools::apply_changes(&webview.dom, &hr_msg);
331 webview.poll_vdom();
332 }
333
334 if !hr_msg.assets.is_empty() {
335 for webview in self.webviews.values_mut() {
336 webview.kick_stylsheets();
337 }
338 }
339 }
340 DevserverMsg::FullReloadCommand
341 | DevserverMsg::FullReloadStart
342 | DevserverMsg::FullReloadFailed => {
343 }
346 DevserverMsg::Shutdown => {
347 self.control_flow = ControlFlow::Exit;
348 }
349 }
350 }
351
352 pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) {
353 let Ok(file_dialog) = serde_json::from_value::<FileDialogRequest>(msg.params()) else {
354 return;
355 };
356
357 let id = ElementId(file_dialog.target);
358 let event_name = &file_dialog.event;
359 let event_bubbles = file_dialog.bubbles;
360 let files = file_dialog.get_file_event();
361
362 let as_any = Box::new(DesktopFileUploadForm {
363 files: Arc::new(NativeFileEngine::new(files)),
364 });
365
366 let data = Rc::new(PlatformEventData::new(as_any));
367
368 let Some(view) = self.webviews.get_mut(&window) else {
369 return;
370 };
371
372 let event = dioxus_core::Event::new(data as Rc<dyn Any>, event_bubbles);
373
374 let runtime = view.dom.runtime();
375 if event_name == "change&input" {
376 runtime.handle_event("input", event.clone(), id);
377 runtime.handle_event("change", event, id);
378 } else {
379 runtime.handle_event(event_name, event, id);
380 }
381 }
382
383 pub fn poll_vdom(&mut self, id: WindowId) {
389 let Some(view) = self.webviews.get_mut(&id) else {
390 return;
391 };
392
393 view.poll_vdom();
394 }
395
396 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
397 fn set_global_hotkey_handler(&self) {
398 let receiver = self.shared.proxy.clone();
399
400 global_hotkey::GlobalHotKeyEvent::set_event_handler(Some(move |t| {
405 _ = receiver.send_event(UserWindowEvent::GlobalHotKeyEvent(t));
407 }));
408 }
409
410 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
411 fn set_menubar_receiver(&self) {
412 let receiver = self.shared.proxy.clone();
413
414 muda::MenuEvent::set_event_handler(Some(move |t| {
419 _ = receiver.send_event(UserWindowEvent::MudaMenuEvent(t));
421 }));
422 }
423
424 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
425 fn set_tray_icon_receiver(&self) {
426 let receiver = self.shared.proxy.clone();
427
428 tray_icon::TrayIconEvent::set_event_handler(Some(move |t| {
433 _ = receiver.send_event(UserWindowEvent::TrayIconEvent(t));
435 }));
436
437 let receiver = self.shared.proxy.clone();
439 tray_icon::menu::MenuEvent::set_event_handler(Some(move |t| {
440 _ = receiver.send_event(UserWindowEvent::TrayMenuEvent(t));
442 }));
443 }
444
445 pub(crate) fn handle_loop_destroyed(&self) {
451 #[cfg(debug_assertions)]
452 self.persist_window_state();
453 }
454
455 #[cfg(debug_assertions)]
456 fn persist_window_state(&self) {
457 if let Some(webview) = self.webviews.values().next() {
458 let window = &webview.desktop_context.window;
459
460 let Some(monitor) = window.current_monitor() else {
461 return;
462 };
463
464 let Ok(position) = window.outer_position() else {
465 return;
466 };
467
468 let size = window.outer_size();
469
470 let x = position.x;
471 let y = position.y;
472
473 let adjustment = match window.is_decorated() {
478 true if cfg!(target_os = "macos") => 56,
479 _ => 0,
480 };
481
482 let Some(monitor_name) = monitor.name() else {
483 return;
484 };
485
486 let state = PreservedWindowState {
487 x,
488 y,
489 width: size.width.max(200),
490 height: size.height.saturating_sub(adjustment).max(200),
491 monitor: monitor_name.to_string(),
492 };
493
494 if let Ok(state) = serde_json::to_string(&state) {
496 _ = std::fs::write(restore_file(), state);
497 }
498 }
499 }
500
501 fn resume_from_state(
503 &mut self,
504 webview: &WebviewInstance,
505 explicit_inner_size: Option<tao::dpi::Size>,
506 explicit_window_position: Option<tao::dpi::Position>,
507 ) {
508 if cfg!(target_os = "android") || cfg!(target_os = "ios") {
510 return;
511 }
512
513 if !cfg!(debug_assertions) {
515 return;
516 }
517
518 if let Ok(state) = std::fs::read_to_string(restore_file()) {
519 if let Ok(state) = serde_json::from_str::<PreservedWindowState>(&state) {
520 let window = &webview.desktop_context.window;
521 let position = (state.x, state.y);
522 let size = (state.width, state.height);
523
524 if explicit_window_position.is_none() {
526 window.set_outer_position(tao::dpi::PhysicalPosition::new(
527 position.0, position.1,
528 ));
529 }
530
531 if explicit_inner_size.is_none() {
533 window.set_inner_size(tao::dpi::PhysicalSize::new(size.0, size.1));
534 }
535 }
536 }
537 }
538
539 #[cfg(debug_assertions)]
542 fn connect_preserve_window_state_handler(&self) {
543 #[cfg(unix)]
545 {
546 let target = self.shared.proxy.clone();
548 std::thread::spawn(move || {
549 use signal_hook::consts::{SIGINT, SIGTERM};
550 let sigkill = signal_hook::iterator::Signals::new([SIGTERM, SIGINT]);
551 if let Ok(mut sigkill) = sigkill {
552 for _ in sigkill.forever() {
553 if target.send_event(UserWindowEvent::Shutdown).is_err() {
554 std::process::exit(0);
555 }
556
557 std::thread::sleep(std::time::Duration::from_secs(1));
559 }
560 }
561 });
562 }
563 }
564}
565
566#[derive(Debug, serde::Serialize, serde::Deserialize)]
567struct PreservedWindowState {
568 x: i32,
569 y: i32,
570 width: u32,
571 height: u32,
572 monitor: String,
573}
574
575#[allow(unused)]
581fn hide_last_window(window: &Window) {
582 #[cfg(target_os = "windows")]
583 {
584 use tao::platform::windows::WindowExtWindows;
585 window.set_visible(false);
586 }
587
588 #[cfg(target_os = "linux")]
589 {
590 use tao::platform::unix::WindowExtUnix;
591 window.set_visible(false);
592 }
593
594 #[cfg(target_os = "macos")]
595 {
596 use objc::runtime::Object;
600 use objc::{msg_send, sel, sel_impl};
601 #[allow(unexpected_cfgs)]
602 objc::rc::autoreleasepool(|| unsafe {
603 let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication];
604 let nil = std::ptr::null_mut::<Object>();
605 let _: () = msg_send![app, hide: nil];
606 });
607 }
608}
609
610fn restore_file() -> std::path::PathBuf {
612 let dir = dioxus_cli_config::session_cache_dir().unwrap_or_else(std::env::temp_dir);
613 dir.join("window-state.json")
614}