app.rs 12 KB

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