From e23f80033dd04c6de4d058c009e00cfd46ab0b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= Date: Mon, 27 May 2024 21:41:28 +0200 Subject: [PATCH] Use ResizeObserver instead of `resize` event (#4536) Currently, if the size of the canvas element changes independently of the size of the browser window (e.g. due to its parent element shrinking), then no repaints are scheduled. This PR replaces the `resize` event with a `ResizeObserver`, which ensures that _any_ resize of the canvas element (including those caused by browser window resizes) trigger a repaint. The repaint is done synchronously as part of the resize event, to reduce any potential flickering. The result seems to pass the rendering tests on most platform+browser combinations. We tested: - Chrome, Firefox, Safari on macOS - Chrome, Firefox on Linux (ubuntu and arch, both running wayland) - Chrome, Firefox on Windows Firefox still has some antialiasing issues on Linux platforms, but this antialiasing also happens on `master`, so this PR is not a regression there. The code setting `canvas.style.width` and `canvas.style.height` at the start of `AppRunner::logic` was also removed - the canvas _display_ size is now fully controlled by CSS, e.g. by setting `canvas { width: 100%; height: 100%; }`. The approach used here is described in https://webglfundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html Note: The only remaining place where egui updates the style of the canvas it is rendering to is some of the IME/mobile input handling code. Fixing that is out of scope for this PR, and will be done in a followup PR. --- crates/eframe/Cargo.toml | 7 +++ crates/eframe/src/epi.rs | 7 --- crates/eframe/src/web/app_runner.rs | 2 +- crates/eframe/src/web/events.rs | 79 ++++++++++++++++++++++++++++- crates/eframe/src/web/mod.rs | 51 ------------------- crates/eframe/src/web/web_runner.rs | 32 +++++++++++- web_demo/index.html | 7 +-- 7 files changed, 121 insertions(+), 64 deletions(-) diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 4a5a9918a12..acd7600f6e6 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -237,7 +237,14 @@ web-sys = { workspace = true, features = [ "MediaQueryListEvent", "MouseEvent", "Navigator", + "Node", + "NodeList", "Performance", + "ResizeObserver", + "ResizeObserverEntry", + "ResizeObserverBoxOptions", + "ResizeObserverOptions", + "ResizeObserverSize", "Storage", "Touch", "TouchEvent", diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 6d15c39e9e8..7b75811ccbc 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -464,11 +464,6 @@ pub struct WebOptions { /// Configures wgpu instance/device/adapter/surface creation and renderloop. #[cfg(feature = "wgpu")] pub wgpu_options: egui_wgpu::WgpuConfiguration, - - /// The size limit of the web app canvas. - /// - /// By default the max size is [`egui::Vec2::INFINITY`], i.e. unlimited. - pub max_size_points: egui::Vec2, } #[cfg(target_arch = "wasm32")] @@ -484,8 +479,6 @@ impl Default for WebOptions { #[cfg(feature = "wgpu")] wgpu_options: egui_wgpu::WgpuConfiguration::default(), - - max_size_points: egui::Vec2::INFINITY, } } } diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 620fe86fd62..872921591ac 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -5,6 +5,7 @@ use crate::{epi, App}; use super::{now_sec, web_painter::WebPainter, NeedRepaint}; pub struct AppRunner { + #[allow(dead_code)] web_options: crate::WebOptions, pub(crate) frame: epi::Frame, egui_ctx: egui::Context, @@ -184,7 +185,6 @@ 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(), self.web_options.max_size_points); let canvas_size = super::canvas_size_in_points(self.canvas(), self.egui_ctx()); let raw_input = self.input.new_frame(canvas_size); diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 56d3fdf071a..33d36c356d7 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -247,7 +247,8 @@ pub(crate) fn install_window_events(runner_ref: &WebRunner) -> Result<(), JsValu runner.save(); })?; - for event_name in &["load", "pagehide", "pageshow", "resize"] { + // NOTE: resize is handled by `ResizeObserver` below + for event_name in &["load", "pagehide", "pageshow"] { runner_ref.add_event_listener(&window, event_name, move |_: web_sys::Event, runner| { // log::debug!("{event_name:?}"); runner.needs_repaint.repaint_asap(); @@ -590,3 +591,79 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu Ok(()) } + +pub(crate) fn install_resize_observer(runner_ref: &WebRunner) -> Result<(), JsValue> { + let closure = Closure::wrap(Box::new({ + let runner_ref = runner_ref.clone(); + move |entries: js_sys::Array| { + // Only call the wrapped closure if the egui code has not panicked + if let Some(mut runner_lock) = runner_ref.try_lock() { + let canvas = runner_lock.canvas(); + let (width, height) = match get_display_size(&entries) { + Ok(v) => v, + Err(err) => { + log::error!("{}", super::string_from_js_value(&err)); + return; + } + }; + canvas.set_width(width); + canvas.set_height(height); + + // force an immediate repaint + runner_lock.needs_repaint.repaint_asap(); + paint_if_needed(&mut runner_lock); + } + } + }) as Box); + + let observer = web_sys::ResizeObserver::new(closure.as_ref().unchecked_ref())?; + let mut options = web_sys::ResizeObserverOptions::new(); + options.box_(web_sys::ResizeObserverBoxOptions::ContentBox); + if let Some(runner_lock) = runner_ref.try_lock() { + observer.observe_with_options(runner_lock.canvas(), &options); + drop(runner_lock); + runner_ref.set_resize_observer(observer, closure); + } + + Ok(()) +} + +// Code ported to Rust from: +// https://webglfundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html +fn get_display_size(resize_observer_entries: &js_sys::Array) -> Result<(u32, u32), JsValue> { + let width; + let height; + let mut dpr = web_sys::window().unwrap().device_pixel_ratio(); + + let entry: web_sys::ResizeObserverEntry = resize_observer_entries.at(0).dyn_into()?; + if JsValue::from_str("devicePixelContentBoxSize").js_in(entry.as_ref()) { + // NOTE: Only this path gives the correct answer for most browsers. + // Unfortunately this doesn't work perfectly everywhere. + let size: web_sys::ResizeObserverSize = + entry.device_pixel_content_box_size().at(0).dyn_into()?; + width = size.inline_size(); + height = size.block_size(); + dpr = 1.0; // no need to apply + } else if JsValue::from_str("contentBoxSize").js_in(entry.as_ref()) { + let content_box_size = entry.content_box_size(); + let idx0 = content_box_size.at(0); + if !idx0.is_undefined() { + let size: web_sys::ResizeObserverSize = idx0.dyn_into()?; + width = size.inline_size(); + height = size.block_size(); + } else { + // legacy + let size = JsValue::clone(content_box_size.as_ref()); + let size: web_sys::ResizeObserverSize = size.dyn_into()?; + width = size.inline_size(); + height = size.block_size(); + } + } else { + // legacy + let content_rect = entry.content_rect(); + width = content_rect.width(); + height = content_rect.height(); + } + + Ok(((width.round() * dpr) as u32, (height.round() * dpr) as u32)) +} diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 566c9b03c8d..f98bbc1ed01 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -40,7 +40,6 @@ pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu; pub use backend::*; -use egui::Vec2; use wasm_bindgen::prelude::*; use web_sys::MediaQueryList; @@ -124,56 +123,6 @@ fn canvas_size_in_points(canvas: &web_sys::HtmlCanvasElement, ctx: &egui::Contex ) } -fn resize_canvas_to_screen_size( - canvas: &web_sys::HtmlCanvasElement, - max_size_points: egui::Vec2, -) -> Option<()> { - let parent = canvas.parent_element()?; - - // In this function we use "pixel" to mean physical pixel, - // and "point" to mean "logical CSS pixel". - let pixels_per_point = native_pixels_per_point(); - - // Prefer the client width and height so that if the parent - // element is resized that the egui canvas resizes appropriately. - let parent_size_points = Vec2 { - x: parent.client_width() as f32, - y: parent.client_height() as f32, - }; - - if parent_size_points.x <= 0.0 || parent_size_points.y <= 0.0 { - log::error!("The parent element of the egui canvas is {}x{}. Try adding `html, body {{ height: 100%; width: 100% }}` to your CSS!", parent_size_points.x, parent_size_points.y); - } - - // We take great care here to ensure the rendered canvas aligns - // perfectly to the physical pixel grid, lest we get blurry text. - // At the time of writing, we get pixel perfection on Chromium and Firefox on Mac, - // but Desktop Safari will be blurry on most zoom levels. - // See https://github.com/emilk/egui/issues/4241 for more. - - let canvas_size_pixels = pixels_per_point * parent_size_points.min(max_size_points); - - // Make sure that the size is always an even number of pixels, - // otherwise, the page renders blurry on some platforms. - // See https://github.com/emilk/egui/issues/103 - let canvas_size_pixels = (canvas_size_pixels / 2.0).round() * 2.0; - - let canvas_size_points = canvas_size_pixels / pixels_per_point; - - canvas - .style() - .set_property("width", &format!("{}px", canvas_size_points.x)) - .ok()?; - canvas - .style() - .set_property("height", &format!("{}px", canvas_size_points.y)) - .ok()?; - canvas.set_width(canvas_size_pixels.x as u32); - canvas.set_height(canvas_size_pixels.y as u32); - - Some(()) -} - // ---------------------------------------------------------------------------- /// Set the cursor icon. diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index f39fc0dcace..35bb4be334b 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -28,8 +28,10 @@ pub struct WebRunner { /// the panic handler, since they aren't `Send`. events_to_unsubscribe: Rc>>, - /// Used in `destroy` to cancel a pending frame. + /// Current animation frame in flight. request_animation_frame_id: Cell>, + + resize_observer: Rc>>, } impl WebRunner { @@ -48,6 +50,7 @@ impl WebRunner { runner: Rc::new(RefCell::new(None)), events_to_unsubscribe: Rc::new(RefCell::new(Default::default())), request_animation_frame_id: Cell::new(None), + resize_observer: Default::default(), } } @@ -78,6 +81,8 @@ impl WebRunner { events::install_color_scheme_change_event(self)?; } + events::install_resize_observer(self)?; + self.request_animation_frame()?; } @@ -109,6 +114,11 @@ impl WebRunner { } } } + + if let Some(context) = self.resize_observer.take() { + context.resize_observer.disconnect(); + drop(context.closure); + } } /// Shut down eframe and clean up resources. @@ -198,15 +208,35 @@ impl WebRunner { 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(()) } + + pub(crate) fn set_resize_observer( + &self, + resize_observer: web_sys::ResizeObserver, + closure: Closure, + ) { + self.resize_observer + .borrow_mut() + .replace(ResizeObserverContext { + resize_observer, + closure, + }); + } } // ---------------------------------------------------------------------------- +struct ResizeObserverContext { + resize_observer: web_sys::ResizeObserver, + closure: Closure, +} + struct TargetEvent { target: web_sys::EventTarget, event_name: String, diff --git a/web_demo/index.html b/web_demo/index.html index 142355b48f8..c4b3bac0095 100644 --- a/web_demo/index.html +++ b/web_demo/index.html @@ -48,9 +48,10 @@ margin-left: auto; display: block; position: absolute; - top: 0%; - left: 50%; - transform: translate(-50%, 0%); + top: 0; + left: 0; + width: 100%; + height: 100%; } .centered {