diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 5506b5f03..3b95aec76 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -225,8 +225,8 @@ impl VirtualDom { /// If there are pending tasks, they will be progressed before returning. This is useful when rendering an application /// that has suspended nodes or suspended tasks. Be warned - any async tasks running forever will prevent this method /// from completing. Consider using `run` and specifing a deadline. - pub async fn run_unbounded<'s>(&'s mut self) -> Result> { - self.run_with_deadline(async {}).await + pub async fn run_unbounded<'s>(&'s mut self) -> Mutations<'s> { + self.run_with_deadline(async {}).await.unwrap() } /// Run the virtualdom with a deadline. @@ -275,7 +275,7 @@ impl VirtualDom { pub async fn run_with_deadline<'s>( &'s mut self, deadline: impl Future, - ) -> Result> { + ) -> Option> { let mut committed_mutations = Mutations::new(); let mut deadline = Box::pin(deadline.fuse()); @@ -293,12 +293,12 @@ impl VirtualDom { let deadline_expired = self.scheduler.wait_for_any_trigger(&mut deadline).await; if deadline_expired { - return Ok(committed_mutations); + return Some(committed_mutations); } } // Create work from the pending event queue - self.scheduler.consume_pending_events()?; + self.scheduler.consume_pending_events(); // Work through the current subtree, and commit the results when it finishes // When the deadline expires, give back the work @@ -316,10 +316,10 @@ impl VirtualDom { */ if !self.scheduler.has_any_work() { - return Ok(committed_mutations); + return Some(committed_mutations); } } - FiberResult::Interrupted => return Ok(committed_mutations), + FiberResult::Interrupted => return None, } } } @@ -327,6 +327,12 @@ impl VirtualDom { pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { self.shared.ui_event_sender.clone() } + + pub fn has_work(&self) -> bool { + true + } + + pub async fn wait_for_any_work(&self) {} } // TODO! diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index aa482b294..9b840d74c 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -56,20 +56,16 @@ use std::rc::Rc; pub use crate::cfg::WebConfig; use crate::dom::load_document; -use dioxus::prelude::{Context, Properties, VNode}; +use dioxus::prelude::Properties; use dioxus::virtual_dom::VirtualDom; pub use dioxus_core as dioxus; -use dioxus_core::error::Result; -use dioxus_core::{events::EventTrigger, prelude::FC}; -use futures_util::{pin_mut, Stream, StreamExt}; -use fxhash::FxHashMap; -use js_sys::Iterator; -use web_sys::{window, Document, Element, Event, Node, NodeList}; +use dioxus_core::prelude::FC; mod cache; mod cfg; mod dom; mod nodeslab; +mod ric_raf; /// Launches the VirtualDOM from the specified component function. /// @@ -100,15 +96,8 @@ where { let config = config(WebConfig::default()); let fut = run_with_props(root, root_props, config); - - wasm_bindgen_futures::spawn_local(async { - match fut.await { - Ok(_) => log::error!("Your app completed running... somehow?"), - Err(e) => log::error!("Your app crashed! {}", e), - } - }); + wasm_bindgen_futures::spawn_local(fut); } - /// This method is the primary entrypoint for Websys Dioxus apps. Will panic if an error occurs while rendering. /// See DioxusErrors for more information on how these errors could occour. /// @@ -122,11 +111,7 @@ where /// /// Run the app to completion, panicing if any error occurs while rendering. /// Pairs well with the wasm_bindgen async handler -pub async fn run_with_props( - root: FC, - root_props: T, - cfg: WebConfig, -) -> Result<()> { +pub async fn run_with_props(root: FC, root_props: T, cfg: WebConfig) { let mut dom = VirtualDom::new_with_props(root, root_props); let hydrating = cfg.hydrate; @@ -138,7 +123,6 @@ pub async fn run_with_props( let mut websys_dom = dom::WebsysDom::new(root_el, cfg, sender_callback); let mut mutations = dom.rebuild(); - log::info!("Mutations: {:#?}", mutations); // hydrating is simply running the dom for a single render. If the page is already written, then the corresponding // ElementIds should already line up because the web_sys dom has already loaded elements with the DioxusID into memory @@ -146,12 +130,18 @@ pub async fn run_with_props( websys_dom.process_edits(&mut mutations.edits); } + let work_loop = ric_raf::RafLoop::new(); loop { - let deadline = gloo_timers::future::TimeoutFuture::new(16); - let mut mutations = dom.run_with_deadline(deadline).await?; + // if virtualdom has nothing, wait for it to have something before requesting idle time + if !dom.has_work() { + dom.wait_for_any_work().await; + } - websys_dom.process_edits(&mut mutations.edits); + let deadline = work_loop.wait_for_idle_time().await; + + if let Some(mut mutations) = dom.run_with_deadline(deadline).await { + work_loop.wait_for_raf().await; + websys_dom.process_edits(&mut mutations.edits); + } } - - Ok(()) } diff --git a/packages/web/src/ric_raf.rs b/packages/web/src/ric_raf.rs new file mode 100644 index 000000000..42850fb3e --- /dev/null +++ b/packages/web/src/ric_raf.rs @@ -0,0 +1,83 @@ +//! RequestAnimationFrame and RequestIdleCallback port and polyfill. + +use std::{cell::RefCell, fmt, rc::Rc}; + +use gloo_timers::future::TimeoutFuture; +use js_sys::Function; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen::{prelude::Closure, JsValue}; +use web_sys::Window; + +pub struct RafLoop { + window: Window, + ric_receiver: async_channel::Receiver<()>, + raf_receiver: async_channel::Receiver<()>, + ric_closure: Closure, + raf_closure: Closure, +} + +impl RafLoop { + pub fn new() -> Self { + let (raf_sender, raf_receiver) = async_channel::unbounded(); + + let raf_closure: Closure = + Closure::wrap(Box::new(move |v: JsValue| raf_sender.try_send(()).unwrap())); + + let (ric_sender, ric_receiver) = async_channel::unbounded(); + + let ric_closure: Closure = + Closure::wrap(Box::new(move |v: JsValue| ric_sender.try_send(()).unwrap())); + + Self { + window: web_sys::window().unwrap(), + raf_receiver, + raf_closure, + ric_receiver, + ric_closure, + } + } + /// 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 { + // comes with its own safari polyfill :) + + let ric_fn = self.ric_closure.as_ref().dyn_ref::().unwrap(); + let deadline: u32 = self.window.request_idle_callback(ric_fn).unwrap(); + + self.ric_receiver.recv().await.unwrap(); + + let deadline = TimeoutFuture::new(deadline); + deadline + } + + pub async fn wait_for_raf(&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(); + } +} + +#[derive(Debug)] +pub struct AnimationFrame { + render_id: i32, + closure: Closure, + callback_wrapper: Rc>>, +} + +struct CallbackWrapper(Box); +impl fmt::Debug for CallbackWrapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("CallbackWrapper") + } +} + +impl Drop for AnimationFrame { + fn drop(&mut self) { + if self.callback_wrapper.borrow_mut().is_some() { + web_sys::window() + .unwrap_throw() + .cancel_animation_frame(self.render_id) + .unwrap_throw() + } + } +}