feat(engine): card-theme picker in Settings → Cosmetic
CI / Test & Lint (push) Failing after 5s
CI / Release Build (push) Has been skipped

Wires the runtime theme system (CARD_PLAN.md phases 1–7) into the
visible Settings UI so a player can switch between every theme
discovered by `ThemeRegistry` without restarting.

solitaire_data/src/settings.rs
  Settings gains `selected_theme_id: String` (default "default"),
  guarded by `#[serde(default = "default_theme_id")]` so existing
  settings.json files deserialize cleanly.

solitaire_engine/src/settings_plugin.rs
  - SettingsButton::SelectTheme(String) variant + focus order 85.
  - sync_settings_panel_visibility now reads
    Option<Res<ThemeRegistry>>, snapshots id+display_name pairs, and
    threads them into spawn_settings_panel. When the registry is
    absent (tests under MinimalPlugins) the picker silently skips —
    every existing test continues to pass unchanged.
  - theme_picker_row helper: like picker_row but keyed by String
    rather than usize, with chips wide enough for theme display
    names. Attaches the canonical tooltip ("Choose card-face
    artwork. Imported themes appear here.") and the FocusRow marker
    so Left/Right arrows cycle within the row.
  - Click handler updates settings.selected_theme_id, persists, and
    fires SettingsChangedEvent — same shape as every other picker.

solitaire_engine/src/theme/plugin.rs
  - load_default_theme renamed to load_initial_theme; reads
    SettingsResource on Startup and seeds ActiveTheme from
    settings.selected_theme_id (falling back to embedded default).
  - react_to_settings_theme_change watches SettingsChangedEvent,
    no-ops when the active theme already matches, and otherwise
    swaps ActiveTheme — the existing
    sync_card_image_set_with_active_theme system then refreshes
    every card sprite on the next AssetEvent::LoadedWithDependencies.

cargo build / clippy --workspace --all-targets -- -D warnings / test
--workspace all green (960 passed, 0 failed, 9 ignored).
This commit is contained in:
funman300
2026-05-01 16:24:24 +00:00
parent a6b8348332
commit 924a1e2af7
3 changed files with 181 additions and 6 deletions
+57 -6
View File
@@ -47,17 +47,68 @@ impl Plugin for ThemePlugin {
app.init_asset::<CardTheme>()
.register_asset_loader(crate::assets::SvgLoader)
.register_asset_loader(CardThemeLoader)
.add_systems(Startup, load_default_theme)
.add_systems(Update, sync_card_image_set_with_active_theme);
.add_systems(Startup, load_initial_theme)
.add_systems(
Update,
(
sync_card_image_set_with_active_theme,
react_to_settings_theme_change,
),
);
}
}
/// Kicks off the default-theme load and stashes the handle on
/// [`ActiveTheme`]. The actual rasterisation runs asynchronously on
/// Kicks off the initial theme load — the one named by
/// `Settings::selected_theme_id` if available, falling back to the
/// embedded default. The actual rasterisation runs asynchronously on
/// the asset task pool; the sync system below picks up the
/// `LoadedWithDependencies` event when every face + back is ready.
fn load_default_theme(asset_server: Res<AssetServer>, mut commands: Commands) {
let handle: Handle<CardTheme> = asset_server.load(DEFAULT_THEME_MANIFEST_URL);
fn load_initial_theme(
asset_server: Res<AssetServer>,
settings: Option<Res<crate::settings_plugin::SettingsResource>>,
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 handle: Handle<CardTheme> = asset_server.load(url);
commands.insert_resource(ActiveTheme(handle));
}
/// Watches [`crate::settings_plugin::SettingsChangedEvent`] and
/// triggers a fresh theme load whenever
/// `Settings::selected_theme_id` changes. The settings panel's theme
/// picker fires the event after persisting; this system is the bridge
/// that turns the persisted choice into a live `set_theme` call.
fn react_to_settings_theme_change(
mut events: MessageReader<crate::settings_plugin::SettingsChangedEvent>,
asset_server: Res<AssetServer>,
active: Option<Res<ActiveTheme>>,
themes: Res<Assets<CardTheme>>,
mut commands: Commands,
) {
let Some(latest) = events.read().last() else {
return;
};
let new_id = latest.0.selected_theme_id.as_str();
// No-op if the active theme already matches the desired id.
if let Some(active) = active.as_deref()
&& let Some(theme) = themes.get(&active.0)
&& theme.meta.id == new_id
{
return;
}
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));
}