brewery
notashelf /
5690e0e883a9bfb1879e8fd1bdd3df1cf796a997

beer

public

A terminal worth pouring time into

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/beer.git
src/render.rsRust238 lines7.5 KB
1
//! Software renderer: compose the grid into an ARGB8888 buffer.
2
//!
3
//! The target is a `wl_shm` buffer in `Argb8888`, which on little-endian is
4
//! `[B, G, R, A]` per pixel. Rendering is two passes per frame - backgrounds
5
//! then glyphs - so a wide glyph that overflows its cell is not clipped by the
6
//! neighbouring cell's background fill.
7
8
use crate::font::{CellMetrics, Fonts, GlyphData, Style};
9
use crate::grid::{Color, Flags, Grid};
10
11
/// Foreground/background used for `Color::Default`.
12
const DEFAULT_FG: Rgb = Rgb(0xc5, 0xc8, 0xc6);
13
const DEFAULT_BG: Rgb = Rgb(0x18, 0x18, 0x18);
14
15
#[derive(Clone, Copy)]
16
struct Rgb(u8, u8, u8);
17
18
/// A mutable view over a BGRA pixel buffer.
19
struct Canvas<'a> {
20
    pixels: &'a mut [u8],
21
    width: usize,
22
    height: usize,
23
}
24
25
impl Canvas<'_> {
26
    fn index(&self, x: i32, y: i32) -> Option<usize> {
27
        if x < 0 || y < 0 || x as usize >= self.width || y as usize >= self.height {
28
            return None;
29
        }
30
        Some((y as usize * self.width + x as usize) * 4)
31
    }
32
33
    fn fill_rect(&mut self, x0: i32, y0: i32, w: u32, h: u32, c: Rgb) {
34
        for dy in 0..h as i32 {
35
            for dx in 0..w as i32 {
36
                if let Some(i) = self.index(x0 + dx, y0 + dy) {
37
                    self.pixels[i] = c.2;
38
                    self.pixels[i + 1] = c.1;
39
                    self.pixels[i + 2] = c.0;
40
                    self.pixels[i + 3] = 0xff;
41
                }
42
            }
43
        }
44
    }
45
46
    /// Alpha-blend `fg` over the existing pixel with coverage `a`.
47
    fn blend(&mut self, x: i32, y: i32, fg: Rgb, a: u8) {
48
        let Some(i) = self.index(x, y) else { return };
49
        let (a, inv) = (u32::from(a), u32::from(255 - a));
50
        let mix = |src: u8, dst: u8| ((u32::from(src) * a + u32::from(dst) * inv) / 255) as u8;
51
        self.pixels[i] = mix(fg.2, self.pixels[i]);
52
        self.pixels[i + 1] = mix(fg.1, self.pixels[i + 1]);
53
        self.pixels[i + 2] = mix(fg.0, self.pixels[i + 2]);
54
        self.pixels[i + 3] = 0xff;
55
    }
56
57
    /// Composite one pre-multiplied BGRA source pixel over the destination.
58
    fn over(&mut self, x: i32, y: i32, src: &[u8]) {
59
        let Some(i) = self.index(x, y) else { return };
60
        let inv = u32::from(255 - src[3]);
61
        let comp = |s: u8, dst: u8| (u32::from(s) + u32::from(dst) * inv / 255).min(255) as u8;
62
        self.pixels[i] = comp(src[0], self.pixels[i]);
63
        self.pixels[i + 1] = comp(src[1], self.pixels[i + 1]);
64
        self.pixels[i + 2] = comp(src[2], self.pixels[i + 2]);
65
        self.pixels[i + 3] = 0xff;
66
    }
67
}
68
69
#[derive(Debug)]
70
pub struct Renderer {
71
    fonts: Fonts,
72
}
73
74
impl Renderer {
75
    pub fn new(fonts: Fonts) -> Self {
76
        Self { fonts }
77
    }
78
79
    pub fn metrics(&self) -> CellMetrics {
80
        self.fonts.metrics()
81
    }
82
83
    /// Compose `grid` into `pixels` (BGRA, `width`×`height` px). The cursor cell
84
    /// is drawn reversed.
85
    pub fn render(&mut self, grid: &Grid, pixels: &mut [u8], width: usize, height: usize) {
86
        let mut canvas = Canvas {
87
            pixels,
88
            width,
89
            height,
90
        };
91
        canvas.fill_rect(0, 0, width as u32, height as u32, DEFAULT_BG);
92
93
        let m = self.fonts.metrics();
94
        let cursor = grid.cursor();
95
96
        for y in 0..grid.rows() {
97
            for x in 0..grid.cols() {
98
                let (_, bg) = cell_colors(grid.cell(x, y), (x, y) == cursor);
99
                let (px, py) = (x as i32 * m.width as i32, y as i32 * m.height as i32);
100
                canvas.fill_rect(px, py, m.width, m.height, bg);
101
            }
102
        }
103
104
        for y in 0..grid.rows() {
105
            for x in 0..grid.cols() {
106
                let cell = grid.cell(x, y);
107
                if cell.flags.contains(Flags::WIDE_CONT) || cell.c == ' ' {
108
                    continue;
109
                }
110
                let (fg, _) = cell_colors(cell, (x, y) == cursor);
111
                let style = Style {
112
                    bold: cell.flags.contains(Flags::BOLD),
113
                    italic: cell.flags.contains(Flags::ITALIC),
114
                };
115
                let origin_x = x as i32 * m.width as i32;
116
                let baseline = y as i32 * m.height as i32 + m.ascent as i32;
117
                self.draw_glyph(&mut canvas, cell.c, style, origin_x, baseline, fg);
118
            }
119
        }
120
    }
121
122
    fn draw_glyph(
123
        &mut self,
124
        canvas: &mut Canvas,
125
        c: char,
126
        style: Style,
127
        origin_x: i32,
128
        baseline: i32,
129
        fg: Rgb,
130
    ) {
131
        let glyph = match self.fonts.glyph(c, style) {
132
            Ok(glyph) => glyph,
133
            Err(err) => {
134
                tracing::debug!("glyph {c:?}: {err}");
135
                return;
136
            }
137
        };
138
        let (left, top, w, h) = (
139
            glyph.left,
140
            glyph.top,
141
            glyph.width as i32,
142
            glyph.height as i32,
143
        );
144
        match &glyph.data {
145
            GlyphData::Mask(mask) => {
146
                for gy in 0..h {
147
                    for gx in 0..w {
148
                        let a = mask[(gy * w + gx) as usize];
149
                        if a != 0 {
150
                            canvas.blend(origin_x + left + gx, baseline - top + gy, fg, a);
151
                        }
152
                    }
153
                }
154
            }
155
            GlyphData::Color(bgra) => {
156
                for gy in 0..h {
157
                    for gx in 0..w {
158
                        let i = ((gy * w + gx) * 4) as usize;
159
                        canvas.over(origin_x + left + gx, baseline - top + gy, &bgra[i..i + 4]);
160
                    }
161
                }
162
            }
163
        }
164
    }
165
}
166
167
/// Resolve a cell's (foreground, background) RGB, applying reverse video,
168
/// bold-as-bright for the foreground, and hidden.
169
fn cell_colors(cell: &crate::grid::Cell, cursor: bool) -> (Rgb, Rgb) {
170
    let bold = cell.flags.contains(Flags::BOLD);
171
    let mut fg = resolve(cell.fg, DEFAULT_FG, bold);
172
    let mut bg = resolve(cell.bg, DEFAULT_BG, false);
173
    if cell.flags.contains(Flags::REVERSE) ^ cursor {
174
        std::mem::swap(&mut fg, &mut bg);
175
    }
176
    if cell.flags.contains(Flags::HIDDEN) {
177
        fg = bg;
178
    }
179
    (fg, bg)
180
}
181
182
fn resolve(color: Color, default: Rgb, bold: bool) -> Rgb {
183
    match color {
184
        Color::Default => default,
185
        Color::Indexed(i) => ansi256(if bold && i < 8 { i + 8 } else { i }),
186
        Color::Rgb(r, g, b) => Rgb(r, g, b),
187
    }
188
}
189
190
/// The xterm 256-colour palette: 16 base, a 6×6×6 cube, then 24 greys.
191
fn ansi256(i: u8) -> Rgb {
192
    const BASE: [Rgb; 16] = [
193
        Rgb(0x00, 0x00, 0x00),
194
        Rgb(0xcd, 0x00, 0x00),
195
        Rgb(0x00, 0xcd, 0x00),
196
        Rgb(0xcd, 0xcd, 0x00),
197
        Rgb(0x00, 0x00, 0xee),
198
        Rgb(0xcd, 0x00, 0xcd),
199
        Rgb(0x00, 0xcd, 0xcd),
200
        Rgb(0xe5, 0xe5, 0xe5),
201
        Rgb(0x7f, 0x7f, 0x7f),
202
        Rgb(0xff, 0x00, 0x00),
203
        Rgb(0x00, 0xff, 0x00),
204
        Rgb(0xff, 0xff, 0x00),
205
        Rgb(0x5c, 0x5c, 0xff),
206
        Rgb(0xff, 0x00, 0xff),
207
        Rgb(0x00, 0xff, 0xff),
208
        Rgb(0xff, 0xff, 0xff),
209
    ];
210
    match i {
211
        0..=15 => BASE[i as usize],
212
        16..=231 => {
213
            let i = i - 16;
214
            Rgb(cube(i / 36), cube((i / 6) % 6), cube(i % 6))
215
        }
216
        _ => {
217
            let v = 8 + 10 * (i - 232);
218
            Rgb(v, v, v)
219
        }
220
    }
221
}
222
223
fn cube(step: u8) -> u8 {
224
    if step == 0 { 0 } else { 55 + 40 * step }
225
}
226
227
#[cfg(test)]
228
mod tests {
229
    use super::*;
230
231
    #[test]
232
    fn palette_cube_and_grey() {
233
        assert_eq!(ansi256(16).0, 0); // cube origin is black
234
        let white = ansi256(231);
235
        assert_eq!((white.0, white.1, white.2), (255, 255, 255));
236
        assert_eq!(ansi256(232).0, 8); // first grey step
237
    }
238
}