//! Dioxus Desktop Renderer //! //! Render the Dioxus VirtualDom using the platform's native WebView implementation. //! //! # Desktop //! //! One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory. //! //! Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc. //! //! //! ## Getting Set up //! //! Getting Set up with Dioxus-Desktop is quite easy. Make sure you have Rust and Cargo installed, and then create a new project: //! //! ```shell //! $ cargo new --bin demo //! $ cd app //! ``` //! //! Add Dioxus with the `desktop` feature: //! //! ```shell //! $ cargo add dioxus --features desktop //! ``` //! //! Edit your `main.rs`: //! //! ```rust //! // main.rs //! use dioxus::prelude::*; //! //! fn main() { //! dioxus::desktop::launch(app); //! } //! //! fn app(cx: Scope) -> Element { //! cx.render(rsx!{ //! div { //! "hello world!" //! } //! }) //! } //! ``` //! //! //! To configure the webview, menubar, and other important desktop-specific features, checkout out some of the launch configuration in the [API reference](https://docs.rs/dioxus-desktop/). //! //! ## Future Steps //! //! Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't! pub mod cfg; pub mod escape; pub mod events; use cfg::DesktopConfig; use dioxus_core::*; use std::{ collections::{HashMap, VecDeque}, sync::atomic::AtomicBool, sync::{Arc, RwLock}, }; use tao::{ event::{Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::{Window, WindowId}, }; pub use wry; pub use wry::application as tao; use wry::{ application::event_loop::EventLoopProxy, webview::RpcRequest, webview::{WebView, WebViewBuilder}, }; /// Launch the WebView and run the event loop. /// /// This function will start a multithreaded Tokio runtime as well the WebView event loop. /// /// ```rust /// 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, (), |c| c) } /// 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 /// 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: impl FnOnce(&mut DesktopConfig) -> &mut DesktopConfig, ) { 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. /// /// You can configure the WebView window with a configuration closure /// /// ```rust /// use dioxus::prelude::*; /// /// fn main() { /// dioxus::desktop::launch_cfg(app, AppProps { name: "asd" }, |c| c); /// } /// /// 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, builder: impl FnOnce(&mut DesktopConfig) -> &mut DesktopConfig, ) { let mut cfg = DesktopConfig::default(); builder(&mut cfg); let event_loop = EventLoop::with_user_event(); let mut desktop = DesktopController::new_on_tokio(root, props, event_loop.create_proxy()); let proxy = event_loop.create_proxy(); event_loop.run(move |window_event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; match window_event { Event::NewEvents(StartCause::Init) => { let builder = cfg.window.clone(); let window = builder.build(event_loop).unwrap(); let window_id = window.id(); let (is_ready, sender) = (desktop.is_ready.clone(), desktop.sender.clone()); let proxy = proxy.clone(); let file_handler = cfg.file_drop_handler.take(); let mut webview = WebViewBuilder::new(window) .unwrap() .with_url("dioxus://index.html/") .unwrap() .with_rpc_handler(move |_window: &Window, req: RpcRequest| { match req.method.as_str() { "user_event" => { let event = events::trigger_from_serialized(req.params.unwrap()); log::trace!("User event: {:?}", event); sender.unbounded_send(SchedulerMsg::Event(event)).unwrap(); } "initialize" => { is_ready.store(true, std::sync::atomic::Ordering::Relaxed); let _ = proxy.send_event(UserWindowEvent::Update); } "browser_open" => { let data = req.params.unwrap(); log::trace!("Open browser: {:?}", data); if let Some(arr) = data.as_array() { if let Some(temp) = arr[0].as_object() { if temp.contains_key("href") { let url = temp.get("href").unwrap().as_str().unwrap(); if let Err(e) = webbrowser::open(url) { log::error!("Open Browser error: {:?}", e); } } } } } _ => {} } None }) .with_custom_protocol(String::from("dioxus"), move |request| { // Any content that that uses the `dioxus://` scheme will be shuttled through this handler as a "special case" // For now, we only serve two pieces of content which get included as bytes into the final binary. let path = request.uri().replace("dioxus://", ""); // all assets shouldbe called from index.html let trimmed = path.trim_start_matches("index.html/"); if trimmed.is_empty() { wry::http::ResponseBuilder::new() .mimetype("text/html") .body(include_bytes!("./index.html").to_vec()) } else if trimmed == "index.js" { wry::http::ResponseBuilder::new() .mimetype("text/javascript") .body(include_bytes!("./interpreter.js").to_vec()) } else { // Read the file content from file path use std::fs::read; let path_buf = std::path::Path::new(trimmed).canonicalize()?; let cur_path = std::path::Path::new(".").canonicalize()?; if !path_buf.starts_with(cur_path) { return wry::http::ResponseBuilder::new() .status(wry::http::status::StatusCode::FORBIDDEN) .body(String::from("Forbidden").into_bytes()); } if !path_buf.exists() { return wry::http::ResponseBuilder::new() .status(wry::http::status::StatusCode::NOT_FOUND) .body(String::from("Not Found").into_bytes()); } let mime = mime_guess::from_path(&path_buf).first_or_octet_stream(); // do not let path searching to go two layers beyond the caller level let data = read(path_buf)?; let meta = format!("{mime}"); wry::http::ResponseBuilder::new().mimetype(&meta).body(data) } }) .with_file_drop_handler(move |window, evet| { file_handler .as_ref() .map(|handler| handler(window, evet)) .unwrap_or_default() }); for (name, handler) in cfg.protocos.drain(..) { webview = webview.with_custom_protocol(name, handler) } desktop.webviews.insert(window_id, webview.build().unwrap()); } Event::WindowEvent { event, window_id, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow), WindowEvent::Resized(_) | WindowEvent::Moved(_) => { if let Some(view) = desktop.webviews.get_mut(&window_id) { let _ = view.resize(); } } _ => {} }, Event::UserEvent(_evt) => { // match _evt { UserWindowEvent::Update => desktop.try_load_ready_webviews(), } } Event::MainEventsCleared => {} Event::Resumed => {} Event::Suspended => {} Event::LoopDestroyed => {} Event::RedrawRequested(_id) => {} _ => {} } }) } pub enum UserWindowEvent { Update, } pub struct DesktopController { pub proxy: EventLoopProxy, pub webviews: HashMap, pub sender: futures_channel::mpsc::UnboundedSender, pub pending_edits: Arc>>, pub quit_app_on_close: bool, pub is_ready: Arc, } impl DesktopController { // Launch the virtualdom on its own thread managed by tokio // returns the desktop state pub fn new_on_tokio( root: Component

, props: P, evt: EventLoopProxy, ) -> Self { let edit_queue = Arc::new(RwLock::new(VecDeque::new())); let pending_edits = edit_queue.clone(); let (sender, receiver) = futures_channel::mpsc::unbounded::(); let return_sender = sender.clone(); let proxy = evt.clone(); std::thread::spawn(move || { // We create the runtime as multithreaded, so you can still "spawn" onto multiple threads let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap(); runtime.block_on(async move { let mut dom = VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver)); let edits = dom.rebuild(); edit_queue .write() .unwrap() .push_front(serde_json::to_string(&edits.edits).unwrap()); loop { dom.wait_for_work().await; let mut muts = dom.work_with_deadline(|| false); while let Some(edit) = muts.pop() { edit_queue .write() .unwrap() .push_front(serde_json::to_string(&edit.edits).unwrap()); } let _ = evt.send_event(UserWindowEvent::Update); } }) }); Self { pending_edits, sender: return_sender, proxy, webviews: HashMap::new(), is_ready: Arc::new(AtomicBool::new(false)), quit_app_on_close: true, } } pub fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) { self.webviews.remove(&window_id); if self.webviews.is_empty() && self.quit_app_on_close { *control_flow = ControlFlow::Exit; } } pub fn try_load_ready_webviews(&mut self) { if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { let mut queue = self.pending_edits.write().unwrap(); let (_id, view) = self.webviews.iter_mut().next().unwrap(); while let Some(edit) = queue.pop_back() { view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) .unwrap(); } } else { println!("waiting for ready"); } } }