Browse Source

Merge branch 'master' of https://github.com/DioxusLabs/dioxus

Miles Murgaw 3 years ago
parent
commit
2fea2fb13a

+ 13 - 6
Cargo.toml

@@ -36,6 +36,13 @@ web = ["dioxus-web"]
 desktop = ["dioxus-desktop"]
 router = ["dioxus-router"]
 
+devtool = ["dioxus-desktop/devtool"]
+fullscreen = ["dioxus-desktop/fullscreen"]
+transparent = ["dioxus-desktop/transparent"]
+
+tray = ["dioxus-desktop/tray"]
+ayatana = ["dioxus-desktop/ayatana"]
+
 # "dioxus-router/web"
 # "dioxus-router/desktop"
 # desktop = ["dioxus-desktop", "dioxus-router/desktop"]
@@ -57,15 +64,15 @@ members = [
 ]
 
 [dev-dependencies]
-futures-util = "0.3.17"
+futures-util = "0.3.21"
 log = "0.4.14"
 num-format = "0.4.0"
 separator = "0.4.1"
-serde = { version = "1.0.131", features = ["derive"] }
+serde = { version = "1.0.136", features = ["derive"] }
 im-rc = "15.0.0"
-anyhow = "1.0.51"
-serde_json = "1.0.73"
+anyhow = "1.0.53"
+serde_json = "1.0.79"
 rand = { version = "0.8.4", features = ["small_rng"] }
-tokio = { version = "1.14.0", features = ["full"] }
-reqwest = { version = "0.11.8", features = ["json"] }
+tokio = { version = "1.16.1", features = ["full"] }
+reqwest = { version = "0.11.9", features = ["json"] }
 dioxus = { path = ".", features = ["desktop", "ssr", "router"] }

+ 1 - 1
docs/guide/src/interactivity/hooks.md

@@ -140,7 +140,7 @@ fn Child(cx: Scope, name: String) -> Element {
 
 // ✅ Or, use a hashmap with use_ref
 ```rust
-let ages = use_ref(&cx, |_| HashMap::new());
+let ages = use_ref(&cx, || HashMap::new());
 
 names.iter().map(|name| {
     let age = ages.get(name).unwrap();

+ 1 - 1
packages/core/Cargo.toml

@@ -32,7 +32,7 @@ smallvec = "1.6"
 
 slab = "0.4"
 
-futures-channel = "0.3"
+futures-channel = "0.3.21"
 
 # used for noderefs
 once_cell = "1.8"

+ 15 - 8
packages/desktop/Cargo.toml

@@ -13,15 +13,15 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
 
 [dependencies]
 dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] }
-argh = "0.1.4"
-serde = "1.0.120"
-serde_json = "1.0.61"
-thiserror = "1.0.23"
-log = "0.4.13"
+argh = "0.1.7"
+serde = "1.0.136"
+serde_json = "1.0.79"
+thiserror = "1.0.30"
+log = "0.4.14"
 html-escape = "0.2.9"
-wry = "0.12.2"
-futures-channel = "0.3"
-tokio = { version = "1.12.0", features = [
+wry = { version = "0.13.1" }
+futures-channel = "0.3.21"
+tokio = { version = "1.16.1", features = [
     "sync",
     "rt-multi-thread",
     "rt",
@@ -38,6 +38,13 @@ dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" }
 default = ["tokio_runtime"]
 tokio_runtime = ["tokio"]
 
+devtool = ["wry/devtool"]
+fullscreen = ["wry/fullscreen"]
+transparent = ["wry/transparent"]
+
+tray = ["wry/tray"]
+ayatana = ["wry/ayatana"]
+
 
 [dev-dependencies]
 dioxus-hooks = { path = "../hooks" }

+ 0 - 0
packages/desktop/src/default_icon.bin → packages/desktop/src/assets/default_icon.bin


+ 0 - 0
packages/desktop/src/default_icon.png → packages/desktop/src/assets/default_icon.png


+ 5 - 5
packages/desktop/src/cfg.rs

@@ -12,14 +12,14 @@ use wry::{
 pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView);
 
 pub struct DesktopConfig {
-    pub window: WindowBuilder,
-    pub file_drop_handler: Option<Box<dyn Fn(&Window, FileDropEvent) -> bool>>,
-    pub protocols: Vec<WryProtocol>,
+    pub(crate) window: WindowBuilder,
+    pub(crate) file_drop_handler: Option<Box<dyn Fn(&Window, FileDropEvent) -> bool>>,
+    pub(crate) protocols: Vec<WryProtocol>,
     pub(crate) pre_rendered: Option<String>,
     pub(crate) event_handler: Option<Box<DynEventHandlerFn>>,
 }
 
-pub type WryProtocol = (
+pub(crate) type WryProtocol = (
     String,
     Box<dyn Fn(&HttpRequest) -> WryResult<HttpResponse> + 'static>,
 );
@@ -88,7 +88,7 @@ impl DesktopConfig {
 
 impl DesktopConfig {
     pub(crate) fn with_default_icon(mut self) -> Self {
-        let bin: &[u8] = include_bytes!("default_icon.bin");
+        let bin: &[u8] = include_bytes!("./assets/default_icon.bin");
         let rgba = Icon::from_rgba(bin.to_owned(), 460, 460).expect("image parse failed");
         self.window.window.window_icon = Some(rgba);
         self

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

@@ -0,0 +1,104 @@
+use crate::desktop_context::DesktopContext;
+use crate::user_window_events::UserWindowEvent;
+use dioxus_core::*;
+use std::{
+    collections::{HashMap, VecDeque},
+    sync::atomic::AtomicBool,
+    sync::{Arc, RwLock},
+};
+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) sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
+    pub(super) pending_edits: Arc<RwLock<VecDeque<String>>>,
+    pub(super) quit_app_on_close: bool,
+    pub(super) is_ready: Arc<AtomicBool>,
+}
+
+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(RwLock::new(VecDeque::new()));
+        let pending_edits = edit_queue.clone();
+
+        let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
+        let return_sender = sender.clone();
+
+        let desktop_context_proxy = proxy.clone();
+        std::thread::spawn(move || {
+            // We create the runtime as multithreaded, so you can still "spawn" onto multiple threads
+            let runtime = tokio::runtime::Builder::new_multi_thread()
+                .enable_all()
+                .build()
+                .unwrap();
+
+            runtime.block_on(async move {
+                let mut dom =
+                    VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver));
+
+                let window_context = DesktopContext::new(desktop_context_proxy);
+
+                dom.base_scope().provide_context(window_context);
+
+                let edits = dom.rebuild();
+
+                edit_queue
+                    .write()
+                    .unwrap()
+                    .push_front(serde_json::to_string(&edits.edits).unwrap());
+
+                loop {
+                    dom.wait_for_work().await;
+                    let mut muts = dom.work_with_deadline(|| false);
+
+                    while let Some(edit) = muts.pop() {
+                        edit_queue
+                            .write()
+                            .unwrap()
+                            .push_front(serde_json::to_string(&edit.edits).unwrap());
+                    }
+
+                    let _ = proxy.send_event(UserWindowEvent::Update);
+                }
+            })
+        });
+
+        Self {
+            pending_edits,
+            sender: return_sender,
+            webviews: HashMap::new(),
+            is_ready: Arc::new(AtomicBool::new(false)),
+            quit_app_on_close: true,
+        }
+    }
+
+    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 queue = self.pending_edits.write().unwrap();
+            let (_id, view) = self.webviews.iter_mut().next().unwrap();
+
+            while let Some(edit) = queue.pop_back() {
+                view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
+                    .unwrap();
+            }
+        }
+    }
+}

+ 25 - 22
packages/desktop/src/desktop_context.rs

@@ -3,7 +3,8 @@ use std::rc::Rc;
 use dioxus_core::ScopeState;
 use wry::application::event_loop::EventLoopProxy;
 
-use crate::UserWindowEvent;
+use crate::user_window_events::UserWindowEvent;
+use UserWindowEvent::*;
 
 type ProxyType = EventLoopProxy<UserWindowEvent>;
 
@@ -35,75 +36,77 @@ impl DesktopContext {
     /// onmousedown: move |_| { desktop.drag_window(); }
     /// ```
     pub fn drag(&self) {
-        let _ = self.proxy.send_event(UserWindowEvent::DragWindow);
+        let _ = self.proxy.send_event(DragWindow);
     }
 
     /// set window minimize state
     pub fn set_minimized(&self, minimized: bool) {
-        let _ = self.proxy.send_event(UserWindowEvent::Minimize(minimized));
+        let _ = self.proxy.send_event(Minimize(minimized));
     }
 
     /// set window maximize state
     pub fn set_maximized(&self, maximized: bool) {
-        let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized));
+        let _ = self.proxy.send_event(Maximize(maximized));
+    }
+
+    /// toggle window maximize state
+    pub fn toggle_maximized(&self) {
+        let _ = self.proxy.send_event(MaximizeToggle);
     }
 
     /// set window visible or not
     pub fn set_visible(&self, visible: bool) {
-        let _ = self.proxy.send_event(UserWindowEvent::Visible(visible));
+        let _ = self.proxy.send_event(Visible(visible));
     }
 
     /// close window
     pub fn close(&self) {
-        let _ = self.proxy.send_event(UserWindowEvent::CloseWindow);
+        let _ = self.proxy.send_event(CloseWindow);
     }
 
     /// set window to focus
     pub fn focus(&self) {
-        let _ = self.proxy.send_event(UserWindowEvent::FocusWindow);
+        let _ = self.proxy.send_event(FocusWindow);
     }
 
     /// change window to fullscreen
     pub fn set_fullscreen(&self, fullscreen: bool) {
-        let _ = self
-            .proxy
-            .send_event(UserWindowEvent::Fullscreen(fullscreen));
+        let _ = self.proxy.send_event(Fullscreen(fullscreen));
     }
 
     /// set resizable state
     pub fn set_resizable(&self, resizable: bool) {
-        let _ = self.proxy.send_event(UserWindowEvent::Resizable(resizable));
+        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(UserWindowEvent::AlwaysOnTop(top));
+        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(UserWindowEvent::CursorVisible(visible));
+        let _ = self.proxy.send_event(CursorVisible(visible));
     }
 
     // set cursor grab
     pub fn set_cursor_grab(&self, grab: bool) {
-        let _ = self.proxy.send_event(UserWindowEvent::CursorGrab(grab));
+        let _ = self.proxy.send_event(CursorGrab(grab));
     }
 
     /// set window title
     pub fn set_title(&self, title: &str) {
-        let _ = self
-            .proxy
-            .send_event(UserWindowEvent::SetTitle(String::from(title)));
+        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(UserWindowEvent::SetDecorations(decoration));
+        let _ = self.proxy.send_event(SetDecorations(decoration));
+    }
+
+    /// opens DevTool window
+    pub fn devtool(&self) {
+        let _ = self.proxy.send_event(DevTool);
     }
 }
 

+ 30 - 9
packages/desktop/src/events.rs

@@ -1,5 +1,4 @@
-//! Convert a serialized event to an event Trigger
-//!
+//! Convert a serialized event to an event trigger
 
 use std::any::Any;
 use std::sync::Arc;
@@ -7,27 +6,49 @@ use std::sync::Arc;
 use dioxus_core::{ElementId, EventPriority, UserEvent};
 use dioxus_html::on::*;
 
+#[derive(serde::Serialize, serde::Deserialize)]
+pub(crate) struct IpcMessage {
+    method: String,
+    params: serde_json::Value,
+}
+
+impl IpcMessage {
+    pub(crate) fn method(&self) -> &str {
+        self.method.as_str()
+    }
+
+    pub(crate) fn params(self) -> serde_json::Value {
+        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
+        }
+    }
+}
+
 #[derive(serde::Serialize, serde::Deserialize)]
 struct ImEvent {
     event: String,
     mounted_dom_id: u64,
-    // scope: u64,
     contents: serde_json::Value,
 }
 
 pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
-    let ims: Vec<ImEvent> = serde_json::from_value(val).unwrap();
-
     let ImEvent {
         event,
         mounted_dom_id,
         contents,
-    } = ims.into_iter().next().unwrap();
+    } = serde_json::from_value(val).unwrap();
 
-    // let scope_id = ScopeId(scope as usize);
     let mounted_dom_id = Some(ElementId(mounted_dom_id as usize));
 
-    let name = event_name_from_typ(&event);
+    let name = event_name_from_type(&event);
     let event = make_synthetic_event(&event, contents);
 
     UserEvent {
@@ -105,7 +126,7 @@ fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc<dyn Any + Sen
     }
 }
 
-fn event_name_from_typ(typ: &str) -> &'static str {
+fn event_name_from_type(typ: &str) -> &'static str {
     match typ {
         "copy" => "copy",
         "cut" => "cut",

+ 35 - 300
packages/desktop/src/lib.rs

@@ -3,31 +3,28 @@
 #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
 
 pub mod cfg;
+mod controller;
 pub mod desktop_context;
 pub mod escape;
 pub mod events;
+mod protocol;
+mod user_window_events;
 
 use cfg::DesktopConfig;
+use controller::DesktopController;
 pub use desktop_context::use_window;
-use desktop_context::DesktopContext;
 use dioxus_core::*;
-use std::{
-    collections::{HashMap, VecDeque},
-    sync::atomic::AtomicBool,
-    sync::{Arc, RwLock},
-};
+use events::parse_ipc_message;
 use tao::{
     event::{Event, StartCause, WindowEvent},
     event_loop::{ControlFlow, EventLoop},
-    window::{Window, WindowId},
+    window::Window,
 };
 pub use wry;
 pub use wry::application as tao;
-use wry::{
-    application::{event_loop::EventLoopProxy, window::Fullscreen},
-    webview::RpcRequest,
-    webview::{WebView, WebViewBuilder},
-};
+use wry::webview::WebViewBuilder;
+
+use crate::events::trigger_from_serialized;
 
 /// Launch the WebView and run the event loop.
 ///
@@ -132,23 +129,24 @@ pub fn launch_with_props<P: 'static + Send>(
                     .with_transparent(cfg.window.window.transparent)
                     .with_url("dioxus://index.html/")
                     .unwrap()
-                    .with_rpc_handler(move |_window: &Window, req: RpcRequest| {
-                        match req.method.as_str() {
-                            "user_event" => {
-                                let event = events::trigger_from_serialized(req.params.unwrap());
-                                log::trace!("User event: {:?}", event);
-                                sender.unbounded_send(SchedulerMsg::Event(event)).unwrap();
-                            }
-                            "initialize" => {
-                                is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
-                                let _ = proxy.send_event(UserWindowEvent::Update);
-                            }
-                            "browser_open" => {
-                                println!("browser_open");
-                                let data = req.params.unwrap();
-                                log::trace!("Open browser: {:?}", data);
-                                if let Some(arr) = data.as_array() {
-                                    if let Some(temp) = arr[0].as_object() {
+                    .with_ipc_handler(move |_window: &Window, payload: String| {
+                        parse_ipc_message(&payload)
+                            .map(|message| match message.method() {
+                                "user_event" => {
+                                    let event = trigger_from_serialized(message.params());
+                                    log::trace!("User event: {:?}", event);
+                                    sender.unbounded_send(SchedulerMsg::Event(event)).unwrap();
+                                }
+                                "initialize" => {
+                                    is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
+                                    let _ = proxy
+                                        .send_event(user_window_events::UserWindowEvent::Update);
+                                }
+                                "browser_open" => {
+                                    println!("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) {
@@ -157,55 +155,13 @@ pub fn launch_with_props<P: 'static + Send>(
                                         }
                                     }
                                 }
-                            }
-                            _ => {}
-                        }
-                        None
-                    })
-                    .with_custom_protocol(String::from("dioxus"), move |request| {
-                        // Any content that that uses the `dioxus://` scheme will be shuttled through this handler as a "special case"
-                        // For now, we only serve two pieces of content which get included as bytes into the final binary.
-                        let path = request.uri().replace("dioxus://", "");
-
-                        // all assets shouldbe called from index.html
-                        let trimmed = path.trim_start_matches("index.html/");
-
-                        if trimmed.is_empty() {
-                            wry::http::ResponseBuilder::new()
-                                .mimetype("text/html")
-                                .body(include_bytes!("./index.html").to_vec())
-                        } else if trimmed == "index.js" {
-                            wry::http::ResponseBuilder::new()
-                                .mimetype("text/javascript")
-                                .body(dioxus_interpreter_js::INTERPRTER_JS.as_bytes().to_vec())
-                        } else {
-                            // Read the file content from file path
-                            use std::fs::read;
-
-                            let path_buf = std::path::Path::new(trimmed).canonicalize()?;
-                            let cur_path = std::path::Path::new(".").canonicalize()?;
-
-                            if !path_buf.starts_with(cur_path) {
-                                return wry::http::ResponseBuilder::new()
-                                    .status(wry::http::status::StatusCode::FORBIDDEN)
-                                    .body(String::from("Forbidden").into_bytes());
-                            }
-
-                            if !path_buf.exists() {
-                                return wry::http::ResponseBuilder::new()
-                                    .status(wry::http::status::StatusCode::NOT_FOUND)
-                                    .body(String::from("Not Found").into_bytes());
-                            }
-
-                            let mime = mime_guess::from_path(&path_buf).first_or_octet_stream();
-
-                            // do not let path searching to go two layers beyond the caller level
-                            let data = read(path_buf)?;
-                            let meta = format!("{}", mime);
-
-                            wry::http::ResponseBuilder::new().mimetype(&meta).body(data)
-                        }
+                                _ => (),
+                            })
+                            .unwrap_or_else(|| {
+                                log::warn!("invalid IPC message received");
+                            });
                     })
+                    .with_custom_protocol(String::from("dioxus"), protocol::desktop_handler)
                     .with_file_drop_handler(move |window, evet| {
                         file_handler
                             .as_ref()
@@ -235,114 +191,8 @@ pub fn launch_with_props<P: 'static + Send>(
                 _ => {}
             },
 
-            Event::UserEvent(_evt) => {
-                //
-                match _evt {
-                    UserWindowEvent::Update => desktop.try_load_ready_webviews(),
-                    UserWindowEvent::DragWindow => {
-                        // this loop just run once, because dioxus-desktop is unsupport multi-window.
-                        for webview in desktop.webviews.values() {
-                            let window = webview.window();
-                            // start to drag the window.
-                            // if the drag_window have any err. we don't do anything.
-
-                            if window.fullscreen().is_some() {
-                                return;
-                            }
-
-                            let _ = window.drag_window();
-                        }
-                    }
-                    UserWindowEvent::CloseWindow => {
-                        // close window
-                        *control_flow = ControlFlow::Exit;
-                    }
-                    UserWindowEvent::Visible(state) => {
-                        for webview in desktop.webviews.values() {
-                            let window = webview.window();
-                            window.set_visible(state);
-                        }
-                    }
-                    UserWindowEvent::Minimize(state) => {
-                        // this loop just run once, because dioxus-desktop is unsupport multi-window.
-                        for webview in desktop.webviews.values() {
-                            let window = webview.window();
-                            // change window minimized state.
-                            window.set_minimized(state);
-                        }
-                    }
-                    UserWindowEvent::Maximize(state) => {
-                        // this loop just run once, because dioxus-desktop is unsupport multi-window.
-                        for webview in desktop.webviews.values() {
-                            let window = webview.window();
-                            // change window maximized state.
-                            window.set_maximized(state);
-                        }
-                    }
-                    UserWindowEvent::Fullscreen(state) => {
-                        for webview in desktop.webviews.values() {
-                            let window = webview.window();
-
-                            let current_monitor = window.current_monitor();
-
-                            if current_monitor.is_none() {
-                                return;
-                            }
-
-                            let fullscreen = if state {
-                                Some(Fullscreen::Borderless(current_monitor))
-                            } else {
-                                None
-                            };
-
-                            window.set_fullscreen(fullscreen);
-                        }
-                    }
-                    UserWindowEvent::FocusWindow => {
-                        for webview in desktop.webviews.values() {
-                            let window = webview.window();
-                            window.set_focus();
-                        }
-                    }
-                    UserWindowEvent::Resizable(state) => {
-                        for webview in desktop.webviews.values() {
-                            let window = webview.window();
-                            window.set_resizable(state);
-                        }
-                    }
-                    UserWindowEvent::AlwaysOnTop(state) => {
-                        for webview in desktop.webviews.values() {
-                            let window = webview.window();
-                            window.set_always_on_top(state);
-                        }
-                    }
-
-                    UserWindowEvent::CursorVisible(state) => {
-                        for webview in desktop.webviews.values() {
-                            let window = webview.window();
-                            window.set_cursor_visible(state);
-                        }
-                    }
-                    UserWindowEvent::CursorGrab(state) => {
-                        for webview in desktop.webviews.values() {
-                            let window = webview.window();
-                            let _ = window.set_cursor_grab(state);
-                        }
-                    }
-
-                    UserWindowEvent::SetTitle(content) => {
-                        for webview in desktop.webviews.values() {
-                            let window = webview.window();
-                            window.set_title(&content);
-                        }
-                    }
-                    UserWindowEvent::SetDecorations(state) => {
-                        for webview in desktop.webviews.values() {
-                            let window = webview.window();
-                            window.set_decorations(state);
-                        }
-                    }
-                }
+            Event::UserEvent(user_event) => {
+                user_window_events::handler(user_event, &mut desktop, control_flow)
             }
             Event::MainEventsCleared => {}
             Event::Resumed => {}
@@ -353,118 +203,3 @@ pub fn launch_with_props<P: 'static + Send>(
         }
     })
 }
-
-pub enum UserWindowEvent {
-    Update,
-    DragWindow,
-    CloseWindow,
-    FocusWindow,
-    Visible(bool),
-    Minimize(bool),
-    Maximize(bool),
-    Resizable(bool),
-    AlwaysOnTop(bool),
-    Fullscreen(bool),
-
-    CursorVisible(bool),
-    CursorGrab(bool),
-
-    SetTitle(String),
-    SetDecorations(bool),
-}
-
-pub struct DesktopController {
-    pub proxy: EventLoopProxy<UserWindowEvent>,
-    pub webviews: HashMap<WindowId, WebView>,
-    pub sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
-    pub pending_edits: Arc<RwLock<VecDeque<String>>>,
-    pub quit_app_on_close: bool,
-    pub is_ready: Arc<AtomicBool>,
-}
-
-impl DesktopController {
-    // Launch the virtualdom on its own thread managed by tokio
-    // returns the desktop state
-    pub fn new_on_tokio<P: Send + 'static>(
-        root: Component<P>,
-        props: P,
-        evt: EventLoopProxy<UserWindowEvent>,
-    ) -> Self {
-        let edit_queue = Arc::new(RwLock::new(VecDeque::new()));
-        let pending_edits = edit_queue.clone();
-
-        let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
-        let return_sender = sender.clone();
-        let proxy = evt.clone();
-
-        let desktop_context_proxy = proxy.clone();
-        std::thread::spawn(move || {
-            // We create the runtime as multithreaded, so you can still "spawn" onto multiple threads
-            let runtime = tokio::runtime::Builder::new_multi_thread()
-                .enable_all()
-                .build()
-                .unwrap();
-
-            runtime.block_on(async move {
-                let mut dom =
-                    VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver));
-
-                let window_context = DesktopContext::new(desktop_context_proxy);
-
-                dom.base_scope().provide_context(window_context);
-
-                let edits = dom.rebuild();
-
-                edit_queue
-                    .write()
-                    .unwrap()
-                    .push_front(serde_json::to_string(&edits.edits).unwrap());
-
-                loop {
-                    dom.wait_for_work().await;
-                    let mut muts = dom.work_with_deadline(|| false);
-
-                    while let Some(edit) = muts.pop() {
-                        edit_queue
-                            .write()
-                            .unwrap()
-                            .push_front(serde_json::to_string(&edit.edits).unwrap());
-                    }
-
-                    let _ = evt.send_event(UserWindowEvent::Update);
-                }
-            })
-        });
-
-        Self {
-            pending_edits,
-            sender: return_sender,
-            proxy,
-            webviews: HashMap::new(),
-            is_ready: Arc::new(AtomicBool::new(false)),
-            quit_app_on_close: true,
-        }
-    }
-
-    pub fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) {
-        self.webviews.remove(&window_id);
-
-        if self.webviews.is_empty() && self.quit_app_on_close {
-            *control_flow = ControlFlow::Exit;
-        }
-    }
-
-    pub fn try_load_ready_webviews(&mut self) {
-        if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) {
-            let mut queue = self.pending_edits.write().unwrap();
-            let (_id, view) = self.webviews.iter_mut().next().unwrap();
-
-            while let Some(edit) = queue.pop_back() {
-                view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
-                    .unwrap();
-            }
-        } else {
-            println!("waiting for ready");
-        }
-    }
-}

+ 47 - 0
packages/desktop/src/protocol.rs

@@ -0,0 +1,47 @@
+use std::path::Path;
+use wry::{
+    http::{status::StatusCode, Request, Response, ResponseBuilder},
+    Result,
+};
+
+pub(super) fn desktop_handler(request: &Request) -> Result<Response> {
+    // Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case".
+    // For now, we only serve two pieces of content which get included as bytes into the final binary.
+    let path = request.uri().replace("dioxus://", "");
+
+    // all assets should be called from index.html
+    let trimmed = path.trim_start_matches("index.html/");
+
+    if trimmed.is_empty() {
+        ResponseBuilder::new()
+            .mimetype("text/html")
+            .body(include_bytes!("./index.html").to_vec())
+    } else if trimmed == "index.js" {
+        ResponseBuilder::new()
+            .mimetype("text/javascript")
+            .body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec())
+    } else {
+        let path_buf = Path::new(trimmed).canonicalize()?;
+        let cur_path = Path::new(".").canonicalize()?;
+
+        if !path_buf.starts_with(cur_path) {
+            return ResponseBuilder::new()
+                .status(StatusCode::FORBIDDEN)
+                .body(String::from("Forbidden").into_bytes());
+        }
+
+        if !path_buf.exists() {
+            return ResponseBuilder::new()
+                .status(StatusCode::NOT_FOUND)
+                .body(String::from("Not Found").into_bytes());
+        }
+
+        let mime = mime_guess::from_path(&path_buf).first_or_octet_stream();
+
+        // do not let path searching to go two layers beyond the caller level
+        let data = std::fs::read(path_buf)?;
+        let meta = format!("{}", mime);
+
+        ResponseBuilder::new().mimetype(&meta).body(data)
+    }
+}

+ 72 - 0
packages/desktop/src/user_window_events.rs

@@ -0,0 +1,72 @@
+use wry::application::event_loop::ControlFlow;
+use wry::application::window::Fullscreen as WryFullscreen;
+
+use crate::controller::DesktopController;
+
+pub(crate) enum UserWindowEvent {
+    Update,
+
+    CloseWindow,
+    DragWindow,
+    FocusWindow,
+
+    Visible(bool),
+    Minimize(bool),
+    Maximize(bool),
+    MaximizeToggle,
+    Resizable(bool),
+    AlwaysOnTop(bool),
+    Fullscreen(bool),
+
+    CursorVisible(bool),
+    CursorGrab(bool),
+
+    SetTitle(String),
+    SetDecorations(bool),
+
+    DevTool,
+}
+
+use UserWindowEvent::*;
+
+pub(super) fn handler(
+    user_event: UserWindowEvent,
+    desktop: &mut DesktopController,
+    control_flow: &mut ControlFlow,
+) {
+    // currently dioxus-desktop supports a single window only,
+    // so we can grab the only webview from the map;
+    let webview = desktop.webviews.values().next().unwrap();
+    let window = webview.window();
+
+    match user_event {
+        Update => desktop.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(|| WryFullscreen::Borderless(Some(handle))));
+            }
+        }
+        FocusWindow => window.set_focus(),
+        Resizable(state) => window.set_resizable(state),
+        AlwaysOnTop(state) => window.set_always_on_top(state),
+
+        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),
+
+        DevTool => webview.devtool(),
+    }
+}

+ 2 - 0
packages/html/src/events.rs

@@ -486,6 +486,8 @@ pub mod on {
     #[derive(Debug)]
     pub struct FormData {
         pub value: String,
+
+        #[serde(default)]
         pub values: HashMap<String, String>,
         /* DOMEvent:  Send + SyncTarget relatedTarget */
     }

+ 9 - 5
packages/interpreter/src/interpreter.js

@@ -2,7 +2,7 @@ export function main() {
   let root = window.document.getElementById("main");
   if (root != null) {
     window.interpreter = new Interpreter(root);
-    window.rpc.call("initialize");
+    window.ipc.postMessage(serializeIpcMessage("initialize"))
   }
 }
 export class Interpreter {
@@ -105,7 +105,7 @@ export class Interpreter {
     if (ns === "style") {
       // @ts-ignore
       node.style[name] = value;
-    } else if (ns != null || ns !== undefined) {
+    } else if (ns != null || ns != undefined) {
       node.setAttributeNS(ns, name, value);
     } else {
       switch (name) {
@@ -207,7 +207,7 @@ export class Interpreter {
                   event.preventDefault();
                   const href = target.getAttribute("href");
                   if (href !== "" && href !== null && href !== undefined) {
-                    window.rpc.call("browser_open", { href });
+                    window.ipc.postMessage(serializeIpcMessage("browser_open", { href }))
                   }
                 }
               }
@@ -261,11 +261,12 @@ export class Interpreter {
             if (realId == null) {
               return;
             }
-            window.rpc.call("user_event", {
+            window.ipc.postMessage(serializeIpcMessage(
+              "user_event", {
               event: edit.event_name,
               mounted_dom_id: parseInt(realId),
               contents: contents,
-            });
+            }));
           }
         };
         this.NewEventListener(edit.event_name, edit.root, handler);
@@ -544,6 +545,9 @@ export function serialize_event(event) {
     }
   }
 }
+function serializeIpcMessage(method, params = {}) {
+  return JSON.stringify({ method, params });
+}
 const bool_attrs = {
   allowfullscreen: true,
   allowpaymentrequest: true,

+ 1 - 1
packages/interpreter/src/lib.rs

@@ -1,4 +1,4 @@
-pub static INTERPRTER_JS: &str = include_str!("./interpreter.js");
+pub static INTERPRETER_JS: &str = include_str!("./interpreter.js");
 
 #[cfg(feature = "web")]
 mod bindings;

+ 40 - 8
packages/router/src/components/link.rs

@@ -34,6 +34,17 @@ pub struct LinkProps<'a> {
     #[props(default, strip_option)]
     title: Option<&'a str>,
 
+    #[props(default = true)]
+    autodetect: bool,
+
+    /// Is this link an external link?
+    #[props(default = false)]
+    external: bool,
+
+    /// New tab?
+    #[props(default = false)]
+    new_tab: bool,
+
     children: Element<'a>,
 
     #[props(default)]
@@ -41,17 +52,38 @@ pub struct LinkProps<'a> {
 }
 
 pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
-    // log::trace!("render Link to {}", cx.props.to);
     if let Some(service) = cx.consume_context::<RouterService>() {
+        let LinkProps {
+            to,
+            href,
+            class,
+            id,
+            title,
+            autodetect,
+            external,
+            new_tab,
+            children,
+            ..
+        } = cx.props;
+
+        let is_http = to.starts_with("http") || to.starts_with("https");
+        let outerlink = (*autodetect && is_http) || *external;
+
+        let prevent_default = if outerlink { "" } else { "onclick" };
+
         return cx.render(rsx! {
             a {
-                href: "{cx.props.to}",
-                class: format_args!("{}", cx.props.class.unwrap_or("")),
-                id: format_args!("{}", cx.props.id.unwrap_or("")),
-                title: format_args!("{}", cx.props.title.unwrap_or("")),
-
-                prevent_default: "onclick",
-                onclick: move |_| service.push_route(cx.props.to),
+                href: "{to}",
+                class: format_args!("{}", class.unwrap_or("")),
+                id: format_args!("{}", id.unwrap_or("")),
+                title: format_args!("{}", title.unwrap_or("")),
+                prevent_default: "{prevent_default}",
+                target: format_args!("{}", if *new_tab { "_blank" } else { "" }),
+                onclick: move |_| {
+                    if !outerlink {
+                        service.push_route(to);
+                    }
+                },
 
                 &cx.props.children
             }

+ 1 - 1
packages/web/Cargo.toml

@@ -23,13 +23,13 @@ wasm-logger = "0.2.0"
 console_error_panic_hook = { version = "0.1.7", optional = true }
 wasm-bindgen-test = "0.3.29"
 once_cell = "1.9.0"
-async-channel = "1.6.1"
 anyhow = "1.0.53"
 gloo-timers = { version = "0.2.3", features = ["futures"] }
 futures-util = "0.3.19"
 smallstr = "0.2.0"
 dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0", features = ["web"] }
 serde-wasm-bindgen = "0.4.2"
+futures-channel = "0.3.21"
 
 [dependencies.web-sys]
 version = "0.3.56"

+ 2 - 5
packages/web/src/dom.rs

@@ -87,14 +87,11 @@ impl WebsysDom {
             }
         });
 
+        // a match here in order to avoid some error during runtime browser test
         let document = load_document();
         let root = match document.get_element_by_id(&cfg.rootname) {
             Some(root) => root,
-            // a match here in order to avoid some error during runtime browser test
-            None => {
-                let body = document.create_element("body").ok().unwrap();
-                body
-            }
+            None => document.create_element("body").ok().unwrap(),
         };
 
         Self {

+ 1 - 1
packages/web/src/lib.rs

@@ -211,7 +211,7 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
         websys_dom.apply_edits(edits.edits);
     }
 
-    let work_loop = ric_raf::RafLoop::new();
+    let mut work_loop = ric_raf::RafLoop::new();
 
     loop {
         log::trace!("waiting for work");

+ 11 - 10
packages/web/src/ric_raf.rs

@@ -7,6 +7,7 @@
 //! Because RIC doesn't work on Safari, we polyfill using the "ricpolyfill.js" file and use some basic detection to see
 //! if RIC is available.
 
+use futures_util::StreamExt;
 use gloo_timers::future::TimeoutFuture;
 use js_sys::Function;
 use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
@@ -14,21 +15,21 @@ use web_sys::{window, Window};
 
 pub(crate) struct RafLoop {
     window: Window,
-    ric_receiver: async_channel::Receiver<u32>,
-    raf_receiver: async_channel::Receiver<()>,
+    ric_receiver: futures_channel::mpsc::UnboundedReceiver<u32>,
+    raf_receiver: futures_channel::mpsc::UnboundedReceiver<()>,
     ric_closure: Closure<dyn Fn(JsValue)>,
     raf_closure: Closure<dyn Fn(JsValue)>,
 }
 
 impl RafLoop {
     pub fn new() -> Self {
-        let (raf_sender, raf_receiver) = async_channel::unbounded();
+        let (raf_sender, raf_receiver) = futures_channel::mpsc::unbounded();
 
         let raf_closure: Closure<dyn Fn(JsValue)> = Closure::wrap(Box::new(move |_v: JsValue| {
-            raf_sender.try_send(()).unwrap()
+            raf_sender.unbounded_send(()).unwrap()
         }));
 
-        let (ric_sender, ric_receiver) = async_channel::unbounded();
+        let (ric_sender, ric_receiver) = futures_channel::mpsc::unbounded();
 
         let has_idle_callback = {
             let bo = window().unwrap().dyn_into::<js_sys::Object>().unwrap();
@@ -45,7 +46,7 @@ impl RafLoop {
                 10
             };
 
-            ric_sender.try_send(time_remaining).unwrap()
+            ric_sender.unbounded_send(time_remaining).unwrap()
         }));
 
         // execute the polyfill for safari
@@ -64,16 +65,16 @@ impl RafLoop {
         }
     }
     /// waits for some idle time and returns a timeout future that expires after the idle time has passed
-    pub async fn wait_for_idle_time(&self) -> TimeoutFuture {
+    pub async fn wait_for_idle_time(&mut self) -> TimeoutFuture {
         let ric_fn = self.ric_closure.as_ref().dyn_ref::<Function>().unwrap();
         let _cb_id: u32 = self.window.request_idle_callback(ric_fn).unwrap();
-        let deadline = self.ric_receiver.recv().await.unwrap();
+        let deadline = self.ric_receiver.next().await.unwrap();
         TimeoutFuture::new(deadline)
     }
 
-    pub async fn wait_for_raf(&self) {
+    pub async fn wait_for_raf(&mut self) {
         let raf_fn = self.raf_closure.as_ref().dyn_ref::<Function>().unwrap();
         let _id: i32 = self.window.request_animation_frame(raf_fn).unwrap();
-        self.raf_receiver.recv().await.unwrap();
+        self.raf_receiver.next().await.unwrap();
     }
 }