feat(engine): rename themes — Classic is default, Dark replaces Default
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>
@@ -143,11 +143,10 @@ pub struct Settings {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub window_geometry: Option<WindowGeometry>,
|
pub window_geometry: Option<WindowGeometry>,
|
||||||
/// Identifier of the active card-art theme. Matches `meta.id` from
|
/// Identifier of the active card-art theme. Matches `meta.id` from
|
||||||
/// the theme's `theme.ron` manifest. `"default"` is the bundled
|
/// the theme's `theme.ron` manifest. `"classic"` and `"dark"` are
|
||||||
/// theme and is always present in the registry; user-supplied
|
/// always present; user-supplied themes register under their own ids.
|
||||||
/// themes register under their own ids when they're imported.
|
/// Older `settings.json` files that stored `"default"` will fall
|
||||||
/// Older `settings.json` files default cleanly to `"default"` via
|
/// back to the dark embedded theme at runtime.
|
||||||
/// `#[serde(default = ...)]`.
|
|
||||||
#[serde(default = "default_theme_id")]
|
#[serde(default = "default_theme_id")]
|
||||||
pub selected_theme_id: String,
|
pub selected_theme_id: String,
|
||||||
/// Set to `true` once the achievement-onboarding info-toast has been
|
/// 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 {
|
fn default_theme_id() -> String {
|
||||||
"default".to_string()
|
"classic".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default tooltip-hover dwell delay in seconds. Mirrors
|
/// Default tooltip-hover dwell delay in seconds. Mirrors
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 956 B After Width: | Height: | Size: 956 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -13,9 +13,9 @@
|
|||||||
// ranks: ace, 2, 3, 4, 5, 6, 7, 8, 9, 10, jack, queen, king
|
// ranks: ace, 2, 3, 4, 5, 6, 7, 8, 9, 10, jack, queen, king
|
||||||
(
|
(
|
||||||
meta: (
|
meta: (
|
||||||
id: "default",
|
id: "dark",
|
||||||
name: "Default",
|
name: "Dark",
|
||||||
author: "Solitaire Quest",
|
author: "Ferrous Solitaire",
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
card_aspect: (2, 3),
|
card_aspect: (2, 3),
|
||||||
),
|
),
|
||||||
@@ -11,8 +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, register_theme_asset_sources,
|
bundled_theme_url, dark_theme_svg_bytes, populate_embedded_dark_theme,
|
||||||
AssetSourcesPlugin, DEFAULT_THEME_MANIFEST_URL, USER_THEMES,
|
register_theme_asset_sources, AssetSourcesPlugin, DARK_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};
|
||||||
|
|||||||
@@ -60,99 +60,89 @@ use crate::assets::user_dir::user_theme_dir;
|
|||||||
/// from the user-themes directory.
|
/// from the user-themes directory.
|
||||||
pub const USER_THEMES: &str = "themes";
|
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
|
/// Code that wants to load the embedded Dark theme — including
|
||||||
/// Phase 4 `ActiveTheme` initialisation — should use exactly this
|
/// `ActiveTheme` initialisation — should use exactly this constant
|
||||||
/// constant rather than re-typing the URL inline. Changing where the
|
/// rather than re-typing the URL inline.
|
||||||
/// default theme lives in the asset graph then becomes a single-line
|
pub const DARK_THEME_MANIFEST_URL: &str =
|
||||||
/// change in this file.
|
"embedded://solitaire_engine/assets/themes/dark/theme.ron";
|
||||||
pub const DEFAULT_THEME_MANIFEST_URL: &str =
|
|
||||||
"embedded://solitaire_engine/assets/themes/default/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
|
/// to the `embedded://` source root. Kept in lockstep with
|
||||||
/// [`DEFAULT_THEME_MANIFEST_URL`] by the unit test
|
/// [`DARK_THEME_MANIFEST_URL`] by the unit test
|
||||||
/// `default_theme_url_constant_matches_embedded_path`.
|
/// `dark_theme_url_constant_matches_embedded_path`.
|
||||||
const DEFAULT_THEME_MANIFEST_PATH: &str = "solitaire_engine/assets/themes/default/theme.ron";
|
const DARK_THEME_MANIFEST_PATH: &str = "solitaire_engine/assets/themes/dark/theme.ron";
|
||||||
|
|
||||||
/// Bytes of the bundled default theme manifest. Embedded at compile
|
/// Bytes of the bundled Dark theme manifest, embedded at compile time.
|
||||||
/// time via `include_bytes!` so the binary is self-contained even if
|
const DARK_THEME_MANIFEST_BYTES: &[u8] =
|
||||||
/// the workspace's `solitaire_engine/assets/` directory is absent at
|
include_bytes!("../../assets/themes/dark/theme.ron");
|
||||||
/// runtime (e.g. when shipped to a player).
|
|
||||||
const DEFAULT_THEME_MANIFEST_BYTES: &[u8] =
|
|
||||||
include_bytes!("../../assets/themes/default/theme.ron");
|
|
||||||
|
|
||||||
/// Generates a `(stable_path, bytes)` entry for one default-theme
|
/// Generates a `(stable_path, bytes)` entry for one Dark-theme SVG.
|
||||||
/// SVG so the bulk-embed table below stays declarative. The path
|
macro_rules! embed_dark_svg {
|
||||||
/// matches what `theme.ron` references; `include_bytes!` resolves
|
|
||||||
/// relative to this source file.
|
|
||||||
macro_rules! embed_default_svg {
|
|
||||||
($name:literal) => {
|
($name:literal) => {
|
||||||
(
|
(
|
||||||
concat!("solitaire_engine/assets/themes/default/", $name),
|
concat!("solitaire_engine/assets/themes/dark/", $name),
|
||||||
include_bytes!(concat!("../../assets/themes/default/", $name)) as &[u8],
|
include_bytes!(concat!("../../assets/themes/dark/", $name)) as &[u8],
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every default-theme SVG file bundled into the binary. Adding a new
|
/// Every Dark-theme SVG file bundled into the binary.
|
||||||
/// face / back artwork is a single `embed_default_svg!(...)` line —
|
const DARK_THEME_SVGS: &[(&str, &[u8])] = &[
|
||||||
/// the populate function below iterates this table.
|
embed_dark_svg!("back.svg"),
|
||||||
const DEFAULT_THEME_SVGS: &[(&str, &[u8])] = &[
|
embed_dark_svg!("clubs_ace.svg"),
|
||||||
embed_default_svg!("back.svg"),
|
embed_dark_svg!("clubs_2.svg"),
|
||||||
embed_default_svg!("clubs_ace.svg"),
|
embed_dark_svg!("clubs_3.svg"),
|
||||||
embed_default_svg!("clubs_2.svg"),
|
embed_dark_svg!("clubs_4.svg"),
|
||||||
embed_default_svg!("clubs_3.svg"),
|
embed_dark_svg!("clubs_5.svg"),
|
||||||
embed_default_svg!("clubs_4.svg"),
|
embed_dark_svg!("clubs_6.svg"),
|
||||||
embed_default_svg!("clubs_5.svg"),
|
embed_dark_svg!("clubs_7.svg"),
|
||||||
embed_default_svg!("clubs_6.svg"),
|
embed_dark_svg!("clubs_8.svg"),
|
||||||
embed_default_svg!("clubs_7.svg"),
|
embed_dark_svg!("clubs_9.svg"),
|
||||||
embed_default_svg!("clubs_8.svg"),
|
embed_dark_svg!("clubs_10.svg"),
|
||||||
embed_default_svg!("clubs_9.svg"),
|
embed_dark_svg!("clubs_jack.svg"),
|
||||||
embed_default_svg!("clubs_10.svg"),
|
embed_dark_svg!("clubs_queen.svg"),
|
||||||
embed_default_svg!("clubs_jack.svg"),
|
embed_dark_svg!("clubs_king.svg"),
|
||||||
embed_default_svg!("clubs_queen.svg"),
|
embed_dark_svg!("diamonds_ace.svg"),
|
||||||
embed_default_svg!("clubs_king.svg"),
|
embed_dark_svg!("diamonds_2.svg"),
|
||||||
embed_default_svg!("diamonds_ace.svg"),
|
embed_dark_svg!("diamonds_3.svg"),
|
||||||
embed_default_svg!("diamonds_2.svg"),
|
embed_dark_svg!("diamonds_4.svg"),
|
||||||
embed_default_svg!("diamonds_3.svg"),
|
embed_dark_svg!("diamonds_5.svg"),
|
||||||
embed_default_svg!("diamonds_4.svg"),
|
embed_dark_svg!("diamonds_6.svg"),
|
||||||
embed_default_svg!("diamonds_5.svg"),
|
embed_dark_svg!("diamonds_7.svg"),
|
||||||
embed_default_svg!("diamonds_6.svg"),
|
embed_dark_svg!("diamonds_8.svg"),
|
||||||
embed_default_svg!("diamonds_7.svg"),
|
embed_dark_svg!("diamonds_9.svg"),
|
||||||
embed_default_svg!("diamonds_8.svg"),
|
embed_dark_svg!("diamonds_10.svg"),
|
||||||
embed_default_svg!("diamonds_9.svg"),
|
embed_dark_svg!("diamonds_jack.svg"),
|
||||||
embed_default_svg!("diamonds_10.svg"),
|
embed_dark_svg!("diamonds_queen.svg"),
|
||||||
embed_default_svg!("diamonds_jack.svg"),
|
embed_dark_svg!("diamonds_king.svg"),
|
||||||
embed_default_svg!("diamonds_queen.svg"),
|
embed_dark_svg!("hearts_ace.svg"),
|
||||||
embed_default_svg!("diamonds_king.svg"),
|
embed_dark_svg!("hearts_2.svg"),
|
||||||
embed_default_svg!("hearts_ace.svg"),
|
embed_dark_svg!("hearts_3.svg"),
|
||||||
embed_default_svg!("hearts_2.svg"),
|
embed_dark_svg!("hearts_4.svg"),
|
||||||
embed_default_svg!("hearts_3.svg"),
|
embed_dark_svg!("hearts_5.svg"),
|
||||||
embed_default_svg!("hearts_4.svg"),
|
embed_dark_svg!("hearts_6.svg"),
|
||||||
embed_default_svg!("hearts_5.svg"),
|
embed_dark_svg!("hearts_7.svg"),
|
||||||
embed_default_svg!("hearts_6.svg"),
|
embed_dark_svg!("hearts_8.svg"),
|
||||||
embed_default_svg!("hearts_7.svg"),
|
embed_dark_svg!("hearts_9.svg"),
|
||||||
embed_default_svg!("hearts_8.svg"),
|
embed_dark_svg!("hearts_10.svg"),
|
||||||
embed_default_svg!("hearts_9.svg"),
|
embed_dark_svg!("hearts_jack.svg"),
|
||||||
embed_default_svg!("hearts_10.svg"),
|
embed_dark_svg!("hearts_queen.svg"),
|
||||||
embed_default_svg!("hearts_jack.svg"),
|
embed_dark_svg!("hearts_king.svg"),
|
||||||
embed_default_svg!("hearts_queen.svg"),
|
embed_dark_svg!("spades_ace.svg"),
|
||||||
embed_default_svg!("hearts_king.svg"),
|
embed_dark_svg!("spades_2.svg"),
|
||||||
embed_default_svg!("spades_ace.svg"),
|
embed_dark_svg!("spades_3.svg"),
|
||||||
embed_default_svg!("spades_2.svg"),
|
embed_dark_svg!("spades_4.svg"),
|
||||||
embed_default_svg!("spades_3.svg"),
|
embed_dark_svg!("spades_5.svg"),
|
||||||
embed_default_svg!("spades_4.svg"),
|
embed_dark_svg!("spades_6.svg"),
|
||||||
embed_default_svg!("spades_5.svg"),
|
embed_dark_svg!("spades_7.svg"),
|
||||||
embed_default_svg!("spades_6.svg"),
|
embed_dark_svg!("spades_8.svg"),
|
||||||
embed_default_svg!("spades_7.svg"),
|
embed_dark_svg!("spades_9.svg"),
|
||||||
embed_default_svg!("spades_8.svg"),
|
embed_dark_svg!("spades_10.svg"),
|
||||||
embed_default_svg!("spades_9.svg"),
|
embed_dark_svg!("spades_jack.svg"),
|
||||||
embed_default_svg!("spades_10.svg"),
|
embed_dark_svg!("spades_queen.svg"),
|
||||||
embed_default_svg!("spades_jack.svg"),
|
embed_dark_svg!("spades_king.svg"),
|
||||||
embed_default_svg!("spades_queen.svg"),
|
|
||||||
embed_default_svg!("spades_king.svg"),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Registers asset sources that must be in place *before*
|
/// Registers asset sources that must be in place *before*
|
||||||
@@ -190,62 +180,53 @@ 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_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
|
/// (e.g. `"back.svg"` or `"spades_ace.svg"`), or `None` when the
|
||||||
/// filename is not bundled.
|
/// filename is not bundled.
|
||||||
///
|
///
|
||||||
/// The thumbnail generator in
|
/// The thumbnail generator uses this to rasterise preview-sized art
|
||||||
/// [`crate::theme::ThemeThumbnailCache`] uses this to rasterise
|
/// without going through Bevy's async asset graph.
|
||||||
/// preview-sized art for the picker UI without going through Bevy's
|
pub fn dark_theme_svg_bytes(filename: &str) -> Option<&'static [u8]> {
|
||||||
/// async asset graph. Lookup is by the filename only — the
|
|
||||||
/// `solitaire_engine/assets/themes/default/` prefix is stripped before
|
|
||||||
/// comparison so callers don't need to know where the embedded files
|
|
||||||
/// live in the binary.
|
|
||||||
pub fn default_theme_svg_bytes(filename: &str) -> Option<&'static [u8]> {
|
|
||||||
let suffix = format!("/{filename}");
|
let suffix = format!("/{filename}");
|
||||||
DEFAULT_THEME_SVGS
|
DARK_THEME_SVGS
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(path, _)| path.ends_with(&suffix))
|
.find(|(path, _)| path.ends_with(&suffix))
|
||||||
.map(|(_, bytes)| *bytes)
|
.map(|(_, bytes)| *bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pushes every bundled default-theme file into the
|
/// Returns the manifest URL for a bundled (non-user) theme by id, or
|
||||||
/// [`EmbeddedAssetRegistry`] under its stable URL. Keeping this in a
|
/// `None` if `id` belongs to a user theme that lives under `themes://`.
|
||||||
/// free function (and not inside the `Plugin::build` body) means the
|
|
||||||
/// unit test below can exercise it without spinning up a full Bevy
|
|
||||||
/// `App` with `AssetPlugin`.
|
|
||||||
///
|
///
|
||||||
/// **Adding files to the bundled default theme** is a single edit:
|
/// Callers that need to resolve a theme URL without access to
|
||||||
/// append one `embed_default_svg!("filename.svg")` line to the
|
/// [`crate::theme::ThemeRegistry`] (e.g. Startup systems where registry
|
||||||
/// `DEFAULT_THEME_SVGS` table above. The file resolves relative to
|
/// ordering isn't guaranteed) should use this instead of constructing
|
||||||
/// `solitaire_engine/assets/themes/default/` and registers under
|
/// the URL manually.
|
||||||
/// the matching `embedded://` URL automatically.
|
pub fn bundled_theme_url(id: &str) -> Option<&'static str> {
|
||||||
pub fn populate_embedded_default_theme(app: &mut App) {
|
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
|
let registry = app
|
||||||
.world_mut()
|
.world_mut()
|
||||||
.get_resource_or_insert_with(EmbeddedAssetRegistry::default);
|
.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(
|
registry.insert_asset(
|
||||||
std::path::PathBuf::from(DEFAULT_THEME_MANIFEST_PATH),
|
std::path::PathBuf::from(DARK_THEME_MANIFEST_PATH),
|
||||||
std::path::Path::new(DEFAULT_THEME_MANIFEST_PATH),
|
std::path::Path::new(DARK_THEME_MANIFEST_PATH),
|
||||||
DEFAULT_THEME_MANIFEST_BYTES,
|
DARK_THEME_MANIFEST_BYTES,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Then every face + back SVG. The manifest references each by the
|
for (path, bytes) in DARK_THEME_SVGS {
|
||||||
// same relative path used here.
|
|
||||||
for (path, bytes) in DEFAULT_THEME_SVGS {
|
|
||||||
registry.insert_asset(
|
registry.insert_asset(
|
||||||
std::path::PathBuf::from(*path),
|
std::path::PathBuf::from(*path),
|
||||||
std::path::Path::new(*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]
|
#[test]
|
||||||
fn populate_embedded_default_theme_runs_without_asset_plugin() {
|
fn populate_embedded_dark_theme_runs_without_asset_plugin() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
populate_embedded_default_theme(&mut app);
|
populate_embedded_dark_theme(&mut app);
|
||||||
|
|
||||||
// Resource exists and has been inserted into.
|
|
||||||
assert!(app
|
assert!(app
|
||||||
.world()
|
.world()
|
||||||
.get_resource::<EmbeddedAssetRegistry>()
|
.get_resource::<EmbeddedAssetRegistry>()
|
||||||
.is_some());
|
.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]
|
#[test]
|
||||||
fn embedded_default_theme_manifest_validates() {
|
fn embedded_dark_theme_manifest_validates() {
|
||||||
use crate::theme::ThemeManifest;
|
use crate::theme::ThemeManifest;
|
||||||
|
|
||||||
let manifest: ThemeManifest = ron::de::from_bytes(DEFAULT_THEME_MANIFEST_BYTES)
|
let manifest: ThemeManifest = ron::de::from_bytes(DARK_THEME_MANIFEST_BYTES)
|
||||||
.expect("default manifest must parse as RON");
|
.expect("dark manifest must parse as RON");
|
||||||
let faces = manifest
|
let faces = manifest
|
||||||
.validate()
|
.validate()
|
||||||
.expect("default manifest must list all 52 faces");
|
.expect("dark manifest must list all 52 faces");
|
||||||
assert_eq!(faces.len(), 52);
|
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]
|
#[test]
|
||||||
fn default_theme_svg_bytes_finds_back_and_ace_of_spades() {
|
fn dark_theme_svg_bytes_finds_back_and_ace_of_spades() {
|
||||||
assert!(
|
assert!(
|
||||||
default_theme_svg_bytes("back.svg").is_some(),
|
dark_theme_svg_bytes("back.svg").is_some(),
|
||||||
"default theme must bundle a back.svg"
|
"dark theme must bundle a back.svg"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
default_theme_svg_bytes("spades_ace.svg").is_some(),
|
dark_theme_svg_bytes("spades_ace.svg").is_some(),
|
||||||
"default theme must bundle a spades_ace.svg"
|
"dark theme must bundle a spades_ace.svg"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_theme_svg_bytes_returns_none_for_unknown_file() {
|
fn dark_theme_svg_bytes_returns_none_for_unknown_file() {
|
||||||
assert!(default_theme_svg_bytes("nope.svg").is_none());
|
assert!(dark_theme_svg_bytes("nope.svg").is_none());
|
||||||
assert!(default_theme_svg_bytes("").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]
|
#[test]
|
||||||
fn default_theme_url_constant_matches_embedded_path() {
|
fn dark_theme_url_constant_matches_embedded_path() {
|
||||||
let url_tail = DEFAULT_THEME_MANIFEST_URL
|
let url_tail = DARK_THEME_MANIFEST_URL
|
||||||
.strip_prefix("embedded://")
|
.strip_prefix("embedded://")
|
||||||
.expect("default theme URL must use embedded:// scheme");
|
.expect("dark theme URL must use embedded:// scheme");
|
||||||
assert_eq!(url_tail, DEFAULT_THEME_MANIFEST_PATH);
|
assert_eq!(url_tail, DARK_THEME_MANIFEST_PATH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ pub mod weekly_goals_plugin;
|
|||||||
pub mod win_summary_plugin;
|
pub mod win_summary_plugin;
|
||||||
|
|
||||||
pub use assets::{
|
pub use assets::{
|
||||||
populate_embedded_default_theme, register_theme_asset_sources, AssetSourcesPlugin,
|
bundled_theme_url, populate_embedded_dark_theme, register_theme_asset_sources,
|
||||||
DEFAULT_THEME_MANIFEST_URL, USER_THEMES,
|
AssetSourcesPlugin, DARK_THEME_MANIFEST_URL, USER_THEMES,
|
||||||
};
|
};
|
||||||
pub use theme::{
|
pub use theme::{
|
||||||
set_theme, ActiveTheme, CardTheme, CardThemeLoader, ThemeEntry, ThemePlugin, ThemeRegistry,
|
set_theme, ActiveTheme, CardTheme, CardThemeLoader, ThemeEntry, ThemePlugin, ThemeRegistry,
|
||||||
|
|||||||
@@ -15,7 +15,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, 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::card_plugin::CardImageSet;
|
||||||
use crate::events::StateChangedEvent;
|
use crate::events::StateChangedEvent;
|
||||||
@@ -126,12 +126,13 @@ 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 url = match settings.as_deref() {
|
let id = settings
|
||||||
Some(s) if s.0.selected_theme_id != "default" => {
|
.as_deref()
|
||||||
format!("themes://{}/theme.ron", s.0.selected_theme_id)
|
.map(|s| s.0.selected_theme_id.as_str())
|
||||||
}
|
.unwrap_or("dark");
|
||||||
_ => DEFAULT_THEME_MANIFEST_URL.to_string(),
|
let url = bundled_theme_url(id)
|
||||||
};
|
.map(str::to_string)
|
||||||
|
.unwrap_or_else(|| format!("themes://{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));
|
||||||
}
|
}
|
||||||
@@ -161,11 +162,9 @@ fn react_to_settings_theme_change(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = if new_id == "default" {
|
let url = bundled_theme_url(new_id)
|
||||||
DEFAULT_THEME_MANIFEST_URL.to_string()
|
.map(str::to_string)
|
||||||
} else {
|
.unwrap_or_else(|| format!("themes://{new_id}/theme.ron"));
|
||||||
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));
|
||||||
}
|
}
|
||||||
@@ -305,16 +304,24 @@ const PREVIEW_BACK_FILENAME: &str = "back.svg";
|
|||||||
/// Resolves the SVG bytes for one preview file (`back.svg` or
|
/// Resolves the SVG bytes for one preview file (`back.svg` or
|
||||||
/// `spades_ace.svg`) belonging to the named theme.
|
/// `spades_ace.svg`) belonging to the named theme.
|
||||||
///
|
///
|
||||||
/// - For the bundled `default` theme, reads from the embedded
|
/// - For the embedded `dark` theme, reads from the in-binary table via
|
||||||
/// `DEFAULT_THEME_SVGS` table via [`default_theme_svg_bytes`]. No
|
/// [`dark_theme_svg_bytes`]. No filesystem I/O.
|
||||||
/// filesystem I/O.
|
/// - For bundled non-embedded themes (e.g. `classic`), reads from the
|
||||||
/// - For any user theme, reads from `<user_theme_dir>/<id>/<filename>`.
|
/// `assets/themes/<id>/` directory.
|
||||||
/// Returns `None` for any I/O failure (file missing, permission
|
/// - For user themes, reads from `<user_theme_dir>/<id>/<filename>`.
|
||||||
/// denied, etc.) — the caller treats `None` as "render placeholder".
|
/// Returns `None` for any I/O failure.
|
||||||
fn read_theme_preview_svg_bytes(theme_id: &str, filename: &str) -> Option<Vec<u8>> {
|
fn read_theme_preview_svg_bytes(theme_id: &str, filename: &str) -> Option<Vec<u8>> {
|
||||||
if theme_id == "default" {
|
if theme_id == "dark" {
|
||||||
return default_theme_svg_bytes(filename).map(|b| b.to_vec());
|
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);
|
let path = user_theme_dir().join(theme_id).join(filename);
|
||||||
std::fs::read(&path).ok()
|
std::fs::read(&path).ok()
|
||||||
}
|
}
|
||||||
@@ -503,22 +510,20 @@ mod tests {
|
|||||||
// set_theme that doesn't require an App. We assert the URL
|
// set_theme that doesn't require an App. We assert the URL
|
||||||
// shape so a future refactor doesn't accidentally change the
|
// shape so a future refactor doesn't accidentally change the
|
||||||
// path layout.
|
// path layout.
|
||||||
let url = format!("themes://{}/theme.ron", "default");
|
let url = format!("themes://{}/theme.ron", "user_uploaded");
|
||||||
assert_eq!(url, "themes://default/theme.ron");
|
assert_eq!(url, "themes://user_uploaded/theme.ron");
|
||||||
let url2 = format!("themes://{}/theme.ron", "user_uploaded");
|
|
||||||
assert_eq!(url2, "themes://user_uploaded/theme.ron");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test 1: the bundled default theme always has embedded SVG bytes
|
/// Test 1: the bundled dark theme always has embedded SVG bytes
|
||||||
/// available, so calling `generate_thumbnail_pair_for("default", …)`
|
/// available, so calling `generate_thumbnail_pair_for("dark", …)`
|
||||||
/// must produce two non-default `Handle<Image>` slots.
|
/// must produce two non-default `Handle<Image>` slots.
|
||||||
#[test]
|
#[test]
|
||||||
fn theme_thumbnails_generated_for_default_theme() {
|
fn theme_thumbnails_generated_for_dark_theme() {
|
||||||
let mut images = Assets::<Image>::default();
|
let mut images = Assets::<Image>::default();
|
||||||
let pair = generate_thumbnail_pair_for("default", &mut images);
|
let pair = generate_thumbnail_pair_for("dark", &mut images);
|
||||||
assert!(
|
assert!(
|
||||||
pair.is_fully_populated(),
|
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
|
// And the underlying images must actually exist in the assets
|
||||||
// collection — the handles are real, not dangling.
|
// collection — the handles are real, not dangling.
|
||||||
@@ -558,18 +563,17 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `read_theme_preview_svg_bytes` for the default theme always
|
/// `read_theme_preview_svg_bytes` for the dark theme always returns
|
||||||
/// returns embedded bytes for the canonical preview pair —
|
/// embedded bytes for the canonical preview pair.
|
||||||
/// covering the happy-path branch of the helper.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn read_default_theme_preview_returns_some_for_canonical_files() {
|
fn read_dark_theme_preview_returns_some_for_canonical_files() {
|
||||||
assert!(
|
assert!(
|
||||||
read_theme_preview_svg_bytes("default", PREVIEW_BACK_FILENAME).is_some(),
|
read_theme_preview_svg_bytes("dark", PREVIEW_BACK_FILENAME).is_some(),
|
||||||
"default theme back.svg must be embedded"
|
"dark theme back.svg must be embedded"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
read_theme_preview_svg_bytes("default", PREVIEW_FACE_FILENAME).is_some(),
|
read_theme_preview_svg_bytes("dark", PREVIEW_FACE_FILENAME).is_some(),
|
||||||
"default theme spades_ace.svg must be embedded"
|
"dark theme spades_ace.svg must be embedded"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,12 +590,12 @@ mod tests {
|
|||||||
app.init_resource::<ThemeThumbnailCache>();
|
app.init_resource::<ThemeThumbnailCache>();
|
||||||
app.insert_resource(ThemeRegistry {
|
app.insert_resource(ThemeRegistry {
|
||||||
entries: vec![crate::theme::ThemeEntry {
|
entries: vec![crate::theme::ThemeEntry {
|
||||||
id: "default".into(),
|
id: "dark".into(),
|
||||||
display_name: "Default".into(),
|
display_name: "Dark".into(),
|
||||||
manifest_url: crate::assets::DEFAULT_THEME_MANIFEST_URL.into(),
|
manifest_url: crate::assets::DARK_THEME_MANIFEST_URL.into(),
|
||||||
meta: ThemeMeta {
|
meta: ThemeMeta {
|
||||||
id: "default".into(),
|
id: "dark".into(),
|
||||||
name: "Default".into(),
|
name: "Dark".into(),
|
||||||
author: "x".into(),
|
author: "x".into(),
|
||||||
version: "x".into(),
|
version: "x".into(),
|
||||||
card_aspect: (2, 3),
|
card_aspect: (2, 3),
|
||||||
@@ -605,18 +609,18 @@ mod tests {
|
|||||||
let first_ace = app
|
let first_ace = app
|
||||||
.world()
|
.world()
|
||||||
.resource::<ThemeThumbnailCache>()
|
.resource::<ThemeThumbnailCache>()
|
||||||
.get("default")
|
.get("dark")
|
||||||
.map(|p| p.ace.clone())
|
.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.
|
// Second tick must NOT replace the cached handle.
|
||||||
app.update();
|
app.update();
|
||||||
let second_ace = app
|
let second_ace = app
|
||||||
.world()
|
.world()
|
||||||
.resource::<ThemeThumbnailCache>()
|
.resource::<ThemeThumbnailCache>()
|
||||||
.get("default")
|
.get("dark")
|
||||||
.map(|p| p.ace.clone())
|
.map(|p| p.ace.clone())
|
||||||
.expect("default theme thumbnail must still exist");
|
.expect("dark theme thumbnail must still exist");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
first_ace.id(),
|
first_ace.id(),
|
||||||
|
|||||||
@@ -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};
|
use crate::assets::{user_theme_dir, DARK_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.
|
||||||
@@ -100,24 +100,23 @@ fn build_registry_on_startup(mut registry: bevy::ecs::system::ResMut<ThemeRegist
|
|||||||
/// [`user_theme_dir`].
|
/// [`user_theme_dir`].
|
||||||
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(classic_entry());
|
entries.push(classic_entry());
|
||||||
|
entries.push(dark_entry());
|
||||||
entries.extend(discover_user_themes(user_dir));
|
entries.extend(discover_user_themes(user_dir));
|
||||||
ThemeRegistry { entries }
|
ThemeRegistry { entries }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The bundled default theme entry — inserted unconditionally so the
|
/// The always-present embedded Dark theme entry.
|
||||||
/// picker always has at least one option.
|
fn dark_entry() -> ThemeEntry {
|
||||||
fn default_entry() -> ThemeEntry {
|
|
||||||
ThemeEntry {
|
ThemeEntry {
|
||||||
id: "default".to_string(),
|
id: "dark".to_string(),
|
||||||
display_name: "Default".to_string(),
|
display_name: "Dark".to_string(),
|
||||||
manifest_url: DEFAULT_THEME_MANIFEST_URL.to_string(),
|
manifest_url: DARK_THEME_MANIFEST_URL.to_string(),
|
||||||
meta: ThemeMeta {
|
meta: ThemeMeta {
|
||||||
id: "default".to_string(),
|
id: "dark".to_string(),
|
||||||
name: "Default".to_string(),
|
name: "Dark".to_string(),
|
||||||
author: "Ferrous Solitaire".to_string(),
|
author: "Ferrous Solitaire".to_string(),
|
||||||
version: "1.0".to_string(),
|
version: "1.0.0".to_string(),
|
||||||
card_aspect: (2, 3),
|
card_aspect: (2, 3),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -265,8 +264,8 @@ mod tests {
|
|||||||
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(), BUNDLED_COUNT);
|
assert_eq!(registry.len(), BUNDLED_COUNT);
|
||||||
assert_eq!(registry.entries[0].id, "default");
|
assert_eq!(registry.entries[0].id, "classic");
|
||||||
assert_eq!(registry.entries[1].id, "classic");
|
assert_eq!(registry.entries[1].id, "dark");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -275,7 +274,8 @@ mod tests {
|
|||||||
"/definitely/not/a/real/path/should/not/panic",
|
"/definitely/not/a/real/path/should/not/panic",
|
||||||
));
|
));
|
||||||
assert_eq!(registry.len(), BUNDLED_COUNT);
|
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]
|
#[test]
|
||||||
@@ -333,7 +333,7 @@ mod tests {
|
|||||||
|
|
||||||
let registry = build_registry(tmp.path());
|
let registry = build_registry(tmp.path());
|
||||||
assert_eq!(registry.len(), BUNDLED_COUNT, "escape attempt must not register");
|
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]
|
#[test]
|
||||||
@@ -373,15 +373,13 @@ mod tests {
|
|||||||
refresh_registry(&mut registry, tmp.path());
|
refresh_registry(&mut registry, tmp.path());
|
||||||
|
|
||||||
assert_eq!(registry.len(), BUNDLED_COUNT);
|
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());
|
assert!(registry.find("stale").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_entry_url_matches_embedded_constant() {
|
fn dark_entry_url_matches_embedded_constant() {
|
||||||
// Ensures the picker always gets a URL it can hand to the
|
let entry = dark_entry();
|
||||||
// asset server for the bundled theme.
|
assert_eq!(entry.manifest_url, DARK_THEME_MANIFEST_URL);
|
||||||
let entry = default_entry();
|
|
||||||
assert_eq!(entry.manifest_url, DEFAULT_THEME_MANIFEST_URL);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||