From 43aa7b05fbd52806e731b69eae127084064e7258 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Sun, 3 Sep 2023 21:22:17 +0200 Subject: [PATCH] Add `Window.requestIdleCallback()` support --- CHANGELOG.md | 3 +- src/platform/web.rs | 54 +++++++++ src/platform_impl/web/event_loop/runner.rs | 21 +++- .../web/event_loop/window_target.rs | 9 ++ src/platform_impl/web/web_sys/schedule.rs | 103 ++++++++++++++---- 5 files changed, 162 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76071f4a68c..2202fbeedef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ And please only add new entries to the top of this list, right below the `# Unre - Make iOS `MonitorHandle` and `VideoMode` usable from other threads. - Fix window size sometimes being invalid when resizing on macOS. -- On Web, `ControlFlow::Poll` and `ControlFlow::WaitUntil` are now using the Prioritized Task Scheduling API. `setTimeout()` with a trick to circumvent throttling to 4ms is used as a fallback. +- On Web, `ControlFlow::WaitUntil` now uses the Prioritized Task Scheduling API. `setTimeout()`, with a trick to circumvent throttling to 4ms, is used as a fallback. - On Web, never return a `MonitorHandle`. - **Breaking:** Move `Event::RedrawRequested` to `WindowEvent::RedrawRequested`. - On macOS, fix crash in `window.set_minimized(false)`. @@ -21,6 +21,7 @@ And please only add new entries to the top of this list, right below the `# Unre - Mark `startup_notify` unsafe functions as safe. - **Breaking:** Moved `ControlFlow` to `EventLoopWindowTarget::set_control_flow()` and `EventLoopWindowTarget::control_flow()`. - **Breaking:** Moved `ControlFlow::ExitWithCode(_)` to `EventLoopWindowTarget::set_exit()` and `EventLoopWindowTarget::is_exit()`. +- On Web, add `EventLoopWindowTargetExtWebSys` and `PollType`, which allows to set different strategies for `ControlFlow::Poll`. By default the Prioritized Task Scheduling API is used, but an option to use `Window.requestIdleCallback` is available as well. Both use `setTimeout()`, with a trick to circumvent throttling to 4ms, as a fallback. # 0.29.1-beta diff --git a/src/platform/web.rs b/src/platform/web.rs index 7e5d25eb7f8..8772a7900d8 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -134,3 +134,57 @@ impl EventLoopExtWebSys for EventLoop { self.event_loop.spawn(event_handler) } } + +pub trait EventLoopWindowTargetExtWebSys { + /// Sets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollType`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn set_poll_type(&self, poll_type: PollType); + + /// Gets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollType`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn poll_type(&self) -> PollType; +} + +impl EventLoopWindowTargetExtWebSys for EventLoopWindowTarget { + #[inline] + fn set_poll_type(&self, poll_type: PollType) { + self.p.set_poll_type(poll_type); + } + + #[inline] + fn poll_type(&self) -> PollType { + self.p.poll_type() + } +} + +/// Strategy used for [`ControlFlow::Poll`](crate::event_loop::ControlFlow::Poll). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum PollType { + /// Uses [`Window.requestIdleCallback()`] to queue the next event loop. If not available + /// this will fallback to [`setTimeout()`]. + /// + /// This strategy will wait for the browser to enter an idle period before running and might + /// be affected by browser throttling. + /// + /// [`Window.requestIdleCallback()`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback + /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout + IdleCallback, + /// Uses the [Prioritized Task Scheduling API] to queue the next event loop. If not available + /// this will fallback to [`setTimeout()`]. + /// + /// This strategy will run as fast as possible without disturbing users from interacting with + /// the page and is not affected by browser throttling. + /// + /// This is the default strategy. + /// + /// [Prioritized Task Scheduling API]: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API + /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout + #[default] + Scheduler, +} diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index b5a48edb00a..c4566b85884 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -6,6 +6,7 @@ use crate::event::{ WindowEvent, }; use crate::event_loop::{ControlFlow, DeviceEvents}; +use crate::platform::web::PollType; use crate::platform_impl::platform::backend::EventListenerHandle; use crate::window::WindowId; @@ -36,6 +37,7 @@ type OnEventHandle = RefCell>>; pub struct Execution { control_flow: Cell, + poll_type: Cell, exit: Cell, runner: RefCell, suspended: Cell, @@ -140,6 +142,7 @@ impl Shared { Shared(Rc::new(Execution { control_flow: Cell::new(ControlFlow::default()), + poll_type: Cell::new(PollType::default()), exit: Cell::new(false), runner: RefCell::new(RunnerEnum::Pending), suspended: Cell::new(false), @@ -635,9 +638,9 @@ impl Shared { let cloned = self.clone(); State::Poll { request: backend::Schedule::new( - self.window().clone(), + self.poll_type(), + self.window(), move || cloned.poll(), - None, ), } } @@ -658,10 +661,10 @@ impl Shared { State::WaitUntil { start, end, - timeout: backend::Schedule::new( - self.window().clone(), + timeout: backend::Schedule::new_with_duration( + self.window(), move || cloned.resume_time_reached(start, end), - Some(delay), + delay, ), } } @@ -769,6 +772,14 @@ impl Shared { pub(crate) fn is_exit(&self) -> bool { self.0.exit.get() } + + pub(crate) fn set_poll_type(&self, poll_type: PollType) { + self.0.poll_type.set(poll_type) + } + + pub(crate) fn poll_type(&self) -> PollType { + self.0.poll_type.get() + } } pub(crate) enum EventWrapper { diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index a2e548980ab..ebbd2be7bcf 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -21,6 +21,7 @@ use crate::event::{ }; use crate::event_loop::{ControlFlow, DeviceEvents}; use crate::keyboard::ModifiersState; +use crate::platform::web::PollType; use crate::window::{Theme, WindowId as RootWindowId}; #[derive(Default)] @@ -694,4 +695,12 @@ impl EventLoopWindowTarget { pub(crate) fn is_exit(&self) -> bool { self.runner.is_exit() } + + pub(crate) fn set_poll_type(&self, poll_type: PollType) { + self.runner.set_poll_type(poll_type) + } + + pub(crate) fn poll_type(&self) -> PollType { + self.runner.poll_type() + } } diff --git a/src/platform_impl/web/web_sys/schedule.rs b/src/platform_impl/web/web_sys/schedule.rs index af6644b6756..eec2ab6bda7 100644 --- a/src/platform_impl/web/web_sys/schedule.rs +++ b/src/platform_impl/web/web_sys/schedule.rs @@ -6,41 +6,61 @@ use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsCast, JsValue}; use web_sys::{AbortController, AbortSignal, MessageChannel, MessagePort}; +use crate::platform::web::PollType; + #[derive(Debug)] -pub struct Schedule(Inner); +pub struct Schedule { + _closure: Closure, + inner: Inner, +} #[derive(Debug)] enum Inner { Scheduler { controller: AbortController, - _closure: Closure, + }, + IdleCallback { + window: web_sys::Window, + handle: u32, }, Timeout { window: web_sys::Window, handle: i32, port: MessagePort, - _message_closure: Closure, _timeout_closure: Closure, }, } impl Schedule { - pub fn new(window: web_sys::Window, f: F, duration: Option) -> Schedule + pub fn new(poll_type: PollType, window: &web_sys::Window, f: F) -> Schedule + where + F: 'static + FnMut(), + { + if poll_type == PollType::Scheduler && has_scheduler_support(window) { + Self::new_scheduler(window, f, None) + } else if poll_type == PollType::IdleCallback && has_idle_callback_support(window) { + Self::new_idle_callback(window.clone(), f) + } else { + Self::new_timeout(window.clone(), f, None) + } + } + + pub fn new_with_duration(window: &web_sys::Window, f: F, duration: Duration) -> Schedule where F: 'static + FnMut(), { - if has_scheduler_support(&window) { - Self::new_scheduler(window, f, duration) + if has_scheduler_support(window) { + Self::new_scheduler(window, f, Some(duration)) } else { - Self::new_timeout(window, f, duration) + Self::new_timeout(window.clone(), f, Some(duration)) } } - fn new_scheduler(window: web_sys::Window, f: F, duration: Option) -> Schedule + fn new_scheduler(window: &web_sys::Window, f: F, duration: Option) -> Schedule where F: 'static + FnMut(), { - let window: WindowSupportExt = window.unchecked_into(); + let window: &WindowSupportExt = window.unchecked_ref(); let scheduler = window.scheduler(); let closure = Closure::new(f); @@ -61,10 +81,25 @@ impl Schedule { .catch(handler); }); - Schedule(Inner::Scheduler { - controller, + Schedule { _closure: closure, - }) + inner: Inner::Scheduler { controller }, + } + } + + fn new_idle_callback(window: web_sys::Window, f: F) -> Schedule + where + F: 'static + FnMut(), + { + let closure = Closure::new(f); + let handle = window + .request_idle_callback(closure.as_ref().unchecked_ref()) + .expect("Failed to request idle callback"); + + Schedule { + _closure: closure, + inner: Inner::IdleCallback { window, handle }, + } } fn new_timeout(window: web_sys::Window, f: F, duration: Option) -> Schedule @@ -72,10 +107,10 @@ impl Schedule { F: 'static + FnMut(), { let channel = MessageChannel::new().unwrap(); - let message_closure = Closure::new(f); + let closure = Closure::new(f); let port_1 = channel.port1(); port_1 - .add_event_listener_with_callback("message", message_closure.as_ref().unchecked_ref()) + .add_event_listener_with_callback("message", closure.as_ref().unchecked_ref()) .expect("Failed to set message handler"); port_1.start(); @@ -95,20 +130,23 @@ impl Schedule { } .expect("Failed to set timeout"); - Schedule(Inner::Timeout { - window, - handle, - port: port_1, - _message_closure: message_closure, - _timeout_closure: timeout_closure, - }) + Schedule { + _closure: closure, + inner: Inner::Timeout { + window, + handle, + port: port_1, + _timeout_closure: timeout_closure, + }, + } } } impl Drop for Schedule { fn drop(&mut self) { - match &self.0 { + match &self.inner { Inner::Scheduler { controller, .. } => controller.abort(), + Inner::IdleCallback { window, handle, .. } => window.cancel_idle_callback(*handle), Inner::Timeout { window, handle, @@ -144,6 +182,27 @@ fn has_scheduler_support(window: &web_sys::Window) -> bool { }) } +fn has_idle_callback_support(window: &web_sys::Window) -> bool { + thread_local! { + static IDLE_CALLBACK_SUPPORT: OnceCell = OnceCell::new(); + } + + IDLE_CALLBACK_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type IdleCallbackSupport; + + #[wasm_bindgen(method, getter, js_name = requestIdleCallback)] + fn has_request_idle_callback(this: &IdleCallbackSupport) -> JsValue; + } + + let support: &IdleCallbackSupport = window.unchecked_ref(); + !support.has_request_idle_callback().is_undefined() + }) + }) +} + #[wasm_bindgen] extern "C" { type WindowSupportExt;