Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`.
Expand All @@ -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)
- **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.
- 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

Expand Down
54 changes: 54 additions & 0 deletions src/platform/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,57 @@ impl<T> EventLoopExtWebSys for EventLoop<T> {
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,
}
21 changes: 16 additions & 5 deletions src/platform_impl/web/event_loop/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -36,6 +37,7 @@ type OnEventHandle<T> = RefCell<Option<EventListenerHandle<dyn FnMut(T)>>>;

pub struct Execution {
control_flow: Cell<ControlFlow>,
poll_type: Cell<PollType>,
exit: Cell<bool>,
runner: RefCell<RunnerEnum>,
suspended: Cell<bool>,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
),
}
}
Expand All @@ -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,
),
}
}
Expand Down Expand Up @@ -769,6 +772,14 @@ impl Shared {
pub(crate) fn exiting(&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 {
Expand Down
9 changes: 9 additions & 0 deletions src/platform_impl/web/event_loop/window_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -694,4 +695,12 @@ impl<T> EventLoopWindowTarget<T> {
pub(crate) fn exiting(&self) -> bool {
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()
}
}
103 changes: 81 additions & 22 deletions src/platform_impl/web/web_sys/schedule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn FnMut()>,
inner: Inner,
}

#[derive(Debug)]
enum Inner {
Scheduler {
controller: AbortController,
_closure: Closure<dyn FnMut()>,
},
IdleCallback {
window: web_sys::Window,
handle: u32,
},
Timeout {
window: web_sys::Window,
handle: i32,
port: MessagePort,
_message_closure: Closure<dyn FnMut()>,
_timeout_closure: Closure<dyn FnMut()>,
},
}

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
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<F>(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<F>(window: web_sys::Window, f: F, duration: Option<Duration>) -> Schedule
fn new_scheduler<F>(window: &web_sys::Window, f: F, duration: Option<Duration>) -> 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);
Expand All @@ -61,21 +81,36 @@ impl Schedule {
.catch(handler);
});

Schedule(Inner::Scheduler {
controller,
Schedule {
_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
where
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();

Expand All @@ -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,
Expand Down Expand Up @@ -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]
extern "C" {
type WindowSupportExt;
Expand Down