diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index f992c5c66a2..8220b9e5306 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -236,7 +236,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 db39a08b15b..feb401c269f 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -455,11 +455,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")] @@ -475,8 +470,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 {