#![doc = include_str!("readme.md")] #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] #![deny(missing_docs)] mod cfg; mod desktop_context; mod element; mod escape; mod eval; mod events; mod file_upload; mod protocol; mod query; mod shortcut; mod waker; mod webview; use crate::query::QueryResult; pub use cfg::Config; pub use desktop_context::{ use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId, }; use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; use dioxus_core::*; use dioxus_html::MountedData; use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; use element::DesktopElement; pub use eval::{use_eval, EvalResult}; use futures_util::{pin_mut, FutureExt}; use shortcut::ShortcutRegistry; pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; use std::rc::Rc; use std::task::Waker; use std::{collections::HashMap, sync::Arc}; pub use tao::dpi::{LogicalSize, PhysicalSize}; use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; pub use tao::window::WindowBuilder; use tao::{ event::{Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoop}, }; pub use wry; pub use wry::application as tao; use wry::webview::WebView; use wry::{application::window::WindowId, webview::WebContext}; /// Launch the WebView and run the event loop. /// /// This function will start a multithreaded Tokio runtime as well the WebView event loop. /// /// ```rust, ignore /// use dioxus::prelude::*; /// /// fn main() { /// dioxus_desktop::launch(app); /// } /// /// fn app(cx: Scope) -> Element { /// cx.render(rsx!{ /// h1 {"hello world!"} /// }) /// } /// ``` pub fn launch(root: Component) { launch_with_props(root, (), Config::default()) } /// Launch the WebView and run the event loop, with configuration. /// /// This function will start a multithreaded Tokio runtime as well the WebView event loop. /// /// You can configure the WebView window with a configuration closure /// /// ```rust, ignore /// use dioxus::prelude::*; /// /// fn main() { /// dioxus_desktop::launch_cfg(app, |c| c.with_window(|w| w.with_title("My App"))); /// } /// /// fn app(cx: Scope) -> Element { /// cx.render(rsx!{ /// h1 {"hello world!"} /// }) /// } /// ``` pub fn launch_cfg(root: Component, config_builder: Config) { launch_with_props(root, (), config_builder) } /// Launch the WebView and run the event loop, with configuration and root props. /// /// This function will start a multithreaded Tokio runtime as well the WebView event loop. This will block the current thread. /// /// You can configure the WebView window with a configuration closure /// /// ```rust, ignore /// use dioxus::prelude::*; /// /// fn main() { /// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default()); /// } /// /// struct AppProps { /// name: &'static str /// } /// /// fn app(cx: Scope) -> Element { /// cx.render(rsx!{ /// h1 {"hello {cx.props.name}!"} /// }) /// } /// ``` pub fn launch_with_props(root: Component

, props: P, cfg: Config) { let event_loop = EventLoop::::with_user_event(); let proxy = event_loop.create_proxy(); // Intialize hot reloading if it is enabled #[cfg(all(feature = "hot-reload", debug_assertions))] { let proxy = proxy.clone(); dioxus_hot_reload::connect(move |template| { let _ = proxy.send_event(UserWindowEvent( EventData::HotReloadEvent(template), unsafe { WindowId::dummy() }, )); }); } // We start the tokio runtime *on this thread* // Any future we poll later will use this runtime to spawn tasks and for IO let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap(); // We enter the runtime but we poll futures manually, circumventing the per-task runtime budget let _guard = rt.enter(); // We only have one webview right now, but we'll have more later // Store them in a hashmap so we can remove them when they're closed let mut webviews = HashMap::::new(); // We use this to allow dynamically adding and removing window event handlers let event_handlers = WindowEventHandlers::default(); let queue = WebviewQueue::default(); let shortcut_manager = ShortcutRegistry::new(&event_loop); // By default, we'll create a new window when the app starts queue.borrow_mut().push(create_new_window( cfg, &event_loop, &proxy, VirtualDom::new_with_props(root, props), &queue, &event_handlers, shortcut_manager.clone(), )); event_loop.run(move |window_event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; event_handlers.apply_event(&window_event, event_loop); match window_event { Event::WindowEvent { event, window_id, .. } => match event { WindowEvent::CloseRequested => { webviews.remove(&window_id); if webviews.is_empty() { *control_flow = ControlFlow::Exit } } WindowEvent::Destroyed { .. } => { webviews.remove(&window_id); if webviews.is_empty() { *control_flow = ControlFlow::Exit; } } _ => {} }, Event::NewEvents(StartCause::Init) | Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => { for handler in queue.borrow_mut().drain(..) { let id = handler.webview.window().id(); webviews.insert(id, handler); _ = proxy.send_event(UserWindowEvent(EventData::Poll, id)); } } Event::UserEvent(event) => match event.0 { #[cfg(all(feature = "hot-reload", debug_assertions))] EventData::HotReloadEvent(msg) => match msg { dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { for webview in webviews.values_mut() { webview.dom.replace_template(template); poll_vdom(webview); } } dioxus_hot_reload::HotReloadMsg::Shutdown => { *control_flow = ControlFlow::Exit; } }, EventData::CloseWindow => { webviews.remove(&event.1); if webviews.is_empty() { *control_flow = ControlFlow::Exit } } EventData::Poll => { if let Some(view) = webviews.get_mut(&event.1) { poll_vdom(view); } } EventData::Ipc(msg) if msg.method() == "user_event" => { let params = msg.params(); let evt = match serde_json::from_value::(params) { Ok(value) => value, Err(_) => return, }; let HtmlEvent { element, name, bubbles, data, } = evt; let view = webviews.get_mut(&event.1).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; let element = DesktopElement::new(element, view.webview.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.webview); } // When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query EventData::Ipc(msg) if msg.method() == "query" => { let params = msg.params(); if let Ok(result) = serde_json::from_value::(params) { let view = webviews.get(&event.1).unwrap(); let query = view .dom .base_scope() .consume_context::() .unwrap() .query; query.send(result); } } EventData::Ipc(msg) if msg.method() == "initialize" => { let view = webviews.get_mut(&event.1).unwrap(); send_edits(view.dom.rebuild(), &view.webview); } EventData::Ipc(msg) if msg.method() == "browser_open" => { 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 { log::error!("Open Browser error: {:?}", e); } } } } EventData::Ipc(msg) if msg.method() == "file_diolog" => { 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 = webviews.get_mut(&event.1).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.webview); } } _ => {} }, Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id), _ => {} } }) } fn create_new_window( mut cfg: Config, event_loop: &EventLoopWindowTarget, proxy: &EventLoopProxy, dom: VirtualDom, queue: &WebviewQueue, event_handlers: &WindowEventHandlers, shortcut_manager: ShortcutRegistry, ) -> WebviewHandler { let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone()); dom.base_scope().provide_context(DesktopContext::new( webview.clone(), proxy.clone(), event_loop.clone(), queue.clone(), event_handlers.clone(), shortcut_manager, )); let id = webview.window().id(); // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both WebviewHandler { webview, dom, waker: waker::tao_waker(proxy, id), web_context, } } struct WebviewHandler { dom: VirtualDom, webview: Rc, waker: Waker, // This is nessisary because of a bug in wry. Wry assumes the webcontext is alive for the lifetime of the webview. We need to keep the webcontext alive, otherwise the webview will crash #[allow(dead_code)] web_context: WebContext, } /// 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 fn poll_vdom(view: &mut WebviewHandler) { 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.webview); } } /// Send a list of mutations to the webview fn send_edits(edits: Mutations, webview: &WebView) { let serialized = serde_json::to_string(&edits).unwrap(); // todo: use SSE and binary data to send the edits with lower overhead _ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})")); }