use super::{
AddText, FontFaceEntry, FontRegistry, FontResource, LocalizedText, LocalizedTextFormat,
TypographyFontStyle, apply_text_transform,
};
use crate::build_pending::UiBuildPending;
use crate::style::{mono_font_asset_path, sans_font_asset_path, serif_font_asset_path};
use crate::theme_config::ui_theme_asset_exists;
use bevy::prelude::*;
use bevy::text::FontWeight;
use bevy_localization::Localization;
pub(super) fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
font_registry: Option<Res<FontRegistry>>,
font_resource: Option<Res<FontResource>>,
) {
if font_registry.is_some() {
return;
}
let mut registry = FontRegistry::default();
if let Some(path) = non_empty_path(sans_font_asset_path()) {
if ui_theme_asset_exists(&path) {
registry.sans.push(FontFaceEntry {
handle: asset_server.load(path),
weight: FontWeight::NORMAL,
style: TypographyFontStyle::Normal,
});
} else {
bevy::log::warn!(
"theme font asset not found for --font-sans: {}; falling back",
path
);
}
}
if let Some(path) = non_empty_path(serif_font_asset_path()) {
if ui_theme_asset_exists(&path) {
registry.serif.push(FontFaceEntry {
handle: asset_server.load(path),
weight: FontWeight::NORMAL,
style: TypographyFontStyle::Normal,
});
} else {
bevy::log::warn!(
"theme font asset not found for --font-serif: {}; falling back",
path
);
}
}
if let Some(path) = non_empty_path(mono_font_asset_path()) {
if ui_theme_asset_exists(&path) {
registry.mono.push(FontFaceEntry {
handle: asset_server.load(path),
weight: FontWeight::NORMAL,
style: TypographyFontStyle::Normal,
});
} else {
bevy::log::warn!(
"theme font asset not found for --font-mono: {}; falling back",
path
);
}
}
if registry.sans.is_empty()
&& registry.serif.is_empty()
&& registry.mono.is_empty()
&& font_resource.is_none()
{
commands.insert_resource(FontResource::default());
return;
}
commands.insert_resource(registry);
if font_resource.is_none() {
commands.insert_resource(FontResource::default());
}
}
pub(super) fn add_text(
mut commands: Commands,
query: Query<(Entity, &AddText)>,
font_registry: Option<Res<FontRegistry>>,
font_resource: Res<FontResource>,
localization: Option<Res<Localization>>,
) {
for (entity, add_text) in query {
let Ok(mut entity_commands) = commands.get_entity(entity) else {
continue;
};
debug_assert!(
!(add_text.localized_text.is_some() && add_text.localized_text_format.is_some()),
"text entity cannot bind both LocalizedText and LocalizedTextFormat"
);
let initial_text = match (
localization.as_deref(),
add_text.localized_text,
add_text.localized_text_format.clone(),
) {
(Some(localization), Some(key), _) => localization.text(key).to_string(),
(Some(localization), _, Some(localized_text_format)) => localization.format_text(
localized_text_format.key,
localized_text_format
.args
.iter()
.map(|arg| (arg.name, arg.value.as_str())),
),
_ => add_text.text.clone(),
};
let initial_text = apply_text_transform(&initial_text, add_text.typography.text_transform);
let font_handle = font_registry
.as_deref()
.and_then(|registry| registry.resolve(&add_text.typography))
.or_else(|| font_resource.primary_font.clone());
let mut text_font = font_handle.map(TextFont::from).unwrap_or_default();
text_font.font_size = add_text.typography.font_size;
text_font.weight = add_text.typography.font_weight;
entity_commands.try_insert((
Text::new(initial_text),
text_font,
add_text.typography.line_height,
add_text.layout.clone(),
TextColor(add_text.color),
));
if let Some(localized_text_format) = add_text.localized_text_format.clone() {
entity_commands.try_remove::<LocalizedText>();
entity_commands.try_insert(localized_text_format);
} else if let Some(key) = add_text.localized_text {
entity_commands.try_remove::<LocalizedTextFormat>();
entity_commands.try_insert(LocalizedText { key });
} else {
entity_commands.try_remove::<LocalizedTextFormat>();
entity_commands.try_remove::<LocalizedText>();
}
entity_commands.try_remove::<AddText>();
entity_commands.try_remove::<UiBuildPending>();
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::asset::AssetPlugin;
use bevy::ecs::system::SystemState;
#[test]
fn add_text_ignores_entities_despawned_before_apply() {
let mut app = App::new();
app.insert_resource(FontResource::default())
.register_required_components::<AddText, UiBuildPending>();
let entity = app.world_mut().spawn(AddText::default()).id();
let mut system_state: SystemState<(
Commands,
Query<(Entity, &AddText)>,
Option<Res<FontRegistry>>,
Res<FontResource>,
Option<Res<Localization>>,
)> = SystemState::new(app.world_mut());
let (commands, query, font_registry, font_resource, localization) =
system_state.get_mut(app.world_mut());
add_text(commands, query, font_registry, font_resource, localization);
app.world_mut().despawn(entity);
system_state.apply(app.world_mut());
}
#[test]
fn setup_keeps_existing_font_resource() {
let mut app = App::new();
app.add_plugins(AssetPlugin::default());
app.insert_resource(FontResource::default());
let mut system_state: SystemState<(
Commands,
Res<AssetServer>,
Option<Res<FontRegistry>>,
Option<Res<FontResource>>,
)> =
SystemState::new(app.world_mut());
let (commands, asset_server, font_registry, font_resource) =
system_state.get_mut(app.world_mut());
setup(commands, asset_server, font_registry, font_resource);
system_state.apply(app.world_mut());
let font_resource = app.world().resource::<FontResource>();
assert!(font_resource.primary_font.is_none());
}
}
fn non_empty_path(path: String) -> Option<String> {
let path = path.trim().to_string();
(!path.is_empty()).then_some(path)
}