use crate::assets::{
TextureAsset, TextureColorSpace, TextureFilter, TextureSampler, TextureWrap,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TextStyle {
pub size: f32,
pub color: [u8; 4],
pub background: [u8; 4],
pub padding: u32,
pub monospace: bool,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
size: 32.0,
color: [255, 255, 255, 255],
background: [0, 0, 0, 255],
padding: 8,
monospace: false,
}
}
}
pub fn text_texture(text: &str, style: TextStyle) -> TextureAsset {
draw(text, style, None)
}
pub fn text_texture_in_font(
text: &str,
style: TextStyle,
font: &[u8],
) -> Option<TextureAsset> {
std::panic::catch_unwind(|| draw(text, style, Some(font))).ok()
}
fn draw(text: &str, style: TextStyle, font: Option<&[u8]>) -> TextureAsset {
let ctx = egui::Context::default();
if let Some(font) = font {
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"game".to_owned(),
std::sync::Arc::new(egui::FontData::from_owned(font.to_vec())),
);
for family in fonts.families.values_mut() {
family.insert(0, "game".to_owned());
}
ctx.set_fonts(fonts);
}
let font = if style.monospace {
egui::FontId::monospace(style.size)
} else {
egui::FontId::proportional(style.size)
};
let mut galley = None;
let _ = ctx.run(egui::RawInput::default(), |ctx| {
galley = Some(ctx.fonts(|fonts| {
fonts.layout_no_wrap(
text.to_owned(),
font.clone(),
egui::Color32::WHITE,
)
}));
});
let galley = galley.expect("egui ran the pass");
let atlas = ctx.fonts(|fonts| fonts.image());
let pad = style.padding;
let width = galley.size().x.ceil() as u32 + 2 * pad;
let height = galley.size().y.ceil() as u32 + 2 * pad;
let (width, height) = (width.max(1), height.max(1));
let mut rgba8 = style.background.repeat((width * height) as usize);
for row in &galley.rows {
for glyph in &row.glyphs {
let uv = glyph.uv_rect;
if uv.is_nothing() {
continue;
}
let left = (glyph.pos.x + uv.offset.x).round() as i64 + pad as i64;
let top = (glyph.pos.y + uv.offset.y).round() as i64 + pad as i64;
for v in uv.min[1]..uv.max[1] {
for u in uv.min[0]..uv.max[0] {
let x = left + (u - uv.min[0]) as i64;
let y = top + (v - uv.min[1]) as i64;
if x < 0 || y < 0 || x >= width as i64 || y >= height as i64
{
continue;
}
let coverage =
atlas.pixels[v as usize * atlas.size[0] + u as usize];
let at = (y as usize * width as usize + x as usize) * 4;
for (channel, fg) in style.color.iter().enumerate() {
let bg = rgba8[at + channel] as f32;
let blend = coverage * style.color[3] as f32 / 255.0;
rgba8[at + channel] =
(bg + (*fg as f32 - bg) * blend).round() as u8;
}
}
}
}
}
TextureAsset {
size: [width, height],
rgba8,
color_space: TextureColorSpace::Srgb,
sampler: TextureSampler {
wrap: [TextureWrap::ClampToEdge; 2],
mipmap_filter: TextureFilter::Linear,
..TextureSampler::default()
},
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ink(texture: &TextureAsset) -> usize {
texture
.rgba8
.chunks(4)
.filter(|pixel| pixel[0] > 128)
.count()
}
#[test]
fn text_draws_ink_inside_its_padding() {
let style = TextStyle {
padding: 4,
..TextStyle::default()
};
let texture = text_texture("CAM 3", style);
let [width, height] = texture.size;
assert!(width > 60 && height >= 32, "{width}x{height}");
assert_eq!(texture.rgba8.len(), (width * height * 4) as usize);
assert!(ink(&texture) > 100, "{} lit texels", ink(&texture));
for x in 0..width as usize {
assert_eq!(&texture.rgba8[x * 4..x * 4 + 4], &[0, 0, 0, 255]);
}
}
#[test]
fn longer_and_multiline_text_grows_the_texture() {
let style = TextStyle::default();
let one = text_texture("REC", style);
let wide = text_texture("REC REC REC", style);
let tall = text_texture("REC\nREC", style);
assert!(wide.size[0] > one.size[0] * 2);
assert!(tall.size[1] > one.size[1] + 20);
assert_eq!(text_texture("REC", style), one, "repeatable");
assert_eq!(ink(&text_texture("", style)), 0);
}
#[test]
fn a_font_file_changes_the_glyphs_and_bad_bytes_give_none() {
let style = TextStyle::default();
let built_in = text_texture("Tag 42", style);
let fonts = egui::FontDefinitions::default();
let font = &fonts.font_data["Hack"].font;
let other = text_texture_in_font("Tag 42", style, font).unwrap();
assert!(ink(&other) > 100, "{} lit texels", ink(&other));
assert_ne!(other, built_in);
assert_eq!(text_texture_in_font("Tag 42", style, b"not a font"), None);
}
}