use core::time::Duration;
use std::future::Future;
use js_sys::Uint8Array;
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::{Document, Element, HtmlCanvasElement, Response};
use winit::application::ApplicationHandler;
use winit::event_loop::EventLoop;
use winit::platform::web::{EventLoopExtWebSys, WindowAttributesExtWebSys, WindowExtWebSys};
use winit::window::{CursorGrabMode, Window, WindowAttributes};
use crate::platform::Platform;
use crate::{Config, Error};
pub use web_time::Instant;
pub(crate) const PLATFORM: Platform = Platform::Browser;
pub(crate) const AFTER_LOSS: &str = "Reload the page to try again. On a laptop with two graphics chips, set the browser to use the high-performance one in the system's graphics settings (Windows: Settings, System, Display, Graphics).";
pub(crate) const AFTER_OUT_OF_MEMORY: &str =
"Reload the page to try again, with other tabs and programs that use the graphics chip closed.";
const CANVAS_STYLE: &str =
"position:fixed;top:0;left:0;width:100%;height:100%;display:block;touch-action:none";
const OVERLAY_STYLE: &str = "position:fixed;top:0;left:0;right:0;bottom:0;margin:0;padding:1rem;\
background:#101014;color:#ff9090;font:14px/1.5 monospace;white-space:pre-wrap;z-index:2147483647";
pub(crate) fn install_diagnostics() {
console_error_panic_hook::set_once();
if let Err(error) = console_log::init_with_level(log::Level::Info) {
web_sys::console::warn_1(&JsValue::from_str(&format!("mirage-engine: {error}")));
}
}
pub(crate) fn spawn(future: impl Future<Output = ()> + 'static) {
wasm_bindgen_futures::spawn_local(future);
}
pub(crate) async fn read(source: &str) -> Result<Vec<u8>, Error> {
let failed = |what: &str, error: JsValue| {
Error::msg(format!("the asset source `{source}` {what}: {error:?}"))
};
let window =
web_sys::window().ok_or_else(|| Error::msg("mirage-engine needs a browser window"))?;
let response: Response = JsFuture::from(window.fetch_with_str(source))
.await
.map_err(|error| failed("could not be fetched", error))?
.dyn_into()
.map_err(|_| Error::msg(format!("the fetch of `{source}` answered with no response")))?;
if !response.ok() {
return Err(Error::msg(format!(
"the asset source `{source}` answered with status {}",
response.status()
)));
}
let buffer = JsFuture::from(
response
.array_buffer()
.map_err(|error| failed("could not be read", error))?,
)
.await
.map_err(|error| failed("could not be read", error))?;
Ok(Uint8Array::new(&buffer).to_vec())
}
pub(crate) fn window_attributes(config: &Config) -> Result<WindowAttributes, Error> {
require_webgpu()?;
let document = document()?;
let canvas = match config.canvas_id() {
Some(id) => page_canvas(&document, id)?,
None => viewport_canvas(&document)?,
};
Ok(Window::default_attributes()
.with_title(config.title())
.with_canvas(Some(canvas)))
}
pub(crate) fn run_app(event_loop: EventLoop<()>, app: impl ApplicationHandler + 'static) {
event_loop.spawn_app(app);
}
pub(crate) fn close(window: &Window, config: &Config) {
if config.canvas_id().is_some() {
return;
}
if let Some(canvas) = window.canvas() {
canvas.remove();
}
}
pub(crate) struct PointerHold {
set: bool,
}
impl PointerHold {
pub(crate) fn released() -> Self {
Self { set: false }
}
pub(crate) fn set(&mut self, window: &Window, held: bool) -> bool {
if core::mem::replace(&mut self.set, held) != held && !held {
let _ = window.set_cursor_grab(CursorGrabMode::None);
}
held && holds(window)
}
pub(crate) fn see_gesture(&mut self, window: &Window) {
if self.set && !holds(window) {
let _ = window.set_cursor_grab(CursorGrabMode::Locked);
}
}
pub(crate) fn see_focus_change(&mut self) {
self.set = false;
}
}
fn holds(window: &Window) -> bool {
let Some(canvas) = window.canvas() else {
return false;
};
document()
.ok()
.and_then(|document| document.pointer_lock_element())
.is_some_and(|held| held == *canvas.unchecked_ref::<Element>())
}
pub(crate) async fn adapter_facts() -> Option<String> {
let navigator = web_sys::window()?.navigator();
let gpu = js_sys::Reflect::get(navigator.as_ref(), &JsValue::from_str("gpu")).ok()?;
let request = js_sys::Reflect::get(&gpu, &JsValue::from_str("requestAdapter"))
.ok()?
.dyn_into::<js_sys::Function>()
.ok()?;
let options = js_sys::Object::new();
js_sys::Reflect::set(
&options,
&JsValue::from_str("powerPreference"),
&JsValue::from_str("high-performance"),
)
.ok()?;
let requested = request
.call1(&gpu, &options)
.ok()?
.dyn_into::<js_sys::Promise>()
.ok()?;
let adapter = JsFuture::from(requested).await.ok()?;
let info = js_sys::Reflect::get(&adapter, &JsValue::from_str("info")).ok()?;
let stated = |key: &str| {
js_sys::Reflect::get(&info, &JsValue::from_str(key))
.ok()
.and_then(|value| value.as_string())
.filter(|text| !text.is_empty())
};
let facts: Vec<String> = ["vendor", "architecture"]
.into_iter()
.filter_map(stated)
.collect();
(!facts.is_empty()).then(|| facts.join(" "))
}
pub(crate) fn report_fatal(error: &Error) {
let message = format!("mirage-engine: {error}");
web_sys::console::error_1(&JsValue::from_str(&message));
let _ = overlay_message(&message);
}
pub(crate) fn double_click_interval() -> Option<Duration> {
None
}
pub(crate) fn store_read(name: &str) -> Option<String> {
match storage()?.get_item(&key(name)) {
Ok(kept) => kept,
Err(error) => {
log::debug!("mirage-engine read nothing from the page: {error:?}");
None
}
}
}
pub(crate) fn store_write(name: &str, text: &str) {
let Some(storage) = storage() else {
return;
};
if let Err(error) = storage.set_item(&key(name), text) {
log::debug!("mirage-engine kept nothing in the page: {error:?}");
}
}
fn storage() -> Option<web_sys::Storage> {
web_sys::window()?.local_storage().ok()?
}
fn key(name: &str) -> String {
format!("mirage-engine.{name}")
}
fn overlay_message(message: &str) -> Option<()> {
let document = document().ok()?;
let overlay = document.create_element("pre").ok()?;
overlay.set_text_content(Some(message));
overlay.set_attribute("style", OVERLAY_STYLE).ok()?;
document.body()?.append_child(&overlay).ok()?;
Some(())
}
fn require_webgpu() -> Result<(), Error> {
web_sys::window()
.is_some_and(|window| {
js_sys::Reflect::get(window.navigator().as_ref(), &JsValue::from_str("gpu"))
.is_ok_and(|gpu| !gpu.is_undefined())
})
.then_some(())
.ok_or_else(|| Error::msg("the browser has no WebGPU (navigator.gpu is missing)"))
}
fn document() -> Result<Document, Error> {
web_sys::window()
.and_then(|window| window.document())
.ok_or_else(|| Error::msg("mirage-engine needs a browser window with a document"))
}
fn viewport_canvas(document: &Document) -> Result<HtmlCanvasElement, Error> {
let canvas: HtmlCanvasElement = document
.create_element("canvas")
.map_err(|error| Error::msg(format!("the document refused a canvas: {error:?}")))?
.dyn_into()
.map_err(|_| Error::msg("the document made something other than a canvas"))?;
canvas
.set_attribute("style", CANVAS_STYLE)
.map_err(|error| Error::msg(format!("the canvas refused its style: {error:?}")))?;
let body = document
.body()
.ok_or_else(|| Error::msg("the page has no body to attach the canvas to"))?;
body.append_child(&canvas)
.map_err(|error| Error::msg(format!("the page refused the canvas: {error:?}")))?;
Ok(canvas)
}
fn page_canvas(document: &Document, id: &str) -> Result<HtmlCanvasElement, Error> {
document
.get_element_by_id(id)
.ok_or_else(|| Error::msg(format!("the page has no element with id `{id}`")))?
.dyn_into()
.map_err(|_| Error::msg(format!("the element with id `{id}` is not a canvas")))
}