Browse Source

Merge pull request #692 from DioxusLabs/jk/main-thread

Move desktop context to main thread to allow file dialogs and expose tao/wry directly
Jon Kelley 2 năm trước cách đây
mục cha
commit
3cfaaea7ea

+ 32 - 0
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}" }
+    })
+}

+ 9 - 13
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<Option<EvalResult>> = 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"
             }

+ 5 - 4
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::<f64>().unwrap_or(1.0);
-                level.set(num);
-                window.set_zoom_level(num);
+                if let Ok(new_zoom) = e.value.parse::<f64>() {
+                    level.set(new_zoom);
+                    window.webview.zoom(new_zoom);
+                }
             }
         }
     })

+ 1 - 1
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"
 

+ 3 - 5
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<SchedulerMsg>,
 
     /// Tasks created with cx.spawn
-    pub tasks: RefCell<Slab<Arc<LocalTask>>>,
+    pub tasks: RefCell<Slab<LocalTask>>,
 
     /// Async components
-    pub leaves: RefCell<Slab<Arc<SuspenseLeaf>>>,
+    pub leaves: RefCell<Slab<SuspenseLeaf>>,
 }
 
 impl Scheduler {

+ 11 - 5
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<SchedulerMsg>,
     pub(crate) notified: Cell<bool>,
     pub(crate) task: *mut dyn Future<Output = Element<'static>>,
+    pub(crate) waker: Waker,
+}
+
+pub struct SuspenseHandle {
+    pub(crate) id: SuspenseId,
+    pub(crate) tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
 }
 
-impl ArcWake for SuspenseLeaf {
+impl ArcWake for SuspenseHandle {
     fn wake_by_ref(arc_self: &Arc<Self>) {
-        arc_self.notified.set(true);
         _ = arc_self
             .tx
             .unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id));

+ 23 - 10
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<Pin<Box<dyn Future<Output = ()> + 'static>>>,
-    id: TaskId,
-    tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
+    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<Output = ()> + '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<SchedulerMsg>,
+}
+
+impl ArcWake for LocalTaskHandle {
     fn wake_by_ref(arc_self: &Arc<Self>) {
-        _ = arc_self
+        arc_self
             .tx
-            .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id));
+            .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id))
+            .unwrap();
     }
 }

+ 9 - 12
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 {

+ 0 - 37
packages/core/src/scheduler/waker.rs

@@ -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<Self>) -> Waker {
-        unsafe fn rc_vtable<T: ArcWake>() -> &'static RawWakerVTable {
-            &RawWakerVTable::new(
-                |data| {
-                    let arc = mem::ManuallyDrop::new(Arc::<T>::from_raw(data.cast::<T>()));
-                    let _rc_clone: mem::ManuallyDrop<_> = arc.clone();
-                    RawWaker::new(data, rc_vtable::<T>())
-                },
-                |data| Arc::from_raw(data.cast::<T>()).wake(),
-                |data| {
-                    let arc = mem::ManuallyDrop::new(Arc::<T>::from_raw(data.cast::<T>()));
-                    ArcWake::wake_by_ref(&arc);
-                },
-                |data| drop(Arc::<T>::from_raw(data.cast::<T>())),
-            )
-        }
-
-        unsafe {
-            Waker::from_raw(RawWaker::new(
-                Arc::into_raw(self.clone()).cast(),
-                rc_vtable::<Self>(),
-            ))
-        }
-    }
-
-    fn wake_by_ref(arc_self: &Arc<Self>);
-
-    fn wake(self: Arc<Self>) {
-        Self::wake_by_ref(&self)
-    }
-}

+ 8 - 8
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()) };

+ 1 - 1
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]

+ 0 - 154
packages/desktop/src/controller.rs

@@ -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<WindowId, WebView>,
-    pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender<Value>,
-    pub(super) pending_edits: Arc<Mutex<Vec<String>>>,
-    pub(super) quit_app_on_close: bool,
-    pub(super) is_ready: Arc<AtomicBool>,
-    pub(super) proxy: EventLoopProxy<UserWindowEvent>,
-
-    pub(super) event_tx: UnboundedSender<serde_json::Value>,
-    #[cfg(debug_assertions)]
-    pub(super) templates_tx: UnboundedSender<Template<'static>>,
-
-    #[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<P: Send + 'static>(
-        root: Component<P>,
-        props: P,
-        proxy: EventLoopProxy<UserWindowEvent>,
-    ) -> 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::<Value>();
-
-        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<Template<'static>>) -> impl Future<Output = dioxus_core::Template<'static>> + '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::<HtmlEvent>(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();
-            }
-        }
-    }
-}

+ 107 - 253
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<UserWindowEvent>;
 
@@ -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<WebView>,
+
+    /// The proxy to the event loop
     pub proxy: ProxyType,
-    pub(super) eval_reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<Value>>>,
+
+    /// The receiver for eval results since eval is async
+    pub(super) eval: tokio::sync::broadcast::Sender<Value>,
+
+    #[cfg(target_os = "ios")]
+    pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
+}
+
+/// 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<Value>,
-    ) -> Self {
+    pub(crate) fn new(webview: Rc<WebView>, 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();
 
-    /// set window minimize state
-    pub fn set_minimized(&self, minimized: bool) {
-        let _ = self.proxy.send_event(Minimize(minimized));
+        // if the drag_window has any errors, we don't do anything
+        window.fullscreen().is_none().then(|| window.drag_window());
     }
 
-    /// 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<f64>) {
-        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");
+    }
+
+    /// 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}");
+        }
 
-    /// 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()));
+        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<objc::runtime::Object>) {
-        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);
-    }
-}
-
-#[derive(Debug)]
-pub enum UserWindowEvent {
-    EditsReady,
-    Initialize,
-
-    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<f64>),
-
-    Print,
-    DevTool,
-
-    Eval(String),
-
-    #[cfg(target_os = "ios")]
-    PushView(objc_id::ShareId<objc::runtime::Object>),
-    #[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 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];
-                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<S: std::string::ToString>(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<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<serde_json::Value>>>,
-}
-
-impl IntoFuture for EvalResult {
-    type Output = Result<serde_json::Value, serde_json::Error>;
+#[derive(Debug)]
+pub enum UserWindowEvent {
+    Poll,
 
-    type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
+    Ipc(IpcMessage),
 
-    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<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
-    }
+    CloseWindow,
 }
 
 #[cfg(target_os = "ios")]

+ 45 - 0
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<serde_json::Value>,
+}
+
+impl EvalResult {
+    pub(crate) fn new(sender: tokio::sync::broadcast::Sender<serde_json::Value>) -> Self {
+        Self { broadcast: sender }
+    }
+}
+
+impl IntoFuture for EvalResult {
+    type Output = Result<serde_json::Value, serde_json::Error>;
+
+    type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
+
+    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<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
+    }
+}
+
+/// Get a closure that executes any JavaScript in the WebView context.
+pub fn use_eval(cx: &ScopeState) -> &Rc<dyn Fn(String) -> EvalResult> {
+    let desktop = use_window(cx);
+    &*cx.use_hook(|| {
+        let desktop = desktop.clone();
+
+        Rc::new(move |script: String| desktop.eval(&script)) as Rc<dyn Fn(String) -> EvalResult>
+    })
+}

+ 2 - 12
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<IpcMessage> {
-    match serde_json::from_str(payload) {
-        Ok(message) => Some(message),
-        Err(e) => {
-            log::error!("could not parse IPC message, error: {}", e);
-            None
-        }
-    }
-}

+ 125 - 151
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<P: 'static + Send>(root: Component<P>, props: P, mut cfg: Config) {
+pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, mut cfg: Config) {
+    let mut dom = VirtualDom::new_with_props(root, props);
+
     let event_loop = EventLoop::with_user_event();
-    let mut desktop = DesktopController::new_on_tokio(root, props, event_loop.create_proxy());
 
-    #[cfg(debug_assertions)]
-    hot_reload::init(desktop.templates_tx.clone());
+    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());
 
-impl DesktopController {
-    fn start(
-        &mut self,
-        cfg: &mut Config,
-        event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
-    ) {
-        let webview = build_webview(
-            cfg,
-            event_loop,
-            self.is_ready.clone(),
-            self.proxy.clone(),
-            self.eval_sender.clone(),
-            self.event_tx.clone(),
-        );
-
-        self.webviews.insert(webview.window().id(), webview);
-    }
-}
+                dom.base_scope()
+                    .provide_context(DesktopContext::new(window.clone(), proxy.clone()));
 
-fn build_webview(
-    cfg: &mut Config,
-    event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
-    is_ready: Arc<AtomicBool>,
-    proxy: tao::event_loop::EventLoopProxy<UserWindowEvent>,
-    eval_sender: tokio::sync::mpsc::UnboundedSender<serde_json::Value>,
-    event_tx: UnboundedSender<serde_json::Value>,
-) -> 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();
-
-    // 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"),
-        ));
-    }
+                webviews.insert(window.window().id(), window);
 
-    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;
-                }
-            };
+                _ = proxy.send_event(UserWindowEvent::Poll);
+            }
 
-            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);
-                            }
+            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::<HtmlEvent>(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::<DesktopContext>()
+                    .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);
                         }
                     }
                 }
-                _ => (),
             }
-        })
-        .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);
+            _ => {}
+        }
+    })
+}
+
+type Webviews = HashMap<tao::window::WindowId, Rc<wry::webview::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);
+
+    loop {
+        {
+            let fut = dom.wait_for_work();
+            pin_mut!(fut);
+
+            match fut.poll_unpin(&mut cx) {
+                std::task::Poll::Ready(_) => {}
+                std::task::Poll::Pending => break,
+            }
+        }
+
+        send_edits(dom.render_immediate(), webviews);
     }
+}
+
+/// 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();
 
-    webview.build().unwrap()
+    // todo: use SSE and binary data to send the edits with lower overhead
+    _ = view.evaluate_script(&format!("window.interpreter.handleEdits({})", serialized));
 }

+ 26 - 0
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<UserWindowEvent>) -> std::task::Waker {
+    struct DomHandle(EventLoopProxy<UserWindowEvent>);
+
+    // 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<Self>) {
+            _ = arc_self.0.send_event(UserWindowEvent::Poll);
+        }
+    }
+
+    futures_util::task::waker(Arc::new(DomHandle(proxy.clone())))
+}

+ 88 - 0
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<UserWindowEvent>,
+    proxy: EventLoopProxy<UserWindowEvent>,
+) -> Rc<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();
+
+    // 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())
+}