268 lines
8.4 KiB
Rust
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,
|
|
]
|
|
}
|