use std::{num::NonZero, sync::OnceLock};
use glyphon::fontdb;
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tessera_ui::{
Color, PxPosition,
renderer::drawer::pipeline::{DrawContext, DrawablePipeline},
wgpu,
};
use super::command::{TextCommand, TextConstraint};
static FONT_SYSTEM: OnceLock<RwLock<glyphon::FontSystem>> = OnceLock::new();
static TEXT_DATA_CACHE: OnceLock<RwLock<lru::LruCache<LruKey, TextData>>> = OnceLock::new();
#[derive(PartialEq)]
struct LruKey {
text: String,
color: Color,
size: f32,
line_height: f32,
constraint: TextConstraint,
}
impl Eq for LruKey {}
impl std::hash::Hash for LruKey {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.text.hash(state);
self.color.r.to_bits().hash(state);
self.color.g.to_bits().hash(state);
self.color.b.to_bits().hash(state);
self.color.a.to_bits().hash(state);
self.size.to_bits().hash(state);
self.line_height.to_bits().hash(state);
self.constraint.hash(state);
}
}
fn write_lru_cache() -> RwLockWriteGuard<'static, lru::LruCache<LruKey, TextData>> {
TEXT_DATA_CACHE
.get_or_init(|| RwLock::new(lru::LruCache::new(NonZero::new(100).unwrap())))
.write()
}
#[cfg(target_os = "android")]
fn init_font_system() -> RwLock<glyphon::FontSystem> {
let mut font_system = glyphon::FontSystem::new();
font_system.db_mut().load_fonts_dir("/system/fonts");
font_system.db_mut().set_sans_serif_family("Roboto");
font_system.db_mut().set_serif_family("Noto Serif");
font_system.db_mut().set_monospace_family("Droid Sans Mono");
font_system.db_mut().set_cursive_family("Dancing Script");
font_system.db_mut().set_fantasy_family("Dancing Script");
RwLock::new(font_system)
}
#[cfg(not(target_os = "android"))]
fn init_font_system() -> RwLock<glyphon::FontSystem> {
RwLock::new(glyphon::FontSystem::new())
}
pub fn read_font_system() -> RwLockReadGuard<'static, glyphon::FontSystem> {
FONT_SYSTEM.get_or_init(init_font_system).read()
}
pub fn write_font_system() -> RwLockWriteGuard<'static, glyphon::FontSystem> {
FONT_SYSTEM.get_or_init(init_font_system).write()
}
pub struct GlyphonTextRender {
atlas: glyphon::TextAtlas,
#[allow(unused)]
cache: glyphon::Cache,
viewport: glyphon::Viewport,
swash_cache: glyphon::SwashCache,
msaa: wgpu::MultisampleState,
renderer: glyphon::TextRenderer,
}
impl GlyphonTextRender {
pub fn new(
gpu: &wgpu::Device,
queue: &wgpu::Queue,
config: &wgpu::SurfaceConfiguration,
sample_count: u32,
) -> Self {
let cache = glyphon::Cache::new(gpu);
let mut atlas = glyphon::TextAtlas::new(gpu, queue, &cache, config.format);
let viewport = glyphon::Viewport::new(gpu, &cache);
let swash_cache = glyphon::SwashCache::new();
let msaa = wgpu::MultisampleState {
count: sample_count,
mask: !0,
alpha_to_coverage_enabled: false,
};
let renderer = glyphon::TextRenderer::new(&mut atlas, gpu, msaa, None);
Self {
atlas,
cache,
viewport,
swash_cache,
msaa,
renderer,
}
}
}
impl DrawablePipeline<TextCommand> for GlyphonTextRender {
fn draw(&mut self, context: &mut DrawContext<TextCommand>) {
if context.commands.is_empty() {
return;
}
self.viewport.update(
context.queue,
glyphon::Resolution {
width: context.config.width,
height: context.config.height,
},
);
let text_areas = context
.commands
.iter()
.map(|(command, _size, start_pos)| command.data.text_area(*start_pos));
self.renderer
.prepare(
context.device,
context.queue,
&mut write_font_system(),
&mut self.atlas,
&self.viewport,
text_areas,
&mut self.swash_cache,
)
.unwrap();
self.renderer
.render(&self.atlas, &self.viewport, context.render_pass)
.unwrap();
let new_renderer =
glyphon::TextRenderer::new(&mut self.atlas, context.device, self.msaa, None);
let _ = std::mem::replace(&mut self.renderer, new_renderer);
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TextData {
text_buffer: glyphon::Buffer,
pub size: [u32; 2],
}
impl TextData {
pub fn new(
text: String,
color: Color,
size: f32,
line_height: f32,
constraint: TextConstraint,
) -> Self {
let key = LruKey {
text: text.clone(),
color,
size,
line_height,
constraint: constraint.clone(),
};
if let Some(cache) = write_lru_cache().get(&key) {
return cache.clone();
}
let mut text_buffer = glyphon::Buffer::new(
&mut write_font_system(),
glyphon::Metrics::new(size, line_height),
);
let color = glyphon::Color::rgba(
(color.r * 255.0) as u8,
(color.g * 255.0) as u8,
(color.b * 255.0) as u8,
(color.a * 255.0) as u8,
);
text_buffer.set_wrap(&mut write_font_system(), glyphon::Wrap::Glyph);
text_buffer.set_size(
&mut write_font_system(),
constraint.max_width,
constraint.max_height,
);
text_buffer.set_text(
&mut write_font_system(),
&text,
&glyphon::Attrs::new()
.family(fontdb::Family::SansSerif)
.color(color),
glyphon::Shaping::Advanced,
None,
);
text_buffer.shape_until_scroll(&mut write_font_system(), false);
let mut run_width: f32 = 0.0;
let metrics = text_buffer.metrics();
let num_lines = text_buffer.layout_runs().count() as f32;
let descent_amount = (metrics.line_height - metrics.font_size).max(0.0);
let total_height = num_lines * metrics.line_height + descent_amount;
for run in text_buffer.layout_runs() {
run_width = run_width.max(run.line_w);
}
let result = Self {
text_buffer,
size: [run_width as u32, total_height.ceil() as u32],
};
write_lru_cache().put(key, result.clone());
result
}
pub fn from_buffer(text_buffer: glyphon::Buffer) -> Self {
let metrics = text_buffer.metrics();
let num_lines = text_buffer.layout_runs().count() as f32;
let descent_amount = (metrics.line_height - metrics.font_size).max(0.0);
let total_height = num_lines * metrics.line_height + descent_amount;
let mut run_width: f32 = 0.0;
for run in text_buffer.layout_runs() {
run_width = run_width.max(run.line_w);
}
Self {
text_buffer,
size: [run_width as u32, total_height.ceil() as u32],
}
}
fn text_area(&'_ self, start_pos: PxPosition) -> glyphon::TextArea<'_> {
let bounds = glyphon::TextBounds {
left: start_pos.x.raw(),
top: start_pos.y.raw(),
right: start_pos.x.raw() + self.size[0] as i32,
bottom: start_pos.y.raw() + self.size[1] as i32,
};
glyphon::TextArea {
buffer: &self.text_buffer,
left: start_pos.x.to_f32(),
top: start_pos.y.to_f32(),
scale: 1.0,
bounds,
default_color: glyphon::Color::rgb(0, 0, 0), custom_glyphs: &[],
}
}
}