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,
font_size: f32,
line_height: f32,
bounds: [u32; 2],
}
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.font_size.to_bits().hash(state);
self.line_height.to_bits().hash(state);
self.bounds.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).expect("text cache size must be non-zero"),
))
})
.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)| {
let start_pos = PxPosition::new(
start_pos.x + command.offset.x,
start_pos.y + command.offset.y,
);
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,
)
.expect("glyphon prepare failed");
self.renderer
.render(&self.atlas, &self.viewport, context.render_pass)
.expect("glyphon render failed");
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)]
pub struct TextData {
text_buffer: glyphon::Buffer,
pub size: [u32; 2],
pub first_baseline: f32,
pub last_baseline: f32,
pub line_count: u32,
base_color: Color,
current_color: Color,
text: String,
font_size: f32,
line_height: f32,
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct TextMeasureInfo {
pub size: [u32; 2],
pub first_baseline: f32,
pub last_baseline: f32,
pub line_count: u32,
}
impl PartialEq for TextData {
fn eq(&self, other: &Self) -> bool {
self.size == other.size
&& self.first_baseline == other.first_baseline
&& self.last_baseline == other.last_baseline
&& self.line_count == other.line_count
&& self.base_color == other.base_color
&& self.current_color == other.current_color
&& self.text == other.text
&& self.font_size == other.font_size
&& self.line_height == other.line_height
}
}
impl TextData {
pub fn measure(
text: String,
color: Color,
font_size: f32,
line_height: f32,
constraint: TextConstraint,
) -> TextMeasureInfo {
let (text_buffer, bounds, first_baseline, last_baseline, line_count) =
Self::build_buffer(&text, color, font_size, line_height, &constraint);
let key = LruKey {
text: text.clone(),
color,
font_size,
line_height,
bounds,
};
let data = Self {
text_buffer,
size: bounds,
first_baseline,
last_baseline,
line_count,
base_color: color,
current_color: color,
text,
font_size,
line_height,
};
write_lru_cache().put(key, data);
TextMeasureInfo {
size: bounds,
first_baseline,
last_baseline,
line_count,
}
}
pub fn get(
text: String,
color: Color,
font_size: f32,
line_height: f32,
bounds: [u32; 2],
) -> Self {
let key = LruKey {
text: text.clone(),
color,
font_size,
line_height,
bounds,
};
if let Some(cached) = write_lru_cache().get(&key).cloned() {
return cached;
}
let constraint = TextConstraint {
max_width: Some(bounds[0] as f32),
max_height: Some(bounds[1] as f32),
};
let (text_buffer, computed_bounds, first_baseline, last_baseline, line_count) =
Self::build_buffer(&text, color, font_size, line_height, &constraint);
let data = Self {
text_buffer,
size: computed_bounds,
first_baseline,
last_baseline,
line_count,
base_color: color,
current_color: color,
text: text.clone(),
font_size,
line_height,
};
write_lru_cache().put(key, data.clone());
data
}
pub fn from_buffer(text_buffer: glyphon::Buffer) -> Self {
let metrics = text_buffer.metrics();
let mut run_width: f32 = 0.0;
let mut first_baseline = 0.0;
let mut last_baseline = 0.0;
let mut line_count: u32 = 0;
for run in text_buffer.layout_runs() {
run_width = run_width.max(run.line_w);
if line_count == 0 {
first_baseline = run.line_y;
}
last_baseline = run.line_y;
line_count += 1;
}
let descent_amount = (metrics.line_height - metrics.font_size).max(0.0);
let total_height = line_count as f32 * metrics.line_height + descent_amount;
Self {
text_buffer,
size: [run_width as u32, total_height.ceil() as u32],
first_baseline,
last_baseline,
line_count,
base_color: Color::WHITE,
current_color: Color::WHITE,
text: String::new(),
font_size: metrics.font_size,
line_height: metrics.line_height,
}
}
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::rgba(
(self.current_color.r * 255.0) as u8,
(self.current_color.g * 255.0) as u8,
(self.current_color.b * 255.0) as u8,
(self.current_color.a * 255.0) as u8,
),
custom_glyphs: &[],
}
}
fn build_buffer(
text: &str,
color: Color,
size: f32,
line_height: f32,
constraint: &TextConstraint,
) -> (glyphon::Buffer, [u32; 2], f32, f32, u32) {
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 mut first_baseline = 0.0;
let mut last_baseline = 0.0;
let mut line_count: u32 = 0;
for run in text_buffer.layout_runs() {
run_width = run_width.max(run.line_w);
if line_count == 0 {
first_baseline = run.line_y;
}
last_baseline = run.line_y;
line_count += 1;
}
let descent_amount = (metrics.line_height - metrics.font_size).max(0.0);
let total_height = line_count as f32 * metrics.line_height + descent_amount;
(
text_buffer,
[run_width as u32, total_height.ceil() as u32],
first_baseline,
last_baseline,
line_count,
)
}
pub(crate) fn apply_opacity(&mut self, opacity: f32) {
let target_alpha = (self.base_color.a * opacity).clamp(0.0, 1.0);
let target_color = self.base_color.with_alpha(target_alpha);
if (target_color.a - self.current_color.a).abs() <= f32::EPSILON
&& (target_color.r - self.current_color.r).abs() <= f32::EPSILON
&& (target_color.g - self.current_color.g).abs() <= f32::EPSILON
&& (target_color.b - self.current_color.b).abs() <= f32::EPSILON
{
return;
}
let constraint = TextConstraint {
max_width: Some(self.size[0] as f32),
max_height: Some(self.size[1] as f32),
};
let (buffer, bounds, first_baseline, last_baseline, line_count) = Self::build_buffer(
&self.text,
target_color,
self.font_size,
self.line_height,
&constraint,
);
self.text_buffer = buffer;
self.size = bounds;
self.first_baseline = first_baseline;
self.last_baseline = last_baseline;
self.line_count = line_count;
self.current_color = target_color;
}
}