Files
Cagire/src/block_renderer.rs

268 lines
8.4 KiB
Rust

//! Programmatic rendering of Unicode block elements for the desktop backend.
//!
//! Real terminals render block characters (█, ▀, ▄, quadrants, sextants) as
//! pixel-perfect filled rectangles. The bitmap font backend can't guarantee
//! gap-free fills and lacks sextant glyphs entirely. This wrapper intercepts
//! block element code points and draws them directly on the pixmap, delegating
//! everything else to EmbeddedGraphics.
use ratatui::buffer::Cell;
use ratatui::style::{Color, Modifier};
use rustc_hash::FxHashSet;
use soft_ratatui::{EmbeddedGraphics, RasterBackend, RgbPixmap};
pub struct BlockCharBackend {
pub inner: EmbeddedGraphics,
}
impl RasterBackend for BlockCharBackend {
fn draw_cell(
&mut self,
x: u16,
y: u16,
cell: &Cell,
always_redraw_list: &mut FxHashSet<(u16, u16)>,
blinking_fast: bool,
blinking_slow: bool,
char_width: usize,
char_height: usize,
rgb_pixmap: &mut RgbPixmap,
) {
let cp = cell.symbol().chars().next().unwrap_or(' ') as u32;
if !is_block_element(cp) {
self.inner.draw_cell(
x,
y,
cell,
always_redraw_list,
blinking_fast,
blinking_slow,
char_width,
char_height,
rgb_pixmap,
);
return;
}
let (fg, bg) = resolve_colors(cell, always_redraw_list, x, y, blinking_fast, blinking_slow);
let px = x as usize * char_width;
let py = y as usize * char_height;
fill_rect(rgb_pixmap, px, py, char_width, char_height, bg);
draw_block_element(rgb_pixmap, cp, px, py, char_width, char_height, fg);
}
}
// ---------------------------------------------------------------------------
// Block element classification and drawing
// ---------------------------------------------------------------------------
fn is_block_element(cp: u32) -> bool {
matches!(cp, 0x2580..=0x2590 | 0x2594..=0x259F | 0x1FB00..=0x1FB3B)
}
fn draw_block_element(
pixmap: &mut RgbPixmap,
cp: u32,
px: usize,
py: usize,
cw: usize,
ch: usize,
fg: [u8; 3],
) {
match cp {
0x2580 => fill_rect(pixmap, px, py, cw, ch / 2, fg),
0x2581..=0x2587 => {
let n = (cp - 0x2580) as usize;
let h = ch * n / 8;
fill_rect(pixmap, px, py + ch - h, cw, h, fg);
}
0x2588 => fill_rect(pixmap, px, py, cw, ch, fg),
0x2589..=0x258F => {
let n = (0x2590 - cp) as usize;
fill_rect(pixmap, px, py, cw * n / 8, ch, fg);
}
0x2590 => {
let hw = cw / 2;
fill_rect(pixmap, px + hw, py, cw - hw, ch, fg);
}
0x2594 => fill_rect(pixmap, px, py, cw, (ch / 8).max(1), fg),
0x2595 => {
let w = (cw / 8).max(1);
fill_rect(pixmap, px + cw - w, py, w, ch, fg);
}
0x2596..=0x259F => draw_quadrants(pixmap, px, py, cw, ch, fg, cp),
0x1FB00..=0x1FB3B => draw_sextants(pixmap, px, py, cw, ch, fg, cp),
_ => unreachable!(),
}
}
// ---------------------------------------------------------------------------
// Quadrants (U+2596-U+259F): 2x2 grid
// ---------------------------------------------------------------------------
// Bits: 3=UL, 2=UR, 1=LL, 0=LR
const QUADRANT: [u8; 10] = [
0b0010, // ▖ LL
0b0001, // ▗ LR
0b1000, // ▘ UL
0b1011, // ▙ UL+LL+LR
0b1001, // ▚ UL+LR
0b1110, // ▛ UL+UR+LL
0b1101, // ▜ UL+UR+LR
0b0100, // ▝ UR
0b0110, // ▞ UR+LL
0b0111, // ▟ UR+LL+LR
];
fn draw_quadrants(
pixmap: &mut RgbPixmap,
px: usize,
py: usize,
cw: usize,
ch: usize,
fg: [u8; 3],
cp: u32,
) {
let pattern = QUADRANT[(cp - 0x2596) as usize];
let hw = cw / 2;
let hh = ch / 2;
let rw = cw - hw;
let rh = ch - hh;
if pattern & 0b1000 != 0 { fill_rect(pixmap, px, py, hw, hh, fg); }
if pattern & 0b0100 != 0 { fill_rect(pixmap, px + hw, py, rw, hh, fg); }
if pattern & 0b0010 != 0 { fill_rect(pixmap, px, py + hh, hw, rh, fg); }
if pattern & 0b0001 != 0 { fill_rect(pixmap, px + hw, py + hh, rw, rh, fg); }
}
// ---------------------------------------------------------------------------
// Sextants (U+1FB00-U+1FB3B): 2x3 grid
// ---------------------------------------------------------------------------
// Bit layout: 0=TL, 1=TR, 2=ML, 3=MR, 4=BL, 5=BR
// The 60 characters encode patterns 1-62, skipping 0 (space), 21 (left half),
// 42 (right half), and 63 (full block) which exist as standard block elements.
fn sextant_pattern(cp: u32) -> u8 {
let mut p = (cp - 0x1FB00) as u8 + 1;
if p >= 21 { p += 1; }
if p >= 42 { p += 1; }
p
}
fn draw_sextants(
pixmap: &mut RgbPixmap,
px: usize,
py: usize,
cw: usize,
ch: usize,
fg: [u8; 3],
cp: u32,
) {
let pattern = sextant_pattern(cp);
let hw = cw / 2;
let rw = cw - hw;
let h0 = ch / 3;
let h1 = (ch - h0) / 2;
let h2 = ch - h0 - h1;
let y1 = py + h0;
let y2 = y1 + h1;
if pattern & 0b000001 != 0 { fill_rect(pixmap, px, py, hw, h0, fg); }
if pattern & 0b000010 != 0 { fill_rect(pixmap, px + hw, py, rw, h0, fg); }
if pattern & 0b000100 != 0 { fill_rect(pixmap, px, y1, hw, h1, fg); }
if pattern & 0b001000 != 0 { fill_rect(pixmap, px + hw, y1, rw, h1, fg); }
if pattern & 0b010000 != 0 { fill_rect(pixmap, px, y2, hw, h2, fg); }
if pattern & 0b100000 != 0 { fill_rect(pixmap, px + hw, y2, rw, h2, fg); }
}
// ---------------------------------------------------------------------------
// Pixel operations
// ---------------------------------------------------------------------------
fn fill_rect(pixmap: &mut RgbPixmap, x0: usize, y0: usize, w: usize, h: usize, color: [u8; 3]) {
let pw = pixmap.width;
let x_end = (x0 + w).min(pw);
let y_end = (y0 + h).min(pixmap.height);
let data = &mut pixmap.data;
for y in y0..y_end {
let start = 3 * (y * pw + x0);
let end = 3 * (y * pw + x_end);
for chunk in data[start..end].chunks_exact_mut(3) {
chunk.copy_from_slice(&color);
}
}
}
// ---------------------------------------------------------------------------
// Color resolution (mirrors soft_ratatui::colors which is private)
// ---------------------------------------------------------------------------
fn resolve_colors(
cell: &Cell,
always_redraw_list: &mut FxHashSet<(u16, u16)>,
x: u16,
y: u16,
blinking_fast: bool,
blinking_slow: bool,
) -> ([u8; 3], [u8; 3]) {
let mut fg = color_to_rgb(&cell.fg, true);
let mut bg = color_to_rgb(&cell.bg, false);
for modifier in cell.modifier.iter() {
match modifier {
Modifier::DIM => {
fg = dim_rgb(fg);
bg = dim_rgb(bg);
}
Modifier::REVERSED => std::mem::swap(&mut fg, &mut bg),
Modifier::HIDDEN => fg = bg,
Modifier::SLOW_BLINK => {
always_redraw_list.insert((x, y));
if blinking_slow { fg = bg; }
}
Modifier::RAPID_BLINK => {
always_redraw_list.insert((x, y));
if blinking_fast { fg = bg; }
}
_ => {}
}
}
(fg, bg)
}
fn color_to_rgb(color: &Color, is_fg: bool) -> [u8; 3] {
match color {
Color::Reset if is_fg => [204, 204, 255],
Color::Reset => [5, 1, 121],
Color::Black => [0, 0, 0],
Color::Red => [139, 0, 0],
Color::Green => [0, 100, 0],
Color::Yellow => [255, 215, 0],
Color::Blue => [0, 0, 139],
Color::Magenta => [255, 0, 255],
Color::Cyan => [0, 0, 255],
Color::Gray => [128, 128, 128],
Color::DarkGray => [64, 64, 64],
Color::LightRed => [255, 0, 0],
Color::LightGreen => [0, 255, 0],
Color::LightBlue => [173, 216, 230],
Color::LightYellow => [255, 255, 224],
Color::LightMagenta => [139, 0, 139],
Color::LightCyan => [224, 255, 255],
Color::White => [255, 255, 255],
Color::Indexed(i) => [i.wrapping_mul(*i), i.wrapping_add(*i), *i],
Color::Rgb(r, g, b) => [*r, *g, *b],
}
}
fn dim_rgb(c: [u8; 3]) -> [u8; 3] {
const F: u32 = 77; // ~30% brightness
[
((c[0] as u32 * F + 127) / 255) as u8,
((c[1] as u32 * F + 127) / 255) as u8,
((c[2] as u32 * F + 127) / 255) as u8,
]
}