repame_shell/gesture.rs
1//! First-gesture hook for host policies that gate audio behind one.
2//!
3//! Browsers refuse to start an `AudioContext` outside a user gesture, so an
4//! engine opened at boot comes up suspended. Games call
5//! [`on_first_user_gesture`] with [`repame_audio::Audio::unlock`] and get a
6//! resumed device; there is no DOM code in the game.
7
8/// Set once a real gesture has been seen, so the hook fires exactly once even
9/// though the listeners stay attached.
10static SEEN: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
11
12/// Run `f` on the first user gesture on this page.
13///
14/// The listener is attached to `window` for `pointerdown`, `touchstart` and
15/// `keydown`, so mouse, touch and keyboard all arm the device. It stays
16/// attached for the page's life; [`SEEN`] makes every later call a no-op, which
17/// keeps the closure alive cheaply instead of tearing listeners down mid-gesture.
18///
19/// Off wasm this does nothing, so callers need no cfg.
20pub fn on_first_user_gesture(f: impl Fn() + 'static) {
21 #[cfg(target_arch = "wasm32")]
22 install(f);
23 #[cfg(not(target_arch = "wasm32"))]
24 let _ = f;
25}
26
27#[cfg(target_arch = "wasm32")]
28fn install(f: impl Fn() + 'static) {
29 use std::sync::atomic::Ordering;
30
31 use wasm_bindgen::closure::Closure;
32 use wasm_bindgen::JsCast;
33
34 let global = js_sys::global();
35 let Ok(add) = js_sys::Reflect::get(&global, &wasm_bindgen::JsValue::from_str(
36 "addEventListener",
37 )) else {
38 return;
39 };
40 let Some(add) = add.dyn_ref::<js_sys::Function>() else {
41 return;
42 };
43 // Leaked into the page on purpose: the closure must outlive this call for
44 // the listener to stay callable, and one 24-byte handler for the life of
45 // the page is cheaper than the bookkeeping to remove it.
46 let handler = Closure::wrap(Box::new(move || {
47 if !SEEN.swap(true, Ordering::SeqCst) {
48 f();
49 }
50 }) as Box<dyn FnMut()>)
51 .into_js_value();
52 for event in ["pointerdown", "touchstart", "keydown"] {
53 let _ = add.call2(
54 &global,
55 &wasm_bindgen::JsValue::from_str(event),
56 &handler,
57 );
58 }
59}
60
61/// Whether a user gesture has been seen. Games that would rather poll than
62/// register a callback can drive [`unlock`](repame_audio::Audio::unlock)
63/// from their frame loop; it is a cheap atomic load.
64pub fn user_gesture_seen() -> bool {
65 SEEN.load(std::sync::atomic::Ordering::SeqCst)
66}