pub use crate::assets::{AssetFuture, AssetHandler, AssetRequest, AssetResponse}; pub use crate::cfg::{Config, WindowCloseBehaviour}; pub use crate::desktop_context::DesktopContext; pub use crate::desktop_context::{window, DesktopService, WryEventHandler, WryEventHandlerId}; use crate::edits::{send_edits, EditQueue, WebviewQueue}; use crate::element::DesktopElement; use crate::eval::init_eval; use crate::events::{IpcMessage, IpcMethod}; use crate::file_upload; use crate::hooks::*; use crate::query::QueryResult; use crate::shortcut::GlobalHotKeyEvent; use crate::shortcut::ShortcutRegistry; pub use crate::shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; use crate::{ desktop_context::{EventData, UserWindowEvent, WindowEventHandlers}, webview::WebviewHandler, }; use dioxus_core::*; use dioxus_html::{event_bubbles, MountedData}; use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; use dioxus_interpreter_js::binary_protocol::Channel; use futures_util::{pin_mut, FutureExt}; use global_hotkey::{ hotkey::{Code, HotKey, Modifiers}, GlobalHotKeyManager, }; use rustc_hash::FxHashMap; use std::rc::Rc; use std::sync::atomic::AtomicU16; use std::task::Waker; use std::{borrow::Borrow, cell::Cell}; use std::{collections::HashMap, sync::Arc}; pub use tao::dpi::{LogicalSize, PhysicalSize}; use tao::event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}; pub use tao::window::WindowBuilder; use tao::window::WindowId; use tao::{ event::{Event, StartCause, WindowEvent}, event_loop::ControlFlow, }; use tao::{event_loop::EventLoopBuilder, window::Window}; use tokio::runtime::Builder; pub use wry; use wry::WebContext; use wry::WebView; pub struct App

{ // move the props into a cell so we can pop it out later to create the first window // iOS panics if we create a window before the event loop is started pub(crate) props: Rc>>, pub(crate) cfg: Rc>>, pub(crate) root: Component

, pub(crate) webviews: HashMap, pub(crate) event_handlers: WindowEventHandlers, pub(crate) queue: WebviewQueue, pub(crate) shortcut_manager: ShortcutRegistry, pub(crate) global_hotkey_channel: crossbeam_channel::Receiver, pub(crate) proxy: EventLoopProxy, pub(crate) window_behavior: WindowCloseBehaviour, pub(crate) control_flow: ControlFlow, pub(crate) is_visible_before_start: bool, } impl App

{ pub fn new(cfg: Config, props: P, root: Component

) -> (EventLoop, Self) { let event_loop = EventLoopBuilder::::with_user_event().build(); let mut app = Self { root, window_behavior: cfg.last_window_close_behaviour, is_visible_before_start: true, webviews: HashMap::new(), event_handlers: WindowEventHandlers::default(), queue: WebviewQueue::default(), shortcut_manager: ShortcutRegistry::new(), global_hotkey_channel: GlobalHotKeyEvent::receiver().clone(), proxy: event_loop.create_proxy(), props: Rc::new(Cell::new(Some(props))), cfg: Rc::new(Cell::new(Some(cfg))), control_flow: ControlFlow::Wait, }; #[cfg(all(feature = "hot-reload", debug_assertions))] app.connect_hotreload(); (event_loop, app) } pub fn tick( &mut self, window_event: &Event<'_, UserWindowEvent>, event_loop: &EventLoopWindowTarget, ) { self.control_flow = ControlFlow::Wait; self.event_handlers.apply_event(window_event, event_loop); _ = self .global_hotkey_channel .try_recv() .map(|event| self.shortcut_manager.call_handlers(event)); } #[cfg(all(feature = "hot-reload", debug_assertions))] pub fn connect_hotreload(&mut self) { let proxy = self.proxy.clone(); dioxus_hot_reload::connect({ move |template| { let _ = proxy.send_event(UserWindowEvent( EventData::HotReloadEvent(template), unsafe { WindowId::dummy() }, )); } }); } // pub fn handle_new_window(&mut self) { for handler in self.queue.borrow_mut().drain(..) { let id = handler.desktop_context.window.id(); self.webviews.insert(id, handler); _ = self.proxy.send_event(UserWindowEvent(EventData::Poll, id)); } } pub fn handle_close_requested(&mut self, id: WindowId) { use WindowCloseBehaviour::*; match self.window_behavior { LastWindowExitsApp => { self.webviews.remove(&id); if self.webviews.is_empty() { self.control_flow = ControlFlow::Exit } } LastWindowHides => { let Some(webview) = self.webviews.get(&id) else { return; }; hide_app_window(&webview.desktop_context.webview); } CloseWindow => { self.webviews.remove(&id); } } } pub fn window_destroyed(&mut self, id: WindowId) { self.webviews.remove(&id); if matches!( self.window_behavior, WindowCloseBehaviour::LastWindowExitsApp ) && self.webviews.is_empty() { self.control_flow = ControlFlow::Exit } } pub fn handle_start_cause_init(&mut self, target: &EventLoopWindowTarget) { let props = self.props.take().unwrap(); let cfg = self.cfg.take().unwrap(); let dom = VirtualDom::new_with_props(self.root, props); self.is_visible_before_start = cfg.window.window.visible; let handler = crate::webview::create_new_window( cfg, target, &self.proxy, dom, &self.queue, &self.event_handlers, self.shortcut_manager.clone(), ); let id = handler.desktop_context.window.id(); self.webviews.insert(id, handler); _ = self.proxy.send_event(UserWindowEvent(EventData::Poll, id)); } pub fn handle_browser_open(&mut self, msg: IpcMessage) { if let Some(temp) = msg.params().as_object() { if temp.contains_key("href") { let open = webbrowser::open(temp["href"].as_str().unwrap()); if let Err(e) = open { tracing::error!("Open Browser error: {:?}", e); } } } } pub fn handle_initialize_msg(&mut self, id: WindowId) { let view = self.webviews.get_mut(&id).unwrap(); send_edits(view.dom.rebuild(), &view.desktop_context); view.desktop_context .window .set_visible(self.is_visible_before_start); } pub fn handle_close_msg(&mut self, id: WindowId) { self.webviews.remove(&id); if self.webviews.is_empty() { self.control_flow = ControlFlow::Exit } } pub fn handle_poll_msg(&mut self, id: WindowId) { self.poll_vdom(id); } pub fn handle_query_msg(&mut self, msg: IpcMessage, id: WindowId) { let Ok(result) = serde_json::from_value::(msg.params()) else { return; }; let Some(view) = self.webviews.get(&id) else { return; }; view.dom .base_scope() .consume_context::() .unwrap() .query .send(result); } pub fn handle_user_event_msg(&mut self, msg: IpcMessage, id: WindowId) { let params = msg.params(); let evt = match serde_json::from_value::(params) { Ok(value) => value, Err(err) => { tracing::error!("Error parsing user_event: {:?}", err); return; } }; let HtmlEvent { element, name, bubbles, data, } = evt; let view = self.webviews.get_mut(&id).unwrap(); // check for a mounted event placeholder and replace it with a desktop specific element let as_any = if let dioxus_html::EventData::Mounted = &data { let query = view .dom .base_scope() .consume_context::() .unwrap() .query .clone(); let element = DesktopElement::new(element, view.desktop_context.clone(), query); Rc::new(MountedData::new(element)) } else { data.into_any() }; view.dom.handle_event(&name, as_any, element, bubbles); send_edits(view.dom.render_immediate(), &view.desktop_context); } #[cfg(all(feature = "hot-reload", debug_assertions))] pub fn handle_hot_reload_msg(&mut self, msg: dioxus_hot_reload::HotReloadMsg) { match msg { dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { for webview in self.webviews.values_mut() { webview.dom.replace_template(template); } let ids = self.webviews.keys().copied().collect::>(); for id in ids { self.poll_vdom(id); } } dioxus_hot_reload::HotReloadMsg::Shutdown => { self.control_flow = ControlFlow::Exit; } } } pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) { if let Ok(file_diolog) = serde_json::from_value::(msg.params()) { let id = ElementId(file_diolog.target); let event_name = &file_diolog.event; let event_bubbles = file_diolog.bubbles; let files = file_upload::get_file_event(&file_diolog); let data = Rc::new(FormData { value: Default::default(), values: Default::default(), files: Some(Arc::new(NativeFileEngine::new(files))), }); let view = self.webviews.get_mut(&window).unwrap(); if event_name == "change&input" { view.dom .handle_event("input", data.clone(), id, event_bubbles); view.dom.handle_event("change", data, id, event_bubbles); } else { view.dom.handle_event(event_name, data, id, event_bubbles); } send_edits(view.dom.render_immediate(), &view.desktop_context); } } /// Poll the virtualdom until it's pending /// /// 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 /// /// All IO is done on the tokio runtime we started earlier pub fn poll_vdom(&mut self, id: WindowId) { let view = self.webviews.get_mut(&id).unwrap(); let mut cx = std::task::Context::from_waker(&view.waker); loop { let fut = view.dom.wait_for_work(); pin_mut!(fut); match fut.poll_unpin(&mut cx) { std::task::Poll::Ready(_) => {} std::task::Poll::Pending => break, } send_edits(view.dom.render_immediate(), &view.desktop_context); } } } /// Different hide implementations per platform #[allow(unused)] pub fn hide_app_window(webview: &WebView) { #[cfg(target_os = "windows")] { use wry::application::platform::windows::WindowExtWindows; window.set_visible(false); window.set_skip_taskbar(true); } #[cfg(target_os = "linux")] { use wry::application::platform::unix::WindowExtUnix; window.set_visible(false); } #[cfg(target_os = "macos")] { // window.set_visible(false); has the wrong behaviour on macOS // It will hide the window but not show it again when the user switches // back to the app. `NSApplication::hide:` has the correct behaviour use objc::runtime::Object; use objc::{msg_send, sel, sel_impl}; objc::rc::autoreleasepool(|| unsafe { let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication]; let nil = std::ptr::null_mut::(); let _: () = msg_send![app, hide: nil]; }); } }