use std::fmt;
use crate::console::{Console, ConsoleOptions, Renderable};
use crate::measure::Measurement;
use crate::segment::Segment;
use crate::style::Style;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Blitter {
#[default]
Braille,
Sextant,
HalfBlock,
Octant,
}
const PIXEL_MAP: [[u8; 2]; 4] = [
[0x01, 0x08], [0x02, 0x10], [0x04, 0x20], [0x40, 0x80], ];
const BRAILLE_BASE: u32 = 0x2800;
const SEXTANT_PIXEL_MAP: [[u8; 2]; 3] = [
[0x01, 0x02], [0x04, 0x08], [0x10, 0x20], ];
const SEXTANT_BASE: u32 = 0x1FB00;
const FULL_BLOCK: char = '\u{2588}';
#[inline]
fn sextant_bits_to_char(bits: u8) -> char {
match bits {
0 => ' ',
63 => FULL_BLOCK,
n => char::from_u32(SEXTANT_BASE + (n as u32) - 1).unwrap_or(' '),
}
}
#[inline]
fn halfblock_bits_to_char(bits: u8) -> char {
match bits & 0x03 {
0b00 => ' ',
0b01 => '\u{2580}', 0b10 => '\u{2584}', _ => '\u{2588}', }
}
const OCTANT_PIXEL_MAP: [[u8; 2]; 4] = [
[0x01, 0x02], [0x04, 0x08], [0x10, 0x20], [0x40, 0x80], ];
#[rustfmt::skip]
const OCTANT_CHARS: [char; 256] = [
'\u{0020}', '\u{1CEA8}', '\u{1CEAB}', '\u{1FB82}', '\u{1CD00}', '\u{2598}', '\u{1CD01}', '\u{1CD02}', '\u{1CD03}', '\u{1CD04}', '\u{259D}', '\u{1CD05}', '\u{1CD06}', '\u{1CD07}', '\u{1CD08}', '\u{2580}', '\u{1CD09}', '\u{1CD0A}', '\u{1CD0B}', '\u{1CD0C}', '\u{1FBE6}', '\u{1CD0D}', '\u{1CD0E}', '\u{1CD0F}', '\u{1CD10}', '\u{1CD11}', '\u{1CD12}', '\u{1CD13}', '\u{1CD14}', '\u{1CD15}', '\u{1CD16}', '\u{1CD17}', '\u{1CD18}', '\u{1CD19}', '\u{1CD1A}', '\u{1CD1B}', '\u{1CD1C}', '\u{1CD1D}', '\u{1CD1E}', '\u{1CD1F}', '\u{1FBE7}', '\u{1CD20}', '\u{1CD21}', '\u{1CD22}', '\u{1CD23}', '\u{1CD24}', '\u{1CD25}', '\u{1CD26}', '\u{1CD27}', '\u{1CD28}', '\u{1CD29}', '\u{1CD2A}', '\u{1CD2B}', '\u{1CD2C}', '\u{1CD2D}', '\u{1CD2E}', '\u{1CD2F}', '\u{1CD30}', '\u{1CD31}', '\u{1CD32}', '\u{1CD33}', '\u{1CD34}', '\u{1CD35}', '\u{1FB85}', '\u{1CEA3}', '\u{1CD36}', '\u{1CD37}', '\u{1CD38}', '\u{1CD39}', '\u{1CD3A}', '\u{1CD3B}', '\u{1CD3C}', '\u{1CD3D}', '\u{1CD3E}', '\u{1CD3F}', '\u{1CD40}', '\u{1CD41}', '\u{1CD42}', '\u{1CD43}', '\u{1CD44}', '\u{2596}', '\u{1CD45}', '\u{1CD46}', '\u{1CD47}', '\u{1CD48}', '\u{258C}', '\u{1CD49}', '\u{1CD4A}', '\u{1CD4B}', '\u{1CD4C}', '\u{259E}', '\u{1CD4D}', '\u{1CD4E}', '\u{1CD4F}', '\u{1CD50}', '\u{259B}', '\u{1CD51}', '\u{1CD52}', '\u{1CD53}', '\u{1CD54}', '\u{1CD55}', '\u{1CD56}', '\u{1CD57}', '\u{1CD58}', '\u{1CD59}', '\u{1CD5A}', '\u{1CD5B}', '\u{1CD5C}', '\u{1CD5D}', '\u{1CD5E}', '\u{1CD5F}', '\u{1CD60}', '\u{1CD61}', '\u{1CD62}', '\u{1CD63}', '\u{1CD64}', '\u{1CD65}', '\u{1CD66}', '\u{1CD67}', '\u{1CD68}', '\u{1CD69}', '\u{1CD6A}', '\u{1CD6B}', '\u{1CD6C}', '\u{1CD6D}', '\u{1CD6E}', '\u{1CD6F}', '\u{1CD70}', '\u{1CEA0}', '\u{1CD71}', '\u{1CD72}', '\u{1CD73}', '\u{1CD74}', '\u{1CD75}', '\u{1CD76}', '\u{1CD77}', '\u{1CD78}', '\u{1CD79}', '\u{1CD7A}', '\u{1CD7B}', '\u{1CD7C}', '\u{1CD7D}', '\u{1CD7E}', '\u{1CD7F}', '\u{1CD80}', '\u{1CD81}', '\u{1CD82}', '\u{1CD83}', '\u{1CD84}', '\u{1CD85}', '\u{1CD86}', '\u{1CD87}', '\u{1CD88}', '\u{1CD89}', '\u{1CD8A}', '\u{1CD8B}', '\u{1CD8C}', '\u{1CD8D}', '\u{1CD8E}', '\u{1CD8F}', '\u{2597}', '\u{1CD90}', '\u{1CD91}', '\u{1CD92}', '\u{1CD93}', '\u{259A}', '\u{1CD94}', '\u{1CD95}', '\u{1CD96}', '\u{1CD97}', '\u{2590}', '\u{1CD98}', '\u{1CD99}', '\u{1CD9A}', '\u{1CD9B}', '\u{259C}', '\u{1CD9C}', '\u{1CD9D}', '\u{1CD9E}', '\u{1CD9F}', '\u{1CDA0}', '\u{1CDA1}', '\u{1CDA2}', '\u{1CDA3}', '\u{1CDA4}', '\u{1CDA5}', '\u{1CDA6}', '\u{1CDA7}', '\u{1CDA8}', '\u{1CDA9}', '\u{1CDAA}', '\u{1CDAB}', '\u{2582}', '\u{1CDAC}', '\u{1CDAD}', '\u{1CDAE}', '\u{1CDAF}', '\u{1CDB0}', '\u{1CDB1}', '\u{1CDB2}', '\u{1CDB3}', '\u{1CDB4}', '\u{1CDB5}', '\u{1CDB6}', '\u{1CDB7}', '\u{1CDB8}', '\u{1CDB9}', '\u{1CDBA}', '\u{1CDBB}', '\u{1CDBC}', '\u{1CDBD}', '\u{1CDBE}', '\u{1CDBF}', '\u{1CDC0}', '\u{1CDC1}', '\u{1CDC2}', '\u{1CDC3}', '\u{1CDC4}', '\u{1CDC5}', '\u{1CDC6}', '\u{1CDC7}', '\u{1CDC8}', '\u{1CDC9}', '\u{1CDCA}', '\u{1CDCB}', '\u{1CDCC}', '\u{1CDCD}', '\u{1CDCE}', '\u{1CDCF}', '\u{1CDD0}', '\u{1CDD1}', '\u{1CDD2}', '\u{1CDD3}', '\u{1CDD4}', '\u{1CDD5}', '\u{1CDD6}', '\u{1CDD7}', '\u{1CDD8}', '\u{1CDD9}', '\u{1CDDA}', '\u{2584}', '\u{1CDDB}', '\u{1CDDC}', '\u{1CDDD}', '\u{1CDDE}', '\u{2599}', '\u{1CDDF}', '\u{1CDE0}', '\u{1CDE1}', '\u{1CDE2}', '\u{259F}', '\u{1CDE3}', '\u{2586}', '\u{1CDE4}', '\u{1CDE5}', '\u{2588}', ];
#[inline]
fn octant_bits_to_char(bits: u8) -> char {
OCTANT_CHARS[bits as usize]
}
#[derive(Debug, Clone)]
pub struct Canvas {
width: usize,
height: usize,
pixels: Vec<Vec<u8>>,
style: Style,
blitter: Blitter,
}
impl Canvas {
pub fn new(width: usize, height: usize) -> Self {
Self {
width,
height,
pixels: vec![vec![0u8; width]; height],
style: Style::null(),
blitter: Blitter::Braille,
}
}
#[must_use]
pub fn with_blitter(mut self, blitter: Blitter) -> Self {
self.blitter = blitter;
self
}
#[must_use]
pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn pixel_width(&self) -> usize {
match self.blitter {
Blitter::HalfBlock => self.width,
_ => self.width * 2,
}
}
pub fn pixel_height(&self) -> usize {
match self.blitter {
Blitter::Braille | Blitter::Octant => self.height * 4,
Blitter::Sextant => self.height * 3,
Blitter::HalfBlock => self.height * 2,
}
}
fn pixel_address(&self, x: usize, y: usize) -> Option<(usize, usize, u8)> {
if x >= self.pixel_width() || y >= self.pixel_height() {
return None;
}
let (cell_col, cell_row, bit) = match self.blitter {
Blitter::Braille => {
let col = x / 2;
let row = y / 4;
let bit = PIXEL_MAP[y % 4][x % 2];
(col, row, bit)
}
Blitter::Octant => {
let col = x / 2;
let row = y / 4;
let bit = OCTANT_PIXEL_MAP[y % 4][x % 2];
(col, row, bit)
}
Blitter::Sextant => {
let col = x / 2;
let row = y / 3;
let bit = SEXTANT_PIXEL_MAP[y % 3][x % 2];
(col, row, bit)
}
Blitter::HalfBlock => {
let col = x; let row = y / 2;
let bit: u8 = if y % 2 == 0 { 0x01 } else { 0x02 };
(col, row, bit)
}
};
Some((cell_col, cell_row, bit))
}
pub fn set(&mut self, x: usize, y: usize) {
if let Some((col, row, bit)) = self.pixel_address(x, y) {
self.pixels[row][col] |= bit;
}
}
pub fn unset(&mut self, x: usize, y: usize) {
if let Some((col, row, bit)) = self.pixel_address(x, y) {
self.pixels[row][col] &= !bit;
}
}
pub fn toggle(&mut self, x: usize, y: usize) {
if let Some((col, row, bit)) = self.pixel_address(x, y) {
self.pixels[row][col] ^= bit;
}
}
pub fn get(&self, x: usize, y: usize) -> bool {
match self.pixel_address(x, y) {
Some((col, row, bit)) => self.pixels[row][col] & bit != 0,
None => false,
}
}
pub fn line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
let mut x0 = x0;
let mut y0 = y0;
let dx = (x1 - x0).abs();
let dy = -(y1 - y0).abs();
let sx: i32 = if x0 < x1 { 1 } else { -1 };
let sy: i32 = if y0 < y1 { 1 } else { -1 };
let mut err = dx + dy;
loop {
if x0 >= 0 && y0 >= 0 {
self.set(x0 as usize, y0 as usize);
}
if x0 == x1 && y0 == y1 {
break;
}
let e2 = 2 * err;
if e2 >= dy {
if x0 == x1 {
break;
}
err += dy;
x0 += sx;
}
if e2 <= dx {
if y0 == y1 {
break;
}
err += dx;
y0 += sy;
}
}
}
pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
if w == 0 || h == 0 {
return;
}
let x1 = x + w - 1;
let y1 = y + h - 1;
self.line(x as i32, y as i32, x1 as i32, y as i32); self.line(x as i32, y1 as i32, x1 as i32, y1 as i32); self.line(x as i32, y as i32, x as i32, y1 as i32); self.line(x1 as i32, y as i32, x1 as i32, y1 as i32); }
pub fn fill_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
for dy in 0..h {
for dx in 0..w {
self.set(x + dx, y + dy);
}
}
}
pub fn circle(&mut self, cx: i32, cy: i32, r: i32) {
if r < 0 {
return;
}
let mut x = r;
let mut y: i32 = 0;
let mut err = 1 - r;
while x >= y {
self.set_signed(cx + x, cy + y);
self.set_signed(cx - x, cy + y);
self.set_signed(cx + x, cy - y);
self.set_signed(cx - x, cy - y);
self.set_signed(cx + y, cy + x);
self.set_signed(cx - y, cy + x);
self.set_signed(cx + y, cy - x);
self.set_signed(cx - y, cy - x);
y += 1;
if err <= 0 {
err += 2 * y + 1;
} else {
x -= 1;
err += 2 * (y - x) + 1;
}
}
}
fn set_signed(&mut self, x: i32, y: i32) {
if x >= 0 && y >= 0 {
self.set(x as usize, y as usize);
}
}
pub fn frame(&self) -> String {
let mut lines: Vec<String> = Vec::with_capacity(self.height);
for row in &self.pixels {
let line: String = match self.blitter {
Blitter::Braille => row
.iter()
.map(|&bits| char::from_u32(BRAILLE_BASE + bits as u32).unwrap_or(' '))
.collect(),
Blitter::Octant => row.iter().map(|&bits| octant_bits_to_char(bits)).collect(),
Blitter::Sextant => row.iter().map(|&bits| sextant_bits_to_char(bits)).collect(),
Blitter::HalfBlock => row
.iter()
.map(|&bits| halfblock_bits_to_char(bits))
.collect(),
};
lines.push(line);
}
lines.join("\n")
}
pub fn clear(&mut self) {
for row in &mut self.pixels {
for cell in row.iter_mut() {
*cell = 0;
}
}
}
}
impl fmt::Display for Canvas {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.frame())
}
}
impl Renderable for Canvas {
fn gilt_console(&self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
let mut segments = Vec::new();
for (i, row) in self.pixels.iter().enumerate() {
let line: String = match self.blitter {
Blitter::Braille => row
.iter()
.map(|&bits| char::from_u32(BRAILLE_BASE + bits as u32).unwrap_or(' '))
.collect(),
Blitter::Octant => row.iter().map(|&bits| octant_bits_to_char(bits)).collect(),
Blitter::Sextant => row.iter().map(|&bits| sextant_bits_to_char(bits)).collect(),
Blitter::HalfBlock => row
.iter()
.map(|&bits| halfblock_bits_to_char(bits))
.collect(),
};
segments.push(Segment::new(&line, Some(self.style.clone()), None));
if i < self.height - 1 {
segments.push(Segment::line());
}
}
segments.push(Segment::line());
segments
}
fn gilt_measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
self.measure(console, options)
}
}
impl Canvas {
pub fn measure(&self, _console: &Console, _options: &ConsoleOptions) -> Measurement {
Measurement::new(self.width, self.width)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::console::{Console, ConsoleDimensions, ConsoleOptions};
fn make_options(max_width: usize) -> ConsoleOptions {
ConsoleOptions {
size: ConsoleDimensions {
width: max_width,
height: 25,
},
legacy_windows: false,
min_width: 1,
max_width,
is_terminal: false,
encoding: std::borrow::Cow::Borrowed("utf-8"),
max_height: 25,
justify: None,
overflow: None,
no_wrap: None,
highlight: None,
markup: None,
height: None,
}
}
#[test]
fn canvas_gilt_measure_delegates_to_measure() {
let canvas = Canvas::new(10, 4);
let console = Console::builder()
.width(80)
.force_terminal(true)
.no_color(true)
.build();
let opts = console.options();
assert_eq!(
canvas.gilt_measure(&console, &opts),
canvas.measure(&console, &opts),
"Canvas::gilt_measure must delegate to Canvas::measure",
);
}
#[test]
fn test_empty_canvas() {
let c = Canvas::new(3, 2);
let frame = c.frame();
let empty_braille = '\u{2800}';
let expected = format!(
"{}{}{}",
std::iter::repeat_n(empty_braille, 3).collect::<String>(),
"\n",
std::iter::repeat_n(empty_braille, 3).collect::<String>(),
);
assert_eq!(frame, expected);
}
#[test]
fn test_single_pixel_set() {
let mut c = Canvas::new(1, 1);
c.set(0, 0);
assert!(c.get(0, 0));
let expected_char = char::from_u32(BRAILLE_BASE + 0x01).unwrap();
assert_eq!(c.frame(), expected_char.to_string());
}
#[test]
fn test_pixel_unset() {
let mut c = Canvas::new(1, 1);
c.set(0, 0);
assert!(c.get(0, 0));
c.unset(0, 0);
assert!(!c.get(0, 0));
}
#[test]
fn test_pixel_toggle() {
let mut c = Canvas::new(1, 1);
assert!(!c.get(0, 0));
c.toggle(0, 0);
assert!(c.get(0, 0));
c.toggle(0, 0);
assert!(!c.get(0, 0));
}
#[test]
fn test_pixel_get_unset() {
let c = Canvas::new(2, 2);
assert!(!c.get(0, 0));
assert!(!c.get(3, 7));
}
#[test]
fn test_braille_mapping() {
let dots: [(usize, usize, u8); 8] = [
(0, 0, 0x01), (0, 1, 0x02), (0, 2, 0x04), (0, 3, 0x40), (1, 0, 0x08), (1, 1, 0x10), (1, 2, 0x20), (1, 3, 0x80), ];
for (px, py, expected_bit) in dots {
let mut c = Canvas::new(1, 1);
c.set(px, py);
assert_eq!(
c.pixels[0][0], expected_bit,
"pixel ({px},{py}) should set bit 0x{expected_bit:02x}"
);
}
}
#[test]
fn test_line_horizontal() {
let mut c = Canvas::new(5, 1);
c.line(0, 0, 9, 0); for x in 0..10 {
assert!(c.get(x, 0), "pixel ({x}, 0) should be set");
}
}
#[test]
fn test_line_vertical() {
let mut c = Canvas::new(1, 2);
c.line(0, 0, 0, 7); for y in 0..8 {
assert!(c.get(0, y), "pixel (0, {y}) should be set");
}
}
#[test]
fn test_line_diagonal() {
let mut c = Canvas::new(4, 2);
c.line(0, 0, 7, 7);
assert!(c.get(0, 0));
assert!(c.get(7, 7));
}
#[test]
fn test_rect_outline() {
let mut c = Canvas::new(4, 2);
c.rect(0, 0, 8, 8);
assert!(c.get(0, 0));
assert!(c.get(7, 0));
assert!(c.get(0, 7));
assert!(c.get(7, 7));
assert!(!c.get(3, 3));
}
#[test]
fn test_fill_rect() {
let mut c = Canvas::new(2, 1);
c.fill_rect(0, 0, 4, 4);
for y in 0..4 {
for x in 0..4 {
assert!(c.get(x, y), "pixel ({x},{y}) should be set");
}
}
}
#[test]
fn test_circle() {
let mut c = Canvas::new(10, 5);
c.circle(10, 10, 8);
assert!(c.get(18, 10));
assert!(c.get(10, 18));
}
#[test]
fn test_out_of_bounds() {
let mut c = Canvas::new(2, 2);
c.set(100, 100);
c.unset(100, 100);
c.toggle(100, 100);
assert!(!c.get(100, 100));
}
#[test]
fn test_clear() {
let mut c = Canvas::new(3, 2);
c.fill_rect(0, 0, 6, 8);
c.clear();
for row in &c.pixels {
for &cell in row {
assert_eq!(cell, 0);
}
}
}
#[test]
fn test_frame_multiline() {
let c = Canvas::new(2, 3);
let frame = c.frame();
let lines: Vec<&str> = frame.split('\n').collect();
assert_eq!(lines.len(), 3);
for line in lines {
assert_eq!(line.chars().count(), 2);
}
}
#[test]
fn test_display_trait() {
let c = Canvas::new(2, 2);
let displayed = format!("{c}");
assert_eq!(displayed, c.frame());
}
#[test]
fn test_renderable() {
let c = Canvas::new(3, 2);
let console = Console::builder().width(80).build();
let opts = make_options(80);
let segments = c.gilt_console(&console, &opts);
assert!(!segments.is_empty());
assert_eq!(segments.last().unwrap().text.as_str(), "\n");
}
#[test]
fn test_renderable_style() {
let style = Style::parse("bold green");
let c = Canvas::new(2, 1).with_style(style.clone());
let console = Console::builder().width(80).build();
let opts = make_options(80);
let segments = c.gilt_console(&console, &opts);
assert_eq!(segments[0].style.as_ref(), Some(&style));
}
#[test]
fn test_measure() {
let c = Canvas::new(20, 10);
let console = Console::builder().width(80).build();
let opts = make_options(80);
let m = c.measure(&console, &opts);
assert_eq!(m, Measurement::new(20, 20));
}
#[test]
fn test_pixel_dimensions() {
let c = Canvas::new(10, 5);
assert_eq!(c.pixel_width(), 20);
assert_eq!(c.pixel_height(), 20);
}
#[test]
fn test_multiple_pixels_same_cell() {
let mut c = Canvas::new(1, 1);
c.set(0, 0); c.set(1, 0); assert_eq!(c.pixels[0][0], 0x01 | 0x08);
assert!(c.get(0, 0));
assert!(c.get(1, 0));
}
#[test]
fn test_circle_radius_zero() {
let mut c = Canvas::new(2, 2);
c.circle(2, 2, 0);
assert!(c.get(2, 2));
}
#[test]
fn test_circle_negative_radius() {
let mut c = Canvas::new(2, 2);
c.circle(2, 2, -5);
for y in 0..c.pixel_height() {
for x in 0..c.pixel_width() {
assert!(!c.get(x, y));
}
}
}
#[test]
fn test_rect_zero() {
let mut c = Canvas::new(2, 2);
c.rect(0, 0, 0, 0);
for y in 0..c.pixel_height() {
for x in 0..c.pixel_width() {
assert!(!c.get(x, y));
}
}
}
#[test]
fn test_full_braille_char() {
let mut c = Canvas::new(1, 1);
for y in 0..4 {
for x in 0..2 {
c.set(x, y);
}
}
assert_eq!(c.pixels[0][0], 0xFF);
let ch = char::from_u32(BRAILLE_BASE + 0xFF).unwrap();
assert_eq!(c.frame(), ch.to_string());
}
#[test]
fn test_octant_anchors() {
assert_eq!(octant_bits_to_char(0x00), ' ');
assert_eq!(octant_bits_to_char(0xFF), '\u{2588}');
}
#[test]
fn test_octant_half_blocks() {
assert_eq!(octant_bits_to_char(0x0F), '\u{2580}'); assert_eq!(octant_bits_to_char(0xF0), '\u{2584}'); assert_eq!(octant_bits_to_char(0x55), '\u{258C}'); assert_eq!(octant_bits_to_char(0xAA), '\u{2590}'); }
#[test]
fn test_octant_new_block_codepoints() {
assert_eq!(octant_bits_to_char(0x04), '\u{1CD00}');
assert_eq!(octant_bits_to_char(0x06), '\u{1CD01}');
assert_eq!(octant_bits_to_char(0x07), '\u{1CD02}');
assert_eq!(octant_bits_to_char(0xFE), '\u{1CDE5}');
}
#[test]
fn test_octant_reused_quarter_blocks() {
assert_eq!(octant_bits_to_char(0x14), '\u{1FBE6}'); assert_eq!(octant_bits_to_char(0x28), '\u{1FBE7}'); assert_eq!(octant_bits_to_char(0x03), '\u{1FB82}'); assert_eq!(octant_bits_to_char(0xC0), '\u{2582}'); assert_eq!(octant_bits_to_char(0x3F), '\u{1FB85}'); assert_eq!(octant_bits_to_char(0xFC), '\u{2586}'); assert_eq!(octant_bits_to_char(0x01), '\u{1CEA8}'); assert_eq!(octant_bits_to_char(0x80), '\u{1CEA0}'); }
#[test]
fn test_octant_pixel_bit_mapping() {
let cases: [(usize, usize, u8); 8] = [
(0, 0, 0x01), (1, 0, 0x02), (0, 1, 0x04), (1, 1, 0x08), (0, 2, 0x10), (1, 2, 0x20), (0, 3, 0x40), (1, 3, 0x80), ];
for (px, py, expected_bit) in cases {
let mut c = Canvas::new(1, 1).with_blitter(Blitter::Octant);
c.set(px, py);
assert_eq!(
c.pixels[0][0], expected_bit,
"octant pixel ({px},{py}) should set bit 0x{expected_bit:02X}"
);
}
}
#[test]
fn test_octant_frame_full_cell() {
let mut c = Canvas::new(1, 1).with_blitter(Blitter::Octant);
for y in 0..4 {
for x in 0..2 {
c.set(x, y);
}
}
assert_eq!(c.pixels[0][0], 0xFF);
assert_eq!(c.frame(), '\u{2588}'.to_string());
}
#[test]
fn test_octant_empty_frame_is_space() {
let c = Canvas::new(3, 1).with_blitter(Blitter::Octant);
assert_eq!(c.frame(), " ");
}
#[test]
fn test_octant_gilt_console_uses_octant_glyphs() {
let mut c = Canvas::new(1, 1).with_blitter(Blitter::Octant);
c.set(0, 1); let console = Console::builder().width(80).build();
let opts = make_options(80);
let segs = c.gilt_console(&console, &opts);
assert_eq!(segs[0].text.as_str(), "\u{1CD00}");
}
#[test]
fn test_octant_table_all_distinct() {
use std::collections::HashSet;
let set: HashSet<char> = OCTANT_CHARS.iter().copied().collect();
assert_eq!(
set.len(),
256,
"every octant pattern must map to a distinct glyph"
);
}
}