use super::charset::Charset;
use super::color::{Color, ColorMode, Resolved};
#[derive(Debug, Clone, Copy, PartialEq)]
struct Cell {
bits: u8,
text: Text,
color: Color,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Text {
None,
Glyph(char),
Continuation,
}
const EMPTY: Cell = Cell {
bits: 0,
text: Text::None,
color: Color::Default,
};
#[derive(Clone, PartialEq)]
pub struct Surface {
width: usize,
height: usize,
charset: Charset,
columns: usize,
rows: usize,
cells: Vec<Cell>,
}
impl Surface {
pub fn new(width: usize, height: usize, charset: Charset) -> Surface {
let (columns, rows) = charset.pixels_per_cell();
Surface {
width,
height,
charset,
columns,
rows,
cells: vec![EMPTY; width * height],
}
}
pub fn size(&self) -> (usize, usize) {
(self.width, self.height)
}
pub fn subpixel_size(&self) -> (usize, usize) {
(self.width * self.columns, self.height * self.rows)
}
pub fn set(&mut self, x: i64, y: i64, color: Color) {
let (sw, sh) = self.subpixel_size();
if x < 0 || y < 0 || x >= sw as i64 || y >= sh as i64 {
return;
}
let (x, y) = (x as usize, y as usize);
let index = (y / self.rows) * self.width + x / self.columns;
let cell = &mut self.cells[index];
cell.bits |= self.charset.bit(x % self.columns, y % self.rows);
cell.color = color;
}
pub fn dot(&mut self, x: f64, y: f64, color: Color) {
if x.is_finite() && y.is_finite() {
self.set(x.round() as i64, y.round() as i64, color);
}
}
pub fn line(&mut self, from: (f64, f64), to: (f64, f64), color: Color) {
if !(from.0.is_finite() && from.1.is_finite() && to.0.is_finite() && to.1.is_finite()) {
return;
}
let (sw, sh) = self.subpixel_size();
if sw == 0 || sh == 0 {
return;
}
let window = ((sw - 1) as f64, (sh - 1) as f64);
let Some((from, to)) = clip(from, to, window) else {
return;
};
let (x1, y1) = (to.0.round() as i64, to.1.round() as i64);
let (mut x, mut y) = (from.0.round() as i64, from.1.round() as i64);
let dx = (x1 - x).abs();
let dy = -(y1 - y).abs();
let sx = if x < x1 { 1 } else { -1 };
let sy = if y < y1 { 1 } else { -1 };
let mut error = dx + dy;
loop {
self.set(x, y, color);
if x == x1 && y == y1 {
return;
}
let doubled = 2 * error;
if doubled >= dy {
error += dy;
x += sx;
}
if doubled <= dx {
error += dx;
y += sy;
}
}
}
pub fn text(&mut self, column: i64, row: i64, text: &str, color: Color) {
use unicode_width::UnicodeWidthChar;
if row < 0 || row >= self.height as i64 {
return;
}
let row = row as usize;
let mut column = column;
for glyph in text.chars() {
let width = glyph.width().unwrap_or(0) as i64;
if width == 0 {
continue;
}
let fits = column >= 0 && column + width <= self.width as i64;
if fits {
self.place(row, column as usize, Text::Glyph(glyph), color);
for offset in 1..width {
self.place(row, (column + offset) as usize, Text::Continuation, color);
}
}
column += width;
}
}
fn place(&mut self, row: usize, column: usize, text: Text, color: Color) {
let base = row * self.width;
if self.cells[base + column].text == Text::Continuation && column > 0 {
self.cells[base + column - 1].text = Text::Glyph(' ');
}
if column + 1 < self.width && self.cells[base + column + 1].text == Text::Continuation {
self.cells[base + column + 1].text = Text::Glyph(' ');
}
let cell = &mut self.cells[base + column];
cell.text = text;
cell.color = color;
}
pub fn to_plain(&self) -> String {
self.encode(ColorMode::Plain)
}
pub fn encode(&self, mode: ColorMode) -> String {
let mut out = String::with_capacity((self.width + 8) * self.height);
for row in 0..self.height {
if row > 0 {
out.push('\n');
}
let mut current = Resolved::Default;
let mut kept = out.len();
for (glyph, color) in self.row(row) {
if glyph != ' ' {
let resolved = color.resolve(mode);
if resolved != current {
resolved.write_sgr(&mut out);
current = resolved;
}
}
out.push(glyph);
if glyph != ' ' {
kept = out.len();
}
}
out.truncate(kept);
if current != Resolved::Default {
out.push_str("\x1b[0m");
}
}
out
}
fn row(&self, row: usize) -> impl Iterator<Item = (char, Color)> + '_ {
self.cells[row * self.width..(row + 1) * self.width]
.iter()
.filter_map(|cell| match cell.text {
Text::Continuation => None,
Text::Glyph(glyph) => Some((glyph, cell.color)),
Text::None => Some((self.charset.glyph(cell.bits), cell.color)),
})
}
}
impl std::fmt::Debug for Surface {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Surface")
.field("width", &self.width)
.field("height", &self.height)
.field("charset", &self.charset)
.finish_non_exhaustive()
}
}
fn clip(from: (f64, f64), to: (f64, f64), window: (f64, f64)) -> Option<((f64, f64), (f64, f64))> {
let (dx, dy) = (to.0 - from.0, to.1 - from.1);
let mut enter = 0.0f64;
let mut exit = 1.0f64;
let tests = [
(-dx, from.0),
(dx, window.0 - from.0),
(-dy, from.1),
(dy, window.1 - from.1),
];
for (p, q) in tests {
if p == 0.0 {
if q < 0.0 {
return None;
}
} else {
let r = q / p;
if p < 0.0 {
if r > exit {
return None;
}
enter = enter.max(r);
} else {
if r < enter {
return None;
}
exit = exit.min(r);
}
}
}
if enter > exit {
return None;
}
Some((
(from.0 + enter * dx, from.1 + enter * dy),
(from.0 + exit * dx, from.1 + exit * dy),
))
}
#[cfg(test)]
#[path = "tests/surface_tests.rs"]
mod tests;