use unicode_width::UnicodeWidthChar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DisplayCell {
pub glyph: Option<char>,
pub char_index: usize,
}
#[derive(Debug, Clone, Default)]
pub struct DisplayLine {
pub cells: Vec<DisplayCell>,
pub char_columns: Vec<usize>,
}
impl DisplayLine {
#[must_use]
pub fn new(line: &str, tab_width: usize) -> Self {
let tab_width = tab_width.max(1);
let mut cells = Vec::with_capacity(line.len());
let mut char_columns = Vec::with_capacity(line.len() + 1);
for (char_index, ch) in line.chars().enumerate() {
char_columns.push(cells.len());
match ch {
'\t' => {
let stop = tab_width - (cells.len() % tab_width);
for _ in 0..stop {
cells.push(DisplayCell {
glyph: Some(' '),
char_index,
});
}
}
_ => {
let (glyph, width) = match ch.width() {
Some(0) | None => ('\u{00b7}', 1),
Some(width) => (ch, width),
};
cells.push(DisplayCell {
glyph: Some(glyph),
char_index,
});
for _ in 1..width {
cells.push(DisplayCell {
glyph: None,
char_index,
});
}
}
}
}
char_columns.push(cells.len());
Self {
cells,
char_columns,
}
}
#[must_use]
pub fn width(&self) -> usize {
self.cells.len()
}
#[must_use]
pub fn column_of(&self, char_index: usize) -> usize {
self.char_columns
.get(char_index)
.copied()
.unwrap_or_else(|| self.width())
}
#[must_use]
pub fn wrap(&self, width: usize) -> Vec<(usize, usize)> {
if width == 0 || self.cells.len() <= width {
return vec![(0, self.cells.len())];
}
let mut rows = Vec::new();
let mut start = 0;
while start < self.cells.len() {
let limit = start + width;
if limit >= self.cells.len() {
rows.push((start, self.cells.len()));
break;
}
let end = self.cells[start..limit]
.iter()
.rposition(|cell| cell.glyph == Some(' '))
.map(|offset| start + offset + 1)
.filter(|&candidate| candidate > start)
.unwrap_or(limit);
rows.push((start, end));
start = end;
}
rows
}
}
#[cfg(test)]
mod tests {
use super::*;
fn glyphs(line: &DisplayLine) -> String {
line.cells.iter().filter_map(|c| c.glyph).collect()
}
#[test]
fn tabs_advance_to_the_next_tab_stop() {
let line = DisplayLine::new("\tx", 4);
assert_eq!(line.width(), 5);
assert_eq!(glyphs(&line), " x");
assert_eq!(line.column_of(1), 4);
}
#[test]
fn a_tab_after_text_fills_only_the_remaining_columns() {
let line = DisplayLine::new("ab\tc", 4);
assert_eq!(line.column_of(3), 4);
assert_eq!(line.width(), 5);
}
#[test]
fn wide_glyphs_occupy_two_columns() {
let line = DisplayLine::new("a漢b", 4);
assert_eq!(line.width(), 4);
assert_eq!(line.column_of(2), 3);
assert_eq!(line.cells[2].glyph, None);
assert_eq!(line.cells[2].char_index, 1);
}
#[test]
fn control_characters_get_a_visible_placeholder() {
let line = DisplayLine::new("a\u{7}b", 4);
assert_eq!(glyphs(&line), "a\u{00b7}b");
}
#[test]
fn the_caret_position_past_the_end_is_addressable() {
let line = DisplayLine::new("abc", 4);
assert_eq!(line.column_of(3), 3);
assert_eq!(line.column_of(99), 3);
}
#[test]
fn a_short_line_wraps_to_a_single_row() {
let line = DisplayLine::new("short", 4);
assert_eq!(line.wrap(20), vec![(0, 5)]);
}
#[test]
fn an_empty_line_still_occupies_one_row() {
assert_eq!(DisplayLine::new("", 4).wrap(20), vec![(0, 0)]);
}
#[test]
fn wrapping_breaks_after_a_space() {
let line = DisplayLine::new("aaa bbb ccc", 4);
assert_eq!(line.wrap(8), vec![(0, 8), (8, 11)]);
}
#[test]
fn a_word_longer_than_the_window_is_broken_hard() {
let line = DisplayLine::new("aaaaaaaaaa", 4);
assert_eq!(line.wrap(4), vec![(0, 4), (4, 8), (8, 10)]);
}
#[test]
fn wrapping_covers_every_cell_exactly_once() {
let line = DisplayLine::new("the quick brown fox jumps over it", 4);
let rows = line.wrap(10);
assert_eq!(rows.first().map(|r| r.0), Some(0));
assert_eq!(rows.last().map(|r| r.1), Some(line.width()));
for pair in rows.windows(2) {
assert_eq!(pair[0].1, pair[1].0);
}
}
}