diff --git a/Cargo.toml b/Cargo.toml index 606aef6be..af5e8a733 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ publish = false [dev-dependencies] dioxus = { path = "./packages/dioxus" } -dioxus-desktop = { path = "./packages/desktop" } +dioxus-desktop = { path = "./packages/desktop", features = ["transparent"] } dioxus-ssr = { path = "./packages/ssr" } dioxus-router = { path = "./packages/router" } fermi = { path = "./packages/fermi" } diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs new file mode 100644 index 000000000..febf0ceba --- /dev/null +++ b/examples/multiwindow.rs @@ -0,0 +1,28 @@ +use dioxus::prelude::*; +use dioxus_desktop::{use_window, WindowBuilder}; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let window = use_window(cx); + + cx.render(rsx! { + div { + button { + onclick: move |_| { + let dom = VirtualDom::new(app); + window.new_window(dom, Default::default()); + }, + "New Window" + } + } + }) +} + +fn popup(cx: Scope) -> Element { + cx.render(rsx! { + div { "This is a popup!" } + }) +} diff --git a/examples/overlay.rs b/examples/overlay.rs new file mode 100644 index 000000000..359fa5169 --- /dev/null +++ b/examples/overlay.rs @@ -0,0 +1,63 @@ +use dioxus::prelude::*; +use dioxus_desktop::{tao::dpi::PhysicalPosition, use_window, Config, LogicalSize, WindowBuilder}; + +fn main() { + dioxus_desktop::launch_cfg(app, make_config()); +} + +fn app(cx: Scope) -> Element { + let window = use_window(cx); + + cx.render(rsx! { + div { + width: "100%", + height: "100%", + background_color: "red", + border: "1px solid black", + + div { + width: "100%", + height: "10px", + background_color: "black", + onmousedown: move |_| window.drag(), + } + + "This is an overlay!" + } + }) +} + +fn make_config() -> dioxus_desktop::Config { + dioxus_desktop::Config::default() + .with_window(make_window()) + .with_custom_head( + r#" + +"# + .to_owned(), + ) +} + +fn make_window() -> WindowBuilder { + WindowBuilder::new() + .with_transparent(true) + .with_decorations(false) + .with_resizable(false) + .with_always_on_top(true) + .with_position(PhysicalPosition::new(0, 0)) + .with_max_inner_size(LogicalSize::new(100000, 50)) +} diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index cb5a62635..0fb340c25 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,14 +1,18 @@ +use std::cell::RefCell; use std::rc::Rc; use crate::eval::EvalResult; use crate::events::IpcMessage; +use crate::Config; use dioxus_core::ScopeState; +use dioxus_core::VirtualDom; use serde_json::Value; use wry::application::event_loop::EventLoopProxy; #[cfg(target_os = "ios")] use wry::application::platform::ios::WindowExtIOS; use wry::application::window::Fullscreen as WryFullscreen; use wry::application::window::Window; +use wry::application::window::WindowId; use wry::webview::WebView; pub type ProxyType = EventLoopProxy; @@ -20,6 +24,8 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext { .unwrap() } +pub type WebviewQueue = Rc>>; + /// An imperative interface to the current window. /// /// To get a handle to the current window, use the [`use_window`] hook. @@ -43,6 +49,8 @@ pub struct DesktopContext { /// The receiver for eval results since eval is async pub(super) eval: tokio::sync::broadcast::Sender, + pub(super) pending_windows: WebviewQueue, + #[cfg(target_os = "ios")] pub(crate) views: Rc>>, } @@ -57,16 +65,25 @@ impl std::ops::Deref for DesktopContext { } impl DesktopContext { - pub(crate) fn new(webview: Rc, proxy: ProxyType) -> Self { + pub(crate) fn new(webview: Rc, proxy: ProxyType, webviews: WebviewQueue) -> Self { Self { webview, proxy, eval: tokio::sync::broadcast::channel(8).0, + pending_windows: webviews, #[cfg(target_os = "ios")] views: Default::default(), } } + /// Create a new window using the props and window builder + pub fn new_window(&self, dom: VirtualDom, cfg: Config) { + self.pending_windows.borrow_mut().push((dom, cfg)); + self.proxy + .send_event(UserWindowEvent(EventData::NewWindow, self.id())) + .unwrap(); + } + /// trigger the drag-window event /// /// Moves the window with the left mouse button until the button is released. @@ -79,7 +96,9 @@ impl DesktopContext { let window = self.webview.window(); // if the drag_window has any errors, we don't do anything - window.fullscreen().is_none().then(|| window.drag_window()); + if window.fullscreen().is_none() { + window.drag_window().unwrap(); + } } /// Toggle whether the window is maximized or not @@ -91,7 +110,9 @@ impl DesktopContext { /// close window pub fn close(&self) { - let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); + let _ = self + .proxy + .send_event(UserWindowEvent(EventData::CloseWindow, self.id())); } /// change window to fullscreen @@ -189,11 +210,16 @@ impl DesktopContext { } #[derive(Debug)] -pub enum UserWindowEvent { +pub struct UserWindowEvent(pub EventData, pub WindowId); + +#[derive(Debug)] +pub enum EventData { Poll, Ipc(IpcMessage), + NewWindow, + CloseWindow, } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index e3002f539..52907c1b1 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -16,12 +16,13 @@ mod webview; mod hot_reload; pub use cfg::Config; -use desktop_context::UserWindowEvent; pub use desktop_context::{use_window, DesktopContext}; +use desktop_context::{EventData, UserWindowEvent}; use dioxus_core::*; use dioxus_html::HtmlEvent; pub use eval::{use_eval, EvalResult}; use futures_util::{pin_mut, FutureExt}; +use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use std::task::Waker; @@ -33,6 +34,8 @@ use tao::{ }; pub use wry; pub use wry::application as tao; +use wry::application::window::WindowId; +use wry::webview::WebView; /// Launch the WebView and run the event loop. /// @@ -101,10 +104,10 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// }) /// } /// ``` -pub fn launch_with_props(root: Component

, props: P, mut cfg: Config) { - let mut dom = VirtualDom::new_with_props(root, props); +pub fn launch_with_props(root: Component

, props: P, cfg: Config) { + let mut _dom = VirtualDom::new_with_props(root, props); - let event_loop = EventLoop::with_user_event(); + let event_loop = EventLoop::::with_user_event(); let proxy = event_loop.create_proxy(); @@ -118,23 +121,26 @@ pub fn launch_with_props(root: Component

, props: P, mut cfg: Conf // We enter the runtime but we poll futures manually, circumventing the per-task runtime budget let _guard = rt.enter(); - // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both - let waker = waker::tao_waker(&proxy); - // 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(); + let mut webviews = HashMap::::new(); + + let queue = Rc::new(RefCell::new(vec![(_dom, cfg)])); event_loop.run(move |window_event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; match window_event { - Event::UserEvent(UserWindowEvent::CloseWindow) => *control_flow = ControlFlow::Exit, - Event::WindowEvent { event, window_id, .. } => match event { - WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::CloseRequested => { + webviews.remove(&window_id); + + if webviews.is_empty() { + *control_flow = ControlFlow::Exit + } + } WindowEvent::Destroyed { .. } => { webviews.remove(&window_id); @@ -145,78 +151,118 @@ pub fn launch_with_props(root: Component

, props: P, mut cfg: Conf _ => {} }, - Event::NewEvents(StartCause::Init) => { - let window = webview::build(&mut cfg, event_loop, proxy.clone()); + Event::NewEvents(StartCause::Init) + | Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => { + for (dom, mut cfg) in queue.borrow_mut().drain(..) { + let webview = webview::build(&mut cfg, event_loop, proxy.clone()); - dom.base_scope() - .provide_context(DesktopContext::new(window.clone(), proxy.clone())); + dom.base_scope().provide_context(DesktopContext::new( + webview.clone(), + proxy.clone(), + queue.clone(), + )); - webviews.insert(window.window().id(), window); + let id = webview.window().id(); - _ = proxy.send_event(UserWindowEvent::Poll); - } + // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both + let waker = waker::tao_waker(&proxy, id); - Event::UserEvent(UserWindowEvent::Poll) => { - poll_vdom(&waker, &mut dom, &mut webviews); - } + let handler = WebviewHandler { + webview, + waker, + dom, + }; - Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "user_event" => { - let evt = match serde_json::from_value::(msg.params()) { - Ok(value) => value, - Err(_) => return, - }; + webviews.insert(id, handler); - dom.handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles); - - send_edits(dom.render_immediate(), &mut webviews); - } - - Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "initialize" => { - send_edits(dom.rebuild(), &mut webviews); - } - - // When the webview chirps back with the result of the eval, we send it to the active receiver - // - // This currently doesn't perform any targeting to the callsite, so if you eval multiple times at once, - // you might the wrong result. This should be fixed - Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "eval_result" => { - dom.base_scope() - .consume_context::() - .unwrap() - .eval - .send(msg.params()) - .unwrap(); - } - - Event::UserEvent(UserWindowEvent::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); - } - } + _ = proxy.send_event(UserWindowEvent(EventData::Poll, id)); } } + Event::UserEvent(event) => match event.0 { + 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 evt = match serde_json::from_value::(msg.params()) { + Ok(value) => value, + Err(_) => return, + }; + + let view = webviews.get_mut(&event.1).unwrap(); + + view.dom + .handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles); + + send_edits(view.dom.render_immediate(), &view.webview); + } + + EventData::Ipc(msg) if msg.method() == "initialize" => { + let view = webviews.get_mut(&event.1).unwrap(); + send_edits(view.dom.rebuild(), &view.webview); + } + + // When the webview chirps back with the result of the eval, we send it to the active receiver + // + // This currently doesn't perform any targeting to the callsite, so if you eval multiple times at once, + // you might the wrong result. This should be fixed + EventData::Ipc(msg) if msg.method() == "eval_result" => { + webviews[&event.1] + .dom + .base_scope() + .consume_context::() + .unwrap() + .eval + .send(msg.params()) + .unwrap(); + } + + 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); + } + } + } + } + + _ => {} + }, _ => {} } }) } -type Webviews = HashMap>; +struct WebviewHandler { + dom: VirtualDom, + webview: Rc, + waker: Waker, +} /// 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(waker: &Waker, dom: &mut VirtualDom, webviews: &mut Webviews) { - let mut cx = std::task::Context::from_waker(waker); +fn poll_vdom(view: &mut WebviewHandler) { + let mut cx = std::task::Context::from_waker(&view.waker); loop { { - let fut = dom.wait_for_work(); + let fut = view.dom.wait_for_work(); pin_mut!(fut); match fut.poll_unpin(&mut cx) { @@ -225,16 +271,14 @@ fn poll_vdom(waker: &Waker, dom: &mut VirtualDom, webviews: &mut Webviews) { } } - send_edits(dom.render_immediate(), webviews); + send_edits(view.dom.render_immediate(), &view.webview); } } /// Send a list of mutations to the webview -fn send_edits(edits: Mutations, webviews: &mut Webviews) { +fn send_edits(edits: Mutations, webview: &WebView) { let serialized = serde_json::to_string(&edits).unwrap(); - let (_id, view) = webviews.iter_mut().next().unwrap(); - // todo: use SSE and binary data to send the edits with lower overhead - _ = view.evaluate_script(&format!("window.interpreter.handleEdits({})", serialized)); + _ = webview.evaluate_script(&format!("window.interpreter.handleEdits({})", serialized)); } diff --git a/packages/desktop/src/waker.rs b/packages/desktop/src/waker.rs index a2586ffed..1e34baffc 100644 --- a/packages/desktop/src/waker.rs +++ b/packages/desktop/src/waker.rs @@ -1,15 +1,18 @@ -use crate::desktop_context::UserWindowEvent; +use crate::desktop_context::{EventData, UserWindowEvent}; use futures_util::task::ArcWake; use std::sync::Arc; -use wry::application::event_loop::EventLoopProxy; +use wry::application::{event_loop::EventLoopProxy, window::WindowId}; /// Create a waker that will send a poll event to the event loop. /// /// This lets the VirtualDom "come up for air" and process events while the main thread is blocked by the WebView. /// /// All other IO lives in the Tokio runtime, -pub fn tao_waker(proxy: &EventLoopProxy) -> std::task::Waker { - struct DomHandle(EventLoopProxy); +pub fn tao_waker(proxy: &EventLoopProxy, id: WindowId) -> std::task::Waker { + struct DomHandle { + proxy: EventLoopProxy, + id: WindowId, + } // this should be implemented by most platforms, but ios is missing this until // https://github.com/tauri-apps/wry/issues/830 is resolved @@ -18,9 +21,14 @@ pub fn tao_waker(proxy: &EventLoopProxy) -> std::task::Waker { impl ArcWake for DomHandle { fn wake_by_ref(arc_self: &Arc) { - _ = arc_self.0.send_event(UserWindowEvent::Poll); + _ = arc_self + .proxy + .send_event(UserWindowEvent(EventData::Poll, arc_self.id)); } } - futures_util::task::waker(Arc::new(DomHandle(proxy.clone()))) + futures_util::task::waker(Arc::new(DomHandle { + id, + proxy: proxy.clone(), + })) } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 4a270bfab..721f37d94 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,5 +1,6 @@ use std::rc::Rc; +use crate::desktop_context::EventData; use crate::protocol; use crate::{desktop_context::UserWindowEvent, Config}; use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; @@ -38,10 +39,10 @@ pub fn build( .with_transparent(cfg.window.window.transparent) .with_url("dioxus://index.html/") .unwrap() - .with_ipc_handler(move |_window: &Window, payload: String| { + .with_ipc_handler(move |window: &Window, payload: String| { // defer the event to the main thread if let Ok(message) = serde_json::from_str(&payload) { - _ = proxy.send_event(UserWindowEvent::Ipc(message)); + _ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id())); } }) .with_custom_protocol(String::from("dioxus"), move |r| {