use std::collections::HashMap;
use glam::Vec2;
use crate::color::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(transparent)]
pub struct Font(pub(crate) u32);
impl Font {
pub const DEFAULT: Font = Font(0);
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TextParams {
pub font: Font,
pub size: f32,
pub color: Color,
pub z: f32,
}
impl Default for TextParams {
fn default() -> Self {
Self {
font: Font::DEFAULT,
size: 16.0,
color: Color::WHITE,
z: 0.0,
}
}
}
const ATLAS: u32 = 1024;
const PAD: u32 = 1;
#[derive(Clone, Copy)]
pub(crate) struct Glyph {
pub at: Option<[u32; 2]>,
pub size: [u32; 2],
pub offset: [f32; 2],
}
pub(crate) struct Placed {
pub pos: Vec2,
pub glyph: Glyph,
#[cfg_attr(not(feature = "3d"), allow(dead_code))]
pub line: u32,
}
#[cfg_attr(not(feature = "3d"), allow(dead_code))]
pub(crate) const SDF_PX: f32 = 64.0;
#[cfg_attr(not(feature = "3d"), allow(dead_code))]
pub(crate) const SDF_SPREAD: u32 = 8;
#[cfg_attr(not(feature = "3d"), allow(dead_code))]
#[derive(Default)]
pub(crate) struct SdfAtlas {
glyphs: HashMap<(u32, char), Glyph>,
cursor: [u32; 2],
row: u32,
full: bool,
pub uploads: Vec<([u32; 2], [u32; 2], Vec<u8>)>,
}
pub(crate) struct Text {
fonts: Vec<Option<fontdue::Font>>,
glyphs: HashMap<(u32, char, u32), Glyph>,
cursor: [u32; 2],
row: u32,
full: bool,
pub uploads: Vec<([u32; 2], [u32; 2], Vec<u8>)>,
placed: Vec<Placed>,
line_widths: Vec<f32>,
pub sdf: SdfAtlas,
}
impl Text {
pub fn new() -> Self {
Self {
fonts: vec![None],
glyphs: HashMap::new(),
cursor: [PAD, PAD],
row: 0,
full: false,
uploads: Vec::new(),
placed: Vec::new(),
line_widths: Vec::new(),
sdf: SdfAtlas::default(),
}
}
pub const ATLAS_SIZE: u32 = ATLAS;
pub fn has_font(&self, font: Font) -> bool {
(font.0 as usize) < self.fonts.len()
}
pub fn add_font(&mut self, bytes: &[u8]) -> Result<Font, String> {
let font = fontdue::Font::from_bytes(bytes, fontdue::FontSettings::default())
.map_err(|e| format!("not a font file this engine can read: {e}"))?;
self.fonts.push(Some(font));
Ok(Font(self.fonts.len() as u32 - 1))
}
pub fn replace_font(&mut self, font: Font, bytes: &[u8]) -> Result<(), String> {
let new = fontdue::Font::from_bytes(bytes, fontdue::FontSettings::default())
.map_err(|e| format!("not a font file this engine can read: {e}"))?;
self.fonts[font.0 as usize] = Some(new);
self.glyphs.retain(|k, _| k.0 != font.0);
self.sdf.glyphs.retain(|k, _| k.0 != font.0);
Ok(())
}
fn font(&mut self, font: Font) -> &fontdue::Font {
let slot = &mut self.fonts[font.0 as usize];
slot.get_or_insert_with(|| {
fontdue::Font::from_bytes(epaint_default_fonts::UBUNTU_LIGHT, fontdue::FontSettings::default())
.expect("the built-in font is valid")
})
}
pub fn begin_frame(&mut self) -> bool {
if self.sdf.full {
self.sdf.full = false;
self.sdf.glyphs.clear();
self.sdf.cursor = [PAD, PAD];
self.sdf.row = 0;
}
if !self.full {
return false;
}
self.full = false;
self.glyphs.clear();
self.cursor = [PAD, PAD];
self.row = 0;
true
}
pub fn layout(&mut self, text: &str, font: Font, px: f32, rasterize: bool) -> Vec2 {
let key_px = px.round().max(1.0) as u32;
self.layout_with(text, font, px, |t, c| match t.glyphs.get(&(font.0, c, key_px)) {
Some(g) => Some(*g),
None if rasterize => t.rasterize(font, c, px, key_px),
None => None,
})
}
#[cfg_attr(not(feature = "3d"), allow(dead_code))]
pub fn layout_sdf(&mut self, text: &str, font: Font) -> Vec2 {
self.layout_with(text, font, SDF_PX, |t, c| match t.sdf.glyphs.get(&(font.0, c)) {
Some(g) => Some(*g),
None => t.make_sdf(font, c),
})
}
fn layout_with(
&mut self,
text: &str,
font: Font,
px: f32,
mut glyph_of: impl FnMut(&mut Self, char) -> Option<Glyph>,
) -> Vec2 {
let mut placed = std::mem::take(&mut self.placed);
placed.clear();
self.line_widths.clear();
let lines = self.font(font).horizontal_line_metrics(px);
let (ascent, line_height) = lines.map_or((px * 0.8, px), |m| (m.ascent, m.new_line_size));
let (mut x, mut y, mut width) = (0.0f32, 0.0f32, 0.0f32);
let mut line = 0;
let mut prev: Option<char> = None;
for c in text.chars() {
if c == '\n' {
width = width.max(x);
self.line_widths.push(x);
x = 0.0;
y += line_height;
line += 1;
prev = None;
continue;
}
let f = self.font(font);
if let Some(p) = prev {
x += f.horizontal_kern(p, c, px).unwrap_or(0.0);
}
let advance = f.metrics(c, px).advance_width;
if let Some(glyph) = glyph_of(self, c) {
let pos = Vec2::new(
(x + glyph.offset[0]).round(),
(y + ascent - glyph.offset[1] - glyph.size[1] as f32).round(),
);
placed.push(Placed { pos, glyph, line });
}
x += advance;
prev = Some(c);
}
width = width.max(x);
self.line_widths.push(x);
self.placed = placed;
Vec2::new(width, y + line_height)
}
#[cfg_attr(not(feature = "3d"), allow(dead_code))]
pub fn line_widths(&self) -> &[f32] {
&self.line_widths
}
pub fn placed(&self) -> &[Placed] {
&self.placed
}
fn rasterize(&mut self, font: Font, c: char, px: f32, key_px: u32) -> Option<Glyph> {
let (m, coverage) = self.font(font).rasterize(c, px);
let size = [m.width as u32, m.height as u32];
let offset = [m.xmin as f32, m.ymin as f32];
let at = if size[0] == 0 || size[1] == 0 {
None
} else {
if self.cursor[0] + size[0] + PAD > ATLAS {
self.cursor = [PAD, self.cursor[1] + self.row + PAD];
self.row = 0;
}
if self.cursor[1] + size[1] + PAD > ATLAS {
self.full = true;
return None;
}
let at = self.cursor;
self.cursor[0] += size[0] + PAD;
self.row = self.row.max(size[1]);
let rgba = coverage.iter().flat_map(|&a| [255, 255, 255, a]).collect();
self.uploads.push((at, size, rgba));
Some(at)
};
let glyph = Glyph { at, size, offset };
self.glyphs.insert((font.0, c, key_px), glyph);
Some(glyph)
}
}
impl Text {
#[cfg_attr(not(feature = "3d"), allow(dead_code))]
fn make_sdf(&mut self, font: Font, c: char) -> Option<Glyph> {
let (m, coverage) = self.font(font).rasterize(c, SDF_PX);
let spread = SDF_SPREAD;
let (w, h) = (m.width as u32, m.height as u32);
let size = if w == 0 || h == 0 {
[0, 0]
} else {
[w + 2 * spread, h + 2 * spread]
};
let offset = [m.xmin as f32 - spread as f32, m.ymin as f32 - spread as f32];
let at = if size[0] == 0 {
None
} else {
let atlas = &mut self.sdf;
if atlas.cursor == [0, 0] {
atlas.cursor = [PAD, PAD];
}
if atlas.cursor[0] + size[0] + PAD > ATLAS {
atlas.cursor = [PAD, atlas.cursor[1] + atlas.row + PAD];
atlas.row = 0;
}
if atlas.cursor[1] + size[1] + PAD > ATLAS {
atlas.full = true;
return None;
}
let at = atlas.cursor;
atlas.cursor[0] += size[0] + PAD;
atlas.row = atlas.row.max(size[1]);
let distance = signed_distance(&coverage, w, h, spread);
let rgba = distance.iter().flat_map(|&a| [255, 255, 255, a]).collect();
atlas.uploads.push((at, size, rgba));
Some(at)
};
let glyph = Glyph { at, size, offset };
self.sdf.glyphs.insert((font.0, c), glyph);
Some(glyph)
}
}
#[cfg_attr(not(feature = "3d"), allow(dead_code))]
fn signed_distance(coverage: &[u8], w: u32, h: u32, spread: u32) -> Vec<u8> {
let (w, h, s) = (w as i32, h as i32, spread as i32);
let at = |x: i32, y: i32| -> f32 {
if x < 0 || y < 0 || x >= w || y >= h {
0.0
} else {
f32::from(coverage[(y * w + x) as usize]) / 255.0
}
};
let (out_w, out_h) = (w + 2 * s, h + 2 * s);
let mut out = Vec::with_capacity((out_w * out_h) as usize);
for oy in 0..out_h {
for ox in 0..out_w {
let (x, y) = (ox - s, oy - s);
let a = at(x, y);
let signed = if a > 0.0 && a < 1.0 {
a - 0.5
} else {
let inside = a >= 1.0;
let mut best = s as f32;
for dy in -s..=s {
for dx in -s..=s {
let b = at(x + dx, y + dy);
let across = if inside { b - 0.5 } else { 0.5 - b };
if (inside && b >= 1.0) || (!inside && b <= 0.0) {
continue;
}
let d = ((dx * dx + dy * dy) as f32).sqrt() + across;
best = best.min(d);
}
}
if inside { best } else { -best }
};
let v = 0.5 + signed / (2.0 * s as f32);
out.push((v.clamp(0.0, 1.0) * 255.0).round() as u8);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layout_measures_lines_and_kerning() {
let mut t = Text::new();
let one = t.layout("Hello", Font::DEFAULT, 20.0, false);
let two = t.layout("Hello\nHello", Font::DEFAULT, 20.0, false);
assert!(one.x > 30.0 && one.x < 70.0, "{one}");
assert_eq!(two.x, one.x);
assert!((two.y - 2.0 * one.y).abs() < 0.01, "{one} {two}");
assert_eq!(t.layout("", Font::DEFAULT, 20.0, false).x, 0.0);
}
#[test]
fn glyphs_are_rasterized_once_and_packed_apart() {
let mut t = Text::new();
t.layout("abca", Font::DEFAULT, 32.0, true);
assert_eq!(t.uploads.len(), 3, "a, b, c once each");
assert_eq!(t.placed().len(), 4);
let spots: Vec<_> = t.uploads.iter().map(|(at, size, _)| (*at, *size)).collect();
for (i, (a, sa)) in spots.iter().enumerate() {
for (b, sb) in &spots[i + 1..] {
let apart =
a[0] + sa[0] <= b[0] || b[0] + sb[0] <= a[0] || a[1] + sa[1] <= b[1] || b[1] + sb[1] <= a[1];
assert!(apart, "glyphs overlap in the atlas");
}
}
t.uploads.clear();
t.layout("cab", Font::DEFAULT, 32.0, true);
assert!(t.uploads.is_empty(), "cached");
}
#[test]
fn a_full_atlas_starts_over_next_frame() {
let mut t = Text::new();
let many: String = ('A'..='Z').chain('a'..='z').collect();
t.layout(&many, Font::DEFAULT, 400.0, true);
assert!(t.full);
assert!(t.begin_frame());
assert!(!t.full && t.glyphs.is_empty());
assert!(!t.begin_frame());
}
#[test]
fn distance_fields_are_half_on_the_edge_and_rise_inside() {
let field = signed_distance(&[255; 16], 4, 4, 3);
let side = 10;
let at = |x: usize, y: usize| field[y * side + x];
assert!(at(5, 5) > 128 + 20, "{}", at(5, 5));
assert!(at(3, 5) > 128 && at(2, 5) < 128, "{} {}", at(3, 5), at(2, 5));
assert_eq!(at(0, 0), 0);
assert_eq!(at(3, 5), at(6, 5));
}
#[test]
fn sdf_glyphs_are_made_once_padded_by_the_spread() {
let mut t = Text::new();
let size = t.layout_sdf("Hi\nthere", Font::DEFAULT);
assert_eq!(t.line_widths().len(), 2);
assert!(size.y > SDF_PX * 1.5, "{size}");
assert_eq!(t.placed().iter().filter(|p| p.line == 1).count(), 5);
let made = t.sdf.uploads.len();
assert_eq!(made, 6, "H, i, t, h, e, r (e twice)");
let ([w, h], bitmap) = (t.sdf.uploads[0].1, t.font(Font::DEFAULT).rasterize('H', SDF_PX).0);
assert_eq!(
[w, h],
[
bitmap.width as u32 + 2 * SDF_SPREAD,
bitmap.height as u32 + 2 * SDF_SPREAD
]
);
t.layout_sdf("there", Font::DEFAULT);
assert_eq!(t.sdf.uploads.len(), made, "cached");
}
#[test]
fn spaces_take_room_but_no_atlas_space() {
let mut t = Text::new();
let w = t.layout("a a", Font::DEFAULT, 20.0, true).x;
assert!(w > t.layout("aa", Font::DEFAULT, 20.0, true).x);
assert!(t.uploads.iter().all(|(_, size, _)| size[0] > 0));
}
}