use anyhow::{anyhow, Error, Result};
use bevy::{
asset::{io::Reader, AssetLoader, LoadContext},
image::{CompressedImageFormats, ImageLoader, ImageLoaderSettings},
render::{
render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin},
sync_component::SyncComponentPlugin,
sync_world::RenderEntity,
Extract, RenderApp,
},
utils::HashMap,
};
use serde::{Deserialize, Serialize};
use crate::{
animation::AnimatedAssetComponent, image::PxImage, palette::asset_palette,
position::DefaultLayer, position::PxLayer, prelude::*,
};
pub(crate) fn plug<L: PxLayer>(app: &mut App) {
app.add_plugins((
RenderAssetPlugin::<PxTypeface>::default(),
SyncComponentPlugin::<PxText>::default(),
))
.init_asset::<PxTypeface>()
.init_asset_loader::<PxTypefaceLoader>()
.sub_app_mut(RenderApp)
.add_systems(ExtractSchedule, extract_texts::<L>);
}
#[derive(Serialize, Deserialize)]
struct PxTypefaceLoaderSettings {
default_frames: u32,
characters: String,
character_frames: HashMap<char, u32>,
separator_widths: HashMap<char, u32>,
image_loader_settings: ImageLoaderSettings,
}
impl Default for PxTypefaceLoaderSettings {
fn default() -> Self {
Self {
default_frames: 1,
characters: String::new(),
character_frames: HashMap::new(),
separator_widths: HashMap::new(),
image_loader_settings: default(),
}
}
}
#[derive(Default)]
struct PxTypefaceLoader;
impl AssetLoader for PxTypefaceLoader {
type Asset = PxTypeface;
type Settings = PxTypefaceLoaderSettings;
type Error = Error;
async fn load(
&self,
reader: &mut dyn Reader,
settings: &PxTypefaceLoaderSettings,
load_context: &mut LoadContext<'_>,
) -> Result<PxTypeface> {
let image = ImageLoader::new(CompressedImageFormats::NONE)
.load(reader, &settings.image_loader_settings, load_context)
.await?;
let palette = asset_palette().await;
let indices = PxImage::palette_indices(palette, &image)?;
let height = indices.height();
let character_count = settings.characters.chars().count();
let characters = if character_count == 0 {
HashMap::new()
} else {
settings
.characters
.chars()
.zip(indices.split_vert(height / character_count).into_iter())
.map(|(character, mut image)| {
image.trim_right();
let image_width = image.width();
let image_area = image.area();
let frames = settings
.character_frames
.get(&character)
.copied()
.unwrap_or(settings.default_frames)
as usize;
(
character,
PxSpriteAsset {
data: PxImage::from_parts_vert(image.split_horz(image_width / frames))
.unwrap(),
frame_size: image_area / frames,
},
)
})
.collect::<HashMap<_, _>>()
};
let max_frame_count =
characters
.values()
.fold(0, |max, character| match character.frame_size > max {
true => character.frame_size,
false => max,
});
Ok(PxTypeface {
height: if image.texture_descriptor.size.height == 0 {
0
} else if settings.characters.is_empty() {
return Err(anyhow!(
"Typeface `{}` was assigned no characters. \
If no `.meta` file exists for that asset, create one. \
See `assets/typeface/` for examples.",
load_context.path().display()
));
} else {
image.texture_descriptor.size.height / character_count as u32
},
characters,
separators: settings
.separator_widths
.iter()
.map(|(&separator, &width)| (separator, PxSeparator { width }))
.collect(),
max_frame_count,
})
}
fn extensions(&self) -> &[&str] {
&["px_typeface.png"]
}
}
#[derive(Clone, Debug, Reflect)]
pub(crate) struct PxSeparator {
pub(crate) width: u32,
}
#[derive(Asset, Clone, Reflect, Debug)]
pub struct PxTypeface {
pub(crate) height: u32,
pub(crate) characters: HashMap<char, PxSpriteAsset>,
pub(crate) separators: HashMap<char, PxSeparator>,
pub(crate) max_frame_count: usize,
}
impl RenderAsset for PxTypeface {
type SourceAsset = Self;
type Param = ();
fn prepare_asset(
source_asset: Self,
&mut (): &mut (),
) -> Result<Self, PrepareAssetError<Self>> {
Ok(source_asset)
}
}
#[derive(Component, Default, Clone, Debug)]
#[require(PxRect, PxAnchor, DefaultLayer, PxCanvas, Visibility)]
pub struct PxText {
pub value: String,
pub typeface: Handle<PxTypeface>,
}
impl AnimatedAssetComponent for PxText {
type Asset = PxTypeface;
fn handle(&self) -> &Handle<Self::Asset> {
&self.typeface
}
fn max_frame_count(typeface: &PxTypeface) -> usize {
typeface.max_frame_count
}
}
pub(crate) type TextComponents<L> = (
&'static PxText,
&'static PxRect,
&'static PxAnchor,
&'static L,
&'static PxCanvas,
Option<&'static PxAnimation>,
Option<&'static PxFilter>,
);
fn extract_texts<L: PxLayer>(
texts: Extract<Query<(TextComponents<L>, &InheritedVisibility, RenderEntity)>>,
mut cmd: Commands,
) {
for ((text, &rect, &alignment, layer, &canvas, animation, filter), visibility, id) in &texts {
if !visibility.get() {
continue;
}
let mut entity = cmd.entity(id);
entity.insert((text.clone(), rect, alignment, layer.clone(), canvas));
if let Some(animation) = animation {
entity.insert(*animation);
} else {
entity.remove::<PxAnimation>();
}
if let Some(filter) = filter {
entity.insert(filter.clone());
} else {
entity.remove::<PxFilter>();
}
}
}