diff --git a/examples/clock.rs b/examples/clock.rs new file mode 100644 index 000000000..a159ef73a --- /dev/null +++ b/examples/clock.rs @@ -0,0 +1,32 @@ +//! Example: README.md showcase +//! +//! The example from the README.md. + +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let count = use_ref(cx, || 0); + + let ct = count.to_owned(); + use_coroutine(cx, |_: UnboundedReceiver<()>| async move { + loop { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + + *ct.write() += 1; + + let current = *ct.read(); + + println!("current: {}", current); + } + }); + + let count = count.read(); + + cx.render(rsx! { + div { "High-Five counter: {count}" } + }) +} diff --git a/examples/eval.rs b/examples/eval.rs index 22e036b9c..3a16b774e 100644 --- a/examples/eval.rs +++ b/examples/eval.rs @@ -1,25 +1,17 @@ use dioxus::prelude::*; -use dioxus_desktop::EvalResult; fn main() { dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { - let script = use_state(cx, String::new); let eval = dioxus_desktop::use_eval(cx); - let future: &UseRef> = use_ref(cx, || None); - if future.read().is_some() { - let future_clone = future.clone(); - cx.spawn(async move { - if let Some(fut) = future_clone.with_mut(|o| o.take()) { - println!("{:?}", fut.await) - } - }); - } + let script = use_state(cx, String::new); + let output = use_state(cx, String::new); cx.render(rsx! { div { + p { "Output: {output}" } input { placeholder: "Enter an expression", value: "{script}", @@ -27,8 +19,12 @@ fn app(cx: Scope) -> Element { } button { onclick: move |_| { - let fut = eval(script); - future.set(Some(fut)); + to_owned![script, eval, output]; + cx.spawn(async move { + if let Ok(res) = eval(script.to_string()).await { + output.set(res.to_string()); + } + }); }, "Execute" } diff --git a/examples/window_zoom.rs b/examples/window_zoom.rs index b7c190896..e45d71be2 100644 --- a/examples/window_zoom.rs +++ b/examples/window_zoom.rs @@ -7,16 +7,17 @@ fn main() { fn app(cx: Scope) -> Element { let window = use_window(cx); - let level = use_state(cx, || 1.0); + cx.render(rsx! { input { r#type: "number", value: "{level}", oninput: |e| { - let num = e.value.parse::().unwrap_or(1.0); - level.set(num); - window.set_zoom_level(num); + if let Ok(new_zoom) = e.value.parse::() { + level.set(new_zoom); + window.webview.zoom(new_zoom); + } } } }) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index bca8bd031..47652e262 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -23,7 +23,7 @@ rustc-hash = "1.1.0" # Used in diffing longest-increasing-subsequence = "0.1.0" -futures-util = { version = "0.3", default-features = false } +futures-util = { version = "0.3", default-features = false, features = ["alloc"]} slab = "0.4" diff --git a/packages/core/src/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs index 7aed417b1..8fb476fbe 100644 --- a/packages/core/src/scheduler/mod.rs +++ b/packages/core/src/scheduler/mod.rs @@ -4,11 +4,9 @@ use slab::Slab; mod suspense; mod task; mod wait; -mod waker; pub use suspense::*; pub use task::*; -pub use waker::ArcWake; /// The type of message that can be sent to the scheduler. /// @@ -25,16 +23,16 @@ pub(crate) enum SchedulerMsg { SuspenseNotified(SuspenseId), } -use std::{cell::RefCell, rc::Rc, sync::Arc}; +use std::{cell::RefCell, rc::Rc}; pub(crate) struct Scheduler { pub sender: futures_channel::mpsc::UnboundedSender, /// Tasks created with cx.spawn - pub tasks: RefCell>>, + pub tasks: RefCell>, /// Async components - pub leaves: RefCell>>, + pub leaves: RefCell>, } impl Scheduler { diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index c7cd650bb..39e404198 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -1,8 +1,11 @@ -use super::{waker::ArcWake, SchedulerMsg}; +use futures_util::task::ArcWake; + +use super::SchedulerMsg; use crate::ElementId; use crate::{innerlude::Mutations, Element, ScopeId}; use std::future::Future; use std::sync::Arc; +use std::task::Waker; use std::{ cell::{Cell, RefCell}, collections::HashSet, @@ -35,16 +38,19 @@ impl SuspenseContext { } pub(crate) struct SuspenseLeaf { - pub(crate) id: SuspenseId, pub(crate) scope_id: ScopeId, - pub(crate) tx: futures_channel::mpsc::UnboundedSender, pub(crate) notified: Cell, pub(crate) task: *mut dyn Future>, + pub(crate) waker: Waker, } -impl ArcWake for SuspenseLeaf { +pub struct SuspenseHandle { + pub(crate) id: SuspenseId, + pub(crate) tx: futures_channel::mpsc::UnboundedSender, +} + +impl ArcWake for SuspenseHandle { fn wake_by_ref(arc_self: &Arc) { - arc_self.notified.set(true); _ = arc_self .tx .unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id)); diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs index 8c1d7ca76..39c770a06 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/scheduler/task.rs @@ -1,9 +1,12 @@ -use super::{waker::ArcWake, Scheduler, SchedulerMsg}; +use futures_util::task::ArcWake; + +use super::{Scheduler, SchedulerMsg}; use crate::ScopeId; use std::cell::RefCell; use std::future::Future; use std::pin::Pin; use std::sync::Arc; +use std::task::Waker; /// A task's unique identifier. /// @@ -17,8 +20,7 @@ pub struct TaskId(pub usize); pub(crate) struct LocalTask { pub scope: ScopeId, pub(super) task: RefCell + 'static>>>, - id: TaskId, - tx: futures_channel::mpsc::UnboundedSender, + pub waker: Waker, } impl Scheduler { @@ -33,15 +35,20 @@ impl Scheduler { /// will only occur when the VirtuaalDom itself has been dropped. pub fn spawn(&self, scope: ScopeId, task: impl Future + 'static) -> TaskId { let mut tasks = self.tasks.borrow_mut(); + let entry = tasks.vacant_entry(); let task_id = TaskId(entry.key()); - entry.insert(Arc::new(LocalTask { - id: task_id, - tx: self.sender.clone(), + let task = LocalTask { task: RefCell::new(Box::pin(task)), scope, - })); + waker: futures_util::task::waker(Arc::new(LocalTaskHandle { + id: task_id, + tx: self.sender.clone(), + })), + }; + + entry.insert(task); self.sender .unbounded_send(SchedulerMsg::TaskNotified(task_id)) @@ -58,10 +65,16 @@ impl Scheduler { } } -impl ArcWake for LocalTask { +pub struct LocalTaskHandle { + id: TaskId, + tx: futures_channel::mpsc::UnboundedSender, +} + +impl ArcWake for LocalTaskHandle { fn wake_by_ref(arc_self: &Arc) { - _ = arc_self + arc_self .tx - .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id)); + .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id)) + .unwrap(); } } diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 205bd8ea0..9268cfeb0 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -10,7 +10,7 @@ use crate::{ ScopeId, TaskId, VNode, VirtualDom, }; -use super::{waker::ArcWake, SuspenseId}; +use super::SuspenseId; impl VirtualDom { /// Handle notifications by tasks inside the scheduler @@ -22,11 +22,11 @@ impl VirtualDom { let task = match tasks.get(id.0) { Some(task) => task, + // The task was removed from the scheduler, so we can just ignore it None => return, }; - let waker = task.waker(); - let mut cx = Context::from_waker(&waker); + let mut cx = Context::from_waker(&task.waker); // If the task completes... if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { @@ -45,19 +45,13 @@ impl VirtualDom { } pub(crate) fn handle_suspense_wakeup(&mut self, id: SuspenseId) { - let leaf = self - .scheduler - .leaves - .borrow_mut() - .get(id.0) - .unwrap() - .clone(); + let leaves = self.scheduler.leaves.borrow_mut(); + let leaf = leaves.get(id.0).unwrap(); let scope_id = leaf.scope_id; // todo: cache the waker - let waker = leaf.waker(); - let mut cx = Context::from_waker(&waker); + let mut cx = Context::from_waker(&leaf.waker); // Safety: the future is always pinned to the bump arena let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) }; @@ -91,6 +85,9 @@ impl VirtualDom { let place_holder_id = scope.placeholder.get().unwrap(); self.scope_stack.push(scope_id); + + drop(leaves); + let created = self.create(template); self.scope_stack.pop(); mutations.push(Mutation::ReplaceWith { diff --git a/packages/core/src/scheduler/waker.rs b/packages/core/src/scheduler/waker.rs deleted file mode 100644 index fc75fe8fd..000000000 --- a/packages/core/src/scheduler/waker.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::mem; -use std::sync::Arc; -use std::task::{RawWaker, RawWakerVTable, Waker}; - -pub trait ArcWake: Sized { - /// Create a waker from this self-wakening object - fn waker(self: &Arc) -> Waker { - unsafe fn rc_vtable() -> &'static RawWakerVTable { - &RawWakerVTable::new( - |data| { - let arc = mem::ManuallyDrop::new(Arc::::from_raw(data.cast::())); - let _rc_clone: mem::ManuallyDrop<_> = arc.clone(); - RawWaker::new(data, rc_vtable::()) - }, - |data| Arc::from_raw(data.cast::()).wake(), - |data| { - let arc = mem::ManuallyDrop::new(Arc::::from_raw(data.cast::())); - ArcWake::wake_by_ref(&arc); - }, - |data| drop(Arc::::from_raw(data.cast::())), - ) - } - - unsafe { - Waker::from_raw(RawWaker::new( - Arc::into_raw(self.clone()).cast(), - rc_vtable::(), - )) - } - } - - fn wake_by_ref(arc_self: &Arc); - - fn wake(self: Arc) { - Self::wake_by_ref(&self) - } -} diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index fbe865021..8d3de2b45 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -2,9 +2,8 @@ use crate::{ any_props::AnyProps, bump_frame::BumpFrame, innerlude::DirtyScope, - innerlude::{SuspenseId, SuspenseLeaf}, + innerlude::{SuspenseHandle, SuspenseId, SuspenseLeaf}, nodes::RenderReturn, - scheduler::ArcWake, scopes::{ScopeId, ScopeState}, virtual_dom::VirtualDom, }; @@ -90,16 +89,17 @@ impl VirtualDom { let entry = leaves.vacant_entry(); let suspense_id = SuspenseId(entry.key()); - let leaf = Arc::new(SuspenseLeaf { + let leaf = SuspenseLeaf { scope_id, task: task.as_mut(), - id: suspense_id, - tx: self.scheduler.sender.clone(), notified: Default::default(), - }); + waker: futures_util::task::waker(Arc::new(SuspenseHandle { + id: suspense_id, + tx: self.scheduler.sender.clone(), + })), + }; - let waker = leaf.waker(); - let mut cx = Context::from_waker(&waker); + let mut cx = Context::from_waker(&leaf.waker); // safety: the task is already pinned in the bump arena let mut pinned = unsafe { Pin::new_unchecked(task.as_mut()) }; diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 4aaae6c64..8e8cc1a90 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -33,7 +33,7 @@ webbrowser = "0.8.0" infer = "0.11.0" dunce = "1.0.2" -interprocess = { version = "1.1.1", optional = true} +interprocess = { version = "1.1.1", optional = true } futures-util = "0.3.25" [target.'cfg(target_os = "ios")'.dependencies] diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs deleted file mode 100644 index 2d08ad79a..000000000 --- a/packages/desktop/src/controller.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::desktop_context::{DesktopContext, UserWindowEvent}; -use dioxus_core::*; -use dioxus_html::HtmlEvent; -use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; -use futures_util::StreamExt; -#[cfg(target_os = "ios")] -use objc::runtime::Object; -use serde_json::Value; -use std::{ - collections::HashMap, - sync::Arc, - sync::{atomic::AtomicBool, Mutex}, - time::Duration, -}; -use wry::{ - self, - application::{event_loop::ControlFlow, event_loop::EventLoopProxy, window::WindowId}, - webview::WebView, -}; - -pub(super) struct DesktopController { - pub(super) webviews: HashMap, - pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender, - pub(super) pending_edits: Arc>>, - pub(super) quit_app_on_close: bool, - pub(super) is_ready: Arc, - pub(super) proxy: EventLoopProxy, - - pub(super) event_tx: UnboundedSender, - #[cfg(debug_assertions)] - pub(super) templates_tx: UnboundedSender>, - - #[cfg(target_os = "ios")] - pub(super) views: Vec<*mut Object>, -} - -impl DesktopController { - // Launch the virtualdom on its own thread managed by tokio - // returns the desktop state - pub(super) fn new_on_tokio( - root: Component

, - props: P, - proxy: EventLoopProxy, - ) -> Self { - let edit_queue = Arc::new(Mutex::new(Vec::new())); - let (event_tx, mut event_rx) = unbounded(); - let (templates_tx, mut templates_rx) = unbounded(); - let proxy2 = proxy.clone(); - - let pending_edits = edit_queue.clone(); - let desktop_context_proxy = proxy.clone(); - let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::(); - - std::thread::spawn(move || { - // We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads - // I'd personally not require tokio to be built-in to Dioxus-Desktop, but the DX is worse without it - - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - - runtime.block_on(async move { - let mut dom = VirtualDom::new_with_props(root, props) - .with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever)); - { - let edits = dom.rebuild(); - let mut queue = edit_queue.lock().unwrap(); - queue.push(serde_json::to_string(&edits).unwrap()); - proxy.send_event(UserWindowEvent::EditsReady).unwrap(); - } - - loop { - tokio::select! { - template = { - #[allow(unused)] - fn maybe_future<'a>(templates_rx: &'a mut UnboundedReceiver>) -> impl Future> + 'a { - #[cfg(debug_assertions)] - return templates_rx.select_next_some(); - #[cfg(not(debug_assertions))] - return std::future::pending(); - } - maybe_future(&mut templates_rx) - } => { - dom.replace_template(template); - } - _ = dom.wait_for_work() => {} - Some(json_value) = event_rx.next() => { - if let Ok(value) = serde_json::from_value::(json_value) { - let HtmlEvent { - name, - element, - bubbles, - data - } = value; - dom.handle_event(&name, data.into_any(), element, bubbles); - } - } - } - - let muts = dom - .render_with_deadline(tokio::time::sleep(Duration::from_millis(16))) - .await; - - edit_queue - .lock() - .unwrap() - .push(serde_json::to_string(&muts).unwrap()); - let _ = proxy.send_event(UserWindowEvent::EditsReady); - } - }) - }); - - Self { - pending_edits, - eval_sender, - webviews: HashMap::new(), - is_ready: Arc::new(AtomicBool::new(false)), - quit_app_on_close: true, - proxy: proxy2, - event_tx, - #[cfg(debug_assertions)] - templates_tx, - #[cfg(target_os = "ios")] - views: vec![], - } - } - - pub(super) 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(super) fn try_load_ready_webviews(&mut self) { - if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { - let mut new_queue = Vec::new(); - - { - let mut queue = self.pending_edits.lock().unwrap(); - std::mem::swap(&mut new_queue, &mut *queue); - } - - let (_id, view) = self.webviews.iter_mut().next().unwrap(); - - for edit in new_queue.drain(..) { - view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) - .unwrap(); - } - } - } -} diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 943fbf91e..cb5a62635 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,20 +1,15 @@ use std::rc::Rc; -use crate::controller::DesktopController; +use crate::eval::EvalResult; +use crate::events::IpcMessage; use dioxus_core::ScopeState; -use serde::de::Error; use serde_json::Value; -use std::future::Future; -use std::future::IntoFuture; -use std::pin::Pin; -use wry::application::dpi::LogicalSize; -use wry::application::event_loop::ControlFlow; use wry::application::event_loop::EventLoopProxy; #[cfg(target_os = "ios")] use wry::application::platform::ios::WindowExtIOS; use wry::application::window::Fullscreen as WryFullscreen; - -use UserWindowEvent::*; +use wry::application::window::Window; +use wry::webview::WebView; pub type ProxyType = EventLoopProxy; @@ -40,18 +35,35 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext { #[derive(Clone)] pub struct DesktopContext { /// The wry/tao proxy to the current window + pub webview: Rc, + + /// The proxy to the event loop pub proxy: ProxyType, - pub(super) eval_reciever: Rc>>, + + /// The receiver for eval results since eval is async + pub(super) eval: tokio::sync::broadcast::Sender, + + #[cfg(target_os = "ios")] + pub(crate) views: Rc>>, +} + +/// A smart pointer to the current window. +impl std::ops::Deref for DesktopContext { + type Target = Window; + + fn deref(&self) -> &Self::Target { + self.webview.window() + } } impl DesktopContext { - pub(crate) fn new( - proxy: ProxyType, - eval_reciever: tokio::sync::mpsc::UnboundedReceiver, - ) -> Self { + pub(crate) fn new(webview: Rc, proxy: ProxyType) -> Self { Self { + webview, proxy, - eval_reciever: Rc::new(tokio::sync::Mutex::new(eval_reciever)), + eval: tokio::sync::broadcast::channel(8).0, + #[cfg(target_os = "ios")] + views: Default::default(), } } @@ -64,283 +76,125 @@ impl DesktopContext { /// onmousedown: move |_| { desktop.drag_window(); } /// ``` pub fn drag(&self) { - let _ = self.proxy.send_event(DragWindow); + 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()); } - /// set window minimize state - pub fn set_minimized(&self, minimized: bool) { - let _ = self.proxy.send_event(Minimize(minimized)); - } - - /// set window maximize state - pub fn set_maximized(&self, maximized: bool) { - let _ = self.proxy.send_event(Maximize(maximized)); - } - - /// toggle window maximize state + /// Toggle whether the window is maximized or not pub fn toggle_maximized(&self) { - let _ = self.proxy.send_event(MaximizeToggle); - } + let window = self.webview.window(); - /// set window visible or not - pub fn set_visible(&self, visible: bool) { - let _ = self.proxy.send_event(Visible(visible)); + window.set_maximized(!window.is_maximized()) } /// close window pub fn close(&self) { - let _ = self.proxy.send_event(CloseWindow); - } - - /// set window to focus - pub fn focus(&self) { - let _ = self.proxy.send_event(FocusWindow); + let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); } /// change window to fullscreen pub fn set_fullscreen(&self, fullscreen: bool) { - let _ = self.proxy.send_event(Fullscreen(fullscreen)); - } - - /// set resizable state - pub fn set_resizable(&self, resizable: bool) { - let _ = self.proxy.send_event(Resizable(resizable)); - } - - /// set the window always on top - pub fn set_always_on_top(&self, top: bool) { - let _ = self.proxy.send_event(AlwaysOnTop(top)); - } - - /// set cursor visible or not - pub fn set_cursor_visible(&self, visible: bool) { - let _ = self.proxy.send_event(CursorVisible(visible)); - } - - /// set cursor grab - pub fn set_cursor_grab(&self, grab: bool) { - let _ = self.proxy.send_event(CursorGrab(grab)); - } - - /// set window title - pub fn set_title(&self, title: &str) { - let _ = self.proxy.send_event(SetTitle(String::from(title))); - } - - /// change window to borderless - pub fn set_decorations(&self, decoration: bool) { - let _ = self.proxy.send_event(SetDecorations(decoration)); - } - - /// set window zoom level - pub fn set_zoom_level(&self, scale_factor: f64) { - let _ = self.proxy.send_event(SetZoomLevel(scale_factor)); - } - - /// modifies the inner size of the window - pub fn set_inner_size(&self, logical_size: LogicalSize) { - let _ = self.proxy.send_event(SetInnerSize(logical_size)); + if let Some(handle) = self.webview.window().current_monitor() { + self.webview + .window() + .set_fullscreen(fullscreen.then_some(WryFullscreen::Borderless(Some(handle)))); + } } /// launch print modal pub fn print(&self) { - let _ = self.proxy.send_event(Print); + if let Err(e) = self.webview.print() { + log::warn!("Open print modal failed: {e}"); + } + } + + /// Set the zoom level of the webview + pub fn set_zoom_level(&self, level: f64) { + self.webview.zoom(level); } /// opens DevTool window pub fn devtool(&self) { - let _ = self.proxy.send_event(DevTool); + #[cfg(debug_assertions)] + self.webview.open_devtools(); + + #[cfg(not(debug_assertions))] + log::warn!("Devtools are disabled in release builds"); } - /// run (evaluate) a script in the WebView context - pub fn eval(&self, script: impl std::string::ToString) { - let _ = self.proxy.send_event(Eval(script.to_string())); + /// Evaluate a javascript expression + pub fn eval(&self, code: &str) -> EvalResult { + // Embed the return of the eval in a function so we can send it back to the main thread + let script = format!( + r#" + window.ipc.postMessage( + JSON.stringify({{ + "method":"eval_result", + "params": ( + function(){{ + {} + }} + )() + }}) + ); + "#, + code + ); + + if let Err(e) = self.webview.evaluate_script(&script) { + // send an error to the eval receiver + log::warn!("Eval script error: {e}"); + } + + EvalResult::new(self.eval.clone()) } - /// Push view + /// Push an objc view to the window #[cfg(target_os = "ios")] pub fn push_view(&self, view: objc_id::ShareId) { - let _ = self.proxy.send_event(PushView(view)); + let window = self.webview.window(); + + unsafe { + use objc::runtime::Object; + use objc::*; + assert!(is_main_thread()); + let ui_view = window.ui_view() as *mut Object; + let ui_view_frame: *mut Object = msg_send![ui_view, frame]; + let _: () = msg_send![view, setFrame: ui_view_frame]; + let _: () = msg_send![view, setAutoresizingMask: 31]; + + let ui_view_controller = window.ui_view_controller() as *mut Object; + let _: () = msg_send![ui_view_controller, setView: view]; + self.views.borrow_mut().push(ui_view); + } } - /// Push view + /// Pop an objc view from the window #[cfg(target_os = "ios")] pub fn pop_view(&self) { - let _ = self.proxy.send_event(PopView); + let window = self.webview.window(); + + unsafe { + use objc::runtime::Object; + use objc::*; + assert!(is_main_thread()); + if let Some(view) = self.views.borrow_mut().pop() { + let ui_view_controller = window.ui_view_controller() as *mut Object; + let _: () = msg_send![ui_view_controller, setView: view]; + } + } } } #[derive(Debug)] pub enum UserWindowEvent { - EditsReady, - Initialize, + Poll, + + Ipc(IpcMessage), CloseWindow, - DragWindow, - FocusWindow, - - Visible(bool), - Minimize(bool), - Maximize(bool), - MaximizeToggle, - Resizable(bool), - AlwaysOnTop(bool), - Fullscreen(bool), - - CursorVisible(bool), - CursorGrab(bool), - - SetTitle(String), - SetDecorations(bool), - - SetZoomLevel(f64), - SetInnerSize(LogicalSize), - - Print, - DevTool, - - Eval(String), - - #[cfg(target_os = "ios")] - PushView(objc_id::ShareId), - #[cfg(target_os = "ios")] - PopView, -} - -impl DesktopController { - pub(super) fn handle_event( - &mut self, - user_event: UserWindowEvent, - control_flow: &mut ControlFlow, - ) { - // currently dioxus-desktop supports a single window only, - // so we can grab the only webview from the map; - // on wayland it is possible that a user event is emitted - // before the webview is initialized. ignore the event. - let webview = if let Some(webview) = self.webviews.values().next() { - webview - } else { - return; - }; - - let window = webview.window(); - - match user_event { - Initialize | EditsReady => self.try_load_ready_webviews(), - CloseWindow => *control_flow = ControlFlow::Exit, - DragWindow => { - // if the drag_window has any errors, we don't do anything - window.fullscreen().is_none().then(|| window.drag_window()); - } - Visible(state) => window.set_visible(state), - Minimize(state) => window.set_minimized(state), - Maximize(state) => window.set_maximized(state), - MaximizeToggle => window.set_maximized(!window.is_maximized()), - Fullscreen(state) => { - if let Some(handle) = window.current_monitor() { - window.set_fullscreen(state.then_some(WryFullscreen::Borderless(Some(handle)))); - } - } - FocusWindow => window.set_focus(), - Resizable(state) => window.set_resizable(state), - AlwaysOnTop(state) => window.set_always_on_top(state), - - Eval(code) => { - let script = format!( - r#"window.ipc.postMessage(JSON.stringify({{"method":"eval_result", params: (function(){{ - {} - }})()}}));"#, - code - ); - if let Err(e) = webview.evaluate_script(&script) { - // we can't panic this error. - log::warn!("Eval script error: {e}"); - } - } - CursorVisible(state) => window.set_cursor_visible(state), - CursorGrab(state) => { - let _ = window.set_cursor_grab(state); - } - - SetTitle(content) => window.set_title(&content), - SetDecorations(state) => window.set_decorations(state), - - SetZoomLevel(scale_factor) => webview.zoom(scale_factor), - SetInnerSize(logical_size) => window.set_inner_size(logical_size), - - Print => { - if let Err(e) = webview.print() { - // we can't panic this error. - log::warn!("Open print modal failed: {e}"); - } - } - DevTool => { - #[cfg(debug_assertions)] - webview.open_devtools(); - #[cfg(not(debug_assertions))] - log::warn!("Devtools are disabled in release builds"); - } - - #[cfg(target_os = "ios")] - PushView(view) => unsafe { - use objc::runtime::Object; - use objc::*; - assert!(is_main_thread()); - let ui_view = window.ui_view() as *mut Object; - let ui_view_frame: *mut Object = msg_send![ui_view, frame]; - let _: () = msg_send![view, setFrame: ui_view_frame]; - let _: () = msg_send![view, setAutoresizingMask: 31]; - - let ui_view_controller = window.ui_view_controller() as *mut Object; - let _: () = msg_send![ui_view_controller, setView: view]; - self.views.push(ui_view); - }, - - #[cfg(target_os = "ios")] - PopView => unsafe { - use objc::runtime::Object; - use objc::*; - assert!(is_main_thread()); - if let Some(view) = self.views.pop() { - let ui_view_controller = window.ui_view_controller() as *mut Object; - let _: () = msg_send![ui_view_controller, setView: view]; - } - }, - } - } -} - -/// Get a closure that executes any JavaScript in the WebView context. -pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult { - let desktop = use_window(cx).clone(); - cx.use_hook(|| { - move |script| { - desktop.eval(script); - let recv = desktop.eval_reciever.clone(); - EvalResult { reciever: recv } - } - }) -} - -/// A future that resolves to the result of a JavaScript evaluation. -pub struct EvalResult { - reciever: Rc>>, -} - -impl IntoFuture for EvalResult { - type Output = Result; - - type IntoFuture = Pin>>>; - - fn into_future(self) -> Self::IntoFuture { - Box::pin(async move { - let mut reciever = self.reciever.lock().await; - match reciever.recv().await { - Some(result) => Ok(result), - None => Err(serde_json::Error::custom("No result returned")), - } - }) as Pin>>> - } } #[cfg(target_os = "ios")] diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/eval.rs new file mode 100644 index 000000000..7a92bedf5 --- /dev/null +++ b/packages/desktop/src/eval.rs @@ -0,0 +1,45 @@ +use std::rc::Rc; + +use crate::use_window; +use dioxus_core::ScopeState; +use serde::de::Error; +use std::future::Future; +use std::future::IntoFuture; +use std::pin::Pin; + +/// A future that resolves to the result of a JavaScript evaluation. +pub struct EvalResult { + pub(crate) broadcast: tokio::sync::broadcast::Sender, +} + +impl EvalResult { + pub(crate) fn new(sender: tokio::sync::broadcast::Sender) -> Self { + Self { broadcast: sender } + } +} + +impl IntoFuture for EvalResult { + type Output = Result; + + type IntoFuture = Pin>>>; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(async move { + let mut reciever = self.broadcast.subscribe(); + match reciever.recv().await { + Ok(result) => Ok(result), + Err(_) => Err(serde_json::Error::custom("No result returned")), + } + }) as Pin>>> + } +} + +/// Get a closure that executes any JavaScript in the WebView context. +pub fn use_eval(cx: &ScopeState) -> &Rc EvalResult> { + let desktop = use_window(cx); + &*cx.use_hook(|| { + let desktop = desktop.clone(); + + Rc::new(move |script: String| desktop.eval(&script)) as Rc EvalResult> + }) +} diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index f9b3fe5cb..50d3fbf3d 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -2,8 +2,8 @@ use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Serialize)] -pub(crate) struct IpcMessage { +#[derive(Deserialize, Serialize, Debug)] +pub struct IpcMessage { method: String, params: serde_json::Value, } @@ -17,13 +17,3 @@ impl IpcMessage { self.params } } - -pub(crate) fn parse_ipc_message(payload: &str) -> Option { - match serde_json::from_str(payload) { - Ok(message) => Some(message), - Err(e) => { - log::error!("could not parse IPC message, error: {}", e); - None - } - } -} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 337a9485c..e3002f539 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -4,36 +4,35 @@ #![deny(missing_docs)] mod cfg; -mod controller; mod desktop_context; mod escape; +mod eval; mod events; mod protocol; +mod waker; +mod webview; #[cfg(all(feature = "hot-reload", debug_assertions))] mod hot_reload; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; - -use desktop_context::UserWindowEvent; -pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult}; -use futures_channel::mpsc::UnboundedSender; -pub use wry; -pub use wry::application as tao; - pub use cfg::Config; -use controller::DesktopController; +use desktop_context::UserWindowEvent; +pub use desktop_context::{use_window, DesktopContext}; use dioxus_core::*; -use events::parse_ipc_message; +use dioxus_html::HtmlEvent; +pub use eval::{use_eval, EvalResult}; +use futures_util::{pin_mut, FutureExt}; +use std::collections::HashMap; +use std::rc::Rc; +use std::task::Waker; pub use tao::dpi::{LogicalSize, PhysicalSize}; pub use tao::window::WindowBuilder; use tao::{ event::{Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoop}, - window::Window, }; -use wry::webview::WebViewBuilder; +pub use wry; +pub use wry::application as tao; /// Launch the WebView and run the event loop. /// @@ -81,7 +80,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// 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 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 /// @@ -89,7 +88,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// use dioxus::prelude::*; /// /// fn main() { -/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, |c| c); +/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default()); /// } /// /// struct AppProps { @@ -102,165 +101,140 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// }) /// } /// ``` -pub fn launch_with_props(root: Component

, props: P, mut cfg: Config) { - let event_loop = EventLoop::with_user_event(); - let mut desktop = DesktopController::new_on_tokio(root, props, event_loop.create_proxy()); +pub fn launch_with_props(root: Component

, props: P, mut cfg: Config) { + let mut dom = VirtualDom::new_with_props(root, props); - #[cfg(debug_assertions)] - hot_reload::init(desktop.templates_tx.clone()); + let event_loop = EventLoop::with_user_event(); + + let proxy = event_loop.create_proxy(); + + // 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 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(); event_loop.run(move |window_event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; match window_event { - Event::NewEvents(StartCause::Init) => desktop.start(&mut cfg, event_loop), + Event::UserEvent(UserWindowEvent::CloseWindow) => *control_flow = ControlFlow::Exit, Event::WindowEvent { event, window_id, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, - WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow), + WindowEvent::Destroyed { .. } => { + webviews.remove(&window_id); + + if webviews.is_empty() { + *control_flow = ControlFlow::Exit; + } + } _ => {} }, - Event::UserEvent(user_event) => desktop.handle_event(user_event, control_flow), - Event::MainEventsCleared => {} - Event::Resumed => {} - Event::Suspended => {} - Event::LoopDestroyed => {} - Event::RedrawRequested(_id) => {} + Event::NewEvents(StartCause::Init) => { + let window = webview::build(&mut cfg, event_loop, proxy.clone()); + + dom.base_scope() + .provide_context(DesktopContext::new(window.clone(), proxy.clone())); + + webviews.insert(window.window().id(), window); + + _ = proxy.send_event(UserWindowEvent::Poll); + } + + Event::UserEvent(UserWindowEvent::Poll) => { + poll_vdom(&waker, &mut dom, &mut webviews); + } + + Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "user_event" => { + let evt = match serde_json::from_value::(msg.params()) { + Ok(value) => value, + Err(_) => return, + }; + + 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); + } + } + } + } + _ => {} } }) } -impl DesktopController { - fn start( - &mut self, - cfg: &mut Config, - event_loop: &tao::event_loop::EventLoopWindowTarget, - ) { - let webview = build_webview( - cfg, - event_loop, - self.is_ready.clone(), - self.proxy.clone(), - self.eval_sender.clone(), - self.event_tx.clone(), - ); +type Webviews = HashMap>; - self.webviews.insert(webview.window().id(), webview); - } -} +/// 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 build_webview( - cfg: &mut Config, - event_loop: &tao::event_loop::EventLoopWindowTarget, - is_ready: Arc, - proxy: tao::event_loop::EventLoopProxy, - eval_sender: tokio::sync::mpsc::UnboundedSender, - event_tx: UnboundedSender, -) -> wry::webview::WebView { - let builder = cfg.window.clone(); - let window = builder.build(event_loop).unwrap(); - let file_handler = cfg.file_drop_handler.take(); - let custom_head = cfg.custom_head.clone(); - let resource_dir = cfg.resource_dir.clone(); - let index_file = cfg.custom_index.clone(); - let root_name = cfg.root_name.clone(); + loop { + { + let fut = dom.wait_for_work(); + pin_mut!(fut); - // We assume that if the icon is None in cfg, then the user just didnt set it - if cfg.window.window.window_icon.is_none() { - window.set_window_icon(Some( - tao::window::Icon::from_rgba( - include_bytes!("./assets/default_icon.bin").to_vec(), - 460, - 460, - ) - .expect("image parse failed"), - )); - } - - let mut webview = WebViewBuilder::new(window) - .unwrap() - .with_transparent(cfg.window.window.transparent) - .with_url("dioxus://index.html/") - .unwrap() - .with_ipc_handler(move |_window: &Window, payload: String| { - let message = match parse_ipc_message(&payload) { - Some(message) => message, - None => { - log::error!("Failed to parse IPC message: {}", payload); - return; - } - }; - - match message.method() { - "eval_result" => { - let result = message.params(); - eval_sender.send(result).unwrap(); - } - "user_event" => { - _ = event_tx.unbounded_send(message.params()); - } - "initialize" => { - is_ready.store(true, std::sync::atomic::Ordering::Relaxed); - let _ = proxy.send_event(UserWindowEvent::EditsReady); - } - "browser_open" => { - let data = message.params(); - log::trace!("Open browser: {:?}", data); - if let Some(temp) = data.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); - } - } - } - } - _ => (), + match fut.poll_unpin(&mut cx) { + std::task::Poll::Ready(_) => {} + std::task::Poll::Pending => break, } - }) - .with_custom_protocol(String::from("dioxus"), move |r| { - protocol::desktop_handler( - r, - resource_dir.clone(), - custom_head.clone(), - index_file.clone(), - &root_name, - ) - }) - .with_file_drop_handler(move |window, evet| { - file_handler - .as_ref() - .map(|handler| handler(window, evet)) - .unwrap_or_default() - }); + } - for (name, handler) in cfg.protocols.drain(..) { - webview = webview.with_custom_protocol(name, handler) + send_edits(dom.render_immediate(), webviews); } - - if cfg.disable_context_menu { - // in release mode, we don't want to show the dev tool or reload menus - webview = webview.with_initialization_script( - r#" - if (document.addEventListener) { - document.addEventListener('contextmenu', function(e) { - e.preventDefault(); - }, false); - } else { - document.attachEvent('oncontextmenu', function() { - window.event.returnValue = false; - }); - } - "#, - ) - } else { - // in debug, we are okay with the reload menu showing and dev tool - webview = webview.with_devtools(true); - } - - webview.build().unwrap() +} + +/// Send a list of mutations to the webview +fn send_edits(edits: Mutations, webviews: &mut Webviews) { + 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)); } diff --git a/packages/desktop/src/waker.rs b/packages/desktop/src/waker.rs new file mode 100644 index 000000000..a2586ffed --- /dev/null +++ b/packages/desktop/src/waker.rs @@ -0,0 +1,26 @@ +use crate::desktop_context::UserWindowEvent; +use futures_util::task::ArcWake; +use std::sync::Arc; +use wry::application::event_loop::EventLoopProxy; + +/// 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); + + // this should be implemented by most platforms, but ios is missing this until + // https://github.com/tauri-apps/wry/issues/830 is resolved + unsafe impl Send for DomHandle {} + unsafe impl Sync for DomHandle {} + + impl ArcWake for DomHandle { + fn wake_by_ref(arc_self: &Arc) { + _ = arc_self.0.send_event(UserWindowEvent::Poll); + } + } + + futures_util::task::waker(Arc::new(DomHandle(proxy.clone()))) +} diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs new file mode 100644 index 000000000..4a270bfab --- /dev/null +++ b/packages/desktop/src/webview.rs @@ -0,0 +1,88 @@ +use std::rc::Rc; + +use crate::protocol; +use crate::{desktop_context::UserWindowEvent, Config}; +use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; +pub use wry; +pub use wry::application as tao; +use wry::application::window::Window; +use wry::webview::{WebView, WebViewBuilder}; + +pub fn build( + cfg: &mut Config, + event_loop: &EventLoopWindowTarget, + proxy: EventLoopProxy, +) -> Rc { + let builder = cfg.window.clone(); + let window = builder.build(event_loop).unwrap(); + let file_handler = cfg.file_drop_handler.take(); + let custom_head = cfg.custom_head.clone(); + let resource_dir = cfg.resource_dir.clone(); + let index_file = cfg.custom_index.clone(); + let root_name = cfg.root_name.clone(); + + // We assume that if the icon is None in cfg, then the user just didnt set it + if cfg.window.window.window_icon.is_none() { + window.set_window_icon(Some( + tao::window::Icon::from_rgba( + include_bytes!("./assets/default_icon.bin").to_vec(), + 460, + 460, + ) + .expect("image parse failed"), + )); + } + + let mut webview = WebViewBuilder::new(window) + .unwrap() + .with_transparent(cfg.window.window.transparent) + .with_url("dioxus://index.html/") + .unwrap() + .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)); + } + }) + .with_custom_protocol(String::from("dioxus"), move |r| { + protocol::desktop_handler( + r, + resource_dir.clone(), + custom_head.clone(), + index_file.clone(), + &root_name, + ) + }) + .with_file_drop_handler(move |window, evet| { + file_handler + .as_ref() + .map(|handler| handler(window, evet)) + .unwrap_or_default() + }); + + for (name, handler) in cfg.protocols.drain(..) { + webview = webview.with_custom_protocol(name, handler) + } + + if cfg.disable_context_menu { + // in release mode, we don't want to show the dev tool or reload menus + webview = webview.with_initialization_script( + r#" + if (document.addEventListener) { + document.addEventListener('contextmenu', function(e) { + e.preventDefault(); + }, false); + } else { + document.attachEvent('oncontextmenu', function() { + window.event.returnValue = false; + }); + } + "#, + ) + } else { + // in debug, we are okay with the reload menu showing and dev tool + webview = webview.with_devtools(true); + } + + Rc::new(webview.build().unwrap()) +}