mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
Call logic even while browser tab is in background (#8257)
* Closes https://github.com/emilk/egui/issues/5112 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -113,12 +113,32 @@ fn install_blur_focus(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
|
||||
// NOTE: because of the text agent we sometime miss 'blur' events,
|
||||
// so we also poll the focus state each frame in `AppRunner::logic`.
|
||||
for event_name in ["blur", "focus", "visibilitychange"] {
|
||||
let closure = move |_event: web_sys::MouseEvent, runner: &mut AppRunner| {
|
||||
log::trace!("{} {event_name:?}", runner.canvas().id());
|
||||
runner.update_focus();
|
||||
};
|
||||
let closure =
|
||||
move |_event: web_sys::MouseEvent, runner: &mut AppRunner, web_runner: &WebRunner| {
|
||||
log::trace!("{} {event_name:?}", runner.canvas().id());
|
||||
runner.update_focus();
|
||||
|
||||
runner_ref.add_event_listener(target, event_name, closure)?;
|
||||
if event_name == "visibilitychange" {
|
||||
// The tab was hidden or shown. An in-flight `requestAnimationFrame` is paused
|
||||
// while hidden, so reschedule the paint loop using the scheduling mechanism
|
||||
// (`setTimeout` vs `requestAnimationFrame`) appropriate for the new state.
|
||||
// This keeps `App::update` running while hidden, and switches back to smooth
|
||||
// animation frames once visible again.
|
||||
if let Err(err) = web_runner.reschedule_frame() {
|
||||
log::error!(
|
||||
"Failed to reschedule frame on visibility change: {}",
|
||||
super::string_from_js_value(&err)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
runner_ref.add_event_listener_ex(
|
||||
target,
|
||||
event_name,
|
||||
&web_sys::AddEventListenerOptions::default(),
|
||||
closure,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -129,8 +129,7 @@ impl WebRunner {
|
||||
self.unsubscribe_from_all_events();
|
||||
|
||||
if let Some(frame) = self.frame.take() {
|
||||
let window = web_sys::window().unwrap();
|
||||
window.cancel_animation_frame(frame.id).ok();
|
||||
frame.cancel(&web_sys::window().unwrap());
|
||||
}
|
||||
|
||||
if let Some(runner) = self.app_runner.replace(None) {
|
||||
@@ -234,6 +233,12 @@ impl WebRunner {
|
||||
///
|
||||
/// It is safe to call `request_animation_frame` multiple times in quick succession,
|
||||
/// this function guarantees that only one animation frame is scheduled at a time.
|
||||
///
|
||||
/// A hidden browser tab (e.g. a backgrounded tab) does not receive
|
||||
/// `requestAnimationFrame` callbacks, which would otherwise stop our paint loop
|
||||
/// and prevent `App::update` from running in response to `request_repaint`.
|
||||
/// To keep running in that case, we fall back to `setTimeout`, which keeps firing
|
||||
/// while hidden (the browser throttles it to roughly once per second).
|
||||
pub(crate) fn request_animation_frame(&self) -> Result<(), wasm_bindgen::JsValue> {
|
||||
if self.frame.borrow().is_some() {
|
||||
// there is already an animation frame in flight
|
||||
@@ -252,14 +257,47 @@ impl WebRunner {
|
||||
}
|
||||
});
|
||||
|
||||
let id = window.request_animation_frame(closure.as_ref().unchecked_ref())?;
|
||||
self.frame.borrow_mut().replace(AnimationFrameRequest {
|
||||
id,
|
||||
_closure: closure,
|
||||
});
|
||||
let hidden = window.document().is_some_and(|document| document.hidden());
|
||||
|
||||
let request = if hidden {
|
||||
// The tab is hidden: `requestAnimationFrame` would not fire, so use a timer instead.
|
||||
let id = window.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
closure.as_ref().unchecked_ref(),
|
||||
// Browsers clamp background timers to ~1s, so the exact value has little effect
|
||||
// while hidden, but keeps us responsive right after the tab is hidden:
|
||||
10,
|
||||
)?;
|
||||
AnimationFrameRequest {
|
||||
id,
|
||||
kind: AnimationFrameKind::Timeout,
|
||||
_closure: closure,
|
||||
}
|
||||
} else {
|
||||
let id = window.request_animation_frame(closure.as_ref().unchecked_ref())?;
|
||||
AnimationFrameRequest {
|
||||
id,
|
||||
kind: AnimationFrameKind::AnimationFrame,
|
||||
_closure: closure,
|
||||
}
|
||||
};
|
||||
|
||||
self.frame.borrow_mut().replace(request);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cancel any in-flight frame request and schedule a fresh one.
|
||||
///
|
||||
/// Called on `visibilitychange` so we switch between `requestAnimationFrame` (visible)
|
||||
/// and `setTimeout` (hidden) scheduling. This is necessary because an in-flight
|
||||
/// `requestAnimationFrame` is paused while the tab is hidden, which would otherwise
|
||||
/// stall the paint loop and stop `App::update` from running while hidden.
|
||||
pub(crate) fn reschedule_frame(&self) -> Result<(), wasm_bindgen::JsValue> {
|
||||
if let Some(frame) = self.frame.borrow_mut().take() {
|
||||
frame.cancel(&web_sys::window().unwrap());
|
||||
}
|
||||
self.request_animation_frame()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -269,11 +307,37 @@ struct AnimationFrameRequest {
|
||||
/// Represents the ID of a frame in flight.
|
||||
id: i32,
|
||||
|
||||
/// How the frame was scheduled, so we know how to cancel it.
|
||||
kind: AnimationFrameKind,
|
||||
|
||||
/// The callback given to `request_animation_frame`, stored here both to prevent it
|
||||
/// from being canceled, and from having to `.forget()` it.
|
||||
_closure: Closure<dyn FnMut() -> Result<(), JsValue>>,
|
||||
}
|
||||
|
||||
impl AnimationFrameRequest {
|
||||
/// Cancel the in-flight frame request, using the API matching how it was scheduled.
|
||||
fn cancel(&self, window: &web_sys::Window) {
|
||||
match self.kind {
|
||||
AnimationFrameKind::AnimationFrame => {
|
||||
window.cancel_animation_frame(self.id).ok();
|
||||
}
|
||||
AnimationFrameKind::Timeout => {
|
||||
window.clear_timeout_with_handle(self.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How an [`AnimationFrameRequest`] was scheduled.
|
||||
enum AnimationFrameKind {
|
||||
/// Scheduled with `requestAnimationFrame` (visible tab).
|
||||
AnimationFrame,
|
||||
|
||||
/// Scheduled with `setTimeout` (hidden tab).
|
||||
Timeout,
|
||||
}
|
||||
|
||||
struct TargetEvent {
|
||||
target: web_sys::EventTarget,
|
||||
event_name: String,
|
||||
|
||||
Reference in New Issue
Block a user