Make media queries more robust

This commit is contained in:
dAxpeDDa 2023-06-05 01:23:48 +02:00 committed by daxpedda
parent c88a4ab221
commit 12fb37d827
5 changed files with 66 additions and 74 deletions

View file

@ -73,6 +73,7 @@ And please only add new entries to the top of this list, right below the `# Unre
- **Breaking:** `WindowExtWebSys::canvas()` now returns an `Option`. - **Breaking:** `WindowExtWebSys::canvas()` now returns an `Option`.
- On Web, use the correct canvas size when calculating the new size during scale factor change, - On Web, use the correct canvas size when calculating the new size during scale factor change,
instead of using the output bitmap size. instead of using the output bitmap size.
- On Web, scale factor and dark mode detection are now more robust.
# 0.28.6 # 0.28.6

View file

@ -145,7 +145,6 @@ features = [
'HtmlElement', 'HtmlElement',
'KeyboardEvent', 'KeyboardEvent',
'MediaQueryList', 'MediaQueryList',
'MediaQueryListEvent',
'Node', 'Node',
'PointerEvent', 'PointerEvent',
'Window', 'Window',

View file

@ -17,9 +17,7 @@ use smol_str::SmolStr;
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{closure::Closure, JsCast, JsValue}; use wasm_bindgen::{closure::Closure, JsCast, JsValue};
use wasm_bindgen_futures::JsFuture; use wasm_bindgen_futures::JsFuture;
use web_sys::{ use web_sys::{Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, WheelEvent};
Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, MediaQueryListEvent, WheelEvent,
};
#[allow(dead_code)] #[allow(dead_code)]
pub struct Canvas { pub struct Canvas {
@ -343,13 +341,11 @@ impl Canvas {
where where
F: 'static + FnMut(bool), F: 'static + FnMut(bool),
{ {
let closure = self.on_dark_mode = Some(MediaQueryListHandle::new(
Closure::wrap( &self.common.window,
Box::new(move |event: MediaQueryListEvent| handler(event.matches())) "(prefers-color-scheme: dark)",
as Box<dyn FnMut(_)>, move |mql| handler(mql.matches()),
); ));
self.on_dark_mode =
MediaQueryListHandle::new(&self.common.window, "(prefers-color-scheme: dark)", closure);
} }
pub fn request_fullscreen(&self) { pub fn request_fullscreen(&self) {

View file

@ -1,54 +1,46 @@
use wasm_bindgen::{prelude::Closure, JsCast}; use wasm_bindgen::{prelude::Closure, JsCast};
use web_sys::{MediaQueryList, MediaQueryListEvent}; use web_sys::MediaQueryList;
pub(super) struct MediaQueryListHandle { pub(super) struct MediaQueryListHandle {
mql: MediaQueryList, mql: MediaQueryList,
listener: Option<Closure<dyn FnMut(MediaQueryListEvent)>>, closure: Closure<dyn FnMut()>,
} }
impl MediaQueryListHandle { impl MediaQueryListHandle {
pub fn new( pub fn new<F>(window: &web_sys::Window, media_query: &str, mut listener: F) -> Self
window: &web_sys::Window, where
media_query: &str, F: 'static + FnMut(&MediaQueryList),
listener: Closure<dyn FnMut(MediaQueryListEvent)>, {
) -> Option<Self> {
let mql = window let mql = window
.match_media(media_query) .match_media(media_query)
.ok() .expect("Failed to parse media query")
.flatten() .expect("Found empty media query");
.and_then(|mql| {
mql.add_listener_with_opt_callback(Some(listener.as_ref().unchecked_ref())) let closure = Closure::new({
.map(|_| mql) let mql = mql.clone();
.ok() move || listener(&mql)
}); });
mql.map(|mql| Self { // TODO: Replace obsolete `addListener()` with `addEventListener()` and use
mql, // `MediaQueryListEvent` instead of cloning the `MediaQueryList`.
listener: Some(listener), // Requires Safari v14.
}) mql.add_listener_with_opt_callback(Some(closure.as_ref().unchecked_ref()))
.expect("Invalid listener");
Self { mql, closure }
} }
pub fn mql(&self) -> &MediaQueryList { pub fn mql(&self) -> &MediaQueryList {
&self.mql &self.mql
} }
/// Removes the listener and returns the original listener closure, which
/// can be reused.
pub fn remove(mut self) -> Closure<dyn FnMut(MediaQueryListEvent)> {
let listener = self.listener.take().unwrap_or_else(|| unreachable!());
remove_listener(&self.mql, &listener);
listener
}
} }
impl Drop for MediaQueryListHandle { impl Drop for MediaQueryListHandle {
fn drop(&mut self) { fn drop(&mut self) {
if let Some(listener) = self.listener.take() { remove_listener(&self.mql, &self.closure);
remove_listener(&self.mql, &listener);
}
} }
} }
fn remove_listener(mql: &MediaQueryList, listener: &Closure<dyn FnMut(MediaQueryListEvent)>) { fn remove_listener(mql: &MediaQueryList, listener: &Closure<dyn FnMut()>) {
mql.remove_listener_with_opt_callback(Some(listener.as_ref().unchecked_ref())) mql.remove_listener_with_opt_callback(Some(listener.as_ref().unchecked_ref()))
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
web_sys::console::error_2(&"Error removing media query listener".into(), &e) web_sys::console::error_2(&"Error removing media query listener".into(), &e)

View file

@ -1,9 +1,9 @@
use web_sys::MediaQueryList;
use super::super::ScaleChangeArgs; use super::super::ScaleChangeArgs;
use super::media_query_handle::MediaQueryListHandle; use super::media_query_handle::MediaQueryListHandle;
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use wasm_bindgen::prelude::Closure;
use web_sys::MediaQueryListEvent;
pub struct ScaleChangeDetector(Rc<RefCell<ScaleChangeDetectorInternal>>); pub struct ScaleChangeDetector(Rc<RefCell<ScaleChangeDetectorInternal>>);
@ -39,53 +39,57 @@ impl ScaleChangeDetectorInternal {
})); }));
let weak_self = Rc::downgrade(&new_self); let weak_self = Rc::downgrade(&new_self);
let closure = Closure::wrap(Box::new(move |event: MediaQueryListEvent| { let mql = Self::create_mql(&window, move |mql| {
if let Some(rc_self) = weak_self.upgrade() { if let Some(rc_self) = weak_self.upgrade() {
rc_self.borrow_mut().handler(event); Self::handler(rc_self, mql);
} }
}) as Box<dyn FnMut(_)>); });
let mql = Self::create_mql(&window, closure);
{ {
let mut borrowed_self = new_self.borrow_mut(); let mut borrowed_self = new_self.borrow_mut();
borrowed_self.mql = mql; borrowed_self.mql = Some(mql);
} }
new_self new_self
} }
fn create_mql( fn create_mql<F>(window: &web_sys::Window, closure: F) -> MediaQueryListHandle
window: &web_sys::Window, where
closure: Closure<dyn FnMut(MediaQueryListEvent)>, F: 'static + FnMut(&MediaQueryList),
) -> Option<MediaQueryListHandle> { {
let current_scale = super::scale_factor(window); let current_scale = super::scale_factor(window);
// This media query initially matches the current `devicePixelRatio`. // TODO: Remove `-webkit-device-pixel-ratio`. Requires Safari v16.
// We add 0.0001 to the lower and upper bounds such that it won't fail
// due to floating point precision limitations.
let media_query = format!( let media_query = format!(
"(min-resolution: {min_scale:.4}dppx) and (max-resolution: {max_scale:.4}dppx), "(resolution: {current_scale}dppx),
(-webkit-min-device-pixel-ratio: {min_scale:.4}) and (-webkit-max-device-pixel-ratio: {max_scale:.4})", (-webkit-device-pixel-ratio: {current_scale})",
min_scale = current_scale - 0.0001, max_scale= current_scale + 0.0001,
); );
let mql = MediaQueryListHandle::new(window, &media_query, closure); let mql = MediaQueryListHandle::new(window, &media_query, closure);
if let Some(mql) = &mql { assert!(
assert!(mql.mql().matches()); mql.mql().matches(),
} "created media query doesn't match, {current_scale} != {}",
super::scale_factor(window,)
);
mql mql
} }
fn handler(&mut self, _event: MediaQueryListEvent) { fn handler(this: Rc<RefCell<Self>>, mql: &MediaQueryList) {
let mql = self let weak_self = Rc::downgrade(&this);
.mql let mut this = this.borrow_mut();
.take() let old_scale = this.last_scale;
.expect("DevicePixelRatioChangeDetector::mql should not be None"); let new_scale = super::scale_factor(&this.window);
let closure = mql.remove(); (this.callback)(ScaleChangeArgs {
let new_scale = super::scale_factor(&self.window); old_scale,
(self.callback)(ScaleChangeArgs {
old_scale: self.last_scale,
new_scale, new_scale,
}); });
let new_mql = Self::create_mql(&self.window, closure);
self.mql = new_mql; // If this matches, then the scale factor is back to it's
self.last_scale = new_scale; // old value again, so we won't need to update the query.
if !mql.matches() {
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 = Some(new_mql);
this.last_scale = new_scale;
}
} }
} }