feat(engine): card-theme picker in Settings → Cosmetic
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:
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user