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 5, 2023
1 parent c88a4ab commit e09c43a
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 74 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
- On Web, use the correct canvas size when calculating the new size during scale factor change,
instead of using the output bitmap size.
- 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 @@ -145,7 +145,6 @@ features = [
'HtmlElement',
'KeyboardEvent',
'MediaQueryList',
'MediaQueryListEvent',
'Node',
'PointerEvent',
'Window',
Expand Down
16 changes: 6 additions & 10 deletions src/platform_impl/web/web_sys/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ use smol_str::SmolStr;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{closure::Closure, JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::{
Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, MediaQueryListEvent, WheelEvent,
};
use web_sys::{Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, WheelEvent};

#[allow(dead_code)]
pub struct Canvas {
Expand Down Expand Up @@ -343,13 +341,11 @@ 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(&self.common.window, "(prefers-color-scheme: dark)", closure);
self.on_dark_mode = Some(MediaQueryListHandle::new(
&self.common.window,
"(prefers-color-scheme: dark)",
move |mql| handler(mql.matches()),
));
}

pub fn request_fullscreen(&self) {
Expand Down
52 changes: 22 additions & 30 deletions src/platform_impl/web/web_sys/media_query_handle.rs
Original file line number Diff line number Diff line change
@@ -1,54 +1,46 @@
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(
window: &web_sys::Window,
media_query: &str,
listener: Closure<dyn FnMut(MediaQueryListEvent)>,
) -> Option<Self> {
pub fn new<F>(window: &web_sys::Window, media_query: &str, mut listener: F) -> Self
where
F: 'static + FnMut(&MediaQueryList),
{
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
70 changes: 37 additions & 33 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 @@ -39,53 +39,57 @@ impl ScaleChangeDetectorInternal {
}));

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() {
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();
borrowed_self.mql = mql;
borrowed_self.mql = Some(mql);
}
new_self
}

fn create_mql(
window: &web_sys::Window,
closure: Closure<dyn FnMut(MediaQueryListEvent)>,
) -> Option<MediaQueryListHandle> {
fn create_mql<F>(window: &web_sys::Window, closure: F) -> MediaQueryListHandle
where
F: 'static + FnMut(&MediaQueryList),
{
let current_scale = super::scale_factor(window);
// 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(window, &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(window,)
);
mql
}

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

0 comments on commit e09c43a

Please sign in to comment.