//! Craft-fonts faces served beside the wasm (`fonts//…`, copied by //! `cargo xtask web-fonts`) instead of embedded: [`load_startup_fonts`] fetches the `startup` //! ones before the app starts, [`spawn_background_fonts`] the rest once it runs. The browser checks //! each file against its SHA-256 (Subresource Integrity). A face that fails is logged and skipped: //! the app runs without it. With `WEB_FONTS` empty (built without craft-fonts) both do nothing. use std::cell::Cell; use std::rc::Rc; use vectorcraft_text::{FontDb, WEB_FONTS, WebFont}; use wasm_bindgen::JsCast as _; use wasm_bindgen::closure::Closure; use wasm_bindgen_futures::JsFuture; /// Per startup face: the page shows its loading text meanwhile. const STARTUP_TIMEOUT_MS: i32 = 20_000; /// Per background face. const BACKGROUND_TIMEOUT_MS: i32 = 120_000; /// Background fetches at once, leaving the connection to the app's own requests. const BACKGROUND_PARALLEL: usize = 4; thread_local! { /// The background fetch runs once per page, though a graphics restart creates the app again. static BACKGROUND_STARTED: Cell = const { Cell::new(false) }; } /// Fetch and register the `startup` faces (one after the other). Returns how many loaded. pub async fn load_startup_fonts() -> usize { let mut loaded = 0; for f in WEB_FONTS.iter().filter(|f| f.startup) { if load(f, STARTUP_TIMEOUT_MS).await { loaded += 1; } } loaded } /// Fetch and register the `background` faces, [`BACKGROUND_PARALLEL`] at a time, asking `ctx` for /// a frame after each so the font menus and the canvas pick it up. Only the first call does /// anything. pub fn spawn_background_fonts(ctx: egui::Context) { if BACKGROUND_STARTED.with(|s| s.replace(true)) { return; } let queue: Rc> = Rc::new(WEB_FONTS.iter().filter(|f| !f.startup).collect()); let next = Rc::new(Cell::new(0usize)); for _ in 0..BACKGROUND_PARALLEL.min(queue.len()) { let (queue, next, ctx) = (queue.clone(), next.clone(), ctx.clone()); wasm_bindgen_futures::spawn_local(async move { loop { let i = next.get(); next.set(i + 1); let Some(f) = queue.get(i) else { break }; if load(f, BACKGROUND_TIMEOUT_MS).await { ctx.request_repaint(); } } }); } } /// Fetch one face and add it to the font database. False (and a console warning) on failure. async fn load(f: &WebFont, timeout_ms: i32) -> bool { let bytes = match fetch(f.url, f.integrity, timeout_ms).await { Ok(bytes) => bytes, Err(e) => { log::warn!("web font {} {}: {e}", f.family, f.style); return false; } }; if FontDb::global().add_font(bytes) == 0 { log::warn!("web font {} {}: not a font, or already loaded", f.family, f.style); return false; } true } /// GET `url` (relative to the page) with Subresource Integrity `integrity`, aborted after /// `timeout_ms`. async fn fetch(url: &str, integrity: &str, timeout_ms: i32) -> Result, String> { let window = web_sys::window().ok_or("no window")?; let abort = web_sys::AbortController::new().map_err(js)?; let init = web_sys::RequestInit::new(); init.set_method("GET"); init.set_integrity(integrity); init.set_signal(Some(&abort.signal())); let on_timeout = { let abort = abort.clone(); Closure::::new(move || abort.abort()) }; let timer = window.set_timeout_with_callback_and_timeout_and_arguments_0(on_timeout.as_ref().unchecked_ref(), timeout_ms).map_err(js)?; let result = async { let response: web_sys::Response = JsFuture::from(window.fetch_with_str_and_init(url, &init)).await.map_err(js)?.dyn_into().map_err(js)?; if !response.ok() { return Err(format!("HTTP {} for {url}", response.status())); } let buffer = JsFuture::from(response.array_buffer().map_err(js)?).await.map_err(js)?; Ok(js_sys::Uint8Array::new(&buffer).to_vec()) } .await; window.clear_timeout_with_handle(timer); // Kept alive until the timer can no longer fire. drop(on_timeout); result } fn js(e: wasm_bindgen::JsValue) -> String { format!("{e:?}") }