use super::model::{Grid, MAX_COLS, MAX_ROWS, Terminal};
use ab_glyph::{Font, FontArc, ScaleFont, point};
use std::collections::HashMap;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
const MAX_FONT_BYTES: u64 = 16 * 1024 * 1024;
const MAX_GLYPH_BYTES: usize = 8 * 1024 * 1024;
const BACKGROUND: [u8; 3] = [17, 23, 33];
const FOREGROUND: [u8; 3] = [216, 224, 238];
const PALETTE: [[u8; 3]; 16] = [
[25, 31, 42],
[226, 92, 105],
[133, 198, 122],
[227, 193, 117],
[114, 163, 224],
[189, 143, 223],
[111, 198, 205],
[200, 208, 224],
[98, 110, 130],
[246, 119, 128],
[158, 221, 146],
[245, 213, 145],
[145, 188, 245],
[212, 169, 242],
[147, 222, 226],
[240, 245, 255],
];
struct GlyphMask {
x: i32,
y: i32,
width: u32,
height: u32,
alpha: Vec<u8>,
}
pub struct Renderer {
fonts: Vec<FontArc>,
font_size: f32,
cell_width: u32,
cell_height: u32,
ascent: f32,
glyphs: HashMap<(char, usize), GlyphMask>,
glyph_bytes: usize,
}
impl Renderer {
pub fn new(path: Option<&Path>, font_size: f32) -> io::Result<Self> {
let path = match path {
Some(path) => path.to_owned(),
None => default_font()?,
};
let regular = load_font(&path)?;
let scaled = regular.as_scaled(font_size);
let cell_width = scaled.h_advance(regular.glyph_id('M')).ceil().max(1.0) as u32;
let cell_height = (scaled.height() + scaled.line_gap())
.ceil()
.max(font_size)
.max(1.0) as u32
+ 3;
let ascent = scaled.ascent();
let mut fonts = vec![regular];
for suffix in ["-Bold", "-Oblique", "-BoldOblique"] {
let variant = path.with_file_name(format!(
"{}{suffix}.{}",
path.file_stem().unwrap_or_default().to_string_lossy(),
path.extension().unwrap_or_default().to_string_lossy()
));
fonts.push(load_font(&variant).unwrap_or_else(|_| fonts[0].clone()));
}
Ok(Self {
fonts,
font_size,
cell_width,
cell_height,
ascent,
glyphs: HashMap::new(),
glyph_bytes: 0,
})
}
pub fn grid(&self, width: u32, height: u32) -> Grid {
Grid {
cols: (width.saturating_sub(16) / self.cell_width).clamp(1, u32::from(MAX_COLS)) as u16,
rows: (height.saturating_sub(16) / self.cell_height).clamp(1, u32::from(MAX_ROWS))
as u16,
cell_width: self.cell_width,
cell_height: self.cell_height,
padding: 8,
}
}
pub fn render(&mut self, terminal: &Terminal, width: u32, height: u32) -> io::Result<Vec<u8>> {
let bytes = aen_window::frame_bytes(width, height)?;
let mut pixels = Vec::with_capacity(bytes);
for _ in 0..bytes / 4 {
pixels.extend_from_slice(&[BACKGROUND[2], BACKGROUND[1], BACKGROUND[0], 255]);
}
let grid = self.grid(width, height);
let screen = terminal.parser.screen();
for row in 0..grid.rows {
for col in 0..grid.cols {
let Some(cell) = screen.cell(row, col) else {
continue;
};
let (_, bg) = colors(cell, terminal.selected(row, col));
rectangle(
&mut pixels,
width,
height,
grid.padding + u32::from(col) * grid.cell_width,
grid.padding + u32::from(row) * grid.cell_height,
grid.cell_width,
grid.cell_height,
bg,
);
}
}
for row in 0..grid.rows {
for col in 0..grid.cols {
let Some(cell) = screen.cell(row, col) else {
continue;
};
if cell.is_wide_continuation() {
continue;
}
let (fg, _) = colors(cell, terminal.selected(row, col));
let x = grid.padding + u32::from(col) * grid.cell_width;
let y = grid.padding + u32::from(row) * grid.cell_height;
let span = if cell.is_wide() { 2 } else { 1 };
self.text(
&mut pixels,
width,
height,
x,
y,
grid.cell_width * span,
grid.cell_height,
cell.contents(),
fg,
usize::from(cell.bold()) + 2 * usize::from(cell.italic()),
);
if cell.underline() {
rectangle(
&mut pixels,
width,
height,
x,
y + grid.cell_height - 2,
grid.cell_width * span,
1,
fg,
);
}
}
}
if !screen.hide_cursor() && screen.scrollback() == 0 {
let (row, mut col) = screen.cursor_position();
if screen
.cell(row, col)
.is_some_and(vt100::Cell::is_wide_continuation)
{
col = col.saturating_sub(1);
}
if row < grid.rows && col < grid.cols {
let x = grid.padding + u32::from(col) * grid.cell_width;
let y = grid.padding + u32::from(row) * grid.cell_height;
if terminal.focused {
rectangle(
&mut pixels,
width,
height,
x,
y,
grid.cell_width,
grid.cell_height,
FOREGROUND,
);
if let Some(cell) = screen.cell(row, col) {
self.text(
&mut pixels,
width,
height,
x,
y,
grid.cell_width,
grid.cell_height,
cell.contents(),
BACKGROUND,
0,
);
}
} else {
for (dx, dy, w, h) in [
(0, 0, grid.cell_width, 1),
(0, grid.cell_height - 1, grid.cell_width, 1),
(0, 0, 1, grid.cell_height),
(grid.cell_width - 1, 0, 1, grid.cell_height),
] {
rectangle(&mut pixels, width, height, x + dx, y + dy, w, h, FOREGROUND);
}
}
}
}
Ok(pixels)
}
fn glyph(&mut self, character: char, style: usize) -> &GlyphMask {
let key = (character, style);
if !self.glyphs.contains_key(&key) {
let font = &self.fonts[style];
let mut id = font.glyph_id(character);
if id.0 == 0 {
id = font.glyph_id('\u{fffd}');
}
let glyph = id.with_scale_and_position(self.font_size, point(0.0, self.ascent));
let mut mask = GlyphMask {
x: 0,
y: 0,
width: 0,
height: 0,
alpha: Vec::new(),
};
if let Some(outline) = font.outline_glyph(glyph) {
let bounds = outline.px_bounds();
let width = bounds.width().ceil() as u32;
let height = bounds.height().ceil() as u32;
if width <= 256 && height <= 256 {
mask = GlyphMask {
x: bounds.min.x as i32,
y: bounds.min.y as i32,
width,
height,
alpha: vec![0; (width * height) as usize],
};
outline.draw(|x, y, coverage| {
if x < width && y < height {
mask.alpha[(y * width + x) as usize] = (coverage * 255.0).round() as u8;
}
});
}
}
if self.glyphs.len() >= 4096 || self.glyph_bytes + mask.alpha.len() > MAX_GLYPH_BYTES {
self.glyphs.clear();
self.glyph_bytes = 0;
}
self.glyph_bytes += mask.alpha.len();
self.glyphs.insert(key, mask);
}
&self.glyphs[&key]
}
#[allow(clippy::too_many_arguments)]
fn text(
&mut self,
pixels: &mut [u8],
width: u32,
height: u32,
x: u32,
y: u32,
cell_width: u32,
cell_height: u32,
text: &str,
color: [u8; 3],
style: usize,
) {
for character in text.chars() {
let mask = self.glyph(character, style);
for gy in 0..mask.height {
for gx in 0..mask.width {
let dx = mask.x + gx as i32;
let dy = mask.y + gy as i32;
if dx < 0 || dy < 0 || dx as u32 >= cell_width || dy as u32 >= cell_height {
continue;
}
let px = x + dx as u32;
let py = y + dy as u32;
if px >= width || py >= height {
continue;
}
let alpha = u32::from(mask.alpha[(gy * mask.width + gx) as usize]);
let offset = ((py * width + px) * 4) as usize;
for channel in 0..3 {
pixels[offset + channel] = ((u32::from(color[2 - channel]) * alpha
+ u32::from(pixels[offset + channel]) * (255 - alpha)
+ 127)
/ 255) as u8;
}
}
}
}
}
}
fn load_font(path: &Path) -> io::Result<FontArc> {
let file = std::fs::File::open(path)?;
let mut bytes = Vec::new();
file.take(MAX_FONT_BYTES + 1).read_to_end(&mut bytes)?;
if bytes.len() as u64 > MAX_FONT_BYTES {
return Err(io::Error::other("Lios font exceeds 16 MiB"));
}
FontArc::try_from_vec(bytes).map_err(|_| io::Error::other("invalid Lios font"))
}
pub fn default_font() -> io::Result<PathBuf> {
for path in [
"/usr/share/fonts/TTF/DejaVuSansMono.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/dejavu/DejaVuSansMono.ttf",
] {
if Path::new(path).is_file() {
return Ok(path.into());
}
}
Err(io::Error::other(
"Lios needs a monospace font: install ttf-dejavu or pass --font-file",
))
}
fn colors(cell: &vt100::Cell, selected: bool) -> ([u8; 3], [u8; 3]) {
let mut fg = color(cell.fgcolor(), FOREGROUND);
let mut bg = color(cell.bgcolor(), BACKGROUND);
if cell.bold()
&& let vt100::Color::Idx(i @ 0..=7) = cell.fgcolor()
{
fg = PALETTE[usize::from(i + 8)];
}
if cell.dim() {
fg = fg.map(|c| (u16::from(c) * 2 / 3) as u8);
}
if cell.inverse() {
std::mem::swap(&mut fg, &mut bg);
}
if selected {
bg = [56, 77, 110];
}
(fg, bg)
}
fn color(value: vt100::Color, default: [u8; 3]) -> [u8; 3] {
match value {
vt100::Color::Default => default,
vt100::Color::Rgb(r, g, b) => [r, g, b],
vt100::Color::Idx(i @ 0..=15) => PALETTE[usize::from(i)],
vt100::Color::Idx(i @ 16..=231) => {
let n = i - 16;
[n / 36, (n / 6) % 6, n % 6].map(|c| if c == 0 { 0 } else { 55 + 40 * c })
}
vt100::Color::Idx(i) => [8 + (i - 232) * 10; 3],
}
}
#[allow(clippy::too_many_arguments)]
fn rectangle(
pixels: &mut [u8],
width: u32,
height: u32,
x: u32,
y: u32,
w: u32,
h: u32,
color: [u8; 3],
) {
for py in y..y.saturating_add(h).min(height) {
for px in x..x.saturating_add(w).min(width) {
let offset = ((py * width + px) * 4) as usize;
pixels[offset..offset + 4].copy_from_slice(&[color[2], color[1], color[0], 255]);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn color_cube_gray_and_clipping() {
assert_eq!(color(vt100::Color::Idx(196), BACKGROUND), [255, 0, 0]);
assert_eq!(color(vt100::Color::Idx(255), BACKGROUND), [238; 3]);
let mut pixels = vec![0; 16];
rectangle(&mut pixels, 2, 2, 1, 1, u32::MAX, u32::MAX, [1, 2, 3]);
assert_eq!(&pixels[12..], &[3, 2, 1, 255]);
assert_eq!(&pixels[..12], &[0; 12]);
}
}