use crate::base::Rgba;
use unicode_width::UnicodeWidthStr;
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Attrs(pub u16);
impl Attrs {
pub const BOLD: u16 = 1 << 0;
pub const DIM: u16 = 1 << 1;
pub const ITALIC: u16 = 1 << 2;
pub const UNDERLINE: u16 = 1 << 3;
pub const REVERSE: u16 = 1 << 4;
pub const STRIKE: u16 = 1 << 5;
pub const BLINK: u16 = 1 << 6;
pub const HIDDEN: u16 = 1 << 7;
pub const UNDERCURL: u16 = 1 << 8;
pub const fn contains(self, bit: u16) -> bool {
self.0 & bit != 0
}
pub fn set(&mut self, bit: u16, on: bool) {
if on {
self.0 |= bit;
} else {
self.0 &= !bit;
}
}
pub const fn is_empty(self) -> bool {
self.0 == 0
}
pub fn label(self) -> String {
let mut s = String::new();
for (bit, ch) in [
(Self::BOLD, 'B'),
(Self::DIM, 'D'),
(Self::ITALIC, 'I'),
(Self::UNDERLINE, 'U'),
(Self::REVERSE, 'R'),
(Self::STRIKE, 'S'),
(Self::BLINK, 'K'),
(Self::HIDDEN, 'H'),
(Self::UNDERCURL, 'W'),
] {
if self.contains(bit) {
s.push(ch);
}
}
s
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Paint {
pub fg: Option<Rgba>,
pub bg: Option<Rgba>,
pub ul: Option<Rgba>,
pub attrs: Attrs,
pub link: Option<u32>,
}
impl Paint {
pub fn is_default(self) -> bool {
self == Paint::default()
}
pub fn erase_paint(self) -> Paint {
Paint {
bg: self.bg,
..Paint::default()
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum CellContent {
#[default]
Blank,
Text(String),
Continuation,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct VtCell {
pub content: CellContent,
pub paint: Paint,
}
impl VtCell {
pub fn display(&self) -> &str {
match &self.content {
CellContent::Blank => " ",
CellContent::Text(s) => s,
CellContent::Continuation => "",
}
}
pub fn ch(&self) -> char {
match &self.content {
CellContent::Blank => ' ',
CellContent::Text(s) => s.chars().next().unwrap_or(' '),
CellContent::Continuation => '\0',
}
}
pub fn is_wide_leader(&self) -> bool {
match &self.content {
CellContent::Text(s) => s.width() >= 2,
_ => false,
}
}
pub fn is_continuation(&self) -> bool {
matches!(self.content, CellContent::Continuation)
}
}
pub struct Grid {
pub w: i32,
pub h: i32,
rows: Vec<Vec<VtCell>>,
}
impl Grid {
pub fn new(w: i32, h: i32) -> Grid {
let w = w.max(1);
let h = h.max(1);
Grid {
w,
h,
rows: (0..h)
.map(|_| (0..w).map(|_| VtCell::default()).collect())
.collect(),
}
}
pub fn cell(&self, x: i32, y: i32) -> Option<&VtCell> {
if x < 0 || y < 0 || x >= self.w || y >= self.h {
return None;
}
Some(&self.rows[y as usize][x as usize])
}
fn cell_mut(&mut self, x: i32, y: i32) -> Option<&mut VtCell> {
if x < 0 || y < 0 || x >= self.w || y >= self.h {
return None;
}
Some(&mut self.rows[y as usize][x as usize])
}
fn blank_content(&mut self, x: i32, y: i32) {
if let Some(c) = self.cell_mut(x, y) {
c.content = CellContent::Blank;
}
}
fn repair_for_write(&mut self, x: i32, y: i32) {
let (is_cont, is_leader) = match self.cell(x, y) {
Some(c) => (c.is_continuation(), c.is_wide_leader()),
None => return,
};
if is_cont {
self.blank_content(x - 1, y);
}
if is_leader {
self.blank_content(x + 1, y);
}
}
fn set_char_reusing(&mut self, x: i32, y: i32, ch: char, paint: Paint) {
if let Some(c) = self.cell_mut(x, y) {
match &mut c.content {
CellContent::Text(s) => {
s.clear();
s.push(ch);
}
_ => c.content = CellContent::Text(ch.to_string()),
}
c.paint = paint;
}
}
pub fn put_narrow(&mut self, x: i32, y: i32, ch: char, paint: Paint) {
if self.cell(x, y).is_none() {
return;
}
self.repair_for_write(x, y);
self.set_char_reusing(x, y, ch, paint);
}
pub fn put_wide(&mut self, x: i32, y: i32, ch: char, paint: Paint) {
if x + 1 >= self.w || self.cell(x, y).is_none() {
return;
}
self.repair_for_write(x, y);
self.repair_for_write(x + 1, y);
self.set_char_reusing(x, y, ch, paint);
if let Some(c) = self.cell_mut(x + 1, y) {
c.content = CellContent::Continuation;
c.paint = paint;
}
}
pub fn widen_to_wide(&mut self, x: i32, y: i32) -> bool {
let needs = self.cell(x, y).map(|c| c.is_wide_leader()).unwrap_or(false)
&& !self
.cell(x + 1, y)
.map(|c| c.is_continuation())
.unwrap_or(false);
if !needs {
return false;
}
if x + 1 >= self.w {
self.blank_content(x, y);
return false;
}
self.repair_for_write(x + 1, y);
let paint = self.cell(x, y).map(|c| c.paint).unwrap_or_default();
if let Some(c) = self.cell_mut(x + 1, y) {
c.content = CellContent::Continuation;
c.paint = paint;
}
true
}
pub fn append_combining(&mut self, x: i32, y: i32, ch: char) -> bool {
let target_x = match self.cell(x, y) {
Some(c) if c.is_continuation() => x - 1,
Some(_) => x,
None => return false,
};
match self.cell_mut(target_x, y) {
Some(VtCell {
content: CellContent::Text(s),
..
}) => {
s.push(ch);
true
}
_ => false,
}
}
pub fn erase_row_range(&mut self, y: i32, x0: i32, x1: i32, erase: Paint) {
let x0 = x0.clamp(0, self.w);
let x1 = x1.clamp(0, self.w);
if y < 0 || y >= self.h || x0 >= x1 {
return;
}
if self
.cell(x0, y)
.map(|c| c.is_continuation())
.unwrap_or(false)
{
self.blank_content(x0 - 1, y);
}
if self
.cell(x1 - 1, y)
.map(|c| c.is_wide_leader())
.unwrap_or(false)
{
self.blank_content(x1, y);
}
for x in x0..x1 {
if let Some(c) = self.cell_mut(x, y) {
c.content = CellContent::Blank;
c.paint = erase;
}
}
}
fn blank_row(&self, erase: Paint) -> Vec<VtCell> {
(0..self.w)
.map(|_| VtCell {
content: CellContent::Blank,
paint: erase,
})
.collect()
}
pub fn scroll_up(&mut self, n: i32, erase: Paint) {
self.scroll_up_region(0, self.h - 1, n, erase);
}
pub fn scroll_down(&mut self, n: i32, erase: Paint) {
self.scroll_down_region(0, self.h - 1, n, erase);
}
pub fn scroll_up_region(&mut self, top: i32, bottom: i32, n: i32, erase: Paint) {
let top = top.clamp(0, self.h - 1) as usize;
let bottom = bottom.clamp(0, self.h - 1) as usize;
if bottom < top {
return;
}
let span = bottom - top + 1;
let n = (n.max(0) as usize).min(span);
if n == 0 {
return;
}
let blank = self.blank_row(erase);
self.rows[top..=bottom].rotate_left(n);
for row in &mut self.rows[bottom + 1 - n..=bottom] {
*row = blank.clone();
}
}
pub fn scroll_down_region(&mut self, top: i32, bottom: i32, n: i32, erase: Paint) {
let top = top.clamp(0, self.h - 1) as usize;
let bottom = bottom.clamp(0, self.h - 1) as usize;
if bottom < top {
return;
}
let span = bottom - top + 1;
let n = (n.max(0) as usize).min(span);
if n == 0 {
return;
}
self.rows[top..=bottom].rotate_right(n);
let blank = self.blank_row(erase);
for row in &mut self.rows[top..top + n] {
*row = blank.clone();
}
}
pub fn insert_lines(&mut self, at: i32, bottom: i32, n: i32, erase: Paint) {
self.scroll_down_region(at, bottom, n, erase);
}
pub fn delete_lines(&mut self, at: i32, bottom: i32, n: i32, erase: Paint) {
self.scroll_up_region(at, bottom, n, erase);
}
pub fn clear_all(&mut self, erase: Paint) {
for y in 0..self.h {
self.erase_row_range(y, 0, self.w, erase);
}
}
pub fn row_text(&self, y: i32) -> String {
let mut s = String::new();
for x in 0..self.w {
if let Some(c) = self.cell(x, y) {
s.push_str(c.display());
}
}
s
}
}
#[cfg(test)]
mod tests {
use super::*;
fn red() -> Paint {
Paint {
fg: Some(Rgba::rgb(255, 0, 0)),
..Paint::default()
}
}
#[test]
fn wide_pair_repair_on_overwrite() {
let mut g = Grid::new(6, 1);
g.put_wide(1, 0, '世', red());
assert!(g.cell(1, 0).unwrap().is_wide_leader());
assert!(g.cell(2, 0).unwrap().is_continuation());
g.put_narrow(2, 0, 'x', Paint::default());
assert_eq!(g.cell(1, 0).unwrap().content, CellContent::Blank);
assert_eq!(g.cell(2, 0).unwrap().ch(), 'x');
}
#[test]
fn erase_range_repairs_boundaries() {
let mut g = Grid::new(6, 1);
g.put_wide(0, 0, '你', red());
g.put_wide(2, 0, '好', red());
g.erase_row_range(0, 1, 3, Paint::default());
assert_eq!(g.cell(0, 0).unwrap().content, CellContent::Blank);
assert_eq!(g.cell(3, 0).unwrap().content, CellContent::Blank);
}
#[test]
fn combining_appends_to_leader() {
let mut g = Grid::new(4, 1);
g.put_narrow(0, 0, 'e', Paint::default());
assert!(g.append_combining(0, 0, '\u{0301}'));
assert_eq!(g.cell(0, 0).unwrap().display(), "e\u{0301}");
assert!(!g.append_combining(3, 0, '\u{0301}'));
}
}