1use crate::{
2 config::{Config, WindowCloseBehaviour},
3 edits::EditWebsocket,
4 event_handlers::WindowEventHandlers,
5 ipc::{IpcMessage, UserWindowEvent},
6 query::QueryResult,
7 shortcut::ShortcutRegistry,
8 webview::{PendingWebview, WebviewInstance},
9};
10use dioxus_core::VirtualDom;
11use std::{
12 cell::{Cell, RefCell},
13 collections::HashMap,
14 rc::Rc,
15 time::Duration,
16};
17use tao::{
18 dpi::PhysicalSize,
19 event::Event,
20 event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget},
21 window::WindowId,
22};
23
24pub(crate) struct App {
26 pub(crate) unmounted_dom: Cell<Option<VirtualDom>>,
29 pub(crate) cfg: Cell<Option<Config>>,
30
31 pub(crate) control_flow: ControlFlow,
33 pub(crate) is_visible_before_start: bool,
34 pub(crate) exit_on_last_window_close: bool,
35 pub(crate) disable_dma_buf_on_wayland: bool,
36 pub(crate) webviews: HashMap<WindowId, WebviewInstance>,
37 pub(crate) float_all: bool,
38 pub(crate) show_devtools: bool,
39 pub(crate) tray_icon_show_window_on_click: 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<PendingWebview>>,
51 pub(crate) shortcut_manager: ShortcutRegistry,
52 pub(crate) proxy: EventLoopProxy<UserWindowEvent>,
53 pub(crate) target: EventLoopWindowTarget<UserWindowEvent>,
54 pub(crate) websocket: EditWebsocket,
55}
56
57impl App {
58 pub fn new(mut cfg: Config, virtual_dom: VirtualDom) -> (EventLoop<UserWindowEvent>, Self) {
59 let event_loop = cfg
60 .event_loop
61 .take()
62 .unwrap_or_else(|| EventLoopBuilder::<UserWindowEvent>::with_user_event().build());
63
64 let tray_icon_show_window_on_click = cfg.tray_icon_show_window_on_click;
65
66 let app = Self {
67 exit_on_last_window_close: cfg.exit_on_last_window_close,
68 disable_dma_buf_on_wayland: cfg.disable_dma_buf_on_wayland,
69 is_visible_before_start: true,
70 webviews: HashMap::new(),
71 control_flow: ControlFlow::Wait,
72 unmounted_dom: Cell::new(Some(virtual_dom)),
73 float_all: false,
74 show_devtools: false,
75 tray_icon_show_window_on_click,
76 cfg: Cell::new(Some(cfg)),
77 shared: Rc::new(SharedContext {
78 event_handlers: WindowEventHandlers::default(),
79 pending_webviews: Default::default(),
80 shortcut_manager: ShortcutRegistry::new(),
81 proxy: event_loop.create_proxy(),
82 target: event_loop.clone(),
83 websocket: EditWebsocket::start(),
84 }),
85 };
86
87 dioxus_html::set_event_converter(Box::new(crate::events::SerializedHtmlEventConverter));
89
90 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
92 app.set_global_hotkey_handler();
93
94 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
96 app.set_menubar_receiver();
97
98 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
100 app.set_tray_icon_receiver();
101
102 #[cfg(all(feature = "devtools", debug_assertions))]
104 app.connect_hotreload();
105
106 #[cfg(debug_assertions)]
107 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
108 app.connect_preserve_window_state_handler();
109
110 app.disable_dma_buf();
112
113 (event_loop, app)
114 }
115
116 pub fn tick(&mut self, window_event: &Event<'_, UserWindowEvent>) {
117 self.control_flow = ControlFlow::Wait;
118 self.shared
119 .event_handlers
120 .apply_event(window_event, &self.shared.target);
121 }
122
123 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
124 pub fn handle_global_hotkey(&self, event: global_hotkey::GlobalHotKeyEvent) {
125 self.shared.shortcut_manager.call_handlers(event);
126 }
127
128 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
129 pub fn handle_menu_event(&mut self, event: muda::MenuEvent) {
130 match event.id().0.as_str() {
131 "dioxus-float-top" => {
132 for webview in self.webviews.values() {
133 webview
134 .desktop_context
135 .window
136 .set_always_on_top(self.float_all);
137 }
138 self.float_all = !self.float_all;
139 }
140 "dioxus-toggle-dev-tools" => {
141 self.show_devtools = !self.show_devtools;
142 for webview in self.webviews.values() {
143 let wv = &webview.desktop_context.webview;
144 if self.show_devtools {
145 wv.open_devtools();
146 } else {
147 wv.close_devtools();
148 }
149 }
150 }
151 _ => (),
152 }
153 }
154 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
155 pub fn handle_tray_menu_event(&mut self, event: tray_icon::menu::MenuEvent) {
156 _ = event;
157 }
158
159 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
160 pub fn handle_tray_icon_event(&mut self, event: tray_icon::TrayIconEvent) {
161 if let tray_icon::TrayIconEvent::Click {
162 id: _,
163 position: _,
164 rect: _,
165 button,
166 button_state: _,
167 } = event
168 {
169 if button == tray_icon::MouseButton::Left && self.tray_icon_show_window_on_click {
170 for webview in self.webviews.values() {
171 webview.desktop_context.window.set_visible(true);
172 webview.desktop_context.window.set_focus();
173 }
174 }
175 }
176 }
177
178 #[cfg(all(feature = "devtools", debug_assertions))]
179 pub fn connect_hotreload(&self) {
180 let proxy = self.shared.proxy.clone();
181 dioxus_devtools::connect(move |msg| {
182 _ = proxy.send_event(UserWindowEvent::HotReloadEvent(msg));
183 })
184 }
185
186 pub fn handle_new_window(&mut self) {
187 for pending_webview in self.shared.pending_webviews.borrow_mut().drain(..) {
188 let window = pending_webview.create_window(&self.shared);
189 let id = window.desktop_context.window.id();
190 self.webviews.insert(id, window);
191 _ = self.shared.proxy.send_event(UserWindowEvent::Poll(id));
192 }
193 }
194
195 pub fn handle_close_requested(&mut self, id: WindowId) {
196 let Some(window) = self.webviews.get(&id) else {
197 return;
199 };
200
201 match window.desktop_context.close_behaviour.get() {
202 WindowCloseBehaviour::WindowHides => {
204 window.desktop_context.window.set_visible(false);
205 }
206
207 WindowCloseBehaviour::WindowCloses => {
210 #[cfg(debug_assertions)]
211 self.persist_window_state();
212
213 self.webviews.remove(&id);
214
215 if self.exit_on_last_window_close && self.webviews.is_empty() {
216 self.control_flow = ControlFlow::Exit
217 }
218 }
219 };
220 }
221
222 pub fn window_destroyed(&mut self, id: WindowId) {
223 self.webviews.remove(&id);
224
225 if self.exit_on_last_window_close && self.webviews.is_empty() {
226 self.control_flow = ControlFlow::Exit
227 }
228 }
229
230 pub fn resize_window(&self, id: WindowId, size: PhysicalSize<u32>) {
231 if let Some(webview) = self.webviews.get(&id) {
235 use wry::Rect;
236
237 _ = webview.desktop_context.webview.set_bounds(Rect {
238 position: wry::dpi::Position::Logical(wry::dpi::LogicalPosition::new(0.0, 0.0)),
239 size: wry::dpi::Size::Physical(wry::dpi::PhysicalSize::new(
240 size.width,
241 size.height,
242 )),
243 });
244 }
245 }
246
247 pub fn handle_start_cause_init(&mut self) {
248 let virtual_dom = self
249 .unmounted_dom
250 .take()
251 .expect("Virtualdom should be set before initialization");
252 #[allow(unused_mut)]
253 let mut cfg = self
254 .cfg
255 .take()
256 .expect("Config should be set before initialization");
257
258 self.is_visible_before_start = cfg.window.window.visible;
259 #[cfg(not(target_os = "linux"))]
260 {
261 cfg.window = cfg.window.with_visible(false);
262 }
263 let explicit_window_size = cfg.window.window.inner_size;
264 let explicit_window_position = cfg.window.window.position;
265
266 let webview = WebviewInstance::new(cfg, virtual_dom, self.shared.clone());
267
268 self.resume_from_state(&webview, explicit_window_size, explicit_window_position);
270
271 let id = webview.desktop_context.window.id();
272 self.webviews.insert(id, webview);
273 }
274
275 pub fn handle_browser_open(&mut self, msg: IpcMessage) {
276 if let Some(temp) = msg.params().as_object() {
277 if temp.contains_key("href") {
278 if let Some(href) = temp.get("href").and_then(|v| v.as_str()) {
279 if let Err(err) = webbrowser::open(href) {
280 tracing::error!("Failed to open URL: {}", err);
281 }
282 }
283 }
284 }
285 }
286
287 pub fn handle_initialize_msg(&mut self, id: WindowId) {
291 let view = self.webviews.get_mut(&id).unwrap();
292
293 view.edits
294 .wry_queue
295 .with_mutation_state_mut(|f| view.dom.rebuild(f));
296
297 view.edits.wry_queue.send_edits();
298
299 #[cfg(not(target_os = "linux"))]
300 {
301 view.desktop_context
302 .window
303 .set_visible(self.is_visible_before_start);
304 }
305
306 _ = self.shared.proxy.send_event(UserWindowEvent::Poll(id));
307 }
308
309 pub fn handle_query_msg(&mut self, msg: IpcMessage, id: WindowId) {
310 let Ok(result) = serde_json::from_value::<QueryResult>(msg.params()) else {
311 return;
312 };
313
314 let Some(view) = self.webviews.get(&id) else {
315 return;
316 };
317
318 view.desktop_context.query.send(result);
319 }
320
321 #[cfg(all(feature = "devtools", debug_assertions))]
322 pub fn handle_hot_reload_msg(&mut self, msg: dioxus_devtools::DevserverMsg) {
323 use std::time::Duration;
324
325 use dioxus_devtools::DevserverMsg;
326
327 const TOAST_TIMEOUT: Duration = Duration::from_secs(2);
329 const TOAST_TIMEOUT_LONG: Duration = Duration::from_secs(3600); match msg {
332 DevserverMsg::HotReload(hr_msg) => {
333 for webview in self.webviews.values_mut() {
334 {
335 #[cfg(target_os = "android")]
338 let _lock = crate::android_sync_lock::android_runtime_lock();
339 dioxus_devtools::apply_changes(&webview.dom, &hr_msg);
340 }
341
342 webview.poll_vdom();
343 }
344
345 if !hr_msg.assets.is_empty() {
346 for webview in self.webviews.values_mut() {
347 webview.kick_stylsheets();
348 }
349 }
350
351 if hr_msg.jump_table.is_some()
352 && hr_msg.for_build_id == Some(dioxus_cli_config::build_id())
353 {
354 self.send_toast_to_all(
355 "Hot-patch success!",
356 &format!("App successfully patched in {} ms", hr_msg.ms_elapsed),
357 "success",
358 TOAST_TIMEOUT,
359 false,
360 );
361 }
362 }
363 DevserverMsg::FullReloadCommand => {
364 self.send_toast_to_all(
365 "Successfully rebuilt.",
366 "Your app was rebuilt successfully and without error.",
367 "success",
368 TOAST_TIMEOUT,
369 true,
370 );
371 }
372 DevserverMsg::FullReloadStart => self.send_toast_to_all(
373 "Your app is being rebuilt.",
374 "A non-hot-reloadable change occurred and we must rebuild.",
375 "info",
376 TOAST_TIMEOUT_LONG,
377 false,
378 ),
379 DevserverMsg::FullReloadFailed => self.send_toast_to_all(
380 "Oops! The build failed.",
381 "We tried to rebuild your app, but something went wrong.",
382 "error",
383 TOAST_TIMEOUT_LONG,
384 false,
385 ),
386 DevserverMsg::HotPatchStart => self.send_toast_to_all(
387 "Hot-patching app...",
388 "Hot-patching modified Rust code.",
389 "info",
390 TOAST_TIMEOUT_LONG,
391 false,
392 ),
393 DevserverMsg::Shutdown => {
394 self.control_flow = ControlFlow::Exit;
395 }
396 _ => {}
397 }
398 }
399
400 #[cfg(all(feature = "devtools", debug_assertions))]
401 fn send_toast_to_all(
402 &self,
403 header_text: &str,
404 message: &str,
405 level: &str,
406 duration: Duration,
407 after_reload: bool,
408 ) {
409 for webview in self.webviews.values() {
410 webview.show_toast(header_text, message, level, duration, after_reload);
411 }
412 }
413
414 pub fn poll_vdom(&mut self, id: WindowId) {
420 let Some(view) = self.webviews.get_mut(&id) else {
421 return;
422 };
423
424 view.poll_vdom();
425 }
426
427 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
428 fn set_global_hotkey_handler(&self) {
429 let receiver = self.shared.proxy.clone();
430
431 global_hotkey::GlobalHotKeyEvent::set_event_handler(Some(move |t| {
436 _ = receiver.send_event(UserWindowEvent::GlobalHotKeyEvent(t));
438 }));
439 }
440
441 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
442 fn set_menubar_receiver(&self) {
443 let receiver = self.shared.proxy.clone();
444
445 muda::MenuEvent::set_event_handler(Some(move |t| {
450 _ = receiver.send_event(UserWindowEvent::MudaMenuEvent(t));
452 }));
453 }
454
455 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
456 fn set_tray_icon_receiver(&self) {
457 let receiver = self.shared.proxy.clone();
458
459 tray_icon::TrayIconEvent::set_event_handler(Some(move |t| {
464 _ = receiver.send_event(UserWindowEvent::TrayIconEvent(t));
466 }));
467
468 let receiver = self.shared.proxy.clone();
470 tray_icon::menu::MenuEvent::set_event_handler(Some(move |t| {
471 _ = receiver.send_event(UserWindowEvent::TrayMenuEvent(t));
473 }));
474 }
475
476 pub(crate) fn handle_loop_destroyed(&self) {
482 #[cfg(debug_assertions)]
483 self.persist_window_state();
484 }
485
486 #[cfg(debug_assertions)]
487 fn persist_window_state(&self) {
488 if let Some(webview) = self.webviews.values().next() {
489 let window = &webview.desktop_context.window;
490
491 let Some(monitor) = window.current_monitor() else {
492 return;
493 };
494
495 let Ok(position) = window.outer_position() else {
496 return;
497 };
498 let (x, y) = if cfg!(target_os = "macos") {
499 let position = position.to_logical::<i32>(window.scale_factor());
500 (position.x, position.y)
501 } else {
502 (position.x, position.y)
503 };
504
505 let (width, height) = if cfg!(target_os = "macos") {
506 let size = window.outer_size();
507 let size = size.to_logical::<u32>(window.scale_factor());
508 let adjustment = if window.is_decorated() { 28 } else { 0 };
513 (size.width, size.height.saturating_sub(adjustment))
514 } else {
515 let size = window.inner_size();
516 (size.width, size.height)
517 };
518
519 let Some(monitor_name) = monitor.name() else {
520 return;
521 };
522
523 let state = PreservedWindowState {
524 x,
525 y,
526 width: width.max(200),
527 height: height.max(200),
528 monitor: monitor_name.to_string(),
529 };
530
531 if let Ok(state) = serde_json::to_string(&state) {
533 _ = std::fs::write(restore_file(), state);
534 }
535 }
536 }
537
538 fn resume_from_state(
540 &mut self,
541 webview: &WebviewInstance,
542 explicit_inner_size: Option<tao::dpi::Size>,
543 explicit_window_position: Option<tao::dpi::Position>,
544 ) {
545 if cfg!(target_os = "android") || cfg!(target_os = "ios") {
547 return;
548 }
549
550 if !cfg!(debug_assertions) {
552 return;
553 }
554
555 if let Ok(state) = std::fs::read_to_string(restore_file()) {
556 if let Ok(state) = serde_json::from_str::<PreservedWindowState>(&state) {
557 let window = &webview.desktop_context.window;
558 let position = (state.x, state.y);
559 let size = (state.width, state.height);
560
561 if explicit_window_position.is_none() {
563 if cfg!(target_os = "macos") {
564 window.set_outer_position(tao::dpi::LogicalPosition::new(
565 position.0, position.1,
566 ));
567 } else {
568 window.set_outer_position(tao::dpi::PhysicalPosition::new(
569 position.0, position.1,
570 ));
571 }
572 }
573
574 if explicit_inner_size.is_none() {
576 if cfg!(target_os = "macos") {
577 window.set_inner_size(tao::dpi::LogicalSize::new(size.0, size.1));
578 } else {
579 window.set_inner_size(tao::dpi::PhysicalSize::new(size.0, size.1));
580 }
581 }
582 }
583 }
584 }
585
586 #[cfg(debug_assertions)]
589 fn connect_preserve_window_state_handler(&self) {
590 #[cfg(unix)]
592 {
593 let target = self.shared.proxy.clone();
595 std::thread::spawn(move || {
596 use signal_hook::consts::{SIGINT, SIGTERM};
597 let sigkill = signal_hook::iterator::Signals::new([SIGTERM, SIGINT]);
598 if let Ok(mut sigkill) = sigkill {
599 for _ in sigkill.forever() {
600 if target.send_event(UserWindowEvent::Shutdown).is_err() {
601 std::process::exit(0);
602 }
603
604 std::thread::sleep(std::time::Duration::from_millis(100));
606 }
607 }
608 });
609 }
610 }
611
612 fn disable_dma_buf(&self) {
614 if cfg!(target_os = "linux") && self.disable_dma_buf_on_wayland {
615 static INIT: std::sync::Once = std::sync::Once::new();
616 INIT.call_once(|| {
617 if std::path::Path::new("/dev/dri").exists()
618 && std::env::var("XDG_SESSION_TYPE").unwrap_or_default() == "wayland"
619 {
620 unsafe {
623 std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
625 }
626 }
627 unsafe {
628 std::env::set_var("GDK_BACKEND", "x11");
629 }
630 });
631 }
632 }
633}
634
635#[derive(Debug, serde::Serialize, serde::Deserialize)]
636struct PreservedWindowState {
637 x: i32,
638 y: i32,
639 width: u32,
640 height: u32,
641 monitor: String,
642}
643
644fn restore_file() -> std::path::PathBuf {
646 let dir = dioxus_cli_config::session_cache_dir().unwrap_or_else(std::env::temp_dir);
647 dir.join("window-state.json")
648}