feat(engine): rename themes — Classic is default, Dark replaces Default
Build and Deploy / build-and-push (push) Successful in 33s
Build and Deploy / build-and-push (push) Successful in 33s
- Rename assets/themes/default/ → assets/themes/dark/; update theme.ron id/name to "dark"/"Dark" - Rename all DEFAULT_THEME_* constants → DARK_THEME_* and default_theme_svg_bytes / populate_embedded_default_theme → dark_* - Add bundled_theme_url() helper for URL resolution without needing the registry (used by Startup systems where ordering isn't guaranteed) - Registry now lists Classic first (new player default), Dark second - settings.rs default_theme_id() returns "classic" so fresh installs start on the white card theme Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,8 @@ pub mod svg_loader;
|
||||
pub mod user_dir;
|
||||
|
||||
pub use sources::{
|
||||
default_theme_svg_bytes, populate_embedded_default_theme, register_theme_asset_sources,
|
||||
AssetSourcesPlugin, DEFAULT_THEME_MANIFEST_URL, USER_THEMES,
|
||||
bundled_theme_url, dark_theme_svg_bytes, populate_embedded_dark_theme,
|
||||
register_theme_asset_sources, AssetSourcesPlugin, DARK_THEME_MANIFEST_URL, USER_THEMES,
|
||||
};
|
||||
pub use svg_loader::{rasterize_svg, SvgLoader, SvgLoaderError, SvgLoaderSettings};
|
||||
pub use user_dir::{set_user_theme_dir, user_theme_dir};
|
||||
|
||||
@@ -60,99 +60,89 @@ use crate::assets::user_dir::user_theme_dir;
|
||||
/// from the user-themes directory.
|
||||
pub const USER_THEMES: &str = "themes";
|
||||
|
||||
/// Stable embedded asset URL of the bundled default theme manifest.
|
||||
/// Stable embedded asset URL of the bundled Dark 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";
|
||||
/// Code that wants to load the embedded Dark theme — including
|
||||
/// `ActiveTheme` initialisation — should use exactly this constant
|
||||
/// rather than re-typing the URL inline.
|
||||
pub const DARK_THEME_MANIFEST_URL: &str =
|
||||
"embedded://solitaire_engine/assets/themes/dark/theme.ron";
|
||||
|
||||
/// Path the embedded default-theme manifest registers under, relative
|
||||
/// Path the embedded Dark-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";
|
||||
/// [`DARK_THEME_MANIFEST_URL`] by the unit test
|
||||
/// `dark_theme_url_constant_matches_embedded_path`.
|
||||
const DARK_THEME_MANIFEST_PATH: &str = "solitaire_engine/assets/themes/dark/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");
|
||||
/// Bytes of the bundled Dark theme manifest, embedded at compile time.
|
||||
const DARK_THEME_MANIFEST_BYTES: &[u8] =
|
||||
include_bytes!("../../assets/themes/dark/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 {
|
||||
/// Generates a `(stable_path, bytes)` entry for one Dark-theme SVG.
|
||||
macro_rules! embed_dark_svg {
|
||||
($name:literal) => {
|
||||
(
|
||||
concat!("solitaire_engine/assets/themes/default/", $name),
|
||||
include_bytes!(concat!("../../assets/themes/default/", $name)) as &[u8],
|
||||
concat!("solitaire_engine/assets/themes/dark/", $name),
|
||||
include_bytes!(concat!("../../assets/themes/dark/", $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"),
|
||||
/// Every Dark-theme SVG file bundled into the binary.
|
||||
const DARK_THEME_SVGS: &[(&str, &[u8])] = &[
|
||||
embed_dark_svg!("back.svg"),
|
||||
embed_dark_svg!("clubs_ace.svg"),
|
||||
embed_dark_svg!("clubs_2.svg"),
|
||||
embed_dark_svg!("clubs_3.svg"),
|
||||
embed_dark_svg!("clubs_4.svg"),
|
||||
embed_dark_svg!("clubs_5.svg"),
|
||||
embed_dark_svg!("clubs_6.svg"),
|
||||
embed_dark_svg!("clubs_7.svg"),
|
||||
embed_dark_svg!("clubs_8.svg"),
|
||||
embed_dark_svg!("clubs_9.svg"),
|
||||
embed_dark_svg!("clubs_10.svg"),
|
||||
embed_dark_svg!("clubs_jack.svg"),
|
||||
embed_dark_svg!("clubs_queen.svg"),
|
||||
embed_dark_svg!("clubs_king.svg"),
|
||||
embed_dark_svg!("diamonds_ace.svg"),
|
||||
embed_dark_svg!("diamonds_2.svg"),
|
||||
embed_dark_svg!("diamonds_3.svg"),
|
||||
embed_dark_svg!("diamonds_4.svg"),
|
||||
embed_dark_svg!("diamonds_5.svg"),
|
||||
embed_dark_svg!("diamonds_6.svg"),
|
||||
embed_dark_svg!("diamonds_7.svg"),
|
||||
embed_dark_svg!("diamonds_8.svg"),
|
||||
embed_dark_svg!("diamonds_9.svg"),
|
||||
embed_dark_svg!("diamonds_10.svg"),
|
||||
embed_dark_svg!("diamonds_jack.svg"),
|
||||
embed_dark_svg!("diamonds_queen.svg"),
|
||||
embed_dark_svg!("diamonds_king.svg"),
|
||||
embed_dark_svg!("hearts_ace.svg"),
|
||||
embed_dark_svg!("hearts_2.svg"),
|
||||
embed_dark_svg!("hearts_3.svg"),
|
||||
embed_dark_svg!("hearts_4.svg"),
|
||||
embed_dark_svg!("hearts_5.svg"),
|
||||
embed_dark_svg!("hearts_6.svg"),
|
||||
embed_dark_svg!("hearts_7.svg"),
|
||||
embed_dark_svg!("hearts_8.svg"),
|
||||
embed_dark_svg!("hearts_9.svg"),
|
||||
embed_dark_svg!("hearts_10.svg"),
|
||||
embed_dark_svg!("hearts_jack.svg"),
|
||||
embed_dark_svg!("hearts_queen.svg"),
|
||||
embed_dark_svg!("hearts_king.svg"),
|
||||
embed_dark_svg!("spades_ace.svg"),
|
||||
embed_dark_svg!("spades_2.svg"),
|
||||
embed_dark_svg!("spades_3.svg"),
|
||||
embed_dark_svg!("spades_4.svg"),
|
||||
embed_dark_svg!("spades_5.svg"),
|
||||
embed_dark_svg!("spades_6.svg"),
|
||||
embed_dark_svg!("spades_7.svg"),
|
||||
embed_dark_svg!("spades_8.svg"),
|
||||
embed_dark_svg!("spades_9.svg"),
|
||||
embed_dark_svg!("spades_10.svg"),
|
||||
embed_dark_svg!("spades_jack.svg"),
|
||||
embed_dark_svg!("spades_queen.svg"),
|
||||
embed_dark_svg!("spades_king.svg"),
|
||||
];
|
||||
|
||||
/// Registers asset sources that must be in place *before*
|
||||
@@ -190,62 +180,53 @@ pub struct AssetSourcesPlugin;
|
||||
|
||||
impl Plugin for AssetSourcesPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
populate_embedded_default_theme(app);
|
||||
populate_embedded_dark_theme(app);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the embedded SVG bytes for a single default-theme file
|
||||
/// Returns the embedded SVG bytes for a single Dark-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]> {
|
||||
/// The thumbnail generator uses this to rasterise preview-sized art
|
||||
/// without going through Bevy's async asset graph.
|
||||
pub fn dark_theme_svg_bytes(filename: &str) -> Option<&'static [u8]> {
|
||||
let suffix = format!("/{filename}");
|
||||
DEFAULT_THEME_SVGS
|
||||
DARK_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`.
|
||||
/// Returns the manifest URL for a bundled (non-user) theme by id, or
|
||||
/// `None` if `id` belongs to a user theme that lives under `themes://`.
|
||||
///
|
||||
/// **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) {
|
||||
/// Callers that need to resolve a theme URL without access to
|
||||
/// [`crate::theme::ThemeRegistry`] (e.g. Startup systems where registry
|
||||
/// ordering isn't guaranteed) should use this instead of constructing
|
||||
/// the URL manually.
|
||||
pub fn bundled_theme_url(id: &str) -> Option<&'static str> {
|
||||
match id {
|
||||
"dark" => Some(DARK_THEME_MANIFEST_URL),
|
||||
"classic" => Some("themes/classic/theme.ron"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes every bundled Dark-theme file into the
|
||||
/// [`EmbeddedAssetRegistry`] under its stable URL.
|
||||
pub fn populate_embedded_dark_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,
|
||||
std::path::PathBuf::from(DARK_THEME_MANIFEST_PATH),
|
||||
std::path::Path::new(DARK_THEME_MANIFEST_PATH),
|
||||
DARK_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 {
|
||||
for (path, bytes) in DARK_THEME_SVGS {
|
||||
registry.insert_asset(
|
||||
std::path::PathBuf::from(*path),
|
||||
std::path::Path::new(*path),
|
||||
@@ -277,72 +258,51 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// `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() {
|
||||
fn populate_embedded_dark_theme_runs_without_asset_plugin() {
|
||||
let mut app = App::new();
|
||||
populate_embedded_default_theme(&mut app);
|
||||
|
||||
// Resource exists and has been inserted into.
|
||||
populate_embedded_dark_theme(&mut app);
|
||||
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() {
|
||||
fn embedded_dark_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 manifest: ThemeManifest = ron::de::from_bytes(DARK_THEME_MANIFEST_BYTES)
|
||||
.expect("dark manifest must parse as RON");
|
||||
let faces = manifest
|
||||
.validate()
|
||||
.expect("default manifest must list all 52 faces");
|
||||
.expect("dark 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() {
|
||||
fn dark_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"
|
||||
dark_theme_svg_bytes("back.svg").is_some(),
|
||||
"dark theme must bundle a back.svg"
|
||||
);
|
||||
assert!(
|
||||
default_theme_svg_bytes("spades_ace.svg").is_some(),
|
||||
"default theme must bundle a spades_ace.svg"
|
||||
dark_theme_svg_bytes("spades_ace.svg").is_some(),
|
||||
"dark 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());
|
||||
fn dark_theme_svg_bytes_returns_none_for_unknown_file() {
|
||||
assert!(dark_theme_svg_bytes("nope.svg").is_none());
|
||||
assert!(dark_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
|
||||
fn dark_theme_url_constant_matches_embedded_path() {
|
||||
let url_tail = DARK_THEME_MANIFEST_URL
|
||||
.strip_prefix("embedded://")
|
||||
.expect("default theme URL must use embedded:// scheme");
|
||||
assert_eq!(url_tail, DEFAULT_THEME_MANIFEST_PATH);
|
||||
.expect("dark theme URL must use embedded:// scheme");
|
||||
assert_eq!(url_tail, DARK_THEME_MANIFEST_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user