use std::hash::{Hash, Hasher};
use std::ops::{Deref, DerefMut};
use super::style::StyleId;
#[derive(Clone, Copy, Debug)]
pub struct Cell {
pub c: char,
pub style_id: StyleId,
pub width: u8,
}
impl Cell {
#[inline]
pub fn new(c: char, style_id: StyleId, width: u8) -> Self {
Self { c, style_id, width }
}
}
impl Hash for Cell {
fn hash<H: Hasher>(&self, state: &mut H) {
self.c.hash(state);
self.style_id.hash(state);
self.width.hash(state);
}
}
impl Default for Cell {
fn default() -> Self {
Self {
c: ' ',
style_id: StyleId::default(),
width: 1,
}
}
}
#[derive(Clone, Debug)]
pub struct Row {
cells: Vec<Cell>,
combining: Vec<(u16, Vec<char>)>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Grapheme<'a> {
pub col: u16,
pub c: char,
pub combining: &'a [char],
pub width: u8,
pub style_id: StyleId,
}
impl Row {
pub fn new(cols: usize) -> Self {
Self {
cells: vec![Cell::default(); cols],
combining: Vec::new(),
}
}
pub fn from_cells(cells: Vec<Cell>) -> Self {
Self {
cells,
combining: Vec::new(),
}
}
#[inline]
pub fn combining(&self, col: u16) -> &[char] {
for &(c, ref marks) in &self.combining {
if c == col {
return marks;
}
}
&[]
}
pub fn push_combining(&mut self, col: u16, mark: char) {
for &mut (c, ref mut marks) in &mut self.combining {
if c == col {
marks.push(mark);
return;
}
}
self.combining.push((col, vec![mark]));
}
#[inline]
pub fn combining_len(&self, col: u16) -> usize {
for &(c, ref marks) in &self.combining {
if c == col {
return marks.len();
}
}
0
}
pub fn clear_combining(&mut self, col: u16) {
self.combining.retain(|&(c, _)| c != col);
}
pub fn clear_all_combining(&mut self) {
self.combining.clear();
}
pub fn clear_combining_range(&mut self, from: u16, to: u16) {
self.combining.retain(|&(c, _)| c < from || c >= to);
}
#[inline]
pub fn len(&self) -> usize {
self.cells.len()
}
pub fn content_len(&self) -> usize {
self.cells
.iter()
.rposition(|c| (c.c != ' ' && c.c != '\0') || !c.style_id.is_default())
.map(|p| p + 1)
.unwrap_or(0)
}
#[inline]
pub fn is_empty(&self) -> bool {
self.cells.is_empty()
}
pub fn resize(&mut self, new_len: usize, value: Cell) {
if new_len < self.cells.len() {
let limit = new_len as u16;
self.combining.retain(|&(c, _)| c < limit);
}
self.cells.resize(new_len, value);
}
pub fn remove(&mut self, index: usize) -> Cell {
let col = index as u16;
self.combining.retain(|&(c, _)| c != col);
for &mut (ref mut c, _) in &mut self.combining {
if *c > col {
*c -= 1;
}
}
self.cells.remove(index)
}
pub fn insert(&mut self, index: usize, cell: Cell) {
let col = index as u16;
for &mut (ref mut c, _) in &mut self.combining {
if *c >= col {
*c = c.saturating_add(1);
}
}
self.cells.insert(index, cell);
}
pub fn push(&mut self, cell: Cell) {
self.cells.push(cell);
}
pub(crate) fn fix_wide_char_orphan_at_boundary(&mut self, new_cols: usize) {
if new_cols == 0 || self.cells.len() <= new_cols {
return;
}
let last = new_cols - 1;
if self.cells[last].width == 2 {
self.cells[last] = Cell::default();
} else if last > 0 && self.cells[last].width == 0 {
self.cells[last] = Cell::default();
self.cells[last - 1] = Cell::default();
}
}
pub fn pop(&mut self) -> Option<Cell> {
if let Some(cell) = self.cells.pop() {
let col = self.cells.len() as u16;
self.combining.retain(|&(c, _)| c != col);
Some(cell)
} else {
None
}
}
pub fn iter(&self) -> std::slice::Iter<'_, Cell> {
self.cells.iter()
}
pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Cell> {
self.cells.iter_mut()
}
pub fn graphemes(&self) -> impl Iterator<Item = Grapheme<'_>> + '_ {
self.cells
.iter()
.enumerate()
.filter(|(_, cell)| cell.width != 0)
.map(|(i, cell)| Grapheme {
col: i as u16,
c: cell.c,
combining: self.combining(i as u16),
width: cell.width,
style_id: cell.style_id,
})
}
pub fn text(&self) -> String {
let content_len = self.content_len();
let mut out = String::with_capacity(content_len);
for g in self.graphemes() {
if (g.col as usize) >= content_len {
break;
}
out.push(g.c);
out.extend(g.combining.iter().copied());
}
out
}
}
impl Hash for Row {
fn hash<H: Hasher>(&self, state: &mut H) {
self.cells.hash(state);
self.combining.hash(state);
}
}
impl Deref for Row {
type Target = [Cell];
fn deref(&self) -> &[Cell] {
&self.cells
}
}
impl DerefMut for Row {
fn deref_mut(&mut self) -> &mut [Cell] {
&mut self.cells
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cell_size() {
let size = std::mem::size_of::<Cell>();
assert!(size <= 8, "Cell should be <= 8 bytes, got {size}");
}
#[test]
fn row_combining_basic() {
let mut row = Row::new(10);
assert_eq!(row.combining(0), &[] as &[char]);
row.push_combining(0, '\u{0301}');
assert_eq!(row.combining(0), &['\u{0301}']);
assert_eq!(row.combining_len(0), 1);
row.push_combining(0, '\u{0308}');
assert_eq!(row.combining_len(0), 2);
assert_eq!(row.combining(0), &['\u{0301}', '\u{0308}']);
}
#[test]
fn row_clear_combining() {
let mut row = Row::new(10);
row.push_combining(3, '\u{0301}');
assert_eq!(row.combining_len(3), 1);
row.clear_combining(3);
assert_eq!(row.combining_len(3), 0);
}
#[test]
fn row_remove_shifts_combining() {
let mut row = Row::new(10);
row.push_combining(5, '\u{0301}');
row.remove(3);
assert_eq!(row.combining(4), &['\u{0301}']);
assert_eq!(row.combining(5), &[] as &[char]);
}
#[test]
fn row_insert_shifts_combining() {
let mut row = Row::new(10);
row.push_combining(3, '\u{0301}');
row.insert(2, Cell::default());
assert_eq!(row.combining(4), &['\u{0301}']);
assert_eq!(row.combining(3), &[] as &[char]);
}
#[test]
fn row_resize_truncates_combining() {
let mut row = Row::new(10);
row.push_combining(8, '\u{0301}');
row.resize(5, Cell::default());
assert_eq!(row.combining(8), &[] as &[char]);
}
#[test]
fn row_fix_wide_char_orphan_at_boundary_width2() {
let mut row = Row::new(10);
row[4].width = 2;
row[5].width = 0;
row.fix_wide_char_orphan_at_boundary(5);
assert_eq!(row[4].c, ' ');
assert_eq!(row[4].width, 1);
}
#[test]
fn row_fix_wide_char_orphan_at_boundary_continuation() {
let mut row = Row::new(10);
row[3].width = 2;
row[4].width = 0;
row.fix_wide_char_orphan_at_boundary(5);
assert_eq!(row[3].c, ' ');
assert_eq!(row[3].width, 1);
assert_eq!(row[4].c, ' ');
assert_eq!(row[4].width, 1);
}
#[test]
fn row_fix_wide_char_orphan_noop_when_short() {
let mut row = Row::new(5);
row[2].c = 'A';
row.fix_wide_char_orphan_at_boundary(10); assert_eq!(row[2].c, 'A'); }
#[test]
fn content_len_blank_row_is_zero() {
assert_eq!(Row::new(10).content_len(), 0);
}
#[test]
fn content_len_counts_to_last_non_blank() {
let mut row = Row::new(10);
row[0].c = 'a';
row[3].c = 'b';
assert_eq!(row.content_len(), 4);
}
#[test]
fn content_len_counts_styled_blank_as_content() {
let mut row = Row::new(10);
row[5].style_id = StyleId(1);
assert_eq!(row.content_len(), 6);
}
#[test]
fn content_len_includes_wide_char_continuation() {
let mut row = Row::new(6);
row[0].c = '界';
row[0].width = 2;
row[1].c = '\0';
row[1].width = 0;
assert_eq!(row.content_len(), 1);
}
#[test]
fn graphemes_skips_wide_continuation_and_attaches_combining() {
let mut row = Row::new(6);
row[0].c = 'a';
row[1].c = '界';
row[1].width = 2;
row[2].width = 0; row[3].c = 'x';
row.push_combining(3, '\u{0301}');
let gs: Vec<_> = row.graphemes().collect();
assert_eq!(gs.len(), 5); assert_eq!((gs[0].col, gs[0].c, gs[0].width), (0, 'a', 1));
assert_eq!((gs[1].col, gs[1].c, gs[1].width), (1, '界', 2));
assert_eq!((gs[2].col, gs[2].c), (3, 'x'));
assert_eq!(gs[2].combining, &['\u{0301}']);
assert!(gs[3].combining.is_empty());
assert_eq!(gs[4].col, 5); }
#[test]
fn text_trims_trailing_blanks_and_includes_combining() {
let mut row = Row::new(10);
row[0].c = 'h';
row[1].c = '界';
row[1].width = 2;
row[2].width = 0;
row[3].c = 'i';
row.push_combining(3, '\u{0301}');
assert_eq!(row.text(), "h界i\u{0301}");
}
#[test]
fn text_blank_row_is_empty() {
assert_eq!(Row::new(10).text(), "");
}
#[test]
fn text_keeps_interior_blanks() {
let mut row = Row::new(10);
row[0].c = 'a';
row[4].c = 'b';
assert_eq!(row.text(), "a b");
}
#[test]
fn text_styled_wide_char_at_end() {
let mut row = Row::new(6);
row[0].c = '界';
row[0].width = 2;
row[0].style_id = StyleId(1);
row[1] = Cell::new('\0', StyleId(1), 0); assert_eq!(row.text(), "界");
}
#[test]
fn graphemes_carry_style_id() {
let mut row = Row::new(4);
row[0].c = 'a';
row[0].style_id = StyleId(7);
let g = row.graphemes().next().unwrap();
assert_eq!(g.style_id, StyleId(7));
}
}