fix(assets,theme): remove assert in svg_loader, log theme failures, fix default theme id (#58, #63, #64)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
funman300
2026-05-27 19:36:05 -07:00
parent 35fde160fa
commit 0437c36463
3 changed files with 38 additions and 14 deletions
+1 -1
View File
@@ -280,7 +280,7 @@ fn default_music_volume() -> f32 {
} }
fn default_theme_id() -> String { fn default_theme_id() -> String {
"classic".to_string() "dark".to_string()
} }
/// Default tooltip-hover dwell delay in seconds. Mirrors /// Default tooltip-hover dwell delay in seconds. Mirrors
+11 -8
View File
@@ -24,6 +24,7 @@ use std::sync::{Arc, OnceLock};
use bevy::asset::io::Reader; use bevy::asset::io::Reader;
use bevy::asset::{AssetLoader, LoadContext, RenderAssetUsages}; use bevy::asset::{AssetLoader, LoadContext, RenderAssetUsages};
use bevy::image::Image; use bevy::image::Image;
use bevy::log::warn;
use bevy::math::UVec2; use bevy::math::UVec2;
use bevy::reflect::TypePath; use bevy::reflect::TypePath;
use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat}; use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat};
@@ -156,7 +157,7 @@ pub fn rasterize_svg(svg_bytes: &[u8], target: UVec2) -> Result<Image, SvgLoader
/// share the same canonical face. /// share the same canonical face.
const BUNDLED_FONT_BYTES: &[u8] = include_bytes!("../../../assets/fonts/main.ttf"); const BUNDLED_FONT_BYTES: &[u8] = include_bytes!("../../../assets/fonts/main.ttf");
/// Returns a process-wide font database holding only the bundled /// Returns a process-wide font database that tries to load the bundled
/// FiraMono-Medium face. Initialised lazily on first SVG that references /// FiraMono-Medium face. Initialised lazily on first SVG that references
/// text, then shared (via `Arc`) across every subsequent rasterisation. /// text, then shared (via `Arc`) across every subsequent rasterisation.
/// ///
@@ -165,17 +166,19 @@ const BUNDLED_FONT_BYTES: &[u8] = include_bytes!("../../../assets/fonts/main.ttf
/// such request directly to FiraMono so rasterisation is deterministic /// such request directly to FiraMono so rasterisation is deterministic
/// across machines and the system font path is never consulted. /// across machines and the system font path is never consulted.
/// ///
/// Aborts the program if the embedded bytes don't parse — bundled at /// If the embedded bytes fail to yield any faces, log a warning and
/// compile time, so a parse failure means the binary is corrupt. /// fall back to an empty database so startup can continue.
fn shared_fontdb() -> Arc<fontdb::Database> { fn shared_fontdb() -> Arc<fontdb::Database> {
static DB: OnceLock<Arc<fontdb::Database>> = OnceLock::new(); static DB: OnceLock<Arc<fontdb::Database>> = OnceLock::new();
DB.get_or_init(|| { DB.get_or_init(|| {
let mut db = fontdb::Database::new(); let mut db = fontdb::Database::new();
db.load_font_data(BUNDLED_FONT_BYTES.to_vec()); let loaded_faces = db.load_font_source(fontdb::Source::Binary(Arc::new(
assert!( BUNDLED_FONT_BYTES.to_vec(),
db.faces().next().is_some(), )));
"bundled FiraMono failed to parse — binary is corrupt" if loaded_faces.is_empty() {
); let e = "no faces loaded from bundled bytes";
warn!("Failed to load bundled FiraMono font: {e}");
}
Arc::new(db) Arc::new(db)
}) })
.clone() .clone()
+26 -5
View File
@@ -21,11 +21,12 @@
use std::path::Path; use std::path::Path;
use bevy::log::warn;
use bevy::prelude::{App, Plugin, Resource, Startup}; 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, DARK_THEME_MANIFEST_URL}; use crate::assets::{DARK_THEME_MANIFEST_URL, user_theme_dir};
/// 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.
@@ -143,7 +144,7 @@ fn classic_entry() -> ThemeEntry {
/// Walks `user_dir`, treating every immediate subdirectory as a /// Walks `user_dir`, treating every immediate subdirectory as a
/// candidate theme. A subdirectory contributes one entry if and only /// candidate theme. A subdirectory contributes one entry if and only
/// if it contains a `theme.ron` whose `meta` block parses cleanly and /// if it contains a `theme.ron` whose `meta` block parses cleanly and
/// passes `ThemeMeta::validate`. Failed candidates are silently /// passes `ThemeMeta::validate`. Failed candidates are warned and
/// skipped — broken themes don't poison discovery. /// skipped — broken themes don't poison discovery.
fn discover_user_themes(user_dir: &Path) -> Vec<ThemeEntry> { fn discover_user_themes(user_dir: &Path) -> Vec<ThemeEntry> {
let mut out = Vec::new(); let mut out = Vec::new();
@@ -184,9 +185,29 @@ struct ManifestMetaOnly {
/// [`ThemeEntry`]. Returns `None` for any I/O / parse / validation /// [`ThemeEntry`]. Returns `None` for any I/O / parse / validation
/// failure — discovery is best-effort. /// failure — discovery is best-effort.
fn read_meta_only(manifest_path: &Path) -> Option<ThemeEntry> { fn read_meta_only(manifest_path: &Path) -> Option<ThemeEntry> {
let bytes = std::fs::read(manifest_path).ok()?; let theme_id = manifest_path
let parsed: ManifestMetaOnly = ron::de::from_bytes(&bytes).ok()?; .parent()
parsed.meta.validate().ok()?; .and_then(Path::file_name)
.and_then(|name| name.to_str())
.unwrap_or("<unknown>");
let bytes = match std::fs::read(manifest_path) {
Ok(bytes) => bytes,
Err(e) => {
warn!("Skipping theme '{}': {}", theme_id, e);
return None;
}
};
let parsed: ManifestMetaOnly = match ron::de::from_bytes(&bytes) {
Ok(parsed) => parsed,
Err(e) => {
warn!("Skipping theme '{}': {}", theme_id, e);
return None;
}
};
if let Err(e) = parsed.meta.validate() {
warn!("Skipping theme '{}': {}", theme_id, e);
return None;
}
let id = parsed.meta.id.clone(); let id = parsed.meta.id.clone();
let display_name = parsed.meta.name.clone(); let display_name = parsed.meta.name.clone();
let manifest_url = format!("themes://{id}/theme.ron"); let manifest_url = format!("themes://{id}/theme.ron");