Browse Source

Fix crash on `request_animation_frame` when destroying web runner (#4169)

Previously, any frames in flight (`requestAnimationFrame`) on web were
not being cancelled (`cancelAnimationFrame`) when `WebRunner::destroy`
was called. If a user called `destroy`, then immediately removed the
canvas from the DOM, eframe could panic with a "failed to find (canvas)
element by id" error message.

This PR changes two things:
- The canvas element is directly referenced everywhere it's needed
instead of being looked up by `canvas_id`[^1]
- The RAF handle is stored in `WebRunner` and `cancelAnimationFrame` is
called on it inside of `WebRunner::destroy`[^2]

[^1]: The WebGL/WGPU backends were already holding onto the canvas (and
associated GPU context), so the change is just converting all the
`get_element_by_id` lookups to retrieve the canvas from the web runner
handle.

[^2]: There is only ever one frame in flight, so we store it directly as
a scalar field.
pull/4170/head
Jan Procházka 8 months ago
committed by GitHub
parent
commit
c5eaba43cd
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 10
      crates/eframe/src/web/app_runner.rs
  2. 26
      crates/eframe/src/web/events.rs
  3. 14
      crates/eframe/src/web/input.rs
  4. 21
      crates/eframe/src/web/mod.rs
  5. 10
      crates/eframe/src/web/text_agent.rs
  6. 4
      crates/eframe/src/web/web_painter.rs
  7. 13
      crates/eframe/src/web/web_painter_glow.rs
  8. 8
      crates/eframe/src/web/web_painter_wgpu.rs
  9. 28
      crates/eframe/src/web/web_runner.rs

10
crates/eframe/src/web/app_runner.rs

@ -165,8 +165,8 @@ impl AppRunner {
self.last_save_time = now_sec();
}
pub fn canvas_id(&self) -> &str {
self.painter.canvas_id()
pub fn canvas(&self) -> &web_sys::HtmlCanvasElement {
self.painter.canvas()
}
pub fn destroy(mut self) {
@ -182,8 +182,8 @@ impl AppRunner {
///
/// The result can be painted later with a call to [`Self::run_and_paint`] or [`Self::paint`].
pub fn logic(&mut self) {
super::resize_canvas_to_screen_size(self.canvas_id(), self.web_options.max_size_points);
let canvas_size = super::canvas_size_in_points(self.canvas_id());
super::resize_canvas_to_screen_size(self.canvas(), self.web_options.max_size_points);
let canvas_size = super::canvas_size_in_points(self.canvas());
let raw_input = self.input.new_frame(canvas_size);
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
@ -268,7 +268,7 @@ impl AppRunner {
self.mutable_text_under_cursor = mutable_text_under_cursor;
if self.ime != ime {
super::text_agent::move_text_cursor(ime, self.canvas_id());
super::text_agent::move_text_cursor(ime, self.canvas());
self.ime = ime;
}
}

26
crates/eframe/src/web/events.rs

@ -5,12 +5,12 @@ use super::*;
/// Calls `request_animation_frame` to schedule repaint.
///
/// It will only paint if needed, but will always call `request_animation_frame` immediately.
fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> {
pub(crate) fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> {
// Only paint and schedule if there has been no panic
if let Some(mut runner_lock) = runner_ref.try_lock() {
paint_if_needed(&mut runner_lock);
drop(runner_lock);
request_animation_frame(runner_ref.clone())?;
runner_ref.request_animation_frame()?;
}
Ok(())
}
@ -45,14 +45,6 @@ fn paint_if_needed(runner: &mut AppRunner) {
runner.auto_save_if_needed();
}
pub(crate) fn request_animation_frame(runner_ref: WebRunner) -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
let closure = Closure::once(move || paint_and_schedule(&runner_ref));
window.request_animation_frame(closure.as_ref().unchecked_ref())?;
closure.forget(); // We must forget it, or else the callback is canceled on drop
Ok(())
}
// ------------------------------------------------------------------------
pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsValue> {
@ -275,7 +267,7 @@ pub(crate) fn install_color_scheme_change_event(runner_ref: &WebRunner) -> Resul
}
pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> {
let canvas = canvas_element(runner_ref.try_lock().unwrap().canvas_id()).unwrap();
let canvas = runner_ref.try_lock().unwrap().canvas().clone();
{
let prevent_default_events = [
@ -304,7 +296,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers;
if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner.canvas_id(), &event);
let pos = pos_from_mouse_event(runner.canvas(), &event);
let modifiers = runner.input.raw.modifiers;
runner.input.raw.events.push(egui::Event::PointerButton {
pos,
@ -331,7 +323,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
|event: web_sys::MouseEvent, runner| {
let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers;
let pos = pos_from_mouse_event(runner.canvas_id(), &event);
let pos = pos_from_mouse_event(runner.canvas(), &event);
runner.input.raw.events.push(egui::Event::PointerMoved(pos));
runner.needs_repaint.repaint_asap();
event.stop_propagation();
@ -343,7 +335,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers;
if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner.canvas_id(), &event);
let pos = pos_from_mouse_event(runner.canvas(), &event);
let modifiers = runner.input.raw.modifiers;
runner.input.raw.events.push(egui::Event::PointerButton {
pos,
@ -381,7 +373,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
"touchstart",
|event: web_sys::TouchEvent, runner| {
let mut latest_touch_pos_id = runner.input.latest_touch_pos_id;
let pos = pos_from_touch_event(runner.canvas_id(), &event, &mut latest_touch_pos_id);
let pos = pos_from_touch_event(runner.canvas(), &event, &mut latest_touch_pos_id);
runner.input.latest_touch_pos_id = latest_touch_pos_id;
runner.input.latest_touch_pos = Some(pos);
let modifiers = runner.input.raw.modifiers;
@ -404,7 +396,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
"touchmove",
|event: web_sys::TouchEvent, runner| {
let mut latest_touch_pos_id = runner.input.latest_touch_pos_id;
let pos = pos_from_touch_event(runner.canvas_id(), &event, &mut latest_touch_pos_id);
let pos = pos_from_touch_event(runner.canvas(), &event, &mut latest_touch_pos_id);
runner.input.latest_touch_pos_id = latest_touch_pos_id;
runner.input.latest_touch_pos = Some(pos);
runner.input.raw.events.push(egui::Event::PointerMoved(pos));
@ -467,7 +459,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu
});
let scroll_multiplier = match unit {
egui::MouseWheelUnit::Page => canvas_size_in_points(runner.canvas_id()).y,
egui::MouseWheelUnit::Page => canvas_size_in_points(runner.canvas()).y,
egui::MouseWheelUnit::Line => {
#[allow(clippy::let_and_return)]
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit.

14
crates/eframe/src/web/input.rs

@ -1,7 +1,9 @@
use super::{canvas_element, canvas_origin, AppRunner};
use super::{canvas_origin, AppRunner};
pub fn pos_from_mouse_event(canvas_id: &str, event: &web_sys::MouseEvent) -> egui::Pos2 {
let canvas = canvas_element(canvas_id).unwrap();
pub fn pos_from_mouse_event(
canvas: &web_sys::HtmlCanvasElement,
event: &web_sys::MouseEvent,
) -> egui::Pos2 {
let rect = canvas.get_bounding_client_rect();
egui::Pos2 {
x: event.client_x() as f32 - rect.left() as f32,
@ -27,7 +29,7 @@ pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option<egui::Poin
/// `touch_id_for_pos` is the [`TouchId`](egui::TouchId) of the [`Touch`](web_sys::Touch) we previously used to determine the
/// pointer position.
pub fn pos_from_touch_event(
canvas_id: &str,
canvas: &web_sys::HtmlCanvasElement,
event: &web_sys::TouchEvent,
touch_id_for_pos: &mut Option<egui::TouchId>,
) -> egui::Pos2 {
@ -47,7 +49,7 @@ pub fn pos_from_touch_event(
.or_else(|| event.touches().get(0))
.map_or(Default::default(), |touch| {
*touch_id_for_pos = Some(egui::TouchId::from(touch.identifier()));
pos_from_touch(canvas_origin(canvas_id), &touch)
pos_from_touch(canvas_origin(canvas), &touch)
})
}
@ -59,7 +61,7 @@ fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Po
}
pub fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys::TouchEvent) {
let canvas_origin = canvas_origin(runner.canvas_id());
let canvas_origin = canvas_origin(runner.canvas());
for touch_idx in 0..event.changed_touches().length() {
if let Some(touch) = event.changed_touches().item(touch_idx) {
runner.input.raw.events.push(egui::Event::Touch {

21
crates/eframe/src/web/mod.rs

@ -100,26 +100,23 @@ fn theme_from_dark_mode(dark_mode: bool) -> Theme {
}
}
fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
fn get_canvas_element_by_id(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
let document = web_sys::window()?.document()?;
let canvas = document.get_element_by_id(canvas_id)?;
canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()
}
fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement {
canvas_element(canvas_id)
fn get_canvas_element_by_id_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement {
get_canvas_element_by_id(canvas_id)
.unwrap_or_else(|| panic!("Failed to find canvas with id {canvas_id:?}"))
}
fn canvas_origin(canvas_id: &str) -> egui::Pos2 {
let rect = canvas_element(canvas_id)
.unwrap()
.get_bounding_client_rect();
fn canvas_origin(canvas: &web_sys::HtmlCanvasElement) -> egui::Pos2 {
let rect = canvas.get_bounding_client_rect();
egui::pos2(rect.left() as f32, rect.top() as f32)
}
fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 {
let canvas = canvas_element(canvas_id).unwrap();
fn canvas_size_in_points(canvas: &web_sys::HtmlCanvasElement) -> egui::Vec2 {
let pixels_per_point = native_pixels_per_point();
egui::vec2(
canvas.width() as f32 / pixels_per_point,
@ -127,8 +124,10 @@ fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 {
)
}
fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2) -> Option<()> {
let canvas = canvas_element(canvas_id)?;
fn resize_canvas_to_screen_size(
canvas: &web_sys::HtmlCanvasElement,
max_size_points: egui::Vec2,
) -> Option<()> {
let parent = canvas.parent_element()?;
// Prefer the client width and height so that if the parent

10
crates/eframe/src/web/text_agent.rs

@ -5,7 +5,7 @@ use std::{cell::Cell, rc::Rc};
use wasm_bindgen::prelude::*;
use super::{canvas_element, AppRunner, WebRunner};
use super::{AppRunner, WebRunner};
static AGENT_ID: &str = "egui_text_agent";
@ -121,7 +121,7 @@ pub fn update_text_agent(runner: &mut AppRunner) -> Option<()> {
let window = web_sys::window()?;
let document = window.document()?;
let input: HtmlInputElement = document.get_element_by_id(AGENT_ID)?.dyn_into().unwrap();
let canvas_style = canvas_element(runner.canvas_id())?.style();
let canvas_style = runner.canvas().style();
if runner.mutable_text_under_cursor {
let is_already_editing = input.hidden();
@ -205,14 +205,16 @@ fn is_mobile() -> Option<bool> {
// candidate window moves following text element (agent),
// so it appears that the IME candidate window moves with text cursor.
// On mobile devices, there is no need to do that.
pub fn move_text_cursor(ime: Option<egui::output::IMEOutput>, canvas_id: &str) -> Option<()> {
pub fn move_text_cursor(
ime: Option<egui::output::IMEOutput>,
canvas: &web_sys::HtmlCanvasElement,
) -> Option<()> {
let style = text_agent().style();
// Note: moving agent on mobile devices will lead to unpredictable scroll.
if is_mobile() == Some(false) {
ime.as_ref().and_then(|ime| {
let egui::Pos2 { x, y } = ime.cursor_rect.left_top();
let canvas = canvas_element(canvas_id)?;
let bounding_rect = text_agent().get_bounding_client_rect();
let y = (y + (canvas.scroll_top() + canvas.offset_top()) as f32)
.min(canvas.client_height() as f32 - bounding_rect.height() as f32);

4
crates/eframe/src/web/web_painter.rs

@ -9,8 +9,8 @@ pub(crate) trait WebPainter {
// where
// Self: Sized;
/// Id of the canvas in use.
fn canvas_id(&self) -> &str;
/// Reference to the canvas in use.
fn canvas(&self) -> &web_sys::HtmlCanvasElement;
/// Maximum size of a texture in one direction.
fn max_texture_side(&self) -> usize;

13
crates/eframe/src/web/web_painter_glow.rs

@ -10,7 +10,6 @@ use super::web_painter::WebPainter;
pub(crate) struct WebPainterGlow {
canvas: HtmlCanvasElement,
canvas_id: String,
painter: egui_glow::Painter,
}
@ -20,7 +19,7 @@ impl WebPainterGlow {
}
pub async fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String> {
let canvas = super::canvas_element_or_die(canvas_id);
let canvas = super::get_canvas_element_by_id_or_die(canvas_id);
let (gl, shader_prefix) =
init_glow_context_from_canvas(&canvas, options.webgl_context_option)?;
@ -30,11 +29,7 @@ impl WebPainterGlow {
let painter = egui_glow::Painter::new(gl, shader_prefix, None)
.map_err(|err| format!("Error starting glow painter: {err}"))?;
Ok(Self {
canvas,
canvas_id: canvas_id.to_owned(),
painter,
})
Ok(Self { canvas, painter })
}
}
@ -43,8 +38,8 @@ impl WebPainter for WebPainterGlow {
self.painter.max_texture_side()
}
fn canvas_id(&self) -> &str {
&self.canvas_id
fn canvas(&self) -> &HtmlCanvasElement {
&self.canvas
}
fn paint_and_update_textures(

8
crates/eframe/src/web/web_painter_wgpu.rs

@ -41,7 +41,6 @@ impl HasDisplayHandle for EguiWebWindow {
pub(crate) struct WebPainterWgpu {
canvas: HtmlCanvasElement,
canvas_id: String,
surface: wgpu::Surface<'static>,
surface_configuration: wgpu::SurfaceConfiguration,
render_state: Option<RenderState>,
@ -163,7 +162,7 @@ impl WebPainterWgpu {
}
}
let canvas = super::canvas_element_or_die(canvas_id);
let canvas = super::get_canvas_element_by_id_or_die(canvas_id);
let surface = instance
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
@ -188,7 +187,6 @@ impl WebPainterWgpu {
Ok(Self {
canvas,
canvas_id: canvas_id.to_owned(),
render_state: Some(render_state),
surface,
surface_configuration,
@ -200,8 +198,8 @@ impl WebPainterWgpu {
}
impl WebPainter for WebPainterWgpu {
fn canvas_id(&self) -> &str {
&self.canvas_id
fn canvas(&self) -> &HtmlCanvasElement {
&self.canvas
}
fn max_texture_side(&self) -> usize {

28
crates/eframe/src/web/web_runner.rs

@ -1,4 +1,7 @@
use std::{cell::RefCell, rc::Rc};
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
use wasm_bindgen::prelude::*;
@ -24,6 +27,9 @@ pub struct WebRunner {
/// They have to be in a separate `Rc` so that we don't need to pass them to
/// the panic handler, since they aren't `Send`.
events_to_unsubscribe: Rc<RefCell<Vec<EventToUnsubscribe>>>,
/// Used in `destroy` to cancel a pending frame.
request_animation_frame_id: Cell<Option<i32>>,
}
impl WebRunner {
@ -41,6 +47,7 @@ impl WebRunner {
panic_handler,
runner: Rc::new(RefCell::new(None)),
events_to_unsubscribe: Rc::new(RefCell::new(Default::default())),
request_animation_frame_id: Cell::new(None),
}
}
@ -71,7 +78,7 @@ impl WebRunner {
events::install_color_scheme_change_event(self)?;
}
events::request_animation_frame(self.clone())?;
self.request_animation_frame()?;
}
Ok(())
@ -108,6 +115,11 @@ impl WebRunner {
pub fn destroy(&self) {
self.unsubscribe_from_all_events();
if let Some(id) = self.request_animation_frame_id.get() {
let window = web_sys::window().unwrap();
window.cancel_animation_frame(id).ok();
}
if let Some(runner) = self.runner.replace(None) {
runner.destroy();
}
@ -179,6 +191,18 @@ impl WebRunner {
Ok(())
}
pub(crate) fn request_animation_frame(&self) -> Result<(), wasm_bindgen::JsValue> {
let window = web_sys::window().unwrap();
let closure = Closure::once({
let runner_ref = self.clone();
move || events::paint_and_schedule(&runner_ref)
});
let id = window.request_animation_frame(closure.as_ref().unchecked_ref())?;
self.request_animation_frame_id.set(Some(id));
closure.forget(); // We must forget it, or else the callback is canceled on drop
Ok(())
}
}
// ----------------------------------------------------------------------------

Loading…
Cancel
Save