123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404 |
- #![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<AppProps>) -> Element {
- /// cx.render(rsx!{
- /// h1 {"hello {cx.props.name}!"}
- /// })
- /// }
- /// ```
- pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config) {
- let event_loop = EventLoop::<UserWindowEvent>::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::<WindowId, WebviewHandler>::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::<HtmlEvent>(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::<DesktopContext>()
- .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::<QueryResult>(params) {
- let view = webviews.get(&event.1).unwrap();
- let query = view
- .dom
- .base_scope()
- .consume_context::<DesktopContext>()
- .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::<file_upload::FileDiologRequest>(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<UserWindowEvent>,
- proxy: &EventLoopProxy<UserWindowEvent>,
- 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<wry::webview::WebView>,
- 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})"));
- }
|