use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use bevy::asset::AssetLoadFailedEvent;
use bevy::prelude::*;
use bevy_cobweb::prelude::*;
use fluent_langneg::LanguageIdentifier;
use smol_str::SmolStr;
use crate::prelude::*;
struct SelectedLocalizedFontFallback
{
lang: SmolStr,
font: String,
}
fn get_loaded_font_variants<'a>(
families: &'a HashMap<SmolStr, Vec<FontVariant>>,
family: &'a SmolStr,
attrs: &'a [FontAttributes],
) -> impl Iterator<Item = &'a FontVariant> + 'a
{
families
.get(family)
.or_else(|| {
tracing::error!("ignoring loaded font ({family:?}, {attrs:?}) with unknown family; this is a bug");
None
})
.into_iter()
.flat_map(|v| v.iter())
.filter_map(|v| {
if attrs.len() > 0 {
let v_attrs = v.attributes();
if !attrs.iter().any(|attrs| *attrs == v_attrs) {
return None;
}
}
Some(v)
})
}
fn get_dependent_font_variants(
families: &HashMap<SmolStr, Vec<FontVariant>>,
reference_font_family: &SmolStr,
reference_font: FontAttributes,
loaded_fallbacks: &Vec<LocalizedFontFallback>,
) -> Vec<SelectedLocalizedFontFallback>
{
let mut fallbacks = Vec::<SelectedLocalizedFontFallback>::default();
for LocalizedFontFallback { lang, family, attributes } in loaded_fallbacks {
let Some(variants) = families.get(family) else {
tracing::error!("failed getting fallback fonts for font family {reference_font_family:?} for language \
{lang:?}; fallback font family {family:?} is unknown (fallback attrs: {attributes:?}); \
this is a bug");
continue;
};
let attributes = if attributes.len() == 0 {
reference_font.negotiate_eligible_fonts(|| variants.iter().map(|v| v.attributes()))
} else {
reference_font.negotiate_eligible_fonts(|| attributes.iter().cloned())
};
let Some(attributes) = attributes else {
tracing::warn!("failed getting fallback fonts for font family {reference_font_family:?} with \
{reference_font:?} for language {lang:?}; unable to select eligible fallback font for fallback \
font family {family:?} (fallback attrs: {attributes:?}, registered variants: {variants:?})");
continue;
};
let Some(variant) = variants.iter().find(|v| v.attributes() == attributes) else {
tracing::warn!("failed getting fallback fonts for font family {reference_font_family:?} with \
{reference_font:?} for language {lang:?}; selected font attributes don't match any registered \
font variants for fallback font family {family:?} (selected: {attributes:?}, fallback attrs: \
{attributes:?}, registered variants: {variants:?})");
continue;
};
fallbacks.push(SelectedLocalizedFontFallback { lang: lang.clone(), font: variant.path.clone() })
}
fallbacks
}
fn get_eligible_font<'a>(
families: &'a HashMap<SmolStr, Vec<FontVariant>>,
font: &FontRequest,
) -> Option<&'a FontVariant>
{
let variants = families.get(&**font.family)?;
let attributes = font
.attributes()
.negotiate_eligible_fonts(|| variants.iter().map(|v| v.attributes()))?;
let Some(variant) = variants.iter().find(|v| v.attributes() == attributes) else {
tracing::error!("failed negotiating eligible fonts for request {font:?} and variants {variants:?} \
even though a variant was presumably found ({attributes:?}); this is a bug");
return None;
};
Some(variant)
}
fn load_localized_fonts(
In(loaded): In<Vec<LocalizedFont>>,
mut c: Commands,
asset_server: Res<AssetServer>,
mut fonts: ResMut<FontMap>,
manifest: Res<LocalizationManifest>,
)
{
fonts.insert_localized(loaded, &asset_server, &manifest, &mut c);
}
fn load_fonts(In(families): In<Vec<SmolStr>>, asset_server: Res<AssetServer>, mut fonts: ResMut<FontMap>)
{
for family in families {
fonts.load(&FontFamily(family), &asset_server);
}
}
fn register_font_families(
In(registrations): In<Vec<RegisterFontFamily>>,
mut c: Commands,
asset_server: Res<AssetServer>,
mut fonts: ResMut<FontMap>,
manifest: Res<LocalizationManifest>,
)
{
for registration in registrations {
fonts.register(registration, &asset_server, &manifest, &mut c);
}
}
fn handle_new_lang_list(
asset_server: Res<AssetServer>,
manifest: Res<LocalizationManifest>,
mut fonts: ResMut<FontMap>,
)
{
fonts.negotiate_languages(&manifest, &asset_server);
}
fn check_loaded_fonts(
mut c: Commands,
mut events: MessageReader<AssetEvent<Font>>,
mut errors: MessageReader<AssetLoadFailedEvent<Font>>,
mut fonts: ResMut<FontMap>,
)
{
for event in events.read() {
let AssetEvent::Added { id } = event else { continue };
fonts.remove_pending(id);
}
for error in errors.read() {
let AssetLoadFailedEvent { id, .. } = error;
fonts.remove_pending(id);
}
fonts.try_emit_load_event(&mut c);
}
pub struct FontMapLoaded;
#[derive(Resource, Default)]
pub struct FontMap
{
waiting_for_load: bool,
pending: HashSet<AssetId<Font>>,
loaded_awaiting_families: Vec<FontFamily>,
localized_awaiting_families: Vec<LocalizedFont>,
families: HashMap<SmolStr, Vec<FontVariant>>,
cached_fonts: HashMap<String, Handle<Font>>,
localization_map: HashMap<AssetId<Font>, HashMap<LanguageIdentifier, String>>,
localization_fonts: HashMap<String, Handle<Font>>,
}
impl FontMap
{
pub fn is_loading(&self) -> bool
{
!self.pending.is_empty()
}
fn try_add_pending(handle: &Handle<Font>, asset_server: &AssetServer, pending: &mut HashSet<AssetId<Font>>)
{
match asset_server.load_state(handle) {
bevy::asset::LoadState::Loaded => (),
_ => {
pending.insert(handle.id());
}
}
}
fn try_emit_load_event(&mut self, c: &mut Commands)
{
if self.is_loading() {
return;
}
if !self.waiting_for_load {
return;
}
self.waiting_for_load = false;
c.react().broadcast(FontMapLoaded);
}
fn negotiate_languages(&mut self, manifest: &LocalizationManifest, asset_server: &AssetServer) -> bool
{
let negotiated = manifest.negotiated();
if negotiated.len() == 0 {
return false;
}
let prev_localization_fonts = std::mem::take(&mut self.localization_fonts);
self.localization_fonts
.reserve(prev_localization_fonts.len());
self.localization_map.iter().for_each(|(_main, fallbacks)| {
for (_lang, font) in fallbacks
.iter()
.filter(|(lang, _)| negotiated.iter().any(|n| n == *lang))
{
let handle = prev_localization_fonts
.get(font)
.or_else(|| self.cached_fonts.get(font))
.cloned()
.unwrap_or_else(|| {
let new_handle = asset_server.load(String::from(font.as_str()));
Self::try_add_pending(&new_handle, asset_server, &mut self.pending);
new_handle
});
self.localization_fonts.insert(font.clone(), handle);
}
});
true
}
fn remove_pending(&mut self, id: &AssetId<Font>) -> bool
{
self.pending.remove(id)
}
fn check_unresolved_font_requests(map: Res<FontMap>)
{
for loaded in &map.loaded_awaiting_families {
tracing::warn!("failed loading requested font {loaded:?} before LoadState::Done; use RegisterFontFamilies");
}
for loaded in &map.localized_awaiting_families {
tracing::warn!("failed loading localized font {loaded:?} or one of its fallbacks before LoadState::Done; \
use RegisterFontFamilies");
}
}
pub fn register(
&mut self,
family: RegisterFontFamily,
asset_server: &AssetServer,
manifest: &LocalizationManifest,
c: &mut Commands,
)
{
if family.fonts.len() == 0 {
tracing::warn!("ignoring font registration {family:?} with no variants");
return;
}
if let Some(mut prev) = self
.families
.insert(family.family.deref().clone(), family.fonts)
{
tracing::warn!("overwriting font family {:?}; old font variants: {prev:?} (\"manual::\" fonts are not \
discarded)", family.family);
let mut manual: Vec<_> = prev
.drain(..)
.filter(|v| v.path.starts_with("manual::"))
.collect();
if manual.len() > 0 {
self.families
.get_mut(&*family.family)
.unwrap()
.extend(manual.drain(..));
}
}
let mut to_insert = Vec::<FontFamily>::default();
self.loaded_awaiting_families.retain(|l| {
if family.family == *l {
to_insert.push(l.clone());
false;
}
true
});
to_insert.drain(..).for_each(|l| {
self.load(&l, asset_server);
});
let mut to_insert = Vec::<LocalizedFont>::default();
self.localized_awaiting_families.retain(|l| {
if self.families.contains_key(&l.family)
&& !l
.fallbacks
.iter()
.any(|fallback| !self.families.contains_key(&fallback.family))
{
to_insert.push(l.clone());
false;
}
true
});
self.insert_localized(to_insert, asset_server, manifest, c);
}
pub fn load(&mut self, family: &FontFamily, asset_server: &AssetServer) -> bool
{
let Some(variants) = self.families.get(&**family) else {
self.loaded_awaiting_families.push(family.clone());
return false;
};
variants.iter().for_each(|v| {
Self::get_or_load_impl(
&mut self.pending,
&mut self.cached_fonts,
&self.localization_fonts,
v,
asset_server,
);
});
true
}
pub fn insert_localized(
&mut self,
mut loaded: Vec<LocalizedFont>,
asset_server: &AssetServer,
manifest: &LocalizationManifest,
c: &mut Commands,
)
{
let mut loaded2 = Vec::with_capacity(loaded.len());
for font in loaded.drain(..) {
if !self.families.contains_key(&font.family)
|| font
.fallbacks
.iter()
.any(|fallback| !self.families.contains_key(&fallback.family))
{
self.localized_awaiting_families.push(font);
} else {
loaded2.push(font);
}
}
for (font_variant, mut loaded_fallbacks) in loaded2.iter().flat_map(|loaded| {
get_loaded_font_variants(&self.families, &loaded.family, &loaded.attributes).map(|font_variant| {
let fallbacks = get_dependent_font_variants(
&self.families,
&loaded.family,
font_variant.attributes(),
&loaded.fallbacks,
);
(font_variant, fallbacks)
})
}) {
let loaded_font = &font_variant.path;
let main_handle = self
.cached_fonts
.get(loaded_font)
.cloned()
.or_else(|| {
self.localization_fonts
.get(loaded_font)
.inspect(|handle| {
self.cached_fonts
.insert(loaded_font.clone(), (*handle).clone());
})
.cloned()
})
.unwrap_or_else(|| {
let new_handle = asset_server.load(String::from(loaded_font.as_str()));
self.cached_fonts
.insert(loaded_font.clone(), new_handle.clone());
Self::try_add_pending(&new_handle, asset_server, &mut self.pending);
new_handle
});
let fallbacks = self.localization_map.entry(main_handle.id()).or_default();
#[cfg(not(feature = "hot_reload"))]
if fallbacks.len() > 0 {
tracing::warn!("overwritting font fallbacks for main font {:?}; main fonts should only appear in one \
LoadFonts command per app", loaded_font);
}
fallbacks.clear();
fallbacks.reserve(loaded_fallbacks.len());
for SelectedLocalizedFontFallback { lang, font } in loaded_fallbacks.drain(..) {
let lang_id = match LanguageIdentifier::from_str(lang.as_str()) {
Ok(lang_id) => lang_id,
Err(err) => {
tracing::error!("failed parsing target language id for font fallback {:?} for main font \
{:?}: {:?}", font, loaded_font, err);
continue;
}
};
if let Some(prev) = fallbacks.insert(lang_id, font) {
tracing::warn!("overwriting font fallback {:?} for font {:?} for lang {:?}",
prev, loaded_font, lang);
}
}
}
if self.negotiate_languages(manifest, asset_server) {
self.waiting_for_load = true;
self.try_emit_load_event(c);
}
}
pub fn get_eligible_font(&self, font: &FontRequest) -> Option<&FontVariant>
{
get_eligible_font(&self.families, font)
}
pub fn get(&self, font: &FontRequest) -> Handle<Font>
{
let Some(eligible_font) = self.get_eligible_font(font) else {
tracing::error!("failed font request {:?} that has no eligible fonts; use RegisterFontFamilies command", font);
return Default::default();
};
let Some(entry) = self.cached_fonts.get(&eligible_font.path) else {
tracing::error!("failed getting font {:?} that was not loaded; use LoadFonts command or \
FontMap::insert", font);
return Default::default();
};
entry.clone()
}
pub fn get_or_load(&mut self, font: &FontRequest, asset_server: &AssetServer) -> Handle<Font>
{
let Some(eligible_font) = get_eligible_font(&self.families, font) else {
tracing::error!("failed font request {:?} that has no eligible fonts; use RegisterFontFamilies command", font);
return Default::default();
};
Self::get_or_load_impl(
&mut self.pending,
&mut self.cached_fonts,
&self.localization_fonts,
eligible_font,
asset_server,
)
}
fn get_or_load_impl(
pending: &mut HashSet<AssetId<Font>>,
cached_fonts: &mut HashMap<String, Handle<Font>>,
localization_fonts: &HashMap<String, Handle<Font>>,
eligible_font: &FontVariant,
asset_server: &AssetServer,
) -> Handle<Font>
{
let path = &eligible_font.path;
cached_fonts
.get(path)
.cloned()
.or_else(|| {
localization_fonts
.get(path)
.inspect(|handle| {
cached_fonts.insert(path.clone(), (*handle).clone());
})
.cloned()
})
.unwrap_or_else(|| {
let new_handle = asset_server.load(String::from(path.as_str()));
cached_fonts.insert(path.clone(), new_handle.clone());
Self::try_add_pending(&new_handle, asset_server, pending);
new_handle
})
}
pub fn manual_insert(&mut self, font: Handle<Font>, req: FontRequest)
{
let path = format!("manual::{}::{:?}::{:?}::{:?}", req.family.as_str(), req.width, req.style, req.weight);
let variant = FontVariant {
path: path.clone(),
width: req.width,
style: req.style,
weight: req.weight,
};
if let Some(_) = self.cached_fonts.insert(path, font) {
tracing::warn!("overwriting handle for manually inserted font {:?}", req);
}
let Some(existing) = self.families.get_mut(&*req.family) else {
self.families.insert(req.family.0, vec![variant]);
return;
};
existing.push(variant);
}
pub fn get_localized(&self, lang_id: &LanguageIdentifier, main_font: AssetId<Font>) -> Option<Handle<Font>>
{
self.localization_map
.get(&main_font)
.and_then(|fallbacks| fallbacks.get(lang_id))
.and_then(|lang_font| {
self.localization_fonts.get(lang_font).or_else(|| {
tracing::error!("font fallback {:?} is missing from loaded fonts, the requested language {:?} \
is probably not in the negotiated languages list of LocalizationManifest", lang_font, lang_id);
None
})
})
.cloned()
}
pub fn get_or_load_localized(
&mut self,
lang_id: &LanguageIdentifier,
main_font: &FontRequest,
asset_server: &AssetServer,
) -> Handle<Font>
{
let Some(eligible_font) = get_eligible_font(&self.families, main_font) else {
tracing::error!("failed get-or-loaded-localized font request {:?} that has no eligible fonts; use \
RegisterFontFamilies command", main_font);
return Default::default();
};
let main_font = &eligible_font.path;
let mut to_cache: Option<(&String, Handle<Font>)> = None;
let req_handle = self.cached_fonts
.get(main_font)
.map(|main| {
self.localization_map
.get(&main.id())
.and_then(|fallbacks| fallbacks.get(lang_id))
.map(|lang_font| {
self.cached_fonts.get(lang_font)
.or_else(|| {
self.localization_fonts.get(lang_font).inspect(|handle| {
to_cache = Some((lang_font, (*handle).clone()));
})
})
.cloned()
.unwrap_or_else(|| {
let new_handle = asset_server.load(String::from(lang_font.as_str()));
to_cache = Some((lang_font, new_handle.clone()));
Self::try_add_pending(&new_handle, asset_server, &mut self.pending);
new_handle
})
})
.unwrap_or_else(|| {
tracing::debug!("failed get-or-load-localized font, requested main font does not have a fallback \
for language {:?}; returning main font {:?} instead", lang_id, main_font);
main.clone()
})
})
.unwrap_or_else(|| {
tracing::debug!("failed get-or-load-localized font, requested main font {:?} is not registered, loading it \
directy instead for language {:?}", main_font, lang_id);
let new_handle = asset_server.load(String::from(main_font.as_str()));
to_cache = Some((main_font, new_handle.clone()));
Self::try_add_pending(&new_handle, asset_server, &mut self.pending);
new_handle
});
if let Some((path, handle)) = to_cache {
self.cached_fonts.insert(path.clone(), handle);
}
req_handle
}
}
impl AssetLoadProgress for FontMap
{
fn pending_assets(&self) -> usize
{
self.pending.len()
}
fn total_assets(&self) -> usize
{
self.localization_fonts.len() + self.cached_fonts.len()
}
}
#[derive(Reflect, Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LocalizedFontFallback
{
pub lang: SmolStr,
pub family: SmolStr,
#[reflect(default)]
pub attributes: Vec<FontAttributes>,
}
#[derive(Reflect, Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LocalizedFont
{
pub family: SmolStr,
#[reflect(default)]
pub attributes: Vec<FontAttributes>,
#[reflect(default)]
pub fallbacks: Vec<LocalizedFontFallback>,
}
#[derive(Reflect, Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FontVariant
{
pub path: String,
#[reflect(default)]
pub width: FontWidth,
#[reflect(default)]
pub style: FontStyle,
#[reflect(default)]
pub weight: FontWeight,
}
impl FontVariant
{
pub fn attributes(&self) -> FontAttributes
{
FontAttributes { width: self.width, style: self.style, weight: self.weight }
}
}
#[derive(Reflect, Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RegisterFontFamily
{
pub family: FontFamily,
pub fonts: Vec<FontVariant>,
}
#[derive(Reflect, Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LoadLocalizedFonts(pub Vec<LocalizedFont>);
impl Command for LoadLocalizedFonts
{
fn apply(self, world: &mut World)
{
world.syscall(self.0, load_localized_fonts);
}
}
#[derive(Reflect, Default, Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub struct LoadFonts(pub Vec<SmolStr>);
impl Command for LoadFonts
{
fn apply(self, world: &mut World)
{
world.syscall(self.0, load_fonts);
}
}
#[derive(Reflect, Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RegisterFontFamilies(pub Vec<RegisterFontFamily>);
impl Command for RegisterFontFamilies
{
fn apply(self, world: &mut World)
{
world.syscall(self.0, register_font_families);
}
}
pub(crate) struct FontLoadPlugin;
impl Plugin for FontLoadPlugin
{
fn build(&self, app: &mut App)
{
app.init_resource::<FontMap>()
.register_asset_tracker::<FontMap>()
.register_command_type::<LoadFonts>()
.register_command_type::<LoadLocalizedFonts>()
.register_command_type::<RegisterFontFamilies>()
.add_reactor(broadcast::<LanguagesNegotiated>(), handle_new_lang_list)
.add_systems(OnEnter(LoadState::Done), FontMap::check_unresolved_font_requests)
.add_systems(PreUpdate, check_loaded_fonts.in_set(LoadProgressSet::Prepare));
}
}