pub mod cache;
pub mod components;
pub mod font_id;
pub mod layout;
pub mod parse;
pub mod systems;
pub use cache::{DynamicGlyphCache, GlyphAlphaMode, GlyphCacheConfig, GlyphInfo, GlyphKey};
pub use components::{
FontLayoutOverride, FontLayoutOverrides, GlyphBaseOffset, GlyphEntity, GlyphReveal,
LayoutGlyph, SegmentStyle, ShakeEffect, TextAlign, TextAnchor, TextBlock, TextBlockLayout,
TextBlockStyling, TextSegment, WaveEffect,
};
pub use font_id::FontId;
pub use parse::parse_text_to_segments;
pub use systems::BitmapTextSet;
use bevy::app::{App, Plugin, PostUpdate};
use bevy::ecs::schedule::IntoScheduleConfigs;
#[derive(bevy::prelude::Resource, Default)]
pub struct FontDirectories {
pub directories: Vec<String>,
}
#[derive(Default)]
pub struct BitmapTextPlugin {
pub atlas_config: GlyphCacheConfig,
}
impl Plugin for BitmapTextPlugin {
fn build(&self, app: &mut App) {
use systems::*;
let mut images = app
.world_mut()
.resource_mut::<bevy::asset::Assets<bevy::image::Image>>();
let cache = DynamicGlyphCache::new(self.atlas_config.clone(), &mut images);
app.insert_resource(cache);
app.init_resource::<FontLayoutOverrides>();
app.register_type::<FontId>()
.register_type::<TextAlign>()
.register_type::<TextAnchor>()
.register_type::<TextSegment>()
.register_type::<SegmentStyle>()
.register_type::<TextBlock>()
.register_type::<TextBlockStyling>()
.register_type::<TextBlockLayout>()
.register_type::<LayoutGlyph>()
.register_type::<GlyphEntity>()
.register_type::<GlyphBaseOffset>()
.register_type::<GlyphReveal>()
.register_type::<ShakeEffect>()
.register_type::<WaveEffect>();
app.configure_sets(
PostUpdate,
BitmapTextSet.before(bevy::transform::TransformSystems::Propagate),
);
app.add_systems(
PostUpdate,
(
rasterize_glyphs_system,
layout_text_system.after(rasterize_glyphs_system),
sync_glyph_entities_system.after(layout_text_system),
glyph_reveal_system.after(sync_glyph_entities_system),
)
.in_set(BitmapTextSet),
);
app.add_systems(bevy::app::Update, (text_shake_system, text_wave_system));
app.add_systems(bevy::app::Startup, load_fonts_from_directories);
}
}
pub fn load_fonts_from_directories(
dirs: Option<bevy::prelude::Res<FontDirectories>>,
mut cache: bevy::prelude::ResMut<DynamicGlyphCache>,
) {
let Some(dirs) = dirs else {
return;
};
for dir in &dirs.directories {
let Ok(entries) = std::fs::read_dir(dir) else {
log::warn!("Font directory not found: {}", dir);
continue;
};
for entry in entries.flatten() {
load_single_font(&entry.path(), &mut cache);
}
}
}
fn load_single_font(path: &std::path::Path, cache: &mut DynamicGlyphCache) {
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
if ext != "ttf" && ext != "otf" {
return;
}
let data = match std::fs::read(path) {
Ok(d) => d,
Err(e) => {
log::warn!("Failed to read font file {:?}: {}", path, e);
return;
}
};
let name = path
.file_stem()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
match cache.add_font(FontId::from_name(&name), &data) {
Ok(()) => log::info!("Loaded font: {} from {:?}", name, path),
Err(e) => log::warn!("Failed to load font {:?}: {}", path, e),
}
}