use crate::{
launcher::AppSettings,
wgpu_surface::{current_surface_texture, SurfaceFrame},
};
use cranpose_app_shell::{default_root_key, AppShell, PlatformFrameDriver};
use cranpose_platform_web::WebPlatform;
use cranpose_render_wgpu::WgpuRenderer;
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::Arc;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{HtmlCanvasElement, MouseEvent, PointerEvent, WheelEvent};
const WEB_WHEEL_LINE_DELTA_PIXELS: f32 = 40.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum WebBackendPreference {
Auto,
WebGpu,
Gl,
}
#[derive(Debug)]
struct BrowserDisplayHandle;
impl wgpu::rwh::HasDisplayHandle for BrowserDisplayHandle {
fn display_handle(&self) -> Result<wgpu::rwh::DisplayHandle<'_>, wgpu::rwh::HandleError> {
Ok(wgpu::rwh::DisplayHandle::web())
}
}
#[derive(Default)]
struct WebFrameTimer {
generation: Cell<u64>,
pending: Cell<bool>,
}
struct WebPlatformFrameDriver<'a> {
frame_timer: &'a Rc<WebFrameTimer>,
frame_pending: &'a Rc<Cell<bool>>,
render_loop: &'a Rc<RefCell<Option<Closure<dyn FnMut()>>>>,
}
impl PlatformFrameDriver for WebPlatformFrameDriver<'_> {
fn request_frame(&self) {
request_web_frame(self.frame_pending, self.render_loop, Some(self.frame_timer));
}
fn request_wake_at(&self, deadline: web_time::Instant) {
request_web_frame_at_deadline(
self.frame_timer,
deadline,
self.frame_pending,
self.render_loop,
);
}
fn clear_wake(&self) {
clear_web_frame_wake(self.frame_timer);
}
}
pub async fn run(
canvas_id: &str,
settings: AppSettings,
content: impl FnMut() + 'static,
) -> Result<(), JsValue> {
console_error_panic_hook::set_once();
let window = web_sys::window().ok_or("no global window exists")?;
let document = window
.document()
.ok_or("should have a document on window")?;
let canvas = document
.get_element_by_id(canvas_id)
.ok_or_else(|| format!("canvas with id '{}' not found", canvas_id))?
.dyn_into::<HtmlCanvasElement>()?;
let scale_factor = window.device_pixel_ratio();
let width = settings.initial_width;
let height = settings.initial_height;
canvas.set_width((width as f64 * scale_factor) as u32);
canvas.set_height((height as f64 * scale_factor) as u32);
if let Some(html_element) = canvas.dyn_ref::<web_sys::HtmlElement>() {
let style = html_element.style();
style.set_property("width", &format!("{}px", width))?;
style.set_property("height", &format!("{}px", height))?;
style.set_property("touch-action", "none")?;
}
let backend_preference = requested_web_backend(&window);
let mut instance_desc =
wgpu::InstanceDescriptor::new_with_display_handle(Box::new(BrowserDisplayHandle));
instance_desc.backends = instance_backends(backend_preference);
let instance = match backend_preference {
WebBackendPreference::Auto => {
wgpu::util::new_instance_with_webgpu_detection(instance_desc).await
}
WebBackendPreference::WebGpu | WebBackendPreference::Gl => {
wgpu::Instance::new(instance_desc)
}
};
let surface = instance
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
.map_err(|e| format!("failed to create surface: {:?}", e))?;
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.map_err(|e| format!("failed to find suitable adapter: {:?}", e))?;
let adapter_info = adapter.get_info();
let adapter_limits = adapter.limits();
let required_limits =
required_limits_for_web_backend(adapter_info.backend, adapter_limits.clone());
log::info!(
"Web backend preference={:?}, selected backend={:?}, max_texture_dimension_2d={}",
backend_preference,
adapter_info.backend,
adapter_limits.max_texture_dimension_2d
);
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("Main Device"),
required_features: wgpu::Features::empty(),
required_limits,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
})
.await
.map_err(|e| format!("failed to create device: {:?}", e))?;
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.copied()
.find(|f| f.is_srgb())
.or_else(|| surface_caps.formats.first().copied())
.ok_or_else(|| JsValue::from_str("web surface reports no supported formats"))?;
let alpha_mode = surface_caps
.alpha_modes
.first()
.copied()
.ok_or_else(|| JsValue::from_str("web surface reports no supported alpha modes"))?;
let present_mode = crate::present_mode::select_present_mode(&surface_caps);
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: (width as f64 * scale_factor) as u32,
height: (height as f64 * scale_factor) as u32,
present_mode,
alpha_mode,
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &surface_config);
let mut surface_config = surface_config;
let (actual_width, actual_height, effective_scale) =
if adapter_info.backend == wgpu::Backend::BrowserWebGpu {
(surface_config.width, surface_config.height, scale_factor)
} else {
let probe = match current_surface_texture(&surface, "web probe") {
SurfaceFrame::Ready(probe) => probe,
SurfaceFrame::Reconfigure => {
return Err(
"failed to probe surface texture: surface needs reconfiguration".into(),
);
}
SurfaceFrame::Skip => {
return Err("failed to probe surface texture: surface unavailable".into());
}
};
let actual_width = probe.texture.width();
let actual_height = probe.texture.height();
probe.present();
let effective_scale =
if actual_width < surface_config.width || actual_height < surface_config.height {
let fit_x = actual_width as f64 / width as f64;
let fit_y = actual_height as f64 / height as f64;
let s = fit_x.min(fit_y);
surface_config.width = actual_width;
surface_config.height = actual_height;
s
} else {
scale_factor
};
(actual_width, actual_height, effective_scale)
};
let fonts: &[&[u8]] = settings.fonts.unwrap_or(&[]);
log::info!("Web renderer startup: {} font(s)", fonts.len());
let mut renderer = WgpuRenderer::new(fonts);
renderer.init_gpu(
Arc::new(device),
Arc::new(queue),
surface_format,
adapter_info.backend,
);
renderer.set_root_scale(effective_scale as f32);
let app = Rc::new(RefCell::new(AppShell::new_with_size_and_density(
renderer,
default_root_key(),
content,
(actual_width, actual_height),
(width as f32, height as f32),
effective_scale as f32,
)));
let platform = Rc::new(RefCell::new(WebPlatform::default()));
platform.borrow_mut().set_scale_factor(effective_scale);
let surface = Rc::new(surface);
let surface_config = Rc::new(RefCell::new(surface_config));
let render_loop: Rc<RefCell<Option<Closure<dyn FnMut()>>>> = Rc::new(RefCell::new(None));
let frame_pending = Rc::new(Cell::new(false));
let frame_timer = Rc::new(WebFrameTimer::default());
let request_frame: Rc<dyn Fn()> = {
let frame_pending = frame_pending.clone();
let render_loop = render_loop.clone();
let frame_timer = frame_timer.clone();
Rc::new(move || request_web_frame(&frame_pending, &render_loop, Some(&frame_timer)))
};
app.borrow_mut().set_frame_waker({
let request_frame = request_frame.clone();
move || request_frame()
});
{
let app = app.clone();
let platform = platform.clone();
let request_frame = request_frame.clone();
let closure = Closure::wrap(Box::new(move |event: MouseEvent| {
let x = event.offset_x() as f64;
let y = event.offset_y() as f64;
let logical = platform.borrow().pointer_position(x, y);
if let Ok(mut app_mut) = app.try_borrow_mut() {
app_mut.set_cursor(logical.x, logical.y);
request_frame();
}
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback("mousemove", closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let app = app.clone();
let platform = platform.clone();
let request_frame = request_frame.clone();
let closure = Closure::wrap(Box::new(move |event: MouseEvent| {
let x = event.offset_x() as f64;
let y = event.offset_y() as f64;
let logical = platform.borrow().pointer_position(x, y);
if let Ok(mut app_mut) = app.try_borrow_mut() {
app_mut.set_cursor(logical.x, logical.y);
app_mut.pointer_pressed();
request_frame();
}
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let app = app.clone();
let platform = platform.clone();
let request_frame = request_frame.clone();
let closure = Closure::wrap(Box::new(move |event: MouseEvent| {
let x = event.offset_x() as f64;
let y = event.offset_y() as f64;
let logical = platform.borrow().pointer_position(x, y);
if let Ok(mut app_mut) = app.try_borrow_mut() {
app_mut.set_cursor(logical.x, logical.y);
app_mut.pointer_released();
request_frame();
}
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback("mouseup", closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let app = app.clone();
let platform = platform.clone();
let wheel_canvas = canvas.clone();
let request_frame = request_frame.clone();
let closure = Closure::wrap(Box::new(move |event: WheelEvent| {
let x = event.offset_x() as f64;
let y = event.offset_y() as f64;
let logical = platform.borrow().pointer_position(x, y);
let mut delta_x = event.delta_x() as f32;
let mut delta_y = event.delta_y() as f32;
match event.delta_mode() {
WheelEvent::DOM_DELTA_PIXEL => {}
WheelEvent::DOM_DELTA_LINE => {
delta_x *= WEB_WHEEL_LINE_DELTA_PIXELS;
delta_y *= WEB_WHEEL_LINE_DELTA_PIXELS;
}
WheelEvent::DOM_DELTA_PAGE => {
let page_width = wheel_canvas.client_width().max(1) as f32;
let page_height = wheel_canvas.client_height().max(1) as f32;
delta_x *= page_width;
delta_y *= page_height;
}
_ => {}
}
if event.alt_key() {
if delta_x.abs() <= f32::EPSILON {
delta_x = delta_y;
}
delta_y = 0.0;
}
if let Ok(mut app_mut) = app.try_borrow_mut() {
app_mut.set_cursor(logical.x, logical.y);
if app_mut.pointer_scrolled(delta_x, delta_y) {
event.prevent_default();
}
request_frame();
}
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback("wheel", closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let app = app.clone();
let platform = platform.clone();
let request_frame = request_frame.clone();
let closure = Closure::wrap(Box::new(move |event: PointerEvent| {
event.prevent_default();
let x = event.offset_x() as f64;
let y = event.offset_y() as f64;
let logical = platform.borrow().pointer_position(x, y);
if let Ok(mut app_mut) = app.try_borrow_mut() {
app_mut.set_cursor(logical.x, logical.y);
request_frame();
}
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback("pointermove", closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let app = app.clone();
let platform = platform.clone();
let request_frame = request_frame.clone();
let closure = Closure::wrap(Box::new(move |event: PointerEvent| {
event.prevent_default();
let x = event.offset_x() as f64;
let y = event.offset_y() as f64;
let logical = platform.borrow().pointer_position(x, y);
if let Ok(mut app_mut) = app.try_borrow_mut() {
app_mut.set_cursor(logical.x, logical.y);
app_mut.pointer_pressed();
request_frame();
}
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback("pointerdown", closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let app = app.clone();
let platform = platform.clone();
let request_frame = request_frame.clone();
let closure = Closure::wrap(Box::new(move |event: PointerEvent| {
event.prevent_default();
let x = event.offset_x() as f64;
let y = event.offset_y() as f64;
let logical = platform.borrow().pointer_position(x, y);
if let Ok(mut app_mut) = app.try_borrow_mut() {
app_mut.set_cursor(logical.x, logical.y);
app_mut.pointer_released();
request_frame();
}
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback("pointerup", closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let app = app.clone();
let request_frame = request_frame.clone();
let closure = Closure::wrap(Box::new(move |event: PointerEvent| {
event.prevent_default();
if let Ok(mut app_mut) = app.try_borrow_mut() {
app_mut.cancel_gesture();
request_frame();
}
}) as Box<dyn FnMut(_)>);
canvas
.add_event_listener_with_callback("pointercancel", closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let app = app.clone();
let request_frame = request_frame.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {
use cranpose_app_shell::{KeyCode, KeyEvent, KeyEventType, Modifiers};
let key_code = match event.code().as_str() {
"KeyA" => KeyCode::A,
"KeyB" => KeyCode::B,
"KeyC" => KeyCode::C,
"KeyD" => KeyCode::D,
"KeyE" => KeyCode::E,
"KeyF" => KeyCode::F,
"KeyG" => KeyCode::G,
"KeyH" => KeyCode::H,
"KeyI" => KeyCode::I,
"KeyJ" => KeyCode::J,
"KeyK" => KeyCode::K,
"KeyL" => KeyCode::L,
"KeyM" => KeyCode::M,
"KeyN" => KeyCode::N,
"KeyO" => KeyCode::O,
"KeyP" => KeyCode::P,
"KeyQ" => KeyCode::Q,
"KeyR" => KeyCode::R,
"KeyS" => KeyCode::S,
"KeyT" => KeyCode::T,
"KeyU" => KeyCode::U,
"KeyV" => KeyCode::V,
"KeyW" => KeyCode::W,
"KeyX" => KeyCode::X,
"KeyY" => KeyCode::Y,
"KeyZ" => KeyCode::Z,
"Digit0" => KeyCode::Digit0,
"Digit1" => KeyCode::Digit1,
"Digit2" => KeyCode::Digit2,
"Digit3" => KeyCode::Digit3,
"Digit4" => KeyCode::Digit4,
"Digit5" => KeyCode::Digit5,
"Digit6" => KeyCode::Digit6,
"Digit7" => KeyCode::Digit7,
"Digit8" => KeyCode::Digit8,
"Digit9" => KeyCode::Digit9,
"ArrowUp" => KeyCode::ArrowUp,
"ArrowDown" => KeyCode::ArrowDown,
"ArrowLeft" => KeyCode::ArrowLeft,
"ArrowRight" => KeyCode::ArrowRight,
"Home" => KeyCode::Home,
"End" => KeyCode::End,
"PageUp" => KeyCode::PageUp,
"PageDown" => KeyCode::PageDown,
"Backspace" => KeyCode::Backspace,
"Delete" => KeyCode::Delete,
"Enter" | "NumpadEnter" => KeyCode::Enter,
"Tab" => KeyCode::Tab,
"Space" => KeyCode::Space,
"Escape" => KeyCode::Escape,
"Minus" => KeyCode::Minus,
"Equal" => KeyCode::Equal,
"BracketLeft" => KeyCode::BracketLeft,
"BracketRight" => KeyCode::BracketRight,
"Backslash" => KeyCode::Backslash,
"Semicolon" => KeyCode::Semicolon,
"Quote" => KeyCode::Quote,
"Comma" => KeyCode::Comma,
"Period" => KeyCode::Period,
"Slash" => KeyCode::Slash,
"Backquote" => KeyCode::Backquote,
_ => KeyCode::Unknown,
};
let modifiers = Modifiers {
shift: event.shift_key(),
ctrl: event.ctrl_key(),
alt: event.alt_key(),
meta: event.meta_key(),
};
let text = {
let key = event.key();
if key.len() == 1 {
key
} else {
String::new()
}
};
let key_event = KeyEvent {
key_code,
text,
modifiers,
event_type: KeyEventType::KeyDown,
};
if let Ok(mut app_mut) = app.try_borrow_mut() {
if app_mut.on_key_event(&key_event) {
event.prevent_default();
}
request_frame();
}
}) as Box<dyn FnMut(_)>);
document.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let app = app.clone();
let request_frame = request_frame.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {
use cranpose_app_shell::{KeyCode, KeyEvent, KeyEventType, Modifiers};
let key_code = match event.code().as_str() {
"KeyA" => KeyCode::A,
"KeyB" => KeyCode::B,
"KeyC" => KeyCode::C,
"KeyD" => KeyCode::D,
"KeyE" => KeyCode::E,
"KeyF" => KeyCode::F,
"KeyG" => KeyCode::G,
"KeyH" => KeyCode::H,
"KeyI" => KeyCode::I,
"KeyJ" => KeyCode::J,
"KeyK" => KeyCode::K,
"KeyL" => KeyCode::L,
"KeyM" => KeyCode::M,
"KeyN" => KeyCode::N,
"KeyO" => KeyCode::O,
"KeyP" => KeyCode::P,
"KeyQ" => KeyCode::Q,
"KeyR" => KeyCode::R,
"KeyS" => KeyCode::S,
"KeyT" => KeyCode::T,
"KeyU" => KeyCode::U,
"KeyV" => KeyCode::V,
"KeyW" => KeyCode::W,
"KeyX" => KeyCode::X,
"KeyY" => KeyCode::Y,
"KeyZ" => KeyCode::Z,
"Digit0" => KeyCode::Digit0,
"Digit1" => KeyCode::Digit1,
"Digit2" => KeyCode::Digit2,
"Digit3" => KeyCode::Digit3,
"Digit4" => KeyCode::Digit4,
"Digit5" => KeyCode::Digit5,
"Digit6" => KeyCode::Digit6,
"Digit7" => KeyCode::Digit7,
"Digit8" => KeyCode::Digit8,
"Digit9" => KeyCode::Digit9,
"ArrowUp" => KeyCode::ArrowUp,
"ArrowDown" => KeyCode::ArrowDown,
"ArrowLeft" => KeyCode::ArrowLeft,
"ArrowRight" => KeyCode::ArrowRight,
"Backspace" => KeyCode::Backspace,
"Delete" => KeyCode::Delete,
"Enter" | "NumpadEnter" => KeyCode::Enter,
"Tab" => KeyCode::Tab,
"Space" => KeyCode::Space,
_ => KeyCode::Unknown,
};
let modifiers = Modifiers {
shift: event.shift_key(),
ctrl: event.ctrl_key(),
alt: event.alt_key(),
meta: event.meta_key(),
};
let key_event = KeyEvent {
key_code,
text: String::new(), modifiers,
event_type: KeyEventType::KeyUp,
};
if let Ok(mut app_mut) = app.try_borrow_mut() {
app_mut.on_key_event(&key_event);
request_frame();
}
}) as Box<dyn FnMut(_)>);
document.add_event_listener_with_callback("keyup", closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let app = app.clone();
let request_frame = request_frame.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::ClipboardEvent| {
if let Some(data) = event.clipboard_data() {
if let Ok(text) = data.get_data("text/plain") {
if !text.is_empty() {
if let Ok(mut app_mut) = app.try_borrow_mut() {
if app_mut.on_paste(&text) {
event.prevent_default();
}
request_frame();
}
}
}
}
}) as Box<dyn FnMut(_)>);
document.add_event_listener_with_callback("paste", closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let app = app.clone();
let request_frame = request_frame.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::ClipboardEvent| {
if let Ok(mut app_mut) = app.try_borrow_mut() {
if let Some(text) = app_mut.on_copy() {
if let Some(data) = event.clipboard_data() {
let _ = data.set_data("text/plain", &text);
event.prevent_default();
}
request_frame();
}
}
}) as Box<dyn FnMut(_)>);
document.add_event_listener_with_callback("copy", closure.as_ref().unchecked_ref())?;
closure.forget();
}
{
let app = app.clone();
let request_frame = request_frame.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::ClipboardEvent| {
if let Ok(mut app_mut) = app.try_borrow_mut() {
if let Some(text) = app_mut.on_cut() {
if let Some(data) = event.clipboard_data() {
let _ = data.set_data("text/plain", &text);
event.prevent_default();
}
request_frame();
}
}
}) as Box<dyn FnMut(_)>);
document.add_event_listener_with_callback("cut", closure.as_ref().unchecked_ref())?;
closure.forget();
}
let frame_pending_for_loop = frame_pending.clone();
let frame_timer_for_loop = frame_timer.clone();
let render_loop_for_deadline = render_loop.clone();
*render_loop.borrow_mut() = Some(Closure::wrap(Box::new(move || {
frame_pending_for_loop.set(false);
app.borrow_mut().update();
let config = surface_config.borrow();
match current_surface_texture(&surface, "web") {
SurfaceFrame::Ready(output) => {
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let render_width = output.texture.width();
let render_height = output.texture.height();
{
let mut app_mut = app.borrow_mut();
if let Err(err) = app_mut
.renderer()
.render(&view, render_width, render_height)
{
log::error!("render failed: {:?}", err);
}
}
output.present();
}
SurfaceFrame::Reconfigure => {
let mut app_mut = app.borrow_mut();
if let Some(device) = app_mut.renderer().try_device() {
surface.configure(device, &*config);
} else {
log::error!("web surface reconfigure skipped: GPU renderer is not initialized");
}
}
SurfaceFrame::Skip => {}
}
let frame_driver = WebPlatformFrameDriver {
frame_timer: &frame_timer_for_loop,
frame_pending: &frame_pending_for_loop,
render_loop: &render_loop_for_deadline,
};
app.borrow().schedule_platform_frame(&frame_driver);
}) as Box<dyn FnMut()>));
request_frame();
Ok(())
}
fn request_animation_frame(f: &Closure<dyn FnMut()>) -> bool {
let Some(window) = web_sys::window() else {
log::error!("requestAnimationFrame unavailable: browser window is not available");
return false;
};
match window.request_animation_frame(f.as_ref().unchecked_ref()) {
Ok(_) => true,
Err(error) => {
log::error!("requestAnimationFrame registration failed: {error:?}");
false
}
}
}
fn request_web_frame(
frame_pending: &Cell<bool>,
render_loop: &Rc<RefCell<Option<Closure<dyn FnMut()>>>>,
timer: Option<&WebFrameTimer>,
) {
if let Some(timer) = timer {
timer.pending.set(false);
timer
.generation
.set(timer.generation.get().saturating_add(1));
}
if frame_pending.replace(true) {
return;
}
let render_loop = render_loop.borrow();
let Some(render_loop) = render_loop.as_ref() else {
frame_pending.set(false);
return;
};
if !request_animation_frame(render_loop) {
frame_pending.set(false);
}
}
fn request_web_frame_at_deadline(
timer: &Rc<WebFrameTimer>,
deadline: web_time::Instant,
frame_pending: &Rc<Cell<bool>>,
render_loop: &Rc<RefCell<Option<Closure<dyn FnMut()>>>>,
) {
if frame_pending.get() || timer.pending.get() {
return;
}
timer.pending.set(true);
let generation = timer.generation.get();
let delay = deadline
.checked_duration_since(web_time::Instant::now())
.unwrap_or_default();
let delay_ms = delay.as_millis().min(i32::MAX as u128) as i32;
let timer_for_timeout = timer.clone();
let frame_pending_for_timeout = frame_pending.clone();
let render_loop_for_timeout = render_loop.clone();
let callback = Closure::once_into_js(move || {
if timer_for_timeout.generation.get() != generation {
return;
}
timer_for_timeout.pending.set(false);
request_web_frame(
&frame_pending_for_timeout,
&render_loop_for_timeout,
Some(&timer_for_timeout),
);
});
let Some(window) = web_sys::window() else {
log::error!("setTimeout unavailable: browser window is not available");
timer.pending.set(false);
request_web_frame(frame_pending, render_loop, Some(timer));
return;
};
if let Err(error) = window
.set_timeout_with_callback_and_timeout_and_arguments_0(callback.unchecked_ref(), delay_ms)
{
log::error!("setTimeout registration failed for frame deadline: {error:?}");
timer.pending.set(false);
request_web_frame(frame_pending, render_loop, Some(timer));
}
}
fn clear_web_frame_wake(timer: &WebFrameTimer) {
timer.pending.set(false);
timer
.generation
.set(timer.generation.get().saturating_add(1));
}
fn requested_web_backend(window: &web_sys::Window) -> WebBackendPreference {
let query = window.location().search().unwrap_or_default();
for pair in query.trim_start_matches('?').split('&') {
let Some((key, value)) = pair.split_once('=') else {
continue;
};
if key != "backend" {
continue;
}
return match value {
"webgpu" => WebBackendPreference::WebGpu,
"gl" => WebBackendPreference::Gl,
_ => WebBackendPreference::Auto,
};
}
WebBackendPreference::Gl
}
fn instance_backends(preference: WebBackendPreference) -> wgpu::Backends {
match preference {
WebBackendPreference::Auto => wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL,
WebBackendPreference::WebGpu => wgpu::Backends::BROWSER_WEBGPU,
WebBackendPreference::Gl => wgpu::Backends::GL,
}
}
fn required_limits_for_web_backend(
backend: wgpu::Backend,
adapter_limits: wgpu::Limits,
) -> wgpu::Limits {
match backend {
wgpu::Backend::BrowserWebGpu => wgpu::Limits::default().using_resolution(adapter_limits),
_ => wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter_limits),
}
}