///|
/// TextRenderer — manages glyph atlas pages and provides text measurement.
/// GPU-independent; consumers use flush_dirty_pages() to upload atlas data.
///|
pub struct TextRenderer {
mut pages : Array[GlyphAtlas]
font : TTFont
pipeline_id : Int
atlas_size : Int
base_page_id : Int
max_pages : Int
mut generation : Int
}
///|
pub fn TextRenderer::new(
font : TTFont,
atlas_size : Int,
base_page_id : Int,
pipeline_id : Int,
) -> TextRenderer {
TextRenderer::new_with_options(
font, atlas_size, base_page_id, pipeline_id, 16,
)
}
///|
pub fn TextRenderer::new_with_options(
font : TTFont,
atlas_size : Int,
base_page_id : Int,
pipeline_id : Int,
max_pages : Int,
) -> TextRenderer {
{
pages: [GlyphAtlas::new(atlas_size, atlas_size, base_page_id)],
font,
pipeline_id,
atlas_size,
base_page_id,
max_pages,
generation: 0,
}
}
///|
pub fn TextRenderer::measure(
self : TextRenderer,
text : String,
size_px : Double,
) -> TextMetrics {
let width = self.font.measure_text(text, size_px)
let ascent = self.font.ascent.to_double() /
self.font.units_per_em.to_double() *
size_px
let descent = -self.font.descent.to_double() /
self.font.units_per_em.to_double() *
size_px
{ width, height: ascent + descent, baseline: ascent }
}
///|
pub fn TextRenderer::get_atlas(self : TextRenderer) -> GlyphAtlas {
self.pages[0]
}
///|
pub fn TextRenderer::get_page_id(self : TextRenderer) -> Int {
self.base_page_id
}
///|
pub fn TextRenderer::glyph_count(self : TextRenderer) -> Int {
let mut total = 0
for page in self.pages {
total += page.cache.entry_count()
}
total
}
///|
pub fn TextRenderer::clear(self : TextRenderer) -> Unit {
let first = self.pages[0]
first.cache.clear()
for i in 0.. Int {
self.pages.length()
}
///|
pub fn TextRenderer::get_page(self : TextRenderer, index : Int) -> GlyphAtlas {
self.pages[index]
}
///|
pub fn TextRenderer::get_page_ids(self : TextRenderer) -> Array[Int] {
let ids : Array[Int] = []
for page in self.pages {
ids.push(page.cache.atlas_page_id)
}
ids
}
///|
pub fn TextRenderer::flush_dirty_pages(
self : TextRenderer,
) -> Array[AtlasPageInfo] {
let result : Array[AtlasPageInfo] = []
for page in self.pages {
if page.dirty {
result.push({
page_id: page.cache.atlas_page_id,
width: page.width,
height: page.height,
dirty: true,
})
page.dirty = false
}
}
result
}
///|
/// Ensure a glyph is rasterized and cached. Returns the page index and cache entry.
/// Used by GPU renderers to build draw commands.
pub fn TextRenderer::ensure_glyph(
self : TextRenderer,
codepoint : Int,
size_px : Double,
) -> (Int, GlyphCacheEntry)? {
self.generation += 1
let glyph_id = self.font.glyph_index(codepoint)
let cache_key = make_glyph_cache_key(glyph_id, size_px)
// Search existing pages
for i, page in self.pages {
match page.cache.get(cache_key) {
Some(entry) => {
page.last_used_generation = self.generation
return Some((i, entry))
}
None => ()
}
}
// Try rasterizing into existing pages
for i, page in self.pages {
match page.rasterize_glyph(self.font, codepoint, size_px) {
Some(entry) => {
page.last_used_generation = self.generation
return Some((i, entry))
}
None => ()
}
}
// Add new page or evict oldest
if self.pages.length() < self.max_pages {
let new_page_id = self.base_page_id + self.pages.length()
let new_page = GlyphAtlas::new(
self.atlas_size,
self.atlas_size,
new_page_id,
)
self.pages.push(new_page)
let idx = self.pages.length() - 1
match self.pages[idx].rasterize_glyph(self.font, codepoint, size_px) {
Some(entry) => {
self.pages[idx].last_used_generation = self.generation
return Some((idx, entry))
}
None => None
}
} else {
// Evict oldest page
let mut oldest_idx = 0
let mut oldest_gen = self.pages[0].last_used_generation
for i = 1; i < self.pages.length(); i = i + 1 {
if self.pages[i].last_used_generation < oldest_gen {
oldest_gen = self.pages[i].last_used_generation
oldest_idx = i
}
}
let evicted = self.pages[oldest_idx]
evicted.cache.clear()
for j in 0.. {
evicted.last_used_generation = self.generation
Some((oldest_idx, entry))
}
None => None
}
}
}