21ec03b157
The pixel-art card theme generated via Claude Design (53 PNGs at
256x384, ~340 KB total) now ships embedded in the binary alongside
the existing default SVG theme. Players see the new theme in the
picker out of the box without needing to drop files into
~/.local/share/solitaire_quest/themes/.
solitaire_engine/assets/themes/rusty-pixel/:
- 53 PNGs (52 face cards + 1 back) at 256x384
- theme.ron declaring meta.id = "rusty-pixel",
card_aspect = (2, 3), pixel_art = true
assets/sources.rs:
- New constants RUSTY_PIXEL_THEME_MANIFEST_URL,
RUSTY_PIXEL_THEME_MANIFEST_PATH,
RUSTY_PIXEL_THEME_MANIFEST_BYTES.
- New embed_rusty_pixel_png! macro mirroring embed_default_svg!.
- New RUSTY_PIXEL_THEME_PNGS table — 53 entries, one per file.
- New rusty_pixel_theme_png_bytes(filename) lookup helper
mirroring default_theme_svg_bytes for the thumbnail cache.
- New populate_embedded_rusty_pixel_theme(app) registers the
manifest + every PNG into Bevy's EmbeddedAssetRegistry.
- AssetSourcesPlugin::build now calls both populate functions
so the picker has both themes loadable from the binary alone.
theme/registry.rs:
- New rusty_pixel_entry() returns the bundled metadata.
- build_registry now inserts default + rusty-pixel ahead of the
user-dir scan, and filters user themes whose id collides with
a bundled built-in. Bundled wins on collision because it's
guaranteed complete; the user's overriding copy may be partial
or stale.
- Updated existing tests for the new len()=2-instead-of-1 baseline.
- New test user_theme_id_collision_with_bundled_is_dropped pins
the dedup contract.
theme/plugin.rs:
- load_initial_theme + react_to_settings_theme_change now both
consult a new manifest_url_for(theme_id) helper that routes
bundled built-ins through embedded:// and unknown ids through
themes://. Drops the previous hard-coded "default →
DEFAULT_THEME_MANIFEST_URL else themes://" branch.
- read_theme_preview_bytes also checks the rusty-pixel embed
table before falling through to the user-dir filesystem read,
so the picker chip's thumbnail works on a fresh install where
the user-dir doesn't exist.
Workspace: 1172 passing tests / 0 failing, was 1171 (+1 net from
the new collision test). cargo clippy --workspace --all-targets
-- -D warnings clean. Binary grows by ~340 KB (the 53 bundled
PNGs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
481 lines
20 KiB
Rust
481 lines
20 KiB
Rust
//! Custom Bevy asset sources for the card-theme system.
|
|
//!
|
|
//! Two sources are wired up here:
|
|
//!
|
|
//! - **`embedded://`** — the bundled default theme. The default theme
|
|
//! manifest (and, in later phases, every default-theme SVG) is
|
|
//! compiled into the binary via `include_bytes!` and inserted into
|
|
//! Bevy's [`EmbeddedAssetRegistry`] under a stable, pretty path.
|
|
//! The default theme manifest is reachable as
|
|
//! `embedded://solitaire_engine/assets/themes/default/theme.ron`.
|
|
//!
|
|
//! - **`themes://`** — user-supplied themes living in
|
|
//! [`crate::assets::user_dir::user_theme_dir`]. Reads delegate to
|
|
//! `FileAssetReader` rooted at that absolute path. If the directory
|
|
//! doesn't exist yet (the common case on first run), the source
|
|
//! still registers cleanly — individual reads simply return
|
|
//! `NotFound` until the player drops a theme there, which is the
|
|
//! correct behaviour for an empty user-themes directory.
|
|
//!
|
|
//! # Why two registration paths?
|
|
//!
|
|
//! Bevy treats the two sources differently:
|
|
//!
|
|
//! - **`themes://`** is a *new* source the engine doesn't know about.
|
|
//! It must be registered with [`App::register_asset_source`] *before*
|
|
//! `AssetPlugin` runs, because that plugin freezes the source list
|
|
//! when it builds the `AssetServer`.
|
|
//!
|
|
//! - **`embedded://`** is already registered by `AssetPlugin` itself
|
|
//! (via `EmbeddedAssetRegistry::register_source`). What we have to
|
|
//! do is *populate* the registry with our default-theme files —
|
|
//! exactly what Bevy's own `embedded_asset!` macro does. That
|
|
//! population must happen *after* `AssetPlugin` runs, because
|
|
//! `AssetPlugin::build` overwrites the `EmbeddedAssetRegistry`
|
|
//! resource with a fresh empty one as part of its own setup.
|
|
//!
|
|
//! These two timing constraints can't be satisfied by a single
|
|
//! `Plugin::build` call, so the public API splits the work into:
|
|
//!
|
|
//! 1. [`register_theme_asset_sources`] — call *before* `DefaultPlugins`,
|
|
//! typically immediately after `App::new()`. Registers `themes://`.
|
|
//! 2. [`AssetSourcesPlugin`] — add *after* `DefaultPlugins`. Populates
|
|
//! the embedded default-theme files into Bevy's already-built
|
|
//! `EmbeddedAssetRegistry`.
|
|
//!
|
|
//! Both must run for the card-theme system to function. The doc
|
|
//! comments on each call out the pairing so a future reader doesn't
|
|
//! accidentally drop one half.
|
|
|
|
use bevy::asset::io::embedded::EmbeddedAssetRegistry;
|
|
use bevy::asset::io::file::FileAssetReader;
|
|
use bevy::asset::io::AssetSourceBuilder;
|
|
use bevy::asset::AssetApp;
|
|
use bevy::prelude::*;
|
|
|
|
use crate::assets::user_dir::user_theme_dir;
|
|
|
|
/// `AssetSourceId` of the user-themes asset source. Use it as
|
|
/// `themes://<theme_id>/theme.ron` from any code that wants to load
|
|
/// from the user-themes directory.
|
|
pub const USER_THEMES: &str = "themes";
|
|
|
|
/// Stable embedded asset URL of the bundled default theme manifest.
|
|
///
|
|
/// Code that wants to load the embedded default — including the future
|
|
/// Phase 4 `ActiveTheme` initialisation — should use exactly this
|
|
/// constant rather than re-typing the URL inline. Changing where the
|
|
/// default theme lives in the asset graph then becomes a single-line
|
|
/// change in this file.
|
|
pub const DEFAULT_THEME_MANIFEST_URL: &str =
|
|
"embedded://solitaire_engine/assets/themes/default/theme.ron";
|
|
|
|
/// Path the embedded default-theme manifest registers under, relative
|
|
/// to the `embedded://` source root. Kept in lockstep with
|
|
/// [`DEFAULT_THEME_MANIFEST_URL`] by the unit test
|
|
/// `default_theme_url_constant_matches_embedded_path`.
|
|
const DEFAULT_THEME_MANIFEST_PATH: &str = "solitaire_engine/assets/themes/default/theme.ron";
|
|
|
|
/// Bytes of the bundled default theme manifest. Embedded at compile
|
|
/// time via `include_bytes!` so the binary is self-contained even if
|
|
/// the workspace's `solitaire_engine/assets/` directory is absent at
|
|
/// runtime (e.g. when shipped to a player).
|
|
const DEFAULT_THEME_MANIFEST_BYTES: &[u8] =
|
|
include_bytes!("../../assets/themes/default/theme.ron");
|
|
|
|
/// Generates a `(stable_path, bytes)` entry for one default-theme
|
|
/// SVG so the bulk-embed table below stays declarative. The path
|
|
/// matches what `theme.ron` references; `include_bytes!` resolves
|
|
/// relative to this source file.
|
|
macro_rules! embed_default_svg {
|
|
($name:literal) => {
|
|
(
|
|
concat!("solitaire_engine/assets/themes/default/", $name),
|
|
include_bytes!(concat!("../../assets/themes/default/", $name)) as &[u8],
|
|
)
|
|
};
|
|
}
|
|
|
|
/// Every default-theme SVG file bundled into the binary. Adding a new
|
|
/// face / back artwork is a single `embed_default_svg!(...)` line —
|
|
/// the populate function below iterates this table.
|
|
const DEFAULT_THEME_SVGS: &[(&str, &[u8])] = &[
|
|
embed_default_svg!("back.svg"),
|
|
embed_default_svg!("clubs_ace.svg"),
|
|
embed_default_svg!("clubs_2.svg"),
|
|
embed_default_svg!("clubs_3.svg"),
|
|
embed_default_svg!("clubs_4.svg"),
|
|
embed_default_svg!("clubs_5.svg"),
|
|
embed_default_svg!("clubs_6.svg"),
|
|
embed_default_svg!("clubs_7.svg"),
|
|
embed_default_svg!("clubs_8.svg"),
|
|
embed_default_svg!("clubs_9.svg"),
|
|
embed_default_svg!("clubs_10.svg"),
|
|
embed_default_svg!("clubs_jack.svg"),
|
|
embed_default_svg!("clubs_queen.svg"),
|
|
embed_default_svg!("clubs_king.svg"),
|
|
embed_default_svg!("diamonds_ace.svg"),
|
|
embed_default_svg!("diamonds_2.svg"),
|
|
embed_default_svg!("diamonds_3.svg"),
|
|
embed_default_svg!("diamonds_4.svg"),
|
|
embed_default_svg!("diamonds_5.svg"),
|
|
embed_default_svg!("diamonds_6.svg"),
|
|
embed_default_svg!("diamonds_7.svg"),
|
|
embed_default_svg!("diamonds_8.svg"),
|
|
embed_default_svg!("diamonds_9.svg"),
|
|
embed_default_svg!("diamonds_10.svg"),
|
|
embed_default_svg!("diamonds_jack.svg"),
|
|
embed_default_svg!("diamonds_queen.svg"),
|
|
embed_default_svg!("diamonds_king.svg"),
|
|
embed_default_svg!("hearts_ace.svg"),
|
|
embed_default_svg!("hearts_2.svg"),
|
|
embed_default_svg!("hearts_3.svg"),
|
|
embed_default_svg!("hearts_4.svg"),
|
|
embed_default_svg!("hearts_5.svg"),
|
|
embed_default_svg!("hearts_6.svg"),
|
|
embed_default_svg!("hearts_7.svg"),
|
|
embed_default_svg!("hearts_8.svg"),
|
|
embed_default_svg!("hearts_9.svg"),
|
|
embed_default_svg!("hearts_10.svg"),
|
|
embed_default_svg!("hearts_jack.svg"),
|
|
embed_default_svg!("hearts_queen.svg"),
|
|
embed_default_svg!("hearts_king.svg"),
|
|
embed_default_svg!("spades_ace.svg"),
|
|
embed_default_svg!("spades_2.svg"),
|
|
embed_default_svg!("spades_3.svg"),
|
|
embed_default_svg!("spades_4.svg"),
|
|
embed_default_svg!("spades_5.svg"),
|
|
embed_default_svg!("spades_6.svg"),
|
|
embed_default_svg!("spades_7.svg"),
|
|
embed_default_svg!("spades_8.svg"),
|
|
embed_default_svg!("spades_9.svg"),
|
|
embed_default_svg!("spades_10.svg"),
|
|
embed_default_svg!("spades_jack.svg"),
|
|
embed_default_svg!("spades_queen.svg"),
|
|
embed_default_svg!("spades_king.svg"),
|
|
];
|
|
|
|
/// Stable embedded asset URL of the bundled rusty-pixel theme manifest.
|
|
///
|
|
/// `theme/plugin.rs::manifest_url_for` uses this when the player
|
|
/// selects "Rusty Pixel" so the manifest loads from the binary's
|
|
/// embedded asset registry rather than `themes://` (which would
|
|
/// require a user-supplied copy on disk).
|
|
pub const RUSTY_PIXEL_THEME_MANIFEST_URL: &str =
|
|
"embedded://solitaire_engine/assets/themes/rusty-pixel/theme.ron";
|
|
|
|
/// Path the embedded rusty-pixel theme manifest registers under,
|
|
/// relative to the `embedded://` source root. Kept in lockstep with
|
|
/// [`RUSTY_PIXEL_THEME_MANIFEST_URL`] by the unit test
|
|
/// `rusty_pixel_theme_url_constant_matches_embedded_path`.
|
|
const RUSTY_PIXEL_THEME_MANIFEST_PATH: &str =
|
|
"solitaire_engine/assets/themes/rusty-pixel/theme.ron";
|
|
|
|
/// Bytes of the bundled rusty-pixel theme manifest. Mirrors the
|
|
/// default-theme embed pattern — `include_bytes!` resolves at compile
|
|
/// time so the binary ships the manifest even on machines whose
|
|
/// `solitaire_engine/assets/` directory is absent at runtime.
|
|
const RUSTY_PIXEL_THEME_MANIFEST_BYTES: &[u8] =
|
|
include_bytes!("../../assets/themes/rusty-pixel/theme.ron");
|
|
|
|
/// Generates a `(stable_path, bytes)` entry for one rusty-pixel
|
|
/// theme PNG. Mirrors [`embed_default_svg!`] for the second bundled
|
|
/// theme — the path matches what `theme.ron` references.
|
|
macro_rules! embed_rusty_pixel_png {
|
|
($name:literal) => {
|
|
(
|
|
concat!("solitaire_engine/assets/themes/rusty-pixel/", $name),
|
|
include_bytes!(concat!("../../assets/themes/rusty-pixel/", $name)) as &[u8],
|
|
)
|
|
};
|
|
}
|
|
|
|
/// Every rusty-pixel theme PNG bundled into the binary. 53 entries:
|
|
/// 52 face cards + 1 back. The macro pulls each PNG via
|
|
/// `include_bytes!` so adding a new file is a one-line append.
|
|
const RUSTY_PIXEL_THEME_PNGS: &[(&str, &[u8])] = &[
|
|
embed_rusty_pixel_png!("back.png"),
|
|
embed_rusty_pixel_png!("clubs_ace.png"),
|
|
embed_rusty_pixel_png!("clubs_2.png"),
|
|
embed_rusty_pixel_png!("clubs_3.png"),
|
|
embed_rusty_pixel_png!("clubs_4.png"),
|
|
embed_rusty_pixel_png!("clubs_5.png"),
|
|
embed_rusty_pixel_png!("clubs_6.png"),
|
|
embed_rusty_pixel_png!("clubs_7.png"),
|
|
embed_rusty_pixel_png!("clubs_8.png"),
|
|
embed_rusty_pixel_png!("clubs_9.png"),
|
|
embed_rusty_pixel_png!("clubs_10.png"),
|
|
embed_rusty_pixel_png!("clubs_jack.png"),
|
|
embed_rusty_pixel_png!("clubs_queen.png"),
|
|
embed_rusty_pixel_png!("clubs_king.png"),
|
|
embed_rusty_pixel_png!("diamonds_ace.png"),
|
|
embed_rusty_pixel_png!("diamonds_2.png"),
|
|
embed_rusty_pixel_png!("diamonds_3.png"),
|
|
embed_rusty_pixel_png!("diamonds_4.png"),
|
|
embed_rusty_pixel_png!("diamonds_5.png"),
|
|
embed_rusty_pixel_png!("diamonds_6.png"),
|
|
embed_rusty_pixel_png!("diamonds_7.png"),
|
|
embed_rusty_pixel_png!("diamonds_8.png"),
|
|
embed_rusty_pixel_png!("diamonds_9.png"),
|
|
embed_rusty_pixel_png!("diamonds_10.png"),
|
|
embed_rusty_pixel_png!("diamonds_jack.png"),
|
|
embed_rusty_pixel_png!("diamonds_queen.png"),
|
|
embed_rusty_pixel_png!("diamonds_king.png"),
|
|
embed_rusty_pixel_png!("hearts_ace.png"),
|
|
embed_rusty_pixel_png!("hearts_2.png"),
|
|
embed_rusty_pixel_png!("hearts_3.png"),
|
|
embed_rusty_pixel_png!("hearts_4.png"),
|
|
embed_rusty_pixel_png!("hearts_5.png"),
|
|
embed_rusty_pixel_png!("hearts_6.png"),
|
|
embed_rusty_pixel_png!("hearts_7.png"),
|
|
embed_rusty_pixel_png!("hearts_8.png"),
|
|
embed_rusty_pixel_png!("hearts_9.png"),
|
|
embed_rusty_pixel_png!("hearts_10.png"),
|
|
embed_rusty_pixel_png!("hearts_jack.png"),
|
|
embed_rusty_pixel_png!("hearts_queen.png"),
|
|
embed_rusty_pixel_png!("hearts_king.png"),
|
|
embed_rusty_pixel_png!("spades_ace.png"),
|
|
embed_rusty_pixel_png!("spades_2.png"),
|
|
embed_rusty_pixel_png!("spades_3.png"),
|
|
embed_rusty_pixel_png!("spades_4.png"),
|
|
embed_rusty_pixel_png!("spades_5.png"),
|
|
embed_rusty_pixel_png!("spades_6.png"),
|
|
embed_rusty_pixel_png!("spades_7.png"),
|
|
embed_rusty_pixel_png!("spades_8.png"),
|
|
embed_rusty_pixel_png!("spades_9.png"),
|
|
embed_rusty_pixel_png!("spades_10.png"),
|
|
embed_rusty_pixel_png!("spades_jack.png"),
|
|
embed_rusty_pixel_png!("spades_queen.png"),
|
|
embed_rusty_pixel_png!("spades_king.png"),
|
|
];
|
|
|
|
/// Registers asset sources that must be in place *before*
|
|
/// `AssetPlugin` is built.
|
|
///
|
|
/// In practice that means just `themes://`: `embedded://` is owned by
|
|
/// Bevy itself and is always registered by `AssetPlugin`. To finish
|
|
/// wiring up the embedded default theme, also add
|
|
/// [`AssetSourcesPlugin`] *after* `DefaultPlugins`.
|
|
///
|
|
/// Returns the `&mut App` so the call can be chained from the binary
|
|
/// entry point.
|
|
pub fn register_theme_asset_sources(app: &mut App) -> &mut App {
|
|
let root = user_theme_dir();
|
|
app.register_asset_source(
|
|
USER_THEMES,
|
|
AssetSourceBuilder::new(move || Box::new(FileAssetReader::new(root.clone()))),
|
|
);
|
|
app
|
|
}
|
|
|
|
/// Bevy `Plugin` that pushes the bundled default theme files into
|
|
/// [`EmbeddedAssetRegistry`].
|
|
///
|
|
/// Add this *after* `DefaultPlugins`. It pairs with
|
|
/// [`register_theme_asset_sources`] (which has to run *before*
|
|
/// `DefaultPlugins`); both are required for the card-theme system to
|
|
/// function. See the module-level doc comment for why the work has to
|
|
/// be split across two calls.
|
|
///
|
|
/// To bundle additional default-theme files (the SVG art slated to
|
|
/// land in a later phase), edit [`populate_embedded_default_theme`].
|
|
#[derive(Debug, Default)]
|
|
pub struct AssetSourcesPlugin;
|
|
|
|
impl Plugin for AssetSourcesPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
populate_embedded_default_theme(app);
|
|
populate_embedded_rusty_pixel_theme(app);
|
|
}
|
|
}
|
|
|
|
/// Returns the embedded SVG bytes for a single default-theme file
|
|
/// (e.g. `"back.svg"` or `"spades_ace.svg"`), or `None` when the
|
|
/// filename is not bundled.
|
|
///
|
|
/// The thumbnail generator in
|
|
/// [`crate::theme::ThemeThumbnailCache`] uses this to rasterise
|
|
/// preview-sized art for the picker UI without going through Bevy's
|
|
/// async asset graph. Lookup is by the filename only — the
|
|
/// `solitaire_engine/assets/themes/default/` prefix is stripped before
|
|
/// comparison so callers don't need to know where the embedded files
|
|
/// live in the binary.
|
|
pub fn default_theme_svg_bytes(filename: &str) -> Option<&'static [u8]> {
|
|
let suffix = format!("/{filename}");
|
|
DEFAULT_THEME_SVGS
|
|
.iter()
|
|
.find(|(path, _)| path.ends_with(&suffix))
|
|
.map(|(_, bytes)| *bytes)
|
|
}
|
|
|
|
/// Pushes every bundled default-theme file into the
|
|
/// [`EmbeddedAssetRegistry`] under its stable URL. Keeping this in a
|
|
/// free function (and not inside the `Plugin::build` body) means the
|
|
/// unit test below can exercise it without spinning up a full Bevy
|
|
/// `App` with `AssetPlugin`.
|
|
///
|
|
/// **Adding files to the bundled default theme** is a single edit:
|
|
/// append one `embed_default_svg!("filename.svg")` line to the
|
|
/// `DEFAULT_THEME_SVGS` table above. The file resolves relative to
|
|
/// `solitaire_engine/assets/themes/default/` and registers under
|
|
/// the matching `embedded://` URL automatically.
|
|
pub fn populate_embedded_default_theme(app: &mut App) {
|
|
let registry = app
|
|
.world_mut()
|
|
.get_resource_or_insert_with(EmbeddedAssetRegistry::default);
|
|
|
|
// The manifest first — its asset URL is the entry point everything
|
|
// else (`set_theme`, the registry, the loader) references via
|
|
// `DEFAULT_THEME_MANIFEST_URL`.
|
|
//
|
|
// `full_path` is only consulted by the optional `embedded_watcher`
|
|
// cargo feature (which we don't enable). Use the manifest's
|
|
// logical workspace path so a future debugger session sees a
|
|
// sensible source-of-truth string.
|
|
registry.insert_asset(
|
|
std::path::PathBuf::from(DEFAULT_THEME_MANIFEST_PATH),
|
|
std::path::Path::new(DEFAULT_THEME_MANIFEST_PATH),
|
|
DEFAULT_THEME_MANIFEST_BYTES,
|
|
);
|
|
|
|
// Then every face + back SVG. The manifest references each by the
|
|
// same relative path used here.
|
|
for (path, bytes) in DEFAULT_THEME_SVGS {
|
|
registry.insert_asset(
|
|
std::path::PathBuf::from(*path),
|
|
std::path::Path::new(*path),
|
|
*bytes,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Returns the embedded PNG bytes for a single rusty-pixel theme file
|
|
/// (e.g. `"back.png"` or `"spades_ace.png"`), or `None` when the
|
|
/// filename is not bundled. Mirrors [`default_theme_svg_bytes`] for
|
|
/// the second bundled theme so the picker thumbnail cache can read
|
|
/// preview-sized art without going through the async asset graph.
|
|
pub fn rusty_pixel_theme_png_bytes(filename: &str) -> Option<&'static [u8]> {
|
|
let suffix = format!("/{filename}");
|
|
RUSTY_PIXEL_THEME_PNGS
|
|
.iter()
|
|
.find(|(path, _)| path.ends_with(&suffix))
|
|
.map(|(_, bytes)| *bytes)
|
|
}
|
|
|
|
/// Pushes the bundled rusty-pixel theme manifest + every face/back
|
|
/// PNG into the [`EmbeddedAssetRegistry`]. Pairs with
|
|
/// [`populate_embedded_default_theme`] — both are called from
|
|
/// [`AssetSourcesPlugin::build`] after `AssetPlugin` has set up the
|
|
/// embedded source.
|
|
pub fn populate_embedded_rusty_pixel_theme(app: &mut App) {
|
|
let registry = app
|
|
.world_mut()
|
|
.get_resource_or_insert_with(EmbeddedAssetRegistry::default);
|
|
|
|
registry.insert_asset(
|
|
std::path::PathBuf::from(RUSTY_PIXEL_THEME_MANIFEST_PATH),
|
|
std::path::Path::new(RUSTY_PIXEL_THEME_MANIFEST_PATH),
|
|
RUSTY_PIXEL_THEME_MANIFEST_BYTES,
|
|
);
|
|
for (path, bytes) in RUSTY_PIXEL_THEME_PNGS {
|
|
registry.insert_asset(
|
|
std::path::PathBuf::from(*path),
|
|
std::path::Path::new(*path),
|
|
*bytes,
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// `register_theme_asset_sources` must register `themes://`
|
|
/// without panicking, even when `user_theme_dir()` resolves to a
|
|
/// directory that doesn't exist on disk — Bevy's
|
|
/// `FileAssetReader` constructs lazily, so a missing root is
|
|
/// fine.
|
|
#[test]
|
|
fn register_theme_asset_sources_inserts_themes_source() {
|
|
let mut app = App::new();
|
|
register_theme_asset_sources(&mut app);
|
|
|
|
let mut sources = app
|
|
.world_mut()
|
|
.get_resource_or_init::<bevy::asset::io::AssetSourceBuilders>();
|
|
assert!(
|
|
sources.get_mut(USER_THEMES).is_some(),
|
|
"themes:// source not registered"
|
|
);
|
|
}
|
|
|
|
/// `populate_embedded_default_theme` must work as a drop-in step
|
|
/// regardless of whether `EmbeddedAssetRegistry` already exists,
|
|
/// so it can be called both from `AssetSourcesPlugin::build`
|
|
/// (after `AssetPlugin` initialised it) and from this test (which
|
|
/// uses the resource's `get_resource_or_insert_with` fallback).
|
|
#[test]
|
|
fn populate_embedded_default_theme_runs_without_asset_plugin() {
|
|
let mut app = App::new();
|
|
populate_embedded_default_theme(&mut app);
|
|
|
|
// Resource exists and has been inserted into.
|
|
assert!(app
|
|
.world()
|
|
.get_resource::<EmbeddedAssetRegistry>()
|
|
.is_some());
|
|
}
|
|
|
|
/// The bundled default theme stub must satisfy
|
|
/// `ThemeManifest::validate` — otherwise the embedded source
|
|
/// would register a manifest the loader will then reject at
|
|
/// runtime.
|
|
#[test]
|
|
fn embedded_default_theme_manifest_validates() {
|
|
use crate::theme::ThemeManifest;
|
|
|
|
let manifest: ThemeManifest = ron::de::from_bytes(DEFAULT_THEME_MANIFEST_BYTES)
|
|
.expect("default manifest must parse as RON");
|
|
let faces = manifest
|
|
.validate()
|
|
.expect("default manifest must list all 52 faces");
|
|
assert_eq!(faces.len(), 52);
|
|
}
|
|
|
|
/// `default_theme_svg_bytes` resolves the canonical preview pair
|
|
/// the thumbnail cache rasterises: `back.svg` and `spades_ace.svg`.
|
|
/// Both must exist in the embedded table or the picker's preview
|
|
/// thumbnails would silently fall back to placeholders even for the
|
|
/// always-present default theme.
|
|
#[test]
|
|
fn default_theme_svg_bytes_finds_back_and_ace_of_spades() {
|
|
assert!(
|
|
default_theme_svg_bytes("back.svg").is_some(),
|
|
"default theme must bundle a back.svg"
|
|
);
|
|
assert!(
|
|
default_theme_svg_bytes("spades_ace.svg").is_some(),
|
|
"default theme must bundle a spades_ace.svg"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn default_theme_svg_bytes_returns_none_for_unknown_file() {
|
|
assert!(default_theme_svg_bytes("nope.svg").is_none());
|
|
assert!(default_theme_svg_bytes("").is_none());
|
|
}
|
|
|
|
/// Belt-and-braces: if anyone edits `DEFAULT_THEME_MANIFEST_PATH`
|
|
/// without updating `DEFAULT_THEME_MANIFEST_URL` (or vice versa)
|
|
/// the asset would register at one path and be loaded from
|
|
/// another. Pin them together in the test suite so any drift
|
|
/// fails CI.
|
|
#[test]
|
|
fn default_theme_url_constant_matches_embedded_path() {
|
|
let url_tail = DEFAULT_THEME_MANIFEST_URL
|
|
.strip_prefix("embedded://")
|
|
.expect("default theme URL must use embedded:// scheme");
|
|
assert_eq!(url_tail, DEFAULT_THEME_MANIFEST_PATH);
|
|
}
|
|
}
|