Implement ResizeObserver (#2859)

Co-authored-by: Liam Murphy <43807659+Liamolucko@users.noreply.github.com>
This commit is contained in:
daxpedda 2023-06-14 09:43:53 +02:00 committed by GitHub
parent 7ce86c3d2a
commit 9a9c9b15ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 584 additions and 364 deletions

View file

@ -1,13 +1,14 @@
use super::super::WindowId;
use super::event_handle::EventListenerHandle;
use super::media_query_handle::MediaQueryListHandle;
use super::pointer::PointerHandler;
use super::{event, ButtonsState};
use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize};
use super::{event, ButtonsState, ResizeScaleHandle};
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};
use crate::platform_impl::{OsError, PlatformSpecificWindowBuilderAttributes};
use crate::window::WindowAttributes;
use crate::window::{WindowAttributes, WindowId as RootWindowId};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
@ -22,6 +23,7 @@ use web_sys::{Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, WheelEvent};
#[allow(dead_code)]
pub struct Canvas {
common: Common,
id: WindowId,
on_touch_start: Option<EventListenerHandle<dyn FnMut(Event)>>,
on_touch_end: Option<EventListenerHandle<dyn FnMut(Event)>>,
on_focus: Option<EventListenerHandle<dyn FnMut(FocusEvent)>>,
@ -29,21 +31,23 @@ pub struct Canvas {
on_keyboard_release: Option<EventListenerHandle<dyn FnMut(KeyboardEvent)>>,
on_keyboard_press: Option<EventListenerHandle<dyn FnMut(KeyboardEvent)>>,
on_mouse_wheel: Option<EventListenerHandle<dyn FnMut(WheelEvent)>>,
on_fullscreen_change: Option<EventListenerHandle<dyn FnMut(Event)>>,
on_dark_mode: Option<MediaQueryListHandle>,
pointer_handler: PointerHandler,
on_resize_scale: Option<ResizeScaleHandle>,
}
pub struct Common {
pub window: web_sys::Window,
/// Note: resizing the HTMLCanvasElement should go through `backend::set_canvas_size` to ensure the DPI factor is maintained.
pub raw: HtmlCanvasElement,
size: Rc<Cell<PhysicalSize<u32>>>,
old_size: Rc<Cell<PhysicalSize<u32>>>,
current_size: Rc<Cell<PhysicalSize<u32>>>,
wants_fullscreen: Rc<RefCell<bool>>,
}
impl Canvas {
pub fn create(
id: WindowId,
window: web_sys::Window,
attr: &WindowAttributes,
platform_attr: PlatformSpecificWindowBuilderAttributes,
@ -73,24 +77,20 @@ 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_logical(super::scale_factor(&window));
super::set_canvas_size(&window, &canvas, size);
}
let canvas = Canvas {
Ok(Canvas {
common: Common {
window,
raw: canvas,
size: Rc::new(Cell::new(size)),
old_size: Rc::default(),
current_size: Rc::default(),
wants_fullscreen: Rc::new(RefCell::new(false)),
},
id,
on_touch_start: None,
on_touch_end: None,
on_blur: None,
@ -98,14 +98,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_scale: None,
})
}
pub fn set_cursor_lock(&self, lock: bool) -> Result<(), RootOE> {
@ -138,12 +134,24 @@ impl Canvas {
}
}
pub fn window(&self) -> &web_sys::Window {
&self.common.window
pub fn old_size(&self) -> PhysicalSize<u32> {
self.common.old_size.get()
}
pub fn size(&self) -> &Rc<Cell<PhysicalSize<u32>>> {
&self.common.size
pub fn inner_size(&self) -> PhysicalSize<u32> {
self.common.current_size.get()
}
pub fn set_old_size(&self, size: PhysicalSize<u32>) {
self.common.old_size.set(size)
}
pub fn set_current_size(&self, size: PhysicalSize<u32>) {
self.common.current_size.set(size)
}
pub fn window(&self) -> &web_sys::Window {
&self.common.window
}
pub fn raw(&self) -> &HtmlCanvasElement {
@ -329,16 +337,6 @@ impl Canvas {
}));
}
pub fn on_fullscreen_change<F>(&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<F>(&mut self, mut handler: F)
where
F: 'static + FnMut(bool),
@ -350,6 +348,19 @@ impl Canvas {
));
}
pub(crate) fn on_resize_scale<S, R>(&mut self, scale_handler: S, size_handler: R)
where
S: 'static + FnMut(PhysicalSize<u32>, f64),
R: 'static + FnMut(PhysicalSize<u32>),
{
self.on_resize_scale = Some(ResizeScaleHandle::new(
self.window().clone(),
self.raw().clone(),
scale_handler,
size_handler,
));
}
pub fn request_fullscreen(&self) {
self.common.request_fullscreen()
}
@ -358,15 +369,54 @@ impl Canvas {
self.common.is_fullscreen()
}
pub(crate) fn handle_scale_change<T: 'static>(
&self,
runner: &super::super::event_loop::runner::Shared<T>,
event_handler: impl FnOnce(crate::event::Event<'_, T>),
current_size: PhysicalSize<u32>,
scale: f64,
) {
// First, we send the `ScaleFactorChanged` event:
self.set_current_size(current_size);
let mut new_size = current_size;
event_handler(crate::event::Event::WindowEvent {
window_id: RootWindowId(self.id),
event: crate::event::WindowEvent::ScaleFactorChanged {
scale_factor: scale,
new_inner_size: &mut new_size,
},
});
if current_size != new_size {
// Then we resize the canvas to the new size, a new
// `Resized` event will be sent by the `ResizeObserver`:
let new_size = new_size.to_logical(scale);
super::set_canvas_size(self.window(), self.raw(), new_size);
// Set the size might not trigger the event because the calculation is inaccurate.
self.on_resize_scale
.as_ref()
.expect("expected Window to still be active")
.notify_resize();
} else if self.old_size() != new_size {
// Then we at least send a resized event.
self.set_old_size(new_size);
runner.send_event(crate::event::Event::WindowEvent {
window_id: RootWindowId(self.id),
event: crate::event::WindowEvent::Resized(new_size),
})
}
}
pub fn remove_listeners(&mut self) {
self.on_focus = None;
self.on_blur = None;
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_scale = None;
}
}

View file

@ -3,19 +3,19 @@ mod event;
mod event_handle;
mod media_query_handle;
mod pointer;
mod scaling;
mod resize_scaling;
mod timeout;
pub use self::canvas::Canvas;
pub use self::event::ButtonsState;
pub use self::scaling::ScaleChangeDetector;
pub use self::resize_scaling::ResizeScaleHandle;
pub use self::timeout::{IdleCallback, Timeout};
use crate::dpi::{LogicalSize, Size};
use crate::dpi::LogicalSize;
use crate::platform::web::WindowExtWebSys;
use crate::window::Window;
use wasm_bindgen::closure::Closure;
use web_sys::{Element, HtmlCanvasElement};
use web_sys::{CssStyleDeclaration, Element, HtmlCanvasElement};
pub fn throw(msg: &str) {
wasm_bindgen::throw_str(msg);
@ -52,38 +52,52 @@ impl WindowExtWebSys for Window {
}
}
pub fn window_size(window: &web_sys::Window) -> LogicalSize<f64> {
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());
pub fn set_canvas_size(
window: &web_sys::Window,
raw: &HtmlCanvasElement,
mut new_size: LogicalSize<f64>,
) {
let document = window.document().expect("Failed to obtain document");
let physical_size = new_size.to_physical(scale_factor);
canvas.size().set(physical_size);
let style = window
.get_computed_style(raw)
.expect("Failed to obtain computed style")
// this can't fail: we aren't using a pseudo-element
.expect("Invalid pseudo-element");
let logical_size = new_size.to_logical::<f64>(scale_factor);
set_canvas_style_property(canvas.raw(), "width", &format!("{}px", logical_size.width));
set_canvas_style_property(
canvas.raw(),
"height",
&format!("{}px", logical_size.height),
);
if !document.contains(Some(raw)) || style.get_property_value("display").unwrap() == "none" {
return;
}
if style.get_property_value("box-sizing").unwrap() == "border-box" {
new_size.width += style_size_property(&style, "border-left-width")
+ style_size_property(&style, "border-right-width")
+ style_size_property(&style, "padding-left")
+ style_size_property(&style, "padding-right");
new_size.height += style_size_property(&style, "border-top-width")
+ style_size_property(&style, "border-bottom-width")
+ style_size_property(&style, "padding-top")
+ style_size_property(&style, "padding-bottom");
}
set_canvas_style_property(raw, "width", &format!("{}px", new_size.width));
set_canvas_style_property(raw, "height", &format!("{}px", new_size.height));
}
/// This function will panic if the element is not inserted in the DOM
/// or is not a CSS property that represents a size in pixel.
pub fn style_size_property(style: &CssStyleDeclaration, property: &str) -> f64 {
let prop = style
.get_property_value(property)
.expect("Found invalid property");
prop.strip_suffix("px")
.expect("Element was not inserted into the DOM or is not a size in pixel")
.parse()
.expect("CSS property is not a size in pixel")
}
pub fn set_canvas_style_property(raw: &HtmlCanvasElement, property: &str, value: &str) {

View file

@ -0,0 +1,321 @@
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, MediaQueryList, ResizeObserver, ResizeObserverBoxOptions,
ResizeObserverEntry, ResizeObserverOptions, ResizeObserverSize, Window,
};
use crate::dpi::{LogicalSize, PhysicalSize};
use super::super::backend;
use super::media_query_handle::MediaQueryListHandle;
use std::cell::{Cell, RefCell};
use std::rc::Rc;
pub struct ResizeScaleHandle(Rc<RefCell<ResizeScaleInternal>>);
impl ResizeScaleHandle {
pub(crate) fn new<S, R>(
window: Window,
canvas: HtmlCanvasElement,
scale_handler: S,
resize_handler: R,
) -> Self
where
S: 'static + FnMut(PhysicalSize<u32>, f64),
R: 'static + FnMut(PhysicalSize<u32>),
{
Self(ResizeScaleInternal::new(
window,
canvas,
scale_handler,
resize_handler,
))
}
pub(crate) fn notify_resize(&self) {
self.0.borrow_mut().notify()
}
}
/// This is a helper type to help manage the `MediaQueryList` used for detecting
/// changes of the `devicePixelRatio`.
struct ResizeScaleInternal {
window: Window,
canvas: HtmlCanvasElement,
mql: MediaQueryListHandle,
observer: ResizeObserver,
_observer_closure: Closure<dyn FnMut(Array, ResizeObserver)>,
scale_handler: Box<dyn FnMut(PhysicalSize<u32>, f64)>,
resize_handler: Box<dyn FnMut(PhysicalSize<u32>)>,
notify_scale: Cell<bool>,
}
impl ResizeScaleInternal {
fn new<S, R>(
window: Window,
canvas: HtmlCanvasElement,
scale_handler: S,
resize_handler: R,
) -> Rc<RefCell<Self>>
where
S: 'static + FnMut(PhysicalSize<u32>, f64),
R: 'static + FnMut(PhysicalSize<u32>),
{
Rc::<RefCell<ResizeScaleInternal>>::new_cyclic(|weak_self| {
let mql = Self::create_mql(&window, {
let weak_self = weak_self.clone();
move |mql| {
if let Some(rc_self) = weak_self.upgrade() {
Self::handle_scale(rc_self, mql);
}
}
});
let weak_self = weak_self.clone();
let observer_closure = Closure::new(move |entries: Array, _| {
if let Some(rc_self) = weak_self.upgrade() {
let mut this = rc_self.borrow_mut();
let size = Self::process_entry(&this.window, &this.canvas, entries);
if this.notify_scale.replace(false) {
let scale = backend::scale_factor(&this.window);
(this.scale_handler)(size, scale)
} else {
(this.resize_handler)(size)
}
}
});
let observer = Self::create_observer(&canvas, observer_closure.as_ref());
RefCell::new(Self {
window,
canvas,
mql,
observer,
_observer_closure: observer_closure,
scale_handler: Box::new(scale_handler),
resize_handler: Box::new(resize_handler),
notify_scale: Cell::new(false),
})
})
}
fn create_mql<F>(window: &Window, closure: F) -> MediaQueryListHandle
where
F: 'static + FnMut(&MediaQueryList),
{
let current_scale = super::scale_factor(window);
// TODO: Remove `-webkit-device-pixel-ratio`. Requires Safari v16.
let media_query = format!(
"(resolution: {current_scale}dppx),
(-webkit-device-pixel-ratio: {current_scale})",
);
let mql = MediaQueryListHandle::new(window, &media_query, closure);
assert!(
mql.mql().matches(),
"created media query doesn't match, {current_scale} != {}",
super::scale_factor(window)
);
mql
}
fn create_observer(canvas: &HtmlCanvasElement, closure: &JsValue) -> ResizeObserver {
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);
}
observer
}
fn notify(&mut self) {
let style = self
.window
.get_computed_style(&self.canvas)
.expect("Failed to obtain computed style")
// this can't fail: we aren't using a pseudo-element
.expect("Invalid pseudo-element");
let document = self.window.document().expect("Failed to obtain document");
if !document.contains(Some(&self.canvas))
|| style.get_property_value("display").unwrap() == "none"
{
let size = PhysicalSize::new(0, 0);
if self.notify_scale.replace(false) {
let scale = backend::scale_factor(&self.window);
(self.scale_handler)(size, scale)
} else {
(self.resize_handler)(size)
}
return;
}
// Safari doesn't support `devicePixelContentBoxSize`
if has_device_pixel_support() {
self.observer.unobserve(&self.canvas);
self.observer.observe(&self.canvas);
return;
}
let mut size = LogicalSize::new(
backend::style_size_property(&style, "width"),
backend::style_size_property(&style, "height"),
);
if style.get_property_value("box-sizing").unwrap() == "border-box" {
size.width -= backend::style_size_property(&style, "border-left-width")
+ backend::style_size_property(&style, "border-right-width")
+ backend::style_size_property(&style, "padding-left")
+ backend::style_size_property(&style, "padding-right");
size.height -= backend::style_size_property(&style, "border-top-width")
+ backend::style_size_property(&style, "border-bottom-width")
+ backend::style_size_property(&style, "padding-top")
+ backend::style_size_property(&style, "padding-bottom");
}
let size = size.to_physical(backend::scale_factor(&self.window));
if self.notify_scale.replace(false) {
let scale = backend::scale_factor(&self.window);
(self.scale_handler)(size, scale)
} else {
(self.resize_handler)(size)
}
}
fn handle_scale(this: Rc<RefCell<Self>>, mql: &MediaQueryList) {
let weak_self = Rc::downgrade(&this);
let mut this = this.borrow_mut();
let scale = super::scale_factor(&this.window);
// TODO: confirm/reproduce this problem, see:
// <https://github.com/rust-windowing/winit/issues/2597>.
// This should never happen, but if it does then apparently the scale factor didn't change.
if mql.matches() {
warn!(
"media query tracking scale factor was triggered without a change:\n\
Media Query: {}\n\
Current Scale: {scale}",
mql.media(),
);
return;
}
let new_mql = Self::create_mql(&this.window, move |mql| {
if let Some(rc_self) = weak_self.upgrade() {
Self::handle_scale(rc_self, mql);
}
});
this.mql = new_mql;
this.notify_scale.set(true);
this.notify();
}
fn process_entry(
window: &Window,
canvas: &HtmlCanvasElement,
entries: Array,
) -> PhysicalSize<u32> {
let entry: ResizeObserverEntry = entries.get(0).unchecked_into();
// Safari doesn't support `devicePixelContentBoxSize`
if !has_device_pixel_support() {
let rect = entry.content_rect();
return LogicalSize::new(rect.width(), rect.height())
.to_physical(backend::scale_factor(window));
}
let entry: ResizeObserverSize = entry
.device_pixel_content_box_size()
.get(0)
.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,
_ => {
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 ResizeScaleInternal {
fn drop(&mut self) {
self.observer.disconnect();
}
}
// TODO: Remove when Safari supports `devicePixelContentBoxSize`.
// See <https://bugs.webkit.org/show_bug.cgi?id=219005>.
pub fn has_device_pixel_support() -> bool {
thread_local! {
static DEVICE_PIXEL_SUPPORT: Lazy<bool> = 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)
}

View file

@ -1,102 +0,0 @@
use web_sys::MediaQueryList;
use super::super::ScaleChangeArgs;
use super::media_query_handle::MediaQueryListHandle;
use std::{cell::RefCell, rc::Rc};
pub struct ScaleChangeDetector(Rc<RefCell<ScaleChangeDetectorInternal>>);
impl ScaleChangeDetector {
pub(crate) fn new<F>(window: web_sys::Window, handler: F) -> Self
where
F: 'static + FnMut(ScaleChangeArgs),
{
Self(ScaleChangeDetectorInternal::new(window, handler))
}
}
/// This is a helper type to help manage the `MediaQueryList` used for detecting
/// changes of the `devicePixelRatio`.
struct ScaleChangeDetectorInternal {
window: web_sys::Window,
callback: Box<dyn FnMut(ScaleChangeArgs)>,
mql: MediaQueryListHandle,
last_scale: f64,
}
impl ScaleChangeDetectorInternal {
fn new<F>(window: web_sys::Window, handler: F) -> Rc<RefCell<Self>>
where
F: 'static + FnMut(ScaleChangeArgs),
{
let current_scale = super::scale_factor(&window);
Rc::new_cyclic(|weak_self| {
let weak_self = weak_self.clone();
let mql = Self::create_mql(&window, move |mql| {
if let Some(rc_self) = weak_self.upgrade() {
Self::handler(rc_self, mql);
}
});
RefCell::new(Self {
window,
callback: Box::new(handler),
mql,
last_scale: current_scale,
})
})
}
fn create_mql<F>(window: &web_sys::Window, closure: F) -> MediaQueryListHandle
where
F: 'static + FnMut(&MediaQueryList),
{
let current_scale = super::scale_factor(window);
// TODO: Remove `-webkit-device-pixel-ratio`. Requires Safari v16.
let media_query = format!(
"(resolution: {current_scale}dppx),
(-webkit-device-pixel-ratio: {current_scale})",
);
let mql = MediaQueryListHandle::new(window, &media_query, closure);
assert!(
mql.mql().matches(),
"created media query doesn't match, {current_scale} != {}",
super::scale_factor(window)
);
mql
}
fn handler(this: Rc<RefCell<Self>>, mql: &MediaQueryList) {
let weak_self = Rc::downgrade(&this);
let mut this = this.borrow_mut();
let old_scale = this.last_scale;
let new_scale = super::scale_factor(&this.window);
// TODO: confirm/reproduce this problem, see:
// <https://github.com/rust-windowing/winit/issues/2597>.
// This should never happen, but if it does then apparently the scale factor didn't change.
if mql.matches() {
warn!(
"media query tracking scale factor was triggered without a change:\n\
Media Query: {}\n\
Current Scale: {new_scale}",
mql.media(),
);
return;
}
(this.callback)(ScaleChangeArgs {
old_scale,
new_scale,
});
let new_mql = Self::create_mql(&this.window, move |mql| {
if let Some(rc_self) = weak_self.upgrade() {
Self::handler(rc_self, mql);
}
});
this.mql = new_mql;
this.last_scale = new_scale;
}
}