diff --git a/solitaire_data/src/settings.rs b/solitaire_data/src/settings.rs index 7b0f9ad..391c93c 100644 --- a/solitaire_data/src/settings.rs +++ b/solitaire_data/src/settings.rs @@ -143,11 +143,10 @@ pub struct Settings { #[serde(default)] pub window_geometry: Option, /// Identifier of the active card-art theme. Matches `meta.id` from - /// the theme's `theme.ron` manifest. `"default"` is the bundled - /// theme and is always present in the registry; user-supplied - /// themes register under their own ids when they're imported. - /// Older `settings.json` files default cleanly to `"default"` via - /// `#[serde(default = ...)]`. + /// the theme's `theme.ron` manifest. `"classic"` and `"dark"` are + /// always present; user-supplied themes register under their own ids. + /// Older `settings.json` files that stored `"default"` will fall + /// back to the dark embedded theme at runtime. #[serde(default = "default_theme_id")] pub selected_theme_id: String, /// Set to `true` once the achievement-onboarding info-toast has been @@ -273,7 +272,7 @@ fn default_music_volume() -> f32 { } fn default_theme_id() -> String { - "default".to_string() + "classic".to_string() } /// Default tooltip-hover dwell delay in seconds. Mirrors diff --git a/solitaire_engine/assets/themes/default/PROVENANCE.md b/solitaire_engine/assets/themes/dark/PROVENANCE.md similarity index 100% rename from solitaire_engine/assets/themes/default/PROVENANCE.md rename to solitaire_engine/assets/themes/dark/PROVENANCE.md diff --git a/solitaire_engine/assets/themes/default/back.svg b/solitaire_engine/assets/themes/dark/back.svg similarity index 100% rename from solitaire_engine/assets/themes/default/back.svg rename to solitaire_engine/assets/themes/dark/back.svg diff --git a/solitaire_engine/assets/themes/default/clubs_10.svg b/solitaire_engine/assets/themes/dark/clubs_10.svg similarity index 100% rename from solitaire_engine/assets/themes/default/clubs_10.svg rename to solitaire_engine/assets/themes/dark/clubs_10.svg diff --git a/solitaire_engine/assets/themes/default/clubs_2.svg b/solitaire_engine/assets/themes/dark/clubs_2.svg similarity index 100% rename from solitaire_engine/assets/themes/default/clubs_2.svg rename to solitaire_engine/assets/themes/dark/clubs_2.svg diff --git a/solitaire_engine/assets/themes/default/clubs_3.svg b/solitaire_engine/assets/themes/dark/clubs_3.svg similarity index 100% rename from solitaire_engine/assets/themes/default/clubs_3.svg rename to solitaire_engine/assets/themes/dark/clubs_3.svg diff --git a/solitaire_engine/assets/themes/default/clubs_4.svg b/solitaire_engine/assets/themes/dark/clubs_4.svg similarity index 100% rename from solitaire_engine/assets/themes/default/clubs_4.svg rename to solitaire_engine/assets/themes/dark/clubs_4.svg diff --git a/solitaire_engine/assets/themes/default/clubs_5.svg b/solitaire_engine/assets/themes/dark/clubs_5.svg similarity index 100% rename from solitaire_engine/assets/themes/default/clubs_5.svg rename to solitaire_engine/assets/themes/dark/clubs_5.svg diff --git a/solitaire_engine/assets/themes/default/clubs_6.svg b/solitaire_engine/assets/themes/dark/clubs_6.svg similarity index 100% rename from solitaire_engine/assets/themes/default/clubs_6.svg rename to solitaire_engine/assets/themes/dark/clubs_6.svg diff --git a/solitaire_engine/assets/themes/default/clubs_7.svg b/solitaire_engine/assets/themes/dark/clubs_7.svg similarity index 100% rename from solitaire_engine/assets/themes/default/clubs_7.svg rename to solitaire_engine/assets/themes/dark/clubs_7.svg diff --git a/solitaire_engine/assets/themes/default/clubs_8.svg b/solitaire_engine/assets/themes/dark/clubs_8.svg similarity index 100% rename from solitaire_engine/assets/themes/default/clubs_8.svg rename to solitaire_engine/assets/themes/dark/clubs_8.svg diff --git a/solitaire_engine/assets/themes/default/clubs_9.svg b/solitaire_engine/assets/themes/dark/clubs_9.svg similarity index 100% rename from solitaire_engine/assets/themes/default/clubs_9.svg rename to solitaire_engine/assets/themes/dark/clubs_9.svg diff --git a/solitaire_engine/assets/themes/default/clubs_ace.svg b/solitaire_engine/assets/themes/dark/clubs_ace.svg similarity index 100% rename from solitaire_engine/assets/themes/default/clubs_ace.svg rename to solitaire_engine/assets/themes/dark/clubs_ace.svg diff --git a/solitaire_engine/assets/themes/default/clubs_jack.svg b/solitaire_engine/assets/themes/dark/clubs_jack.svg similarity index 100% rename from solitaire_engine/assets/themes/default/clubs_jack.svg rename to solitaire_engine/assets/themes/dark/clubs_jack.svg diff --git a/solitaire_engine/assets/themes/default/clubs_king.svg b/solitaire_engine/assets/themes/dark/clubs_king.svg similarity index 100% rename from solitaire_engine/assets/themes/default/clubs_king.svg rename to solitaire_engine/assets/themes/dark/clubs_king.svg diff --git a/solitaire_engine/assets/themes/default/clubs_queen.svg b/solitaire_engine/assets/themes/dark/clubs_queen.svg similarity index 100% rename from solitaire_engine/assets/themes/default/clubs_queen.svg rename to solitaire_engine/assets/themes/dark/clubs_queen.svg diff --git a/solitaire_engine/assets/themes/default/diamonds_10.svg b/solitaire_engine/assets/themes/dark/diamonds_10.svg similarity index 100% rename from solitaire_engine/assets/themes/default/diamonds_10.svg rename to solitaire_engine/assets/themes/dark/diamonds_10.svg diff --git a/solitaire_engine/assets/themes/default/diamonds_2.svg b/solitaire_engine/assets/themes/dark/diamonds_2.svg similarity index 100% rename from solitaire_engine/assets/themes/default/diamonds_2.svg rename to solitaire_engine/assets/themes/dark/diamonds_2.svg diff --git a/solitaire_engine/assets/themes/default/diamonds_3.svg b/solitaire_engine/assets/themes/dark/diamonds_3.svg similarity index 100% rename from solitaire_engine/assets/themes/default/diamonds_3.svg rename to solitaire_engine/assets/themes/dark/diamonds_3.svg diff --git a/solitaire_engine/assets/themes/default/diamonds_4.svg b/solitaire_engine/assets/themes/dark/diamonds_4.svg similarity index 100% rename from solitaire_engine/assets/themes/default/diamonds_4.svg rename to solitaire_engine/assets/themes/dark/diamonds_4.svg diff --git a/solitaire_engine/assets/themes/default/diamonds_5.svg b/solitaire_engine/assets/themes/dark/diamonds_5.svg similarity index 100% rename from solitaire_engine/assets/themes/default/diamonds_5.svg rename to solitaire_engine/assets/themes/dark/diamonds_5.svg diff --git a/solitaire_engine/assets/themes/default/diamonds_6.svg b/solitaire_engine/assets/themes/dark/diamonds_6.svg similarity index 100% rename from solitaire_engine/assets/themes/default/diamonds_6.svg rename to solitaire_engine/assets/themes/dark/diamonds_6.svg diff --git a/solitaire_engine/assets/themes/default/diamonds_7.svg b/solitaire_engine/assets/themes/dark/diamonds_7.svg similarity index 100% rename from solitaire_engine/assets/themes/default/diamonds_7.svg rename to solitaire_engine/assets/themes/dark/diamonds_7.svg diff --git a/solitaire_engine/assets/themes/default/diamonds_8.svg b/solitaire_engine/assets/themes/dark/diamonds_8.svg similarity index 100% rename from solitaire_engine/assets/themes/default/diamonds_8.svg rename to solitaire_engine/assets/themes/dark/diamonds_8.svg diff --git a/solitaire_engine/assets/themes/default/diamonds_9.svg b/solitaire_engine/assets/themes/dark/diamonds_9.svg similarity index 100% rename from solitaire_engine/assets/themes/default/diamonds_9.svg rename to solitaire_engine/assets/themes/dark/diamonds_9.svg diff --git a/solitaire_engine/assets/themes/default/diamonds_ace.svg b/solitaire_engine/assets/themes/dark/diamonds_ace.svg similarity index 100% rename from solitaire_engine/assets/themes/default/diamonds_ace.svg rename to solitaire_engine/assets/themes/dark/diamonds_ace.svg diff --git a/solitaire_engine/assets/themes/default/diamonds_jack.svg b/solitaire_engine/assets/themes/dark/diamonds_jack.svg similarity index 100% rename from solitaire_engine/assets/themes/default/diamonds_jack.svg rename to solitaire_engine/assets/themes/dark/diamonds_jack.svg diff --git a/solitaire_engine/assets/themes/default/diamonds_king.svg b/solitaire_engine/assets/themes/dark/diamonds_king.svg similarity index 100% rename from solitaire_engine/assets/themes/default/diamonds_king.svg rename to solitaire_engine/assets/themes/dark/diamonds_king.svg diff --git a/solitaire_engine/assets/themes/default/diamonds_queen.svg b/solitaire_engine/assets/themes/dark/diamonds_queen.svg similarity index 100% rename from solitaire_engine/assets/themes/default/diamonds_queen.svg rename to solitaire_engine/assets/themes/dark/diamonds_queen.svg diff --git a/solitaire_engine/assets/themes/default/hearts_10.svg b/solitaire_engine/assets/themes/dark/hearts_10.svg similarity index 100% rename from solitaire_engine/assets/themes/default/hearts_10.svg rename to solitaire_engine/assets/themes/dark/hearts_10.svg diff --git a/solitaire_engine/assets/themes/default/hearts_2.svg b/solitaire_engine/assets/themes/dark/hearts_2.svg similarity index 100% rename from solitaire_engine/assets/themes/default/hearts_2.svg rename to solitaire_engine/assets/themes/dark/hearts_2.svg diff --git a/solitaire_engine/assets/themes/default/hearts_3.svg b/solitaire_engine/assets/themes/dark/hearts_3.svg similarity index 100% rename from solitaire_engine/assets/themes/default/hearts_3.svg rename to solitaire_engine/assets/themes/dark/hearts_3.svg diff --git a/solitaire_engine/assets/themes/default/hearts_4.svg b/solitaire_engine/assets/themes/dark/hearts_4.svg similarity index 100% rename from solitaire_engine/assets/themes/default/hearts_4.svg rename to solitaire_engine/assets/themes/dark/hearts_4.svg diff --git a/solitaire_engine/assets/themes/default/hearts_5.svg b/solitaire_engine/assets/themes/dark/hearts_5.svg similarity index 100% rename from solitaire_engine/assets/themes/default/hearts_5.svg rename to solitaire_engine/assets/themes/dark/hearts_5.svg diff --git a/solitaire_engine/assets/themes/default/hearts_6.svg b/solitaire_engine/assets/themes/dark/hearts_6.svg similarity index 100% rename from solitaire_engine/assets/themes/default/hearts_6.svg rename to solitaire_engine/assets/themes/dark/hearts_6.svg diff --git a/solitaire_engine/assets/themes/default/hearts_7.svg b/solitaire_engine/assets/themes/dark/hearts_7.svg similarity index 100% rename from solitaire_engine/assets/themes/default/hearts_7.svg rename to solitaire_engine/assets/themes/dark/hearts_7.svg diff --git a/solitaire_engine/assets/themes/default/hearts_8.svg b/solitaire_engine/assets/themes/dark/hearts_8.svg similarity index 100% rename from solitaire_engine/assets/themes/default/hearts_8.svg rename to solitaire_engine/assets/themes/dark/hearts_8.svg diff --git a/solitaire_engine/assets/themes/default/hearts_9.svg b/solitaire_engine/assets/themes/dark/hearts_9.svg similarity index 100% rename from solitaire_engine/assets/themes/default/hearts_9.svg rename to solitaire_engine/assets/themes/dark/hearts_9.svg diff --git a/solitaire_engine/assets/themes/default/hearts_ace.svg b/solitaire_engine/assets/themes/dark/hearts_ace.svg similarity index 100% rename from solitaire_engine/assets/themes/default/hearts_ace.svg rename to solitaire_engine/assets/themes/dark/hearts_ace.svg diff --git a/solitaire_engine/assets/themes/default/hearts_jack.svg b/solitaire_engine/assets/themes/dark/hearts_jack.svg similarity index 100% rename from solitaire_engine/assets/themes/default/hearts_jack.svg rename to solitaire_engine/assets/themes/dark/hearts_jack.svg diff --git a/solitaire_engine/assets/themes/default/hearts_king.svg b/solitaire_engine/assets/themes/dark/hearts_king.svg similarity index 100% rename from solitaire_engine/assets/themes/default/hearts_king.svg rename to solitaire_engine/assets/themes/dark/hearts_king.svg diff --git a/solitaire_engine/assets/themes/default/hearts_queen.svg b/solitaire_engine/assets/themes/dark/hearts_queen.svg similarity index 100% rename from solitaire_engine/assets/themes/default/hearts_queen.svg rename to solitaire_engine/assets/themes/dark/hearts_queen.svg diff --git a/solitaire_engine/assets/themes/default/spades_10.svg b/solitaire_engine/assets/themes/dark/spades_10.svg similarity index 100% rename from solitaire_engine/assets/themes/default/spades_10.svg rename to solitaire_engine/assets/themes/dark/spades_10.svg diff --git a/solitaire_engine/assets/themes/default/spades_2.svg b/solitaire_engine/assets/themes/dark/spades_2.svg similarity index 100% rename from solitaire_engine/assets/themes/default/spades_2.svg rename to solitaire_engine/assets/themes/dark/spades_2.svg diff --git a/solitaire_engine/assets/themes/default/spades_3.svg b/solitaire_engine/assets/themes/dark/spades_3.svg similarity index 100% rename from solitaire_engine/assets/themes/default/spades_3.svg rename to solitaire_engine/assets/themes/dark/spades_3.svg diff --git a/solitaire_engine/assets/themes/default/spades_4.svg b/solitaire_engine/assets/themes/dark/spades_4.svg similarity index 100% rename from solitaire_engine/assets/themes/default/spades_4.svg rename to solitaire_engine/assets/themes/dark/spades_4.svg diff --git a/solitaire_engine/assets/themes/default/spades_5.svg b/solitaire_engine/assets/themes/dark/spades_5.svg similarity index 100% rename from solitaire_engine/assets/themes/default/spades_5.svg rename to solitaire_engine/assets/themes/dark/spades_5.svg diff --git a/solitaire_engine/assets/themes/default/spades_6.svg b/solitaire_engine/assets/themes/dark/spades_6.svg similarity index 100% rename from solitaire_engine/assets/themes/default/spades_6.svg rename to solitaire_engine/assets/themes/dark/spades_6.svg diff --git a/solitaire_engine/assets/themes/default/spades_7.svg b/solitaire_engine/assets/themes/dark/spades_7.svg similarity index 100% rename from solitaire_engine/assets/themes/default/spades_7.svg rename to solitaire_engine/assets/themes/dark/spades_7.svg diff --git a/solitaire_engine/assets/themes/default/spades_8.svg b/solitaire_engine/assets/themes/dark/spades_8.svg similarity index 100% rename from solitaire_engine/assets/themes/default/spades_8.svg rename to solitaire_engine/assets/themes/dark/spades_8.svg diff --git a/solitaire_engine/assets/themes/default/spades_9.svg b/solitaire_engine/assets/themes/dark/spades_9.svg similarity index 100% rename from solitaire_engine/assets/themes/default/spades_9.svg rename to solitaire_engine/assets/themes/dark/spades_9.svg diff --git a/solitaire_engine/assets/themes/default/spades_ace.svg b/solitaire_engine/assets/themes/dark/spades_ace.svg similarity index 100% rename from solitaire_engine/assets/themes/default/spades_ace.svg rename to solitaire_engine/assets/themes/dark/spades_ace.svg diff --git a/solitaire_engine/assets/themes/default/spades_jack.svg b/solitaire_engine/assets/themes/dark/spades_jack.svg similarity index 100% rename from solitaire_engine/assets/themes/default/spades_jack.svg rename to solitaire_engine/assets/themes/dark/spades_jack.svg diff --git a/solitaire_engine/assets/themes/default/spades_king.svg b/solitaire_engine/assets/themes/dark/spades_king.svg similarity index 100% rename from solitaire_engine/assets/themes/default/spades_king.svg rename to solitaire_engine/assets/themes/dark/spades_king.svg diff --git a/solitaire_engine/assets/themes/default/spades_queen.svg b/solitaire_engine/assets/themes/dark/spades_queen.svg similarity index 100% rename from solitaire_engine/assets/themes/default/spades_queen.svg rename to solitaire_engine/assets/themes/dark/spades_queen.svg diff --git a/solitaire_engine/assets/themes/default/theme.ron b/solitaire_engine/assets/themes/dark/theme.ron similarity index 97% rename from solitaire_engine/assets/themes/default/theme.ron rename to solitaire_engine/assets/themes/dark/theme.ron index 3587c29..2c69c54 100644 --- a/solitaire_engine/assets/themes/default/theme.ron +++ b/solitaire_engine/assets/themes/dark/theme.ron @@ -13,9 +13,9 @@ // ranks: ace, 2, 3, 4, 5, 6, 7, 8, 9, 10, jack, queen, king ( meta: ( - id: "default", - name: "Default", - author: "Solitaire Quest", + id: "dark", + name: "Dark", + author: "Ferrous Solitaire", version: "0.1.0", card_aspect: (2, 3), ), diff --git a/solitaire_engine/src/assets/mod.rs b/solitaire_engine/src/assets/mod.rs index dad1634..c107205 100644 --- a/solitaire_engine/src/assets/mod.rs +++ b/solitaire_engine/src/assets/mod.rs @@ -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}; diff --git a/solitaire_engine/src/assets/sources.rs b/solitaire_engine/src/assets/sources.rs index ab6275a..967f796 100644 --- a/solitaire_engine/src/assets/sources.rs +++ b/solitaire_engine/src/assets/sources.rs @@ -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::() .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); } } diff --git a/solitaire_engine/src/lib.rs b/solitaire_engine/src/lib.rs index 9306689..235e1cd 100644 --- a/solitaire_engine/src/lib.rs +++ b/solitaire_engine/src/lib.rs @@ -53,8 +53,8 @@ pub mod weekly_goals_plugin; pub mod win_summary_plugin; pub use assets::{ - populate_embedded_default_theme, register_theme_asset_sources, AssetSourcesPlugin, - DEFAULT_THEME_MANIFEST_URL, USER_THEMES, + bundled_theme_url, populate_embedded_dark_theme, register_theme_asset_sources, + AssetSourcesPlugin, DARK_THEME_MANIFEST_URL, USER_THEMES, }; pub use theme::{ set_theme, ActiveTheme, CardTheme, CardThemeLoader, ThemeEntry, ThemePlugin, ThemeRegistry, diff --git a/solitaire_engine/src/theme/plugin.rs b/solitaire_engine/src/theme/plugin.rs index fcd6f20..c86f580 100644 --- a/solitaire_engine/src/theme/plugin.rs +++ b/solitaire_engine/src/theme/plugin.rs @@ -15,7 +15,7 @@ use bevy::prelude::*; use solitaire_core::card::{Rank, Suit}; use crate::assets::{ - default_theme_svg_bytes, rasterize_svg, user_theme_dir, DEFAULT_THEME_MANIFEST_URL, + bundled_theme_url, dark_theme_svg_bytes, rasterize_svg, user_theme_dir, }; use crate::card_plugin::CardImageSet; use crate::events::StateChangedEvent; @@ -126,12 +126,13 @@ fn load_initial_theme( settings: Option>, mut commands: Commands, ) { - let url = match settings.as_deref() { - Some(s) if s.0.selected_theme_id != "default" => { - format!("themes://{}/theme.ron", s.0.selected_theme_id) - } - _ => DEFAULT_THEME_MANIFEST_URL.to_string(), - }; + let id = settings + .as_deref() + .map(|s| s.0.selected_theme_id.as_str()) + .unwrap_or("dark"); + let url = bundled_theme_url(id) + .map(str::to_string) + .unwrap_or_else(|| format!("themes://{id}/theme.ron")); let handle: Handle = asset_server.load(url); commands.insert_resource(ActiveTheme(handle)); } @@ -161,11 +162,9 @@ fn react_to_settings_theme_change( return; } - let url = if new_id == "default" { - DEFAULT_THEME_MANIFEST_URL.to_string() - } else { - format!("themes://{new_id}/theme.ron") - }; + let url = bundled_theme_url(new_id) + .map(str::to_string) + .unwrap_or_else(|| format!("themes://{new_id}/theme.ron")); let handle: Handle = asset_server.load(url); commands.insert_resource(ActiveTheme(handle)); } @@ -305,16 +304,24 @@ const PREVIEW_BACK_FILENAME: &str = "back.svg"; /// Resolves the SVG bytes for one preview file (`back.svg` or /// `spades_ace.svg`) belonging to the named theme. /// -/// - For the bundled `default` theme, reads from the embedded -/// `DEFAULT_THEME_SVGS` table via [`default_theme_svg_bytes`]. No -/// filesystem I/O. -/// - For any user theme, reads from `//`. -/// Returns `None` for any I/O failure (file missing, permission -/// denied, etc.) — the caller treats `None` as "render placeholder". +/// - For the embedded `dark` theme, reads from the in-binary table via +/// [`dark_theme_svg_bytes`]. No filesystem I/O. +/// - For bundled non-embedded themes (e.g. `classic`), reads from the +/// `assets/themes//` directory. +/// - For user themes, reads from `//`. +/// Returns `None` for any I/O failure. fn read_theme_preview_svg_bytes(theme_id: &str, filename: &str) -> Option> { - if theme_id == "default" { - return default_theme_svg_bytes(filename).map(|b| b.to_vec()); + if theme_id == "dark" { + return dark_theme_svg_bytes(filename).map(|b| b.to_vec()); } + // Bundled non-embedded themes live alongside the binary in assets/. + let bundled_path = std::path::Path::new("assets/themes") + .join(theme_id) + .join(filename); + if let Ok(bytes) = std::fs::read(&bundled_path) { + return Some(bytes); + } + // Fall back to user theme dir. let path = user_theme_dir().join(theme_id).join(filename); std::fs::read(&path).ok() } @@ -503,22 +510,20 @@ mod tests { // set_theme that doesn't require an App. We assert the URL // shape so a future refactor doesn't accidentally change the // path layout. - let url = format!("themes://{}/theme.ron", "default"); - assert_eq!(url, "themes://default/theme.ron"); - let url2 = format!("themes://{}/theme.ron", "user_uploaded"); - assert_eq!(url2, "themes://user_uploaded/theme.ron"); + let url = format!("themes://{}/theme.ron", "user_uploaded"); + assert_eq!(url, "themes://user_uploaded/theme.ron"); } - /// Test 1: the bundled default theme always has embedded SVG bytes - /// available, so calling `generate_thumbnail_pair_for("default", …)` + /// Test 1: the bundled dark theme always has embedded SVG bytes + /// available, so calling `generate_thumbnail_pair_for("dark", …)` /// must produce two non-default `Handle` slots. #[test] - fn theme_thumbnails_generated_for_default_theme() { + fn theme_thumbnails_generated_for_dark_theme() { let mut images = Assets::::default(); - let pair = generate_thumbnail_pair_for("default", &mut images); + let pair = generate_thumbnail_pair_for("dark", &mut images); assert!( pair.is_fully_populated(), - "default theme must yield both ace + back thumbnail handles" + "dark theme must yield both ace + back thumbnail handles" ); // And the underlying images must actually exist in the assets // collection — the handles are real, not dangling. @@ -558,18 +563,17 @@ mod tests { ); } - /// `read_theme_preview_svg_bytes` for the default theme always - /// returns embedded bytes for the canonical preview pair — - /// covering the happy-path branch of the helper. + /// `read_theme_preview_svg_bytes` for the dark theme always returns + /// embedded bytes for the canonical preview pair. #[test] - fn read_default_theme_preview_returns_some_for_canonical_files() { + fn read_dark_theme_preview_returns_some_for_canonical_files() { assert!( - read_theme_preview_svg_bytes("default", PREVIEW_BACK_FILENAME).is_some(), - "default theme back.svg must be embedded" + read_theme_preview_svg_bytes("dark", PREVIEW_BACK_FILENAME).is_some(), + "dark theme back.svg must be embedded" ); assert!( - read_theme_preview_svg_bytes("default", PREVIEW_FACE_FILENAME).is_some(), - "default theme spades_ace.svg must be embedded" + read_theme_preview_svg_bytes("dark", PREVIEW_FACE_FILENAME).is_some(), + "dark theme spades_ace.svg must be embedded" ); } @@ -586,12 +590,12 @@ mod tests { app.init_resource::(); app.insert_resource(ThemeRegistry { entries: vec![crate::theme::ThemeEntry { - id: "default".into(), - display_name: "Default".into(), - manifest_url: crate::assets::DEFAULT_THEME_MANIFEST_URL.into(), + id: "dark".into(), + display_name: "Dark".into(), + manifest_url: crate::assets::DARK_THEME_MANIFEST_URL.into(), meta: ThemeMeta { - id: "default".into(), - name: "Default".into(), + id: "dark".into(), + name: "Dark".into(), author: "x".into(), version: "x".into(), card_aspect: (2, 3), @@ -605,18 +609,18 @@ mod tests { let first_ace = app .world() .resource::() - .get("default") + .get("dark") .map(|p| p.ace.clone()) - .expect("default theme thumbnail must exist after one tick"); + .expect("dark theme thumbnail must exist after one tick"); // Second tick must NOT replace the cached handle. app.update(); let second_ace = app .world() .resource::() - .get("default") + .get("dark") .map(|p| p.ace.clone()) - .expect("default theme thumbnail must still exist"); + .expect("dark theme thumbnail must still exist"); assert_eq!( first_ace.id(), diff --git a/solitaire_engine/src/theme/registry.rs b/solitaire_engine/src/theme/registry.rs index 62f1da2..3c32316 100644 --- a/solitaire_engine/src/theme/registry.rs +++ b/solitaire_engine/src/theme/registry.rs @@ -25,7 +25,7 @@ use bevy::prelude::{App, Plugin, Resource, Startup}; use serde::Deserialize; use super::ThemeMeta; -use crate::assets::{user_theme_dir, DEFAULT_THEME_MANIFEST_URL}; +use crate::assets::{user_theme_dir, DARK_THEME_MANIFEST_URL}; /// One entry in the [`ThemeRegistry`] — the data the picker UI needs /// to render a row and load the theme on selection. @@ -100,24 +100,23 @@ fn build_registry_on_startup(mut registry: bevy::ecs::system::ResMut ThemeRegistry { let mut entries = Vec::new(); - entries.push(default_entry()); entries.push(classic_entry()); + entries.push(dark_entry()); entries.extend(discover_user_themes(user_dir)); ThemeRegistry { entries } } -/// The bundled default theme entry — inserted unconditionally so the -/// picker always has at least one option. -fn default_entry() -> ThemeEntry { +/// The always-present embedded Dark theme entry. +fn dark_entry() -> ThemeEntry { ThemeEntry { - id: "default".to_string(), - display_name: "Default".to_string(), - manifest_url: DEFAULT_THEME_MANIFEST_URL.to_string(), + id: "dark".to_string(), + display_name: "Dark".to_string(), + manifest_url: DARK_THEME_MANIFEST_URL.to_string(), meta: ThemeMeta { - id: "default".to_string(), - name: "Default".to_string(), + id: "dark".to_string(), + name: "Dark".to_string(), author: "Ferrous Solitaire".to_string(), - version: "1.0".to_string(), + version: "1.0.0".to_string(), card_aspect: (2, 3), }, } @@ -265,8 +264,8 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let registry = build_registry(tmp.path()); assert_eq!(registry.len(), BUNDLED_COUNT); - assert_eq!(registry.entries[0].id, "default"); - assert_eq!(registry.entries[1].id, "classic"); + assert_eq!(registry.entries[0].id, "classic"); + assert_eq!(registry.entries[1].id, "dark"); } #[test] @@ -275,7 +274,8 @@ mod tests { "/definitely/not/a/real/path/should/not/panic", )); assert_eq!(registry.len(), BUNDLED_COUNT); - assert_eq!(registry.entries[0].id, "default"); + assert!(registry.find("classic").is_some()); + assert!(registry.find("dark").is_some()); } #[test] @@ -333,7 +333,7 @@ mod tests { let registry = build_registry(tmp.path()); assert_eq!(registry.len(), BUNDLED_COUNT, "escape attempt must not register"); - assert_eq!(registry.entries[0].id, "default"); + assert!(registry.find("classic").is_some()); } #[test] @@ -373,15 +373,13 @@ mod tests { refresh_registry(&mut registry, tmp.path()); assert_eq!(registry.len(), BUNDLED_COUNT); - assert_eq!(registry.entries[0].id, "default"); + assert!(registry.find("classic").is_some()); assert!(registry.find("stale").is_none()); } #[test] - fn default_entry_url_matches_embedded_constant() { - // Ensures the picker always gets a URL it can hand to the - // asset server for the bundled theme. - let entry = default_entry(); - assert_eq!(entry.manifest_url, DEFAULT_THEME_MANIFEST_URL); + fn dark_entry_url_matches_embedded_constant() { + let entry = dark_entry(); + assert_eq!(entry.manifest_url, DARK_THEME_MANIFEST_URL); } }