use ratatui_core::layout::Rect;
use ratatui_core::style::{Color, Style};
use crate::geometry::Size;
use crate::surface::Surface;
use crate::view::{RenderCtx, View};
pub const FONT_HEIGHT: u16 = 5;
const LETTER_SPACING: u16 = 1;
fn glyph(ch: char) -> Option<[&'static str; 5]> {
let up = ch.to_ascii_uppercase();
Some(match up {
' ' => [" ", " ", " ", " ", " "],
'A' => [" ### ", "# #", "#####", "# #", "# #"],
'B' => ["#### ", "# #", "#### ", "# #", "#### "],
'C' => [" ####", "# ", "# ", "# ", " ####"],
'D' => ["#### ", "# #", "# #", "# #", "#### "],
'E' => ["#####", "# ", "#### ", "# ", "#####"],
'F' => ["#####", "# ", "#### ", "# ", "# "],
'G' => [" ####", "# ", "# ##", "# #", " ####"],
'H' => ["# #", "# #", "#####", "# #", "# #"],
'I' => ["#####", " # ", " # ", " # ", "#####"],
'J' => [" ###", " # ", " # ", "# # ", " ## "],
'K' => ["# #", "# # ", "### ", "# # ", "# #"],
'L' => ["# ", "# ", "# ", "# ", "#####"],
'M' => ["# #", "## ##", "# # #", "# #", "# #"],
'N' => ["# #", "## #", "# # #", "# ##", "# #"],
'O' => [" ### ", "# #", "# #", "# #", " ### "],
'P' => ["#### ", "# #", "#### ", "# ", "# "],
'Q' => [" ### ", "# #", "# # #", "# # ", " ## #"],
'R' => ["#### ", "# #", "#### ", "# # ", "# #"],
'S' => [" ####", "# ", " ### ", " #", "#### "],
'T' => ["#####", " # ", " # ", " # ", " # "],
'U' => ["# #", "# #", "# #", "# #", " ### "],
'V' => ["# #", "# #", "# #", " # # ", " # "],
'W' => ["# #", "# #", "# # #", "## ##", "# #"],
'X' => ["# #", " # # ", " # ", " # # ", "# #"],
'Y' => ["# #", " # # ", " # ", " # ", " # "],
'Z' => ["#####", " # ", " # ", " # ", "#####"],
'0' => [" ### ", "# ##", "# # #", "## #", " ### "],
'1' => [" # ", " ## ", " # ", " # ", "#####"],
'2' => [" ### ", "# #", " ## ", " # ", "#####"],
'3' => ["#### ", " #", " ### ", " #", "#### "],
'4' => ["# # ", "# # ", "#####", " # ", " # "],
'5' => ["#####", "# ", "#### ", " #", "#### "],
'6' => [" ####", "# ", "#### ", "# #", " ### "],
'7' => ["#####", " # ", " # ", " # ", " # "],
'8' => [" ### ", "# #", " ### ", "# #", " ### "],
'9' => [" ### ", "# #", " ####", " #", "#### "],
'.' => [" ", " ", " ", " ", "#"],
',' => [" ", " ", " ", " #", "# "],
'!' => ["#", "#", "#", " ", "#"],
'?' => [" ### ", "# #", " ## ", " ", " # "],
':' => [" ", "#", " ", "#", " "],
'-' => [" ", " ", "###", " ", " "],
'+' => [" ", " # ", "###", " # ", " "],
'=' => [" ", "###", " ", "###", " "],
'/' => [" #", " # ", " # ", " # ", "# "],
_ => return None,
})
}
fn glyph_width(ch: char) -> u16 {
glyph(ch).map(|g| g[0].len() as u16).unwrap_or(0)
}
pub struct AsciiFont {
text: String,
color: Option<Color>,
fill: char,
}
impl AsciiFont {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
color: None,
fill: '█',
}
}
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn fill(mut self, fill: char) -> Self {
self.fill = fill;
self
}
fn total_width(&self) -> u16 {
let mut width = 0u16;
let mut first = true;
for ch in self.text.chars() {
let gw = glyph_width(ch);
if gw == 0 {
continue;
}
if !first {
width = width.saturating_add(LETTER_SPACING);
}
width = width.saturating_add(gw);
first = false;
}
width
}
}
impl View for AsciiFont {
fn measure(&self, available: Size) -> Size {
let width = self.total_width().min(available.width);
let height = if self.text.is_empty() {
0
} else {
FONT_HEIGHT.min(available.height)
};
Size::new(width, height)
}
fn render(&self, area: Rect, surface: &mut Surface, ctx: &RenderCtx) {
if area.is_empty() {
return;
}
let style = Style::default().fg(self.color.unwrap_or(ctx.theme.accent));
let mut x = area.x;
for ch in self.text.chars() {
let Some(rows) = glyph(ch) else { continue };
let gw = rows[0].len() as u16;
for (r, row) in rows.iter().enumerate() {
let y = area.y.saturating_add(r as u16);
if y >= area.bottom() {
break;
}
for (c, cell) in row.chars().enumerate() {
if cell == '#' {
surface.set(x + c as u16, y, self.fill, style);
}
}
}
x = x.saturating_add(gw).saturating_add(LETTER_SPACING);
if x >= area.right() {
break;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Theme;
use crate::test_support::{rainbow_theme, row};
#[test]
fn measures_five_rows_and_summed_width() {
let font = AsciiFont::new("HI");
assert_eq!(font.measure(Size::new(80, 24)), Size::new(11, 5));
assert_eq!(AsciiFont::new("").measure(Size::new(80, 24)).height, 0);
}
#[test]
fn renders_block_glyphs_case_insensitively() {
let theme = Theme::default();
let buf = crate::testing::render(&AsciiFont::new("a"), 6, 5, &theme);
let filled: usize = (0..5)
.map(|y| row(&buf, y).chars().filter(|&c| c == '█').count())
.sum();
assert!(filled > 0, "at least some pixels painted");
assert_eq!(row(&buf, 0).matches('█').count(), 3);
}
#[test]
fn color_defaults_to_accent_and_is_overridable() {
let t = rainbow_theme();
let buf = crate::testing::render(&AsciiFont::new("I"), 6, 5, &t);
assert_eq!(buf[(0, 0)].fg, t.accent);
let custom = Color::Rgb(1, 2, 3);
let buf = crate::testing::render(&AsciiFont::new("I").color(custom), 6, 5, &t);
assert_eq!(buf[(0, 0)].fg, custom);
}
#[test]
fn unknown_chars_and_tiny_areas_do_not_panic() {
let theme = Theme::default();
let _ = crate::testing::render(&AsciiFont::new("~"), 10, 5, &theme);
for (w, h) in [(0u16, 0u16), (1, 1), (2, 3)] {
let _ = crate::testing::render(&AsciiFont::new("HELLO 123"), w, h, &theme);
}
}
}