Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 129 additions & 5 deletions assets/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,57 @@
transform: rotate(360deg);
}
}

.bar-container {
width: 128px;
height: 12px;
border: 3px solid #ececec;
border-radius: 0.375em;
position: relative;
overflow: hidden;
margin-top: 24px;
margin-bottom: 12px;
}

.loading-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #b2b2b2;
transform-origin: center left;
transform: scaleX(0);
}

.progress-text {
font-size: 0.9rem;
color: #ececec;
font-family:
DejaVu Sans Mono,
monospace;
}

.error {
font-size: 0.9rem;
color: salmon;
font-family:
DejaVu Sans Mono,
monospace;
margin-top: 12px;
}
</style>
</head>

<body class="center">
<noscript>JavaScript support is required to run this app</noscript>
<div id="loading-screen" class="center">
<span class="spinner"></span>
<div hidden="hidden" class="bar-container">
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a fan of semantic HTML, which is also more accessible.
Can we use the <progress> element instead?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In most cases I would absolutely agree. However, styling the progress element is a little nasty between browsers. It would require a bit more CSS than this PR adds already.

Maybe we could compromise with a few additional aria attributes?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair! The aria attributes work for me as well :)

<div class="loading-bar"></div>
</div>
<div class="progress-text"></div>
<div class="error"></div>
</div>

<script type="module">
Expand Down Expand Up @@ -134,14 +178,94 @@
</script>

<script type="module">
// Starting the game

// When this file is used as the default `index.html`, the CLI will automatically replace
// `bevy_app.js` with the name of the generated JS entrypoint. If you copy this file and
// customize it, you will need to manually change the name. For more information, please see
// `bevy_app.js` and `bevy_app_bg.wasm` with the name of the generated JS entrypoint and Wasm
// binary. If you copy this file and customize it, you will need to manually change
// the names. For more information, please see
// <https://thebevyflock.github.io/bevy_cli/cli/web/default-index-html.html>!
import init from "./build/bevy_app.js";
init().catch((error) => {
const wasmPath = "./build/bevy_app_bg.wasm";
const wasmSize = null;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading this by itself is quite confusing. I wonder if we need a more visual indicator for the template replacement, especially so that users can use it for their custom index files.

But that issue existed before, so it probably makes sense to tackle it in a different PR

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The three calls to replace are also rather ugly in the Rust code.


// Provide progress reporting when fetching the Wasm binary
async function fetchWithProgress(url, total, onProgress) {
const response = await fetch(url);

if (!response.ok) {
throw new Error(`request failed with status ${response.status}`);
}

if (total === null) {
// If the content length isn't provided,
// we'll just skip the progress reporting
return await response.arrayBuffer();
}

let receivedLength = 0;
const chunks = [];

for await (const chunk of response.body) {
chunks.push(chunk);
receivedLength += chunk.length;

const progress = receivedLength / total;

// Interacting with the DOM or other operations
// here should not cause downloading to fail
try {
onProgress({
loaded: receivedLength,
total: total,
progress: progress,
});
} catch (e) {
console.error(e);
}
}

const chunksAll = new Uint8Array(receivedLength);
let position = 0;
for (let chunk of chunks) {
chunksAll.set(chunk, position);
position += chunk.length;
}

return chunksAll.buffer;
}

function humanReadable(number) {
// Working with Bevy on the web, we can safely
// assume this scale
return `${(number / 1e6).toFixed(2)}MB`;
Comment thread
CorvusPrudens marked this conversation as resolved.
Outdated
}

async function loadWasm(url, size) {
let arrayBuffer;
try {
arrayBuffer = await fetchWithProgress(url, size, (stats) => {
const container = document.querySelector(".bar-container");
container.removeAttribute("hidden");

const bar = document.querySelector(".loading-bar");
bar.style.transform = `scaleX(${stats.progress})`;

const text = document.querySelector(".progress-text");
text.innerText = `${humanReadable(stats.loaded)} / ${humanReadable(stats.total)}`;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think the progress indicator is a bit busy right now. Not yet sure what I would prefer, maybe just the loaded size or a percentage? Even though the percentage is kinda redundant because of the progress bar.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a few variations:

Screenshot 2026-02-21 at 2 18 16 PM Screenshot 2026-02-21 at 2 24 16 PM Screenshot 2026-02-21 at 2 30 02 PM Screenshot 2026-02-21 at 2 30 28 PM

We could even go the unity route with a Bevy logo (unless there are any sort of trademark concerns):

Screenshot 2026-02-21 at 2 35 44 PM

Though I do appreciate having the bytes downloaded text over merely a bar.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remember correctly, using the Bevy logo requires approval on a per-game basis. But I'm not entirely sure.

});
} catch (e) {
// Here, we communicate any request failure to the user
const errorNode = document.querySelector(".error");
if (errorNode !== null) {
errorNode.innerText = e.toString();
}
throw e;
}

return await init(arrayBuffer);
}

// Starting the game
loadWasm(wasmPath, wasmSize).catch((error) => {
if (
!error.message.startsWith(
"Using exceptions for control flow, don't mind me. This isn't actually an error!"
Expand Down
22 changes: 20 additions & 2 deletions src/web/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,28 @@ fn default_index(bin_target: &BinTarget) -> String {
));

// Insert correct path to JS bindings
template.replace(
let template = template.replace(
"./build/bevy_app.js",
format!("./build/{}.js", bin_target.bin_name).as_str(),
)
);
let template = template.replace(
"./build/bevy_app_bg.wasm",
format!("./build/{}_bg.wasm", bin_target.bin_name).as_str(),
);

if let Ok(metadata) = std::fs::metadata(
bin_target
.artifact_directory
.join(format!("{}_bg.wasm", bin_target.bin_name)),
) {
let length = metadata.len();
template.replace(
"const wasmSize = null;",
format!("const wasmSize = {}", length).as_str(),
)
} else {
template
}
}

/// Generate a title to display on the web page by default.
Expand Down