diff --git a/solitaire_engine/src/assets/user_dir.rs b/solitaire_engine/src/assets/user_dir.rs index 020116a..4db9637 100644 --- a/solitaire_engine/src/assets/user_dir.rs +++ b/solitaire_engine/src/assets/user_dir.rs @@ -53,12 +53,12 @@ pub fn set_user_theme_dir(path: PathBuf) -> Result<(), PathBuf> { /// Returns the absolute path of the user-themes directory on the /// current platform. /// -/// # Panics -/// -/// Panics if [`solitaire_data::data_dir`] returns `None`, which on -/// desktop indicates a broken `$HOME` / `$XDG_*` configuration. -/// Android always returns `Some`. The panic message names the -/// supported workaround ([`set_user_theme_dir`]). +/// When [`solitaire_data::data_dir`] returns `None` (broken `$HOME` / +/// `$XDG_*` on desktop; always on wasm32, which has no filesystem) this +/// returns an empty path — callers treat that as "no user themes" and +/// the bundled default theme still works. A warning naming the +/// [`set_user_theme_dir`] workaround is logged once. Android always +/// resolves. pub fn user_theme_dir() -> PathBuf { if let Some(p) = USER_THEME_DIR_OVERRIDE.get() { return p.clone(); @@ -76,29 +76,32 @@ fn user_theme_dir_for(data_dir: PathBuf) -> PathBuf { /// Per-target-os resolution of the platform's data dir. Delegates /// to [`solitaire_data::data_dir`] which encapsulates the /// per-target shape (desktop: `dirs::data_dir()`; android: the -/// hardcoded `/data/data//files` sandbox path). Panics -/// only when the underlying resolver returns `None`, which on -/// desktop indicates a broken `$HOME` / `$XDG_*` configuration — -/// the panic message names the supported workaround. +/// hardcoded `/data/data//files` sandbox path). +/// +/// When the resolver returns `None` — always on wasm32 (no +/// filesystem), or a broken `$HOME` / `$XDG_*` configuration on +/// desktop — this degrades to an empty path, which downstream theme +/// scanning treats as "no user themes"; the bundled default theme is +/// unaffected. CLAUDE.md §2.3 forbids panicking here: losing custom +/// themes must not take the whole game down with it. fn detected_platform_data_dir() -> PathBuf { solitaire_data::data_dir().unwrap_or_else(|| { - // On wasm32, data_dir() always returns None — there is no filesystem. - // User themes are not supported in the browser build; return an empty - // path so callers produce a benign empty dir rather than panicking. - #[cfg(target_arch = "wasm32")] - { - PathBuf::new() - } #[cfg(not(target_arch = "wasm32"))] { - panic!( - "user_theme_dir(): platform data directory is unavailable. \ - On Linux check $XDG_DATA_HOME or $HOME; on macOS / Windows \ - the OS reported no Application Support / AppData path. \ - As a workaround call solitaire_engine::assets::user_dir::\ - set_user_theme_dir() before App::run()." - ) + use std::sync::Once; + static WARN_ONCE: Once = Once::new(); + WARN_ONCE.call_once(|| { + bevy::log::warn!( + "user_theme_dir(): platform data directory is unavailable; \ + user themes are disabled. On Linux check $XDG_DATA_HOME or \ + $HOME; on macOS / Windows the OS reported no Application \ + Support / AppData path. As a workaround call \ + solitaire_engine::assets::user_dir::set_user_theme_dir() \ + before App::run()." + ); + }); } + PathBuf::new() }) }