Add Window.requestIdleCallback() support (#3084)

This commit is contained in:
daxpedda 2023-09-07 12:12:35 +02:00 committed by GitHub
parent b99403b1b9
commit 83950acd5a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 162 additions and 28 deletions

View file

@ -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. - Make iOS `MonitorHandle` and `VideoMode` usable from other threads.
- Fix window size sometimes being invalid when resizing on macOS. - 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`. - On Web, never return a `MonitorHandle`.
- **Breaking:** Move `Event::RedrawRequested` to `WindowEvent::RedrawRequested`. - **Breaking:** Move `Event::RedrawRequested` to `WindowEvent::RedrawRequested`.
- On macOS, fix crash in `window.set_minimized(false)`. - On macOS, fix crash in `window.set_minimized(false)`.
@ -22,6 +22,7 @@ And please only add new entries to the top of this list, right below the `# Unre
- Fix a bug where Wayland would be chosen on Linux even if the user specified `with_x11`. (#3058) - Fix a bug where Wayland would be chosen on Linux even if the user specified `with_x11`. (#3058)
- **Breaking:** Moved `ControlFlow` to `EventLoopWindowTarget::set_control_flow()` and `EventLoopWindowTarget::control_flow()`. - **Breaking:** Moved `ControlFlow` to `EventLoopWindowTarget::set_control_flow()` and `EventLoopWindowTarget::control_flow()`.
- **Breaking:** Moved `ControlFlow::Exit` to `EventLoopWindowTarget::exit()` and `EventLoopWindowTarget::exiting()` and removed `ControlFlow::ExitWithCode(_)` entirely. - **Breaking:** Moved `ControlFlow::Exit` to `EventLoopWindowTarget::exit()` and `EventLoopWindowTarget::exiting()` and removed `ControlFlow::ExitWithCode(_)` entirely.
- 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 # 0.29.1-beta

View file

@ -134,3 +134,57 @@ impl<T> EventLoopExtWebSys for EventLoop<T> {
self.event_loop.spawn(event_handler) 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<T> EventLoopWindowTargetExtWebSys for EventLoopWindowTarget<T> {
#[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,
}

View file

@ -6,6 +6,7 @@ use crate::event::{
WindowEvent, WindowEvent,
}; };
use crate::event_loop::{ControlFlow, DeviceEvents}; use crate::event_loop::{ControlFlow, DeviceEvents};
use crate::platform::web::PollType;
use crate::platform_impl::platform::backend::EventListenerHandle; use crate::platform_impl::platform::backend::EventListenerHandle;
use crate::window::WindowId; use crate::window::WindowId;
@ -36,6 +37,7 @@ type OnEventHandle<T> = RefCell<Option<EventListenerHandle<dyn FnMut(T)>>>;
pub struct Execution { pub struct Execution {
control_flow: Cell<ControlFlow>, control_flow: Cell<ControlFlow>,
poll_type: Cell<PollType>,
exit: Cell<bool>, exit: Cell<bool>,
runner: RefCell<RunnerEnum>, runner: RefCell<RunnerEnum>,
suspended: Cell<bool>, suspended: Cell<bool>,
@ -140,6 +142,7 @@ impl Shared {
Shared(Rc::new(Execution { Shared(Rc::new(Execution {
control_flow: Cell::new(ControlFlow::default()), control_flow: Cell::new(ControlFlow::default()),
poll_type: Cell::new(PollType::default()),
exit: Cell::new(false), exit: Cell::new(false),
runner: RefCell::new(RunnerEnum::Pending), runner: RefCell::new(RunnerEnum::Pending),
suspended: Cell::new(false), suspended: Cell::new(false),
@ -635,9 +638,9 @@ impl Shared {
let cloned = self.clone(); let cloned = self.clone();
State::Poll { State::Poll {
request: backend::Schedule::new( request: backend::Schedule::new(
self.window().clone(), self.poll_type(),
self.window(),
move || cloned.poll(), move || cloned.poll(),
None,
), ),
} }
} }
@ -658,10 +661,10 @@ impl Shared {
State::WaitUntil { State::WaitUntil {
start, start,
end, end,
timeout: backend::Schedule::new( timeout: backend::Schedule::new_with_duration(
self.window().clone(), self.window(),
move || cloned.resume_time_reached(start, end), move || cloned.resume_time_reached(start, end),
Some(delay), delay,
), ),
} }
} }
@ -769,6 +772,14 @@ impl Shared {
pub(crate) fn exiting(&self) -> bool { pub(crate) fn exiting(&self) -> bool {
self.0.exit.get() 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 { pub(crate) enum EventWrapper {

View file

@ -21,6 +21,7 @@ use crate::event::{
}; };
use crate::event_loop::{ControlFlow, DeviceEvents}; use crate::event_loop::{ControlFlow, DeviceEvents};
use crate::keyboard::ModifiersState; use crate::keyboard::ModifiersState;
use crate::platform::web::PollType;
use crate::window::{Theme, WindowId as RootWindowId}; use crate::window::{Theme, WindowId as RootWindowId};
#[derive(Default)] #[derive(Default)]
@ -694,4 +695,12 @@ impl<T> EventLoopWindowTarget<T> {
pub(crate) fn exiting(&self) -> bool { pub(crate) fn exiting(&self) -> bool {
self.runner.exiting() self.runner.exiting()
} }
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()
}
} }

View file

@ -6,41 +6,61 @@ use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::{JsCast, JsValue};
use web_sys::{AbortController, AbortSignal, MessageChannel, MessagePort}; use web_sys::{AbortController, AbortSignal, MessageChannel, MessagePort};
use crate::platform::web::PollType;
#[derive(Debug)] #[derive(Debug)]
pub struct Schedule(Inner); pub struct Schedule {
_closure: Closure<dyn FnMut()>,
inner: Inner,
}
#[derive(Debug)] #[derive(Debug)]
enum Inner { enum Inner {
Scheduler { Scheduler {
controller: AbortController, controller: AbortController,
_closure: Closure<dyn FnMut()>, },
IdleCallback {
window: web_sys::Window,
handle: u32,
}, },
Timeout { Timeout {
window: web_sys::Window, window: web_sys::Window,
handle: i32, handle: i32,
port: MessagePort, port: MessagePort,
_message_closure: Closure<dyn FnMut()>,
_timeout_closure: Closure<dyn FnMut()>, _timeout_closure: Closure<dyn FnMut()>,
}, },
} }
impl Schedule { impl Schedule {
pub fn new<F>(window: web_sys::Window, f: F, duration: Option<Duration>) -> Schedule pub fn new<F>(poll_type: PollType, window: &web_sys::Window, f: F) -> Schedule
where where
F: 'static + FnMut(), F: 'static + FnMut(),
{ {
if has_scheduler_support(&window) { if poll_type == PollType::Scheduler && has_scheduler_support(window) {
Self::new_scheduler(window, f, duration) 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 { } else {
Self::new_timeout(window, f, duration) Self::new_timeout(window.clone(), f, None)
} }
} }
fn new_scheduler<F>(window: web_sys::Window, f: F, duration: Option<Duration>) -> Schedule pub fn new_with_duration<F>(window: &web_sys::Window, f: F, duration: Duration) -> Schedule
where where
F: 'static + FnMut(), F: 'static + FnMut(),
{ {
let window: WindowSupportExt = window.unchecked_into(); if has_scheduler_support(window) {
Self::new_scheduler(window, f, Some(duration))
} else {
Self::new_timeout(window.clone(), f, Some(duration))
}
}
fn new_scheduler<F>(window: &web_sys::Window, f: F, duration: Option<Duration>) -> Schedule
where
F: 'static + FnMut(),
{
let window: &WindowSupportExt = window.unchecked_ref();
let scheduler = window.scheduler(); let scheduler = window.scheduler();
let closure = Closure::new(f); let closure = Closure::new(f);
@ -61,10 +81,25 @@ impl Schedule {
.catch(handler); .catch(handler);
}); });
Schedule(Inner::Scheduler { Schedule {
controller,
_closure: closure, _closure: closure,
}) inner: Inner::Scheduler { controller },
}
}
fn new_idle_callback<F>(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<F>(window: web_sys::Window, f: F, duration: Option<Duration>) -> Schedule fn new_timeout<F>(window: web_sys::Window, f: F, duration: Option<Duration>) -> Schedule
@ -72,10 +107,10 @@ impl Schedule {
F: 'static + FnMut(), F: 'static + FnMut(),
{ {
let channel = MessageChannel::new().unwrap(); let channel = MessageChannel::new().unwrap();
let message_closure = Closure::new(f); let closure = Closure::new(f);
let port_1 = channel.port1(); let port_1 = channel.port1();
port_1 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"); .expect("Failed to set message handler");
port_1.start(); port_1.start();
@ -95,20 +130,23 @@ impl Schedule {
} }
.expect("Failed to set timeout"); .expect("Failed to set timeout");
Schedule(Inner::Timeout { Schedule {
window, _closure: closure,
handle, inner: Inner::Timeout {
port: port_1, window,
_message_closure: message_closure, handle,
_timeout_closure: timeout_closure, port: port_1,
}) _timeout_closure: timeout_closure,
},
}
} }
} }
impl Drop for Schedule { impl Drop for Schedule {
fn drop(&mut self) { fn drop(&mut self) {
match &self.0 { match &self.inner {
Inner::Scheduler { controller, .. } => controller.abort(), Inner::Scheduler { controller, .. } => controller.abort(),
Inner::IdleCallback { window, handle, .. } => window.cancel_idle_callback(*handle),
Inner::Timeout { Inner::Timeout {
window, window,
handle, 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<bool> = 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] #[wasm_bindgen]
extern "C" { extern "C" {
type WindowSupportExt; type WindowSupportExt;