Revert "feat(engine): bundle Rusty Pixel as a built-in theme"
This reverts commit 21ec03b157.
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
@@ -1,76 +0,0 @@
|
|||||||
// Rusty Pixel — pixel-art card theme generated via Claude Design.
|
|
||||||
//
|
|
||||||
// 53 PNGs at 256×384 (4× nearest-neighbor upscale of the source
|
|
||||||
// 64×96 grid). Card aspect 2:3 matches the engine's layout
|
|
||||||
// assumption. Drop this directory under
|
|
||||||
// `<data_dir>/solitaire_quest/themes/rusty-pixel/` and the theme
|
|
||||||
// registry picks it up at next launch.
|
|
||||||
(
|
|
||||||
meta: (
|
|
||||||
id: "rusty-pixel",
|
|
||||||
name: "Rusty Pixel",
|
|
||||||
author: "Claude Design",
|
|
||||||
version: "0.1.0",
|
|
||||||
card_aspect: (2, 3),
|
|
||||||
// Opt in to nearest-neighbor sampling so the pixel grid stays
|
|
||||||
// crisp at non-integer scales (Bevy's default bilinear filter
|
|
||||||
// mushes 256x384 pixel art when displayed at ~150x200 on a
|
|
||||||
// typical desktop window).
|
|
||||||
pixel_art: true,
|
|
||||||
),
|
|
||||||
back: "back.png",
|
|
||||||
faces: {
|
|
||||||
"clubs_ace": "clubs_ace.png",
|
|
||||||
"clubs_2": "clubs_2.png",
|
|
||||||
"clubs_3": "clubs_3.png",
|
|
||||||
"clubs_4": "clubs_4.png",
|
|
||||||
"clubs_5": "clubs_5.png",
|
|
||||||
"clubs_6": "clubs_6.png",
|
|
||||||
"clubs_7": "clubs_7.png",
|
|
||||||
"clubs_8": "clubs_8.png",
|
|
||||||
"clubs_9": "clubs_9.png",
|
|
||||||
"clubs_10": "clubs_10.png",
|
|
||||||
"clubs_jack": "clubs_jack.png",
|
|
||||||
"clubs_queen": "clubs_queen.png",
|
|
||||||
"clubs_king": "clubs_king.png",
|
|
||||||
"diamonds_ace": "diamonds_ace.png",
|
|
||||||
"diamonds_2": "diamonds_2.png",
|
|
||||||
"diamonds_3": "diamonds_3.png",
|
|
||||||
"diamonds_4": "diamonds_4.png",
|
|
||||||
"diamonds_5": "diamonds_5.png",
|
|
||||||
"diamonds_6": "diamonds_6.png",
|
|
||||||
"diamonds_7": "diamonds_7.png",
|
|
||||||
"diamonds_8": "diamonds_8.png",
|
|
||||||
"diamonds_9": "diamonds_9.png",
|
|
||||||
"diamonds_10": "diamonds_10.png",
|
|
||||||
"diamonds_jack": "diamonds_jack.png",
|
|
||||||
"diamonds_queen": "diamonds_queen.png",
|
|
||||||
"diamonds_king": "diamonds_king.png",
|
|
||||||
"hearts_ace": "hearts_ace.png",
|
|
||||||
"hearts_2": "hearts_2.png",
|
|
||||||
"hearts_3": "hearts_3.png",
|
|
||||||
"hearts_4": "hearts_4.png",
|
|
||||||
"hearts_5": "hearts_5.png",
|
|
||||||
"hearts_6": "hearts_6.png",
|
|
||||||
"hearts_7": "hearts_7.png",
|
|
||||||
"hearts_8": "hearts_8.png",
|
|
||||||
"hearts_9": "hearts_9.png",
|
|
||||||
"hearts_10": "hearts_10.png",
|
|
||||||
"hearts_jack": "hearts_jack.png",
|
|
||||||
"hearts_queen": "hearts_queen.png",
|
|
||||||
"hearts_king": "hearts_king.png",
|
|
||||||
"spades_ace": "spades_ace.png",
|
|
||||||
"spades_2": "spades_2.png",
|
|
||||||
"spades_3": "spades_3.png",
|
|
||||||
"spades_4": "spades_4.png",
|
|
||||||
"spades_5": "spades_5.png",
|
|
||||||
"spades_6": "spades_6.png",
|
|
||||||
"spades_7": "spades_7.png",
|
|
||||||
"spades_8": "spades_8.png",
|
|
||||||
"spades_9": "spades_9.png",
|
|
||||||
"spades_10": "spades_10.png",
|
|
||||||
"spades_jack": "spades_jack.png",
|
|
||||||
"spades_queen": "spades_queen.png",
|
|
||||||
"spades_king": "spades_king.png",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -11,10 +11,8 @@ pub mod svg_loader;
|
|||||||
pub mod user_dir;
|
pub mod user_dir;
|
||||||
|
|
||||||
pub use sources::{
|
pub use sources::{
|
||||||
default_theme_svg_bytes, populate_embedded_default_theme,
|
default_theme_svg_bytes, populate_embedded_default_theme, register_theme_asset_sources,
|
||||||
populate_embedded_rusty_pixel_theme, register_theme_asset_sources,
|
AssetSourcesPlugin, DEFAULT_THEME_MANIFEST_URL, USER_THEMES,
|
||||||
rusty_pixel_theme_png_bytes, AssetSourcesPlugin, DEFAULT_THEME_MANIFEST_URL,
|
|
||||||
RUSTY_PIXEL_THEME_MANIFEST_URL, USER_THEMES,
|
|
||||||
};
|
};
|
||||||
pub use svg_loader::{rasterize_svg, SvgLoader, SvgLoaderError, SvgLoaderSettings};
|
pub use svg_loader::{rasterize_svg, SvgLoader, SvgLoaderError, SvgLoaderSettings};
|
||||||
pub use user_dir::{set_user_theme_dir, user_theme_dir};
|
pub use user_dir::{set_user_theme_dir, user_theme_dir};
|
||||||
|
|||||||
@@ -155,100 +155,6 @@ const DEFAULT_THEME_SVGS: &[(&str, &[u8])] = &[
|
|||||||
embed_default_svg!("spades_king.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*
|
/// Registers asset sources that must be in place *before*
|
||||||
/// `AssetPlugin` is built.
|
/// `AssetPlugin` is built.
|
||||||
///
|
///
|
||||||
@@ -285,7 +191,6 @@ pub struct AssetSourcesPlugin;
|
|||||||
impl Plugin for AssetSourcesPlugin {
|
impl Plugin for AssetSourcesPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
populate_embedded_default_theme(app);
|
populate_embedded_default_theme(app);
|
||||||
populate_embedded_rusty_pixel_theme(app);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,43 +254,6 @@ pub fn populate_embedded_default_theme(app: &mut App) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ use bevy::prelude::*;
|
|||||||
use solitaire_core::card::{Rank, Suit};
|
use solitaire_core::card::{Rank, Suit};
|
||||||
|
|
||||||
use crate::assets::{
|
use crate::assets::{
|
||||||
default_theme_svg_bytes, rasterize_svg, rusty_pixel_theme_png_bytes, user_theme_dir,
|
default_theme_svg_bytes, rasterize_svg, user_theme_dir, DEFAULT_THEME_MANIFEST_URL,
|
||||||
DEFAULT_THEME_MANIFEST_URL, RUSTY_PIXEL_THEME_MANIFEST_URL,
|
|
||||||
};
|
};
|
||||||
use crate::card_plugin::CardImageSet;
|
use crate::card_plugin::CardImageSet;
|
||||||
use crate::events::StateChangedEvent;
|
use crate::events::StateChangedEvent;
|
||||||
@@ -129,32 +128,16 @@ fn load_initial_theme(
|
|||||||
settings: Option<Res<crate::settings_plugin::SettingsResource>>,
|
settings: Option<Res<crate::settings_plugin::SettingsResource>>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
) {
|
) {
|
||||||
let id = settings
|
let url = match settings.as_deref() {
|
||||||
.as_deref()
|
Some(s) if s.0.selected_theme_id != "default" => {
|
||||||
.map(|s| s.0.selected_theme_id.as_str())
|
format!("themes://{}/theme.ron", s.0.selected_theme_id)
|
||||||
.unwrap_or("default");
|
}
|
||||||
let url = manifest_url_for(id);
|
_ => DEFAULT_THEME_MANIFEST_URL.to_string(),
|
||||||
|
};
|
||||||
let handle: Handle<CardTheme> = asset_server.load(url);
|
let handle: Handle<CardTheme> = asset_server.load(url);
|
||||||
commands.insert_resource(ActiveTheme(handle));
|
commands.insert_resource(ActiveTheme(handle));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolves a theme id to its manifest asset URL.
|
|
||||||
///
|
|
||||||
/// Bundled built-ins (default, rusty-pixel) route to `embedded://`
|
|
||||||
/// so the binary's compile-time-baked manifest + face files load
|
|
||||||
/// without touching disk. Anything else routes to `themes://`,
|
|
||||||
/// which `register_theme_asset_sources` points at the user themes
|
|
||||||
/// directory. Callers (load_initial_theme,
|
|
||||||
/// react_to_settings_theme_change) consult this helper instead of
|
|
||||||
/// hard-coding the URL shape per id.
|
|
||||||
fn manifest_url_for(theme_id: &str) -> String {
|
|
||||||
match theme_id {
|
|
||||||
"default" => DEFAULT_THEME_MANIFEST_URL.to_string(),
|
|
||||||
"rusty-pixel" => RUSTY_PIXEL_THEME_MANIFEST_URL.to_string(),
|
|
||||||
_ => format!("themes://{theme_id}/theme.ron"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Watches [`crate::settings_plugin::SettingsChangedEvent`] and
|
/// Watches [`crate::settings_plugin::SettingsChangedEvent`] and
|
||||||
/// triggers a fresh theme load whenever
|
/// triggers a fresh theme load whenever
|
||||||
/// `Settings::selected_theme_id` changes. The settings panel's theme
|
/// `Settings::selected_theme_id` changes. The settings panel's theme
|
||||||
@@ -180,7 +163,11 @@ fn react_to_settings_theme_change(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = manifest_url_for(new_id);
|
let url = if new_id == "default" {
|
||||||
|
DEFAULT_THEME_MANIFEST_URL.to_string()
|
||||||
|
} else {
|
||||||
|
format!("themes://{new_id}/theme.ron")
|
||||||
|
};
|
||||||
let handle: Handle<CardTheme> = asset_server.load(url);
|
let handle: Handle<CardTheme> = asset_server.load(url);
|
||||||
commands.insert_resource(ActiveTheme(handle));
|
commands.insert_resource(ActiveTheme(handle));
|
||||||
}
|
}
|
||||||
@@ -375,20 +362,11 @@ enum ThemePreviewBytes {
|
|||||||
/// `<basename>.svg` then `<basename>.png`. Either branch returns
|
/// `<basename>.svg` then `<basename>.png`. Either branch returns
|
||||||
/// `None` on I/O failure (file missing, permission denied, etc.).
|
/// `None` on I/O failure (file missing, permission denied, etc.).
|
||||||
fn read_theme_preview_bytes(theme_id: &str, basename: &str) -> Option<ThemePreviewBytes> {
|
fn read_theme_preview_bytes(theme_id: &str, basename: &str) -> Option<ThemePreviewBytes> {
|
||||||
// Bundled built-ins consult their embed tables before any
|
|
||||||
// filesystem I/O so the thumbnail works on a fresh install where
|
|
||||||
// the user themes directory doesn't exist yet.
|
|
||||||
if theme_id == "default" {
|
if theme_id == "default" {
|
||||||
let filename = format!("{basename}.svg");
|
let filename = format!("{basename}.svg");
|
||||||
return default_theme_svg_bytes(&filename)
|
return default_theme_svg_bytes(&filename)
|
||||||
.map(|b| ThemePreviewBytes::Svg(b.to_vec()));
|
.map(|b| ThemePreviewBytes::Svg(b.to_vec()));
|
||||||
}
|
}
|
||||||
if theme_id == "rusty-pixel" {
|
|
||||||
let filename = format!("{basename}.png");
|
|
||||||
if let Some(bytes) = rusty_pixel_theme_png_bytes(&filename) {
|
|
||||||
return Some(ThemePreviewBytes::Png(bytes.to_vec()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let dir = user_theme_dir().join(theme_id);
|
let dir = user_theme_dir().join(theme_id);
|
||||||
if let Ok(bytes) = std::fs::read(dir.join(format!("{basename}.svg"))) {
|
if let Ok(bytes) = std::fs::read(dir.join(format!("{basename}.svg"))) {
|
||||||
return Some(ThemePreviewBytes::Svg(bytes));
|
return Some(ThemePreviewBytes::Svg(bytes));
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ use bevy::prelude::{App, Plugin, Resource, Startup};
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::ThemeMeta;
|
use super::ThemeMeta;
|
||||||
use crate::assets::{user_theme_dir, DEFAULT_THEME_MANIFEST_URL, RUSTY_PIXEL_THEME_MANIFEST_URL};
|
use crate::assets::{user_theme_dir, DEFAULT_THEME_MANIFEST_URL};
|
||||||
|
|
||||||
/// One entry in the [`ThemeRegistry`] — the data the picker UI needs
|
/// One entry in the [`ThemeRegistry`] — the data the picker UI needs
|
||||||
/// to render a row and load the theme on selection.
|
/// to render a row and load the theme on selection.
|
||||||
@@ -98,24 +98,10 @@ fn build_registry_on_startup(mut registry: bevy::ecs::system::ResMut<ThemeRegist
|
|||||||
/// Pure helper: builds a registry given an explicit user-themes
|
/// Pure helper: builds a registry given an explicit user-themes
|
||||||
/// directory. Tests pass a temp dir; production uses
|
/// directory. Tests pass a temp dir; production uses
|
||||||
/// [`user_theme_dir`].
|
/// [`user_theme_dir`].
|
||||||
///
|
|
||||||
/// Order: bundled built-ins first (default, then rusty-pixel), then
|
|
||||||
/// user themes in `read_dir` order. User themes whose `id` collides
|
|
||||||
/// with a bundled built-in are silently dropped — built-ins win the
|
|
||||||
/// collision because they're guaranteed to be a complete, valid set;
|
|
||||||
/// the user's overriding copy may be partial or stale and silently
|
|
||||||
/// preferring a complete-but-different theme is less surprising than
|
|
||||||
/// crashing on a missing face.
|
|
||||||
pub fn build_registry(user_dir: &Path) -> ThemeRegistry {
|
pub fn build_registry(user_dir: &Path) -> ThemeRegistry {
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
entries.push(default_entry());
|
entries.push(default_entry());
|
||||||
entries.push(rusty_pixel_entry());
|
entries.extend(discover_user_themes(user_dir));
|
||||||
let bundled_ids: std::collections::HashSet<String> =
|
|
||||||
entries.iter().map(|e| e.id.clone()).collect();
|
|
||||||
let user = discover_user_themes(user_dir)
|
|
||||||
.into_iter()
|
|
||||||
.filter(|t| !bundled_ids.contains(&t.id));
|
|
||||||
entries.extend(user);
|
|
||||||
ThemeRegistry { entries }
|
ThemeRegistry { entries }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,26 +123,6 @@ fn default_entry() -> ThemeEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The bundled rusty-pixel theme entry — pixel-art faces by Claude
|
|
||||||
/// Design, embedded under `embedded://solitaire_engine/assets/themes/rusty-pixel/`.
|
|
||||||
/// Inserted alongside the default so the picker offers both
|
|
||||||
/// out-of-the-box on a fresh install with no user themes directory.
|
|
||||||
fn rusty_pixel_entry() -> ThemeEntry {
|
|
||||||
ThemeEntry {
|
|
||||||
id: "rusty-pixel".to_string(),
|
|
||||||
display_name: "Rusty Pixel".to_string(),
|
|
||||||
manifest_url: RUSTY_PIXEL_THEME_MANIFEST_URL.to_string(),
|
|
||||||
meta: ThemeMeta {
|
|
||||||
id: "rusty-pixel".to_string(),
|
|
||||||
name: "Rusty Pixel".to_string(),
|
|
||||||
author: "Claude Design".to_string(),
|
|
||||||
version: "0.1.0".to_string(),
|
|
||||||
card_aspect: (2, 3),
|
|
||||||
pixel_art: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Walks `user_dir`, treating every immediate subdirectory as a
|
/// Walks `user_dir`, treating every immediate subdirectory as a
|
||||||
/// candidate theme. A subdirectory contributes one entry if and only
|
/// candidate theme. A subdirectory contributes one entry if and only
|
||||||
/// if it contains a `theme.ron` whose `meta` block parses cleanly and
|
/// if it contains a `theme.ron` whose `meta` block parses cleanly and
|
||||||
@@ -274,22 +240,20 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_user_dir_yields_only_the_bundled_built_ins() {
|
fn empty_user_dir_yields_only_the_default_entry() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let registry = build_registry(tmp.path());
|
let registry = build_registry(tmp.path());
|
||||||
assert_eq!(registry.len(), 2, "default + rusty-pixel always present");
|
assert_eq!(registry.len(), 1);
|
||||||
assert_eq!(registry.entries[0].id, "default");
|
assert_eq!(registry.entries[0].id, "default");
|
||||||
assert_eq!(registry.entries[1].id, "rusty-pixel");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nonexistent_user_dir_still_yields_bundled_built_ins() {
|
fn nonexistent_user_dir_still_yields_default() {
|
||||||
let registry = build_registry(Path::new(
|
let registry = build_registry(Path::new(
|
||||||
"/definitely/not/a/real/path/should/not/panic",
|
"/definitely/not/a/real/path/should/not/panic",
|
||||||
));
|
));
|
||||||
assert_eq!(registry.len(), 2);
|
assert_eq!(registry.len(), 1);
|
||||||
assert_eq!(registry.entries[0].id, "default");
|
assert_eq!(registry.entries[0].id, "default");
|
||||||
assert_eq!(registry.entries[1].id, "rusty-pixel");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -300,40 +264,12 @@ mod tests {
|
|||||||
write_manifest(&theme_dir, "midnight", "Midnight");
|
write_manifest(&theme_dir, "midnight", "Midnight");
|
||||||
|
|
||||||
let registry = build_registry(tmp.path());
|
let registry = build_registry(tmp.path());
|
||||||
assert_eq!(registry.len(), 3, "default + rusty-pixel + midnight");
|
assert_eq!(registry.len(), 2);
|
||||||
let entry = registry.find("midnight").expect("midnight registered");
|
let entry = registry.find("midnight").expect("midnight registered");
|
||||||
assert_eq!(entry.display_name, "Midnight");
|
assert_eq!(entry.display_name, "Midnight");
|
||||||
assert_eq!(entry.manifest_url, "themes://midnight/theme.ron");
|
assert_eq!(entry.manifest_url, "themes://midnight/theme.ron");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn user_theme_id_collision_with_bundled_is_dropped() {
|
|
||||||
// A user-supplied directory whose `id` matches a bundled
|
|
||||||
// built-in (rusty-pixel) must not produce a duplicate
|
|
||||||
// registry entry. The bundled version wins because it's
|
|
||||||
// guaranteed complete; the user's overriding copy may be
|
|
||||||
// partial, stale, or otherwise broken.
|
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
|
||||||
let theme_dir = tmp.path().join("rusty-pixel");
|
|
||||||
fs::create_dir_all(&theme_dir).unwrap();
|
|
||||||
write_manifest(&theme_dir, "rusty-pixel", "User Override");
|
|
||||||
|
|
||||||
let registry = build_registry(tmp.path());
|
|
||||||
assert_eq!(
|
|
||||||
registry.len(), 2,
|
|
||||||
"user override of bundled id must not appear as a duplicate",
|
|
||||||
);
|
|
||||||
let entry = registry.find("rusty-pixel").expect("rusty-pixel registered");
|
|
||||||
assert_eq!(
|
|
||||||
entry.display_name, "Rusty Pixel",
|
|
||||||
"bundled entry's display_name wins over the user override",
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
entry.manifest_url, RUSTY_PIXEL_THEME_MANIFEST_URL,
|
|
||||||
"bundled embed:// URL wins over the user themes:// URL",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn full_manifest_also_works_via_meta_only_parser() {
|
fn full_manifest_also_works_via_meta_only_parser() {
|
||||||
// The meta-only deserialiser must tolerate the full ThemeManifest
|
// The meta-only deserialiser must tolerate the full ThemeManifest
|
||||||
@@ -374,9 +310,8 @@ mod tests {
|
|||||||
write_manifest(&theme_dir, "../etc/passwd", "Evil");
|
write_manifest(&theme_dir, "../etc/passwd", "Evil");
|
||||||
|
|
||||||
let registry = build_registry(tmp.path());
|
let registry = build_registry(tmp.path());
|
||||||
assert_eq!(registry.len(), 2, "escape attempt must not register; built-ins remain");
|
assert_eq!(registry.len(), 1, "escape attempt must not register");
|
||||||
assert_eq!(registry.entries[0].id, "default");
|
assert_eq!(registry.entries[0].id, "default");
|
||||||
assert_eq!(registry.entries[1].id, "rusty-pixel");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -387,11 +322,7 @@ mod tests {
|
|||||||
fs::write(lonely.join("readme.md"), "wrong filename").unwrap();
|
fs::write(lonely.join("readme.md"), "wrong filename").unwrap();
|
||||||
|
|
||||||
let registry = build_registry(tmp.path());
|
let registry = build_registry(tmp.path());
|
||||||
assert_eq!(
|
assert_eq!(registry.len(), 1);
|
||||||
registry.len(),
|
|
||||||
2,
|
|
||||||
"no user themes register; only the bundled built-ins remain",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -420,9 +351,8 @@ mod tests {
|
|||||||
|
|
||||||
refresh_registry(&mut registry, tmp.path());
|
refresh_registry(&mut registry, tmp.path());
|
||||||
|
|
||||||
assert_eq!(registry.len(), 2, "stale entry replaced; built-ins remain");
|
assert_eq!(registry.len(), 1);
|
||||||
assert_eq!(registry.entries[0].id, "default");
|
assert_eq!(registry.entries[0].id, "default");
|
||||||
assert_eq!(registry.entries[1].id, "rusty-pixel");
|
|
||||||
assert!(registry.find("stale").is_none());
|
assert!(registry.find("stale").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||