diff --git a/CHANGELOG.md b/CHANGELOG.md index 86adc811..18ee8b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ And please only add new entries to the top of this list, right below the `# Unre # Unreleased - 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. # 0.29.1-beta diff --git a/Cargo.toml b/Cargo.toml index 43a8862e..a7fcd29c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -164,6 +164,8 @@ redox_syscall = "0.3" package = "web-sys" version = "0.3.64" features = [ + 'AbortController', + 'AbortSignal', 'console', 'CssStyleDeclaration', 'Document', @@ -179,6 +181,8 @@ features = [ 'IntersectionObserverEntry', 'KeyboardEvent', 'MediaQueryList', + 'MessageChannel', + 'MessagePort', 'Node', 'PageTransitionEvent', 'PointerEvent', diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index ebc035d4..fb40ce60 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -627,9 +627,11 @@ impl Shared { ControlFlow::Poll => { let cloned = self.clone(); State::Poll { - request: backend::IdleCallback::new(self.window().clone(), move || { - cloned.poll() - }), + request: backend::Schedule::new( + self.window().clone(), + move || cloned.poll(), + None, + ), } } ControlFlow::Wait => State::Wait { @@ -649,10 +651,10 @@ impl Shared { State::WaitUntil { start, end, - timeout: backend::Timeout::new( + timeout: backend::Schedule::new( self.window().clone(), move || cloned.resume_time_reached(start, end), - delay, + Some(delay), ), } } diff --git a/src/platform_impl/web/event_loop/state.rs b/src/platform_impl/web/event_loop/state.rs index b9dacee6..0dca6715 100644 --- a/src/platform_impl/web/event_loop/state.rs +++ b/src/platform_impl/web/event_loop/state.rs @@ -7,7 +7,7 @@ use web_time::Instant; pub enum State { Init, WaitUntil { - timeout: backend::Timeout, + timeout: backend::Schedule, start: Instant, end: Instant, }, @@ -15,7 +15,7 @@ pub enum State { start: Instant, }, Poll { - request: backend::IdleCallback, + request: backend::Schedule, }, Exit, } diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 72a6d703..752dc2f7 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -7,13 +7,13 @@ mod intersection_handle; mod media_query_handle; mod pointer; mod resize_scaling; -mod timeout; +mod schedule; pub use self::canvas::Canvas; pub use self::event::ButtonsState; pub use self::event_handle::EventListenerHandle; pub use self::resize_scaling::ResizeScaleHandle; -pub use self::timeout::{IdleCallback, Timeout}; +pub use self::schedule::Schedule; use crate::dpi::{LogicalPosition, LogicalSize}; use wasm_bindgen::closure::Closure; diff --git a/src/platform_impl/web/web_sys/schedule.rs b/src/platform_impl/web/web_sys/schedule.rs new file mode 100644 index 00000000..af6644b6 --- /dev/null +++ b/src/platform_impl/web/web_sys/schedule.rs @@ -0,0 +1,182 @@ +use js_sys::{Function, Object, Promise, Reflect}; +use once_cell::unsync::{Lazy, OnceCell}; +use std::time::Duration; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{AbortController, AbortSignal, MessageChannel, MessagePort}; + +#[derive(Debug)] +pub struct Schedule(Inner); + +#[derive(Debug)] +enum Inner { + Scheduler { + controller: AbortController, + _closure: Closure, + }, + 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 + where + F: 'static + FnMut(), + { + if has_scheduler_support(&window) { + Self::new_scheduler(window, f, duration) + } else { + Self::new_timeout(window, f, duration) + } + } + + fn new_scheduler(window: web_sys::Window, f: F, duration: Option) -> Schedule + where + F: 'static + FnMut(), + { + let window: WindowSupportExt = window.unchecked_into(); + let scheduler = window.scheduler(); + + let closure = Closure::new(f); + let mut options = SchedulerPostTaskOptions::new(); + let controller = AbortController::new().expect("Failed to create `AbortController`"); + options.signal(&controller.signal()); + + if let Some(duration) = duration { + options.delay(duration.as_millis() as f64); + } + + thread_local! { + static REJECT_HANDLER: Lazy> = Lazy::new(|| Closure::new(|_| ())); + } + REJECT_HANDLER.with(|handler| { + let _ = scheduler + .post_task_with_options(closure.as_ref().unchecked_ref(), &options) + .catch(handler); + }); + + Schedule(Inner::Scheduler { + controller, + _closure: closure, + }) + } + + fn new_timeout(window: web_sys::Window, f: F, duration: Option) -> Schedule + where + F: 'static + FnMut(), + { + let channel = MessageChannel::new().unwrap(); + let message_closure = Closure::new(f); + let port_1 = channel.port1(); + port_1 + .add_event_listener_with_callback("message", message_closure.as_ref().unchecked_ref()) + .expect("Failed to set message handler"); + port_1.start(); + + let port_2 = channel.port2(); + let timeout_closure = Closure::new(move || { + port_2 + .post_message(&JsValue::UNDEFINED) + .expect("Failed to send message") + }); + let handle = if let Some(duration) = duration { + window.set_timeout_with_callback_and_timeout_and_arguments_0( + timeout_closure.as_ref().unchecked_ref(), + duration.as_millis() as i32, + ) + } else { + window.set_timeout_with_callback(timeout_closure.as_ref().unchecked_ref()) + } + .expect("Failed to set timeout"); + + Schedule(Inner::Timeout { + window, + handle, + port: port_1, + _message_closure: message_closure, + _timeout_closure: timeout_closure, + }) + } +} + +impl Drop for Schedule { + fn drop(&mut self) { + match &self.0 { + Inner::Scheduler { controller, .. } => controller.abort(), + Inner::Timeout { + window, + handle, + port, + .. + } => { + window.clear_timeout_with_handle(*handle); + port.close(); + } + } + } +} + +fn has_scheduler_support(window: &web_sys::Window) -> bool { + thread_local! { + static SCHEDULER_SUPPORT: OnceCell = OnceCell::new(); + } + + SCHEDULER_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type SchedulerSupport; + + #[wasm_bindgen(method, getter, js_name = scheduler)] + fn has_scheduler(this: &SchedulerSupport) -> JsValue; + } + + let support: &SchedulerSupport = window.unchecked_ref(); + + !support.has_scheduler().is_undefined() + }) + }) +} + +#[wasm_bindgen] +extern "C" { + type WindowSupportExt; + + #[wasm_bindgen(method, getter)] + fn scheduler(this: &WindowSupportExt) -> Scheduler; + + type Scheduler; + + #[wasm_bindgen(method, js_name = postTask)] + fn post_task_with_options( + this: &Scheduler, + callback: &Function, + options: &SchedulerPostTaskOptions, + ) -> Promise; + + type SchedulerPostTaskOptions; +} + +impl SchedulerPostTaskOptions { + fn new() -> Self { + Object::new().unchecked_into() + } + + fn delay(&mut self, val: f64) -> &mut Self { + let r = Reflect::set(self, &JsValue::from("delay"), &val.into()); + debug_assert!(r.is_ok(), "Failed to set `delay` property"); + self + } + + fn signal(&mut self, val: &AbortSignal) -> &mut Self { + let r = Reflect::set(self, &JsValue::from("signal"), &val.into()); + debug_assert!(r.is_ok(), "Failed to set `signal` property"); + self + } +} diff --git a/src/platform_impl/web/web_sys/timeout.rs b/src/platform_impl/web/web_sys/timeout.rs deleted file mode 100644 index 1273a84d..00000000 --- a/src/platform_impl/web/web_sys/timeout.rs +++ /dev/null @@ -1,124 +0,0 @@ -use once_cell::unsync::OnceCell; -use std::cell::Cell; -use std::rc::Rc; -use std::time::Duration; -use wasm_bindgen::closure::Closure; -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::JsCast; -use wasm_bindgen::JsValue; - -#[derive(Debug)] -pub struct Timeout { - window: web_sys::Window, - handle: i32, - _closure: Closure, -} - -impl Timeout { - pub fn new(window: web_sys::Window, f: F, duration: Duration) -> Timeout - where - F: 'static + FnMut(), - { - let closure = Closure::wrap(Box::new(f) as Box); - - let handle = window - .set_timeout_with_callback_and_timeout_and_arguments_0( - closure.as_ref().unchecked_ref(), - duration.as_millis() as i32, - ) - .expect("Failed to set timeout"); - - Timeout { - window, - handle, - _closure: closure, - } - } -} - -impl Drop for Timeout { - fn drop(&mut self) { - self.window.clear_timeout_with_handle(self.handle); - } -} - -#[derive(Debug)] -pub struct IdleCallback { - window: web_sys::Window, - handle: Handle, - fired: Rc>, - _closure: Closure, -} - -#[derive(Clone, Copy, Debug)] -enum Handle { - IdleCallback(u32), - Timeout(i32), -} - -impl IdleCallback { - pub fn new(window: web_sys::Window, mut f: F) -> IdleCallback - where - F: 'static + FnMut(), - { - let fired = Rc::new(Cell::new(false)); - let c_fired = fired.clone(); - let closure = Closure::wrap(Box::new(move || { - (*c_fired).set(true); - f(); - }) as Box); - - let handle = if has_idle_callback_support(&window) { - Handle::IdleCallback( - window - .request_idle_callback(closure.as_ref().unchecked_ref()) - .expect("Failed to request idle callback"), - ) - } else { - Handle::Timeout( - window - .set_timeout_with_callback(closure.as_ref().unchecked_ref()) - .expect("Failed to set timeout"), - ) - }; - - IdleCallback { - window, - handle, - fired, - _closure: closure, - } - } -} - -impl Drop for IdleCallback { - fn drop(&mut self) { - if !(*self.fired).get() { - match self.handle { - Handle::IdleCallback(handle) => self.window.cancel_idle_callback(handle), - Handle::Timeout(handle) => self.window.clear_timeout_with_handle(handle), - } - } - } -} - -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() - }) - }) -}