//! 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, ] }