#[allow(unused_imports)]
use crate::{
render::cell::{cellsym, cellsym_block, is_prerendered_emoji, Cell},
render::style::{Color, Style},
render::symbol_map::ascii_to_petscii,
util::{Rect, PointU16},
util::shape::{circle, line, prepare_line},
};
use bitflags::bitflags;
use log::info;
use serde::{Deserialize, Serialize};
use std::cmp::min;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
pub const SYMBOL_LINE: [&str; 37] = [
"│", "║", "┃", "─", "═", "━", "┐", "╮", "╗", "┓", "┌", "╭", "╔", "┏", "┘", "╯", "╝", "┛", "└",
"╰", "╚", "┗", "┤", "╣", "┫", "├", "╠", "┣", "┬", "╦", "┳", "┴", "╩", "┻", "┼", "╬", "╋",
];
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Borders: u32 {
const NONE = 0b0000_0001;
const TOP = 0b0000_0010;
const RIGHT = 0b0000_0100;
const BOTTOM = 0b0000_1000;
const LEFT = 0b0001_0000;
const ALL = Self::TOP.bits() | Self::RIGHT.bits() | Self::BOTTOM.bits() | Self::LEFT.bits();
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BorderType {
Plain,
Rounded,
Double,
Thick,
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub enum BufferMode {
#[default]
Tui,
Sprite,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Buffer {
pub area: Rect,
pub content: Vec<Cell>,
#[serde(default)]
pub mode: BufferMode,
}
impl Default for Buffer {
fn default() -> Self {
Buffer {
area: Rect::default(),
content: Vec::new(),
mode: BufferMode::Tui,
}
}
}
impl Buffer {
pub fn empty(area: Rect) -> Buffer {
let cell: Cell = Default::default();
Buffer::filled(area, &cell)
}
pub fn empty_sprite(area: Rect) -> Buffer {
let cell: Cell = Default::default();
Buffer::filled_with_mode(area, &cell, BufferMode::Sprite)
}
pub fn filled(area: Rect, cell: &Cell) -> Buffer {
Buffer::filled_with_mode(area, cell, BufferMode::Tui)
}
pub fn filled_with_mode(area: Rect, cell: &Cell, mode: BufferMode) -> Buffer {
let size = area.area() as usize;
let mut content = Vec::with_capacity(size);
for _ in 0..size {
content.push(cell.clone());
}
Buffer { area, content, mode }
}
pub fn is_tui(&self) -> bool {
self.mode == BufferMode::Tui
}
pub fn is_sprite(&self) -> bool {
self.mode == BufferMode::Sprite
}
pub fn set_mode(&mut self, mode: BufferMode) {
self.mode = mode;
}
pub fn with_lines<S>(lines: Vec<S>) -> Buffer
where
S: AsRef<str>,
{
let height = lines.len() as u16;
let width = lines
.iter()
.map(|i| i.as_ref().width() as u16)
.max()
.unwrap_or_default();
let mut buffer = Buffer::empty(Rect {
x: 0,
y: 0,
width,
height,
});
for (y, line) in lines.iter().enumerate() {
buffer.set_string(0, y as u16, line, Style::default());
}
buffer
}
pub fn content(&self) -> &[Cell] {
&self.content
}
pub fn area(&self) -> &Rect {
&self.area
}
pub fn get(&self, x: u16, y: u16) -> &Cell {
let i = self.index_of(x, y);
&self.content[i]
}
pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
let i = self.index_of(x, y);
&mut self.content[i]
}
pub fn index_of(&self, x: u16, y: u16) -> usize {
debug_assert!(
x >= self.area.left()
&& x < self.area.right()
&& y >= self.area.top()
&& y < self.area.bottom(),
"Trying to access position outside the buffer: x={}, y={}, area={:?}",
x,
y,
self.area
);
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
}
pub fn pos_of(&self, i: usize) -> (u16, u16) {
debug_assert!(
i < self.content.len(),
"Trying to get the coords of a cell outside the buffer: i={} len={}",
i,
self.content.len()
);
(
self.area.x + i as u16 % self.area.width,
self.area.y + i as u16 / self.area.width,
)
}
pub fn dstr<S>(&mut self, string: S)
where
S: AsRef<str>,
{
self.set_str(0, 0, string, Style::default());
}
pub fn set_str<S>(&mut self, x: u16, y: u16, string: S, style: Style)
where
S: AsRef<str>,
{
self.set_stringn(
x + self.area.x,
y + self.area.y,
string,
style,
);
}
pub fn set_string<S>(&mut self, x: u16, y: u16, string: S, style: Style)
where
S: AsRef<str>,
{
self.set_stringn(x, y, string, style);
}
pub fn set_stringn<S>(
&mut self,
x: u16,
y: u16,
string: S,
style: Style,
)
where
S: AsRef<str>,
{
if x < self.area.left() || x >= self.area.right()
|| y < self.area.top() || y >= self.area.bottom() {
return;
}
let mut index = self.index_of(x, y);
let mut x_offset = x as usize;
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
let max_offset = self.area.right() as usize;
for s in graphemes {
let width = s.width();
if width == 0 {
continue;
}
if width > max_offset.saturating_sub(x_offset) {
break;
}
let is_emoji = is_prerendered_emoji(s);
self.content[index].set_symbol(s);
self.content[index].set_style(style);
let effective_width = if is_emoji { 2 } else { width };
for i in index + 1..index + effective_width {
if i < self.content.len() {
self.content[i].reset();
}
}
index += effective_width;
x_offset += effective_width;
}
}
pub fn set_style(&mut self, area: Rect, style: Style) {
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
self.get_mut(x, y).set_style(style);
}
}
}
pub fn resize(&mut self, area: Rect) {
let length = area.area() as usize;
if self.content.len() > length {
self.content.truncate(length);
} else {
self.content.resize(length, Default::default());
}
self.area = area;
}
pub fn reset(&mut self) {
for c in &mut self.content {
c.reset();
}
}
pub fn clear_area(&mut self, area: Rect) {
let x_start = area.x.max(self.area.x);
let y_start = area.y.max(self.area.y);
let x_end = (area.x + area.width).min(self.area.x + self.area.width);
let y_end = (area.y + area.height).min(self.area.y + self.area.height);
for y in y_start..y_end {
for x in x_start..x_end {
let idx = self.index_of(x, y);
self.content[idx].reset();
}
}
}
pub fn set_fg(&mut self, color: Color) {
for c in &mut self.content {
c.set_fg(color);
}
}
pub fn set_color_str<S>(&mut self, x: u16, y: u16, string: S, fg: Color, bg: Color)
where
S: AsRef<str>,
{
self.set_str(x, y, string, Style::default().fg(fg).bg(bg));
}
pub fn set_petscii_str<S>(&mut self, x: u16, y: u16, string: S, fg: Color, bg: Color)
where
S: AsRef<str>,
{
self.set_str(x, y, ascii_to_petscii(string.as_ref()), Style::default().fg(fg).bg(bg));
}
pub fn set_default_str<S>(&mut self, string: S)
where
S: AsRef<str>,
{
self.set_str(0, 0, string, Style::default());
}
pub fn set_graph_sym(&mut self, x: u16, y: u16, texture_id: u8, sym: u8, fg: Color) {
self.set_str(
x,
y,
cellsym_block(texture_id, sym),
Style::default().fg(fg).bg(Color::Reset),
);
}
pub fn draw_circle(
&mut self,
x0: u16,
y0: u16,
radius: u16,
sym: &str,
fg_color: u8,
bg_color: u8,
) {
for p in circle(x0, y0, radius) {
if (p.0 as u16) < self.area.width && (p.1 as u16) < self.area.height {
self.set_str(
p.0 as u16,
p.1 as u16,
sym,
Style::default()
.fg(Color::Indexed(fg_color))
.bg(Color::Indexed(bg_color)),
);
}
}
}
pub fn draw_line(
&mut self,
p0: PointU16,
p1: PointU16,
sym: Option<Vec<Option<u8>>>,
fg_color: u8,
bg_color: u8,
) {
let (x0, y0, x1, y1) = prepare_line(p0.x, p0.y, p1.x, p1.y);
let mut syms: Vec<Option<u8>> = vec![None, None, Some(119), Some(116), Some(77), Some(78)];
if let Some(s) = sym {
syms = s;
}
for p in line(x0, y0, x1, y1) {
let x = p.0 as u16;
let y = p.1 as u16;
let sym = syms[p.2 as usize];
if let Some(s) = sym {
if x < self.area.width && y < self.area.height {
self.set_str(
x,
y,
cellsym_block(bg_color, s),
Style::default()
.fg(Color::Indexed(fg_color))
.bg(Color::Reset),
);
}
}
}
}
fn set_border_sym(&mut self, x: u16, y: u16, sym: &str, style: Style) {
use crate::render::symbol_map::get_layered_symbol_map;
if self.mode == BufferMode::Sprite {
if let Some(map) = get_layered_symbol_map() {
if let Some(pua) = map.petscii_lookup(sym) {
let abs_x = x + self.area.x;
let abs_y = y + self.area.y;
let index = self.index_of(abs_x, abs_y);
if index < self.content.len() {
self.content[index].set_symbol(pua);
self.content[index].set_style(style);
}
return;
}
if let Some((block, idx)) = map.reverse_lookup(sym) {
use crate::render::cell::cellsym_block;
let abs_x = x + self.area.x;
let abs_y = y + self.area.y;
let index = self.index_of(abs_x, abs_y);
if index < self.content.len() {
self.content[index].set_symbol(&cellsym_block(block, idx));
self.content[index].set_style(style);
}
}
}
} else {
self.set_str(x, y, sym, style);
}
}
pub fn set_border(&mut self, borders: Borders, border_type: BorderType, style: Style) {
let lineidx: [usize; 11] = match border_type {
BorderType::Plain => [0, 3, 6, 10, 14, 18, 22, 25, 28, 31, 34],
BorderType::Rounded => [0, 3, 7, 11, 15, 19, 22, 25, 28, 31, 34],
BorderType::Double => [1, 4, 8, 12, 16, 20, 23, 26, 29, 33, 35],
BorderType::Thick => [2, 5, 9, 13, 17, 21, 24, 27, 30, 34, 36],
};
if borders.intersects(Borders::LEFT) {
for y in 0..self.area.height {
self.set_border_sym(0, y, SYMBOL_LINE[lineidx[0]], style);
}
}
if borders.intersects(Borders::TOP) {
for x in 0..self.area.width {
self.set_border_sym(x, 0, SYMBOL_LINE[lineidx[1]], style);
}
}
if borders.intersects(Borders::RIGHT) {
let x = self.area.width - 1;
for y in 0..self.area.height {
self.set_border_sym(x, y, SYMBOL_LINE[lineidx[0]], style);
}
}
if borders.intersects(Borders::BOTTOM) {
let y = self.area.height - 1;
for x in 0..self.area.width {
self.set_border_sym(x, y, SYMBOL_LINE[lineidx[1]], style);
}
}
if borders.contains(Borders::RIGHT | Borders::BOTTOM) {
self.set_border_sym(
self.area.width - 1,
self.area.height - 1,
SYMBOL_LINE[lineidx[4]],
style,
);
}
if borders.contains(Borders::RIGHT | Borders::TOP) {
self.set_border_sym(
self.area.width - 1,
0,
SYMBOL_LINE[lineidx[2]],
style,
);
}
if borders.contains(Borders::LEFT | Borders::BOTTOM) {
self.set_border_sym(
0,
self.area.height - 1,
SYMBOL_LINE[lineidx[5]],
style,
);
}
if borders.contains(Borders::LEFT | Borders::TOP) {
self.set_border_sym(0, 0, SYMBOL_LINE[lineidx[3]], style);
}
}
#[allow(unused_variables)]
pub fn copy_cell(&mut self, pos_self: usize, other: &Buffer, alpha: u8, pos_other: usize) {
self.content[pos_self] = other.content[pos_other].clone();
#[cfg(graphics_mode)]
{
let fc = other.content[pos_other].fg.get_rgba();
if other.content[pos_other].bg != Color::Reset {
let bc = other.content[pos_other].bg.get_rgba();
self.content[pos_self].bg = Color::Rgba(bc.0, bc.1, bc.2, alpha);
}
self.content[pos_self].fg = Color::Rgba(fc.0, fc.1, fc.2, alpha);
}
}
pub fn blit(
&mut self,
dstx: u16,
dsty: u16,
other: &Buffer,
other_part: Rect,
alpha: u8,
) -> Result<(u16, u16), String> {
if dstx >= self.area.width || dsty >= self.area.height {
return Err(String::from("buffer blit:dstx, dsty too large"));
}
let oa = Rect::new(0, 0, other.area.width, other.area.height);
if !other_part.intersects(oa) {
info!(
"buffer blit:error oa = {:?} other_part = {:?}",
oa, other_part
);
return Err(String::from("buffer blit:error other_part"));
}
let bw = min(other_part.width, self.area.width - dstx);
let bh = min(other_part.height, self.area.height - dsty);
for i in 0..bh {
for j in 0..bw {
let pos_self = (self.area.width * (dsty + i) + dstx + j) as usize;
let pos_other =
(other.area.width * other_part.y + other_part.x + i * other.area.width + j) as usize;
self.copy_cell(pos_self, other, alpha, pos_other);
}
}
Ok((bw, bh))
}
pub fn merge(&mut self, other: &Buffer, alpha: u8, fast: bool) {
let area = self.area.union(other.area);
let cell: Cell = Default::default();
self.content.resize(area.area() as usize, cell.clone());
if !fast {
let size = self.area.area() as usize;
for i in (0..size).rev() {
let (x, y) = self.pos_of(i);
let k = ((y - area.y) * area.width + x - area.x) as usize;
if i != k {
self.content[k] = self.content[i].clone();
self.content[i] = cell.clone();
}
}
}
let size = other.area.area() as usize;
for i in 0..size {
let (x, y) = other.pos_of(i);
let k = ((y - area.y) * area.width + x - area.x) as usize;
if !other.content[i].is_blank() {
self.copy_cell(k, other, alpha, i);
}
}
self.area = area;
}
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
let previous_buffer = &self.content;
let next_buffer = &other.content;
let width = self.area.width;
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
let mut invalidated: usize = 0;
let mut to_skip: usize = 0;
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
if (current != previous || invalidated > 0) && to_skip == 0 {
let x = i as u16 % width;
let y = i as u16 / width;
updates.push((x, y, &next_buffer[i]));
}
to_skip = current.symbol.width().saturating_sub(1);
let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width());
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
}
updates
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_translates_to_and_from_coordinates() {
let rect = Rect::new(200, 100, 50, 80);
let buf = Buffer::empty(rect);
assert_eq!(buf.pos_of(0), (200, 100));
assert_eq!(buf.index_of(200, 100), 0);
assert_eq!(buf.pos_of(buf.content.len() - 1), (249, 179));
assert_eq!(buf.index_of(249, 179), buf.content.len() - 1);
}
}