use crate::renderer;
use glyphr_types::{AlignH, AlignV, BitmapFormat, Font};
pub struct Callbacks<P>
where
P: FnMut(u16, u16, u32) -> bool,
{
width: u16,
height: u16,
write_pixel: P,
}
impl<P> Callbacks<P>
where
P: FnMut(u16, u16, u32) -> bool,
{
pub fn new(width: u16, height: u16, write_pixel: P) -> Self {
Self {
width,
height,
write_pixel,
}
}
pub fn dimensions(&self) -> (u16, u16) {
(self.width, self.height)
}
pub fn write_pixel(&mut self, x: u16, y: u16, color: u32) -> bool {
(self.write_pixel)(x, y, color)
}
}
pub struct BulkCallbacks<'a, P>
where
P: FnMut(i32, i32, u16, u16, &[u32]) -> bool,
{
width: u16,
height: u16,
scratch: &'a mut [u32],
write_glyph: P,
}
impl<'a, P> BulkCallbacks<'a, P>
where
P: FnMut(i32, i32, u16, u16, &[u32]) -> bool,
{
pub fn new(width: u16, height: u16, scratch: &'a mut [u32], write_glyph: P) -> Self {
Self {
width,
height,
scratch,
write_glyph,
}
}
pub fn dimensions(&self) -> (u16, u16) {
(self.width, self.height)
}
pub fn scratch_capacity(&self) -> usize {
self.scratch.len()
}
pub fn scratch_mut(&mut self) -> &mut [u32] {
self.scratch
}
pub fn write_glyph(&mut self, x: i32, y: i32, width: u16, height: u16, pixels: &[u32]) -> bool {
(self.write_glyph)(x, y, width, height, pixels)
}
pub(crate) fn emit_from_scratch(
&mut self,
x: i32,
y: i32,
width: u16,
height: u16,
used_pixels: usize,
) -> bool {
let pixels = &self.scratch[..used_pixels];
(self.write_glyph)(x, y, width, height, pixels)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RenderConfig {
pub color: u32,
pub sdf: SdfConfig,
}
impl RenderConfig {
pub const fn with_color(mut self, color: u32) -> Self {
self.color = color;
self
}
pub const fn with_sdf(mut self, sdf: SdfConfig) -> Self {
self.sdf = sdf;
self
}
}
impl Default for RenderConfig {
fn default() -> Self {
Self {
color: 0xffffff,
sdf: SdfConfig::default(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SdfConfig {
pub size: u16,
pub mid_value: f32,
pub smoothing: f32,
}
impl SdfConfig {
pub const fn with_size(mut self, size: u16) -> Self {
self.size = size;
self
}
pub const fn with_mid_value(mut self, mid_value: f32) -> Self {
self.mid_value = mid_value;
self
}
pub const fn with_smoothing(mut self, smoothing: f32) -> Self {
self.smoothing = smoothing;
self
}
}
impl Default for SdfConfig {
fn default() -> Self {
Self {
size: 16,
mid_value: 0.5,
smoothing: 0.1,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TextAlign {
pub horizontal: AlignH,
pub vertical: AlignV,
}
impl TextAlign {
pub const fn new(horizontal: AlignH, vertical: AlignV) -> Self {
Self {
horizontal,
vertical,
}
}
}
impl Default for TextAlign {
fn default() -> Self {
Self {
horizontal: AlignH::Left,
vertical: AlignV::Top,
}
}
}
pub struct Glyphr {
render_config: RenderConfig,
}
impl Default for Glyphr {
fn default() -> Self {
Self::new()
}
}
impl Glyphr {
pub fn new() -> Self {
Self {
render_config: RenderConfig::default(),
}
}
pub fn with_config(render_config: RenderConfig) -> Self {
Self { render_config }
}
pub fn set_config(&mut self, config: RenderConfig) {
self.render_config = config;
}
pub fn config(&self) -> &RenderConfig {
&self.render_config
}
pub fn draw_text<P>(
&self,
target: &mut Callbacks<P>,
text: &str,
font: Font,
mut x: i32,
y: i32,
align: TextAlign,
) -> Result<(), GlyphrError>
where
P: FnMut(u16, u16, u32) -> bool,
{
let scale = match font.format {
BitmapFormat::SDF => self.render_config.sdf.size as f32 / font.size as f32,
BitmapFormat::Bitmap => 1.0,
};
let ascent = i32::from(font.ascent);
let descent = i32::from(font.descent);
let x_offset = match align.horizontal {
AlignH::Center => self.measure_text(text, font) / 2,
AlignH::Right => self.measure_text(text, font),
AlignH::Left => 0,
};
let y_offset = match align.vertical {
AlignV::Top => (descent as f32 * scale) as i32,
AlignV::Center => {
let total_height = (ascent - descent) as f32 * scale;
-(total_height / 2.0) as i32
}
AlignV::Baseline => -(ascent as f32 * scale) as i32,
};
for c in text.chars() {
let glyph = font.find_glyph(c).ok_or(GlyphrError::InvalidGlyph(c))?;
let glyph_y = y
+ y_offset
+ ((ascent - i32::from(glyph.ymin) - i32::from(glyph.height)) as f32 * scale)
as i32;
renderer::render_glyph(x - x_offset, glyph_y, c, font, self, scale, target)?;
x += (glyph.advance_width as f32 * scale) as i32;
}
Ok(())
}
pub fn draw_text_bulk<P>(
&self,
target: &mut BulkCallbacks<'_, P>,
text: &str,
font: Font,
mut x: i32,
y: i32,
align: TextAlign,
) -> Result<(), GlyphrError>
where
P: FnMut(i32, i32, u16, u16, &[u32]) -> bool,
{
let scale = match font.format {
BitmapFormat::SDF => self.render_config.sdf.size as f32 / font.size as f32,
BitmapFormat::Bitmap => 1.0,
};
let ascent = i32::from(font.ascent);
let descent = i32::from(font.descent);
let x_offset = match align.horizontal {
AlignH::Center => self.measure_text(text, font) / 2,
AlignH::Right => self.measure_text(text, font),
AlignH::Left => 0,
};
let y_offset = match align.vertical {
AlignV::Top => (descent as f32 * scale) as i32,
AlignV::Center => {
let total_height = (ascent - descent) as f32 * scale;
-(total_height / 2.0) as i32
}
AlignV::Baseline => -(ascent as f32 * scale) as i32,
};
for c in text.chars() {
let glyph = font.find_glyph(c).ok_or(GlyphrError::InvalidGlyph(c))?;
let glyph_y = y
+ y_offset
+ ((ascent - i32::from(glyph.ymin) - i32::from(glyph.height)) as f32 * scale)
as i32;
renderer::render_glyph_bulk(x - x_offset, glyph_y, c, font, self, scale, target)?;
x += (glyph.advance_width as f32 * scale) as i32;
}
Ok(())
}
pub fn measure_text(&self, phrase: &str, font: Font) -> i32 {
let scale = match font.format {
BitmapFormat::SDF => self.render_config.sdf.size as f32 / font.size as f32,
BitmapFormat::Bitmap => 1.0,
};
let mut tot = 0;
for c in phrase.chars() {
if let Some(glyph) = font.find_glyph(c) {
tot += (glyph.advance_width as f32 * scale) as i32;
}
}
tot
}
}
#[derive(Debug, Clone)]
pub enum GlyphrError {
OutOfBounds,
InvalidGlyph(char),
BufferTooSmall { needed: usize, available: usize },
InvalidTarget,
}
impl core::fmt::Display for GlyphrError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
GlyphrError::OutOfBounds => write!(f, "Rendering position is out of bounds"),
GlyphrError::InvalidGlyph(c) => write!(f, "Glyph not found: '{c}'"),
GlyphrError::BufferTooSmall { needed, available } => {
write!(
f,
"Scratch buffer too small: needed {needed} pixels, available {available}"
)
}
GlyphrError::InvalidTarget => write!(f, "Invalid render target"),
}
}
}
impl core::error::Error for GlyphrError {}
#[cfg(test)]
mod tests {
use super::*;
use glyphr_types::Glyph;
fn bitmap_font<'a>(glyphs: &'a [Glyph<'a>]) -> Font<'a> {
Font {
glyphs,
size: 16,
ascent: 2,
descent: 0,
line_gap: 0,
format: BitmapFormat::Bitmap,
}
}
#[test]
fn sdf_config_default_values() {
let cfg = SdfConfig::default();
assert_eq!(cfg.size, 16);
assert_eq!(cfg.mid_value, 0.5);
assert_eq!(cfg.smoothing, 0.1);
}
#[test]
fn render_config_default_values() {
let cfg = RenderConfig::default();
assert_eq!(cfg.color, 0xffffff);
assert_eq!(cfg.sdf.size, 16);
assert_eq!(cfg.sdf.mid_value, 0.5);
assert_eq!(cfg.sdf.smoothing, 0.1);
}
#[test]
fn glyphr_new_initializes_correctly() {
let glyphr = Glyphr::new();
assert_eq!(glyphr.render_config.color, 0xffffff);
assert_eq!(glyphr.render_config.sdf.size, 16);
assert_eq!(glyphr.render_config.sdf.mid_value, 0.5);
assert_eq!(glyphr.render_config.sdf.smoothing, 0.1);
}
#[test]
fn callbacks_sink_works() {
let mut writes = 0u32;
let mut target = Callbacks::new(8, 8, |_x, _y, _color| {
writes += 1;
true
});
assert!(target.write_pixel(1, 1, 0xff00ff00));
assert_eq!(writes, 1);
}
#[test]
fn bulk_callbacks_sink_works() {
let mut writes = 0u32;
let mut scratch = [0u32; 16];
let mut target = BulkCallbacks::new(8, 8, &mut scratch, |_x, _y, w, h, pixels| {
writes += 1;
assert_eq!(w, 2);
assert_eq!(h, 2);
assert_eq!(pixels.len(), 4);
true
});
let tile = [0xff00ff00u32; 4];
assert!(target.write_glyph(1, 1, 2, 2, &tile));
assert_eq!(writes, 1);
}
#[test]
fn draw_text_bulk_errors_when_scratch_too_small() {
let glyph_bitmap = [0b1111_0000u8];
let glyphs = [Glyph {
character: 'A',
bitmap: &glyph_bitmap,
width: 2,
height: 2,
xmin: 0,
ymin: 0,
advance_width: 2,
}];
let font = Font {
glyphs: &glyphs,
size: 16,
ascent: 2,
descent: 0,
line_gap: 0,
format: BitmapFormat::Bitmap,
};
let glyphr = Glyphr::new();
let mut scratch = [0u32; 3];
let mut target = BulkCallbacks::new(16, 16, &mut scratch, |_x, _y, _w, _h, _pixels| true);
let result = glyphr.draw_text_bulk(
&mut target,
"A",
font,
0,
0,
TextAlign::new(AlignH::Left, AlignV::Top),
);
assert!(matches!(
result,
Err(GlyphrError::BufferTooSmall {
needed: 4,
available: 3
})
));
}
#[test]
fn draw_text_returns_invalid_glyph_for_missing_character() {
let glyph_bitmap = [0b1000_0000u8];
let glyphs = [Glyph {
character: 'A',
bitmap: &glyph_bitmap,
width: 1,
height: 1,
xmin: 0,
ymin: 0,
advance_width: 1,
}];
let font = bitmap_font(&glyphs);
let glyphr = Glyphr::new();
let mut target = Callbacks::new(8, 8, |_x, _y, _color| true);
let result = glyphr.draw_text(
&mut target,
"AZ",
font,
0,
0,
TextAlign::new(AlignH::Left, AlignV::Top),
);
assert!(matches!(result, Err(GlyphrError::InvalidGlyph('Z'))));
}
#[test]
fn draw_text_bulk_emits_once_per_glyph() {
let glyph_bitmap = [0b1000_0000u8, 0b1000_0000u8];
let glyphs = [
Glyph {
character: 'A',
bitmap: &glyph_bitmap[0..1],
width: 1,
height: 1,
xmin: 0,
ymin: 0,
advance_width: 2,
},
Glyph {
character: 'B',
bitmap: &glyph_bitmap[1..2],
width: 1,
height: 1,
xmin: 0,
ymin: 0,
advance_width: 3,
},
];
let font = bitmap_font(&glyphs);
let glyphr = Glyphr::new();
let mut scratch = [0u32; 8];
let mut calls = 0usize;
let mut x_positions = [0i32; 2];
let mut target = BulkCallbacks::new(32, 32, &mut scratch, |x, _y, w, h, pixels| {
x_positions[calls] = x;
assert_eq!(w, 1);
assert_eq!(h, 1);
assert_eq!(pixels.len(), 1);
calls += 1;
true
});
glyphr
.draw_text_bulk(
&mut target,
"AB",
font,
5,
0,
TextAlign::new(AlignH::Left, AlignV::Top),
)
.unwrap();
assert_eq!(calls, 2);
assert_eq!(x_positions[0], 5);
assert_eq!(x_positions[1], 7);
}
#[test]
fn measure_text_ignores_missing_glyphs() {
let glyph_bitmap = [0b1000_0000u8];
let glyphs = [Glyph {
character: 'A',
bitmap: &glyph_bitmap,
width: 1,
height: 1,
xmin: 0,
ymin: 0,
advance_width: 4,
}];
let font = bitmap_font(&glyphs);
let glyphr = Glyphr::new();
assert_eq!(glyphr.measure_text("AZA", font), 8);
}
}