Revert "feat(engine): bundle Rusty Pixel as a built-in theme"
This reverts commit 21ec03b157.
This commit is contained in:
@@ -17,8 +17,7 @@ use bevy::prelude::*;
|
||||
use solitaire_core::card::{Rank, Suit};
|
||||
|
||||
use crate::assets::{
|
||||
default_theme_svg_bytes, rasterize_svg, rusty_pixel_theme_png_bytes, user_theme_dir,
|
||||
DEFAULT_THEME_MANIFEST_URL, RUSTY_PIXEL_THEME_MANIFEST_URL,
|
||||
default_theme_svg_bytes, rasterize_svg, user_theme_dir, DEFAULT_THEME_MANIFEST_URL,
|
||||
};
|
||||
use crate::card_plugin::CardImageSet;
|
||||
use crate::events::StateChangedEvent;
|
||||
@@ -129,32 +128,16 @@ fn load_initial_theme(
|
||||
settings: Option<Res<crate::settings_plugin::SettingsResource>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
let id = settings
|
||||
.as_deref()
|
||||
.map(|s| s.0.selected_theme_id.as_str())
|
||||
.unwrap_or("default");
|
||||
let url = manifest_url_for(id);
|
||||
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 handle: Handle<CardTheme> = asset_server.load(url);
|
||||
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
|
||||
/// triggers a fresh theme load whenever
|
||||
/// `Settings::selected_theme_id` changes. The settings panel's theme
|
||||
@@ -180,7 +163,11 @@ fn react_to_settings_theme_change(
|
||||
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);
|
||||
commands.insert_resource(ActiveTheme(handle));
|
||||
}
|
||||
@@ -375,20 +362,11 @@ enum ThemePreviewBytes {
|
||||
/// `<basename>.svg` then `<basename>.png`. Either branch returns
|
||||
/// `None` on I/O failure (file missing, permission denied, etc.).
|
||||
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" {
|
||||
let filename = format!("{basename}.svg");
|
||||
return default_theme_svg_bytes(&filename)
|
||||
.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);
|
||||
if let Ok(bytes) = std::fs::read(dir.join(format!("{basename}.svg"))) {
|
||||
return Some(ThemePreviewBytes::Svg(bytes));
|
||||
|
||||
@@ -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, 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
|
||||
/// 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
|
||||
/// directory. Tests pass a temp dir; production uses
|
||||
/// [`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 {
|
||||
let mut entries = Vec::new();
|
||||
entries.push(default_entry());
|
||||
entries.push(rusty_pixel_entry());
|
||||
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);
|
||||
entries.extend(discover_user_themes(user_dir));
|
||||
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
|
||||
/// candidate theme. A subdirectory contributes one entry if and only
|
||||
/// if it contains a `theme.ron` whose `meta` block parses cleanly and
|
||||
@@ -274,22 +240,20 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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 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[1].id, "rusty-pixel");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonexistent_user_dir_still_yields_bundled_built_ins() {
|
||||
fn nonexistent_user_dir_still_yields_default() {
|
||||
let registry = build_registry(Path::new(
|
||||
"/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[1].id, "rusty-pixel");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -300,40 +264,12 @@ mod tests {
|
||||
write_manifest(&theme_dir, "midnight", "Midnight");
|
||||
|
||||
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");
|
||||
assert_eq!(entry.display_name, "Midnight");
|
||||
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]
|
||||
fn full_manifest_also_works_via_meta_only_parser() {
|
||||
// The meta-only deserialiser must tolerate the full ThemeManifest
|
||||
@@ -374,9 +310,8 @@ mod tests {
|
||||
write_manifest(&theme_dir, "../etc/passwd", "Evil");
|
||||
|
||||
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[1].id, "rusty-pixel");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -387,11 +322,7 @@ mod tests {
|
||||
fs::write(lonely.join("readme.md"), "wrong filename").unwrap();
|
||||
|
||||
let registry = build_registry(tmp.path());
|
||||
assert_eq!(
|
||||
registry.len(),
|
||||
2,
|
||||
"no user themes register; only the bundled built-ins remain",
|
||||
);
|
||||
assert_eq!(registry.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -420,9 +351,8 @@ mod tests {
|
||||
|
||||
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[1].id, "rusty-pixel");
|
||||
assert!(registry.find("stale").is_none());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user