This avoids using JavaScript exceptions to support `EventLoop::run_app` on the web, which is a huge hack, and doesn't work with the Exception Handling Proposal for WebAssembly: https://github.com/WebAssembly/exception-handling This needs the application handler passed to `run_app` to be `'static`, but that works better on iOS too anyhow (since you can't accidentally forget to pass in state that then wouldn't be dropped when terminating).
258 lines
7.9 KiB
Rust
258 lines
7.9 KiB
Rust
mod animation_frame;
|
|
mod canvas;
|
|
pub mod event;
|
|
mod event_handle;
|
|
mod fullscreen;
|
|
mod intersection_handle;
|
|
mod media_query_handle;
|
|
mod pointer;
|
|
mod resize_scaling;
|
|
mod safe_area;
|
|
mod schedule;
|
|
|
|
use std::cell::OnceCell;
|
|
|
|
use dpi::{LogicalPosition, LogicalSize};
|
|
use js_sys::Array;
|
|
use wasm_bindgen::JsCast;
|
|
use wasm_bindgen::closure::Closure;
|
|
use wasm_bindgen::prelude::wasm_bindgen;
|
|
use web_sys::{Document, HtmlCanvasElement, Navigator, PageTransitionEvent, VisibilityState};
|
|
|
|
pub use self::canvas::{Canvas, Style};
|
|
pub use self::event_handle::EventListenerHandle;
|
|
pub use self::resize_scaling::ResizeScaleHandle;
|
|
pub use self::safe_area::SafeAreaHandle;
|
|
pub use self::schedule::Schedule;
|
|
|
|
pub struct PageTransitionEventHandle {
|
|
_show_listener: event_handle::EventListenerHandle<dyn FnMut(PageTransitionEvent)>,
|
|
_hide_listener: event_handle::EventListenerHandle<dyn FnMut(PageTransitionEvent)>,
|
|
}
|
|
|
|
pub fn on_page_transition(
|
|
window: web_sys::Window,
|
|
show_handler: impl FnMut(PageTransitionEvent) + 'static,
|
|
hide_handler: impl FnMut(PageTransitionEvent) + 'static,
|
|
) -> PageTransitionEventHandle {
|
|
let show_closure = Closure::new(show_handler);
|
|
let hide_closure = Closure::new(hide_handler);
|
|
|
|
let show_listener =
|
|
event_handle::EventListenerHandle::new(window.clone(), "pageshow", show_closure);
|
|
let hide_listener = event_handle::EventListenerHandle::new(window, "pagehide", hide_closure);
|
|
PageTransitionEventHandle { _show_listener: show_listener, _hide_listener: hide_listener }
|
|
}
|
|
|
|
pub fn scale_factor(window: &web_sys::Window) -> f64 {
|
|
window.device_pixel_ratio()
|
|
}
|
|
|
|
fn fix_canvas_size(style: &Style, mut size: LogicalSize<f64>) -> LogicalSize<f64> {
|
|
if style.get("box-sizing") == "border-box" {
|
|
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");
|
|
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");
|
|
}
|
|
|
|
size
|
|
}
|
|
|
|
pub fn set_canvas_size(
|
|
document: &Document,
|
|
raw: &HtmlCanvasElement,
|
|
style: &Style,
|
|
new_size: LogicalSize<f64>,
|
|
) {
|
|
if !document.contains(Some(raw)) || style.get("display") == "none" {
|
|
return;
|
|
}
|
|
|
|
let new_size = fix_canvas_size(style, new_size);
|
|
|
|
style.set("width", &format!("{}px", new_size.width));
|
|
style.set("height", &format!("{}px", new_size.height));
|
|
}
|
|
|
|
pub fn set_canvas_min_size(
|
|
document: &Document,
|
|
raw: &HtmlCanvasElement,
|
|
style: &Style,
|
|
dimensions: Option<LogicalSize<f64>>,
|
|
) {
|
|
if let Some(dimensions) = dimensions {
|
|
if !document.contains(Some(raw)) || style.get("display") == "none" {
|
|
return;
|
|
}
|
|
|
|
let new_size = fix_canvas_size(style, dimensions);
|
|
|
|
style.set("min-width", &format!("{}px", new_size.width));
|
|
style.set("min-height", &format!("{}px", new_size.height));
|
|
} else {
|
|
style.remove("min-width");
|
|
style.remove("min-height");
|
|
}
|
|
}
|
|
|
|
pub fn set_canvas_max_size(
|
|
document: &Document,
|
|
raw: &HtmlCanvasElement,
|
|
style: &Style,
|
|
dimensions: Option<LogicalSize<f64>>,
|
|
) {
|
|
if let Some(dimensions) = dimensions {
|
|
if !document.contains(Some(raw)) || style.get("display") == "none" {
|
|
return;
|
|
}
|
|
|
|
let new_size = fix_canvas_size(style, dimensions);
|
|
|
|
style.set("max-width", &format!("{}px", new_size.width));
|
|
style.set("max-height", &format!("{}px", new_size.height));
|
|
} else {
|
|
style.remove("max-width");
|
|
style.remove("max-height");
|
|
}
|
|
}
|
|
|
|
pub fn set_canvas_position(
|
|
document: &Document,
|
|
raw: &HtmlCanvasElement,
|
|
style: &Style,
|
|
mut position: LogicalPosition<f64>,
|
|
) {
|
|
if document.contains(Some(raw)) && style.get("display") != "none" {
|
|
position.x -= style_size_property(style, "margin-left")
|
|
+ style_size_property(style, "border-left-width")
|
|
+ style_size_property(style, "padding-left");
|
|
position.y -= style_size_property(style, "margin-top")
|
|
+ style_size_property(style, "border-top-width")
|
|
+ style_size_property(style, "padding-top");
|
|
}
|
|
|
|
style.set("position", "fixed");
|
|
style.set("left", &format!("{}px", position.x));
|
|
style.set("top", &format!("{}px", position.y));
|
|
}
|
|
|
|
/// 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: &Style, property: &str) -> f64 {
|
|
let prop = style.get(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 is_dark_mode(window: &web_sys::Window) -> Option<bool> {
|
|
window.match_media("(prefers-color-scheme: dark)").ok().flatten().map(|media| media.matches())
|
|
}
|
|
|
|
pub fn is_visible(document: &Document) -> bool {
|
|
document.visibility_state() == VisibilityState::Visible
|
|
}
|
|
|
|
pub type RawCanvasType = HtmlCanvasElement;
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub enum Engine {
|
|
Chromium,
|
|
Gecko,
|
|
WebKit,
|
|
}
|
|
|
|
struct UserAgentData {
|
|
engine: Option<Engine>,
|
|
chrome_linux: bool,
|
|
}
|
|
|
|
thread_local! {
|
|
static USER_AGENT_DATA: OnceCell<UserAgentData> = const { OnceCell::new() };
|
|
}
|
|
|
|
pub fn chrome_linux(navigator: &Navigator) -> bool {
|
|
USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(navigator)).chrome_linux)
|
|
}
|
|
|
|
pub fn engine(navigator: &Navigator) -> Option<Engine> {
|
|
USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(navigator)).engine)
|
|
}
|
|
|
|
fn user_agent(navigator: &Navigator) -> UserAgentData {
|
|
#[wasm_bindgen]
|
|
extern "C" {
|
|
#[wasm_bindgen(extends = Navigator)]
|
|
type NavigatorExt;
|
|
|
|
#[wasm_bindgen(method, getter, js_name = userAgentData)]
|
|
fn user_agent_data(this: &NavigatorExt) -> Option<NavigatorUaData>;
|
|
|
|
type NavigatorUaData;
|
|
|
|
#[wasm_bindgen(method, getter)]
|
|
fn brands(this: &NavigatorUaData) -> Array;
|
|
|
|
#[wasm_bindgen(method, getter)]
|
|
fn platform(this: &NavigatorUaData) -> String;
|
|
|
|
type NavigatorUaBrandVersion;
|
|
|
|
#[wasm_bindgen(method, getter)]
|
|
fn brand(this: &NavigatorUaBrandVersion) -> String;
|
|
}
|
|
|
|
let navigator: &NavigatorExt = navigator.unchecked_ref();
|
|
|
|
if let Some(data) = navigator.user_agent_data() {
|
|
let engine = 'engine: {
|
|
for brand in data
|
|
.brands()
|
|
.iter()
|
|
.map(NavigatorUaBrandVersion::unchecked_from_js)
|
|
.map(|brand| brand.brand())
|
|
{
|
|
match brand.as_str() {
|
|
"Chromium" => break 'engine Some(Engine::Chromium),
|
|
// TODO: verify when Firefox actually implements it.
|
|
"Gecko" => break 'engine Some(Engine::Gecko),
|
|
// TODO: verify when Safari actually implements it.
|
|
"WebKit" => break 'engine Some(Engine::WebKit),
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
None
|
|
};
|
|
|
|
let chrome_linux = matches!(engine, Some(Engine::Chromium))
|
|
.then(|| data.platform() == "Linux")
|
|
.unwrap_or(false);
|
|
|
|
UserAgentData { engine, chrome_linux }
|
|
} else {
|
|
let engine = 'engine: {
|
|
let Ok(data) = navigator.user_agent() else {
|
|
break 'engine None;
|
|
};
|
|
|
|
if data.contains("Chrome/") {
|
|
Some(Engine::Chromium)
|
|
} else if data.contains("Gecko/") {
|
|
Some(Engine::Gecko)
|
|
} else if data.contains("AppleWebKit/") {
|
|
Some(Engine::WebKit)
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
|
|
UserAgentData { engine, chrome_linux: false }
|
|
}
|
|
}
|