pub(crate) const TAB_WIDTH: usize = 4;
const H_MARGIN: usize = 4;
pub(crate) fn visual_col(line: &str, col: usize) -> usize {
line.chars().take(col).fold(0, advance)
}
pub(crate) fn advance(v: usize, c: char) -> usize {
if c == '\t' {
(v / TAB_WIDTH + 1) * TAB_WIDTH
} else {
v + 1
}
}
pub(crate) fn line_width(line: &str) -> usize {
line.chars().fold(0, advance)
}
pub(crate) fn col_at_visual(line: &str, v: f32) -> usize {
let mut cell = 0;
for (col, c) in line.chars().enumerate() {
let next = advance(cell, c);
if v < (cell + next) as f32 * 0.5 {
return col;
}
cell = next;
}
line.chars().count()
}
pub(crate) fn display_slice(line: &str, left: usize, width: usize) -> String {
let mut out = String::new();
let mut cell = 0;
for c in line.chars() {
let next = advance(cell, c);
let glyph = if c == '\t' || c.is_ascii_graphic() || c == ' ' {
if c == '\t' { ' ' } else { c }
} else {
'?'
};
for v in cell..next {
if v >= left + width {
return out;
}
if v >= left {
out.push(glyph);
}
}
cell = next;
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct Scroll {
pub(crate) top: usize,
pub(crate) left: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ViewSize {
pub(crate) rows: usize,
pub(crate) cols: usize,
}
impl Default for ViewSize {
fn default() -> Self {
Self { rows: 1, cols: 1 }
}
}
impl Scroll {
pub(crate) fn reveal(&mut self, line: usize, vcol: usize, view: ViewSize) {
let rows = view.rows.max(1);
if line < self.top {
self.top = line;
} else if line >= self.top + rows {
self.top = line + 1 - rows;
}
let cols = view.cols.max(1);
let margin = H_MARGIN.min(cols / 4);
if vcol < self.left + margin {
self.left = vcol.saturating_sub(margin);
} else if vcol + margin >= self.left + cols {
self.left = vcol + margin + 1 - cols;
}
}
pub(crate) fn clamp(&mut self, line_count: usize, widest: usize, view: ViewSize) {
self.top = self.top.min(line_count.saturating_sub(view.rows.max(1)));
self.left = self.left.min((widest + 1).saturating_sub(view.cols.max(1)));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tabs_expand_to_the_next_stop() {
assert_eq!(visual_col("\tx", 1), 4);
assert_eq!(visual_col("ab\tx", 3), 4);
assert_eq!(visual_col("abcd\tx", 5), 8);
assert_eq!(line_width("a\tb"), 5);
assert_eq!(visual_col("héllo", 2), 2, "one cell per character");
}
#[test]
fn col_at_visual_rounds_to_the_nearer_boundary() {
assert_eq!(col_at_visual("abc", 0.4), 0);
assert_eq!(col_at_visual("abc", 0.6), 1);
assert_eq!(
col_at_visual("abc", 9.0),
3,
"past the end lands at the end"
);
assert_eq!(col_at_visual("\tx", 1.5), 0);
assert_eq!(col_at_visual("\tx", 2.5), 1);
assert_eq!(col_at_visual("", 3.0), 0);
}
#[test]
fn display_slice_windows_cells_and_expands_tabs() {
assert_eq!(display_slice("abcdef", 2, 3), "cde");
assert_eq!(display_slice("\tx", 0, 8), " x");
assert_eq!(display_slice("\tx", 2, 8), " x", "a tab cut by the window");
assert_eq!(
display_slice("aé!", 0, 8),
"a?!",
"undrawable characters keep their cell"
);
assert_eq!(display_slice("abc", 5, 4), "");
}
#[test]
fn reveal_scrolls_the_least_distance() {
let view = ViewSize { rows: 10, cols: 40 };
let mut s = Scroll::default();
s.reveal(25, 0, view);
assert_eq!(s.top, 16, "the line lands on the last row");
s.reveal(3, 0, view);
assert_eq!(s.top, 3, "the line lands on the first row");
s.reveal(5, 0, view);
assert_eq!(s.top, 3, "a visible line does not scroll");
}
#[test]
fn reveal_keeps_a_margin_horizontally() {
let view = ViewSize { rows: 10, cols: 40 };
let mut s = Scroll::default();
s.reveal(0, 50, view);
assert_eq!(s.left, 50 + 4 + 1 - 40);
s.reveal(0, 12, view);
assert_eq!(s.left, 8);
s.reveal(0, 2, view);
assert_eq!(s.left, 0);
}
#[test]
fn clamp_keeps_the_window_over_the_text() {
let view = ViewSize { rows: 10, cols: 40 };
let mut s = Scroll {
top: 100,
left: 100,
};
s.clamp(25, 60, view);
assert_eq!(s, Scroll { top: 15, left: 21 });
s.clamp(3, 10, view);
assert_eq!(s, Scroll::default(), "a short text never scrolls");
}
}