123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- use crate::{
- config::{Config, WindowCloseBehaviour},
- desktop_context::WindowEventHandlers,
- element::DesktopElement,
- file_upload::FileDialogRequest,
- ipc::IpcMessage,
- ipc::{EventData, UserWindowEvent},
- query::QueryResult,
- shortcut::{GlobalHotKeyEvent, ShortcutRegistry},
- webview::WebviewInstance,
- };
- use crossbeam_channel::Receiver;
- use dioxus_core::{Component, ElementId, VirtualDom};
- use dioxus_html::{
- native_bind::NativeFileEngine, FileEngine, HasFileData, HasFormData, HtmlEvent,
- PlatformEventData,
- };
- use std::{
- cell::{Cell, RefCell},
- collections::HashMap,
- rc::Rc,
- sync::Arc,
- };
- use tao::{
- event::Event,
- event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget},
- window::WindowId,
- };
- /// The single top-level object that manages all the running windows, assets, shortcuts, etc
- pub(crate) struct App<P> {
- // 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, so we toss them into a cell
- pub(crate) props: Cell<Option<P>>,
- pub(crate) cfg: Cell<Option<Config>>,
- // Stuff we need mutable access to
- pub(crate) root: Component<P>,
- pub(crate) control_flow: ControlFlow,
- pub(crate) is_visible_before_start: bool,
- pub(crate) window_behavior: WindowCloseBehaviour,
- pub(crate) webviews: HashMap<WindowId, WebviewInstance>,
- /// This single blob of state is shared between all the windows so they have access to the runtime state
- ///
- /// This includes stuff like the event handlers, shortcuts, etc as well as ways to modify *other* windows
- pub(crate) shared: Rc<SharedContext>,
- }
- /// A bundle of state shared between all the windows, providing a way for us to communicate with running webview.
- ///
- /// Todo: everything in this struct is wrapped in Rc<>, but we really only need the one top-level refcell
- pub struct SharedContext {
- pub(crate) event_handlers: WindowEventHandlers,
- pub(crate) pending_webviews: RefCell<Vec<WebviewInstance>>,
- pub(crate) shortcut_manager: ShortcutRegistry,
- pub(crate) global_hotkey_channel: Receiver<GlobalHotKeyEvent>,
- pub(crate) proxy: EventLoopProxy<UserWindowEvent>,
- pub(crate) target: EventLoopWindowTarget<UserWindowEvent>,
- }
- impl<P: 'static> App<P> {
- pub fn new(cfg: Config, props: P, root: Component<P>) -> (EventLoop<UserWindowEvent>, Self) {
- let event_loop = EventLoopBuilder::<UserWindowEvent>::with_user_event().build();
- let app = Self {
- root,
- window_behavior: cfg.last_window_close_behaviour,
- is_visible_before_start: true,
- webviews: HashMap::new(),
- control_flow: ControlFlow::Wait,
- props: Cell::new(Some(props)),
- cfg: Cell::new(Some(cfg)),
- shared: Rc::new(SharedContext {
- event_handlers: WindowEventHandlers::default(),
- pending_webviews: Default::default(),
- shortcut_manager: ShortcutRegistry::new(),
- global_hotkey_channel: GlobalHotKeyEvent::receiver().clone(),
- proxy: event_loop.create_proxy(),
- target: event_loop.clone(),
- }),
- };
- // Copy over any assets we find
- // todo - re-enable this when we have a faster way of copying assets
- #[cfg(feature = "collect-assets")]
- crate::collect_assets::copy_assets();
- // Set the event converter
- dioxus_html::set_event_converter(Box::new(crate::events::SerializedHtmlEventConverter));
- // Allow hotreloading to work - but only in debug mode
- #[cfg(all(feature = "hot-reload", debug_assertions))]
- app.connect_hotreload();
- (event_loop, app)
- }
- pub fn tick(&mut self, window_event: &Event<'_, UserWindowEvent>) {
- self.control_flow = ControlFlow::Wait;
- self.shared
- .event_handlers
- .apply_event(window_event, &self.shared.target);
- if let Ok(event) = self.shared.global_hotkey_channel.try_recv() {
- self.shared.shortcut_manager.call_handlers(event);
- }
- }
- #[cfg(all(feature = "hot-reload", debug_assertions))]
- pub fn connect_hotreload(&self) {
- dioxus_hot_reload::connect({
- let proxy = self.shared.proxy.clone();
- move |template| {
- let _ = proxy.send_event(UserWindowEvent(
- EventData::HotReloadEvent(template),
- unsafe { WindowId::dummy() },
- ));
- }
- });
- }
- pub fn handle_new_window(&mut self) {
- for handler in self.shared.pending_webviews.borrow_mut().drain(..) {
- let id = handler.desktop_context.window.id();
- self.webviews.insert(id, handler);
- _ = self
- .shared
- .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) {
- let props = self.props.take().unwrap();
- let cfg = self.cfg.take().unwrap();
- self.is_visible_before_start = cfg.window.window.visible;
- let webview = WebviewInstance::new(
- cfg,
- VirtualDom::new_with_props(self.root, props),
- self.shared.clone(),
- );
- let id = webview.desktop_context.window.id();
- self.webviews.insert(id, webview);
- _ = self
- .shared
- .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();
- view.desktop_context.send_edits(view.dom.rebuild());
- 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_query_msg(&mut self, msg: IpcMessage, id: WindowId) {
- let Ok(result) = serde_json::from_value::<QueryResult>(msg.params()) else {
- return;
- };
- let Some(view) = self.webviews.get(&id) else {
- return;
- };
- view.desktop_context.query.send(result);
- }
- pub fn handle_user_event_msg(&mut self, msg: IpcMessage, id: WindowId) {
- let parsed_params = serde_json::from_value(msg.params())
- .map_err(|err| tracing::error!("Error parsing user_event: {:?}", err));
- let Ok(evt) = parsed_params else { return };
- let HtmlEvent {
- element,
- name,
- bubbles,
- data,
- } = evt;
- let view = self.webviews.get_mut(&id).unwrap();
- let query = view.desktop_context.query.clone();
- // check for a mounted event placeholder and replace it with a desktop specific element
- let as_any = match data {
- dioxus_html::EventData::Mounted => {
- let element = DesktopElement::new(element, view.desktop_context.clone(), query);
- Rc::new(PlatformEventData::new(Box::new(element)))
- }
- _ => data.into_any(),
- };
- view.dom.handle_event(&name, as_any, element, bubbles);
- view.desktop_context.send_edits(view.dom.render_immediate());
- }
- #[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);
- webview.poll_vdom();
- }
- }
- dioxus_hot_reload::HotReloadMsg::Shutdown => {
- self.control_flow = ControlFlow::Exit;
- }
- }
- }
- pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) {
- let Ok(file_dialog) = serde_json::from_value::<FileDialogRequest>(msg.params()) else {
- return;
- };
- struct DesktopFileUploadForm {
- files: Arc<NativeFileEngine>,
- }
- impl HasFileData for DesktopFileUploadForm {
- fn files(&self) -> Option<Arc<dyn FileEngine>> {
- Some(self.files.clone())
- }
- }
- impl HasFormData for DesktopFileUploadForm {
- fn as_any(&self) -> &dyn std::any::Any {
- self
- }
- }
- let id = ElementId(file_dialog.target);
- let event_name = &file_dialog.event;
- let event_bubbles = file_dialog.bubbles;
- let files = file_dialog.get_file_event();
- let data = Rc::new(PlatformEventData::new(Box::new(DesktopFileUploadForm {
- files: 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);
- }
- view.desktop_context.send_edits(view.dom.render_immediate());
- }
- /// 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 Some(view) = self.webviews.get_mut(&id) else {
- return;
- };
- view.poll_vdom();
- }
- }
- /// Different hide implementations per platform
- #[allow(unused)]
- pub fn hide_app_window(window: &wry::WebView) {
- #[cfg(target_os = "windows")]
- {
- use tao::platform::windows::WindowExtWindows;
- window.set_visible(false);
- // window.set_skip_taskbar(true);
- }
- #[cfg(target_os = "linux")]
- {
- use tao::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::<Object>();
- let _: () = msg_send![app, hide: nil];
- });
- }
- }
|