use crate::char_offset::{CHARS_PER_ROW, char_offset_impl};
#[derive(Clone, Copy)]
pub struct FbFont {
data: &'static [u8],
char_width: usize,
char_height: usize,
scale: usize,
}
impl FbFont {
#[cfg(feature = "regular8x8")]
pub const fn regular_8x8() -> Self {
Self {
data: crate::IBM437_8X8_REGULAR_DATA,
char_width: 8,
char_height: 8,
scale: 1,
}
}
#[cfg(feature = "bold8x8")]
pub const fn bold_8x8() -> Self {
Self {
data: crate::IBM437_8X8_BOLD_DATA,
char_width: 8,
char_height: 8,
scale: 1,
}
}
#[cfg(feature = "regular9x14")]
pub const fn regular_9x14() -> Self {
Self {
data: crate::IBM437_9X14_REGULAR_DATA,
char_width: 9,
char_height: 14,
scale: 1,
}
}
#[inline]
#[must_use]
pub const fn with_scale(mut self, scale: usize) -> Self {
self.scale = if scale == 0 { 1 } else { scale };
self
}
#[inline]
pub const fn scale(&self) -> usize {
self.scale
}
#[inline]
pub const fn char_width(&self) -> usize {
self.char_width * self.scale
}
#[inline]
pub const fn char_height(&self) -> usize {
self.char_height * self.scale
}
pub fn draw_char(
&self,
buffer: &mut [u32],
buf_width: usize,
x: usize,
y: usize,
c: char,
fg: u32,
bg: Option<u32>,
) {
let idx = char_offset_impl(c);
let glyph_col = idx % CHARS_PER_ROW;
let glyph_row = idx / CHARS_PER_ROW;
let row_bits = CHARS_PER_ROW * self.char_width;
let Some(buf_height) = buffer.len().checked_div(buf_width) else {
return;
};
let scale = self.scale;
for gy in 0..self.char_height {
let y0 = y + gy * scale;
if y0 >= buf_height {
break;
}
let y1 = (y0 + scale).min(buf_height);
let bit_row =
(glyph_row * self.char_height + gy) * row_bits + glyph_col * self.char_width;
for gx in 0..self.char_width {
let x0 = x + gx * scale;
if x0 >= buf_width {
break;
}
let bit_index = bit_row + gx;
let is_set = (self.data[bit_index / 8] >> (7 - bit_index % 8)) & 1 != 0;
let color = if is_set {
fg
} else {
match bg {
Some(c) => c,
None => continue, }
};
let x1 = (x0 + scale).min(buf_width);
for py in y0..y1 {
let start = py * buf_width;
buffer[start + x0..start + x1].fill(color);
}
}
}
}
pub fn draw_str(
&self,
buffer: &mut [u32],
buf_width: usize,
x: usize,
y: usize,
text: &str,
fg: u32,
bg: Option<u32>,
) -> (usize, usize) {
let mut cx = x;
let mut cy = y;
for c in text.chars() {
if c == '\n' {
cx = x;
cy += self.char_height();
continue;
}
self.draw_char(buffer, buf_width, cx, cy, c, fg, bg);
cx += self.char_width();
}
(cx, cy)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn draw_char_a_matches_expected_pattern() {
let font = FbFont::regular_8x8();
let w = 8;
let h = 8;
let mut buf = vec![0u32; w * h];
font.draw_char(&mut buf, w, 0, 0, 'a', 1, Some(0));
let pattern: Vec<Vec<u8>> = (0..h)
.map(|y| (0..w).map(|x| buf[y * w + x] as u8).collect())
.collect();
assert_eq!(pattern[0], [0, 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(pattern[1], [0, 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(pattern[2], [0, 0, 1, 1, 1, 1, 0, 0]);
assert_eq!(pattern[3], [0, 0, 0, 0, 0, 0, 1, 0]);
assert_eq!(pattern[4], [0, 0, 1, 1, 1, 1, 1, 0]);
assert_eq!(pattern[5], [0, 1, 0, 0, 0, 0, 1, 0]);
assert_eq!(pattern[6], [0, 0, 1, 1, 1, 1, 1, 1]);
assert_eq!(pattern[7], [0, 0, 0, 0, 0, 0, 0, 0]);
}
#[test]
fn draw_str_newline_resets_column() {
let font = FbFont::regular_8x8();
let w = 80;
let h = 24;
let mut buf = vec![0u32; w * h];
let (cx, cy) = font.draw_str(&mut buf, w, 0, 0, "AB\nC", 1, None);
assert_eq!(cx, 8); assert_eq!(cy, 8); }
#[test]
fn clipping_does_not_panic() {
let font = FbFont::regular_8x8();
let mut buf = vec![0u32; 4 * 4]; font.draw_char(&mut buf, 4, 2, 2, 'X', 0x00FFFFFF, None);
font.draw_str(&mut buf, 4, 0, 0, "Hello, World!", 0x00FFFFFF, None);
}
#[test]
fn scale_doubles_rendered_dimensions() {
let font = FbFont::regular_8x8().with_scale(2);
assert_eq!(font.scale(), 2);
assert_eq!(font.char_width(), 16);
assert_eq!(font.char_height(), 16);
}
#[test]
fn scale_replicates_each_pixel_as_a_block() {
let base = FbFont::regular_8x8();
let scaled = base.with_scale(3);
let w = 8 * 3;
let h = 8 * 3;
let mut base_buf = vec![0u32; 8 * 8];
let mut scaled_buf = vec![0u32; w * h];
base.draw_char(&mut base_buf, 8, 0, 0, 'a', 1, Some(0));
scaled.draw_char(&mut scaled_buf, w, 0, 0, 'a', 1, Some(0));
for by in 0..8 {
for bx in 0..8 {
let src = base_buf[by * 8 + bx];
for sy in 0..3 {
for sx in 0..3 {
let px = bx * 3 + sx;
let py = by * 3 + sy;
assert_eq!(scaled_buf[py * w + px], src, "mismatch at ({bx},{by})");
}
}
}
}
}
#[test]
fn scale_zero_falls_back_to_one() {
let font = FbFont::regular_8x8().with_scale(0);
assert_eq!(font.scale(), 1);
assert_eq!(font.char_width(), 8);
}
#[test]
fn draw_str_scaled_newline_uses_scaled_height() {
let font = FbFont::regular_8x8().with_scale(2);
let w = 160;
let h = 48;
let mut buf = vec![0u32; w * h];
let (cx, cy) = font.draw_str(&mut buf, w, 0, 0, "AB\nC", 1, None);
assert_eq!(cx, 16); assert_eq!(cy, 16); }
#[test]
fn transparent_background_does_not_overwrite() {
let font = FbFont::regular_8x8();
let w = 8;
let h = 8;
let sentinel = 0xDEAD_BEEF;
let mut buf = vec![sentinel; w * h];
font.draw_char(&mut buf, w, 0, 0, ' ', 0x00FFFFFF, None);
for &px in &buf {
assert_eq!(px, sentinel);
}
}
}