From 288ebdbbc914b3ffe000e5022459b6b3d21077ad Mon Sep 17 00:00:00 2001 From: dAxpeDDa Date: Wed, 7 Jun 2023 17:32:52 +0200 Subject: [PATCH] Implement `ResizeObserver` Co-Authored-By: Liam Murphy <43807659+Liamolucko@users.noreply.github.com> --- CHANGELOG.md | 4 +- Cargo.toml | 15 +- examples/web.rs | 24 ++- src/platform_impl/web/event_loop/runner.rs | 17 +-- .../web/event_loop/window_target.rs | 40 ++--- src/platform_impl/web/web_sys/canvas.rs | 68 ++++----- src/platform_impl/web/web_sys/mod.rs | 28 +--- src/platform_impl/web/web_sys/resize.rs | 143 ++++++++++++++++++ src/platform_impl/web/window.rs | 26 +--- 9 files changed, 237 insertions(+), 128 deletions(-) create mode 100644 src/platform_impl/web/web_sys/resize.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eea1dc0259..23478be49a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,7 +68,7 @@ And please only add new entries to the top of this list, right below the `# Unre - On Web, fix pen treated as mouse input. - On Web, send mouse position on button release as well. - On Web, fix touch input not gaining or loosing focus. -- **Breaking:** On Web, dropped support for Safari versions below 13. +- **Breaking:** On Web, dropped support for Safari versions below 13.1. - On Web, prevent clicks on the canvas to select text. - On Web, use high-frequency pointer input events when supported by the browser. - On Web, `EventLoopProxy` now implements `Send`. @@ -79,6 +79,8 @@ And please only add new entries to the top of this list, right below the `# Unre - On Web, scale factor and dark mode detection are now more robust. - On Web, fix the bfcache by not using the `beforeunload` event. - On Web, fix scale factor resize suggestion always overwriting the canvas size. +- **Breaking:** On Web, the canvas size is not controlled by Winit anymore and external changes to + the canvas size will be reported through `WindowEvent::Resized`. # 0.28.6 diff --git a/Cargo.toml b/Cargo.toml index fc364dd15ca..ab678a5857c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,12 +129,13 @@ redox_syscall = "0.3" [target.'cfg(target_family = "wasm")'.dependencies.web_sys] package = "web-sys" -version = "0.3.22" +version = "0.3.63" features = [ 'console', 'CssStyleDeclaration', 'Document', 'DomRect', + 'DomRectReadOnly', 'Element', 'Event', "EventListenerOptions", @@ -146,6 +147,11 @@ features = [ 'MediaQueryList', 'Node', 'PointerEvent', + 'ResizeObserver', + 'ResizeObserverBoxOptions', + 'ResizeObserverEntry', + 'ResizeObserverOptions', + 'ResizeObserverSize', 'Window', 'WheelEvent' ] @@ -165,3 +171,10 @@ web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d'] } members = [ "run-wasm", ] + +[patch.crates-io] +js-sys = { git = "https://github.com/daxpedda/wasm-bindgen", branch = "winit" } +wasm-bindgen = { git = "https://github.com/daxpedda/wasm-bindgen", branch = "winit" } +wasm-bindgen-cli-support = { git = "https://github.com/daxpedda/wasm-bindgen", branch = "winit" } +wasm-bindgen-futures = { git = "https://github.com/daxpedda/wasm-bindgen", branch = "winit" } +web-sys = { git = "https://github.com/daxpedda/wasm-bindgen", branch = "winit" } diff --git a/examples/web.rs b/examples/web.rs index ea32f06e32c..c1df6987737 100644 --- a/examples/web.rs +++ b/examples/web.rs @@ -1,9 +1,10 @@ #![allow(clippy::disallowed_methods, clippy::single_match)] use winit::{ - event::{Event, WindowEvent}, + event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::EventLoop, - window::WindowBuilder, + keyboard::KeyCode, + window::{Fullscreen, WindowBuilder}, }; pub fn main() { @@ -31,6 +32,25 @@ pub fn main() { Event::MainEventsCleared => { window.request_redraw(); } + Event::WindowEvent { + window_id, + event: + WindowEvent::KeyboardInput { + event: + KeyEvent { + physical_key: KeyCode::KeyF, + state: ElementState::Released, + .. + }, + .. + }, + } if window_id == window.id() => { + if window.fullscreen().is_some() { + window.set_fullscreen(None); + } else { + window.set_fullscreen(Some(Fullscreen::Borderless(None))); + } + } _ => (), } }); diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index f634083006e..b91021bd866 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -327,14 +327,13 @@ impl Shared { // Now handle the `ScaleFactorChanged` events. for &(id, ref canvas) in &*self.0.all_canvases.borrow() { - let rc = match canvas.upgrade() { + let canvas = match canvas.upgrade() { Some(rc) => rc, // This shouldn't happen, but just in case... None => continue, }; - let canvas = rc.borrow(); // First, we send the `ScaleFactorChanged` event: - let current_size = canvas.size().get(); + let current_size = canvas.borrow().inner_size(); let logical_size = current_size.to_logical::(old_scale); let mut new_size = logical_size.to_physical(new_scale); self.handle_single_event_sync( @@ -348,16 +347,10 @@ impl Shared { &mut control, ); - // Then we resize the canvas to the new size and send a `Resized` event: + // Then we resize the canvas to the new size, a new + // `Resized` event will be sent by the `ResizeObserver`: if current_size != new_size { - 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, - ); + backend::set_canvas_size(canvas.borrow().raw(), new_size); } } diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index 6b142e2d6cc..fa65204ac57 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -16,7 +16,6 @@ use super::{ runner, window::WindowId, }; -use crate::dpi::Size; use crate::event::{ DeviceEvent, DeviceId as RootDeviceId, ElementState, Event, KeyEvent, Touch, TouchPhase, WindowEvent, @@ -503,35 +502,6 @@ impl EventLoopWindowTarget { prevent_default, ); - // The size to restore to after exiting fullscreen. - let mut intended_size = canvas.size().get(); - - canvas.on_fullscreen_change({ - let window = self.runner.window().clone(); - let runner = self.runner.clone(); - - move || { - let canvas = canvas_clone.borrow(); - - // 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(&window, canvas.raw()) { - intended_size = canvas.size().get(); - - backend::window_size(&window).to_physical(backend::scale_factor(&window)) - } else { - intended_size - }; - - backend::set_canvas_size(&canvas, Size::Physical(new_size)); - runner.send_event(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::Resized(new_size), - }); - runner.request_redraw(RootWindowId(id)); - } - }); - let runner = self.runner.clone(); canvas.on_touch_cancel(move |device_id, location, force| { runner.send_event(Event::WindowEvent { @@ -558,6 +528,16 @@ impl EventLoopWindowTarget { event: WindowEvent::ThemeChanged(theme), }); }); + + let runner = self.runner.clone(); + canvas.on_resize(move |size| { + canvas_clone.borrow().set_inner_size(size); + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Resized(size), + }); + runner.request_redraw(RootWindowId(id)); + }); } pub fn available_monitors(&self) -> VecDequeIter { diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 25bc63a7647..a0725a9df4f 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -1,8 +1,9 @@ use super::event_handle::EventListenerHandle; use super::media_query_handle::MediaQueryListHandle; use super::pointer::PointerHandler; +use super::resize::ResizeHandle; use super::{event, ButtonsState}; -use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; use crate::error::OsError as RootOE; use crate::event::{Force, MouseButton, MouseScrollDelta}; use crate::keyboard::{Key, KeyCode, KeyLocation, ModifiersState}; @@ -29,9 +30,9 @@ pub struct Canvas { on_keyboard_release: Option>, on_keyboard_press: Option>, on_mouse_wheel: Option>, - on_fullscreen_change: Option>, on_dark_mode: Option, pointer_handler: PointerHandler, + on_resize: Option, } pub struct Common { @@ -73,22 +74,16 @@ impl Canvas { .map_err(|_| os_error!(OsError("Failed to set a tabindex".to_owned())))?; } - let size = attr - .inner_size - .unwrap_or( - LogicalSize { - width: 1024.0, - height: 768.0, - } - .into(), - ) - .to_physical(super::scale_factor(&window)); + if let Some(size) = attr.inner_size { + let size = size.to_physical(super::scale_factor(&window)); + super::set_canvas_size(&canvas, size); + } - let canvas = Canvas { + Ok(Canvas { common: Common { window, raw: canvas, - size: Rc::new(Cell::new(size)), + size: Rc::default(), wants_fullscreen: Rc::new(RefCell::new(false)), }, on_touch_start: None, @@ -98,14 +93,10 @@ impl Canvas { on_keyboard_release: None, on_keyboard_press: None, on_mouse_wheel: None, - on_fullscreen_change: None, on_dark_mode: None, pointer_handler: PointerHandler::new(), - }; - - super::set_canvas_size(&canvas, size.into()); - - Ok(canvas) + on_resize: None, + }) } pub fn set_cursor_lock(&self, lock: bool) -> Result<(), RootOE> { @@ -138,12 +129,16 @@ impl Canvas { } } - pub fn window(&self) -> &web_sys::Window { - &self.common.window + pub fn inner_size(&self) -> PhysicalSize { + self.common.size.get() } - pub fn size(&self) -> &Rc>> { - &self.common.size + pub fn set_inner_size(&self, size: PhysicalSize) { + self.common.size.set(size) + } + + pub fn window(&self) -> &web_sys::Window { + &self.common.window } pub fn raw(&self) -> &HtmlCanvasElement { @@ -327,16 +322,6 @@ impl Canvas { })); } - pub fn on_fullscreen_change(&mut self, mut handler: F) - where - F: 'static + FnMut(), - { - self.on_fullscreen_change = Some( - self.common - .add_event("fullscreenchange", move |_: Event| handler()), - ); - } - pub fn on_dark_mode(&mut self, mut handler: F) where F: 'static + FnMut(bool), @@ -348,6 +333,17 @@ impl Canvas { )); } + pub fn on_resize(&mut self, handler: F) + where + F: 'static + FnMut(PhysicalSize), + { + self.on_resize = Some(ResizeHandle::new( + self.window().clone(), + self.raw().clone(), + handler, + )); + } + pub fn request_fullscreen(&self) { self.common.request_fullscreen() } @@ -362,9 +358,9 @@ impl Canvas { self.on_keyboard_release = None; self.on_keyboard_press = None; self.on_mouse_wheel = None; - self.on_fullscreen_change = None; self.on_dark_mode = None; - self.pointer_handler.remove_listeners() + self.pointer_handler.remove_listeners(); + self.on_resize = None; } } diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 69a6783aa6e..30d73409f54 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -3,6 +3,7 @@ mod event; mod event_handle; mod media_query_handle; mod pointer; +mod resize; mod scaling; mod timeout; @@ -11,7 +12,7 @@ pub use self::event::ButtonsState; pub use self::scaling::ScaleChangeDetector; pub use self::timeout::{AnimationFrameRequest, Timeout}; -use crate::dpi::{LogicalSize, Size}; +use crate::dpi::PhysicalSize; use crate::platform::web::WindowExtWebSys; use crate::window::Window; use wasm_bindgen::closure::Closure; @@ -52,32 +53,13 @@ impl WindowExtWebSys for Window { } } -pub fn window_size(window: &web_sys::Window) -> LogicalSize { - 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(window: &web_sys::Window) -> f64 { window.device_pixel_ratio() } -pub fn set_canvas_size(canvas: &Canvas, new_size: Size) { - let scale_factor = scale_factor(canvas.window()); - let new_size = new_size.to_physical(scale_factor); - - canvas.size().set(new_size); - set_canvas_style_property(canvas.raw(), "width", &format!("{}px", new_size.width)); - set_canvas_style_property(canvas.raw(), "height", &format!("{}px", new_size.height)); +pub fn set_canvas_size(raw: &HtmlCanvasElement, new_size: PhysicalSize) { + set_canvas_style_property(raw, "width", &format!("{}px", new_size.width)); + set_canvas_style_property(raw, "height", &format!("{}px", new_size.height)); } pub fn set_canvas_style_property(raw: &HtmlCanvasElement, property: &str, value: &str) { diff --git a/src/platform_impl/web/web_sys/resize.rs b/src/platform_impl/web/web_sys/resize.rs new file mode 100644 index 00000000000..ae39141940b --- /dev/null +++ b/src/platform_impl/web/web_sys/resize.rs @@ -0,0 +1,143 @@ +use js_sys::{Array, Object}; +use once_cell::unsync::Lazy; +use wasm_bindgen::prelude::{wasm_bindgen, Closure}; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{ + HtmlCanvasElement, ResizeObserver, ResizeObserverBoxOptions, ResizeObserverEntry, + ResizeObserverOptions, ResizeObserverSize, Window, +}; + +use crate::dpi::PhysicalSize; + +pub struct ResizeHandle { + observer: ResizeObserver, + _closure: Closure, +} + +impl ResizeHandle { + pub fn new(window: Window, canvas: HtmlCanvasElement, mut listener: F) -> Self + where + F: 'static + FnMut(PhysicalSize), + { + let closure = Closure::new({ + let canvas = canvas.clone(); + move |entries: Array| { + let size = Self::process_entry(&window, &canvas, entries); + + listener(size) + } + }); + let observer = ResizeObserver::new(closure.as_ref().unchecked_ref()) + .expect("Failed to create `ResizeObserver`"); + + // Safari doesn't support `devicePixelContentBoxSize` + if has_device_pixel_support() { + observer.observe_with_options( + &canvas, + ResizeObserverOptions::new().box_(ResizeObserverBoxOptions::DevicePixelContentBox), + ); + } else { + observer.observe(&canvas); + } + + Self { + observer, + _closure: closure, + } + } + + fn process_entry( + window: &Window, + canvas: &HtmlCanvasElement, + entries: Array, + ) -> PhysicalSize { + debug_assert_eq!(entries.length(), 1, "expected exactly one entry"); + let entry = entries.get(0); + debug_assert!(entry.has_type::()); + let entry: ResizeObserverEntry = entry.unchecked_into(); + + // Safari doesn't support `devicePixelContentBoxSize` + if !has_device_pixel_support() { + let rect = entry.content_rect(); + + return PhysicalSize::new(rect.width().round() as u32, rect.height().round() as u32); + } + + let entries = entry.device_pixel_content_box_size(); + debug_assert_eq!( + entries.length(), + 1, + "a canvas can't be split into multiple fragments" + ); + let entry = entries.get(0); + debug_assert!(entry.has_type::()); + let entry: ResizeObserverSize = entry.unchecked_into(); + + let style = window + .get_computed_style(canvas) + .expect("Failed to get computed style of canvas") + // this can only be empty if we provided an invalid `pseudoElt` + .expect("`getComputedStyle` can not be empty"); + + let writing_mode = style + .get_property_value("writing-mode") + .expect("`wirting-mode` is a valid CSS property"); + + // means the canvas is not inserted into the DOM + if writing_mode.is_empty() { + debug_assert_eq!(entry.inline_size(), 0.); + debug_assert_eq!(entry.block_size(), 0.); + + return PhysicalSize::new(0, 0); + } + + let horizontal = match writing_mode.as_str() { + _ if writing_mode.starts_with("horizontal") => true, + _ if writing_mode.starts_with("vertical") | writing_mode.starts_with("sideways") => { + false + } + // deprecated values + "lr" | "lr-tb" | "rl" => true, + "tb" | "tb-lr" | "tb-rl" => false, + _ => { + log::warn!("unrecognized `writing-mode`, assuming horizontal"); + true + } + }; + + if horizontal { + PhysicalSize::new(entry.inline_size() as u32, entry.block_size() as u32) + } else { + PhysicalSize::new(entry.block_size() as u32, entry.inline_size() as u32) + } + } +} + +impl Drop for ResizeHandle { + fn drop(&mut self) { + self.observer.disconnect(); + } +} + +fn has_device_pixel_support() -> bool { + thread_local! { + static DEVICE_PIXEL_SUPPORT: Lazy = Lazy::new(|| { + #[wasm_bindgen] + extern "C" { + type ResizeObserverEntryExt; + + #[wasm_bindgen(js_class = ResizeObserverEntry, static_method_of = ResizeObserverEntryExt, getter)] + fn prototype() -> Object; + } + + let prototype = ResizeObserverEntryExt::prototype(); + let descriptor = Object::get_own_property_descriptor( + &prototype, + &JsValue::from_str("devicePixelContentBoxSize"), + ); + !descriptor.is_undefined() + }); + } + + DEVICE_PIXEL_SUPPORT.with(|support| **support) +} diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 9b1caefbbde..55ff141e2be 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -1,6 +1,5 @@ use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{ExternalError, NotSupportedError, OsError as RootOE}; -use crate::event; use crate::icon::Icon; use crate::window::{ CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, @@ -31,7 +30,6 @@ pub struct Inner { canvas: Rc>, previous_pointer: RefCell<&'static str>, register_redraw_request: Box, - resize_notify_fn: Box)>, destroy_fn: Option>, } @@ -56,14 +54,6 @@ impl Window { let has_focus = Arc::new(AtomicBool::new(false)); target.register(&canvas, id, prevent_default, has_focus.clone()); - let runner = target.runner.clone(); - let resize_notify_fn = Box::new(move |new_size| { - runner.send_event(event::Event::WindowEvent { - window_id: RootWI(id), - event: event::WindowEvent::Resized(new_size), - }); - }); - let runner = target.runner.clone(); let destroy_fn = Box::new(move || runner.notify_destroy_window(RootWI(id))); @@ -75,7 +65,6 @@ impl Window { canvas, previous_pointer: RefCell::new("auto"), register_redraw_request, - resize_notify_fn, destroy_fn: Some(destroy_fn), }) .unwrap(), @@ -149,7 +138,7 @@ impl Window { #[inline] pub fn inner_size(&self) -> PhysicalSize { - self.inner.queue(|inner| inner.inner_size()) + self.inner.queue(|inner| inner.canvas.borrow().inner_size()) } #[inline] @@ -161,12 +150,8 @@ impl Window { #[inline] pub fn set_inner_size(&self, size: Size) { self.inner.dispatch(move |inner| { - let old_size = inner.inner_size(); - backend::set_canvas_size(&inner.canvas.borrow(), size); - let new_size = inner.inner_size(); - if old_size != new_size { - (inner.resize_notify_fn)(new_size); - } + let size = size.to_physical(inner.scale_factor()); + backend::set_canvas_size(inner.canvas.borrow().raw(), size); }); } @@ -442,11 +427,6 @@ impl Inner { pub fn scale_factor(&self) -> f64 { super::backend::scale_factor(&self.window) } - - #[inline] - pub fn inner_size(&self) -> PhysicalSize { - self.canvas.borrow().size().get() - } } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]