beer/src/render.rs
NotAShelf 5690e0e883
render: draw the grid with rasterized glyphs
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6350824abb506c2af98884a7374228116a6a6964
2026-06-24 15:36:27 +03:00

238 lines
7.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Software renderer: compose the grid into an ARGB8888 buffer.
//!
//! The target is a `wl_shm` buffer in `Argb8888`, which on little-endian is
//! `[B, G, R, A]` per pixel. Rendering is two passes per frame - backgrounds
//! then glyphs - so a wide glyph that overflows its cell is not clipped by the
//! neighbouring cell's background fill.
use crate::font::{CellMetrics, Fonts, GlyphData, Style};
use crate::grid::{Color, Flags, Grid};
/// Foreground/background used for `Color::Default`.
const DEFAULT_FG: Rgb = Rgb(0xc5, 0xc8, 0xc6);
const DEFAULT_BG: Rgb = Rgb(0x18, 0x18, 0x18);
#[derive(Clone, Copy)]
struct Rgb(u8, u8, u8);
/// A mutable view over a BGRA pixel buffer.
struct Canvas<'a> {
pixels: &'a mut [u8],
width: usize,
height: usize,
}
impl Canvas<'_> {
fn index(&self, x: i32, y: i32) -> Option<usize> {
if x < 0 || y < 0 || x as usize >= self.width || y as usize >= self.height {
return None;
}
Some((y as usize * self.width + x as usize) * 4)
}
fn fill_rect(&mut self, x0: i32, y0: i32, w: u32, h: u32, c: Rgb) {
for dy in 0..h as i32 {
for dx in 0..w as i32 {
if let Some(i) = self.index(x0 + dx, y0 + dy) {
self.pixels[i] = c.2;
self.pixels[i + 1] = c.1;
self.pixels[i + 2] = c.0;
self.pixels[i + 3] = 0xff;
}
}
}
}
/// Alpha-blend `fg` over the existing pixel with coverage `a`.
fn blend(&mut self, x: i32, y: i32, fg: Rgb, a: u8) {
let Some(i) = self.index(x, y) else { return };
let (a, inv) = (u32::from(a), u32::from(255 - a));
let mix = |src: u8, dst: u8| ((u32::from(src) * a + u32::from(dst) * inv) / 255) as u8;
self.pixels[i] = mix(fg.2, self.pixels[i]);
self.pixels[i + 1] = mix(fg.1, self.pixels[i + 1]);
self.pixels[i + 2] = mix(fg.0, self.pixels[i + 2]);
self.pixels[i + 3] = 0xff;
}
/// Composite one pre-multiplied BGRA source pixel over the destination.
fn over(&mut self, x: i32, y: i32, src: &[u8]) {
let Some(i) = self.index(x, y) else { return };
let inv = u32::from(255 - src[3]);
let comp = |s: u8, dst: u8| (u32::from(s) + u32::from(dst) * inv / 255).min(255) as u8;
self.pixels[i] = comp(src[0], self.pixels[i]);
self.pixels[i + 1] = comp(src[1], self.pixels[i + 1]);
self.pixels[i + 2] = comp(src[2], self.pixels[i + 2]);
self.pixels[i + 3] = 0xff;
}
}
#[derive(Debug)]
pub struct Renderer {
fonts: Fonts,
}
impl Renderer {
pub fn new(fonts: Fonts) -> Self {
Self { fonts }
}
pub fn metrics(&self) -> CellMetrics {
self.fonts.metrics()
}
/// Compose `grid` into `pixels` (BGRA, `width`×`height` px). The cursor cell
/// is drawn reversed.
pub fn render(&mut self, grid: &Grid, pixels: &mut [u8], width: usize, height: usize) {
let mut canvas = Canvas {
pixels,
width,
height,
};
canvas.fill_rect(0, 0, width as u32, height as u32, DEFAULT_BG);
let m = self.fonts.metrics();
let cursor = grid.cursor();
for y in 0..grid.rows() {
for x in 0..grid.cols() {
let (_, bg) = cell_colors(grid.cell(x, y), (x, y) == cursor);
let (px, py) = (x as i32 * m.width as i32, y as i32 * m.height as i32);
canvas.fill_rect(px, py, m.width, m.height, bg);
}
}
for y in 0..grid.rows() {
for x in 0..grid.cols() {
let cell = grid.cell(x, y);
if cell.flags.contains(Flags::WIDE_CONT) || cell.c == ' ' {
continue;
}
let (fg, _) = cell_colors(cell, (x, y) == cursor);
let style = Style {
bold: cell.flags.contains(Flags::BOLD),
italic: cell.flags.contains(Flags::ITALIC),
};
let origin_x = x as i32 * m.width as i32;
let baseline = y as i32 * m.height as i32 + m.ascent as i32;
self.draw_glyph(&mut canvas, cell.c, style, origin_x, baseline, fg);
}
}
}
fn draw_glyph(
&mut self,
canvas: &mut Canvas,
c: char,
style: Style,
origin_x: i32,
baseline: i32,
fg: Rgb,
) {
let glyph = match self.fonts.glyph(c, style) {
Ok(glyph) => glyph,
Err(err) => {
tracing::debug!("glyph {c:?}: {err}");
return;
}
};
let (left, top, w, h) = (
glyph.left,
glyph.top,
glyph.width as i32,
glyph.height as i32,
);
match &glyph.data {
GlyphData::Mask(mask) => {
for gy in 0..h {
for gx in 0..w {
let a = mask[(gy * w + gx) as usize];
if a != 0 {
canvas.blend(origin_x + left + gx, baseline - top + gy, fg, a);
}
}
}
}
GlyphData::Color(bgra) => {
for gy in 0..h {
for gx in 0..w {
let i = ((gy * w + gx) * 4) as usize;
canvas.over(origin_x + left + gx, baseline - top + gy, &bgra[i..i + 4]);
}
}
}
}
}
}
/// Resolve a cell's (foreground, background) RGB, applying reverse video,
/// bold-as-bright for the foreground, and hidden.
fn cell_colors(cell: &crate::grid::Cell, cursor: bool) -> (Rgb, Rgb) {
let bold = cell.flags.contains(Flags::BOLD);
let mut fg = resolve(cell.fg, DEFAULT_FG, bold);
let mut bg = resolve(cell.bg, DEFAULT_BG, false);
if cell.flags.contains(Flags::REVERSE) ^ cursor {
std::mem::swap(&mut fg, &mut bg);
}
if cell.flags.contains(Flags::HIDDEN) {
fg = bg;
}
(fg, bg)
}
fn resolve(color: Color, default: Rgb, bold: bool) -> Rgb {
match color {
Color::Default => default,
Color::Indexed(i) => ansi256(if bold && i < 8 { i + 8 } else { i }),
Color::Rgb(r, g, b) => Rgb(r, g, b),
}
}
/// The xterm 256-colour palette: 16 base, a 6×6×6 cube, then 24 greys.
fn ansi256(i: u8) -> Rgb {
const BASE: [Rgb; 16] = [
Rgb(0x00, 0x00, 0x00),
Rgb(0xcd, 0x00, 0x00),
Rgb(0x00, 0xcd, 0x00),
Rgb(0xcd, 0xcd, 0x00),
Rgb(0x00, 0x00, 0xee),
Rgb(0xcd, 0x00, 0xcd),
Rgb(0x00, 0xcd, 0xcd),
Rgb(0xe5, 0xe5, 0xe5),
Rgb(0x7f, 0x7f, 0x7f),
Rgb(0xff, 0x00, 0x00),
Rgb(0x00, 0xff, 0x00),
Rgb(0xff, 0xff, 0x00),
Rgb(0x5c, 0x5c, 0xff),
Rgb(0xff, 0x00, 0xff),
Rgb(0x00, 0xff, 0xff),
Rgb(0xff, 0xff, 0xff),
];
match i {
0..=15 => BASE[i as usize],
16..=231 => {
let i = i - 16;
Rgb(cube(i / 36), cube((i / 6) % 6), cube(i % 6))
}
_ => {
let v = 8 + 10 * (i - 232);
Rgb(v, v, v)
}
}
}
fn cube(step: u8) -> u8 {
if step == 0 { 0 } else { 55 + 40 * step }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn palette_cube_and_grey() {
assert_eq!(ansi256(16).0, 0); // cube origin is black
let white = ansi256(231);
assert_eq!((white.0, white.1, white.2), (255, 255, 255));
assert_eq!(ansi256(232).0, 8); // first grey step
}
}