diff --git a/image-generator-demo/src/app/app.element.scss b/image-generator-demo/src/app/app.element.scss
index 2687776..4473358 100644
--- a/image-generator-demo/src/app/app.element.scss
+++ b/image-generator-demo/src/app/app.element.scss
@@ -166,17 +166,3 @@
background: transparent;
color: #FFFC;
}
-
-.loading {
- position: absolute;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #222;
- color: #FFF;
- font: {
- family: "Fira Sans", sans-serif;
- weight: 700;
- }
-}
diff --git a/image-generator-demo/src/app/app.element.ts b/image-generator-demo/src/app/app.element.ts
index 6d18acd..853f6d6 100644
--- a/image-generator-demo/src/app/app.element.ts
+++ b/image-generator-demo/src/app/app.element.ts
@@ -16,6 +16,7 @@ import { styleMap } from "lit/directives/style-map.js";
import { ImageInfo, LcmClient } from "./lcm-client";
import "@storyteller/studio-web/canvas-provider";
+import "@storyteller/studio-web/loading";
import "@storyteller/studio-web/scene-hierarchy";
import "@storyteller/studio-web/studio";
import "@storyteller/studio-web/transform-toolbar";
@@ -293,10 +294,7 @@ export class AppElement extends LitElement {
nothing}
${this.loading ? html`
-
-
-
Loading
-
+
` : nothing}
`
diff --git a/studio-frontend/src/app/app.element.scss b/studio-frontend/src/app/app.element.scss
index f580b98..c292849 100644
--- a/studio-frontend/src/app/app.element.scss
+++ b/studio-frontend/src/app/app.element.scss
@@ -39,17 +39,3 @@ sts-button.active {
max-height: calc(100vh - 32px);
pointer-events: auto;
}
-
-.loading {
- position: absolute;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #222;
- color: #FFF;
- font: {
- family: "Fira Sans", sans-serif;
- weight: 700;
- }
-}
diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts
index da2ace6..d50e912 100644
--- a/studio-frontend/src/app/app.element.ts
+++ b/studio-frontend/src/app/app.element.ts
@@ -25,6 +25,7 @@ import "@storyteller/studio-ui/icon";
import "@storyteller/studio-ui/toolbar";
import "@storyteller/studio-ui/tree";
import "@storyteller/studio-web/canvas-provider";
+import "@storyteller/studio-web/loading";
import "@storyteller/studio-web/scene-hierarchy";
import "@storyteller/studio-web/studio";
import "@storyteller/studio-web/transform-toolbar";
@@ -161,10 +162,7 @@ export class AppElement extends LitElement {
` : nothing}
${this.loading ? html`
-
-
-
Loading
-
+
` : nothing}
`;
diff --git a/studio-ui/src/index.ts b/studio-ui/src/index.ts
index 6cbf9ae..fe3b7a5 100644
--- a/studio-ui/src/index.ts
+++ b/studio-ui/src/index.ts
@@ -1,4 +1,5 @@
export * from "./lib/button.element";
export * from "./lib/icon.element";
+export * from "./lib/progress.element";
export * from "./lib/toolbar.element";
export * from "./lib/tree.element";
diff --git a/studio-ui/src/lib/progress.element.scss b/studio-ui/src/lib/progress.element.scss
new file mode 100644
index 0000000..c388254
--- /dev/null
+++ b/studio-ui/src/lib/progress.element.scss
@@ -0,0 +1,15 @@
+:host {
+ display: block;
+ height: 0.5rem;
+ background: #713837;
+ overflow: hidden;
+}
+
+.fill {
+ height: 100%;
+ background: #E66462;
+}
+
+.animated {
+ // TODO
+}
diff --git a/studio-ui/src/lib/progress.element.ts b/studio-ui/src/lib/progress.element.ts
new file mode 100644
index 0000000..cd188da
--- /dev/null
+++ b/studio-ui/src/lib/progress.element.ts
@@ -0,0 +1,51 @@
+import { LitElement, type PropertyValues, html, unsafeCSS } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { styleMap } from "lit/directives/style-map.js";
+
+import styles from "./progress.element.scss?inline";
+
+@customElement("sts-progress")
+export class ProgressElement extends LitElement {
+ static override styles = unsafeCSS(styles);
+
+ @property() override role = "progressbar";
+
+ @property({ type: Number }) min = 0;
+ @property({ type: Number }) max = 100;
+ @property({ type: Number }) value = 0;
+ @property({ type: Boolean }) indeterminate = false;
+
+ protected override willUpdate(changes: PropertyValues): void {
+ if (this.min === this.max)
+ this.indeterminate = true;
+
+ if (this.indeterminate) {
+ this.removeAttribute("aria-valuenow");
+ this.removeAttribute("aria-valuemin");
+ this.removeAttribute("aria-valuemax");
+ } else {
+ this.setAttribute("aria-valuenow", this.value.toString());
+
+ if (this.min !== 0)
+ this.setAttribute("aria-valuemin", this.min.toString());
+
+ if (this.max !== 100)
+ this.setAttribute("aria-valuemax", this.max.toString());
+ }
+
+ super.willUpdate(changes);
+ }
+
+ protected override render = () => html`
+ ${this.indeterminate ? html`
+
+ ` : html`
+
+ `}
+ `;
+}
diff --git a/studio-web/src/index.ts b/studio-web/src/index.ts
index e97dae9..0420917 100644
--- a/studio-web/src/index.ts
+++ b/studio-web/src/index.ts
@@ -1,4 +1,5 @@
export * from "./lib/canvas-provider.element";
+export * from "./lib/loading.element";
export * from "./lib/scene-hierarchy.element";
export * from "./lib/studio.element";
export * from "./lib/transform-toolbar.element";
diff --git a/studio-web/src/lib/loading.element.scss b/studio-web/src/lib/loading.element.scss
new file mode 100644
index 0000000..b54eca1
--- /dev/null
+++ b/studio-web/src/lib/loading.element.scss
@@ -0,0 +1,23 @@
+:host {
+ position: fixed;
+ inset: 0;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 1rem;
+
+ background: #1A1A27;
+ color: #FFF;
+
+ font: {
+ family: "Fira Sans", sans-serif;
+ weight: 700;
+ }
+}
+
+.progress {
+ width: 512px;
+ border-radius: 0.25rem;
+}
diff --git a/studio-web/src/lib/loading.element.ts b/studio-web/src/lib/loading.element.ts
new file mode 100644
index 0000000..8e6f78e
--- /dev/null
+++ b/studio-web/src/lib/loading.element.ts
@@ -0,0 +1,39 @@
+import { on } from "@storyteller/framework";
+import { type LoadingQueueEvent } from "@storyteller/studio";
+import { LitElement, html, unsafeCSS } from "lit";
+import { customElement, state } from "lit/decorators.js";
+
+import "@storyteller/studio-ui/progress";
+
+import styles from "./loading.element.scss?inline";
+
+@customElement("sts-loading")
+export class LoadingElement extends LitElement {
+ static override styles = unsafeCSS(styles);
+
+ @state() queueMax = 0;
+ @state() queueLoaded = 0;
+
+ override connectedCallback(): void {
+ this.queueMax = 0;
+ this.queueLoaded = 0;
+ super.connectedCallback();
+ }
+
+ @on("window:loading-queue")
+ onLoadingQueue({ detail: queueLen }: LoadingQueueEvent): void {
+ this.queueMax = Math.max(queueLen, this.queueMax);
+ this.queueLoaded = this.queueMax - queueLen;
+ }
+
+ protected override render = () => html`
+ Loading
+
+
+ `;
+}
diff --git a/studio/src/scene.rs b/studio/src/scene.rs
index 3eca8f2..ebd3c9c 100644
--- a/studio/src/scene.rs
+++ b/studio/src/scene.rs
@@ -96,6 +96,7 @@ impl Plugin for ScenePlugin {
wasm::notify_state_changes
.map(bevy::utils::error)
.run_if(resource_changed::>()),
+ wasm::notify_loading_queue_changes.map(bevy::utils::error),
),
);
}
@@ -516,7 +517,7 @@ mod wasm {
use bevy::{prelude::*, window::PrimaryWindow};
use wasm_bindgen::prelude::*;
- use super::{LoadLibraryObjectEvent, SceneState};
+ use super::{LoadLibraryObjectEvent, LoadingQueue, SceneState};
use crate::wasm::{self, StaticBuffer, WasmMessageBuffer};
static OBJECT_LOAD_REQUEST: WasmMessageBuffer = WasmMessageBuffer::new();
@@ -559,4 +560,40 @@ declare global {
}
Ok(())
}
+
+ #[wasm_bindgen(typescript_custom_section)]
+ const LOADING_QUEUE_EVENT: &str = r#"
+export interface LoadingQueueEvent extends CustomEvent {
+ type: "loading-queue";
+ detail: number;
+}
+
+declare global {
+ export interface GlobalEventHandlersEventMap {
+ "loading-queue": LoadingQueueEvent;
+ }
+}
+ "#;
+
+ pub(super) fn notify_loading_queue_changes(
+ mut l_last_sent: Local