From f5e3d77ae1d9e1d11436852cdf92bbe8c4d87021 Mon Sep 17 00:00:00 2001 From: Liam Murphy Date: Mon, 22 Nov 2021 20:24:38 +1100 Subject: [PATCH 1/8] Use `ResizeObserver` to control the size of the canvas --- CHANGELOG.md | 1 + Cargo.toml | 14 +- src/platform_impl/web/event_loop/mod.rs | 2 + src/platform_impl/web/event_loop/resize.rs | 214 ++++++++++++ src/platform_impl/web/event_loop/runner.rs | 321 +++++++++++++----- .../web/event_loop/window_target.rs | 60 ++-- src/platform_impl/web/mod.rs | 9 +- src/platform_impl/web/web_sys/canvas.rs | 4 + src/platform_impl/web/web_sys/mod.rs | 124 +++++-- src/platform_impl/web/web_sys/scaling.rs | 17 +- src/platform_impl/web/window.rs | 48 ++- 11 files changed, 655 insertions(+), 159 deletions(-) create mode 100644 src/platform_impl/web/event_loop/resize.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e648fd508..4c088bb80c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -147,6 +147,7 @@ And please only add new entries to the top of this list, right below the `# Unre - On Wayland, implement a workaround for wrong configure size when using `xdg_decoration` in `kwin_wayland` - On macOS, fix an issue that prevented the menu bar from showing in borderless fullscreen mode. - On X11, EINTR while polling for events no longer causes a panic. Instead it will be treated as a spurious wakeup. +- **Breaking:** On Web, size the canvas based on the page layout using `ResizeObserver`. `set_inner_size` will just set the CSS `width` and `height` properties. # 0.25.0 (2021-05-15) diff --git a/Cargo.toml b/Cargo.toml index 6bd0ee780d..329bc9bb7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,14 @@ wayland-dlopen = ["sctk/dlopen", "wayland-client/dlopen"] wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/ab_glyph"] wayland-csd-adwaita-crossfont = ["sctk-adwaita", "sctk-adwaita/crossfont"] wayland-csd-adwaita-notitle = ["sctk-adwaita"] +css-size = [ + "js-sys", + "web_sys/ResizeObserver", + "web_sys/ResizeObserverBoxOptions", + "web_sys/ResizeObserverEntry", + "web_sys/ResizeObserverOptions", + "web_sys/ResizeObserverSize", +] [dependencies] instant = { version = "0.1", features = ["wasm-bindgen"] } @@ -113,7 +121,7 @@ libc = "0.2.64" [target.'cfg(target_arch = "wasm32")'.dependencies.web_sys] package = "web-sys" -version = "0.3.22" +version = "0.3.56" features = [ 'console', "AddEventListenerOptions", @@ -140,6 +148,10 @@ features = [ [target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen] version = "0.2.45" +[target.'cfg(target_arch = "wasm32")'.dependencies.js-sys] +version = "0.3.56" +optional = true + [target.'cfg(target_arch = "wasm32")'.dev-dependencies] console_log = "0.2" web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d'] } diff --git a/src/platform_impl/web/event_loop/mod.rs b/src/platform_impl/web/event_loop/mod.rs index 6bec70c972..7bcd780046 100644 --- a/src/platform_impl/web/event_loop/mod.rs +++ b/src/platform_impl/web/event_loop/mod.rs @@ -1,4 +1,6 @@ mod proxy; +#[cfg(feature = "css-size")] +mod resize; mod runner; mod state; mod window_target; diff --git a/src/platform_impl/web/event_loop/resize.rs b/src/platform_impl/web/event_loop/resize.rs new file mode 100644 index 0000000000..78a2e49d6a --- /dev/null +++ b/src/platform_impl/web/event_loop/resize.rs @@ -0,0 +1,214 @@ +use js_sys::Array; +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; +use web_sys::HtmlCanvasElement; +use web_sys::ResizeObserver; +use web_sys::ResizeObserverBoxOptions; +use web_sys::ResizeObserverEntry; +use web_sys::ResizeObserverOptions; +use web_sys::ResizeObserverSize; + +use crate::dpi::LogicalSize; +use crate::dpi::PhysicalSize; +use crate::platform_impl::platform::backend; +use crate::platform_impl::platform::backend::ScaleChangeDetector; +use crate::window::WindowId; + +use super::runner::Shared; + +fn process_entry(entry: &ResizeObserverEntry) -> (WindowId, HtmlCanvasElement, PhysicalSize) { + let canvas: HtmlCanvasElement = entry.target().dyn_into().unwrap(); + + let id = WindowId(super::window::Id( + canvas + .get_attribute("data-raw-handle") + .expect("Canvas was missing `data-raw-handle` attribute") + .parse() + .expect("Canvas had invalid `data-raw-handle` attribute"), + )); + + let size = if entry.device_pixel_content_box_size().is_undefined() { + // Safari doesn't support `devicePixelContentBoxSize` yet (nor `contentBoxSize`), so fall back to `contentRect` and use the scale factor to convert. + let rect = entry.content_rect(); + + LogicalSize::new(rect.width(), rect.height()).to_physical(super::backend::scale_factor()) + } else { + // TODO: what exactly would cause there to be multiple of these? + let size: ResizeObserverSize = entry + .device_pixel_content_box_size() + .get(0) + .dyn_into() + .expect( + "`ResizeObserverEntry.devicePixelContentBoxSize` was not a `ResizeObserverSize`", + ); + + let window = web_sys::window().unwrap(); + let style = window + .get_computed_style(&canvas) + .expect("`getComputedStyle` failed") + .expect("`getComputedStyle` returned `None`"); + + match style.get_property_value("writing-mode").unwrap().as_str() { + "vertical-lr" | "vertical-rl" | "sideways-lr" | "sideways-rl" | "tb" | "tb-lr" + | "tb-rl" => { + // The text is flowing vertically, so `inline_size` is height and `block_size` is width. + PhysicalSize { + width: size.block_size() as u32, + height: size.inline_size() as u32, + } + } + // If it isn't a known value, default to horizontal, + // since it's probably a browser which doesn't support this or something. + _ => PhysicalSize { + width: size.inline_size() as u32, + height: size.block_size() as u32, + }, + } + }; + + (id, canvas, size) +} + +pub enum ResizeState { + /// Used on platforms with support for `device-pixel-content-box`. + /// + /// `physical_observer`, a `ResizeObserver` configured to watch `device-pixel-content-box`, is used most of the time. + /// However, sometimes when the scale factor has changed, only the logical sizes of the canvases will have changed, + /// and we'll fall back on `logical_observer`. + WithPhysicalObserver { + _logical_closure: Closure, + _physical_closure: Closure, + logical_observer: ResizeObserver, + physical_observer: ResizeObserver, + }, + /// Used on platforms without support for `device-pixel-content-box`. + /// + /// `observer`, a `ResizeObserver` configured to watch the logical size, is mostly used. + /// However, sometimes when the scale factor changes, only the physical sizes of the canvases change, + /// in which case `scale_change_detector` is used. + NoPhysicalObserver { + scale_change_detector: ScaleChangeDetector, + closure: Closure, + observer: ResizeObserver, + }, +} + +impl ResizeState { + pub fn new(runner: Shared) -> Self { + if backend::supports_device_pixel_content_size() { + let physical_closure = { + let runner = runner.clone(); + Closure::wrap(Box::new(move |entries: Array| { + let resizes: Vec<_> = entries + .iter() + .map(|entry| { + let entry: ResizeObserverEntry = entry.dyn_into().expect("`ResizeObserver` callback not called with array of `ResizeObserverEntry`"); + + process_entry(&entry) + }) + .collect(); + + runner.handle_resizes(resizes); + }) as Box) + }; + + let logical_closure = Closure::wrap(Box::new(move || { + if runner.scale_factor_changed() { + // If the scale factor is still incorrect, the physical `ResizeObserver` must not have run. + // Just call this with an empty `Vec`, since it'll then automatically resize everything to its existing size, + // which is correct because none of them must have changed for the physical `ResizeObserver` not to have run. + runner.handle_resizes(vec![]); + } + }) as Box); + + // Create the physical `ResizeObserver` first, because that'll make it fire first. + // It will handle everything most of the time, and the logical `ResizeObserver` will only do anything if the physical one hasn't run. + let physical_observer = + ResizeObserver::new(physical_closure.as_ref().unchecked_ref()).unwrap(); + let logical_observer = + ResizeObserver::new(logical_closure.as_ref().unchecked_ref()).unwrap(); + + Self::WithPhysicalObserver { + _logical_closure: logical_closure, + _physical_closure: physical_closure, + logical_observer, + physical_observer, + } + } else { + let scale_change_detector = { + let runner = runner.clone(); + ScaleChangeDetector::new(move || runner.handle_scale_changed(true)) + }; + + let closure = Closure::wrap(Box::new(move |entries: Array| { + if runner.scale_factor_changed() { + runner.handle_scale_changed(false); + } else { + let resizes = entries.iter().map(|entry| { + let entry: ResizeObserverEntry = entry.dyn_into().expect("`ResizeObserver` callback not called with array of `ResizeObserverEntry`"); + + process_entry(&entry) + }).collect(); + + runner.handle_resizes(resizes) + } + }) as Box); + + let observer = ResizeObserver::new(closure.as_ref().unchecked_ref()).unwrap(); + + Self::NoPhysicalObserver { + scale_change_detector, + closure, + observer, + } + } + } + + pub fn observe(&self, canvas: &HtmlCanvasElement) { + match self { + Self::WithPhysicalObserver { + logical_observer, + physical_observer, + .. + } => { + logical_observer.observe(canvas); + physical_observer.observe_with_options( + canvas, + ResizeObserverOptions::new() + .box_(ResizeObserverBoxOptions::DevicePixelContentBox), + ); + } + Self::NoPhysicalObserver { observer, .. } => observer.observe(canvas), + } + } + + pub fn unobserve(&self, canvas: &HtmlCanvasElement) { + match self { + Self::WithPhysicalObserver { + logical_observer, + physical_observer, + .. + } => { + logical_observer.unobserve(canvas); + physical_observer.unobserve(canvas); + } + Self::NoPhysicalObserver { observer, .. } => observer.unobserve(canvas), + } + } +} + +impl Drop for ResizeState { + fn drop(&mut self) { + match self { + Self::WithPhysicalObserver { + logical_observer, + physical_observer, + .. + } => { + logical_observer.disconnect(); + physical_observer.disconnect(); + } + Self::NoPhysicalObserver { observer, .. } => observer.disconnect(), + } + } +} diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index 55acc1ec1c..5b3aaa5497 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -1,9 +1,15 @@ -use super::{super::ScaleChangeArgs, backend, state::State}; +#[cfg(feature = "css-size")] +use super::resize::ResizeState; +use super::{backend, state::State}; +use crate::dpi::PhysicalSize; use crate::event::{Event, StartCause}; use crate::event_loop::ControlFlow; use crate::window::WindowId; use instant::{Duration, Instant}; +use std::cell::Cell; +#[cfg(feature = "css-size")] +use std::cell::Ref; use std::{ cell::RefCell, clone::Clone, @@ -12,6 +18,7 @@ use std::{ ops::Deref, rc::{Rc, Weak}, }; +use web_sys::HtmlCanvasElement; pub struct Shared(Rc>); @@ -27,8 +34,13 @@ pub struct Execution { id: RefCell, all_canvases: RefCell>)>>, redraw_pending: RefCell>, - destroy_pending: RefCell>, + destroy_pending: RefCell>, + /// This is initially `None`, because it requires a handle on the runner. + #[cfg(feature = "css-size")] + resize_state: RefCell>, + #[cfg(not(feature = "css-size"))] scale_change_detector: RefCell>, + last_scale: Cell, unload_event_handle: RefCell>, } @@ -97,27 +109,61 @@ impl Runner { impl Shared { pub fn new() -> Self { - Shared(Rc::new(Execution { + let this = Shared(Rc::new(Execution { runner: RefCell::new(RunnerEnum::Pending), events: RefCell::new(VecDeque::new()), id: RefCell::new(0), all_canvases: RefCell::new(Vec::new()), redraw_pending: RefCell::new(HashSet::new()), destroy_pending: RefCell::new(VecDeque::new()), + #[cfg(feature = "css-size")] + resize_state: RefCell::new(None), + #[cfg(not(feature = "css-size"))] scale_change_detector: RefCell::new(None), + last_scale: Cell::new(backend::scale_factor()), unload_event_handle: RefCell::new(None), - })) + })); + + #[cfg(feature = "css-size")] + { + *this.0.resize_state.borrow_mut() = Some(ResizeState::new(this.clone())); + } + #[cfg(not(feature = "css-size"))] + { + let runner = this.clone(); + *this.0.scale_change_detector.borrow_mut() = + Some(backend::ScaleChangeDetector::new(move || { + runner.handle_scale_changed(false) + })) + } + + this } pub fn add_canvas(&self, id: WindowId, canvas: &Rc>) { + if self.num_canvases() == 0 { + // If we've had no canvases, the `ResizeObserver`s won't have been observing anything, + // and so we might not have noticed some changes in scale factor. + // So, make sure it's up to date. + self.0.last_scale.set(backend::scale_factor()); + } + self.0 .all_canvases .borrow_mut() .push((id, Rc::downgrade(canvas))); + + #[cfg(feature = "css-size")] + { + let resize_state = Ref::map(self.0.resize_state.borrow(), |resize_state| { + resize_state.as_ref().unwrap() + }); + resize_state.observe(canvas.borrow().raw()); + } } - pub fn notify_destroy_window(&self, id: WindowId) { - self.0.destroy_pending.borrow_mut().push_back(id); + pub fn notify_destroy_window(&self, id: WindowId, canvas: HtmlCanvasElement) { + self.0.destroy_pending.borrow_mut().push_back((id, canvas)); } // Set the event callback to use for the event loop runner @@ -136,14 +182,6 @@ impl Shared { Some(backend::on_unload(move || close_instance.handle_unload())); } - pub(crate) fn set_on_scale_change(&self, handler: F) - where - F: 'static + FnMut(ScaleChangeArgs), - { - *self.0.scale_change_detector.borrow_mut() = - Some(backend::ScaleChangeDetector::new(handler)); - } - // Generate a strictly increasing ID // This is used to differentiate windows when handling events pub fn generate_id(&self) -> u32 { @@ -244,7 +282,16 @@ impl Shared { // `run_until_cleared` and `handle_scale_changed`, somewhere between emitting // `NewEvents` and `MainEventsCleared`. fn process_destroy_pending_windows(&self, control: &mut ControlFlow) { - while let Some(id) = self.0.destroy_pending.borrow_mut().pop_front() { + #[cfg(feature = "css-size")] + let resize_state = Ref::map(self.0.resize_state.borrow(), |resize_state| { + resize_state.as_ref().unwrap() + }); + + // `canvas` isn't used when we aren't using `ResizeObserver`. + #[cfg_attr(not(feature = "css-size"), allow(unused_variables))] + while let Some((id, canvas)) = self.0.destroy_pending.borrow_mut().pop_front() { + #[cfg(feature = "css-size")] + resize_state.unobserve(&canvas); self.0 .all_canvases .borrow_mut() @@ -287,82 +334,174 @@ impl Shared { } } - pub fn handle_scale_changed(&self, old_scale: f64, new_scale: f64) { - // If there aren't any windows, then there is nothing to do here. - if self.0.all_canvases.borrow().is_empty() { + /// Handle a change in scale factor, without any other information. + /// + /// `initial` is whether this is coming from the initial `ScaleChangeDetector`, rather than the `ResizeObserver`. + pub fn handle_scale_changed(&self, initial: bool) { + let canvases = self.0.all_canvases.borrow(); + if canvases.is_empty() { + self.0.last_scale.set(backend::scale_factor()); return; } - let start_cause = match (self.0.runner.borrow().maybe_runner()) - .unwrap_or_else(|| unreachable!("`scale_changed` should not happen without a runner")) - .maybe_start_cause() - { - Some(c) => c, - // If we're in the exit state, don't do event processing - None => return, - }; - let mut control = self.current_control_flow(); + let old_scale = self.0.last_scale.get(); + let new_scale = backend::scale_factor(); + // Whether or not the `ResizeObserver` will run; i.e., whether any canvases' logical sizes have changed. + let mut observer_will_run = false; - // Handle the start event and all other events in the queue. - self.handle_event(Event::NewEvents(start_cause), &mut control); + let resizes = self + .0 + .all_canvases + .borrow() + .iter() + .filter_map(|(id, canvas)| { + // If the canvas was destroyed, it's not going to have been resized, so just skip it. + let canvas = canvas.upgrade()?; + let canvas = canvas.borrow(); + + // If the canvas isn't in the DOM, we don't need to handle the scale factor change for it. + let content_size = backend::inner_size(canvas.raw())?; + + if canvas.size() != content_size.to_physical(old_scale) { + observer_will_run = true; + } - // It is possible for windows to be dropped before this point. We don't - // want to send `ScaleFactorChanged` for destroyed windows, so we process - // the destroy-pending windows here. - self.process_destroy_pending_windows(&mut control); + Some(( + *id, + canvas.raw().clone(), + content_size.to_physical(new_scale), + )) + }) + .collect(); - // Now handle the `ScaleFactorChanged` events. - for &(id, ref canvas) in &*self.0.all_canvases.borrow() { - let canvas = match canvas.upgrade() { - Some(rc) => rc.borrow().raw().clone(), - // This shouldn't happen, but just in case... - None => continue, - }; - // First, we send the `ScaleFactorChanged` event: - let current_size = crate::dpi::PhysicalSize { - width: canvas.width() as u32, - height: canvas.height() as u32, + // Don't handle the resizes yet if the `ResizeObserver` is going to run still. + if !(initial && observer_will_run) { + self.handle_resizes(resizes); + } + } + + /// Handle a set of window resizes, as well as a scale factor change if any. + pub fn handle_resizes( + &self, + mut resizes: Vec<(WindowId, HtmlCanvasElement, PhysicalSize)>, + ) { + // Don't send resize events for destroyed canvases. + resizes.retain(|&(id, ..)| { + !self + .0 + .destroy_pending + .borrow() + .iter() + .any(|&(other_id, ..)| other_id == id) + }); + + let scale = backend::scale_factor(); + + let scale_changed = self.0.last_scale.replace(scale) != scale; + + let mut control = self.current_control_flow(); + + if scale_changed { + let start_cause = match (self.0.runner.borrow().maybe_runner()) + .and_then(|runner| runner.maybe_start_cause()) + { + Some(c) => c, + // If the runner's not initialized yet or we're in the exit state, don't do event processing. + None => return, }; - let logical_size = current_size.to_logical::(old_scale); - let mut new_size = logical_size.to_physical(new_scale); - self.handle_single_event_sync( - Event::WindowEvent { - window_id: id, - event: crate::event::WindowEvent::ScaleFactorChanged { - scale_factor: new_scale, - new_inner_size: &mut new_size, - }, - }, - &mut control, + // Handle the start event and all other events in the queue. + self.handle_event(Event::NewEvents(start_cause), &mut control); + + // It is possible for windows to be dropped before this point. We don't + // want to send `ScaleFactorChanged` for destroyed windows, so we process + // the destroy-pending windows here. + self.process_destroy_pending_windows(&mut control); + + // Even if their physical sizes haven't changed, at least one of the two sizes of all the canvases must have changed when a scale factor change occurs. + // So, issue a resize event for all the other canvases as well, using their existing sizes. + let canvases = self.0.all_canvases.borrow(); + let resized_ids: Vec<_> = resizes.iter().map(|&(id, ..)| id).collect(); + + resizes.extend( + canvases + .iter() + .filter(|&(id, ..)| !resized_ids.contains(id)) + .map(|(id, canvas)| { + let canvas = canvas.upgrade().unwrap(); + let canvas = canvas.borrow(); + (*id, canvas.raw().clone(), canvas.size()) + }), ); + } + + for (id, canvas, size) in resizes.iter() { + let mut mut_size = *size; + + let mut should_fire_resize = true; + + // First, send a `ScaleFactorChanged` event if applicable. + if scale_changed { + self.handle_single_event_sync( + Event::WindowEvent { + window_id: *id, + event: crate::event::WindowEvent::ScaleFactorChanged { + scale_factor: scale, + new_inner_size: &mut mut_size, + }, + }, + &mut control, + ); + + if mut_size != *size { + // Treat a change through this route the same way as `set_inner_size`, setting the CSS `width` and `height`. + backend::set_inner_size(canvas, mut_size.into()); + } + } else if size.width == canvas.width() && size.height == canvas.height() { + // If the canvas is already the correct size, don't send any resize events. + // This should only really happen immediately after a window is created, + // if the initial size based on CSS was already correct. + should_fire_resize = false; + } // Then we resize the canvas to the new size and send a `Resized` event: - backend::set_canvas_size(&canvas, crate::dpi::Size::Physical(new_size)); - self.handle_single_event_sync( - Event::WindowEvent { - window_id: id, - event: crate::event::WindowEvent::Resized(new_size), - }, - &mut control, - ); - } + canvas.set_width(mut_size.width); + canvas.set_height(mut_size.height); - // Process the destroy-pending windows again. - self.process_destroy_pending_windows(&mut control); - self.handle_event(Event::MainEventsCleared, &mut control); + if should_fire_resize { + let event = Event::WindowEvent { + window_id: *id, + event: crate::event::WindowEvent::Resized(mut_size), + }; - // Discard all the pending redraw as we shall just redraw all windows. - self.0.redraw_pending.borrow_mut().clear(); - for &(window_id, _) in &*self.0.all_canvases.borrow() { - self.handle_event(Event::RedrawRequested(window_id), &mut control); + if scale_changed { + self.handle_single_event_sync(event, &mut control); + } else { + self.send_event(event); + + self.request_redraw(*id); + } + } } - self.handle_event(Event::RedrawEventsCleared, &mut control); - self.apply_control_flow(control); - // If the event loop is closed, it has been closed this iteration and now the closing - // event should be emitted - if self.is_closed() { - self.handle_loop_destroyed(&mut control); + if scale_changed { + // Process the destroy-pending windows again. + self.process_destroy_pending_windows(&mut control); + self.handle_event(Event::MainEventsCleared, &mut control); + + // Discard all the pending redraw as we shall just redraw all windows. + self.0.redraw_pending.borrow_mut().clear(); + for &(window_id, _) in &*self.0.all_canvases.borrow() { + self.handle_event(Event::RedrawRequested(window_id), &mut control); + } + + self.handle_event(Event::RedrawEventsCleared, &mut control); + + self.apply_control_flow(control); + // If the event loop is closed, it has been closed this iteration and now the closing + // event should be emitted + if self.is_closed() { + self.handle_loop_destroyed(&mut control); + } } } @@ -465,7 +604,10 @@ impl Shared { fn handle_loop_destroyed(&self, control: &mut ControlFlow) { self.handle_event(Event::LoopDestroyed, control); let all_canvases = std::mem::take(&mut *self.0.all_canvases.borrow_mut()); - *self.0.scale_change_detector.borrow_mut() = None; + #[cfg(feature = "css-size")] + { + *self.0.resize_state.borrow_mut() = None; + } *self.0.unload_event_handle.borrow_mut() = None; // Dropping the `Runner` drops the event handler closure, which will in // turn drop all `Window`s moved into the closure. @@ -512,4 +654,27 @@ impl Shared { RunnerEnum::Destroyed => ControlFlow::Exit, } } + + /// Retruns the number of canvases controlled by this runner which aren't pending destruction. + pub fn num_canvases(&self) -> usize { + self.0 + .all_canvases + .borrow() + .iter() + .filter(|&&(id, _)| { + !self + .0 + .destroy_pending + .borrow() + .iter() + .any(|&(other_id, ..)| other_id == id) + }) + .count() + } + + #[cfg(feature = "css-size")] + /// Whether the scale factor has changed since the last `ScaleFactorChanged` event. + pub fn scale_factor_changed(&self) -> bool { + self.0.last_scale.get() != backend::scale_factor() + } } diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index c9863bf99c..17b96a2ed7 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -9,7 +9,7 @@ use super::{ super::monitor::MonitorHandle, backend, device::DeviceId, proxy::EventLoopProxy, runner, window::WindowId, }; -use crate::dpi::{PhysicalSize, Size}; +use crate::dpi::PhysicalSize; use crate::event::{ DeviceEvent, DeviceId as RootDeviceId, ElementState, Event, KeyboardInput, TouchPhase, WindowEvent, @@ -43,10 +43,6 @@ impl EventLoopWindowTarget { pub fn run(&self, event_handler: Box, &mut ControlFlow)>) { self.runner.set_listener(event_handler); - let runner = self.runner.clone(); - self.runner.set_on_scale_change(move |arg| { - runner.handle_scale_changed(arg.old_scale, arg.new_scale) - }); } pub fn generate_id(&self) -> WindowId { @@ -235,35 +231,45 @@ impl EventLoopWindowTarget { prevent_default, ); - let runner = self.runner.clone(); - let raw = canvas.raw().clone(); + if cfg!(not(feature = "css-size")) { + let runner = self.runner.clone(); + let raw = canvas.raw().clone(); - // The size to restore to after exiting fullscreen. - let mut intended_size = PhysicalSize { - width: raw.width() as u32, - height: raw.height() as u32, - }; - canvas.on_fullscreen_change(move || { - // If the canvas is marked as fullscreen, it is moving *into* fullscreen - // If it is not, it is moving *out of* fullscreen - let new_size = if backend::is_fullscreen(&raw) { - intended_size = PhysicalSize { + // The size to restore to after exiting fullscreen. + let mut intended_size = PhysicalSize { + width: raw.width() as u32, + height: raw.height() as u32, + }; + canvas.on_fullscreen_change(move || { + let old_size = PhysicalSize { width: raw.width() as u32, height: raw.height() as u32, }; - backend::window_size().to_physical(backend::scale_factor()) - } else { - intended_size - }; + // If the canvas is marked as fullscreen, it is moving *into* fullscreen + // If it is not, it is moving *out of* fullscreen + let new_size = if backend::is_fullscreen(&raw) { + intended_size = old_size; - backend::set_canvas_size(&raw, Size::Physical(new_size)); - runner.send_event(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::Resized(new_size), + backend::inner_size(&raw) + // I don't think it's possible for an element to become fullscreen whilst not being in the DOM. + .unwrap() + .to_physical(backend::scale_factor()) + } else { + intended_size + }; + + if old_size != new_size { + raw.set_width(new_size.width); + raw.set_height(new_size.height); + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Resized(new_size), + }); + runner.request_redraw(RootWindowId(id)); + } }); - runner.request_redraw(RootWindowId(id)); - }); + } let runner = self.runner.clone(); canvas.on_dark_mode(move |is_dark_mode| { diff --git a/src/platform_impl/web/mod.rs b/src/platform_impl/web/mod.rs index 139110333b..384b3e18b7 100644 --- a/src/platform_impl/web/mod.rs +++ b/src/platform_impl/web/mod.rs @@ -26,6 +26,9 @@ mod window; #[path = "web_sys/mod.rs"] mod backend; +#[cfg(all(feature = "css-size", not(web_sys_unstable_apis)))] +compile_error!("`web_sys_unstable_apis` must be enabled to use the `css-size` feature"); + pub use self::device::DeviceId; pub use self::error::OsError; pub(crate) use self::event_loop::{ @@ -35,9 +38,3 @@ pub use self::monitor::{MonitorHandle, VideoMode}; pub use self::window::{PlatformSpecificWindowBuilderAttributes, Window, WindowId}; pub(crate) use crate::icon::NoIcon as PlatformIcon; - -#[derive(Clone, Copy)] -pub(crate) struct ScaleChangeArgs { - old_scale: f64, - new_scale: f64, -} diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 2e87587442..bcf88ea089 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -59,6 +59,10 @@ impl Canvas { } }; + #[cfg(not(feature = "css-size"))] + // Try to avoid this changing from under us when we don't have `ResizeObserver`. + super::set_canvas_style_property(&canvas, "box-sizing", "content-box"); + // A tabindex is needed in order to capture local keyboard events. // A "0" value means that the element should be focusable in // sequential keyboard navigation, but its order is defined by the diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 5e2d6c3820..58aa6b61dd 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -12,7 +12,7 @@ pub use self::timeout::{AnimationFrameRequest, Timeout}; use crate::dpi::{LogicalSize, Size}; use crate::platform::web::WindowExtWebSys; use crate::window::Window; -use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::*; use web_sys::{window, BeforeUnloadEvent, Element, HtmlCanvasElement}; pub fn throw(msg: &str) { @@ -60,38 +60,86 @@ impl WindowExtWebSys for Window { } } -pub fn window_size() -> LogicalSize { - let window = web_sys::window().expect("Failed to obtain window"); - let width = window - .inner_width() - .expect("Failed to get width") - .as_f64() - .expect("Failed to get width as f64"); - let height = window - .inner_height() - .expect("Failed to get height") - .as_f64() - .expect("Failed to get height as f64"); - - LogicalSize { width, height } -} - pub fn scale_factor() -> f64 { let window = web_sys::window().expect("Failed to obtain window"); window.device_pixel_ratio() } -pub fn set_canvas_size(raw: &HtmlCanvasElement, size: Size) { +/// Gets the size of the content box of `element` based on CSS. +/// +/// Returns `None` if the element isn't in the DOM. +pub fn inner_size(element: &HtmlCanvasElement) -> Option> { + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + if !document.contains(Some(element)) { + return None; + } + + // Use `getBoundingClientRect` instead of the width and height properties because it doesn't round to the nearest integer. + let rect = element.get_bounding_client_rect(); + let style = window + .get_computed_style(element) + .unwrap() + .expect("`getComputedStyle` returned `None`"); + + let prop = |name| -> f64 { + let value = style.get_property_value(name).unwrap(); + // Cut off the last two characters to remove the `px` from the end. + value[..value.len() - 2].parse().unwrap() + }; + + Some(LogicalSize { + width: rect.width() + - prop("border-left-width") + - prop("border-right-width") + - prop("padding-left") + - prop("padding-right"), + height: rect.height() + - prop("border-top-width") + - prop("border-bottom-width") + - prop("padding-top") + - prop("padding-bottom"), + }) +} + +pub fn set_inner_size(element: &HtmlCanvasElement, size: Size) { let scale_factor = scale_factor(); - let physical_size = size.to_physical::(scale_factor); - let logical_size = size.to_logical::(scale_factor); + let mut logical_size = size.to_logical::(scale_factor); - raw.set_width(physical_size.width); - raw.set_height(physical_size.height); + if cfg!(not(feature = "css-size")) { + let physical_size = size.to_physical(scale_factor); + element.set_width(physical_size.width); + element.set_height(physical_size.height); + } + + let window = web_sys::window().unwrap(); + let style = window + .get_computed_style(element) + // This can't fail according to the spec; I don't know why web-sys marks it as throwing and having an optional result. + .expect("`getComputedStyle` failed") + .expect("`getComputedStyle` returned `None`"); + + // This also can't fail according to the spec. + if style.get_property_value("box-sizing").unwrap() == "border-box" { + let prop = |name| -> f64 { + let value = style.get_property_value(name).unwrap(); + // Cut off the last two characters to remove the `px` from the end. + value[..value.len() - 2].parse().unwrap() + }; + + logical_size.width += prop("border-left-width") + + prop("border-right-width") + + prop("padding-left") + + prop("padding-right"); + logical_size.height += prop("border-top-width") + + prop("border-bottom-width") + + prop("padding-top") + + prop("padding-bottom"); + } - set_canvas_style_property(raw, "width", &format!("{}px", logical_size.width)); - set_canvas_style_property(raw, "height", &format!("{}px", logical_size.height)); + set_canvas_style_property(element, "width", &format!("{}px", logical_size.width)); + set_canvas_style_property(element, "height", &format!("{}px", logical_size.height)); } pub fn set_canvas_style_property(raw: &HtmlCanvasElement, property: &str, value: &str) { @@ -114,4 +162,32 @@ pub fn is_fullscreen(canvas: &HtmlCanvasElement) -> bool { } } +// A slight hack to get at the prototype of `ResizeObserverEntry`, so that we can check for `device-pixel-content-box` support. +#[cfg(feature = "css-size")] +mod prototype { + use js_sys::Object; + use wasm_bindgen::prelude::*; + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen] + pub type ResizeObserverEntry; + + #[wasm_bindgen(static_method_of = ResizeObserverEntry, getter)] + pub fn prototype() -> Object; + } +} + +#[cfg(feature = "css-size")] +pub fn supports_device_pixel_content_size() -> bool { + use js_sys::Object; + + let proto = prototype::ResizeObserverEntry::prototype(); + let desc = Object::get_own_property_descriptor( + &proto, + &JsValue::from_str("devicePixelContentBoxSize"), + ); + !desc.is_undefined() +} + pub type RawCanvasType = HtmlCanvasElement; diff --git a/src/platform_impl/web/web_sys/scaling.rs b/src/platform_impl/web/web_sys/scaling.rs index 7eb82c9fb5..b700591a8e 100644 --- a/src/platform_impl/web/web_sys/scaling.rs +++ b/src/platform_impl/web/web_sys/scaling.rs @@ -1,4 +1,3 @@ -use super::super::ScaleChangeArgs; use super::media_query_handle::MediaQueryListHandle; use std::{cell::RefCell, rc::Rc}; @@ -10,7 +9,7 @@ pub struct ScaleChangeDetector(Rc>); impl ScaleChangeDetector { pub(crate) fn new(handler: F) -> Self where - F: 'static + FnMut(ScaleChangeArgs), + F: 'static + FnMut(), { Self(ScaleChangeDetectorInternal::new(handler)) } @@ -19,21 +18,18 @@ impl ScaleChangeDetector { /// This is a helper type to help manage the `MediaQueryList` used for detecting /// changes of the `devicePixelRatio`. struct ScaleChangeDetectorInternal { - callback: Box, + callback: Box, mql: Option, - last_scale: f64, } impl ScaleChangeDetectorInternal { fn new(handler: F) -> Rc> where - F: 'static + FnMut(ScaleChangeArgs), + F: 'static + FnMut(), { - let current_scale = super::scale_factor(); let new_self = Rc::new(RefCell::new(Self { callback: Box::new(handler), mql: None, - last_scale: current_scale, })); let weak_self = Rc::downgrade(&new_self); @@ -77,13 +73,8 @@ impl ScaleChangeDetectorInternal { .take() .expect("DevicePixelRatioChangeDetector::mql should not be None"); let closure = mql.remove(); - let new_scale = super::scale_factor(); - (self.callback)(ScaleChangeArgs { - old_scale: self.last_scale, - new_scale, - }); + (self.callback)(); let new_mql = Self::create_mql(closure); self.mql = new_mql; - self.last_scale = new_scale; } } diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 6fe5839e9d..9e40238947 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -38,6 +38,7 @@ impl Window { let prevent_default = platform_attr.prevent_default; let canvas = backend::Canvas::create(platform_attr)?; + let raw = canvas.raw().clone(); let canvas = Rc::new(RefCell::new(canvas)); let register_redraw_request = Box::new(move || runner.request_redraw(RootWI(id))); @@ -53,7 +54,10 @@ impl Window { }); let runner = target.runner.clone(); - let destroy_fn = Box::new(move || runner.notify_destroy_window(RootWI(id))); + let destroy_fn = { + let raw = raw.clone(); + Box::new(move || runner.notify_destroy_window(RootWI(id), raw)) + }; let window = Window { canvas, @@ -64,13 +68,23 @@ impl Window { destroy_fn: Some(destroy_fn), }; - backend::set_canvas_size( - window.canvas.borrow().raw(), - attr.inner_size.unwrap_or(Size::Logical(LogicalSize { - width: 1024.0, - height: 768.0, - })), - ); + if let Some(size) = attr.inner_size { + backend::set_inner_size(&raw, size); + } else if cfg!(not(feature = "css-size")) { + backend::set_inner_size(&raw, LogicalSize::new(1024, 768).into()) + } + + // The `ResizeObserver`s don't fire synchronously, so we need to set the canvas' size to an estimate based on the CSS size. + // If we don't, any initialization code relying on the canvas' dimensions will be messed up. + #[cfg(feature = "css-size")] + if let Some(size) = backend::inner_size(&raw) { + let size = size.to_physical(backend::scale_factor()); + raw.set_width(size.width); + raw.set_height(size.height); + } + // Make sure not to set the width and height here, + // to signal that we need to calculate it in the first call to `inner_size`. + window.set_title(&attr.title); window.set_maximized(attr.maximized); window.set_visible(attr.visible); @@ -124,6 +138,20 @@ impl Window { #[inline] pub fn inner_size(&self) -> PhysicalSize { + let canvas = Ref::map(self.canvas(), backend::Canvas::raw); + if !canvas.has_attribute("width") || !canvas.has_attribute("height") { + // We haven't set the framebuffer size yet. Set its initial size. + if let Some(size) = backend::inner_size(&canvas) { + let size = size.to_physical(self.scale_factor()); + canvas.set_width(size.width); + canvas.set_height(size.height); + } else { + // The value returned by `inner_size` shouldn't change without a corresponding `Resized` event, + // so if the canvas still hasn't been added to the DOM by this point, 0x0 it is. + canvas.set_width(0); + canvas.set_height(0); + } + } self.canvas.borrow().size() } @@ -136,9 +164,9 @@ impl Window { #[inline] pub fn set_inner_size(&self, size: Size) { let old_size = self.inner_size(); - backend::set_canvas_size(self.canvas.borrow().raw(), size); + backend::set_inner_size(self.canvas.borrow().raw(), size); let new_size = self.inner_size(); - if old_size != new_size { + if cfg!(not(feature = "css-size")) && old_size != new_size { (self.resize_notify_fn)(new_size); } } From f9a540bc7a506159b42d4c078a3df6ac3af1a54b Mon Sep 17 00:00:00 2001 From: Liam Murphy Date: Fri, 25 Feb 2022 18:14:29 +1100 Subject: [PATCH 2/8] Fix the changelog and add some documentation --- CHANGELOG.md | 2 +- src/platform/web.rs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c088bb80c..a381216dab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ And please only add new entries to the top of this list, right below the `# Unre # Unreleased +- Add the `css-size` feature, which will size the canvas based on the page layout on the Web, rather than having to do so manually using `set_inner_size`. - Migrated `WindowBuilderExtUnix::with_resize_increments` to `WindowBuilder`. - Added `Window::resize_increments`/`Window::set_resize_increments` to update resize increments at runtime for X11/macOS. - macOS/iOS: Use `objc2` instead of `objc` internally. @@ -147,7 +148,6 @@ And please only add new entries to the top of this list, right below the `# Unre - On Wayland, implement a workaround for wrong configure size when using `xdg_decoration` in `kwin_wayland` - On macOS, fix an issue that prevented the menu bar from showing in borderless fullscreen mode. - On X11, EINTR while polling for events no longer causes a panic. Instead it will be treated as a spurious wakeup. -- **Breaking:** On Web, size the canvas based on the page layout using `ResizeObserver`. `set_inner_size` will just set the CSS `width` and `height` properties. # 0.25.0 (2021-05-15) diff --git a/src/platform/web.rs b/src/platform/web.rs index f78a01fb02..d6e1e88139 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -2,6 +2,25 @@ //! allow end users to determine how the page should be laid out. Use the [`WindowExtWebSys`] trait //! to retrieve the canvas from the Window. Alternatively, use the [`WindowBuilderExtWebSys`] trait //! to provide your own canvas. +//! +//! # The `css-size` feature +//! +//! By default, the canvas' size is fixed; it can only be resized by calling +//! [`Window::set_inner_size`]. The `css-size` feature changes this, setting the size of the +//! canvas based on CSS. This allows much more easily laying it out within the page. +//! +//! `css-size` relies on `ResizeObserver`, which is still an unstable feature; so, to use it, you +//! have to enable `web_sys_unstable_apis`. For example: +//! +//! ```sh +//! RUSTFLAGS="--cfg=web_sys_unstable_apis" cargo build ... +//! ``` +//! +//! If a window's canvas isn't in the DOM when the window is created, the initial size won't be +//! calculated until the first call to `inner_size`, to allow it to be added to the DOM. +//! Otherwise, the canvas' size would be reported as 0x0 to any initialization code. +//! +//! [`Window::set_inner_size`]: window::Window::set_inner_size use crate::event::Event; use crate::event_loop::ControlFlow; From 63e15e0b589a1d78e8b9de903b781e724bac4cec Mon Sep 17 00:00:00 2001 From: Liam Murphy Date: Fri, 25 Feb 2022 18:45:29 +1100 Subject: [PATCH 3/8] Fix leak of `ScaleChangeDetector` and some unnecessary changes --- src/platform_impl/web/event_loop/runner.rs | 4 ++++ src/platform_impl/web/web_sys/mod.rs | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index 5b3aaa5497..b373178b73 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -608,6 +608,10 @@ impl Shared { { *self.0.resize_state.borrow_mut() = None; } + #[cfg(not(feature = "css-size"))] + { + *self.0.scale_change_detector.borrow_mut() = None; + } *self.0.unload_event_handle.borrow_mut() = None; // Dropping the `Runner` drops the event handler closure, which will in // turn drop all `Window`s moved into the closure. diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 58aa6b61dd..48955d1e66 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -68,17 +68,17 @@ pub fn scale_factor() -> f64 { /// Gets the size of the content box of `element` based on CSS. /// /// Returns `None` if the element isn't in the DOM. -pub fn inner_size(element: &HtmlCanvasElement) -> Option> { +pub fn inner_size(raw: &HtmlCanvasElement) -> Option> { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); - if !document.contains(Some(element)) { + if !document.contains(Some(raw)) { return None; } // Use `getBoundingClientRect` instead of the width and height properties because it doesn't round to the nearest integer. - let rect = element.get_bounding_client_rect(); + let rect = raw.get_bounding_client_rect(); let style = window - .get_computed_style(element) + .get_computed_style(raw) .unwrap() .expect("`getComputedStyle` returned `None`"); @@ -102,20 +102,20 @@ pub fn inner_size(element: &HtmlCanvasElement) -> Option> { }) } -pub fn set_inner_size(element: &HtmlCanvasElement, size: Size) { +pub fn set_inner_size(raw: &HtmlCanvasElement, size: Size) { let scale_factor = scale_factor(); let mut logical_size = size.to_logical::(scale_factor); if cfg!(not(feature = "css-size")) { let physical_size = size.to_physical(scale_factor); - element.set_width(physical_size.width); - element.set_height(physical_size.height); + raw.set_width(physical_size.width); + raw.set_height(physical_size.height); } let window = web_sys::window().unwrap(); let style = window - .get_computed_style(element) + .get_computed_style(raw) // This can't fail according to the spec; I don't know why web-sys marks it as throwing and having an optional result. .expect("`getComputedStyle` failed") .expect("`getComputedStyle` returned `None`"); @@ -138,8 +138,8 @@ pub fn set_inner_size(element: &HtmlCanvasElement, size: Size) { + prop("padding-bottom"); } - set_canvas_style_property(element, "width", &format!("{}px", logical_size.width)); - set_canvas_style_property(element, "height", &format!("{}px", logical_size.height)); + set_canvas_style_property(raw, "width", &format!("{}px", logical_size.width)); + set_canvas_style_property(raw, "height", &format!("{}px", logical_size.height)); } pub fn set_canvas_style_property(raw: &HtmlCanvasElement, property: &str, value: &str) { From 10bd65c16050211ec48dba3e500f06d0f6d9fad0 Mon Sep 17 00:00:00 2001 From: Liam Murphy Date: Sun, 27 Feb 2022 17:23:31 +1100 Subject: [PATCH 4/8] Improve docs and make sure the first `Resize` event is always sent if `inner_size` doesn't get called. --- src/platform/web.rs | 22 ++++++++++++++++------ src/platform_impl/web/event_loop/runner.rs | 6 +++++- src/window.rs | 4 ++-- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/platform/web.rs b/src/platform/web.rs index d6e1e88139..45c2649605 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -6,8 +6,8 @@ //! # The `css-size` feature //! //! By default, the canvas' size is fixed; it can only be resized by calling -//! [`Window::set_inner_size`]. The `css-size` feature changes this, setting the size of the -//! canvas based on CSS. This allows much more easily laying it out within the page. +//! [`Window::set_inner_size`]. The `css-size` feature changes this, setting the size of the canvas +//! based on CSS. This allows much more easily laying it out within the page. //! //! `css-size` relies on `ResizeObserver`, which is still an unstable feature; so, to use it, you //! have to enable `web_sys_unstable_apis`. For example: @@ -16,11 +16,21 @@ //! RUSTFLAGS="--cfg=web_sys_unstable_apis" cargo build ... //! ``` //! -//! If a window's canvas isn't in the DOM when the window is created, the initial size won't be -//! calculated until the first call to `inner_size`, to allow it to be added to the DOM. -//! Otherwise, the canvas' size would be reported as 0x0 to any initialization code. +//! ## Initial size handling //! -//! [`Window::set_inner_size`]: window::Window::set_inner_size +//! If the canvas is created by `Window::new` (i.e., isn't passed via [`with_canvas`]), its size +//! isn't initially known, since the canvas hasn't yet been put into the DOM. To work around this, +//! the `Window` doesn't calculate its size until the first call to [`Window::inner_size`], to +//! allow the canvas to be inserted into the page. +//! +//! This has some caveats; if you use a library which gets the size directly from the canvas, it +//! won't trigger this, and will end up with an incorrect initial size. The most reliable method is +//! to create the canvas yourself, insert it into the page, and then pass it to [`with_canvas`]. +//! +//! [`Window::new`]: crate::window::Window::new +//! [`Window::inner_size`]: crate::window::Window::inner_size +//! [`Window::set_inner_size`]: crate::window::Window::set_inner_size +//! [`with_canvas`]: crate::platform::web::WindowBuilderExtWebSys::with_canvas use crate::event::Event; use crate::event_loop::ControlFlow; diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index b373178b73..1032f43df1 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -456,7 +456,11 @@ impl Shared { // Treat a change through this route the same way as `set_inner_size`, setting the CSS `width` and `height`. backend::set_inner_size(canvas, mut_size.into()); } - } else if size.width == canvas.width() && size.height == canvas.height() { + } else if canvas.has_attribute("width") + && size.width == canvas.width() + && canvas.has_attribute("height") + && size.height == canvas.height() + { // If the canvas is already the correct size, don't send any resize events. // This should only really happen immediately after a window is created, // if the initial size based on CSS was already correct. diff --git a/src/window.rs b/src/window.rs index ba7af31193..66411dbe8b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -521,7 +521,7 @@ impl Window { /// /// - **iOS:** Can only be called on the main thread. Returns the `PhysicalSize` of the window's /// [safe area] in screen space coordinates. - /// - **Web:** Returns the size of the canvas element. + /// - **Web:** Returns the size of the canvas element's framebuffer. /// /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc #[inline] @@ -550,7 +550,7 @@ impl Window { /// ## Platform-specific /// /// - **iOS / Android:** Unsupported. - /// - **Web:** Sets the size of the canvas element. + /// - **Web:** Sets the size of the canvas element's context box via the `style` attribute. #[inline] pub fn set_inner_size>(&self, size: S) { self.window.set_inner_size(size.into()) From d1193e9ff7365652102eeee0d4673c6c2cf6e9b9 Mon Sep 17 00:00:00 2001 From: Liam Murphy Date: Tue, 12 Jul 2022 15:00:40 +1000 Subject: [PATCH 5/8] Fix outdated reference to `Id` --- src/platform_impl/web/event_loop/resize.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform_impl/web/event_loop/resize.rs b/src/platform_impl/web/event_loop/resize.rs index 78a2e49d6a..56b4784d3e 100644 --- a/src/platform_impl/web/event_loop/resize.rs +++ b/src/platform_impl/web/event_loop/resize.rs @@ -19,7 +19,7 @@ use super::runner::Shared; fn process_entry(entry: &ResizeObserverEntry) -> (WindowId, HtmlCanvasElement, PhysicalSize) { let canvas: HtmlCanvasElement = entry.target().dyn_into().unwrap(); - let id = WindowId(super::window::Id( + let id = WindowId(super::window::WindowId( canvas .get_attribute("data-raw-handle") .expect("Canvas was missing `data-raw-handle` attribute") From 22fa26eec7e6597c93a56735961635464ed23605 Mon Sep 17 00:00:00 2001 From: Liam Murphy Date: Fri, 15 Jul 2022 10:55:20 +1000 Subject: [PATCH 6/8] fix typo --- src/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/window.rs b/src/window.rs index 66411dbe8b..2635341b0e 100644 --- a/src/window.rs +++ b/src/window.rs @@ -550,7 +550,7 @@ impl Window { /// ## Platform-specific /// /// - **iOS / Android:** Unsupported. - /// - **Web:** Sets the size of the canvas element's context box via the `style` attribute. + /// - **Web:** Sets the size of the canvas element's content box via the `style` attribute. #[inline] pub fn set_inner_size>(&self, size: S) { self.window.set_inner_size(size.into()) From 2c59062597f39e05639c3b8e3c3524e1b36f3bc6 Mon Sep 17 00:00:00 2001 From: Liam Murphy Date: Fri, 2 Sep 2022 20:22:36 +1000 Subject: [PATCH 7/8] Make `inner_size` a bit more robust --- src/platform_impl/web/web_sys/mod.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 48955d1e66..37fa0a42de 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -82,10 +82,25 @@ pub fn inner_size(raw: &HtmlCanvasElement) -> Option> { .unwrap() .expect("`getComputedStyle` returned `None`"); + let display_none = style.get_property_value("display").unwrap() == "none"; + let prop = |name| -> f64 { let value = style.get_property_value(name).unwrap(); - // Cut off the last two characters to remove the `px` from the end. - value[..value.len() - 2].parse().unwrap() + if display_none && name.starts_with("padding") { + // When `display` is `none`, the value returned for padding isn't + // guaranteed to be in `px` (it's left as a percentage if the + // property is specified as such, when normally it's resolved to + // `px`). + // So, return 0, since getting the size right isn't particularly + // important for an invisible element. + return 0.0; + } + // Remove the `px` from the end of the value and parse it. + value + .strip_suffix("px") + .expect("border and padding should always be in units of `px`") + .parse() + .unwrap() }; Some(LogicalSize { From a1a121026006e5a3d0687764eb01f88ad5504dc8 Mon Sep 17 00:00:00 2001 From: Liam Murphy Date: Fri, 2 Sep 2022 20:24:37 +1000 Subject: [PATCH 8/8] Add a note about `display: contents` --- src/platform_impl/web/web_sys/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 37fa0a42de..8fad05d8aa 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -91,6 +91,9 @@ pub fn inner_size(raw: &HtmlCanvasElement) -> Option> { // guaranteed to be in `px` (it's left as a percentage if the // property is specified as such, when normally it's resolved to // `px`). + // Note: that's also true when `display` is `contents`, but for + // `` that gets resolved to `display: none` and can never + // happen. // So, return 0, since getting the size right isn't particularly // important for an invisible element. return 0.0;