use crate::style::{Color, Weight};
use crossterm::{
csi,
style::{Attribute, Colored},
};
use std::{
fmt::{self, Display},
io::{self, Write},
};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
#[derive(Clone, Debug, PartialEq)]
struct Character {
value: String,
style: CanvasTextStyle,
}
impl Character {
fn required_padding(&self) -> usize {
if self.value.contains('\u{fe0f}') {
self.value.width() - 1
} else {
0
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct CanvasTextStyle {
pub color: Option<Color>,
pub weight: Weight,
pub underline: bool,
}
#[derive(Clone, Default, PartialEq)]
struct Cell {
background_color: Option<Color>,
character: Option<Character>,
}
impl Cell {
fn is_empty(&self) -> bool {
self.background_color.is_none() && self.character.is_none()
}
}
#[derive(Clone, PartialEq)]
pub struct Canvas {
width: usize,
cells: Vec<Vec<Cell>>,
}
impl Canvas {
pub fn new(width: usize, height: usize) -> Self {
Self {
width,
cells: vec![vec![Cell::default(); width]; height],
}
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.cells.len()
}
fn set_background_color(&mut self, x: usize, y: usize, w: usize, h: usize, color: Color) {
for y in y..y + h {
let row = &mut self.cells[y];
for x in x..x + w {
if x < row.len() {
row[x].background_color = Some(color);
}
}
}
}
fn set_text_row_chars<I>(&mut self, mut x: usize, y: usize, chars: I, style: CanvasTextStyle)
where
I: IntoIterator<Item = char>,
{
let row = &mut self.cells[y];
let mut buf = String::new();
for c in chars.into_iter() {
if x >= row.len() {
break;
}
let width = c.width().unwrap_or(0);
if width > 0 && !buf.is_empty() {
row[x].character = Some(Character {
value: buf.clone(),
style,
});
x += buf.width().max(1);
buf.clear();
}
buf.push(c);
}
if !buf.is_empty() && x < row.len() {
row[x].character = Some(Character { value: buf, style });
}
}
pub fn subview_mut(
&mut self,
x: usize,
y: usize,
width: usize,
height: usize,
clip: bool,
) -> CanvasSubviewMut {
CanvasSubviewMut {
y,
x,
width,
height,
clip,
canvas: self,
}
}
fn write_impl<W: Write>(
&self,
mut w: W,
ansi: bool,
omit_final_newline: bool,
) -> io::Result<()> {
if ansi {
write!(w, csi!("0m"))?;
}
let mut background_color = None;
let mut text_style = CanvasTextStyle::default();
for y in 0..self.cells.len() {
let row = &self.cells[y];
let last_non_empty = row.iter().rposition(|cell| !cell.is_empty());
let row = &row[..last_non_empty.map_or(0, |i| i + 1)];
let mut col = 0;
while col < row.len() {
let cell = &row[col];
if ansi {
let mut needs_reset = false;
if let Some(c) = &cell.character {
if c.style.weight != text_style.weight && c.style.weight == Weight::Normal {
needs_reset = true;
}
if !c.style.underline && text_style.underline {
needs_reset = true;
}
} else if text_style.underline {
needs_reset = true;
}
if needs_reset {
write!(w, csi!("0m"))?;
background_color = None;
text_style = CanvasTextStyle::default();
}
if cell.background_color != background_color {
write!(
w,
csi!("{}m"),
Colored::BackgroundColor(cell.background_color.unwrap_or(Color::Reset))
)?;
background_color = cell.background_color;
}
if let Some(c) = &cell.character {
if c.style.color != text_style.color {
write!(
w,
csi!("{}m"),
Colored::ForegroundColor(c.style.color.unwrap_or(Color::Reset))
)?;
}
if c.style.weight != text_style.weight {
match c.style.weight {
Weight::Bold => write!(w, csi!("{}m"), Attribute::Bold.sgr())?,
Weight::Normal => {}
Weight::Light => write!(w, csi!("{}m"), Attribute::Dim.sgr())?,
}
}
if c.style.underline && !text_style.underline {
write!(w, csi!("{}m"), Attribute::Underlined.sgr())?;
}
text_style = c.style;
}
}
if let Some(c) = &cell.character {
write!(w, "{}{}", c.value, " ".repeat(c.required_padding()))?;
col += c.value.width().max(1);
} else {
w.write_all(b" ")?;
col += 1;
}
}
if ansi {
if background_color.is_some() {
write!(w, csi!("{}m"), Colored::BackgroundColor(Color::Reset))?;
background_color = None;
}
write!(w, csi!("K"))?;
}
if !omit_final_newline || y < self.cells.len() - 1 {
if ansi {
w.write_all(b"\r\n")?;
} else {
w.write_all(b"\n")?;
}
}
}
if ansi {
write!(w, csi!("0m"))?;
}
w.flush()?;
Ok(())
}
pub fn write_ansi<W: Write>(&self, w: W) -> io::Result<()> {
self.write_impl(w, true, false)
}
pub(crate) fn write_ansi_without_final_newline<W: Write>(&self, w: W) -> io::Result<()> {
self.write_impl(w, true, true)
}
pub fn write<W: Write>(&self, w: W) -> io::Result<()> {
self.write_impl(w, false, false)
}
}
impl Display for Canvas {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buf = Vec::with_capacity(self.width * self.cells.len());
self.write(&mut buf).unwrap();
f.write_str(&String::from_utf8_lossy(&buf))?;
Ok(())
}
}
pub struct CanvasSubviewMut<'a> {
x: usize,
y: usize,
width: usize,
height: usize,
clip: bool,
canvas: &'a mut Canvas,
}
impl<'a> CanvasSubviewMut<'a> {
pub fn set_background_color(&mut self, x: isize, y: isize, w: usize, h: usize, color: Color) {
let mut left = self.x as isize + x;
let mut top = self.y as isize + y;
let mut right = left + w as isize;
let mut bottom = top + h as isize;
if self.clip {
left = left.max(self.x as isize);
top = top.max(self.y as isize);
right = right.min((self.x + self.width) as isize);
bottom = bottom.min((self.y + self.height) as isize);
}
self.canvas.set_background_color(
left as _,
top as _,
(right - left) as _,
(bottom - top) as _,
color,
);
}
pub fn set_text(&mut self, x: isize, mut y: isize, text: &str, style: CanvasTextStyle) {
let mut x = self.x as isize + x;
let min_x = if self.clip { self.x as isize } else { 0 };
let mut to_skip = 0;
if x < min_x {
to_skip = min_x - x;
x = min_x;
}
let max_x = if self.clip {
(self.x + self.width) as isize - 1
} else {
self.canvas.width as isize - 1
};
let horizontal_space = max_x - x + 1;
for line in text.lines() {
if !self.clip || (y >= 0 && y < self.height as isize) {
let y = self.y as isize + y;
if y >= 0 && y < self.canvas.height() as _ {
let mut skipped_width = 0;
let mut taken_width = 0;
self.canvas.set_text_row_chars(
x as usize,
y as usize,
line.chars()
.skip_while(|c| {
if skipped_width < to_skip {
skipped_width += c.width().unwrap_or(0) as isize;
true
} else {
false
}
})
.take_while(|c| {
if taken_width < horizontal_space {
taken_width += c.width().unwrap_or(0) as isize;
true
} else {
false
}
}),
style,
);
}
}
y += 1;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
#[test]
fn test_canvas_background_color() {
let mut canvas = Canvas::new(6, 3);
assert_eq!(canvas.width(), 6);
assert_eq!(canvas.height(), 3);
canvas
.subview_mut(2, 0, 3, 2, true)
.set_background_color(0, 0, 5, 5, Color::Red);
let mut actual = Vec::new();
canvas.write_ansi(&mut actual).unwrap();
let mut expected = Vec::new();
write!(expected, csi!("0m")).unwrap();
write!(expected, " ").unwrap();
write!(expected, csi!("{}m"), Colored::BackgroundColor(Color::Red)).unwrap();
write!(expected, " ").unwrap();
write!(
expected,
csi!("{}m"),
Colored::BackgroundColor(Color::Reset)
)
.unwrap();
write!(expected, csi!("K")).unwrap();
write!(expected, "\r\n").unwrap();
write!(expected, " ").unwrap();
write!(expected, csi!("{}m"), Colored::BackgroundColor(Color::Red)).unwrap();
write!(expected, " ").unwrap();
write!(
expected,
csi!("{}m"),
Colored::BackgroundColor(Color::Reset)
)
.unwrap();
write!(expected, csi!("K")).unwrap();
write!(expected, "\r\n").unwrap();
write!(expected, csi!("K")).unwrap();
write!(expected, "\r\n").unwrap();
write!(expected, csi!("0m")).unwrap();
assert_eq!(actual, expected);
}
#[test]
fn test_canvas_text_styles() {
let mut canvas = Canvas::new(100, 1);
assert_eq!(canvas.width(), 100);
assert_eq!(canvas.height(), 1);
canvas
.subview_mut(0, 0, 1, 1, true)
.set_text(0, 0, ".", CanvasTextStyle::default());
canvas.subview_mut(1, 0, 1, 1, true).set_text(
0,
0,
".",
CanvasTextStyle {
color: Some(Color::Red),
weight: Weight::Bold,
underline: true,
..Default::default()
},
);
canvas.subview_mut(2, 0, 1, 1, true).set_text(
0,
0,
".",
CanvasTextStyle {
color: Some(Color::Red),
weight: Weight::Bold,
..Default::default()
},
);
canvas.subview_mut(3, 0, 1, 1, true).set_text(
0,
0,
".",
CanvasTextStyle {
color: Some(Color::Red),
weight: Weight::Light,
..Default::default()
},
);
canvas.subview_mut(4, 0, 1, 1, true).set_text(
0,
0,
".",
CanvasTextStyle {
color: Some(Color::Red),
..Default::default()
},
);
canvas.subview_mut(5, 0, 1, 1, true).set_text(
0,
0,
".",
CanvasTextStyle {
color: Some(Color::Green),
..Default::default()
},
);
let mut actual = Vec::new();
canvas.write_ansi(&mut actual).unwrap();
let mut expected = Vec::new();
write!(expected, csi!("0m")).unwrap();
write!(expected, ".").unwrap();
write!(expected, csi!("{}m"), Colored::ForegroundColor(Color::Red)).unwrap();
write!(expected, csi!("{}m"), Attribute::Bold.sgr()).unwrap();
write!(expected, csi!("{}m"), Attribute::Underlined.sgr()).unwrap();
write!(expected, ".").unwrap();
write!(expected, csi!("0m")).unwrap();
write!(expected, csi!("{}m"), Colored::ForegroundColor(Color::Red)).unwrap();
write!(expected, csi!("{}m"), Attribute::Bold.sgr()).unwrap();
write!(expected, ".").unwrap();
write!(expected, csi!("{}m"), Attribute::Dim.sgr()).unwrap();
write!(expected, ".").unwrap();
write!(expected, csi!("0m")).unwrap();
write!(expected, csi!("{}m"), Colored::ForegroundColor(Color::Red)).unwrap();
write!(expected, ".").unwrap();
write!(
expected,
csi!("{}m"),
Colored::ForegroundColor(Color::Green)
)
.unwrap();
write!(expected, ".").unwrap();
write!(expected, csi!("K")).unwrap();
write!(expected, "\r\n").unwrap();
write!(expected, csi!("0m")).unwrap();
assert_eq!(actual, expected);
}
#[test]
fn test_canvas_text_clipping() {
let mut canvas = Canvas::new(10, 5);
assert_eq!(canvas.width(), 10);
assert_eq!(canvas.height(), 5);
canvas.subview_mut(2, 2, 4, 2, true).set_text(
-2,
-1,
"line 1\nline 2\nline 3\nline 4",
CanvasTextStyle::default(),
);
let actual = canvas.to_string();
assert_eq!(actual, "\n\n ne 2\n ne 3\n\n");
}
#[test]
fn test_write_ansi_without_final_newline() {
let mut canvas = Canvas::new(10, 3);
canvas
.subview_mut(0, 0, 10, 3, true)
.set_text(0, 0, "hello!", CanvasTextStyle::default());
let mut actual = Vec::new();
canvas
.write_ansi_without_final_newline(&mut actual)
.unwrap();
let mut expected = Vec::new();
write!(expected, csi!("0m")).unwrap();
write!(expected, "hello!").unwrap();
write!(expected, csi!("K")).unwrap();
write!(expected, "\r\n").unwrap();
write!(expected, csi!("K")).unwrap();
write!(expected, "\r\n").unwrap();
write!(expected, csi!("K")).unwrap();
write!(expected, csi!("0m")).unwrap();
assert_eq!(actual, expected);
}
}