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:
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user