Skip to content

Commit

Permalink
Use ResizeObserver instead of resize event (emilk#4536)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jprochazk authored and hacknus committed Oct 30, 2024
1 parent 4b9c7b4 commit e23f800
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 64 deletions.
7 changes: 7 additions & 0 deletions crates/eframe/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,14 @@ web-sys = { workspace = true, features = [
"MediaQueryListEvent",
"MouseEvent",
"Navigator",
"Node",
"NodeList",
"Performance",
"ResizeObserver",
"ResizeObserverEntry",
"ResizeObserverBoxOptions",
"ResizeObserverOptions",
"ResizeObserverSize",
"Storage",
"Touch",
"TouchEvent",
Expand Down
7 changes: 0 additions & 7 deletions crates/eframe/src/epi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -484,8 +479,6 @@ impl Default for WebOptions {

#[cfg(feature = "wgpu")]
wgpu_options: egui_wgpu::WgpuConfiguration::default(),

max_size_points: egui::Vec2::INFINITY,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/eframe/src/web/app_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);

Expand Down
79 changes: 78 additions & 1 deletion crates/eframe/src/web/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<dyn FnMut(js_sys::Array)>);

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))
}
51 changes: 0 additions & 51 deletions crates/eframe/src/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand Down
32 changes: 31 additions & 1 deletion crates/eframe/src/web/web_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ pub struct WebRunner {
/// the panic handler, since they aren't `Send`.
events_to_unsubscribe: Rc<RefCell<Vec<EventToUnsubscribe>>>,

/// Used in `destroy` to cancel a pending frame.
/// Current animation frame in flight.
request_animation_frame_id: Cell<Option<i32>>,

resize_observer: Rc<RefCell<Option<ResizeObserverContext>>>,
}

impl WebRunner {
Expand All @@ -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(),
}
}

Expand Down Expand Up @@ -78,6 +81,8 @@ impl WebRunner {
events::install_color_scheme_change_event(self)?;
}

events::install_resize_observer(self)?;

self.request_animation_frame()?;
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<dyn FnMut(js_sys::Array)>,
) {
self.resize_observer
.borrow_mut()
.replace(ResizeObserverContext {
resize_observer,
closure,
});
}
}

// ----------------------------------------------------------------------------

struct ResizeObserverContext {
resize_observer: web_sys::ResizeObserver,
closure: Closure<dyn FnMut(js_sys::Array)>,
}

struct TargetEvent {
target: web_sys::EventTarget,
event_name: String,
Expand Down
7 changes: 4 additions & 3 deletions web_demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit e23f800

Please sign in to comment.