#![warn(missing_docs)]
use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::ops::Range;
use std::sync::Arc;
use cosmic_text::{
Attrs, Buffer, CacheKey, Family, FontSystem, Metrics, Shaping, Style, Weight, Wrap, fontdb,
};
use lru::LruCache;
use swash::scale::image::{Content as SwashContent, Image as SwashImage};
use swash::scale::{Render, ScaleContext, Source as SwashSource, StrikeWith};
use crate::ir::TextAnchor;
use crate::text::metrics::{TextLayout, TextLine, line_height};
use crate::tree::{Color, FontFamily, FontWeight, TextWrap};
const PAGE_SIZE: u32 = 512;
const PAGE_BUDGET: usize = 8;
const DEFAULT_SANS_FAMILY: &str = "Inter Variable";
#[derive(Clone, Debug, PartialEq)]
pub struct ShapedGlyph {
pub key: GlyphKey,
pub x: f32,
pub y: f32,
pub byte_range: Range<usize>,
pub color: Color,
pub run_index: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ShapedRun {
pub layout: TextLayout,
pub glyphs: Vec<ShapedGlyph>,
pub highlights: Vec<HighlightRect>,
pub decorations: Vec<DecorationRect>,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct HighlightRect {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
pub color: Color,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct DecorationRect {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
pub color: Color,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct RunStyle {
pub family: FontFamily,
pub mono_family: FontFamily,
pub weight: FontWeight,
pub italic: bool,
pub mono: bool,
pub color: Color,
pub bg: Option<Color>,
pub underline: bool,
pub strikethrough: bool,
pub link: Option<String>,
pub tabular_numerals: bool,
pub letter_spacing_bits: u32,
}
impl RunStyle {
pub fn new(weight: FontWeight, color: Color) -> Self {
Self {
family: FontFamily::default(),
mono_family: FontFamily::JetBrainsMono,
weight,
italic: false,
mono: false,
color,
bg: None,
underline: false,
strikethrough: false,
link: None,
tabular_numerals: false,
letter_spacing_bits: 0.0_f32.to_bits(),
}
}
pub fn italic(mut self) -> Self {
self.italic = true;
self
}
pub fn mono(mut self) -> Self {
self.mono = true;
self
}
pub fn family(mut self, family: FontFamily) -> Self {
self.family = family;
self
}
pub fn mono_family(mut self, family: FontFamily) -> Self {
self.mono_family = family;
self
}
pub fn with_bg(mut self, bg: Color) -> Self {
self.bg = Some(bg);
self
}
pub fn underline(mut self) -> Self {
self.underline = true;
self
}
pub fn tabular_numerals(mut self) -> Self {
self.tabular_numerals = true;
self
}
pub fn with_letter_spacing(mut self, px: f32) -> Self {
self.letter_spacing_bits = px.to_bits();
self
}
pub fn letter_spacing(&self) -> f32 {
f32::from_bits(self.letter_spacing_bits)
}
pub fn strikethrough(mut self) -> Self {
self.strikethrough = true;
self
}
pub fn with_link(mut self, url: impl Into<String>) -> Self {
self.link = Some(url.into());
self.color = crate::tokens::LINK_FOREGROUND;
self.underline = true;
self
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct DecorationMetricsEm {
pub underline_offset: f32,
pub thickness: f32,
pub strikeout_offset: f32,
}
impl DecorationMetricsEm {
pub const FALLBACK: Self = Self {
underline_offset: 0.10,
thickness: 0.06,
strikeout_offset: -0.28,
};
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct GlyphKey {
pub font: fontdb::ID,
pub glyph_id: u16,
pub size_bits: u32,
pub weight: fontdb::Weight,
}
impl GlyphKey {
pub fn size(&self) -> f32 {
f32::from_bits(self.size_bits)
}
pub fn at_scale(self, scale_factor: f32) -> Self {
Self {
size_bits: (self.size() * scale_factor).to_bits(),
..self
}
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct GlyphSlot {
pub page: u32,
pub rect: AtlasRect,
pub offset: (i32, i32),
pub is_color: bool,
pub raster_size: f32,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct AtlasRect {
pub x: u32,
pub y: u32,
pub w: u32,
pub h: u32,
}
impl AtlasRect {
pub fn right(&self) -> u32 {
self.x + self.w
}
pub fn bottom(&self) -> u32 {
self.y + self.h
}
}
pub const ATLAS_BYTES_PER_PIXEL: u32 = 4;
pub struct AtlasPage {
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
dirty: Option<AtlasRect>,
shelves: Vec<Shelf>,
last_used: u64,
}
#[derive(Copy, Clone)]
struct Shelf {
y_top: u32,
height: u32,
cursor: u32,
}
pub struct GlyphAtlas {
font_system: FontSystem,
scale_ctx: ScaleContext,
pages: Vec<AtlasPage>,
map: HashMap<GlyphKey, GlyphSlot>,
color_font_cache: HashMap<fontdb::ID, bool>,
wght_axis_cache: HashMap<fontdb::ID, bool>,
decoration_metrics_cache: HashMap<fontdb::ID, DecorationMetricsEm>,
default_family_stack: Vec<String>,
shape_cache: LruCache<ShapeRunKey, Arc<ShapedRun>>,
registry_loaded: usize,
frame: u64,
page_budget: usize,
lru_protection_window: u64,
}
#[derive(Clone, PartialEq, Eq, Hash)]
struct ShapeRunKey {
runs: Vec<(Box<str>, RunStyle)>,
size_bits: u32,
line_h_bits: u32,
wrap: TextWrap,
anchor: TextAnchor,
available_width_bits: Option<u32>,
}
const SHAPE_RUN_CACHE_CAPACITY: usize = 1024;
#[derive(Copy, Clone)]
struct ShapeRunOptions {
line_h: f32,
wrap: TextWrap,
anchor: TextAnchor,
available_width: Option<f32>,
rasterize_into_color_atlas: bool,
}
impl Default for GlyphAtlas {
fn default() -> Self {
Self::new()
}
}
impl GlyphAtlas {
pub fn new() -> Self {
let mut font_system = bundled_font_system();
let mut registry_loaded = 0;
crate::text::registry::sync_font_system(&mut font_system, &mut registry_loaded);
Self {
font_system,
scale_ctx: ScaleContext::new(),
pages: vec![AtlasPage::new(PAGE_SIZE, PAGE_SIZE)],
map: HashMap::new(),
color_font_cache: HashMap::new(),
wght_axis_cache: HashMap::new(),
decoration_metrics_cache: HashMap::new(),
default_family_stack: vec![DEFAULT_SANS_FAMILY.to_string()],
shape_cache: LruCache::new(NonZeroUsize::new(SHAPE_RUN_CACHE_CAPACITY).unwrap()),
registry_loaded,
frame: 0,
page_budget: PAGE_BUDGET,
lru_protection_window: 1,
}
}
pub fn set_lru_protection_window(&mut self, n: u32) {
self.lru_protection_window = u64::from(n.max(1));
}
pub fn font_system(&self) -> &FontSystem {
&self.font_system
}
pub fn font_system_mut(&mut self) -> &mut FontSystem {
&mut self.font_system
}
pub fn is_color_font(&mut self, id: fontdb::ID) -> bool {
if let Some(&cached) = self.color_font_cache.get(&id) {
return cached;
}
let result = self
.font_system
.db()
.with_face_data(id, |bytes, face_index| {
let face = ttf_parser::Face::parse(bytes, face_index).ok()?;
let tables = face.tables();
Some(tables.cbdt.is_some() || tables.colr.is_some() || tables.sbix.is_some())
})
.flatten()
.unwrap_or(false);
self.color_font_cache.insert(id, result);
result
}
pub fn msdf_raster_weight(&mut self, id: fontdb::ID, resolved: fontdb::Weight) -> u16 {
let has_wght = match self.wght_axis_cache.get(&id) {
Some(&cached) => cached,
None => {
let result = self
.font_system
.db()
.with_face_data(id, |bytes, face_index| {
let face = ttf_parser::Face::parse(bytes, face_index).ok()?;
Some(
face.variation_axes()
.into_iter()
.any(|a| a.tag == ttf_parser::Tag::from_bytes(b"wght")),
)
})
.flatten()
.unwrap_or(false);
self.wght_axis_cache.insert(id, result);
result
}
};
if has_wght { resolved.0 } else { 0 }
}
pub fn decoration_metrics_em(&mut self, family_name: &str) -> DecorationMetricsEm {
let Some(id) = self.font_system.db().query(&fontdb::Query {
families: &[fontdb::Family::Name(family_name)],
..fontdb::Query::default()
}) else {
return DecorationMetricsEm::FALLBACK;
};
if let Some(&cached) = self.decoration_metrics_cache.get(&id) {
return cached;
}
let metrics = self
.font_system
.db()
.with_face_data(id, |bytes, face_index| {
let face = ttf_parser::Face::parse(bytes, face_index).ok()?;
let upem = face.units_per_em() as f32;
let underline = face.underline_metrics()?;
let mut m = DecorationMetricsEm {
underline_offset: -underline.position as f32 / upem,
thickness: (underline.thickness as f32 / upem)
.max(DecorationMetricsEm::FALLBACK.thickness * 0.5),
strikeout_offset: DecorationMetricsEm::FALLBACK.strikeout_offset,
};
if let Some(strikeout) = face.strikeout_metrics() {
m.strikeout_offset = -(strikeout.position as f32) / upem;
}
Some(m)
})
.flatten()
.unwrap_or(DecorationMetricsEm::FALLBACK);
self.decoration_metrics_cache.insert(id, metrics);
metrics
}
pub fn register_font(&mut self, bytes: Vec<u8>) {
crate::text::registry::register_font(bytes);
self.sync_registered_fonts();
}
fn sync_registered_fonts(&mut self) {
if crate::text::registry::sync_font_system(&mut self.font_system, &mut self.registry_loaded)
{
self.shape_cache.clear();
}
}
pub fn set_default_family_stack(&mut self, stack: Vec<String>) {
if !stack.is_empty() {
self.default_family_stack = stack;
}
}
pub fn default_family(&self) -> &str {
self.default_family_stack
.first()
.map(String::as_str)
.unwrap_or(DEFAULT_SANS_FAMILY)
}
pub fn pages(&self) -> &[AtlasPage] {
&self.pages
}
pub fn page(&self, index: u32) -> Option<&AtlasPage> {
self.pages.get(index as usize)
}
pub fn slot(&self, key: GlyphKey) -> Option<GlyphSlot> {
self.map.get(&quantize_key(key)).copied()
}
pub fn take_dirty(&mut self) -> Vec<(usize, AtlasRect)> {
self.frame += 1;
let mut out = Vec::new();
for (i, page) in self.pages.iter_mut().enumerate() {
if let Some(rect) = page.dirty.take() {
out.push((i, rect));
}
}
out
}
#[allow(clippy::too_many_arguments)]
pub fn shape_and_rasterize(
&mut self,
text: &str,
size: f32,
weight: FontWeight,
wrap: TextWrap,
anchor: TextAnchor,
available_width: Option<f32>,
color: Color,
) -> Arc<ShapedRun> {
self.shape_and_rasterize_runs(
&[(text, RunStyle::new(weight, color))],
size,
wrap,
anchor,
available_width,
)
}
pub fn shape_runs(
&mut self,
runs: &[(&str, RunStyle)],
size: f32,
wrap: TextWrap,
anchor: TextAnchor,
available_width: Option<f32>,
) -> Arc<ShapedRun> {
self.shape_runs_with_line_height(
runs,
size,
line_height(size),
wrap,
anchor,
available_width,
)
}
pub fn shape_runs_with_line_height(
&mut self,
runs: &[(&str, RunStyle)],
size: f32,
line_height: f32,
wrap: TextWrap,
anchor: TextAnchor,
available_width: Option<f32>,
) -> Arc<ShapedRun> {
self.shape_runs_inner(
runs,
size,
ShapeRunOptions {
line_h: line_height,
wrap,
anchor,
available_width,
rasterize_into_color_atlas: false,
},
)
}
pub fn ensure_color_glyph(&mut self, key: GlyphKey) {
self.ensure(key);
}
pub fn shape_and_rasterize_runs(
&mut self,
runs: &[(&str, RunStyle)],
size: f32,
wrap: TextWrap,
anchor: TextAnchor,
available_width: Option<f32>,
) -> Arc<ShapedRun> {
self.shape_runs_inner(
runs,
size,
ShapeRunOptions {
line_h: line_height(size),
wrap,
anchor,
available_width,
rasterize_into_color_atlas: true,
},
)
}
fn shape_runs_inner(
&mut self,
runs: &[(&str, RunStyle)],
size: f32,
options: ShapeRunOptions,
) -> Arc<ShapedRun> {
self.sync_registered_fonts();
let ShapeRunOptions {
line_h,
wrap,
anchor,
available_width,
rasterize_into_color_atlas,
} = options;
if !rasterize_into_color_atlas {
let key = ShapeRunKey {
runs: runs
.iter()
.map(|(text, style)| (Box::from(*text), style.clone()))
.collect(),
size_bits: size.to_bits(),
line_h_bits: line_h.to_bits(),
wrap,
anchor,
available_width_bits: available_width.map(f32::to_bits),
};
if let Some(cached) = self.shape_cache.get(&key) {
return Arc::clone(cached);
}
let shaped = Arc::new(self.shape_runs_compute(runs, size, options));
self.shape_cache.put(key, Arc::clone(&shaped));
return shaped;
}
Arc::new(self.shape_runs_compute(runs, size, options))
}
fn shape_runs_compute(
&mut self,
runs: &[(&str, RunStyle)],
size: f32,
options: ShapeRunOptions,
) -> ShapedRun {
let ShapeRunOptions {
line_h,
wrap,
anchor,
available_width,
rasterize_into_color_atlas,
} = options;
let mut buffer = Buffer::new(&mut self.font_system, Metrics::new(size, line_h));
buffer.set_wrap(match wrap {
TextWrap::NoWrap => Wrap::None,
TextWrap::Wrap => Wrap::WordOrGlyph,
});
buffer.set_size(available_width, None);
let primary_family = runs
.iter()
.find(|(_, style)| !style.mono)
.map(|(_, style)| style.family.family_name().to_string())
.unwrap_or_else(|| self.default_family().to_string());
let default_attrs = Attrs::new().family(Family::Name(&primary_family));
let spans = runs.iter().enumerate().map(|(i, (text, style))| {
let family = if style.mono {
style.mono_family.family_name()
} else {
style.family.family_name()
};
let mut attrs = Attrs::new()
.family(Family::Name(family))
.weight(cosmic_weight(style.weight))
.style(if style.italic {
Style::Italic
} else {
Style::Normal
})
.metadata(i);
if style.tabular_numerals {
attrs = attrs.font_features(crate::text::metrics::tabular_features());
}
if style.letter_spacing() != 0.0 {
attrs = attrs.letter_spacing(style.letter_spacing() / size);
}
(*text, attrs)
});
let alignment = match anchor {
TextAnchor::Start => None,
TextAnchor::Middle => Some(cosmic_text::Align::Center),
TextAnchor::End => Some(cosmic_text::Align::End),
};
buffer.set_rich_text(spans, &default_attrs, Shaping::Advanced, alignment);
buffer.shape_until_scroll(&mut self.font_system, false);
let mut lines = Vec::new();
let mut shaped_glyphs = Vec::new();
let mut highlights: Vec<HighlightRect> = Vec::new();
let mut decorations: Vec<DecorationRect> = Vec::new();
let mut height: f32 = 0.0;
let mut max_width: f32 = 0.0;
let wants_decorations = runs
.iter()
.any(|(_, s)| s.underline || s.strikethrough || s.link.is_some());
let deco = if wants_decorations {
self.decoration_metrics_em(&primary_family)
} else {
DecorationMetricsEm::FALLBACK
};
let decoration_thickness = (size * deco.thickness).max(1.0);
let underline_offset = size * deco.underline_offset;
let strikethrough_offset = size * deco.strikeout_offset;
for run in buffer.layout_runs() {
height = height.max(run.line_top + run.line_height);
max_width = max_width.max(run.line_w);
let (line_start, line_end) = run_byte_range(&run);
lines.push(TextLine {
text: line_slice(&run, line_start, line_end),
width: run.line_w,
y: run.line_top,
baseline: run.line_y,
rtl: run.rtl,
});
let mut open_bg: Option<(usize, Color, f32, f32)> = None;
let mut open_underline: Option<(usize, Color, f32, f32)> = None;
let mut open_strike: Option<(usize, Color, f32, f32)> = None;
let close_underline =
|open: &mut Option<(usize, Color, f32, f32)>, sink: &mut Vec<DecorationRect>| {
if let Some((_, c, lo, hi)) = open.take() {
sink.push(DecorationRect {
x: lo,
y: run.line_y + underline_offset,
w: (hi - lo).max(0.0),
h: decoration_thickness,
color: c,
});
}
};
let close_strike = |open: &mut Option<(usize, Color, f32, f32)>,
sink: &mut Vec<DecorationRect>| {
if let Some((_, c, lo, hi)) = open.take() {
sink.push(DecorationRect {
x: lo,
y: run.line_y + strikethrough_offset - decoration_thickness * 0.5,
w: (hi - lo).max(0.0),
h: decoration_thickness,
color: c,
});
}
};
for glyph in run.glyphs.iter() {
let physical = glyph.physical((0.0, 0.0), 1.0);
let key = glyph_key(physical.cache_key);
if rasterize_into_color_atlas {
self.ensure(key);
}
let run_idx = glyph.metadata.min(runs.len().saturating_sub(1));
let style = runs.get(run_idx).map(|(_, s)| s);
let color = style.map(|s| s.color).unwrap_or(Color::srgb_u8(0, 0, 0));
let bg = style.and_then(|s| s.bg);
let want_underline = style.is_some_and(|s| s.underline);
let want_strike = style.is_some_and(|s| s.strikethrough);
let g_left = glyph.x;
let g_right = glyph.x + glyph.w;
match (open_bg, bg) {
(Some((idx, c, lo, hi)), Some(_)) if idx == run_idx => {
open_bg = Some((idx, c, lo.min(g_left), hi.max(g_right)));
}
(Some((idx, c, lo, hi)), _) => {
highlights.push(HighlightRect {
x: lo,
y: run.line_top,
w: (hi - lo).max(0.0),
h: run.line_height,
color: c,
});
let _ = idx;
open_bg = bg.map(|c| (run_idx, c, g_left, g_right));
}
(None, Some(c)) => {
open_bg = Some((run_idx, c, g_left, g_right));
}
(None, None) => {}
}
match (open_underline, want_underline) {
(Some((idx, c, lo, hi)), true) if idx == run_idx => {
open_underline = Some((idx, c, lo.min(g_left), hi.max(g_right)));
}
(Some(_), _) => {
close_underline(&mut open_underline, &mut decorations);
if want_underline {
open_underline = Some((run_idx, color, g_left, g_right));
}
}
(None, true) => {
open_underline = Some((run_idx, color, g_left, g_right));
}
(None, false) => {}
}
match (open_strike, want_strike) {
(Some((idx, c, lo, hi)), true) if idx == run_idx => {
open_strike = Some((idx, c, lo.min(g_left), hi.max(g_right)));
}
(Some(_), _) => {
close_strike(&mut open_strike, &mut decorations);
if want_strike {
open_strike = Some((run_idx, color, g_left, g_right));
}
}
(None, true) => {
open_strike = Some((run_idx, color, g_left, g_right));
}
(None, false) => {}
}
shaped_glyphs.push(ShapedGlyph {
key,
x: glyph.x + glyph.x_offset,
y: run.line_y + glyph.y_offset,
byte_range: glyph.start..glyph.end,
color,
run_index: run_idx as u32,
});
}
if let Some((_, c, lo, hi)) = open_bg {
highlights.push(HighlightRect {
x: lo,
y: run.line_top,
w: (hi - lo).max(0.0),
h: run.line_height,
color: c,
});
}
close_underline(&mut open_underline, &mut decorations);
close_strike(&mut open_strike, &mut decorations);
}
let layout = TextLayout {
width: max_width,
height: height.max(line_h),
line_height: line_h,
lines,
};
ShapedRun {
layout,
glyphs: shaped_glyphs,
highlights,
decorations,
}
}
fn ensure(&mut self, key: GlyphKey) {
let key = quantize_key(key);
if let Some(slot) = self.map.get(&key) {
if slot.rect.w > 0 {
let page = slot.page as usize;
self.pages[page].last_used = self.frame;
}
return;
}
let Some(slot) = self.rasterize_and_pack(key) else {
self.map.insert(
key,
GlyphSlot {
page: 0,
rect: AtlasRect {
x: 0,
y: 0,
w: 0,
h: 0,
},
offset: (0, 0),
is_color: false,
raster_size: 0.0,
},
);
return;
};
self.map.insert(key, slot);
}
fn rasterize_and_pack(&mut self, key: GlyphKey) -> Option<GlyphSlot> {
let font = self.font_system.get_font(key.font, key.weight)?;
let mut scaler = self
.scale_ctx
.builder(font.as_swash())
.size(key.size())
.hint(true)
.build();
let sources = [
SwashSource::ColorOutline(0),
SwashSource::ColorBitmap(StrikeWith::BestFit),
SwashSource::Outline,
];
let render = Render::new(&sources);
let image = render.render(&mut scaler, key.glyph_id)?;
let width = image.placement.width;
let height = image.placement.height;
if width == 0 || height == 0 || image.data.is_empty() {
return None;
}
let (rgba, is_color) = expand_to_rgba(&image)?;
let (page_idx, rect) = self.allocate(width, height)?;
let page = &mut self.pages[page_idx];
copy_rgba_bitmap(&mut page.pixels, page.width, &rect, &rgba);
merge_dirty(&mut page.dirty, rect);
Some(GlyphSlot {
page: page_idx as u32,
rect,
offset: (image.placement.left, image.placement.top),
is_color,
raster_size: key.size(),
})
}
fn allocate(&mut self, w: u32, h: u32) -> Option<(usize, AtlasRect)> {
for (i, page) in self.pages.iter_mut().enumerate() {
if let Some(rect) = page.allocate(w, h) {
page.last_used = self.frame;
return Some((i, rect));
}
}
if self.pages.len() >= self.page_budget
&& let Some(i) = self
.pages
.iter()
.enumerate()
.filter(|(_, p)| {
p.last_used + self.lru_protection_window <= self.frame
&& p.width >= w
&& p.height >= h
})
.min_by_key(|(_, p)| p.last_used)
.map(|(i, _)| i)
{
self.recycle_page(i);
let page = &mut self.pages[i];
let rect = page.allocate(w, h).expect("recycled page fits the glyph");
page.last_used = self.frame;
return Some((i, rect));
}
let new_w = PAGE_SIZE.max(w.next_power_of_two());
let new_h = PAGE_SIZE.max(h.next_power_of_two());
let mut page = AtlasPage::new(new_w, new_h);
page.last_used = self.frame;
let rect = page.allocate(w, h)?;
self.pages.push(page);
Some((self.pages.len() - 1, rect))
}
fn recycle_page(&mut self, index: usize) {
let page_idx = index as u32;
self.map.retain(|_, s| s.page != page_idx || s.rect.w == 0);
let page = &mut self.pages[index];
page.pixels.fill(0);
page.shelves.clear();
page.dirty = Some(AtlasRect {
x: 0,
y: 0,
w: page.width,
h: page.height,
});
}
#[cfg(test)]
fn set_page_budget_for_tests(&mut self, budget: usize) {
self.page_budget = budget;
}
}
impl AtlasPage {
fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
pixels: vec![0; (width * height * ATLAS_BYTES_PER_PIXEL) as usize],
dirty: None,
shelves: Vec::new(),
last_used: 0,
}
}
fn allocate(&mut self, w: u32, h: u32) -> Option<AtlasRect> {
if w > self.width || h > self.height {
return None;
}
let mut best: Option<usize> = None;
for (i, shelf) in self.shelves.iter().enumerate() {
if shelf.cursor + w > self.width || shelf.height < h {
continue;
}
let waste = shelf.height - h;
if best
.map(|b| waste < self.shelves[b].height - h)
.unwrap_or(true)
{
best = Some(i);
}
}
if let Some(i) = best {
let shelf = &mut self.shelves[i];
let rect = AtlasRect {
x: shelf.cursor,
y: shelf.y_top,
w,
h,
};
shelf.cursor += w;
return Some(rect);
}
let next_y = self.shelves.last().map(|s| s.y_top + s.height).unwrap_or(0);
if next_y + h > self.height {
return None;
}
let shelf = Shelf {
y_top: next_y,
height: h,
cursor: w,
};
self.shelves.push(shelf);
Some(AtlasRect {
x: 0,
y: next_y,
w,
h,
})
}
}
fn expand_to_rgba(image: &SwashImage) -> Option<(Vec<u8>, bool)> {
let pixels = (image.placement.width * image.placement.height) as usize;
match image.content {
SwashContent::Mask => {
if image.data.len() < pixels {
return None;
}
let mut rgba = Vec::with_capacity(pixels * 4);
for &a in &image.data[..pixels] {
rgba.extend_from_slice(&[0xFF, 0xFF, 0xFF, a]);
}
Some((rgba, false))
}
SwashContent::Color => {
if image.data.len() < pixels * 4 {
return None;
}
Some((image.data[..pixels * 4].to_vec(), true))
}
SwashContent::SubpixelMask => {
if image.data.len() < pixels * 4 {
return None;
}
let mut rgba = Vec::with_capacity(pixels * 4);
for chunk in image.data[..pixels * 4].chunks_exact(4) {
let a = chunk[0].max(chunk[1]).max(chunk[2]);
rgba.extend_from_slice(&[0xFF, 0xFF, 0xFF, a]);
}
Some((rgba, false))
}
}
}
fn copy_rgba_bitmap(dst: &mut [u8], dst_stride_pixels: u32, rect: &AtlasRect, src_rgba: &[u8]) {
let bpp = ATLAS_BYTES_PER_PIXEL as usize;
let dst_row_bytes = dst_stride_pixels as usize * bpp;
let row_bytes = rect.w as usize * bpp;
for row in 0..rect.h as usize {
let dst_off = (rect.y as usize + row) * dst_row_bytes + rect.x as usize * bpp;
let src_off = row * row_bytes;
dst[dst_off..dst_off + row_bytes].copy_from_slice(&src_rgba[src_off..src_off + row_bytes]);
}
}
fn merge_dirty(dirty: &mut Option<AtlasRect>, rect: AtlasRect) {
*dirty = Some(match *dirty {
None => rect,
Some(prev) => {
let x = prev.x.min(rect.x);
let y = prev.y.min(rect.y);
let r = prev.right().max(rect.right());
let b = prev.bottom().max(rect.bottom());
AtlasRect {
x,
y,
w: r - x,
h: b - y,
}
}
});
}
fn quantize_key(key: GlyphKey) -> GlyphKey {
GlyphKey {
size_bits: key.size().round().max(1.0).to_bits(),
..key
}
}
fn glyph_key(cache_key: CacheKey) -> GlyphKey {
GlyphKey {
font: cache_key.font_id,
glyph_id: cache_key.glyph_id,
size_bits: cache_key.font_size_bits,
weight: cache_key.font_weight,
}
}
fn run_byte_range(run: &cosmic_text::LayoutRun<'_>) -> (usize, usize) {
let start = run.glyphs.iter().map(|g| g.start).min().unwrap_or(0);
let end = run.glyphs.iter().map(|g| g.end).max().unwrap_or(start);
(start, end)
}
fn line_slice(run: &cosmic_text::LayoutRun<'_>, start: usize, end: usize) -> String {
run.text
.get(start..end)
.unwrap_or_default()
.trim_end()
.to_string()
}
fn bundled_font_system() -> FontSystem {
let mut db = fontdb::Database::new();
db.set_sans_serif_family(DEFAULT_SANS_FAMILY);
for bytes in damascene_fonts::DEFAULT_FONTS {
db.load_font_data(bytes.to_vec());
}
FontSystem::new_with_locale_and_db("en-US".to_string(), db)
}
fn cosmic_weight(weight: FontWeight) -> Weight {
match weight {
FontWeight::Regular => Weight::NORMAL,
FontWeight::Medium => Weight::MEDIUM,
FontWeight::Semibold => Weight::SEMIBOLD,
FontWeight::Bold => Weight::BOLD,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shaping_emits_one_glyph_per_visible_codepoint() {
let mut atlas = GlyphAtlas::new();
let run = atlas.shape_and_rasterize(
"abc",
16.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
assert_eq!(run.glyphs.len(), 3);
assert_eq!(run.layout.lines.len(), 1);
assert!(run.layout.width > 0.0);
}
#[test]
fn repeated_glyph_reuses_atlas_slot() {
let mut atlas = GlyphAtlas::new();
atlas.shape_and_rasterize(
"aaa",
16.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
let pages_before = atlas.pages().len();
let dirty_before: u32 = atlas
.pages()
.iter()
.map(|p| p.dirty.map(|r| r.w * r.h).unwrap_or(0))
.sum();
atlas.take_dirty();
atlas.shape_and_rasterize(
"aa",
16.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
assert_eq!(atlas.pages().len(), pages_before);
let dirty_after: u32 = atlas
.pages()
.iter()
.map(|p| p.dirty.map(|r| r.w * r.h).unwrap_or(0))
.sum();
assert_eq!(dirty_after, 0);
assert!(dirty_before > 0);
}
#[test]
fn distinct_sizes_get_distinct_slots() {
let mut atlas = GlyphAtlas::new();
let r16 = atlas.shape_and_rasterize(
"A",
16.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
let r24 = atlas.shape_and_rasterize(
"A",
24.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
assert_eq!(r16.glyphs.len(), 1);
assert_eq!(r24.glyphs.len(), 1);
let s16 = atlas.slot(r16.glyphs[0].key).unwrap();
let s24 = atlas.slot(r24.glyphs[0].key).unwrap();
assert_ne!(s16.rect, s24.rect);
assert!(s24.rect.h >= s16.rect.h);
}
#[test]
fn distinct_weights_get_distinct_slots() {
let mut atlas = GlyphAtlas::new();
let regular = atlas.shape_and_rasterize(
"A",
16.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
let bold = atlas.shape_and_rasterize(
"A",
16.0,
FontWeight::Bold,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
let r = atlas.slot(regular.glyphs[0].key).unwrap();
let b = atlas.slot(bold.glyphs[0].key).unwrap();
assert_ne!(regular.glyphs[0].key, bold.glyphs[0].key);
assert_ne!(r.rect, b.rect);
}
#[test]
fn dirty_region_covers_new_glyphs_and_clears_on_take() {
let mut atlas = GlyphAtlas::new();
atlas.shape_and_rasterize(
"Hello",
18.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
let dirty = atlas.take_dirty();
assert_eq!(dirty.len(), 1, "expected one dirty page after first run");
let (page_idx, rect) = dirty[0];
assert_eq!(page_idx, 0);
assert!(rect.w > 0 && rect.h > 0);
assert!(atlas.take_dirty().is_empty());
}
#[test]
fn shelves_pack_a_realistic_text_run_into_one_page() {
let mut atlas = GlyphAtlas::new();
atlas.shape_and_rasterize(
"The quick brown fox jumps over the lazy dog 0123456789",
16.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
assert_eq!(atlas.pages().len(), 1);
}
#[test]
fn many_distinct_glyphs_can_grow_to_a_second_page() {
let mut atlas = GlyphAtlas::new();
for size in [10.0, 12.0, 14.0, 16.0, 18.0, 20.0, 24.0, 28.0, 32.0] {
for weight in [FontWeight::Regular, FontWeight::Bold] {
atlas.shape_and_rasterize(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
size,
weight,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
}
}
let total_glyphs: usize = atlas.map.len();
assert!(total_glyphs > 100, "only stored {total_glyphs} glyphs");
}
#[test]
fn shaped_weights_map_to_distinct_msdf_raster_weights() {
let mut atlas = GlyphAtlas::new();
let black = Color::srgb_u8(0, 0, 0);
let mut buckets = Vec::new();
for (weight, expected) in [
(FontWeight::Regular, 400u16),
(FontWeight::Medium, 500),
(FontWeight::Semibold, 600),
(FontWeight::Bold, 700),
] {
let runs = [("H", RunStyle::new(weight, black))];
let shaped = atlas.shape_runs_with_line_height(
&runs,
16.0,
20.0,
TextWrap::NoWrap,
TextAnchor::Start,
None,
);
let g = shaped.glyphs.first().expect("one glyph").key;
let bucket = atlas.msdf_raster_weight(g.font, g.weight);
assert_eq!(bucket, expected, "resolved {weight:?}");
buckets.push(bucket);
}
buckets.dedup();
assert_eq!(buckets.len(), 4, "each weight gets its own raster bucket");
}
#[test]
fn decoration_metrics_come_from_the_font_tables() {
let mut atlas = GlyphAtlas::new();
let m = atlas.decoration_metrics_em(FontFamily::Inter.family_name());
assert!(
(m.underline_offset - 0.17).abs() < 0.02,
"Inter underline offset ≈ 0.17 em, got {}",
m.underline_offset
);
assert!(
(m.thickness - 0.068).abs() < 0.02,
"Inter underline thickness ≈ 0.068 em, got {}",
m.thickness
);
assert!(
m.strikeout_offset < -0.2 && m.strikeout_offset > -0.4,
"Inter strikeout sits ~0.3 em above the baseline, got {}",
m.strikeout_offset
);
let fb = atlas.decoration_metrics_em("NoSuchFamily");
assert_eq!(fb, DecorationMetricsEm::FALLBACK);
}
#[test]
fn attributed_runs_bake_per_run_color_and_run_index() {
let mut atlas = GlyphAtlas::new();
let red = Color::srgb_u8(255, 0, 0);
let green = Color::srgb_u8(0, 255, 0);
let blue = Color::srgb_u8(0, 0, 255);
let runs = [
("AA", RunStyle::new(FontWeight::Regular, red)),
("BB", RunStyle::new(FontWeight::Bold, green)),
("CC", RunStyle::new(FontWeight::Regular, blue).italic()),
];
let shaped =
atlas.shape_and_rasterize_runs(&runs, 16.0, TextWrap::NoWrap, TextAnchor::Start, None);
assert_eq!(shaped.glyphs.len(), 6);
assert_eq!(shaped.glyphs[0].run_index, 0);
assert_eq!(shaped.glyphs[0].color, red);
assert_eq!(shaped.glyphs[2].run_index, 1);
assert_eq!(shaped.glyphs[2].color, green);
assert_eq!(shaped.glyphs[4].run_index, 2);
assert_eq!(shaped.glyphs[4].color, blue);
assert_ne!(shaped.glyphs[0].key.weight, shaped.glyphs[2].key.weight);
assert_ne!(shaped.glyphs[4].key.font, shaped.glyphs[0].key.font);
assert_ne!(shaped.glyphs[4].key.font, shaped.glyphs[2].key.font);
}
#[test]
fn run_with_bg_emits_one_highlight_per_line() {
let mut atlas = GlyphAtlas::new();
let yellow = Color::srgb_u8(220, 200, 60);
let runs = [
(
"plain ",
RunStyle::new(FontWeight::Regular, Color::srgb_u8(0, 0, 0)),
),
(
"marked",
RunStyle::new(FontWeight::Regular, Color::srgb_u8(0, 0, 0)).with_bg(yellow),
),
];
let shaped =
atlas.shape_and_rasterize_runs(&runs, 16.0, TextWrap::NoWrap, TextAnchor::Start, None);
assert_eq!(
shaped.highlights.len(),
1,
"expected one highlight rect, got {:?}",
shaped.highlights
);
let h = shaped.highlights[0];
assert_eq!(h.color, yellow);
assert!(h.w > 0.0, "zero-width highlight: {h:?}");
assert_eq!(h.h, shaped.layout.line_height);
let last_plain = shaped
.glyphs
.iter()
.filter(|g| g.run_index == 0)
.map(|g| g.x)
.fold(0.0_f32, f32::max);
assert!(
h.x + 1e-3 >= last_plain,
"highlight starts before plain runs end: hx={} last_plain={}",
h.x,
last_plain,
);
}
#[test]
fn run_with_bg_wraps_to_two_highlight_rects() {
let mut atlas = GlyphAtlas::new();
let blue = Color::srgb_u8(60, 120, 240);
let runs = [(
"the quick brown fox jumps over the lazy dog",
RunStyle::new(FontWeight::Regular, Color::srgb_u8(0, 0, 0)).with_bg(blue),
)];
let shaped = atlas.shape_and_rasterize_runs(
&runs,
16.0,
TextWrap::Wrap,
TextAnchor::Start,
Some(120.0),
);
assert!(
shaped.layout.lines.len() >= 2,
"expected wrapped layout, got {:?}",
shaped.layout.lines.len()
);
assert_eq!(
shaped.highlights.len(),
shaped.layout.lines.len(),
"expected one highlight per wrapped line: highlights={:?}",
shaped.highlights,
);
for h in &shaped.highlights {
assert_eq!(h.color, blue);
assert!(h.w > 0.0);
}
}
#[test]
fn run_without_bg_emits_no_highlights() {
let mut atlas = GlyphAtlas::new();
let shaped = atlas.shape_and_rasterize(
"no highlight",
16.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
assert!(shaped.highlights.is_empty());
}
#[test]
fn run_with_underline_emits_one_decoration_per_line() {
let mut atlas = GlyphAtlas::new();
let teal = Color::srgb_u8(20, 200, 200);
let runs = [(
"underlined",
RunStyle::new(FontWeight::Regular, teal).underline(),
)];
let shaped =
atlas.shape_and_rasterize_runs(&runs, 16.0, TextWrap::NoWrap, TextAnchor::Start, None);
assert_eq!(
shaped.decorations.len(),
1,
"expected one underline rect, got {:?}",
shaped.decorations,
);
let d = shaped.decorations[0];
assert_eq!(d.color, teal);
assert!(d.h >= 1.0, "thickness must clamp to >= 1px, got {}", d.h);
let line = &shaped.layout.lines[0];
assert!(
d.y > line.baseline,
"underline y={} should be below baseline={}",
d.y,
line.baseline,
);
assert!(
d.w > 0.0,
"underline must span the glyph extent, got w={}",
d.w,
);
}
#[test]
fn run_with_strikethrough_sits_above_baseline() {
let mut atlas = GlyphAtlas::new();
let runs = [(
"struck",
RunStyle::new(FontWeight::Regular, Color::srgb_u8(0, 0, 0)).strikethrough(),
)];
let shaped =
atlas.shape_and_rasterize_runs(&runs, 16.0, TextWrap::NoWrap, TextAnchor::Start, None);
assert_eq!(shaped.decorations.len(), 1);
let d = shaped.decorations[0];
let line = &shaped.layout.lines[0];
assert!(
d.y < line.baseline,
"strikethrough y={} should sit above baseline={}",
d.y,
line.baseline,
);
}
#[test]
fn run_with_link_emits_underline_in_link_color() {
let mut atlas = GlyphAtlas::new();
let runs = [(
"click me",
RunStyle::new(FontWeight::Regular, Color::srgb_u8(0, 0, 0))
.with_link("https://example.com"),
)];
let shaped =
atlas.shape_and_rasterize_runs(&runs, 16.0, TextWrap::NoWrap, TextAnchor::Start, None);
assert_eq!(shaped.decorations.len(), 1);
assert_eq!(shaped.decorations[0].color, crate::tokens::LINK_FOREGROUND);
assert_eq!(shaped.glyphs[0].color, crate::tokens::LINK_FOREGROUND);
}
#[test]
fn underline_wraps_with_text_to_two_decoration_rects() {
let mut atlas = GlyphAtlas::new();
let runs = [(
"the quick brown fox jumps over the lazy dog",
RunStyle::new(FontWeight::Regular, Color::srgb_u8(0, 0, 0)).underline(),
)];
let shaped = atlas.shape_and_rasterize_runs(
&runs,
16.0,
TextWrap::Wrap,
TextAnchor::Start,
Some(120.0),
);
assert!(
shaped.decorations.len() >= 2,
"expected one decoration rect per wrapped line, got {:?}",
shaped.decorations,
);
let mut ys: Vec<f32> = shaped.decorations.iter().map(|d| d.y).collect();
ys.dedup_by(|a, b| (*a - *b).abs() < 0.5);
assert_eq!(ys.len(), shaped.decorations.len());
}
#[test]
fn run_without_decorations_emits_no_decoration_rects() {
let mut atlas = GlyphAtlas::new();
let shaped = atlas.shape_and_rasterize(
"plain",
16.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
assert!(shaped.decorations.is_empty());
}
#[test]
fn fallback_face_resolves_math_arrow() {
let mut atlas = GlyphAtlas::new();
let run = atlas.shape_and_rasterize(
"→",
16.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
assert_eq!(run.glyphs.len(), 1, "expected one glyph for arrow");
let slot = atlas.slot(run.glyphs[0].key).expect("arrow slot");
assert!(
slot.rect.w > 0 && slot.rect.h > 0,
"expected real bitmap, got {slot:?}"
);
}
#[test]
#[cfg(feature = "inter")]
fn register_font_adds_to_database() {
let mut atlas = GlyphAtlas::new();
let before = atlas.font_system.db().faces().count();
atlas.register_font(damascene_fonts::INTER_VARIABLE.to_vec());
let after = atlas.font_system.db().faces().count();
assert!(after > before, "register_font should add a face");
}
#[test]
fn set_default_family_stack_changes_primary_family() {
let mut atlas = GlyphAtlas::new();
assert_eq!(atlas.default_family(), "Inter Variable");
atlas.set_default_family_stack(vec!["MyBrand".into(), "Inter Variable".into()]);
assert_eq!(atlas.default_family(), "MyBrand");
atlas.set_default_family_stack(vec![]);
assert_eq!(atlas.default_family(), "MyBrand");
}
#[test]
fn colr_v0_glyph_rasterizes_with_palette_colors() {
const COLR_FONT: &[u8] = include_bytes!("../../tests/fixtures/test_colr.ttf");
let mut atlas = GlyphAtlas::new();
atlas.register_font(COLR_FONT.to_vec());
atlas.set_default_family_stack(vec!["DamasceneColrTest".into()]);
let run = atlas.shape_and_rasterize(
"\u{E001}",
48.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(255, 255, 255),
);
assert_eq!(run.glyphs.len(), 1, "expected one glyph for U+E001");
let slot = atlas.slot(run.glyphs[0].key).expect("colr slot");
assert!(
slot.is_color,
"COLR glyph should be marked is_color = true; got {slot:?}"
);
let page = &atlas.pages()[slot.page as usize];
let stride = page.width as usize * ATLAS_BYTES_PER_PIXEL as usize;
let mut found_red = false;
let mut found_blue = false;
for row in 0..slot.rect.h as usize {
for col in 0..slot.rect.w as usize {
let off = (slot.rect.y as usize + row) * stride + (slot.rect.x as usize + col) * 4;
let r = page.pixels[off];
let g = page.pixels[off + 1];
let b = page.pixels[off + 2];
let a = page.pixels[off + 3];
if a < 200 {
continue;
}
if r > 200 && g < 60 && b < 60 {
found_red = true;
}
if b > 200 && r < 60 && g < 60 {
found_blue = true;
}
}
}
assert!(
found_red,
"expected red pixels from CPAL palette index 0 (square layer)"
);
assert!(
found_blue,
"expected blue pixels from CPAL palette index 1 (diamond layer)"
);
}
#[cfg(feature = "emoji")]
#[test]
fn color_emoji_glyph_rasterizes_in_color() {
let mut atlas = GlyphAtlas::new();
let run = atlas.shape_and_rasterize(
"😀",
32.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
assert_eq!(run.glyphs.len(), 1, "expected one glyph for 😀");
let slot = atlas.slot(run.glyphs[0].key).expect("emoji slot");
assert!(
slot.is_color,
"expected color glyph, got {slot:?} on a font that should be NotoColorEmoji"
);
let page = &atlas.pages()[slot.page as usize];
let stride = page.width as usize * ATLAS_BYTES_PER_PIXEL as usize;
let mut found_color = false;
for row in 0..slot.rect.h as usize {
for col in 0..slot.rect.w as usize {
let off = (slot.rect.y as usize + row) * stride + (slot.rect.x as usize + col) * 4;
let r = page.pixels[off];
let g = page.pixels[off + 1];
let b = page.pixels[off + 2];
let a = page.pixels[off + 3];
if a > 0 && (r != g || g != b) {
found_color = true;
break;
}
}
if found_color {
break;
}
}
assert!(
found_color,
"expected at least one pixel with non-grayscale RGB inside 😀 bitmap"
);
}
#[test]
fn outline_glyph_stores_white_alpha_in_rgba_atlas() {
let mut atlas = GlyphAtlas::new();
let run = atlas.shape_and_rasterize(
"A",
16.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
let slot = atlas.slot(run.glyphs[0].key).expect("A slot");
assert!(!slot.is_color);
let page = &atlas.pages()[slot.page as usize];
let stride = page.width as usize * ATLAS_BYTES_PER_PIXEL as usize;
let mut sampled_alpha = 0;
for row in 0..slot.rect.h as usize {
for col in 0..slot.rect.w as usize {
let off = (slot.rect.y as usize + row) * stride + (slot.rect.x as usize + col) * 4;
let r = page.pixels[off];
let g = page.pixels[off + 1];
let b = page.pixels[off + 2];
let a = page.pixels[off + 3];
if a > 0 {
assert_eq!(
(r, g, b),
(255, 255, 255),
"outline glyph rgb should be white"
);
sampled_alpha = sampled_alpha.max(a);
}
}
}
assert!(sampled_alpha > 0, "expected at least one covered pixel");
}
#[test]
fn fractional_sizes_share_a_quantized_slot() {
fn shape(atlas: &mut GlyphAtlas, size: f32) -> GlyphKey {
atlas
.shape_and_rasterize(
"A",
size,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
)
.glyphs[0]
.key
}
let mut atlas = GlyphAtlas::new();
let k_low = shape(&mut atlas, 15.8);
let k_high = shape(&mut atlas, 16.2);
assert_ne!(k_low, k_high);
let s_low = atlas.slot(k_low).unwrap();
let s_high = atlas.slot(k_high).unwrap();
assert_eq!(s_low, s_high);
assert_eq!(s_low.raster_size, 16.0);
let k_17 = shape(&mut atlas, 16.6);
let s_17 = atlas.slot(k_17).unwrap();
assert_eq!(s_17.raster_size, 17.0);
assert_ne!(s_17.rect, s_low.rect);
}
#[test]
fn allocation_recycles_lru_page_at_budget() {
let mut atlas = GlyphAtlas::new();
atlas.set_page_budget_for_tests(1);
let key_a = atlas
.shape_and_rasterize(
"A",
16.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
)
.glyphs[0]
.key;
assert!(atlas.slot(key_a).is_some());
let (grown, _) = atlas.allocate(PAGE_SIZE, PAGE_SIZE).unwrap();
assert_eq!(grown, 1);
assert_eq!(atlas.pages().len(), 2);
assert!(atlas.slot(key_a).is_some());
atlas.take_dirty();
let (recycled, rect) = atlas.allocate(PAGE_SIZE, PAGE_SIZE).unwrap();
assert_eq!(recycled, 0);
assert_eq!(atlas.pages().len(), 2);
assert_eq!((rect.x, rect.y), (0, 0));
assert!(atlas.slot(key_a).is_none());
let dirty = atlas.take_dirty();
assert!(
dirty
.iter()
.any(|(i, r)| *i == 0 && r.w == PAGE_SIZE && r.h == PAGE_SIZE),
"expected a full-page dirty rect on page 0, got {dirty:?}"
);
}
#[test]
fn per_frame_ensure_protects_pages_from_recycling() {
let mut atlas = GlyphAtlas::new();
atlas.set_page_budget_for_tests(1);
let key_a = atlas
.shape_and_rasterize(
"A",
16.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
)
.glyphs[0]
.key;
atlas.take_dirty();
atlas.ensure_color_glyph(key_a);
let (idx, _) = atlas.allocate(PAGE_SIZE, PAGE_SIZE).unwrap();
assert_eq!(idx, 1);
assert!(atlas.slot(key_a).is_some());
}
#[test]
fn empty_glyph_caches_zero_slot_without_panicking() {
let mut atlas = GlyphAtlas::new();
atlas.shape_and_rasterize(
" ",
16.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::srgb_u8(0, 0, 0),
);
}
}