diff --git a/Cargo.toml b/Cargo.toml index fc6999c29..44d265e0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,12 @@ 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" @@ -60,15 +66,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/README.md b/README.md index 02857ed18..23cd9c516 100644 --- a/README.md +++ b/README.md @@ -101,10 +101,10 @@ cargo run --example EXAMPLE - - - - + + + +
TutorialWebDesktopSSRMobileWebDesktopSSRMobile State
diff --git a/docs/guide/src/README.md b/docs/guide/src/README.md index bbbd9f85f..da6c0156f 100644 --- a/docs/guide/src/README.md +++ b/docs/guide/src/README.md @@ -18,7 +18,7 @@ fn app(cx: Scope) -> Element { In general, Dioxus and React share many functional similarities. If this guide is lacking in any general concept or an error message is confusing, React's documentation might be more helpful. We are dedicated to providing a *familiar* toolkit for UI in Rust, so we've chosen to follow in the footsteps of popular UI frameworks (React, Redux, etc). If you know React, then you already know Dioxus. If you don't know either, this guide will still help you! -> This is an introduction book! For advanced topics, check out the [Reference](https://dioxuslabs.com/reference) instead. +> This is an introduction book! For advanced topics, check out the [Reference](/reference) instead. ## Multiplatform @@ -37,7 +37,7 @@ The Web is the best-supported target platform for Dioxus. To run on the Web, you Because the web is a fairly mature platform, we expect there to be very little API churn for web-based features. -[Jump to the getting started guide for the web.]() +[Jump to the getting started guide for the web.](/reference/platforms/web) Examples: - [TodoMVC](https://github.com/DioxusLabs/example-projects/tree/master/todomvc) @@ -55,7 +55,7 @@ For rendering statically to an `.html` file or from a WebServer, then you'll wan let contents = dioxus::ssr::render_vdom(&dom); ``` -[Jump to the getting started guide for SSR.]() +[Jump to the getting started guide for SSR.](/reference/platforms/ssr) Examples: - [Example DocSite](https://github.com/dioxusLabs/docsite) @@ -68,13 +68,13 @@ The desktop is a powerful target for Dioxus, but is currently limited in capabil Desktop APIs will likely be in flux as we figure out better patterns than our ElectronJS counterpart. -[Jump to the getting started guide for Desktop.]() +[Jump to the getting started guide for Desktop.](/reference/platforms/desktop) Examples: - [File explorer](https://github.com/dioxusLabs/file-explorer/) - [WiFi scanner](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) -[![File ExplorerExample](https://github.com/DioxusLabs/file-explorer-example/raw/master/image.png)](https://github.com/dioxusLabs/file-explorer/) +[![File ExplorerExample](https://raw.githubusercontent.com/DioxusLabs/example-projects/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/tree/master/file-explorer) ### Mobile Support --- @@ -82,7 +82,7 @@ Mobile is currently the least-supported renderer target for Dioxus. Mobile apps Mobile support is currently best suited for CRUD-style apps, ideally for internal teams who need to develop quickly but don't care much about animations or native widgets. -[Jump to the getting started guide for Mobile.]() +[Jump to the getting started guide for Mobile.](/reference/platforms/mobile) Examples: - [Todo App](https://github.com/DioxusLabs/example-projects/blob/master/ios_demo) 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/docs/guide/src/setup.md b/docs/guide/src/setup.md index 1dc9434c5..75cfc6606 100644 --- a/docs/guide/src/setup.md +++ b/docs/guide/src/setup.md @@ -8,7 +8,7 @@ We'll learn about: - Suggested cargo extensions -For platform-specific guides, check out the [Platform Specific Guides](../platforms/00-index.md). +For platform-specific guides, check out the [Platform Specific Guides](/reference/platforms/index.md). # Setting up Dioxus diff --git a/docs/reference/src/SUMMARY.md b/docs/reference/src/SUMMARY.md index 3feb9fe36..18f351bfa 100644 --- a/docs/reference/src/SUMMARY.md +++ b/docs/reference/src/SUMMARY.md @@ -2,11 +2,12 @@ - [Introduction](README.md) -- [Web](web/index.md) -- [Desktop](desktop/index.md) -- [Mobile](mobile/index.md) -- [SSR](ssr/index.md) -- [TUI](tui/index.md) +- [Platforms](platforms/index.md) + - [Web](platforms/web.md) + - [Server Side Rendering](platforms/ssr.md) + - [Desktop](platforms/desktop.md) + - [Mobile](platforms/mobile.md) + - [TUI](platforms/tui.md) - [Advanced Guides](guide/index.md) - [RSX in Depth](guide/rsx_in_depth.md) diff --git a/docs/reference/src/desktop/index.md b/docs/reference/src/platforms/desktop.md similarity index 98% rename from docs/reference/src/desktop/index.md rename to docs/reference/src/platforms/desktop.md index 8011f1eed..18393f132 100644 --- a/docs/reference/src/desktop/index.md +++ b/docs/reference/src/platforms/desktop.md @@ -1,4 +1,4 @@ -# Desktop +# Getting Started: Desktop One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory. diff --git a/docs/reference/src/platforms/index.md b/docs/reference/src/platforms/index.md new file mode 100644 index 000000000..fbbabd35d --- /dev/null +++ b/docs/reference/src/platforms/index.md @@ -0,0 +1,10 @@ +# Platforms + +Dioxus supports many different platforms. Below are a list of guides that walk you through setting up Dioxus for a specific platform. + +### Setup Guides +- [Web](web.md) +- [Server Side Rendering](ssr.md) +- [Desktop](desktop.md) +- [Mobile](mobile.md) +- [TUI](tui.md) \ No newline at end of file diff --git a/docs/reference/src/mobile/index.md b/docs/reference/src/platforms/mobile.md similarity index 99% rename from docs/reference/src/mobile/index.md rename to docs/reference/src/platforms/mobile.md index 2e8344ed1..5a41ffe60 100644 --- a/docs/reference/src/mobile/index.md +++ b/docs/reference/src/platforms/mobile.md @@ -21,7 +21,7 @@ $ cargo install --git https://github.com/BrainiumLLC/cargo-mobile And then initialize your app for the right platform. Use the `winit` template for now. Right now, there's no "Dioxus" template in cargo-mobile. ```shell -$ cargo moble init +$ cargo mobile init ``` We're going to completely clear out the `dependencies` it generates for us, swapping out `winit` with `dioxus-mobile`. diff --git a/docs/reference/src/ssr/index.md b/docs/reference/src/platforms/ssr.md similarity index 100% rename from docs/reference/src/ssr/index.md rename to docs/reference/src/platforms/ssr.md diff --git a/docs/reference/src/tui/index.md b/docs/reference/src/platforms/tui.md similarity index 98% rename from docs/reference/src/tui/index.md rename to docs/reference/src/platforms/tui.md index 3947883d4..e646bd4ba 100644 --- a/docs/reference/src/tui/index.md +++ b/docs/reference/src/platforms/tui.md @@ -1,4 +1,4 @@ -# TUI +# Getting Started: TUI TUI support is currently quite experimental. Even the project name will change. But, if you're willing to venture into the realm of the unknown, this guide will get you started. diff --git a/docs/reference/src/web/index.md b/docs/reference/src/platforms/web.md similarity index 98% rename from docs/reference/src/web/index.md rename to docs/reference/src/platforms/web.md index 062cba177..2ff5f621e 100644 --- a/docs/reference/src/web/index.md +++ b/docs/reference/src/platforms/web.md @@ -1,4 +1,4 @@ -# Getting Started: Dioxus for Web +# Getting Started: Web [*"Pack your things, we're going on an adventure!"*](https://trunkrs.dev) diff --git a/examples/login_form.rs b/examples/login_form.rs new file mode 100644 index 000000000..7fd91262d --- /dev/null +++ b/examples/login_form.rs @@ -0,0 +1,48 @@ +//! This example demonstrates the following: +//! Futures in a callback, Router, and Forms + +use dioxus::events::*; +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let onsubmit = move |evt: FormEvent| { + cx.spawn(async move { + let resp = reqwest::Client::new() + .post("http://localhost/login") + .form(&[ + ("username", &evt.values["username"]), + ("password", &evt.values["password"]), + ]) + .send() + .await; + + match resp { + // Parse data from here, such as storing a response token + Ok(_data) => println!("Login successful"), + + //Handle any errors from the fetch here + Err(_err) => println!("Login failed"), + } + }); + }; + + cx.render(rsx! { + h1 { "Login" } + form { + onsubmit: onsubmit, + prevent_default: "onsubmit", // Prevent the default behavior of
to post + + input { "type": "text" } + label { "Username" } + br {} + input { "type": "password" } + label { "Password" } + br {} + button { "Login" } + } + }) +} diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index b5514ad5a..94f8e6e87 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -16,7 +16,6 @@ proc-macro = true [dependencies] dioxus-macro-inner = { path = "../macro-inner" } -once_cell = "1.8" proc-macro-error = "1.0.4" proc-macro2 = { version = "1.0.6" } quote = "1.0" 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..6fd277ba4 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -13,21 +13,18 @@ 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" -html-escape = "0.2.9" -wry = "0.12.2" -futures-channel = "0.3" -tokio = { version = "1.12.0", features = [ +serde = "1.0.136" +serde_json = "1.0.79" +thiserror = "1.0.30" +log = "0.4.14" +wry = { version = "0.13.1" } +futures-channel = "0.3.21" +tokio = { version = "1.16.1", features = [ "sync", "rt-multi-thread", "rt", "time", ], optional = true, default-features = false } -dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" } webbrowser = "0.5.5" mime_guess = "2.0.3" @@ -38,7 +35,15 @@ 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-core-macro = { path = "../core-macro", version = "^0.1.7" } dioxus-hooks = { path = "../hooks" } # image = "0.24.0" # enable this when generating a new desktop image 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/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index f0acd0802..76da73752 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,9 @@ 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 +263,13 @@ export class Interpreter { if (realId == null) { return; } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - }); + 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); @@ -341,6 +345,7 @@ export function serialize_event(event) { } return { value: value, + values: {}, }; } case "input": @@ -544,6 +549,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/Cargo.toml b/packages/router/Cargo.toml index 239a3c5d8..221372b50 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -16,8 +16,8 @@ dioxus-html = { path = "../html", version = "^0.1.6", default-features = false } dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } serde = "1" -url = "2.2.2" serde_urlencoded = "0.7" +# url = "2.2.2" # for wasm web-sys = { version = "0.3", features = [ 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/router/src/components/router.rs b/packages/router/src/components/router.rs index bf23645f5..c117c6650 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -32,7 +32,5 @@ pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { cx.props.onchange.call(path.to_string()); } - cx.render(rsx!( - div { &cx.props.children } - )) + cx.render(rsx!(&cx.props.children)) } diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 31a157a8f..4de6dff12 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -106,7 +106,7 @@ impl Drop for UseRouteListener { } /// This hook provides access to the `RouterService` for the app. -pub fn use_router(cx: &ScopeState) -> &RouterService { +pub fn use_router(cx: &ScopeState) -> &Rc { cx.use_hook(|_| { cx.consume_context::() .expect("Cannot call use_route outside the scope of a Router component") diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 7a3baa955..333838398 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -15,21 +15,18 @@ dioxus-core = { path = "../core", version = "^0.1.9" } dioxus-html = { path = "../html", version = "^0.1.6" } js-sys = "0.3.56" wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] } -lazy_static = "1.4.0" wasm-bindgen-futures = "0.4.29" log = { version = "0.4.14", features = ["release_max_level_off"] } fxhash = "0.2.1" -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" @@ -80,3 +77,5 @@ panic_hook = ["console_error_panic_hook"] dioxus-core-macro = { path = "../core-macro" } wasm-bindgen-test = "0.3.29" dioxus-ssr = { path = "../ssr" } +wasm-logger = "0.2.0" + 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(); } }