From e63ad8dcae01d3ae658944c618a2c7627d9903ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Esp=C3=ADn?= Date: Sun, 22 Oct 2023 17:42:52 +0200 Subject: [PATCH] feat: Event Loop ticker for components (#345) * feat: Event Loop ticker for components * improvements and fixes * improvements * use_camera improvements * improvements * simplified * clean up * clean up --- .vscode/settings.json | 3 +- crates/hooks/src/use_accessibility.rs | 2 +- crates/hooks/src/use_animation.rs | 47 +++++++--- crates/hooks/src/use_animation_transition.rs | 37 +++++--- crates/hooks/src/use_camera.rs | 27 ++---- crates/hooks/src/use_focus.rs | 2 +- crates/hooks/src/use_node.rs | 4 +- crates/hooks/src/use_platform.rs | 28 +++++- crates/renderer/src/app.rs | 11 +++ crates/renderer/src/event_loop.rs | 3 +- crates/testing/src/config.rs | 99 +++++++++++--------- crates/testing/src/launch.rs | 2 + crates/testing/src/test_handler.rs | 61 +++++++----- 13 files changed, 203 insertions(+), 123 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 27cc6116c..d01413707 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "rust-analyzer.cargo.features": [ "devtools", - "log" + "log", + "use_camera" ] } \ No newline at end of file diff --git a/crates/hooks/src/use_accessibility.rs b/crates/hooks/src/use_accessibility.rs index 5467aa6cb..1717b2c72 100644 --- a/crates/hooks/src/use_accessibility.rs +++ b/crates/hooks/src/use_accessibility.rs @@ -66,7 +66,7 @@ mod test { let mut utils = launch_test_with_config( use_focus_app, - TestingConfig::default().with_size((100.0, 100.0).into()), + *TestingConfig::default().with_size((100.0, 100.0).into()), ); // Initial state diff --git a/crates/hooks/src/use_animation.rs b/crates/hooks/src/use_animation.rs index 1caad2148..b6ab36135 100644 --- a/crates/hooks/src/use_animation.rs +++ b/crates/hooks/src/use_animation.rs @@ -1,12 +1,9 @@ use dioxus_core::ScopeState; use dioxus_hooks::{use_state, UseState}; -use std::time::Duration; -use tokio::time::interval; +use tokio::time::Instant; use uuid::Uuid; -use crate::Animation; - -const ANIMATION_MS: i32 = 16; // Assume 60 FPS for now +use crate::{use_platform, Animation, UsePlatform}; /// Manage the lifecyle of an [Animation]. #[derive(Clone)] @@ -15,14 +12,16 @@ pub struct AnimationManager<'a> { current_animation_id: &'a UseState>, value: &'a UseState, cx: &'a ScopeState, + platform: UsePlatform, } impl<'a> AnimationManager<'a> { /// Start the given [Animation]. pub fn start(&self, mut anim: Animation) { let new_id = Uuid::new_v4(); - let mut index = 0; + let platform = self.platform.clone(); + let mut ticker = platform.new_ticker(); let value = self.value.clone(); let current_animation_id = self.current_animation_id.clone(); @@ -31,8 +30,16 @@ impl<'a> AnimationManager<'a> { // Spawn the animation that will run at 1ms speed self.cx.spawn(async move { - let mut ticker = interval(Duration::from_millis(ANIMATION_MS as u64)); + platform.request_animation_frame(); + + let mut index = 0; + let mut prev_frame = Instant::now(); + loop { + // Wait for the event loop to tick + ticker.tick().await; + platform.request_animation_frame(); + // Stop running the animation if it was removed if *current_animation_id.current() == Some(new_id) { // Remove the current animation if it has finished @@ -41,12 +48,10 @@ impl<'a> AnimationManager<'a> { break; } - // Advance one tick + index += prev_frame.elapsed().as_millis() as i32; value.set(anim.move_value(index)); - index += ANIMATION_MS; - // Wait 1m - ticker.tick().await; + prev_frame = Instant::now(); } else { break; } @@ -102,12 +107,14 @@ pub fn use_animation(cx: &ScopeState, init_value: impl FnOnce() -> f64) -> Anima let current_animation_id = use_state(cx, || None); let init_value = *cx.use_hook(init_value); let value = use_state(cx, || init_value); + let platform = use_platform(cx); AnimationManager { current_animation_id, value, cx, init_value, + platform, } } @@ -139,20 +146,24 @@ mod test { let mut utils = launch_test(use_animation_app); + // Disable event loop ticker + utils.config().enable_ticker(false); + // Initial state utils.wait_for_update().await; assert_eq!(utils.root().get(0).layout().unwrap().width(), 0.0); // State somewhere in the middle - utils.wait_for_update().await; + sleep(Duration::from_millis(32)).await; utils.wait_for_update().await; let width = utils.root().get(0).layout().unwrap().width(); assert!(width > 0.0); assert!(width < 100.0); - sleep(Duration::from_millis(50)).await; + // Enable event loop ticker + utils.config().enable_ticker(true); // State in the end utils.wait_for_update().await; @@ -189,26 +200,34 @@ mod test { let mut utils = launch_test(use_animation_app); + // Disable event loop ticker + utils.config().enable_ticker(false); + // Initial state utils.wait_for_update().await; assert_eq!(utils.root().get(0).layout().unwrap().width(), 10.0); // State somewhere in the middle - utils.wait_for_update().await; + sleep(Duration::from_millis(32)).await; utils.wait_for_update().await; let width = utils.root().get(0).layout().unwrap().width(); assert!(width > 10.0); + // Trigger the click event to restart the animation utils.push_event(FreyaEvent::Mouse { name: "click".to_string(), cursor: (5.0, 5.0).into(), button: Some(MouseButton::Left), }); + // Enable event loop ticker + utils.config().enable_ticker(true); + // State has been restarted utils.wait_for_update().await; + utils.wait_for_update().await; let width = utils.root().get(0).layout().unwrap().width(); assert_eq!(width, 10.0); diff --git a/crates/hooks/src/use_animation_transition.rs b/crates/hooks/src/use_animation_transition.rs index 6487e37f0..1164dd0c9 100644 --- a/crates/hooks/src/use_animation_transition.rs +++ b/crates/hooks/src/use_animation_transition.rs @@ -2,13 +2,10 @@ use dioxus_core::ScopeState; use dioxus_hooks::{use_memo, use_state, UseFutureDep, UseState}; use freya_engine::prelude::Color; use freya_node_state::Parse; -use std::time::Duration; -use tokio::time::interval; +use tokio::time::Instant; use uuid::Uuid; -use crate::{Animation, TransitionAnimation}; - -const ANIMATION_MS: i32 = 16; // Assume 60 FPS for now +use crate::{use_platform, Animation, TransitionAnimation, UsePlatform}; /// Configure a `Transition` animation. #[derive(Clone, Debug, Copy, PartialEq)] @@ -145,6 +142,8 @@ pub struct TransitionsManager<'a> { current_animation_id: &'a UseState>, /// The scope. cx: &'a ScopeState, + /// Platform APIs + platform: UsePlatform, } impl<'a> TransitionsManager<'a> { @@ -165,6 +164,8 @@ impl<'a> TransitionsManager<'a> { fn run_with_animation(&self, mut animation: Animation) { let animation_id = Uuid::new_v4(); + let platform = self.platform.clone(); + let mut ticker = platform.new_ticker(); let transitions = self.transitions.clone(); let transitions_storage = self.transitions_storage.clone(); let current_animation_id = self.current_animation_id.clone(); @@ -174,9 +175,16 @@ impl<'a> TransitionsManager<'a> { // Spawn the animation that will run at 1ms speed self.cx.spawn(async move { - let mut ticker = interval(Duration::from_millis(ANIMATION_MS as u64)); + platform.request_animation_frame(); + let mut index = 0; + let mut prev_frame = Instant::now(); + loop { + // Wait for the event loop to tick + ticker.tick().await; + platform.request_animation_frame(); + // Stop running the animation if it's no longer selected if *current_animation_id.current() == Some(animation_id) { // Remove the current animation if it has finished @@ -185,7 +193,7 @@ impl<'a> TransitionsManager<'a> { break; } - // Advance one tick + index += prev_frame.elapsed().as_millis() as i32; let value = animation.move_value(index); transitions_storage.with_mut(|storage| { for (i, storage) in storage.iter_mut().enumerate() { @@ -195,10 +203,7 @@ impl<'a> TransitionsManager<'a> { } }); - index += ANIMATION_MS; - - // Wait 1ms - ticker.tick().await; + prev_frame = Instant::now(); } else { break; } @@ -278,6 +283,7 @@ where let current_animation_id = use_state(cx, || None); let transitions = use_memo(cx, dependencies.clone(), &mut init); let transitions_storage = use_state(cx, || animations_map(transitions)); + let platform = use_platform(cx); use_memo(cx, dependencies, { let storage_setter = transitions_storage.setter(); @@ -292,6 +298,7 @@ where transitions_storage, cx, transition_animation: transition, + platform, } } @@ -332,20 +339,24 @@ mod test { let mut utils = launch_test(use_animation_transition_app); + // Disable event loop ticker + utils.config().enable_ticker(false); + // Initial state utils.wait_for_update().await; assert_eq!(utils.root().get(0).layout().unwrap().width(), 0.0); // State somewhere in the middle - utils.wait_for_update().await; + sleep(Duration::from_millis(32)).await; utils.wait_for_update().await; let width = utils.root().get(0).layout().unwrap().width(); assert!(width > 0.0); assert!(width < 100.0); - sleep(Duration::from_millis(50)).await; + // Enable event loop ticker + utils.config().enable_ticker(true); // State in the end utils.wait_for_update().await; diff --git a/crates/hooks/src/use_camera.rs b/crates/hooks/src/use_camera.rs index c33c9b1ce..f1d58e954 100644 --- a/crates/hooks/src/use_camera.rs +++ b/crates/hooks/src/use_camera.rs @@ -1,33 +1,20 @@ -use std::{ - sync::{Arc, Mutex}, - time::Duration, -}; +use std::sync::{Arc, Mutex}; use crate::use_platform; use dioxus_core::{AttributeValue, ScopeState}; use dioxus_hooks::{to_owned, use_effect, use_state, UseState}; -use freya_common::EventMessage; use freya_node_state::{CustomAttributeValues, ImageReference}; -use nokhwa::{pixel_format::RgbFormat, utils::RequestedFormat, Camera, NokhwaError}; -use tokio::time::sleep; - pub use nokhwa::utils::{CameraIndex, RequestedFormatType, Resolution}; +use nokhwa::{pixel_format::RgbFormat, utils::RequestedFormat, Camera, NokhwaError}; /// Configuration for a camera pub struct CameraSettings { - frame_rate: u32, camera_index: CameraIndex, resolution: Option, camera_format: RequestedFormatType, } impl CameraSettings { - /// Specify a frame rate - pub fn with_frame_rate(mut self, frame_rate: u32) -> Self { - self.frame_rate = frame_rate; - self - } - /// Specify a camera index pub fn with_camera_index(mut self, camera_index: CameraIndex) -> Self { self.camera_index = camera_index; @@ -50,7 +37,6 @@ impl CameraSettings { impl Default for CameraSettings { fn default() -> Self { Self { - frame_rate: 30, camera_index: CameraIndex::Index(0), resolution: None, camera_format: RequestedFormatType::AbsoluteHighestFrameRate, @@ -89,11 +75,11 @@ pub fn use_camera( .unwrap_or_else(handle_error); } - let frame_rate = camera_settings.frame_rate; - let fps = 1000 / frame_rate; + let mut ticker = platform.new_ticker(); loop { - sleep(Duration::from_millis(fps as u64)).await; + // Wait for the event loop to tick + ticker.tick().await; // Capture the next frame let frame = camera.frame(); @@ -104,9 +90,10 @@ pub fn use_camera( image_reference.lock().unwrap().replace(bts); // Request the renderer to rerender - platform.send(EventMessage::RequestRerender).unwrap(); + platform.request_animation_frame(); } else if let Err(err) = frame { handle_error(err); + break; } } } else if let Err(err) = camera { diff --git a/crates/hooks/src/use_focus.rs b/crates/hooks/src/use_focus.rs index 0a9d83dd0..4aeacc730 100644 --- a/crates/hooks/src/use_focus.rs +++ b/crates/hooks/src/use_focus.rs @@ -93,7 +93,7 @@ mod test { let mut utils = launch_test_with_config( use_focus_app, - TestingConfig::default().with_size((100.0, 100.0).into()), + *TestingConfig::default().with_size((100.0, 100.0).into()), ); // Initial state diff --git a/crates/hooks/src/use_node.rs b/crates/hooks/src/use_node.rs index 58249d17f..39a264df9 100644 --- a/crates/hooks/src/use_node.rs +++ b/crates/hooks/src/use_node.rs @@ -77,14 +77,14 @@ mod test { let mut utils = launch_test_with_config( use_node_app, - TestingConfig::default().with_size((500.0, 800.0).into()), + *TestingConfig::default().with_size((500.0, 800.0).into()), ); utils.wait_for_update().await; let root = utils.root().get(0); assert_eq!(root.get(0).text().unwrap().parse::(), Ok(500.0 * 0.5)); - utils.set_config(TestingConfig::default().with_size((300.0, 800.0).into())); + utils.config().with_size((300.0, 800.0).into()); utils.wait_for_update().await; let root = utils.root().get(0); diff --git a/crates/hooks/src/use_platform.rs b/crates/hooks/src/use_platform.rs index 40f78ec70..fc77e5c03 100644 --- a/crates/hooks/src/use_platform.rs +++ b/crates/hooks/src/use_platform.rs @@ -1,10 +1,13 @@ +use std::sync::Arc; + use dioxus_core::ScopeState; use freya_common::EventMessage; -use tokio::sync::mpsc::UnboundedSender; +use tokio::sync::{broadcast, mpsc::UnboundedSender}; use winit::event_loop::EventLoopProxy; #[derive(Clone)] pub struct UsePlatform { + ticker: Arc>, event_loop_proxy: Option>, platform_emitter: Option>, } @@ -28,11 +31,34 @@ impl UsePlatform { } Ok(()) } + + pub fn request_animation_frame(&self) { + self.send(EventMessage::RequestRerender).ok(); + } + + pub fn new_ticker(&self) -> Ticker { + Ticker { + inner: self.ticker.resubscribe(), + } + } } pub fn use_platform(cx: &ScopeState) -> UsePlatform { UsePlatform { event_loop_proxy: cx.consume_context::>(), platform_emitter: cx.consume_context::>(), + ticker: cx + .consume_context::>>() + .expect("This is not expected, and likely a bug. Please, report it."), + } +} + +pub struct Ticker { + inner: broadcast::Receiver<()>, +} + +impl Ticker { + pub async fn tick(&mut self) { + self.inner.recv().await.ok(); } } diff --git a/crates/renderer/src/app.rs b/crates/renderer/src/app.rs index 885b1b33c..312319d09 100644 --- a/crates/renderer/src/app.rs +++ b/crates/renderer/src/app.rs @@ -11,6 +11,7 @@ use futures::{ pin_mut, task::{self, ArcWake}, }; +use tokio::sync::broadcast; use tokio::{ select, sync::{mpsc, watch, Notify}, @@ -65,6 +66,8 @@ pub struct App { accessibility: NativeAccessibility, font_collection: FontCollection, + + ticker_sender: broadcast::Sender<()>, } impl App { @@ -114,6 +117,7 @@ impl App { focus_sender, focus_receiver, font_collection, + ticker_sender: broadcast::channel(5).0, } } @@ -126,6 +130,9 @@ impl App { self.vdom .base_scope() .provide_context(self.focus_receiver.clone()); + self.vdom + .base_scope() + .provide_context(Arc::new(self.ticker_sender.subscribe())); } /// Make the first build of the VirtualDOM. @@ -321,4 +328,8 @@ impl App { self.accessibility .focus_next_node(direction, &self.focus_sender) } + + pub fn tick(&self) { + self.ticker_sender.send(()).unwrap(); + } } diff --git a/crates/renderer/src/event_loop.rs b/crates/renderer/src/event_loop.rs index e36da4e53..a835c28a9 100644 --- a/crates/renderer/src/event_loop.rs +++ b/crates/renderer/src/event_loop.rs @@ -45,7 +45,7 @@ pub fn run_event_loop( app.accessibility().set_accessibility_focus(id); } Event::UserEvent(EventMessage::RequestRerender) => { - app.render(&hovered_node); + app.window_env().window().request_redraw(); } Event::UserEvent(EventMessage::RequestRelayout) => { app.process_layout(); @@ -83,6 +83,7 @@ pub fn run_event_loop( Event::RedrawRequested(_) => { app.process_layout(); app.render(&hovered_node); + app.tick(); } Event::WindowEvent { event, .. } if app.on_window_event(&event) => { match event { diff --git a/crates/testing/src/config.rs b/crates/testing/src/config.rs index b88179378..74217c7a3 100644 --- a/crates/testing/src/config.rs +++ b/crates/testing/src/config.rs @@ -1,46 +1,53 @@ -use std::time::Duration; - -use torin::geometry::Size2D; - -/// Configuration for [`crate::test_handler::TestingHandler`]. -pub struct TestingConfig { - vdom_timeout: Duration, - size: Size2D, -} - -impl Default for TestingConfig { - fn default() -> Self { - Self { - vdom_timeout: Duration::from_millis(16), - size: Size2D::from((500.0, 500.0)), - } - } -} - -impl TestingConfig { - pub fn new() -> Self { - TestingConfig::default() - } - - /// Specify a custom canvas size. - pub fn with_size(mut self, size: Size2D) -> Self { - self.size = size; - self - } - - /// Specify a custom duration for the VirtualDOM polling timeout, default is 16ms. - pub fn with_vdom_timeout(mut self, vdom_timeout: Duration) -> Self { - self.vdom_timeout = vdom_timeout; - self - } - - /// Get the canvas size. - pub fn size(&self) -> Size2D { - self.size - } - - /// Get the VirtualDOM polling timeout. - pub fn vdom_timeout(&self) -> Duration { - self.vdom_timeout - } -} +use std::time::Duration; + +use torin::geometry::Size2D; + +/// Configuration for [`crate::test_handler::TestingHandler`]. +#[derive(Clone, Copy)] +pub struct TestingConfig { + pub(crate) vdom_timeout: Duration, + pub(crate) size: Size2D, + pub(crate) run_ticker: bool, +} + +impl Default for TestingConfig { + fn default() -> Self { + Self { + vdom_timeout: Duration::from_millis(16), + size: Size2D::from((500.0, 500.0)), + run_ticker: true, + } + } +} + +impl TestingConfig { + pub fn new() -> Self { + TestingConfig::default() + } + + /// Specify a custom canvas size. + pub fn with_size(&mut self, size: Size2D) -> &mut Self { + self.size = size; + self + } + + /// Specify a custom duration for the VirtualDOM polling timeout, default is 16ms. + pub fn with_vdom_timeout(&mut self, vdom_timeout: Duration) -> &mut Self { + self.vdom_timeout = vdom_timeout; + self + } + + /// Get the canvas size. + pub fn size(&self) -> Size2D { + self.size + } + + /// Get the VirtualDOM polling timeout. + pub fn vdom_timeout(&self) -> Duration { + self.vdom_timeout + } + + pub fn enable_ticker(&mut self, ticker: bool) { + self.run_ticker = ticker; + } +} diff --git a/crates/testing/src/launch.rs b/crates/testing/src/launch.rs index 6b6e69fe3..8f7bfe49c 100644 --- a/crates/testing/src/launch.rs +++ b/crates/testing/src/launch.rs @@ -10,6 +10,7 @@ use freya_hooks::{use_init_accessibility, use_init_focus}; use freya_layout::Layers; use rustc_hash::FxHashMap; use std::sync::{Arc, Mutex}; +use tokio::sync::broadcast; use tokio::sync::mpsc::unbounded_channel; pub use freya_core::events::FreyaEvent; @@ -52,6 +53,7 @@ pub fn launch_test_with_config(root: Component<()>, config: TestingConfig) -> Te platform_event_emitter, platform_event_receiver, accessibility_state, + ticker_sender: broadcast::channel(5).0, }; handler.init_dom(); diff --git a/crates/testing/src/test_handler.rs b/crates/testing/src/test_handler.rs index f720af975..98dc5561d 100644 --- a/crates/testing/src/test_handler.rs +++ b/crates/testing/src/test_handler.rs @@ -1,14 +1,18 @@ +use std::sync::Arc; +use std::time::Duration; + use accesskit::NodeId as AccessibilityId; use dioxus_core::VirtualDom; use freya_common::EventMessage; use freya_core::prelude::*; use freya_engine::prelude::FontCollection; +use tokio::sync::broadcast; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use torin::geometry::{Area, Size2D}; pub use freya_core::events::FreyaEvent; pub use freya_elements::events::mouse::MouseButton; -use tokio::time::timeout; +use tokio::time::{sleep, timeout}; use crate::test_node::TestNode; use crate::test_utils::TestUtils; @@ -32,6 +36,8 @@ pub struct TestingHandler { pub(crate) accessibility_state: SharedAccessibilityState, pub(crate) config: TestingConfig, + + pub(crate) ticker_sender: broadcast::Sender<()>, } impl TestingHandler { @@ -44,9 +50,9 @@ impl TestingHandler { fdom.init_dom(mutations, SCALE_FACTOR as f32); } - /// Replace the current [`TestingConfig`]. - pub fn set_config(&mut self, config: TestingConfig) { - self.config = config; + /// Get a mutable reference to the current [`TestingConfig`]. + pub fn config(&mut self) -> &mut TestingConfig { + &mut self.config } /// Provide some values to the app @@ -54,6 +60,9 @@ impl TestingHandler { self.vdom .base_scope() .provide_context(self.platform_event_emitter.clone()); + self.vdom + .base_scope() + .provide_context(Arc::new(self.ticker_sender.subscribe())); } /// Wait and apply new changes @@ -64,32 +73,36 @@ impl TestingHandler { let vdom = &mut self.vdom; - // Handle platform events + // Handle platform and VDOM events loop { - let ev = self.platform_event_receiver.try_recv(); - - if let Ok(ev) = ev { - #[allow(clippy::match_single_binding)] - if let EventMessage::FocusAccessibilityNode(node_id) = ev { - self.accessibility_state - .lock() - .unwrap() - .set_focus(Some(node_id)); - } - } else { + let platform_ev = self.platform_event_receiver.try_recv(); + let vdom_ev = self.event_receiver.try_recv(); + + if vdom_ev.is_err() && platform_ev.is_err() { break; } - } - // Handle virtual dom events - loop { - let ev = self.event_receiver.try_recv(); + if let Ok(ev) = platform_ev { + match ev { + EventMessage::RequestRerender => { + if self.config.run_ticker { + sleep(Duration::from_millis(16)).await; + self.ticker_sender.send(()).unwrap(); + } + } + EventMessage::FocusAccessibilityNode(node_id) => { + self.accessibility_state + .lock() + .unwrap() + .set_focus(Some(node_id)); + } + _ => {} + } + } - if let Ok(ev) = ev { + if let Ok(ev) = vdom_ev { vdom.handle_event(&ev.name, ev.data.any(), ev.element_id, false); vdom.process_events(); - } else { - break; } } @@ -107,6 +120,8 @@ impl TestingHandler { self.wait_for_work(self.config.size()); + self.ticker_sender.send(()).unwrap(); + (must_repaint, must_relayout) }