use std::sync::Arc;
use assets_manager::{
loader::{BytesLoader, LoadFrom, RonLoader},
Asset, AssetCache,
};
use cosmic_text::fontdb::FaceInfo;
use glam::Vec3Swizzles;
use glyphon::{
fontdb::Source, Attrs, Buffer, Color, FontSystem, Metrics, Shaping, SwashCache, TextArea,
TextBounds,
};
use sys_locale::get_locale;
use crate::screen::WORLD_HALF_EXTENTS;
pub struct TextSystem {
pub fonts: FontSystem,
pub swash: SwashCache,
glyphon_to_world: glam::Affine3A,
}
impl TextSystem {
pub fn new<S: assets_manager::source::Source>(
assets: &AssetCache<S>,
) -> color_eyre::Result<Self> {
let fonts_handle = assets.load_rec_dir::<FontData>("fonts")?;
let fonts: Vec<_> = fonts_handle
.read()
.iter(assets)
.filter_map(|maybe_font| match maybe_font {
Ok(font_handle) => {
let font_data: Arc<Vec<u8>> = Arc::new(font_handle.read().0.clone());
let id = font_handle.id();
Some((id, Source::Binary(font_data)))
}
Err(e) => {
log::info!("failed to recursively load a font: {e}");
None
}
})
.collect();
let locale = get_locale().unwrap_or_else(|| "en-US".to_string());
let mut db = glyphon::fontdb::Database::new();
#[cfg(not(target_arch = "wasm32"))]
{
db.load_system_fonts();
}
for (asset_id, source) in fonts.into_iter() {
let ids = db.load_font_source(source);
log::warn!("loaded {asset_id} into {ids}");
let font_info = ids.iter().find_map(|id| db.face(*id));
if font_info
.expect("fontdb failed to load faces from {asset_id}")
.monospaced
{
log::warn!("{asset_id} is monospaced");
}
for face_info in ids
.into_iter()
.filter_map(|id| db.face(id).cloned())
.collect::<Vec<_>>()
{
let FaceInfo {
ref families,
ref post_script_name,
ref style,
ref monospaced,
ref weight,
ref stretch,
..
} = face_info;
log::warn!(
"{families:?} {post_script_name} {style:?} {monospaced} {weight:?} {stretch:?}"
);
}
}
let fonts = FontSystem::new_with_locale_and_db(locale, db);
Ok(Self {
fonts,
swash: SwashCache::new(),
glyphon_to_world: (glam::Affine3A::from_translation(glam::Vec3::new(
-(WORLD_HALF_EXTENTS.x as f32),
-(WORLD_HALF_EXTENTS.y as f32),
0.0,
)) * glam::Affine3A::from_scale(glam::Vec3::new(
2.0 * WORLD_HALF_EXTENTS.x as f32,
2.0 * WORLD_HALF_EXTENTS.y as f32,
1.0,
)))
.inverse(),
})
}
}
struct FontData(Vec<u8>);
impl From<Vec<u8>> for FontData {
fn from(value: Vec<u8>) -> Self {
Self(value)
}
}
impl Asset for FontData {
const EXTENSIONS: &'static [&'static str] = &["ttf"];
type Loader = LoadFrom<Vec<u8>, BytesLoader>;
}
#[derive(serde::Deserialize, Debug, Clone, Copy)]
struct TextColor(
#[serde(deserialize_with = "palette::serde::deserialize_with_optional_alpha")] palette::Srgba,
);
#[derive(serde::Deserialize, Debug, Clone)]
struct Text {
font_size: f32,
#[serde(default)]
text_dims: Option<glam::Vec2>,
text: String,
#[serde(default)]
position: Option<glam::Vec2>,
#[serde(default)]
scale: Option<f32>,
#[serde(default)]
color: Option<TextColor>,
#[serde(default)]
lines: Option<usize>,
}
#[derive(serde::Deserialize)]
struct TextList(Vec<Text>);
impl Asset for TextList {
const EXTENSION: &'static str = "textl";
type Loader = RonLoader;
}
impl TextSystem {
pub fn prepare<S: assets_manager::source::Source>(
&mut self,
_data: &TextSystemData,
_assets: &AssetCache<S>,
) {
}
fn convert_world_to_glyphon_world(&self, point: glam::Vec2) -> glam::Vec2 {
let normalized = self.glyphon_to_world.transform_point3(point.extend(0.0));
normalized.xy() * 2.0 * WORLD_HALF_EXTENTS.as_vec2()
}
pub fn produce_world<S: assets_manager::source::Source>(
&mut self,
_data: &TextSystemData,
assets: &AssetCache<S>,
) -> impl IntoIterator<Item = (Buffer, BufferMapper)> {
let Ok(world_text) = assets.load::<TextList>("text_data.world") else {
let metrics = Metrics::new(32.0, 20.0);
let mut buffer = Buffer::new(&mut self.fonts, metrics);
{
let mut buffer = buffer.borrow_with(&mut self.fonts);
buffer.set_size(250.0, 25.0);
let attrs = Attrs::new().family(glyphon::Family::Name("Poiret One"));
buffer.set_text("Hello, game! 🦀\n", attrs, Shaping::Advanced);
buffer.shape_until_scroll(true);
}
let loc = glam::Vec2::new(32.0, 32.0);
let mut buffer2 = Buffer::new(&mut self.fonts, metrics);
{
let mut buffer2 = buffer2.borrow_with(&mut self.fonts);
buffer2.set_size(250.0, 25.0);
let attrs = Attrs::new()
.family(glyphon::Family::Name("Poiret One"))
.color(glyphon::Color::rgb(255, 255, 255));
buffer2.set_text("Y tho? 🦀\n", attrs, Shaping::Advanced);
buffer2.shape_until_scroll(true);
}
let loc2 = glam::Vec2::new(32.0, 64.0);
return vec![
(
buffer,
BufferMapper::new(self.convert_world_to_glyphon_world(loc), 1.0),
),
(
buffer2,
BufferMapper::new(self.convert_world_to_glyphon_world(loc2), 1.0),
),
];
};
world_text
.read()
.0
.iter()
.map(|text| {
let metrics = Metrics::new(text.font_size, 20.0);
let mut buffer = Buffer::new(&mut self.fonts, metrics);
{
let mut buffer = buffer.borrow_with(&mut self.fonts);
if let Some(text_dims) = text.text_dims {
buffer.set_size(text_dims.x, text_dims.y);
}
let mut attrs = Attrs::new();
if let Some(color) = text.color {
let color: palette::Srgba<u8> = color.0.into_format();
attrs = attrs.color(glyphon::Color::rgba(
color.red,
color.green,
color.blue,
color.alpha,
));
}
buffer.set_rich_text(
[(
text.text.as_str(),
attrs.family(glyphon::Family::Name("Poiret One")),
)],
attrs,
Shaping::Advanced,
);
for line_idx in 0..text.lines.unwrap_or(1) {
buffer.line_shape(line_idx);
buffer.line_layout(line_idx);
}
}
(
buffer,
BufferMapper::new(
self.convert_world_to_glyphon_world(text.position.unwrap_or_default()),
text.scale.unwrap_or(1.0),
),
)
})
.collect()
}
pub fn produce_camera<S: assets_manager::source::Source>(
&mut self,
_data: &TextSystemData,
assets: &AssetCache<S>,
) -> impl IntoIterator<Item = (Buffer, BufferMapper)> {
let Ok(cam_text) = assets.load::<TextList>("text_data.camera") else {
let metrics = Metrics::new(32.0, 20.0);
let mut buffer = Buffer::new(&mut self.fonts, metrics);
{
let mut buffer = buffer.borrow_with(&mut self.fonts);
buffer.set_size(200.0, 25.0);
let attrs = Attrs::new().family(glyphon::Family::Name("Poiret One"));
buffer.set_text("Hello, Rust! 🦀\n", attrs, Shaping::Advanced);
buffer.shape_until_scroll(true);
}
return vec![(buffer, BufferMapper::new((32.0, 32.0).into(), 1.0))];
};
cam_text
.read()
.0
.iter()
.map(|text| {
let metrics = Metrics::new(text.font_size, 20.0);
let mut buffer = Buffer::new(&mut self.fonts, metrics);
{
let mut buffer = buffer.borrow_with(&mut self.fonts);
if let Some(text_dims) = text.text_dims {
buffer.set_size(text_dims.x, text_dims.y);
}
let attrs = Attrs::new().family(glyphon::Family::Name("Poiret One"));
buffer.set_text(
&text.text,
if let Some(color) = text.color {
let color: palette::Srgba<u8> = color.0.into_format();
attrs.color(glyphon::Color::rgba(
color.red,
color.green,
color.blue,
color.alpha,
))
} else {
attrs
},
Shaping::Advanced,
);
buffer.shape_until_scroll(true);
}
(
buffer,
BufferMapper::new(text.position.unwrap_or_default(), text.scale.unwrap_or(1.0)),
)
})
.collect()
}
}
pub struct BufferMapper {
position: glam::Vec2,
scale: f32,
}
impl BufferMapper {
fn new(position: glam::Vec2, scale: f32) -> Self {
Self { position, scale }
}
pub(crate) fn produce<'a>(&self, buffer: &'a Buffer) -> TextArea<'a> {
TextArea {
buffer,
left: self.position.x,
top: self.position.y,
scale: self.scale,
bounds: TextBounds {
..Default::default()
},
default_color: Color::rgba(255, 0, 0, 200),
}
}
}
pub struct TextSystemData {}
impl TextSystemData {
pub fn extract(_world: &mut hecs::World) -> Self {
Self {}
}
}