use crate::types::MIN_COL_WIDTH_PX;
#[derive(Clone, Debug)]
pub struct ColumnWidths {
widths: Vec<f64>,
cum_offsets: Vec<f64>,
default_width: f64,
}
impl ColumnWidths {
pub fn new(num_columns: usize, default_px: f64) -> Self {
let widths = vec![default_px; num_columns];
let cum_offsets = Self::build_cumulative(&widths);
Self {
widths,
cum_offsets,
default_width: default_px,
}
}
pub fn len(&self) -> usize {
self.widths.len()
}
pub fn is_empty(&self) -> bool {
self.widths.is_empty()
}
pub fn width(&self, col: usize) -> f64 {
self.widths.get(col).copied().unwrap_or(self.default_width)
}
pub fn set_width(&mut self, col: usize, px: f64) {
if col < self.widths.len() {
self.widths[col] = px.max(MIN_COL_WIDTH_PX);
self.cum_offsets = Self::build_cumulative(&self.widths);
}
}
pub fn total_width(&self) -> f64 {
self.cum_offsets.last().copied().unwrap_or(0.0)
}
pub fn left_offset(&self, col: usize) -> f64 {
self.cum_offsets.get(col).copied().unwrap_or(0.0)
}
pub fn visible_range(
&self,
scroll_left: f64,
viewport_width: f64,
buffer: usize,
) -> (usize, usize) {
if self.widths.is_empty() {
return (0, 0);
}
let n = self.widths.len();
let first_raw = self
.cum_offsets
.partition_point(|&offset| offset <= scroll_left)
.saturating_sub(1);
let right_edge = scroll_left + viewport_width;
let last_raw = self
.cum_offsets
.partition_point(|&offset| offset < right_edge)
.min(n);
let first = first_raw.saturating_sub(buffer);
let last = (last_raw + buffer).min(n);
(first, last - first)
}
fn build_cumulative(widths: &[f64]) -> Vec<f64> {
let mut cum = Vec::with_capacity(widths.len() + 1);
cum.push(0.0);
let mut acc = 0.0;
for &w in widths {
acc += w;
cum.push(acc);
}
cum
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cumulative_offsets_correct() {
let cw = ColumnWidths::new(4, 100.0);
assert_eq!(cw.cum_offsets, vec![0.0, 100.0, 200.0, 300.0, 400.0]);
assert!((cw.total_width() - 400.0).abs() < f64::EPSILON);
assert!((cw.left_offset(0) - 0.0).abs() < f64::EPSILON);
assert!((cw.left_offset(2) - 200.0).abs() < f64::EPSILON);
}
#[test]
fn set_width_rebuilds_offsets() {
let mut cw = ColumnWidths::new(3, 100.0);
cw.set_width(1, 200.0);
assert_eq!(cw.cum_offsets, vec![0.0, 100.0, 300.0, 400.0]);
assert!((cw.total_width() - 400.0).abs() < f64::EPSILON);
}
#[test]
fn visible_range_no_scroll() {
let cw = ColumnWidths::new(20, 100.0); let (first, count) = cw.visible_range(0.0, 500.0, 2);
assert_eq!(first, 0);
assert_eq!(count, 7);
}
#[test]
fn visible_range_scrolled() {
let cw = ColumnWidths::new(20, 100.0);
let (first, count) = cw.visible_range(500.0, 300.0, 2);
assert_eq!(first, 3);
assert_eq!(count, 7);
}
#[test]
fn visible_range_end_clamped() {
let cw = ColumnWidths::new(10, 100.0); let (first, count) = cw.visible_range(800.0, 400.0, 2);
assert_eq!(first, 6); assert!(first + count <= 10);
}
#[test]
fn visible_range_empty() {
let cw = ColumnWidths::new(0, 100.0);
assert_eq!(cw.visible_range(0.0, 500.0, 2), (0, 0));
}
#[test]
fn out_of_bounds_returns_default() {
let cw = ColumnWidths::new(2, 80.0);
assert!((cw.width(99) - 80.0).abs() < f64::EPSILON);
}
#[test]
fn set_width_out_of_bounds_is_noop() {
let mut cw = ColumnWidths::new(2, 100.0);
cw.set_width(99, 500.0);
assert!((cw.total_width() - 200.0).abs() < f64::EPSILON);
}
#[test]
fn empty() {
let cw = ColumnWidths::new(0, 100.0);
assert!(cw.is_empty());
assert!((cw.total_width() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn set_width_enforces_minimum() {
let mut cw = ColumnWidths::new(3, 100.0);
cw.set_width(1, 10.0); assert!((cw.width(1) - MIN_COL_WIDTH_PX).abs() < f64::EPSILON);
}
}