app.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. use crate::{
  2. config::{Config, WindowCloseBehaviour},
  3. element::DesktopElement,
  4. event_handlers::WindowEventHandlers,
  5. file_upload::{DesktopFileDragEvent, DesktopFileUploadForm, FileDialogRequest},
  6. ipc::{IpcMessage, UserWindowEvent},
  7. query::QueryResult,
  8. shortcut::ShortcutRegistry,
  9. webview::WebviewInstance,
  10. };
  11. use dioxus_core::ElementId;
  12. use dioxus_core::VirtualDom;
  13. use dioxus_html::{native_bind::NativeFileEngine, HasFileData, HtmlEvent, PlatformEventData};
  14. use std::{
  15. cell::{Cell, RefCell},
  16. collections::HashMap,
  17. rc::Rc,
  18. sync::Arc,
  19. };
  20. use tao::{
  21. event::Event,
  22. event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget},
  23. 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. /// This single blob of state is shared between all the windows so they have access to the runtime state
  37. ///
  38. /// This includes stuff like the event handlers, shortcuts, etc as well as ways to modify *other* windows
  39. pub(crate) shared: Rc<SharedContext>,
  40. }
  41. /// A bundle of state shared between all the windows, providing a way for us to communicate with running webview.
  42. ///
  43. /// Todo: everything in this struct is wrapped in Rc<>, but we really only need the one top-level refcell
  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(cfg: Config, virtual_dom: VirtualDom) -> (EventLoop<UserWindowEvent>, Self) {
  53. let event_loop = EventLoopBuilder::<UserWindowEvent>::with_user_event().build();
  54. let app = Self {
  55. window_behavior: cfg.last_window_close_behaviour,
  56. is_visible_before_start: true,
  57. webviews: HashMap::new(),
  58. control_flow: ControlFlow::Wait,
  59. unmounted_dom: Cell::new(Some(virtual_dom)),
  60. cfg: Cell::new(Some(cfg)),
  61. shared: Rc::new(SharedContext {
  62. event_handlers: WindowEventHandlers::default(),
  63. pending_webviews: Default::default(),
  64. shortcut_manager: ShortcutRegistry::new(),
  65. proxy: event_loop.create_proxy(),
  66. target: event_loop.clone(),
  67. }),
  68. };
  69. // Set the event converter
  70. dioxus_html::set_event_converter(Box::new(crate::events::SerializedHtmlEventConverter));
  71. // Wire up the global hotkey handler
  72. #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
  73. app.set_global_hotkey_handler();
  74. // Allow hotreloading to work - but only in debug mode
  75. #[cfg(all(feature = "hot-reload", debug_assertions))]
  76. app.connect_hotreload();
  77. (event_loop, app)
  78. }
  79. pub fn tick(&mut self, window_event: &Event<'_, UserWindowEvent>) {
  80. self.control_flow = ControlFlow::Wait;
  81. self.shared
  82. .event_handlers
  83. .apply_event(window_event, &self.shared.target);
  84. }
  85. #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
  86. pub fn handle_global_hotkey(&self, event: global_hotkey::GlobalHotKeyEvent) {
  87. self.shared.shortcut_manager.call_handlers(event);
  88. }
  89. #[cfg(all(feature = "hot-reload", debug_assertions))]
  90. pub fn connect_hotreload(&self) {
  91. dioxus_hot_reload::connect({
  92. let proxy = self.shared.proxy.clone();
  93. move |template| {
  94. let _ = proxy.send_event(UserWindowEvent::HotReloadEvent(template));
  95. }
  96. });
  97. }
  98. pub fn handle_new_window(&mut self) {
  99. for handler in self.shared.pending_webviews.borrow_mut().drain(..) {
  100. let id = handler.desktop_context.window.id();
  101. self.webviews.insert(id, handler);
  102. _ = self.shared.proxy.send_event(UserWindowEvent::Poll(id));
  103. }
  104. }
  105. pub fn handle_close_requested(&mut self, id: WindowId) {
  106. use WindowCloseBehaviour::*;
  107. match self.window_behavior {
  108. LastWindowExitsApp => {
  109. self.webviews.remove(&id);
  110. if self.webviews.is_empty() {
  111. self.control_flow = ControlFlow::Exit
  112. }
  113. }
  114. LastWindowHides => {
  115. let Some(webview) = self.webviews.get(&id) else {
  116. return;
  117. };
  118. hide_app_window(&webview.desktop_context.webview);
  119. }
  120. CloseWindow => {
  121. self.webviews.remove(&id);
  122. }
  123. }
  124. }
  125. pub fn window_destroyed(&mut self, id: WindowId) {
  126. self.webviews.remove(&id);
  127. if matches!(
  128. self.window_behavior,
  129. WindowCloseBehaviour::LastWindowExitsApp
  130. ) && self.webviews.is_empty()
  131. {
  132. self.control_flow = ControlFlow::Exit
  133. }
  134. }
  135. pub fn handle_start_cause_init(&mut self) {
  136. let virtual_dom = self.unmounted_dom.take().unwrap();
  137. let cfg = self.cfg.take().unwrap();
  138. self.is_visible_before_start = cfg.window.window.visible;
  139. let webview = WebviewInstance::new(cfg, virtual_dom, self.shared.clone());
  140. let id = webview.desktop_context.window.id();
  141. self.webviews.insert(id, webview);
  142. }
  143. pub fn handle_browser_open(&mut self, msg: IpcMessage) {
  144. if let Some(temp) = msg.params().as_object() {
  145. if temp.contains_key("href") {
  146. let open = webbrowser::open(temp["href"].as_str().unwrap());
  147. if let Err(e) = open {
  148. tracing::error!("Open Browser error: {:?}", e);
  149. }
  150. }
  151. }
  152. }
  153. /// The webview is finally loaded
  154. ///
  155. /// Let's rebuild it and then start polling it
  156. pub fn handle_initialize_msg(&mut self, id: WindowId) {
  157. let view = self.webviews.get_mut(&id).unwrap();
  158. view.dom
  159. .rebuild(&mut *view.desktop_context.mutation_state.borrow_mut());
  160. view.desktop_context.send_edits();
  161. view.desktop_context
  162. .window
  163. .set_visible(self.is_visible_before_start);
  164. _ = self.shared.proxy.send_event(UserWindowEvent::Poll(id));
  165. }
  166. /// Todo: maybe we should poll the virtualdom asking if it has any final actions to apply before closing the webview
  167. ///
  168. /// Technically you can handle this with the use_window_event hook
  169. pub fn handle_close_msg(&mut self, id: WindowId) {
  170. self.webviews.remove(&id);
  171. if self.webviews.is_empty() {
  172. self.control_flow = ControlFlow::Exit
  173. }
  174. }
  175. pub fn handle_query_msg(&mut self, msg: IpcMessage, id: WindowId) {
  176. let Ok(result) = serde_json::from_value::<QueryResult>(msg.params()) else {
  177. return;
  178. };
  179. let Some(view) = self.webviews.get(&id) else {
  180. return;
  181. };
  182. view.desktop_context.query.send(result);
  183. }
  184. pub fn handle_user_event_msg(&mut self, msg: IpcMessage, id: WindowId) {
  185. let parsed_params = serde_json::from_value(msg.params())
  186. .map_err(|err| tracing::error!("Error parsing user_event: {:?}", err));
  187. let Ok(evt) = parsed_params else { return };
  188. let HtmlEvent {
  189. element,
  190. name,
  191. bubbles,
  192. data,
  193. } = evt;
  194. let view = self.webviews.get_mut(&id).unwrap();
  195. let query = view.desktop_context.query.clone();
  196. let recent_file = view.desktop_context.file_hover.clone();
  197. // check for a mounted event placeholder and replace it with a desktop specific element
  198. let as_any = match data {
  199. dioxus_html::EventData::Mounted => {
  200. let element = DesktopElement::new(element, view.desktop_context.clone(), query);
  201. Rc::new(PlatformEventData::new(Box::new(element)))
  202. }
  203. dioxus_html::EventData::Drag(ref drag) => {
  204. // we want to override this with a native file engine, provided by the most recent drag event
  205. if drag.files().is_some() {
  206. let file_event = recent_file.current().unwrap();
  207. let paths = match file_event {
  208. wry::FileDropEvent::Hovered { paths, .. } => paths,
  209. wry::FileDropEvent::Dropped { paths, .. } => paths,
  210. _ => vec![],
  211. };
  212. Rc::new(PlatformEventData::new(Box::new(DesktopFileDragEvent {
  213. mouse: drag.mouse.clone(),
  214. files: Arc::new(NativeFileEngine::new(paths)),
  215. })))
  216. } else {
  217. data.into_any()
  218. }
  219. }
  220. _ => data.into_any(),
  221. };
  222. view.dom.handle_event(&name, as_any, element, bubbles);
  223. view.dom
  224. .render_immediate(&mut *view.desktop_context.mutation_state.borrow_mut());
  225. view.desktop_context.send_edits();
  226. }
  227. #[cfg(all(feature = "hot-reload", debug_assertions))]
  228. pub fn handle_hot_reload_msg(&mut self, msg: dioxus_hot_reload::HotReloadMsg) {
  229. match msg {
  230. dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
  231. for webview in self.webviews.values_mut() {
  232. webview.dom.replace_template(template);
  233. webview.poll_vdom();
  234. }
  235. }
  236. dioxus_hot_reload::HotReloadMsg::Shutdown => {
  237. self.control_flow = ControlFlow::Exit;
  238. }
  239. }
  240. }
  241. pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) {
  242. let Ok(file_dialog) = serde_json::from_value::<FileDialogRequest>(msg.params()) else {
  243. return;
  244. };
  245. let id = ElementId(file_dialog.target);
  246. let event_name = &file_dialog.event;
  247. let event_bubbles = file_dialog.bubbles;
  248. let files = file_dialog.get_file_event();
  249. let as_any = Box::new(DesktopFileUploadForm {
  250. files: Arc::new(NativeFileEngine::new(files)),
  251. });
  252. let data = Rc::new(PlatformEventData::new(as_any));
  253. let view = self.webviews.get_mut(&window).unwrap();
  254. if event_name == "change&input" {
  255. view.dom
  256. .handle_event("input", data.clone(), id, event_bubbles);
  257. view.dom.handle_event("change", data, id, event_bubbles);
  258. } else {
  259. view.dom.handle_event(event_name, data, id, event_bubbles);
  260. }
  261. view.dom
  262. .render_immediate(&mut *view.desktop_context.mutation_state.borrow_mut());
  263. view.desktop_context.send_edits();
  264. }
  265. /// Poll the virtualdom until it's pending
  266. ///
  267. /// 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
  268. ///
  269. /// All IO is done on the tokio runtime we started earlier
  270. pub fn poll_vdom(&mut self, id: WindowId) {
  271. let Some(view) = self.webviews.get_mut(&id) else {
  272. return;
  273. };
  274. view.poll_vdom();
  275. }
  276. #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
  277. fn set_global_hotkey_handler(&self) {
  278. let receiver = self.shared.proxy.clone();
  279. // The event loop becomes the hotkey receiver
  280. // This means we don't need to poll the receiver on every tick - we just get the events as they come in
  281. // This is a bit more efficient than the previous implementation, but if someone else sets a handler, the
  282. // receiver will become inert.
  283. global_hotkey::GlobalHotKeyEvent::set_event_handler(Some(move |t| {
  284. // todo: should we unset the event handler when the app shuts down?
  285. _ = receiver.send_event(UserWindowEvent::GlobalHotKeyEvent(t));
  286. }));
  287. }
  288. }
  289. /// Different hide implementations per platform
  290. #[allow(unused)]
  291. pub fn hide_app_window(window: &wry::WebView) {
  292. #[cfg(target_os = "windows")]
  293. {
  294. use tao::platform::windows::WindowExtWindows;
  295. window.set_visible(false);
  296. }
  297. #[cfg(target_os = "linux")]
  298. {
  299. use tao::platform::unix::WindowExtUnix;
  300. window.set_visible(false);
  301. }
  302. #[cfg(target_os = "macos")]
  303. {
  304. // window.set_visible(false); has the wrong behaviour on macOS
  305. // It will hide the window but not show it again when the user switches
  306. // back to the app. `NSApplication::hide:` has the correct behaviour
  307. use objc::runtime::Object;
  308. use objc::{msg_send, sel, sel_impl};
  309. objc::rc::autoreleasepool(|| unsafe {
  310. let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication];
  311. let nil = std::ptr::null_mut::<Object>();
  312. let _: () = msg_send![app, hide: nil];
  313. });
  314. }
  315. }