use std::path::PathBuf;
use std::sync::Arc;
use bevy::prelude::*;
use crate::font::FontRegistry;
use crate::render::NoesisView;
use crate::xaml::XamlRegistry;
const THEME_FONT_FALLBACK: &str = "Fonts/#PT Root UI";
pub struct NoesisDefaultThemePlugin {
pub theme: String,
}
impl Default for NoesisDefaultThemePlugin {
fn default() -> Self {
Self {
theme: "DarkBlue".into(),
}
}
}
impl Plugin for NoesisDefaultThemePlugin {
fn build(&self, app: &mut App) {
let staged = stage_theme(&self.theme);
if staged.xamls.is_empty() {
warn!(
"NoesisDefaultThemePlugin: no theme files staged for {:?} — \
controls will render unstyled (magenta). Check NOESIS_SDK_DIR.",
self.theme
);
}
app.insert_resource(staged)
.add_systems(Startup, inject_theme_registries)
.add_systems(Update, apply_theme_to_scene);
}
}
#[derive(Resource, Default)]
struct StagedTheme {
name: String,
xamls: Vec<(String, PathBuf)>,
fonts: Vec<(String, String, PathBuf)>,
}
fn stage_theme(theme: &str) -> StagedTheme {
let mut staged = StagedTheme {
name: theme.to_string(),
..default()
};
let Some(sdk) = std::env::var_os("NOESIS_SDK_DIR") else {
warn!("NOESIS_SDK_DIR unset — cannot load Noesis default theme");
return staged;
};
let root = PathBuf::from(sdk).join("Src/Packages/App/Theme/Data/Theme");
if !root.is_dir() {
warn!("Noesis theme dir {} not found", root.display());
return staged;
}
for entry in std::fs::read_dir(&root).into_iter().flatten().flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "xaml")
&& let Some(name) = path.file_name().and_then(|n| n.to_str())
{
staged.xamls.push((name.to_string(), path.clone()));
}
}
let fonts_dir = root.join("Fonts");
for entry in std::fs::read_dir(&fonts_dir)
.into_iter()
.flatten()
.flatten()
{
let path = entry.path();
if path.extension().is_some_and(|e| e == "otf" || e == "ttf")
&& let Some(name) = path.file_name().and_then(|n| n.to_str())
{
staged
.fonts
.push(("Fonts".to_string(), name.to_string(), path.clone()));
}
}
let want = format!("NoesisTheme.{theme}.xaml");
if !staged.xamls.iter().any(|(n, _)| n == &want) {
warn!(
"NoesisDefaultThemePlugin: {want} not found under {} — available \
variants are the NoesisTheme.<Variant>.xaml files there",
root.display()
);
}
info!(
"NoesisDefaultThemePlugin: staged {} theme XAML(s) + {} font(s) for {theme}",
staged.xamls.len(),
staged.fonts.len()
);
staged
}
#[allow(clippy::needless_pass_by_value)]
fn inject_theme_registries(
staged: Res<StagedTheme>,
xaml: Option<ResMut<XamlRegistry>>,
fonts: Option<ResMut<FontRegistry>>,
) {
let (Some(mut xaml), Some(mut fonts)) = (xaml, fonts) else {
warn!("NoesisDefaultThemePlugin requires NoesisPlugin (registries missing)");
return;
};
for (name, path) in &staged.xamls {
match std::fs::read(path) {
Ok(bytes) => xaml.insert(name.clone(), Arc::new(bytes)),
Err(err) => warn!("theme xaml read failed {}: {err}", path.display()),
}
}
for (folder, filename, path) in &staged.fonts {
match std::fs::read(path) {
Ok(bytes) => fonts.insert(folder.clone(), filename.clone(), Arc::new(bytes)),
Err(err) => warn!("theme font read failed {}: {err}", path.display()),
}
}
}
#[allow(clippy::needless_pass_by_value)]
fn apply_theme_to_scene(
staged: Res<StagedTheme>,
mut views: Query<&mut NoesisView>,
mut applied: Local<bool>,
) {
if *applied {
return;
}
if staged.xamls.is_empty() {
*applied = true;
return;
}
if views.is_empty() {
return;
}
let theme_uri = format!("NoesisTheme.{}.xaml", staged.name);
for mut scene in &mut views {
if !scene.application_resources.contains(&theme_uri) {
scene.application_resources.insert(0, theme_uri.clone());
}
if !scene.wait_for_fonts.iter().any(|f| f == "Fonts") {
scene.wait_for_fonts.push("Fonts".to_string());
}
for (folder, filename, _) in &staged.fonts {
let pair = (folder.clone(), filename.clone());
if !scene.wait_for_font_files.contains(&pair) {
scene.wait_for_font_files.push(pair);
}
}
let fallback = THEME_FONT_FALLBACK.to_string();
if !scene.font_fallbacks.contains(&fallback) {
scene.font_fallbacks.push(fallback);
}
}
*applied = true;
}