diff --git a/Cargo.toml b/Cargo.toml index 5e3a0a032..3f0d50187 100644 --- a/Cargo.toml +++ b/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"] } diff --git a/docs/guide/src/interactivity/hooks.md b/docs/guide/src/interactivity/hooks.md index a90afe2dc..a84a0b129 100644 --- a/docs/guide/src/interactivity/hooks.md +++ b/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(); diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 03b378e88..4126eb47f 100644 --- a/packages/core/Cargo.toml +++ b/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" diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 0d7c13e3f..2622355eb 100644 --- a/packages/desktop/Cargo.toml +++ b/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" } diff --git a/packages/desktop/src/default_icon.bin b/packages/desktop/src/assets/default_icon.bin similarity index 100% rename from packages/desktop/src/default_icon.bin rename to packages/desktop/src/assets/default_icon.bin diff --git a/packages/desktop/src/default_icon.png b/packages/desktop/src/assets/default_icon.png similarity index 100% rename from packages/desktop/src/default_icon.png rename to packages/desktop/src/assets/default_icon.png diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 49a26ae78..c5d6eef50 100644 --- a/packages/desktop/src/cfg.rs +++ b/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 bool>>, - pub protocols: Vec, + pub(crate) window: WindowBuilder, + pub(crate) file_drop_handler: Option bool>>, + pub(crate) protocols: Vec, pub(crate) pre_rendered: Option, pub(crate) event_handler: Option>, } -pub type WryProtocol = ( +pub(crate) type WryProtocol = ( String, Box WryResult + '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 diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs new file mode 100644 index 000000000..8497a42e9 --- /dev/null +++ b/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, + pub(super) sender: futures_channel::mpsc::UnboundedSender, + pub(super) pending_edits: Arc>>, + pub(super) quit_app_on_close: bool, + pub(super) is_ready: Arc, +} + +impl DesktopController { + // Launch the virtualdom on its own thread managed by tokio + // returns the desktop state + pub(super) fn new_on_tokio( + root: Component

, + props: P, + proxy: EventLoopProxy, + ) -> Self { + let edit_queue = Arc::new(RwLock::new(VecDeque::new())); + let pending_edits = edit_queue.clone(); + + let (sender, receiver) = futures_channel::mpsc::unbounded::(); + 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(); + } + } + } +} diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index d180e1235..e1b726d73 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/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; @@ -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); } } diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 760bf7662..7be4273e9 100644 --- a/packages/desktop/src/events.rs +++ b/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 { + 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 = 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 &'static str { +fn event_name_from_type(typ: &str) -> &'static str { match typ { "copy" => "copy", "cut" => "cut", diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index f4e28ca2a..371adced5 100644 --- a/packages/desktop/src/lib.rs +++ b/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( .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( } } } - } - _ => {} - } - 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( _ => {} }, - 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( } }) } - -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, - pub webviews: HashMap, - pub sender: futures_channel::mpsc::UnboundedSender, - pub pending_edits: Arc>>, - pub quit_app_on_close: bool, - pub is_ready: Arc, -} - -impl DesktopController { - // Launch the virtualdom on its own thread managed by tokio - // returns the desktop state - pub fn new_on_tokio( - root: Component

, - props: P, - evt: EventLoopProxy, - ) -> Self { - let edit_queue = Arc::new(RwLock::new(VecDeque::new())); - let pending_edits = edit_queue.clone(); - - let (sender, receiver) = futures_channel::mpsc::unbounded::(); - let return_sender = sender.clone(); - let proxy = evt.clone(); - - 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"); - } - } -} diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs new file mode 100644 index 000000000..c8f007c1d --- /dev/null +++ b/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 { + // 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) + } +} diff --git a/packages/desktop/src/user_window_events.rs b/packages/desktop/src/user_window_events.rs new file mode 100644 index 000000000..aab14561b --- /dev/null +++ b/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(), + } +} diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 8067e6314..4336d2476 100644 --- a/packages/html/src/events.rs +++ b/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, /* DOMEvent: Send + SyncTarget relatedTarget */ } diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index f0acd0802..b1271b303 100644 --- a/packages/interpreter/src/interpreter.js +++ b/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, diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index ebca3cbf4..076a89197 100644 --- a/packages/interpreter/src/lib.rs +++ b/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; diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 18e9fd92e..b2b69108f 100644 --- a/packages/router/src/components/link.rs +++ b/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::() { + 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 } diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 7a3baa955..31a7ff6be 100644 --- a/packages/web/Cargo.toml +++ b/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" diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 48fb25e6e..b2eb65d08 100644 --- a/packages/web/src/dom.rs +++ b/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 { diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index cef1bc14c..90fe84d04 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -211,7 +211,7 @@ pub async fn run_with_props(root: Component, 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"); diff --git a/packages/web/src/ric_raf.rs b/packages/web/src/ric_raf.rs index 2ac0668b9..89819fff0 100644 --- a/packages/web/src/ric_raf.rs +++ b/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, - raf_receiver: async_channel::Receiver<()>, + ric_receiver: futures_channel::mpsc::UnboundedReceiver, + raf_receiver: futures_channel::mpsc::UnboundedReceiver<()>, ric_closure: Closure, raf_closure: Closure, } 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 = 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::().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::().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::().unwrap(); let _id: i32 = self.window.request_animation_frame(raf_fn).unwrap(); - self.raf_receiver.recv().await.unwrap(); + self.raf_receiver.next().await.unwrap(); } }