use crate::FbSurface;
use dotzuki_engine::render::Rgba;
pub const GLYPH_SIZE: u32 = 10;
const CJK_BASELINE: i32 = 2;
static GLYPH_BLOB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/glyphs.bin"));
#[inline]
fn blob_u32(off: usize) -> u32 {
u32::from_le_bytes([
GLYPH_BLOB[off],
GLYPH_BLOB[off + 1],
GLYPH_BLOB[off + 2],
GLYPH_BLOB[off + 3],
])
}
struct GlyphInfo {
width: u32,
height: u32,
x_off: i32,
y_off: i32,
advance: u32,
rows: &'static [u8],
}
fn lookup_glyph(ch: char) -> Option<GlyphInfo> {
let cp = ch as u32;
let count = blob_u32(0) as usize;
let (mut lo, mut hi) = (0usize, count);
while lo < hi {
let mid = lo + (hi - lo) / 2;
let entry = 4 + mid * 8;
let mcp = blob_u32(entry);
if mcp < cp {
lo = mid + 1;
} else if mcp > cp {
hi = mid;
} else {
let off = blob_u32(entry + 4) as usize;
let width = GLYPH_BLOB[off] as u32;
let nrows = GLYPH_BLOB[off + 1] as usize;
let x_off = i16::from_le_bytes([GLYPH_BLOB[off + 2], GLYPH_BLOB[off + 3]]) as i32;
let y_off = i16::from_le_bytes([GLYPH_BLOB[off + 4], GLYPH_BLOB[off + 5]]) as i32;
let advance = GLYPH_BLOB[off + 6] as u32;
let rows = &GLYPH_BLOB[off + 7..off + 7 + nrows * 2];
return Some(GlyphInfo {
width,
height: nrows as u32,
x_off,
y_off,
advance,
rows,
});
}
}
None
}
pub fn is_cjk(ch: char) -> bool {
lookup_glyph(ch).map_or(false, |g| g.advance >= 10)
}
pub fn char_advance(ch: char) -> u32 {
if ch == '▶' || ch == '▼' {
return 10;
}
lookup_glyph(ch).map_or(0, |g| g.advance)
}
pub fn measure_text(text: &str) -> u32 {
text.chars().map(char_advance).sum()
}
const GLYPH_CURSOR_RIGHT: [u8; 8] = [
0b00000000, 0b01111000, 0b01111100, 0b01111110, 0b01111110, 0b01111100, 0b01111000, 0b00000000,
];
const GLYPH_CURSOR_DOWN: [u8; 8] = [
0b00000000, 0b00000000, 0b01111110, 0b01111110, 0b00111100, 0b00011000, 0b00000000, 0b00000000,
];
fn draw_glyph_pixel(x: u32, y: u32, color: Rgba, fb: &mut impl FbSurface) {
if x < fb.width() && y < fb.height() {
fb.set_pixel(x, y, color);
}
}
pub fn draw_char(ch: char, x: u32, y: u32, color: Rgba, fb: &mut impl FbSurface) -> u32 {
let fb_h = fb.height();
if ch == '▶' {
for row in 0..8u32 {
let py = y + row;
if py >= fb_h { break; }
let byte = GLYPH_CURSOR_RIGHT[row as usize];
for col in 0..8u32 {
if byte & (0x80 >> col) != 0 {
draw_glyph_pixel(x + col, py, color, fb);
}
}
}
return 10;
}
if ch == '▼' {
for row in 0..8u32 {
let py = y + row;
if py >= fb_h { break; }
let byte = GLYPH_CURSOR_DOWN[row as usize];
for col in 0..8u32 {
if byte & (0x80 >> col) != 0 {
draw_glyph_pixel(x + col, py, color, fb);
}
}
}
return 10;
}
if let Some(g) = lookup_glyph(ch) {
let start_y = y as i32 + CJK_BASELINE + g.y_off;
for ri in 0..g.height as usize {
let row = u16::from_le_bytes([g.rows[ri * 2], g.rows[ri * 2 + 1]]);
let py = start_y + ri as i32;
if py < 0 || py >= fb_h as i32 { continue; }
for ci in 0..g.width {
if row & (1 << (g.width - 1 - ci)) != 0 {
let px = x + ci + g.x_off as u32;
if px < fb.width() {
fb.set_pixel(px, py as u32, color);
}
}
}
}
return g.advance;
}
0
}
pub fn draw_text(text: &str, mut x: u32, y: u32, color: Rgba, fb: &mut impl FbSurface) {
let fb_w = fb.width();
for ch in text.chars() {
if x >= fb_w {
break;
}
let adv = draw_char(ch, x, y, color, fb);
x += adv;
}
}
fn fill_glyph_block(x: i32, y: i32, scale: u32, color: Rgba, fb: &mut impl FbSurface) {
for dy in 0..scale as i32 {
let py = y + dy;
if py < 0 || py >= fb.height() as i32 {
continue;
}
for dx in 0..scale as i32 {
let px = x + dx;
if px < 0 || px >= fb.width() as i32 {
continue;
}
fb.set_pixel(px as u32, py as u32, color);
}
}
}
pub fn draw_char_scaled(ch: char, x: u32, y: u32, scale: u32, color: Rgba, fb: &mut impl FbSurface) -> u32 {
let scale = scale.max(1);
if scale == 1 {
return draw_char(ch, x, y, color, fb);
}
let s = scale as i32;
if ch == '▶' || ch == '▼' {
let bmp = if ch == '▶' { &GLYPH_CURSOR_RIGHT } else { &GLYPH_CURSOR_DOWN };
for row in 0..8i32 {
let byte = bmp[row as usize];
for col in 0..8i32 {
if byte & (0x80 >> col) != 0 {
fill_glyph_block(x as i32 + col * s, y as i32 + row * s, scale, color, fb);
}
}
}
return 10 * scale;
}
if let Some(g) = lookup_glyph(ch) {
let start_y = y as i32 + (CJK_BASELINE + g.y_off) * s;
for ri in 0..g.height as usize {
let row = u16::from_le_bytes([g.rows[ri * 2], g.rows[ri * 2 + 1]]);
let py = start_y + ri as i32 * s;
for ci in 0..g.width {
if row & (1 << (g.width - 1 - ci)) != 0 {
let px = x as i32 + (ci as i32 + g.x_off) * s;
fill_glyph_block(px, py, scale, color, fb);
}
}
}
return g.advance * scale;
}
0
}
pub fn draw_text_scaled(text: &str, mut x: u32, y: u32, scale: u32, color: Rgba, fb: &mut impl FbSurface) {
for ch in text.chars() {
x += draw_char_scaled(ch, x, y, scale, color, fb);
}
}
pub fn measure_text_scaled(text: &str, scale: u32) -> u32 {
measure_text(text) * scale.max(1)
}
pub mod box_tiles {
pub const TOP_LEFT: [u8; 8] = [
0b00000000, 0b00001111, 0b00011111, 0b00110000, 0b00100000, 0b01100000, 0b01000000,
0b01000000,
];
pub const TOP_RIGHT: [u8; 8] = [
0b00000000, 0b11110000, 0b11111000, 0b00001100, 0b00000100, 0b00000110, 0b00000010,
0b00000010,
];
pub const BOTTOM_LEFT: [u8; 8] = [
0b01000000, 0b01000000, 0b01100000, 0b00100000, 0b00110000, 0b00011111, 0b00001111,
0b00000000,
];
pub const BOTTOM_RIGHT: [u8; 8] = [
0b00000010, 0b00000010, 0b00000110, 0b00000100, 0b00001100, 0b11111000, 0b11110000,
0b00000000,
];
pub const HORIZONTAL: [u8; 8] = [
0b00000000, 0b11111111, 0b11111111, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000000,
];
pub const HORIZONTAL_BOTTOM: [u8; 8] = [
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11111111, 0b11111111,
0b00000000,
];
pub const VERTICAL_LEFT: [u8; 8] = [
0b01100000, 0b01100000, 0b01100000, 0b01100000, 0b01100000, 0b01100000, 0b01100000,
0b01100000,
];
pub const VERTICAL_RIGHT: [u8; 8] = [
0b00000110, 0b00000110, 0b00000110, 0b00000110, 0b00000110, 0b00000110, 0b00000110,
0b00000110,
];
pub mod outside {
pub const TOP_LEFT: [u8; 8] = [
0b11111111, 0b11110000, 0b11100000, 0b11000000, 0b11000000, 0b10000000, 0b10000000,
0b10000000,
];
pub const TOP_RIGHT: [u8; 8] = [
0b11111111, 0b00001111, 0b00000111, 0b00000011, 0b00000011, 0b00000001, 0b00000001,
0b00000001,
];
pub const BOTTOM_LEFT: [u8; 8] = [
0b10000000, 0b10000000, 0b10000000, 0b11000000, 0b11000000, 0b11100000, 0b11110000,
0b11111111,
];
pub const BOTTOM_RIGHT: [u8; 8] = [
0b00000001, 0b00000001, 0b00000001, 0b00000011, 0b00000011, 0b00000111, 0b00001111,
0b11111111,
];
pub const HORIZONTAL: [u8; 8] = [
0b11111111, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000000,
];
pub const HORIZONTAL_BOTTOM: [u8; 8] = [
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b11111111,
];
pub const VERTICAL_LEFT: [u8; 8] = [
0b10000000, 0b10000000, 0b10000000, 0b10000000, 0b10000000, 0b10000000, 0b10000000,
0b10000000,
];
pub const VERTICAL_RIGHT: [u8; 8] = [
0b00000001, 0b00000001, 0b00000001, 0b00000001, 0b00000001, 0b00000001, 0b00000001,
0b00000001,
];
}
}
pub fn draw_glyph(glyph: &[u8; 8], x: u32, y: u32, color: Rgba, bg: Rgba, fb: &mut impl FbSurface) {
let fb_h = fb.height();
let fb_w = fb.width();
for row in 0..8u32 {
let py = y + row;
if py >= fb_h { break; }
let byte = glyph[row as usize];
for col in 0..8u32 {
let px = x + col;
if px >= fb_w { break; }
if byte & (0x80 >> col) != 0 {
fb.set_pixel(px, py, color);
} else {
fb.set_pixel(px, py, bg);
}
}
}
}
pub fn fill_tile(x: u32, y: u32, color: Rgba, fb: &mut impl FbSurface) {
let fb_h = fb.height();
let fb_w = fb.width();
for row in 0..8u32 {
let py = y + row;
if py >= fb_h {
break;
}
for col in 0..8u32 {
let px = x + col;
if px >= fb_w {
break;
}
fb.set_pixel(px, py, color);
}
}
}
pub fn draw_box_tile(
glyph: &[u8; 8],
outside: &[u8; 8],
x: u32,
y: u32,
color: Rgba,
bg: Rgba,
fb: &mut impl FbSurface,
) {
let fb_h = fb.height();
let fb_w = fb.width();
for row in 0..8u32 {
let py = y + row;
if py >= fb_h { break; }
let byte = glyph[row as usize];
let out = outside[row as usize];
for col in 0..8u32 {
let px = x + col;
if px >= fb_w { break; }
let mask = 0x80 >> col;
if out & mask != 0 {
continue;
}
if byte & mask != 0 {
fb.set_pixel(px, py, color);
} else {
fb.set_pixel(px, py, bg);
}
}
}
}
#[cfg(test)]
mod tests {
use crate::FrameBuffer;
use super::*;
use dotzuki_engine::render_config::RenderConfig;
#[test]
fn draw_text_does_not_panic() {
let mut fb = FrameBuffer::new(RenderConfig::new(160, 144), Rgba::WHITE);
draw_text("Hello, World!", 0, 0, Rgba::BLACK, &mut fb);
draw_text("Edge", 150, 140, Rgba::BLACK, &mut fb);
draw_text("", 0, 0, Rgba::BLACK, &mut fb);
}
#[test]
fn draw_text_sets_pixels() {
let mut fb = FrameBuffer::new(RenderConfig::new(160, 144), Rgba::WHITE);
draw_text("A", 0, 0, Rgba::BLACK, &mut fb);
let has_black = (0..12).any(|y| (0..10).any(|x| fb.get_pixel(x, y) == Some(Rgba::BLACK)));
assert!(has_black, "Drawing 'A' should set at least one black pixel");
}
#[test]
fn cjk_lookup_and_is_cjk() {
assert!(is_cjk('你'));
assert!(is_cjk('好'));
assert!(!is_cjk('A'));
assert!(!is_cjk(' '));
}
#[test]
fn draw_char_returns_advance() {
let mut fb = FrameBuffer::new(RenderConfig::new(160, 144), Rgba::WHITE);
let adv_a = draw_char('A', 0, 0, Rgba::BLACK, &mut fb);
let adv_ni = draw_char('你', 50, 0, Rgba::BLACK, &mut fb);
assert_eq!(adv_a, 5); assert_eq!(adv_ni, 10); }
#[test]
fn draw_text_mixed_pixels() {
let mut fb = FrameBuffer::new(RenderConfig::new(160, 144), Rgba::WHITE);
draw_text("Hello你好", 0, 0, Rgba::BLACK, &mut fb);
assert!(fb.get_pixel(0, 3) == Some(Rgba::BLACK), "H left stroke");
let has_cjk = (25..45).any(|x| (0..12).any(|y| fb.get_pixel(x, y) == Some(Rgba::BLACK)));
assert!(has_cjk, "你/好 should render in x=25..44");
}
#[test]
fn box_tiles_outside_masks_disjoint_from_strokes() {
let pairs = [
(&box_tiles::TOP_LEFT, &box_tiles::outside::TOP_LEFT),
(&box_tiles::TOP_RIGHT, &box_tiles::outside::TOP_RIGHT),
(&box_tiles::BOTTOM_LEFT, &box_tiles::outside::BOTTOM_LEFT),
(&box_tiles::BOTTOM_RIGHT, &box_tiles::outside::BOTTOM_RIGHT),
(&box_tiles::HORIZONTAL, &box_tiles::outside::HORIZONTAL),
(&box_tiles::HORIZONTAL_BOTTOM, &box_tiles::outside::HORIZONTAL_BOTTOM),
(&box_tiles::VERTICAL_LEFT, &box_tiles::outside::VERTICAL_LEFT),
(&box_tiles::VERTICAL_RIGHT, &box_tiles::outside::VERTICAL_RIGHT),
];
for (glyph, outside) in pairs {
for row in 0..8 {
assert_eq!(
glyph[row] & outside[row],
0,
"outside mask overlaps the stroke at row {row}"
);
}
}
}
#[test]
fn draw_box_tile_leaves_outside_pixels_untouched() {
let green = Rgba::rgb(0x20, 0x60, 0x20);
let mut fb = FrameBuffer::new(RenderConfig::new(16, 16), green);
draw_box_tile(
&box_tiles::TOP_LEFT,
&box_tiles::outside::TOP_LEFT,
0,
0,
Rgba::BLACK,
Rgba::WHITE,
&mut fb,
);
assert_eq!(fb.get_pixel(0, 0), Some(green));
assert_eq!(fb.get_pixel(4, 1), Some(Rgba::BLACK));
assert_eq!(fb.get_pixel(7, 7), Some(Rgba::WHITE));
}
#[test]
fn full_cjk_repertoire_baked() {
for ch in "陈剑江湖金土星令传奇侠君臣岂".chars() {
assert!(lookup_glyph(ch).is_some(), "CJK glyph {ch:?} should be baked");
assert!(is_cjk(ch), "{ch:?} should be full-width CJK (advance >= 10)");
}
for ch in ":,。!?「」、".chars() {
assert!(lookup_glyph(ch).is_some(), "punctuation {ch:?} should be baked");
}
assert!(
blob_u32(0) > 20_000,
"expected full CJK repertoire, got {} glyphs",
blob_u32(0)
);
}
}