Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use ResizeObserver instead of resize event #4536

Merged
merged 15 commits into from
May 27, 2024
7 changes: 7 additions & 0 deletions crates/eframe/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,14 @@ web-sys = { workspace = true, features = [
"MediaQueryListEvent",
"MouseEvent",
"Navigator",
"Node",
"NodeList",
"Performance",
"ResizeObserver",
"ResizeObserverEntry",
"ResizeObserverBoxOptions",
"ResizeObserverOptions",
"ResizeObserverSize",
"Storage",
"Touch",
"TouchEvent",
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);
jprochazk marked this conversation as resolved.
Show resolved Hide resolved
let canvas_size = super::canvas_size_in_points(self.canvas(), self.egui_ctx());
let raw_input = self.input.new_frame(canvas_size);

Expand Down
71 changes: 70 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,71 @@ 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() {
runner_lock.needs_repaint.repaint_asap();
let canvas = runner_lock.canvas();
let (width, height) = get_display_size(&entries).unwrap();
jprochazk marked this conversation as resolved.
Show resolved Hide resolved
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);
observer.observe_with_options(runner_ref.try_lock().unwrap().canvas(), &options);
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.
jprochazk marked this conversation as resolved.
Show resolved Hide resolved
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))
jprochazk marked this conversation as resolved.
Show resolved Hide resolved
}
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
68 changes: 57 additions & 11 deletions crates/eframe/src/web/web_runner.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
use std::{cell::RefCell, rc::Rc};

use wasm_bindgen::prelude::*;

Expand All @@ -28,8 +25,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.
request_animation_frame_id: Cell<Option<i32>>,
/// Current animation frame in flight.
emilk marked this conversation as resolved.
Show resolved Hide resolved
frame: Rc<RefCell<Option<AnimationFrameRequest>>>,

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

impl WebRunner {
Expand All @@ -47,7 +46,8 @@ 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),
frame: Default::default(),
resize_observer: Default::default(),
}
}

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

events::install_resize_observer(self)?;

self.request_animation_frame()?;
}

Expand Down Expand Up @@ -109,15 +111,21 @@ impl WebRunner {
}
}
}

if let Some(context) = self.resize_observer.take() {
context.resize_observer.disconnect();
drop(context.closure);
}
}

/// Shut down eframe and clean up resources.
pub fn destroy(&self) {
self.unsubscribe_from_all_events();

if let Some(id) = self.request_animation_frame_id.get() {
if let Some(frame) = self.frame.take() {
let window = web_sys::window().unwrap();
window.cancel_animation_frame(id).ok();
window.cancel_animation_frame(frame.id).ok();
drop(frame.closure);
}

if let Some(runner) = self.runner.replace(None) {
Expand Down Expand Up @@ -193,20 +201,58 @@ impl WebRunner {
}

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
return Ok(());
}

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
self.frame
.borrow_mut()
.replace(AnimationFrameRequest { id, closure });

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 AnimationFrameRequest {
/// Represents the ID of a frame in flight.
///
/// This is only set between a call to `request_animation_frame` and the invocation of its callback,
/// which means that repeated calls to `request_animation_frame` will be ignored.
id: i32,

/// 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>>,
}

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
2 changes: 2 additions & 0 deletions web_demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
position: absolute;
top: 0%;
left: 50%;
width: 100%;
height: 100%;
Comment on lines +53 to +54
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this now required? Then please add a migration guide to the PR description!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not required, but by default a canvas element is very small (300x150px). It's now entirely up to the user how they decide to display it, so any width/height is fine.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure where to add a migration guide, because there's nothing to migrate over here yet, because it is still not possible to set width/height without having it overriden by egui. The IME handling still sets the canvas style, but it'd be better to leave that to its own PR. This one is just to get the canvas resize working properly.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So a user that has copy-pasted the old index.html would not need to change anything when updating eframe?

Copy link
Collaborator Author

@jprochazk jprochazk May 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They would have to change things to get the old behavior (take up 100% of the parent element), but it doesn't yet to the right thing. I guess what I'm asking is does the migration guide go into this PR or the followup that fixes IME and makes this whole thing work like it should?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it can wait 👍

transform: translate(-50%, 0%);
}

Expand Down
Loading