use std::cell::RefCell;
use std::rc::Rc;
use anyhow::{Result, anyhow};
use game_toolkit_audio::DetachHandle;
use wasm_bindgen::JsCast;
use wasm_bindgen::closure::Closure;
use winit::platform::web::{WindowAttributesExtWebSys, WindowExtWebSys};
use winit::window::{Window, WindowAttributes};
pub(crate) fn canvas_attributes(
attrs: WindowAttributes,
canvas_id: Option<&str>,
) -> Result<WindowAttributes> {
let Some(id) = canvas_id else {
return Ok(attrs.with_append(true));
};
let canvas = web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.get_element_by_id(id))
.ok_or_else(|| anyhow!("no element with id {id:?}"))?
.dyn_into::<web_sys::HtmlCanvasElement>()
.map_err(|_| anyhow!("element {id:?} is not a <canvas>"))?;
Ok(attrs.with_canvas(Some(canvas)))
}
const AUTOPLAY_PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(1500);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Autoplay {
Allowed,
Denied,
Pending,
}
pub(crate) struct AutoplayProbe {
context: web_sys::AudioContext,
since: web_time::Instant,
}
impl AutoplayProbe {
pub(crate) fn start() -> Option<Self> {
let window = web_sys::window()?;
let navigator = window.navigator();
let activation = js_sys::Reflect::get(&navigator, &"userActivation".into())
.ok()
.and_then(|v| v.dyn_into::<web_sys::UserActivation>().ok());
if activation.is_some_and(|a| !a.has_been_active()) {
return None;
}
let context = web_sys::AudioContext::new().ok()?;
Some(Self {
context,
since: web_time::Instant::now(),
})
}
pub(crate) fn poll(&self) -> Autoplay {
if self.context.state() == web_sys::AudioContextState::Running {
Autoplay::Allowed
} else if self.since.elapsed() > AUTOPLAY_PROBE_TIMEOUT {
Autoplay::Denied
} else {
Autoplay::Pending
}
}
}
impl Drop for AutoplayProbe {
fn drop(&mut self) {
let _ = self.context.close();
}
}
pub(crate) fn install_hooks() {
std::panic::set_hook(Box::new(|info| {
web_sys::console::error_1(&info.to_string().into());
}));
if log::set_logger(&CONSOLE_LOGGER).is_ok() {
log::set_max_level(log::LevelFilter::Info);
}
}
static CONSOLE_LOGGER: ConsoleLogger = ConsoleLogger;
struct ConsoleLogger;
impl log::Log for ConsoleLogger {
fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
metadata.level() <= log::max_level()
}
fn log(&self, record: &log::Record<'_>) {
if !self.enabled(record.metadata()) {
return;
}
let target = record.target();
if record.level() > log::Level::Warn
&& (target.starts_with("wgpu") || target.starts_with("naga"))
{
return;
}
let msg = format!("[{}] {}", target, record.args()).into();
match record.level() {
log::Level::Error => web_sys::console::error_1(&msg),
log::Level::Warn => web_sys::console::warn_1(&msg),
log::Level::Info => web_sys::console::info_1(&msg),
log::Level::Debug | log::Level::Trace => web_sys::console::debug_1(&msg),
}
}
fn flush(&self) {}
}
pub(crate) struct CanvasWatch {
observer: web_sys::MutationObserver,
_callback: Closure<dyn FnMut()>,
state: Rc<RefCell<WatchState>>,
}
struct WatchState {
attached: bool,
changes: Vec<bool>,
}
impl CanvasWatch {
pub(crate) fn new(window: &Window, audio: Option<DetachHandle>) -> Result<Self> {
let canvas = window
.canvas()
.ok_or_else(|| anyhow!("window has no canvas"))?;
let document = web_sys::window()
.and_then(|w| w.document())
.ok_or_else(|| anyhow!("no document"))?;
let state = Rc::new(RefCell::new(WatchState {
attached: canvas.is_connected(),
changes: Vec::new(),
}));
if let Some(audio) = &audio {
audio.set_detached(!state.borrow().attached);
}
let callback = Closure::<dyn FnMut()>::new({
let state = state.clone();
move || {
let attached = canvas.is_connected();
let mut state = state.borrow_mut();
if state.attached == attached {
return;
}
state.attached = attached;
state.changes.push(attached);
if let Some(audio) = &audio {
audio.set_detached(!attached);
}
}
});
let observer = web_sys::MutationObserver::new(callback.as_ref().unchecked_ref())
.map_err(|e| anyhow!("MutationObserver: {e:?}"))?;
let init = web_sys::MutationObserverInit::new();
init.set_child_list(true);
init.set_subtree(true);
observer
.observe_with_options(&document, &init)
.map_err(|e| anyhow!("MutationObserver.observe: {e:?}"))?;
Ok(Self {
observer,
_callback: callback,
state,
})
}
pub(crate) fn take_changes(&self) -> Vec<bool> {
std::mem::take(&mut self.state.borrow_mut().changes)
}
}
impl Drop for CanvasWatch {
fn drop(&mut self) {
self.observer.disconnect();
}
}