use std::collections::HashMap;
use bevy::prelude::*;
use crate::brushes::BrushSpec;
use crate::dp::DpValue;
use crate::render::{
NoesisRenderState, NoesisSet, NoesisView, sync_font_provider_map, sync_xaml_provider_map,
};
#[derive(Debug, Clone, PartialEq)]
pub enum ResourceEntry {
Brush(BrushSpec),
Value(DpValue),
}
#[derive(Resource, Clone, Default, Debug)]
pub struct NoesisResources {
pub entries: HashMap<String, ResourceEntry>,
pub merged_xaml: Vec<String>,
}
impl NoesisResources {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn entry(mut self, key: impl Into<String>, entry: ResourceEntry) -> Self {
self.entries.insert(key.into(), entry);
self
}
#[must_use]
pub fn brush(self, key: impl Into<String>, spec: BrushSpec) -> Self {
self.entry(key, ResourceEntry::Brush(spec))
}
#[must_use]
pub fn solid(self, key: impl Into<String>, rgba: [f32; 4]) -> Self {
self.brush(key, BrushSpec::Solid(rgba))
}
#[must_use]
pub fn value(self, key: impl Into<String>, value: DpValue) -> Self {
self.entry(key, ResourceEntry::Value(value))
}
#[must_use]
pub fn merged(mut self, xaml: impl Into<String>) -> Self {
self.merged_xaml.push(xaml.into());
self
}
}
#[derive(Message, Debug, Clone)]
pub struct NoesisResourcesInstalled {
pub present: Vec<String>,
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_resources_bridge(
resources: Option<Res<NoesisResources>>,
views: Query<&NoesisView>,
state: Option<NonSendMut<NoesisRenderState>>,
mut installed: MessageWriter<NoesisResourcesInstalled>,
mut warned_conflict: Local<bool>,
) {
let Some(mut state) = state else {
return;
};
let mut chain_uris: Vec<String> = Vec::new();
let mut distinct_chains: Vec<&[String]> = Vec::new();
let mut wait_fonts: Vec<String> = Vec::new();
let mut wait_font_files: Vec<(String, String)> = Vec::new();
for view in &views {
if view.application_resources.is_empty() {
continue;
}
if !distinct_chains.contains(&view.application_resources.as_slice()) {
distinct_chains.push(&view.application_resources);
}
for uri in &view.application_resources {
if !chain_uris.contains(uri) {
chain_uris.push(uri.clone());
}
}
for folder in &view.wait_for_fonts {
if !wait_fonts.contains(folder) {
wait_fonts.push(folder.clone());
}
}
for pair in &view.wait_for_font_files {
if !wait_font_files.contains(pair) {
wait_font_files.push(pair.clone());
}
}
}
if distinct_chains.len() > 1 && !*warned_conflict {
warn!(
"NoesisView.application_resources: views declare different chains {distinct_chains:?}; \
application resources are process-global, so all are merged into one dictionary"
);
*warned_conflict = true;
}
let empty = NoesisResources::default();
let resources = resources.as_deref().unwrap_or(&empty);
if let Some(present) = state.reconcile_app_resources(
&resources.entries,
&resources.merged_xaml,
&chain_uris,
&wait_fonts,
&wait_font_files,
) {
if !resources.entries.is_empty() {
installed.write(NoesisResourcesInstalled { present });
}
}
}
pub struct NoesisResourcesPlugin;
impl Plugin for NoesisResourcesPlugin {
fn build(&self, app: &mut App) {
app.add_message::<NoesisResourcesInstalled>().add_systems(
PostUpdate,
sync_resources_bridge
.in_set(NoesisSet::Sync)
.after(sync_xaml_provider_map)
.after(sync_font_provider_map),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_collects_entries() {
let r = NoesisResources::new()
.solid("AccentBrush", [1.0, 0.0, 0.0, 1.0])
.value("PanelWidth", DpValue::F64(40.0))
.merged("<ResourceDictionary/>");
assert_eq!(
r.entries.get("AccentBrush"),
Some(&ResourceEntry::Brush(BrushSpec::Solid([
1.0, 0.0, 0.0, 1.0
]))),
);
assert_eq!(
r.entries.get("PanelWidth"),
Some(&ResourceEntry::Value(DpValue::F64(40.0))),
);
assert_eq!(r.merged_xaml, vec!["<ResourceDictionary/>".to_string()]);
}
}