use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::ops::Range;
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 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>,
}
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,
}
}
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 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, 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)
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct GlyphSlot {
pub page: u32,
pub rect: AtlasRect,
pub offset: (i32, i32),
pub is_color: bool,
}
#[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>,
}
#[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>,
default_family_stack: Vec<String>,
shape_cache: LruCache<ShapeRunKey, ShapedRun>,
}
#[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 font_system = bundled_font_system();
Self {
font_system,
scale_ctx: ScaleContext::new(),
pages: vec![AtlasPage::new(PAGE_SIZE, PAGE_SIZE)],
map: HashMap::new(),
color_font_cache: HashMap::new(),
default_family_stack: vec![DEFAULT_SANS_FAMILY.to_string()],
shape_cache: LruCache::new(NonZeroUsize::new(SHAPE_RUN_CACHE_CAPACITY).unwrap()),
}
}
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 register_font(&mut self, bytes: Vec<u8>) {
self.font_system.db_mut().load_font_data(bytes);
}
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(&key).copied()
}
pub fn take_dirty(&mut self) -> Vec<(usize, AtlasRect)> {
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,
) -> 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>,
) -> 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>,
) -> 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>,
) -> 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,
) -> ShapedRun {
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).cloned() {
return cached;
}
let shaped = self.shape_runs_compute(runs, size, options);
self.shape_cache.put(key, shaped.clone());
return shaped;
}
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 attrs = Attrs::new()
.family(Family::Name(family))
.weight(cosmic_weight(style.weight))
.style(if style.italic {
Style::Italic
} else {
Style::Normal
})
.metadata(i);
(*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 decoration_thickness = (size * 0.06).max(1.0);
let underline_offset = size * 0.10;
let strikethrough_offset = -size * 0.28;
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::rgb(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) {
if self.map.contains_key(&key) {
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,
},
);
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,
})
}
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) {
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);
let rect = page.allocate(w, h)?;
self.pages.push(page);
Some((self.pages.len() - 1, rect))
}
}
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(),
}
}
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 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 aetna_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::rgb(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::rgb(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::rgb(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::rgb(0, 0, 0),
);
let r24 = atlas.shape_and_rasterize(
"A",
24.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::rgb(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::rgb(0, 0, 0),
);
let bold = atlas.shape_and_rasterize(
"A",
16.0,
FontWeight::Bold,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::rgb(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::rgb(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::rgb(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::rgb(0, 0, 0),
);
}
}
let total_glyphs: usize = atlas.map.len();
assert!(total_glyphs > 100, "only stored {total_glyphs} glyphs");
}
#[test]
fn attributed_runs_bake_per_run_color_and_run_index() {
let mut atlas = GlyphAtlas::new();
let red = Color::rgb(255, 0, 0);
let green = Color::rgb(0, 255, 0);
let blue = Color::rgb(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::rgb(220, 200, 60);
let runs = [
(
"plain ",
RunStyle::new(FontWeight::Regular, Color::rgb(0, 0, 0)),
),
(
"marked",
RunStyle::new(FontWeight::Regular, Color::rgb(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::rgb(60, 120, 240);
let runs = [(
"the quick brown fox jumps over the lazy dog",
RunStyle::new(FontWeight::Regular, Color::rgb(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::rgb(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::rgb(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::rgb(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::rgb(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::rgb(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::rgb(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::rgb(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]
fn register_font_adds_to_database() {
let mut atlas = GlyphAtlas::new();
let before = atlas.font_system.db().faces().count();
atlas.register_font(aetna_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!["AetnaColrTest".into()]);
let run = atlas.shape_and_rasterize(
"\u{E001}",
48.0,
FontWeight::Regular,
TextWrap::NoWrap,
TextAnchor::Start,
None,
Color::rgb(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::rgb(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::rgb(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 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::rgb(0, 0, 0),
);
}
}