#[derive(Debug, Clone)]
pub(crate) struct VisualRowCache {
pub(crate) width: usize,
pub(crate) visual_rows_per_line: Vec<usize>,
pub(crate) visual_row_prefix_sum: Vec<usize>,
}
impl VisualRowCache {
pub(crate) fn build<F>(line_count: usize, width: usize, mut rows_for: F) -> Self
where
F: FnMut(usize) -> usize,
{
let mut per_line = Vec::with_capacity(line_count);
let mut prefix = Vec::with_capacity(line_count + 1);
prefix.push(0usize);
let mut acc = 0usize;
for idx in 0..line_count {
let rows = rows_for(idx).max(1);
per_line.push(rows);
acc = acc.saturating_add(rows);
prefix.push(acc);
}
Self {
width,
visual_rows_per_line: per_line,
visual_row_prefix_sum: prefix,
}
}
pub(crate) fn width(&self) -> usize {
self.width
}
pub(crate) fn for_line(&self, idx: usize) -> usize {
self.visual_rows_per_line.get(idx).copied().unwrap_or(1)
}
pub(crate) fn before(&self, idx: usize) -> usize {
let clamped = idx.min(self.visual_rows_per_line.len());
self.visual_row_prefix_sum
.get(clamped)
.copied()
.unwrap_or(0)
}
#[allow(dead_code)]
pub(crate) fn between(&self, first: usize, last: usize) -> usize {
if first > last || self.visual_rows_per_line.is_empty() {
return 0;
}
let last = last.min(self.visual_rows_per_line.len() - 1);
self.before(last + 1).saturating_sub(self.before(first))
}
pub(crate) fn total(&self) -> usize {
self.before(self.visual_rows_per_line.len())
}
pub(crate) fn find_visual_row(&self, visual_row: usize) -> (usize, usize) {
let total = self.total();
if visual_row >= total {
return (self.visual_rows_per_line.len(), 0);
}
let target = visual_row + 1;
let upper = self.visual_row_prefix_sum.partition_point(|&p| p < target);
let line = upper.saturating_sub(1);
let start = self.visual_row_prefix_sum[line];
(line, visual_row - start)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_cache_reports_zero_total() {
let cache = VisualRowCache::build(0, 80, |_| 1);
assert_eq!(cache.total(), 0);
assert_eq!(cache.before(0), 0);
assert_eq!(cache.find_visual_row(0), (0, 0));
}
#[test]
fn prefix_sum_matches_per_line_counts() {
let counts = [1usize, 2, 1, 3, 1];
let cache = VisualRowCache::build(counts.len(), 80, |i| counts[i]);
assert_eq!(cache.total(), 8);
assert_eq!(cache.before(0), 0);
assert_eq!(cache.before(1), 1);
assert_eq!(cache.before(3), 4);
assert_eq!(cache.before(5), 8);
assert_eq!(cache.between(1, 3), 2 + 1 + 3);
assert_eq!(cache.for_line(2), 1);
}
#[test]
fn find_visual_row_lands_on_correct_line_and_subrow() {
let counts = [2usize, 1, 3];
let cache = VisualRowCache::build(counts.len(), 80, |i| counts[i]);
assert_eq!(cache.find_visual_row(0), (0, 0));
assert_eq!(cache.find_visual_row(1), (0, 1));
assert_eq!(cache.find_visual_row(2), (1, 0));
assert_eq!(cache.find_visual_row(3), (2, 0));
assert_eq!(cache.find_visual_row(4), (2, 1));
assert_eq!(cache.find_visual_row(5), (2, 2));
assert_eq!(cache.find_visual_row(6), (3, 0));
assert_eq!(cache.find_visual_row(100), (3, 0));
}
#[test]
fn rows_for_zero_is_clamped_to_one() {
let cache = VisualRowCache::build(2, 80, |_| 0);
assert_eq!(cache.for_line(0), 1);
assert_eq!(cache.total(), 2);
}
}