#![deny(missing_docs)]
use std::io::{Result, Write};
use std::ops::Index;
use anathema_geometry::Size;
use anathema_widgets::Style;
use anathema_widgets::paint::{Glyph, GlyphMap};
use crossterm::style::Print;
use crossterm::{QueueableCommand, cursor};
use super::LocalPos;
use super::style::write_style;
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Cell {
pub(crate) style: Style,
pub(crate) state: CellState,
}
impl Cell {
pub(crate) fn empty() -> Self {
Self {
style: Style::reset(),
state: CellState::Empty,
}
}
pub(crate) fn reset() -> Self {
Self {
style: Style::reset(),
state: CellState::Occupied(Glyph::space()),
}
}
fn continuation(style: Style) -> Self {
Self {
style,
state: CellState::Continuation,
}
}
pub(crate) fn new(glyph: Glyph, style: Style) -> Self {
Self {
style,
state: CellState::Occupied(glyph),
}
}
}
#[derive(Copy, Clone, PartialEq)]
pub(crate) enum CellState {
Empty,
Occupied(Glyph),
Continuation,
}
impl std::fmt::Debug for CellState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CellState::Empty => write!(f, "<E>"),
CellState::Occupied(Glyph::Single(glyph, _width)) => write!(f, "{glyph}"),
CellState::Occupied(Glyph::Cluster(index, width)) => write!(f, "<C{index:?},{width}>"),
CellState::Continuation => write!(f, "<C>"),
}
}
}
#[derive(Debug, Clone)]
pub struct Buffer {
size: Size,
pub(crate) inner: Box<[Cell]>,
}
impl Buffer {
pub fn new(size: impl Into<Size>) -> Self {
let size = size.into();
Self {
inner: vec![Cell::empty(); (size.width * size.height) as usize].into_boxed_slice(),
size,
}
}
pub(crate) fn reset(size: impl Into<Size>) -> Self {
let size = size.into();
Self {
inner: vec![Cell::reset(); (size.width * size.height) as usize].into_boxed_slice(),
size,
}
}
pub fn size(&self) -> Size {
self.size
}
pub fn resize(&mut self, size: Size) {
let mut new_buf = Buffer::new(size);
for (y, line) in self.cell_lines().enumerate() {
if y >= size.height as usize {
break;
}
for (x, cell) in line.iter().enumerate() {
if x >= size.width as usize {
break;
}
let pos = LocalPos::new(x as u16, y as u16);
new_buf.put(*cell, pos);
}
}
self.size = size;
self.inner = new_buf.inner;
}
pub fn put_glyph(&mut self, glyph: Glyph, pos: LocalPos) {
assert!(
pos.x < self.size.width && pos.y < self.size.height,
"position out of bounds"
);
let style = match self.get(pos) {
Some((_, style)) => *style,
None => Style::new(),
};
let cell = Cell::new(glyph, style);
self.put(cell, pos);
}
pub fn update_cell(&mut self, style: Style, pos: LocalPos) {
if pos.x >= self.size.width || pos.y >= self.size.height {
return;
}
let index = pos.to_index(self.size.width);
let cell = &mut self.inner[index];
if let fg @ Some(_) = style.fg {
cell.style.fg = fg;
}
if let bg @ Some(_) = style.bg {
cell.style.bg = bg;
}
cell.style.attributes |= style.attributes;
}
pub fn get(&self, pos: LocalPos) -> Option<(&Glyph, &Style)> {
if pos.x >= self.size.width || pos.y >= self.size.height {
return None;
}
let index = pos.to_index(self.size.width);
let cell = self.inner.get(index)?;
match &cell.state {
CellState::Occupied(c) => Some((c, &cell.style)),
_ => None,
}
}
pub fn get_mut(&mut self, pos: LocalPos) -> Option<(&mut Glyph, &mut Style)> {
if pos.x >= self.size.width || pos.y >= self.size.height {
return None;
}
let index = pos.to_index(self.size.width);
let cell = self.inner.get_mut(index)?;
match &mut cell.state {
CellState::Occupied(c) => Some((c, &mut cell.style)),
_ => None,
}
}
pub(super) fn reset_cell(&mut self, pos: LocalPos) {
let index = pos.to_index(self.size.width);
self.inner[index] = Cell::empty();
}
fn put(&mut self, mut cell: Cell, pos: LocalPos) {
let index = pos.to_index(self.size.width);
if let CellState::Occupied(c) = cell.state {
if pos.x < self.size.width && c.width() >= 2 {
self.put(Cell::continuation(cell.style), LocalPos::new(pos.x + 1, pos.y));
}
}
let current = &mut self.inner[index];
cell.style.merge(current.style);
match (&mut current.state, cell.state) {
(CellState::Occupied(current_char), CellState::Occupied(new_char)) => {
*current_char = new_char;
current.style.attributes |= cell.style.attributes;
if let Some(col) = cell.style.fg {
current.style.fg = Some(col);
}
if let Some(col) = cell.style.bg {
current.style.bg = Some(col);
}
}
_ => *current = cell,
}
}
pub(super) fn cell_lines(&mut self) -> impl Iterator<Item = &mut [Cell]> {
self.inner.chunks_mut(self.size.width as usize)
}
}
impl Index<(usize, usize)> for Buffer {
type Output = Cell;
fn index(&self, pos: (usize, usize)) -> &Self::Output {
let pos = LocalPos::from(pos);
let index = pos.to_index(self.size.width);
&self.inner[index]
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum Change {
Remove,
Continuation,
Insert(Glyph),
}
impl Change {
fn width(self) -> usize {
match self {
Self::Remove => 1,
Self::Continuation => 0,
Self::Insert(c) => c.width(),
}
}
}
pub(crate) fn diff(
old: &mut Buffer,
new: &mut Buffer,
changes: &mut Vec<(LocalPos, Option<Style>, Change)>,
) -> Result<()> {
let mut previous_style = None;
for (y, (old_line, new_line)) in old.cell_lines().zip(new.cell_lines()).enumerate() {
for (x, (old_cell, new_cell)) in old_line.iter_mut().zip(new_line).enumerate() {
if old_cell == new_cell {
continue;
}
let change = match new_cell.state {
CellState::Empty => Change::Remove,
CellState::Continuation => Change::Continuation,
CellState::Occupied(c) => Change::Insert(c),
};
let style = match previous_style {
Some(previous) => (previous != new_cell.style).then_some(new_cell.style),
None => Some(new_cell.style),
};
previous_style = Some(new_cell.style);
*old_cell = *new_cell;
changes.push((LocalPos::from((x, y)), style, change));
}
}
Ok(())
}
pub(crate) fn draw_changes(
mut w: impl Write,
glyph_map: &GlyphMap,
changes: &Vec<(LocalPos, Option<Style>, Change)>,
) -> Result<()> {
let mut last_y = None;
let mut next_cell_x = None;
for (screen_pos, style, change) in changes {
let should_move = match (last_y, next_cell_x) {
(Some(last_y), Some(next_x)) => screen_pos.y > last_y || next_x != screen_pos.x,
_ => true,
};
if should_move {
w.queue(cursor::MoveTo(screen_pos.x, screen_pos.y))?;
}
last_y = Some(screen_pos.y);
next_cell_x = Some(screen_pos.x + change.width() as u16);
if let Some(style) = style {
write_style(style, &mut w)?;
}
match change {
Change::Insert(c) => match c {
Glyph::Single(c, _) => {
w.queue(Print(c))?;
}
Glyph::Cluster(index, _) => {
if let Some(glyph) = glyph_map.get(*index) {
w.queue(Print(glyph))?;
}
}
},
Change::Continuation => (), Change::Remove => {
w.queue(Print(' '))?;
}
};
}
write_style(&Style::reset(), &mut w)?;
w.queue(cursor::MoveTo(0, 0))?;
Ok(())
}
#[cfg(test)]
mod test {
use anathema_state::Color;
use super::*;
use crate::tui;
#[test]
fn changes() {
let mut changes = vec![];
let mut old_buffer = Buffer::new((5u16, 3));
old_buffer.inner[0] = Cell::new(Glyph::from_char('O', 1), Style::reset());
old_buffer.inner[1] = Cell::new(Glyph::from_char('V', 1), Style::reset());
let mut new_buffer = Buffer::new((5u16, 3));
new_buffer.inner[0] = Cell::new(Glyph::from_char('C', 1), Style::reset());
new_buffer.inner[2] = Cell::new(Glyph::from_char('N', 1), Style::reset());
diff(&mut old_buffer, &mut new_buffer, &mut changes).unwrap();
let (_, _, change_1) = changes[0]; let (_, _, change_2) = changes[1]; let (_, _, change_3) = changes[2];
assert_eq!(Change::Insert(Glyph::from_char('C', 1)), change_1);
assert_eq!(Change::Remove, change_2);
assert_eq!(Change::Insert(Glyph::from_char('N', 1)), change_3);
}
#[test]
fn resize() {
let mut buffer = Buffer::new((2u16, 2));
buffer.inner[0] = Cell::new(Glyph::from_char('1', 1), Style::reset());
buffer.inner[1] = Cell::new(Glyph::from_char('2', 1), Style::reset());
buffer.inner[2] = Cell::new(Glyph::from_char('3', 1), Style::reset());
buffer.inner[3] = Cell::new(Glyph::from_char('4', 1), Style::reset());
buffer.resize(Size::new(1, 2));
assert_eq!(buffer.inner[0], Cell::new(Glyph::from_char('1', 1), Style::reset()));
assert_eq!(buffer.inner[1], Cell::new(Glyph::from_char('3', 1), Style::reset()));
}
#[test]
fn update_cell_checks_range() {
let mut under_test = Buffer::new((1, 2));
let valid_pos = LocalPos::new(0, 1);
under_test.put_glyph(Glyph::from_char('1', 1), valid_pos);
under_test.put_glyph(Glyph::from_char('2', 1), valid_pos);
let new_style = Style {
fg: Some(Color::Red),
bg: None,
attributes: tui::Attributes::empty(),
};
under_test.update_cell(new_style, LocalPos::new(1, 0));
assert_eq!(under_test.get(valid_pos).unwrap().1.clone(), Style::reset());
}
#[test]
#[should_panic(expected = "position out of bounds")]
fn put_glyph_checks_range() {
let mut under_test = Buffer::new((1, 2));
under_test.put_glyph(Glyph::from_char('x', 1), LocalPos::new(0, 0));
under_test.put_glyph(Glyph::from_char('x', 1), LocalPos::new(0, 1));
under_test.put_glyph(Glyph::from_char('o', 1), LocalPos::new(1, 0));
}
#[test]
fn get_checks_range() {
let mut under_test = Buffer::new((1, 2));
under_test.put_glyph(Glyph::from_char('1', 1), LocalPos::new(0, 0));
under_test.put_glyph(Glyph::from_char('2', 1), LocalPos::new(0, 1));
assert_eq!(under_test.get(LocalPos::new(1, 0)), None);
}
#[test]
fn get_mut_checks_range() {
let mut under_test = Buffer::new((1, 2));
under_test.put_glyph(Glyph::from_char('1', 1), LocalPos::new(0, 0));
under_test.put_glyph(Glyph::from_char('2', 1), LocalPos::new(0, 1));
assert_eq!(under_test.get_mut(LocalPos::new(1, 0)), None);
}
}