pub const NO_OWNER: usize = usize::MAX;
pub struct Glyphs {
pub w: usize,
pub h: usize,
pub chars: Vec<char>,
pub owner: Vec<usize>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, clap::ValueEnum)]
pub enum FontKind {
Auto,
Shadow,
Block,
Plain,
}
const SHADOW: [[&str; 6]; 16] = [
[
" ██████╗ ",
"██╔═████╗",
"██║██╔██║",
"████╔╝██║",
"╚██████╔╝",
" ╚═════╝ ",
],
[" ██╗", "███║", "╚██║", " ██║", " ██║", " ╚═╝"],
["██████╗ ", "╚════██╗", " █████╔╝", "██╔═══╝ ", "███████╗", "╚══════╝"],
["██████╗ ", "╚════██╗", " █████╔╝", " ╚═══██╗", "██████╔╝", "╚═════╝ "],
["██╗ ██╗", "██║ ██║", "███████║", "╚════██║", " ██║", " ╚═╝"],
["███████╗", "██╔════╝", "███████╗", "╚════██║", "███████║", "╚══════╝"],
[
" ██████╗ ",
"██╔════╝ ",
"███████╗ ",
"██╔═══██╗",
"╚██████╔╝",
" ╚═════╝ ",
],
["███████╗", "╚════██║", " ██╔╝", " ██╔╝ ", " ██║ ", " ╚═╝ "],
[" █████╗ ", "██╔══██╗", "╚█████╔╝", "██╔══██╗", "╚█████╔╝", " ╚════╝ "],
[" █████╗ ", "██╔══██╗", "╚██████║", " ╚═══██║", " █████╔╝", " ╚════╝ "],
[" ", "██╗", "╚═╝", "██╗", "╚═╝", " "],
[" ", " ", " ", " ", "██╗", "╚═╝"],
["██████╗ ", "██╔══██╗", "██║ ██║", "██║ ██║", "██████╔╝", "╚═════╝ "],
["██╗ ██╗", "██║ ██║", "███████║", "██╔══██║", "██║ ██║", "╚═╝ ╚═╝"],
[
"███╗ ███╗",
"████╗ ████║",
"██╔████╔██║",
"██║╚██╔╝██║",
"██║ ╚═╝ ██║",
"╚═╝ ╚═╝",
],
[" ", " ", " ", " ", " ", " "],
];
const SHADOW_DIGIT_W: usize = 9;
#[rustfmt::skip]
fn pixels(c: char) -> Option<[&'static str; 5]> {
Some(match c.to_ascii_uppercase() {
'0' => ["###", "#.#", "#.#", "#.#", "###"],
'1' => ["##.", ".#.", ".#.", ".#.", "###"],
'2' => ["###", "..#", "###", "#..", "###"],
'3' => ["###", "..#", ".##", "..#", "###"],
'4' => ["#.#", "#.#", "###", "..#", "..#"],
'5' => ["###", "#..", "###", "..#", "###"],
'6' => ["###", "#..", "###", "#.#", "###"],
'7' => ["###", "..#", "..#", "..#", "..#"],
'8' => ["###", "#.#", "###", "#.#", "###"],
'9' => ["###", "#.#", "###", "..#", "###"],
':' => [".", "#", ".", "#", "."],
'.' => [".", ".", ".", ".", "#"],
'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' => ["###", "..#", ".#.", "#..", "###"],
' ' => ["..", "..", "..", "..", ".."],
'-' => ["...", "...", "###", "...", "..."],
'+' => ["...", ".#.", "###", ".#.", "..."],
'/' => ["..#", "..#", ".#.", "#..", "#.."],
'!' => ["#", "#", "#", ".", "#"],
'?' => ["###", "..#", ".#.", "...", ".#."],
',' => [".", ".", ".", "#", "#"],
'\'' => ["#", "#", ".", ".", "."],
_ => return None,
})
}
pub fn shadow_supports(text: &str) -> bool {
text.chars().all(|c| glyph_index(c).is_some())
}
pub fn block_supports(text: &str) -> bool {
text.chars().all(|c| pixels(c).is_some())
}
fn glyph_index(c: char) -> Option<usize> {
match c {
'0'..='9' => Some(c as usize - '0' as usize),
':' => Some(10),
'.' => Some(11),
'd' | 'D' => Some(12),
'h' | 'H' => Some(13),
'm' | 'M' => Some(14),
' ' => Some(15),
_ => None,
}
}
pub fn shadow(text: &str) -> Glyphs {
let mut rows: Vec<Vec<(char, usize)>> = vec![Vec::new(); 6];
for (n, c) in text.chars().enumerate() {
let Some(g) = glyph_index(c) else { continue };
let gw = SHADOW[g][0].chars().count();
let pad_w = if g < 10 { SHADOW_DIGIT_W } else { gw };
let left = (pad_w - gw) / 2;
for (r, row) in rows.iter_mut().enumerate() {
if n > 0 {
row.push((' ', NO_OWNER));
}
row.extend(std::iter::repeat_n((' ', n), left));
row.extend(SHADOW[g][r].chars().map(|ch| (ch, n)));
row.extend(std::iter::repeat_n((' ', n), pad_w - gw - left));
}
}
from_rows(&rows)
}
pub fn block_width(text: &str, scale: usize) -> usize {
let px: usize =
text.chars().filter_map(pixels).map(|g| g[0].len()).sum::<usize>() + text.chars().count().saturating_sub(1);
px * scale
}
pub fn block_height(scale: usize) -> usize {
(5 * scale).div_ceil(2)
}
pub fn block(text: &str, scale: usize) -> Glyphs {
let scale = scale.max(1);
let mut px: Vec<Vec<bool>> = vec![Vec::new(); 5];
let mut px_owner = Vec::new();
for (n, c) in text.chars().enumerate() {
let Some(g) = pixels(c) else { continue };
if n > 0 {
px_owner.push(NO_OWNER);
}
px_owner.extend(std::iter::repeat_n(n, g[0].len()));
for (r, row) in px.iter_mut().enumerate() {
if n > 0 {
row.push(false);
}
row.extend(g[r].chars().map(|ch| ch == '#'));
}
}
let pw = px[0].len() * scale;
let ph = 5 * scale;
let get = |x: usize, y: usize| y < ph && px[y / scale][x / scale];
let h = block_height(scale);
let mut chars = Vec::with_capacity(pw * h);
let mut owner = Vec::with_capacity(pw * h);
for r in 0..h {
for x in 0..pw {
chars.push(match (get(x, 2 * r), get(x, 2 * r + 1)) {
(true, true) => '█',
(true, false) => '▀',
(false, true) => '▄',
(false, false) => ' ',
});
owner.push(px_owner[x / scale]);
}
}
Glyphs { w: pw, h, chars, owner }
}
pub fn plain(text: &str) -> Glyphs {
use unicode_width::UnicodeWidthChar;
let mut row = Vec::new();
for (i, c) in text.chars().enumerate() {
match c.width() {
Some(1) => row.push((c, i)),
Some(2) => row.extend([(c, i), (crate::term::WIDE_TAIL, i)]),
_ => {}
}
}
from_rows(&[row])
}
fn from_rows(rows: &[Vec<(char, usize)>]) -> Glyphs {
let w = rows.iter().map(Vec::len).max().unwrap_or(0);
let mut chars = Vec::with_capacity(w * rows.len());
let mut owner = Vec::with_capacity(w * rows.len());
for r in rows {
chars.extend(r.iter().map(|&(c, _)| c));
owner.extend(r.iter().map(|&(_, o)| o));
chars.extend(std::iter::repeat_n(' ', w - r.len()));
owner.extend(std::iter::repeat_n(NO_OWNER, w - r.len()));
}
Glyphs {
w,
h: rows.len(),
chars,
owner,
}
}
pub fn stack(top: &Glyphs, gap: usize, bottom: &Glyphs) -> (Glyphs, usize) {
let w = top.w.max(bottom.w);
let h = top.h + gap + bottom.h;
let mut chars = vec![' '; w * h];
let mut owner = vec![NO_OWNER; w * h];
let tx = (w - top.w) / 2;
for i in 0..top.chars.len() {
let (r, c) = (i / top.w, i % top.w);
chars[r * w + tx + c] = top.chars[i];
owner[r * w + tx + c] = top.owner[i];
}
let (bx, by) = ((w - bottom.w) / 2, top.h + gap);
for i in 0..bottom.chars.len() {
let (r, c) = (i / bottom.w, i % bottom.w);
chars[(by + r) * w + bx + c] = bottom.chars[i];
}
(Glyphs { w, h, chars, owner }, by)
}
pub fn render(text: &str, kind: FontKind, max_w: usize, max_h: usize, scale: Option<usize>) -> Glyphs {
let fits = |g: &Glyphs| g.w <= max_w && g.h <= max_h;
if let Some(s) = scale {
let g = block(text, s);
if fits(&g) {
return g;
}
}
let kind = match kind {
FontKind::Shadow | FontKind::Auto if !shadow_supports(text) => FontKind::Block,
FontKind::Block if !block_supports(text) => FontKind::Plain,
k => k,
};
match kind {
FontKind::Plain => plain(text),
FontKind::Shadow => {
let g = shadow(text);
if fits(&g) {
g
} else {
render(text, FontKind::Auto, max_w, max_h, None)
}
}
FontKind::Block => {
let mut s = 1;
while block_width(text, s + 1) <= max_w * 4 / 5 && block_height(s + 1) <= max_h * 3 / 5 {
s += 1;
}
let g = block(text, s);
if fits(&g) { g } else { plain(text) }
}
FontKind::Auto => {
let g = shadow(text);
if fits(&g) {
return g;
}
let g = block(text, 1);
if fits(&g) { g } else { plain(text) }
}
}
}