From f78c8a88715f06162aeff89dbc9c2ec6f4433aa0 Mon Sep 17 00:00:00 2001 From: 69520 Date: Tue, 23 Jun 2026 17:44:52 +0800 Subject: [PATCH] feat(miku-web): add branded boot screen with loading progress and error states Replace the blank loading canvas with a styled boot panel that shows progress, startup steps, and clear error messaging when the wasm runtime fails to initialize. The Rust web entrypoint now signals ready/failed state to the DOM so the loading overlay fades out on success or surfaces the underlying error on failure. Co-Authored-By: Claude Opus 4.7 --- crates/miku-web/index.html | 306 ++++++++++++++++++++++++++++++++++++- crates/miku-web/src/lib.rs | 71 ++++++++- 2 files changed, 372 insertions(+), 5 deletions(-) diff --git a/crates/miku-web/index.html b/crates/miku-web/index.html index ce01b6a..4f86c34 100644 --- a/crates/miku-web/index.html +++ b/crates/miku-web/index.html @@ -5,6 +5,20 @@ Miku +
+
+
+ +
+

Miku Web

+

Miku

+
+
+

Preparing workspace

+ +
    +
  • Web shell
  • +
  • Runtime
  • +
  • Services
  • +
+

Loading WebAssembly runtime

+
+
diff --git a/crates/miku-web/src/lib.rs b/crates/miku-web/src/lib.rs index c595a84..dfcda5c 100644 --- a/crates/miku-web/src/lib.rs +++ b/crates/miku-web/src/lib.rs @@ -24,7 +24,8 @@ impl WebHandle { #[wasm_bindgen] pub async fn start(&self, canvas: web_sys::HtmlCanvasElement) -> Result<(), JsValue> { tracing::info!("starting web app"); - self.runner + let result = self + .runner .start( canvas, eframe::WebOptions::default(), @@ -37,7 +38,14 @@ impl WebHandle { Ok(Box::new(app)) }), ) - .await + .await; + + match &result { + Ok(()) => mark_loading_ready(), + Err(error) => mark_loading_failed(error), + } + + result } #[wasm_bindgen] @@ -87,3 +95,62 @@ fn web_app() -> Result { client, ))) } + +#[cfg(target_arch = "wasm32")] +fn mark_loading_ready() { + let Some(document) = browser_document() else { + return; + }; + + if let Some(root) = document.document_element() { + add_class(&root, "miku-ready"); + let _ = root.set_attribute("data-miku-boot", "ready"); + } + + if let Some(loading) = document.get_element_by_id("miku-loading") { + let _ = loading.set_attribute("aria-busy", "false"); + } +} + +#[cfg(target_arch = "wasm32")] +fn mark_loading_failed(error: &JsValue) { + let Some(document) = browser_document() else { + return; + }; + + if let Some(root) = document.document_element() { + add_class(&root, "miku-boot-error"); + let _ = root.set_attribute("data-miku-boot", "failed"); + } + + if let Some(message) = document.get_element_by_id("miku-loading-message") { + message.set_text_content(Some("Miku could not start")); + } + + if let Some(detail) = document.get_element_by_id("miku-loading-detail") { + detail.set_text_content(Some(&format!("{error:?}"))); + } +} + +#[cfg(target_arch = "wasm32")] +fn browser_document() -> Option { + web_sys::window()?.document() +} + +#[cfg(target_arch = "wasm32")] +fn add_class(element: &web_sys::Element, class_name: &str) { + let classes = element.class_name(); + if classes + .split_ascii_whitespace() + .any(|current| current == class_name) + { + return; + } + + let next_classes = if classes.is_empty() { + class_name.to_owned() + } else { + format!("{classes} {class_name}") + }; + element.set_class_name(&next_classes); +}