ibm437 0.5.0

IBM437 bitmap font — works with embedded-graphics and raw framebuffers (minifb, softbuffer, SDL2…)
Documentation
//! Render IBM437 text into a raw `&mut [u32]` ARGB framebuffer.
//!
//! This module works with any pixel buffer laid out as packed `u32` values
//! in `0xAA_RR_GG_BB` order — the format used by
//! [minifb](https://crates.io/crates/minifb),
//! [softbuffer](https://crates.io/crates/softbuffer),
//! and many other windowing crates.
//!
//! # Quick start (with minifb)
//!
//! ```rust,no_run
//! use ibm437::framebuffer::FbFont;
//!
//! let font = FbFont::regular_8x8();
//! let mut buffer = vec![0u32; 640 * 480];
//!
//! font.draw_str(&mut buffer, 640, 10, 10, "Hello, IBM437!", 0x00FF_FF00, None);
//! ```
//!
//! # Scaling
//!
//! Glyphs can be enlarged by an integer factor with [`FbFont::with_scale`].
//! Each source pixel is drawn as a `scale × scale` block, so a `2×` 8×8 font
//! renders at 16×16 pixels:
//!
//! ```rust,no_run
//! use ibm437::framebuffer::FbFont;
//!
//! let font = FbFont::regular_8x8().with_scale(2);
//! let mut buffer = vec![0u32; 640 * 480];
//!
//! font.draw_str(&mut buffer, 640, 10, 10, "BIG", 0x00FF_FF00, None);
//! ```

use crate::char_offset::{CHARS_PER_ROW, char_offset_impl};

/// A lightweight handle to one of the IBM437 bitmap fonts,
/// ready to render into a `u32` framebuffer.
///
/// Instances are created via `const` constructors and carry no allocation.
#[derive(Clone, Copy)]
pub struct FbFont {
    data: &'static [u8],
    char_width: usize,
    char_height: usize,
    scale: usize,
}

impl FbFont {
    /// 8×8 regular font.
    #[cfg(feature = "regular8x8")]
    pub const fn regular_8x8() -> Self {
        Self {
            data: crate::IBM437_8X8_REGULAR_DATA,
            char_width: 8,
            char_height: 8,
            scale: 1,
        }
    }

    /// 8×8 bold font.
    #[cfg(feature = "bold8x8")]
    pub const fn bold_8x8() -> Self {
        Self {
            data: crate::IBM437_8X8_BOLD_DATA,
            char_width: 8,
            char_height: 8,
            scale: 1,
        }
    }

    /// 9×14 regular font.
    #[cfg(feature = "regular9x14")]
    pub const fn regular_9x14() -> Self {
        Self {
            data: crate::IBM437_9X14_REGULAR_DATA,
            char_width: 9,
            char_height: 14,
            scale: 1,
        }
    }

    /// Return a copy of this font scaled by an integer factor.
    ///
    /// Every source pixel is rendered as a `scale × scale` block, so the
    /// effective glyph size becomes `char_width * scale` by
    /// `char_height * scale`. A `scale` of `0` is treated as `1`.
    #[inline]
    #[must_use]
    pub const fn with_scale(mut self, scale: usize) -> Self {
        self.scale = if scale == 0 { 1 } else { scale };
        self
    }

    /// Current scale factor (`1` unless changed with [`with_scale`](Self::with_scale)).
    #[inline]
    pub const fn scale(&self) -> usize {
        self.scale
    }

    /// Rendered character width in pixels (base width × scale).
    #[inline]
    pub const fn char_width(&self) -> usize {
        self.char_width * self.scale
    }

    /// Rendered character height in pixels (base height × scale).
    #[inline]
    pub const fn char_height(&self) -> usize {
        self.char_height * self.scale
    }

    /// Draw a single character into `buffer`.
    ///
    /// - `buf_width` — width of the framebuffer in pixels.
    /// - `(x, y)` — top-left corner of the glyph, in pixels.
    /// - `fg` — foreground colour (`0x00RRGGBB`).
    /// - `bg` — optional background colour; `None` = transparent (skip off-pixels).
    ///
    /// Pixels that fall outside the buffer are silently clipped.
    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; // bits per spritesheet row

        let Some(buf_height) = buffer.len().checked_div(buf_width) else {
            return;
        };

        let scale = self.scale;
        for gy in 0..self.char_height {
            // Vertical output span for this source row, clipped to the buffer.
            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;
                }

                // Read the source bit once, regardless of scale.
                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, // transparent off-pixel
                    }
                };

                // Fill the clipped scale×scale block as contiguous row spans.
                let x1 = (x0 + scale).min(buf_width);
                for py in y0..y1 {
                    let start = py * buf_width;
                    buffer[start + x0..start + x1].fill(color);
                }
            }
        }
    }

    /// Draw a string into `buffer`.
    ///
    /// Characters are placed left to right starting at `(x, y)`.
    /// Newlines (`'\n'`) move the cursor to the next line at column `x`.
    ///
    /// Returns `(next_x, next_y)` — the cursor position after the last character,
    /// which is useful for chaining multiple `draw_str` calls.
    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)
    }
}

// ─── Tests ────────────────────────────────────────────────────────────

#[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));

        // Collect "lit" positions
        let pattern: Vec<Vec<u8>> = (0..h)
            .map(|y| (0..w).map(|x| buf[y * w + x] as u8).collect())
            .collect();

        // Row 0 and 1: blank
        assert_eq!(pattern[0], [0, 0, 0, 0, 0, 0, 0, 0]);
        assert_eq!(pattern[1], [0, 0, 0, 0, 0, 0, 0, 0]);
        // Row 2: ··####··
        assert_eq!(pattern[2], [0, 0, 1, 1, 1, 1, 0, 0]);
        // Row 3: ······#·
        assert_eq!(pattern[3], [0, 0, 0, 0, 0, 0, 1, 0]);
        // Row 4: ··#####·
        assert_eq!(pattern[4], [0, 0, 1, 1, 1, 1, 1, 0]);
        // Row 5: ·#····#·
        assert_eq!(pattern[5], [0, 1, 0, 0, 0, 0, 1, 0]);
        // Row 6: ··######
        assert_eq!(pattern[6], [0, 0, 1, 1, 1, 1, 1, 1]);
        // Row 7: blank
        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); // one char after newline
        assert_eq!(cy, 8); // second line
    }

    #[test]
    fn clipping_does_not_panic() {
        let font = FbFont::regular_8x8();
        let mut buf = vec![0u32; 4 * 4]; // tiny 4×4 buffer
        // should draw partially, no panic
        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));

        // Each source pixel (bx, by) must equal every pixel of its 3×3 block.
        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); // one scaled char after newline
        assert_eq!(cy, 16); // second line at scaled height
    }

    #[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);

        // Space glyph, transparent bg → every pixel should still be sentinel
        for &px in &buf {
            assert_eq!(px, sentinel);
        }
    }
}