ibm437 0.4.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);
//! ```

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,
}

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,
        }
    }

    /// 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,
        }
    }

    /// 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,
        }
    }

    /// Character width in pixels.
    #[inline]
    pub const fn char_width(&self) -> usize {
        self.char_width
    }

    /// Character height in pixels.
    #[inline]
    pub const fn char_height(&self) -> usize {
        self.char_height
    }

    /// 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 buf_height = if buf_width > 0 {
            buffer.len() / buf_width
        } else {
            return;
        };

        for gy in 0..self.char_height {
            let py = y + gy;
            if py >= buf_height {
                break;
            }
            for gx in 0..self.char_width {
                let px = x + gx;
                if px >= buf_width {
                    break;
                }

                let bit_index = (glyph_row * self.char_height + gy) * row_bits
                    + glyph_col * self.char_width
                    + gx;
                let byte = self.data[bit_index / 8];
                let is_set = (byte >> (7 - (bit_index % 8))) & 1 != 0;

                if is_set {
                    buffer[py * buf_width + px] = fg;
                } else if let Some(bg_color) = bg {
                    buffer[py * buf_width + px] = bg_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 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);
        }
    }
}