use std::{
collections::VecDeque,
num::NonZero,
ops::{Deref, DerefMut},
sync::{Arc, Mutex, MutexGuard},
};
use bevy::{
asset::{AssetId, Assets},
ecs::resource::Resource,
image::Image,
};
use cosmic_text::{
ttf_parser::Face, Attrs, Buffer, Family, FontSystem, Metrics, Shaping, Style, Weight,
};
use crate::{
render::{cache_glyph, CommandEncoder},
Text3dPlugin, TextAtlas,
};
#[derive(Debug, Resource, Clone)]
pub struct TextRenderer(pub(crate) Arc<Mutex<TextRendererInner>>);
impl TextRenderer {
pub fn new(font_system: FontSystem) -> Self {
Self(Arc::new(Mutex::new(TextRendererInner {
font_system,
queue: VecDeque::new(),
})))
}
pub fn lock(&mut self) -> FontSystemGuard {
FontSystemGuard(self.0.lock().unwrap())
}
pub fn try_lock(&mut self) -> Option<FontSystemGuard> {
self.0.try_lock().ok().map(FontSystemGuard)
}
}
pub struct FontSystemGuard<'t>(MutexGuard<'t, TextRendererInner>);
impl Deref for FontSystemGuard<'_> {
type Target = FontSystem;
fn deref(&self) -> &Self::Target {
&self.0.font_system
}
}
impl DerefMut for FontSystemGuard<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0.font_system
}
}
#[derive(Debug)]
pub(crate) struct TextRendererInner {
pub(crate) font_system: FontSystem,
pub(crate) queue: VecDeque<(AssetId<TextAtlas>, TextAtlas, Image)>,
}
#[derive(Debug, Clone, Default)]
pub struct DrawStyle {
pub family: Arc<str>,
pub size: f32,
pub stroke: Option<NonZero<u32>>,
pub weight: Weight,
pub style: Style,
}
pub(crate) fn family(name: &str) -> Family {
match name {
"" | "serif" => Family::Serif,
"sans-serif" => Family::SansSerif,
"monospace" => Family::Monospace,
"cursive" => Family::Cursive,
"fantasy" => Family::Fantasy,
_ => Family::Name(name),
}
}
impl DrawStyle {
pub fn as_attrs(&self) -> Attrs {
Attrs::new()
.family(family(&self.family))
.weight(self.weight)
.style(self.style)
}
}
pub trait TextProgressReportCallback: Send + Sync + 'static {
fn glyph_drawn(&mut self) {}
fn style_drawn(&mut self) {}
fn atlas_drawn(&mut self) {}
}
impl TextProgressReportCallback for () {}
impl TextRenderer {
pub fn prepare_task<S, I>(
&self,
settings: &Text3dPlugin,
workload: impl IntoIterator<Item = (AssetId<TextAtlas>, TextAtlas, Image, I)>
+ Send
+ Sync
+ 'static,
mut callback: impl TextProgressReportCallback,
) -> impl FnOnce() + Send + Sync + 'static
where
S: AsRef<str> + 'static,
I: IntoIterator<Item = (S, DrawStyle)>,
{
let font_system = self.clone();
let scale_factor = settings.scale_factor;
move || {
let mut guard = font_system.0.lock().unwrap();
let TextRendererInner { font_system, queue } = guard.deref_mut();
let mut tess_commands = CommandEncoder::default();
for (id, mut atlas, mut image, workload) in workload {
for (str, style) in workload {
let mut buffer = Buffer::new(font_system, Metrics::new(style.size, style.size));
buffer.set_text(
font_system,
str.as_ref(),
&style.as_attrs(),
Shaping::Advanced,
);
buffer.shape_until_scroll(font_system, false);
let stroke = style.stroke;
let weight = style.weight;
for run in buffer.layout_runs() {
for glyph in run.glyphs {
font_system.db().with_face_data(glyph.font_id, |file, _| {
let Ok(face) = Face::parse(file, 0) else {
return;
};
cache_glyph(
scale_factor,
&mut atlas,
&mut image,
&mut tess_commands,
glyph,
stroke,
weight,
face,
);
});
callback.glyph_drawn();
}
}
callback.style_drawn();
}
queue.push_back((id, atlas, image));
callback.atlas_drawn();
}
}
}
pub fn prepare_images_cloned<S, I>(
&self,
settings: &Text3dPlugin,
workload: impl IntoIterator<Item = (AssetId<TextAtlas>, I)> + Send + Sync + 'static,
atlases: &mut Assets<TextAtlas>,
images: &mut Assets<Image>,
callback: impl TextProgressReportCallback,
) -> impl FnOnce() + Send + Sync + 'static
where
S: AsRef<str> + 'static,
I: IntoIterator<Item = (S, DrawStyle)> + Send + Sync + 'static,
{
let workload: Vec<_> = workload
.into_iter()
.filter_map(|(id, iter)| {
let atlas = atlases.get(id)?.clone();
let image = images.get(atlas.image.id())?.clone();
Some((id, atlas, image, iter))
})
.collect();
self.prepare_task(settings, workload, callback)
}
pub fn prepare_images<S, I>(
&self,
settings: &Text3dPlugin,
workload: impl IntoIterator<Item = (AssetId<TextAtlas>, I)> + Send + Sync + 'static,
atlases: &mut Assets<TextAtlas>,
images: &mut Assets<Image>,
callback: impl TextProgressReportCallback,
) -> impl FnOnce() + Send + Sync + 'static
where
S: AsRef<str> + 'static,
I: IntoIterator<Item = (S, DrawStyle)> + Send + Sync + 'static,
{
let workload: Vec<_> = workload
.into_iter()
.filter_map(|(id, iter)| {
let atlas = atlases.remove(id)?.clone();
let image = images.remove(atlas.image.id())?.clone();
Some((id, atlas, image, iter))
})
.collect();
self.prepare_task(settings, workload, callback)
}
}