Skip to content

Commit

Permalink
Make media queries more robust
Browse files Browse the repository at this point in the history
  • Loading branch information
daxpedda committed Jun 4, 2023
1 parent 7500a88 commit a01d703
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 69 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ And please only add new entries to the top of this list, right below the `# Unre
- **Breaking:** On Web, `instant` is now replaced by `web_time`.
- On Windows, port to `windows-sys` version 0.48.0.
- On Web, fix pen treated as mouse input.
- On Web, scale factor and dark mode detection are now more robust.

# 0.28.6

Expand Down
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ features = [
'HtmlElement',
'KeyboardEvent',
'MediaQueryList',
'MediaQueryListEvent',
'MouseEvent',
'Node',
'PointerEvent',
Expand Down
14 changes: 6 additions & 8 deletions src/platform_impl/web/web_sys/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{closure::Closure, JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::{
AddEventListenerOptions, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent,
MediaQueryListEvent, MouseEvent, WheelEvent,
AddEventListenerOptions, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, MouseEvent,
WheelEvent,
};

mod mouse_handler;
Expand Down Expand Up @@ -331,12 +331,10 @@ impl Canvas {
where
F: 'static + FnMut(bool),
{
let closure =
Closure::wrap(
Box::new(move |event: MediaQueryListEvent| handler(event.matches()))
as Box<dyn FnMut(_)>,
);
self.on_dark_mode = MediaQueryListHandle::new("(prefers-color-scheme: dark)", closure);
self.on_dark_mode = Some(MediaQueryListHandle::new(
"(prefers-color-scheme: dark)",
move |mql| handler(mql.matches()),
));
}

pub fn request_fullscreen(&self) {
Expand Down
51 changes: 22 additions & 29 deletions src/platform_impl/web/web_sys/media_query_handle.rs
Original file line number Diff line number Diff line change
@@ -1,54 +1,47 @@
use wasm_bindgen::{prelude::Closure, JsCast};
use web_sys::{MediaQueryList, MediaQueryListEvent};
use web_sys::MediaQueryList;

pub(super) struct MediaQueryListHandle {
mql: MediaQueryList,
listener: Option<Closure<dyn FnMut(MediaQueryListEvent)>>,
closure: Closure<dyn FnMut()>,
}

impl MediaQueryListHandle {
pub fn new(
media_query: &str,
listener: Closure<dyn FnMut(MediaQueryListEvent)>,
) -> Option<Self> {
pub fn new<F>(media_query: &str, mut listener: F) -> Self
where
F: 'static + FnMut(&MediaQueryList),
{
let window = web_sys::window().expect("Failed to obtain window");
let mql = window
.match_media(media_query)
.ok()
.flatten()
.and_then(|mql| {
mql.add_listener_with_opt_callback(Some(listener.as_ref().unchecked_ref()))
.map(|_| mql)
.ok()
});
mql.map(|mql| Self {
mql,
listener: Some(listener),
})
.expect("Failed to parse media query")
.expect("Found empty media query");

let closure = Closure::new({
let mql = mql.clone();
move || listener(&mql)
});
// TODO: Replace obsolete `addListener()` with `addEventListener()` and use
// `MediaQueryListEvent` instead of cloning the `MediaQueryList`.
// 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 {
&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 {
fn drop(&mut self) {
if let Some(listener) = self.listener.take() {
remove_listener(&self.mql, &listener);
}
remove_listener(&self.mql, &self.closure);
}
}

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()))
.unwrap_or_else(|e| {
web_sys::console::error_2(&"Error removing media query listener".into(), &e)
Expand Down
67 changes: 36 additions & 31 deletions src/platform_impl/web/web_sys/scaling.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use web_sys::MediaQueryList;

use super::super::ScaleChangeArgs;
use super::media_query_handle::MediaQueryListHandle;

use std::{cell::RefCell, rc::Rc};
use wasm_bindgen::prelude::Closure;
use web_sys::MediaQueryListEvent;

pub struct ScaleChangeDetector(Rc<RefCell<ScaleChangeDetectorInternal>>);

Expand Down Expand Up @@ -37,52 +37,57 @@ impl ScaleChangeDetectorInternal {
}));

let weak_self = Rc::downgrade(&new_self);
let closure = Closure::wrap(Box::new(move |event: MediaQueryListEvent| {
let mql = Self::create_mql(move |mql| {
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(closure);
});
{
let mut borrowed_self = new_self.borrow_mut();
borrowed_self.mql = mql;
borrowed_self.mql = Some(mql);
}
new_self
}

fn create_mql(
closure: Closure<dyn FnMut(MediaQueryListEvent)>,
) -> Option<MediaQueryListHandle> {
fn create_mql<F>(closure: F) -> MediaQueryListHandle
where
F: 'static + FnMut(&MediaQueryList),
{
let current_scale = super::scale_factor();
// This media query initially matches the current `devicePixelRatio`.
// We add 0.0001 to the lower and upper bounds such that it won't fail
// due to floating point precision limitations.
// TODO: Remove `-webkit-device-pixel-ratio`. Requires Safari v16.
let media_query = format!(
"(min-resolution: {min_scale:.4}dppx) and (max-resolution: {max_scale:.4}dppx),
(-webkit-min-device-pixel-ratio: {min_scale:.4}) and (-webkit-max-device-pixel-ratio: {max_scale:.4})",
min_scale = current_scale - 0.0001, max_scale= current_scale + 0.0001,
"(resolution: {current_scale}dppx),
(-webkit-device-pixel-ratio: {current_scale})",
);
let mql = MediaQueryListHandle::new(&media_query, closure);
if let Some(mql) = &mql {
assert!(mql.mql().matches());
}
assert!(
mql.mql().matches(),
"created media query doesn't match, {current_scale} != {}",
super::scale_factor()
);
mql
}

fn handler(&mut self, _event: MediaQueryListEvent) {
let mql = self
.mql
.take()
.expect("DevicePixelRatioChangeDetector::mql should not be None");
let closure = mql.remove();
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();
(self.callback)(ScaleChangeArgs {
old_scale: self.last_scale,
(this.callback)(ScaleChangeArgs {
old_scale,
new_scale,
});
let new_mql = Self::create_mql(closure);
self.mql = new_mql;
self.last_scale = new_scale;

// If this matches, then the scale factor is back to it's
// old value again, so we won't need to update the query.
if !mql.matches() {
let new_mql = Self::create_mql(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;
}
}
}

0 comments on commit a01d703

Please sign in to comment.