pub const MIN_COL_WIDTH: u16 = 4;
pub const MAX_COL_WIDTH: u16 = 200;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum SlackPolicy {
#[default]
None,
RightmostLimited,
}
pub struct ColumnWidthConfig<'a> {
pub ideal_widths: &'a [u16],
pub min_widths: &'a [u16],
}
pub struct SelectionContext {
pub horizontal_offset: usize,
pub available_width: u16,
pub fixed_count: Option<usize>,
pub max_offset: usize,
pub slack_policy: SlackPolicy,
}
fn total_width_with_separators(widths: &[u16]) -> u16 {
let sum: u16 = widths.iter().sum();
let separators = if widths.len() > 1 {
(widths.len() - 1) as u16
} else {
0
};
sum + separators
}
fn shrink_columns(
widths: &mut [u16],
min_widths: &[u16],
indices: &[usize],
mut excess: u16,
from_left: bool,
) -> u16 {
let len = widths.len();
if len == 0 {
return excess;
}
if from_left {
for viewport_idx in 0..len {
if excess == 0 {
break;
}
let col_idx = indices[viewport_idx];
let min_w = min_widths.get(col_idx).copied().unwrap_or(MIN_COL_WIDTH);
let shrinkable = widths[viewport_idx].saturating_sub(min_w);
let shrink = shrinkable.min(excess);
widths[viewport_idx] -= shrink;
excess -= shrink;
}
} else {
for viewport_idx in (0..len).rev() {
if excess == 0 {
break;
}
let col_idx = indices[viewport_idx];
let min_w = min_widths.get(col_idx).copied().unwrap_or(MIN_COL_WIDTH);
let shrinkable = widths[viewport_idx].saturating_sub(min_w);
let shrink = shrinkable.min(excess);
widths[viewport_idx] -= shrink;
excess -= shrink;
}
}
excess
}
fn apply_slack_to_rightmost(widths: &mut [u16], available_width: u16) {
if widths.is_empty() {
return;
}
let current_total = total_width_with_separators(widths);
if current_total >= available_width {
return;
}
let slack = available_width - current_total;
if let Some(rightmost) = widths.last_mut() {
*rightmost += slack;
}
}
fn try_add_bonus_column(
config: &ColumnWidthConfig,
indices: &mut Vec<usize>,
widths: &mut Vec<u16>,
available_width: u16,
) -> bool {
let Some(&rightmost_idx) = indices.last() else {
return false;
};
let next_idx = rightmost_idx + 1;
if next_idx >= config.ideal_widths.len() {
return false;
}
let current_total = total_width_with_separators(widths);
let slack = available_width.saturating_sub(current_total);
let next_ideal_width = config.ideal_widths[next_idx];
let needed = next_ideal_width + 1;
if slack >= needed {
indices.push(next_idx);
widths.push(next_ideal_width);
return true;
}
false
}
pub fn select_viewport_columns(
config: &ColumnWidthConfig,
ctx: &SelectionContext,
) -> (Vec<usize>, Vec<u16>) {
if config.ideal_widths.is_empty() || ctx.horizontal_offset >= config.ideal_widths.len() {
return (Vec::new(), Vec::new());
}
let (indices, mut widths) = match ctx.fixed_count {
Some(count) => select_fixed_columns(config, ctx, count),
None => select_dynamic_columns(config, ctx.horizontal_offset, ctx.available_width),
};
if ctx.slack_policy == SlackPolicy::RightmostLimited {
apply_slack_to_rightmost(&mut widths, ctx.available_width);
}
(indices, widths)
}
fn select_fixed_columns(
config: &ColumnWidthConfig,
ctx: &SelectionContext,
count: usize,
) -> (Vec<usize>, Vec<u16>) {
let end = (ctx.horizontal_offset + count).min(config.ideal_widths.len());
let mut indices: Vec<usize> = (ctx.horizontal_offset..end).collect();
if indices.is_empty() {
return (Vec::new(), Vec::new());
}
let mut widths: Vec<u16> = indices.iter().map(|&i| config.ideal_widths[i]).collect();
if ctx.max_offset > 0 {
try_add_bonus_column(config, &mut indices, &mut widths, ctx.available_width);
}
let total_needed = total_width_with_separators(&widths);
if total_needed > ctx.available_width {
let excess = total_needed - ctx.available_width;
let at_right_edge = ctx.horizontal_offset >= ctx.max_offset && ctx.max_offset > 0;
let remaining = shrink_columns(
&mut widths,
config.min_widths,
&indices,
excess,
at_right_edge,
);
if at_right_edge && remaining > 0 && indices.len() > 1 {
indices.remove(0);
widths.remove(0);
if let Some(last_idx) = indices.last()
&& let Some(last_w) = widths.last_mut()
{
*last_w = config.ideal_widths[*last_idx];
}
let new_total = total_width_with_separators(&widths);
if new_total > ctx.available_width {
let new_excess = new_total - ctx.available_width;
shrink_columns(&mut widths, config.min_widths, &indices, new_excess, true);
}
}
}
(indices, widths)
}
fn select_dynamic_columns(
config: &ColumnWidthConfig,
horizontal_offset: usize,
available_width: u16,
) -> (Vec<usize>, Vec<u16>) {
let mut indices = Vec::new();
let mut widths = Vec::new();
let mut used_width: u16 = 0;
for (i, &width) in config
.ideal_widths
.iter()
.enumerate()
.skip(horizontal_offset)
{
let separator = u16::from(!indices.is_empty());
let needed = width + separator;
if used_width + needed <= available_width {
used_width += needed;
indices.push(i);
widths.push(width);
} else {
let remaining = available_width.saturating_sub(used_width + separator);
let min_w = config.min_widths.get(i).copied().unwrap_or(MIN_COL_WIDTH);
if remaining >= min_w {
indices.push(i);
widths.push(remaining);
}
break;
}
}
if indices.is_empty() && horizontal_offset < config.ideal_widths.len() {
indices.push(horizontal_offset);
widths.push(config.ideal_widths[horizontal_offset].min(available_width));
}
(indices, widths)
}
pub fn calculate_viewport_column_count(
ideal_widths: &[u16],
min_widths: &[u16],
available_width: u16,
) -> usize {
if ideal_widths.is_empty() {
return 0;
}
for n in (1..=ideal_widths.len()).rev() {
let all_windows_fit = (0..=ideal_widths.len() - n).all(|start| {
let window = &ideal_widths[start..start + n];
total_width_with_separators(window) <= available_width
});
if all_windows_fit {
return n;
}
}
for n in (1..=min_widths.len()).rev() {
let all_windows_fit = (0..=min_widths.len() - n).all(|start| {
let window = &min_widths[start..start + n];
total_width_with_separators(window) <= available_width
});
if all_windows_fit {
return n;
}
}
1
}
#[derive(Debug, Clone, Default)]
pub struct ViewportPlan {
pub column_count: usize,
pub max_offset: usize,
pub available_width: u16,
pub min_widths_sum: u16,
pub ideal_widths_sum: u16,
pub ideal_widths_max: u16,
pub slack_policy: SlackPolicy,
}
impl ViewportPlan {
pub fn calculate(ideal_widths: &[u16], min_widths: &[u16], available_width: u16) -> Self {
let column_count =
calculate_viewport_column_count(ideal_widths, min_widths, available_width);
let max_offset = calculate_max_offset(ideal_widths.len(), column_count);
let min_widths_sum = min_widths.iter().sum();
let ideal_widths_sum = ideal_widths.iter().sum();
let ideal_widths_max = ideal_widths.iter().copied().max().unwrap_or(0);
let slack_policy = if max_offset == 0 {
SlackPolicy::RightmostLimited
} else {
SlackPolicy::None
};
Self {
column_count,
max_offset,
available_width,
min_widths_sum,
ideal_widths_sum,
ideal_widths_max,
slack_policy,
}
}
pub fn needs_recalculation(
&self,
new_widths_len: usize,
new_available_width: u16,
new_min_widths_sum: u16,
new_ideal_widths_sum: u16,
new_ideal_widths_max: u16,
) -> bool {
self.column_count == 0
|| self.available_width != new_available_width
|| self.max_offset + self.column_count != new_widths_len
|| self.min_widths_sum != new_min_widths_sum
|| self.ideal_widths_sum != new_ideal_widths_sum
|| self.ideal_widths_max != new_ideal_widths_max
}
}
pub fn calculate_max_offset(all_widths_len: usize, viewport_column_count: usize) -> usize {
all_widths_len.saturating_sub(viewport_column_count)
}
pub fn calculate_next_column_offset(
all_widths_len: usize,
current_offset: usize,
viewport_column_count: usize,
) -> usize {
let max_offset = calculate_max_offset(all_widths_len, viewport_column_count);
(current_offset + 1).min(max_offset)
}
pub fn calculate_prev_column_offset(current_offset: usize) -> usize {
current_offset.saturating_sub(1)
}
#[derive(Debug, Clone, Default)]
pub struct ColumnWidthsCache {
pub ideal_widths: Vec<u16>,
pub header_min_widths: Vec<u16>,
generation: u64,
history_index: Option<usize>,
}
impl ColumnWidthsCache {
pub fn new(
ideal_widths: Vec<u16>,
header_min_widths: Vec<u16>,
generation: u64,
history_index: Option<usize>,
) -> Self {
Self {
ideal_widths,
header_min_widths,
generation,
history_index,
}
}
pub fn is_valid(&self, generation: u64, history_index: Option<usize>) -> bool {
self.generation == generation
&& self.history_index == history_index
&& !self.ideal_widths.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn config<'a>(ideal: &'a [u16], min: &'a [u16]) -> ColumnWidthConfig<'a> {
ColumnWidthConfig {
ideal_widths: ideal,
min_widths: min,
}
}
fn ctx(offset: usize, width: u16, fixed: Option<usize>, max: usize) -> SelectionContext {
SelectionContext {
horizontal_offset: offset,
available_width: width,
fixed_count: fixed,
max_offset: max,
slack_policy: SlackPolicy::None,
}
}
mod total_width {
use super::*;
#[test]
fn empty_widths_return_zero() {
assert_eq!(total_width_with_separators(&[]), 0);
}
#[test]
fn single_column_no_separator() {
assert_eq!(total_width_with_separators(&[10]), 10);
}
#[test]
fn multiple_columns_includes_separators() {
assert_eq!(total_width_with_separators(&[10, 20, 30]), 62);
}
}
mod column_count {
use super::*;
#[test]
fn uses_ideal_widths_primarily() {
let ideal = vec![15, 15, 15, 15];
let min = vec![10, 10, 10, 10];
let count = calculate_viewport_column_count(&ideal, &min, 50);
assert_eq!(count, 3);
}
#[test]
fn scroll_enabled_when_ideal_exceeds_available() {
let ideal = vec![30, 30, 30, 30, 30];
let min = vec![10, 10, 10, 10, 10];
let available = 80;
let count = calculate_viewport_column_count(&ideal, &min, available);
let max_offset = calculate_max_offset(5, count);
assert_eq!(count, 2);
assert_eq!(max_offset, 3);
}
#[test]
fn falls_back_to_min_when_ideal_too_wide() {
let ideal = vec![100, 100, 100];
let min = vec![10, 10, 10];
let available = 50;
let count = calculate_viewport_column_count(&ideal, &min, available);
assert_eq!(count, 3);
}
#[test]
fn handles_varying_widths() {
let ideal = vec![6, 6, 25, 6, 6];
let min = vec![6, 6, 25, 6, 6];
let available = 40;
let count = calculate_viewport_column_count(&ideal, &min, available);
assert_eq!(count, 3);
}
#[test]
fn reduces_count_when_long_column_in_middle() {
let ideal = vec![10, 10, 50, 10, 10];
let min = vec![10, 10, 50, 10, 10];
let available = 60;
let count = calculate_viewport_column_count(&ideal, &min, available);
assert_eq!(count, 1);
}
#[test]
fn at_least_one_column() {
let ideal = vec![100, 100];
let min = vec![100, 100];
let count = calculate_viewport_column_count(&ideal, &min, 50);
assert_eq!(count, 1);
}
#[test]
fn empty_input_returns_zero() {
let count = calculate_viewport_column_count(&[], &[], 100);
assert_eq!(count, 0);
}
}
mod viewport_plan {
use super::*;
use rstest::rstest;
#[test]
fn calculate_returns_valid_plan() {
let ideal = vec![30, 30, 30, 30, 30];
let min = vec![10, 10, 10, 10, 10];
let plan = ViewportPlan::calculate(&ideal, &min, 80);
assert_eq!(plan.column_count, 2);
assert_eq!(plan.max_offset, 3);
assert_eq!(plan.available_width, 80);
assert_eq!(plan.min_widths_sum, 50);
assert_eq!(plan.ideal_widths_sum, 150);
assert_eq!(plan.ideal_widths_max, 30);
}
fn make_plan() -> ViewportPlan {
ViewportPlan {
column_count: 2,
max_offset: 3,
available_width: 80,
min_widths_sum: 50,
ideal_widths_sum: 150,
ideal_widths_max: 30,
..Default::default()
}
}
#[rstest]
#[case(5, 100, 50, 150, 30, true)] #[case(5, 80, 50, 150, 30, false)] #[case(10, 80, 50, 150, 30, true)] #[case(5, 80, 60, 150, 30, true)] #[case(5, 80, 50, 200, 30, true)] #[case(5, 80, 50, 150, 50, true)] fn recalculates_when_inputs_change(
#[case] len: usize,
#[case] width: u16,
#[case] min_sum: u16,
#[case] ideal_sum: u16,
#[case] ideal_max: u16,
#[case] expected: bool,
) {
let plan = make_plan();
let result = plan.needs_recalculation(len, width, min_sum, ideal_sum, ideal_max);
assert_eq!(result, expected);
}
#[test]
fn default_plan_needs_recalculation() {
let plan = ViewportPlan::default();
let result = plan.needs_recalculation(5, 80, 50, 150, 30);
assert!(result);
}
#[test]
fn different_max_triggers_recalculation() {
let original_ideal = vec![10, 20, 30, 40, 50];
let original_min = vec![5, 5, 5, 5, 5];
let plan = ViewportPlan::calculate(&original_ideal, &original_min, 80);
let changed = [10, 20, 30, 40, 60];
let sum: u16 = changed.iter().sum();
let max: u16 = changed.iter().copied().max().unwrap();
let result = plan.needs_recalculation(5, 80, 25, sum, max);
assert!(result);
}
#[test]
fn same_sum_and_max_skips_recalculation() {
let original_ideal = vec![10, 20, 30, 40, 50];
let original_min = vec![5, 5, 5, 5, 5];
let plan = ViewportPlan::calculate(&original_ideal, &original_min, 80);
let reordered = [50, 40, 30, 20, 10];
let sum: u16 = reordered.iter().sum();
let max: u16 = reordered.iter().copied().max().unwrap();
let result = plan.needs_recalculation(5, 80, 25, sum, max);
assert!(!result);
}
}
mod select_dynamic {
use super::*;
#[test]
fn basic_fit() {
let ideal = vec![10, 10, 10, 10];
let min = vec![4, 4, 4, 4];
let cfg = config(&ideal, &min);
let (indices, selected) = select_viewport_columns(&cfg, &ctx(0, 35, None, 0));
assert_eq!(indices, vec![0, 1, 2]);
assert_eq!(selected, vec![10, 10, 10]);
}
#[test]
fn offset_selects_columns() {
let ideal = vec![10, 10, 10, 10];
let min = vec![4, 4, 4, 4];
let cfg = config(&ideal, &min);
let (indices, _) = select_viewport_columns(&cfg, &ctx(1, 25, None, 0));
assert_eq!(indices, vec![1, 2]);
}
#[test]
fn shrinks_rightmost() {
let ideal = vec![10, 10, 50];
let min = vec![4, 4, 4];
let cfg = config(&ideal, &min);
let (indices, selected) = select_viewport_columns(&cfg, &ctx(0, 30, None, 0));
assert_eq!(indices, vec![0, 1, 2]);
assert_eq!(selected, vec![10, 10, 8]);
}
#[test]
fn at_least_one() {
let ideal = vec![100];
let min = vec![4];
let cfg = config(&ideal, &min);
let (indices, selected) = select_viewport_columns(&cfg, &ctx(0, 50, None, 0));
assert_eq!(indices, vec![0]);
assert_eq!(selected, vec![50]);
}
}
mod select_fixed {
use super::*;
#[test]
fn exact_count() {
let ideal = vec![10, 10, 10, 10];
let min = vec![4, 4, 4, 4];
let cfg = config(&ideal, &min);
let (indices, selected) = select_viewport_columns(&cfg, &ctx(0, 100, Some(3), 1));
assert_eq!(indices, vec![0, 1, 2, 3]); assert_eq!(selected, vec![10, 10, 10, 10]);
}
#[test]
fn offset_selects_fixed_columns() {
let ideal = vec![10, 10, 10, 10];
let min = vec![4, 4, 4, 4];
let cfg = config(&ideal, &min);
let (indices, selected) = select_viewport_columns(&cfg, &ctx(1, 100, Some(2), 2));
assert_eq!(indices, vec![1, 2, 3]); assert_eq!(selected, vec![10, 10, 10]);
}
#[test]
fn shrinks_to_fit_from_right() {
let ideal = vec![20, 20, 20];
let min = vec![4, 4, 4];
let cfg = config(&ideal, &min);
let (indices, selected) = select_viewport_columns(&cfg, &ctx(0, 50, Some(3), 1));
assert_eq!(indices, vec![0, 1, 2]);
assert_eq!(selected, vec![20, 20, 8]);
}
#[test]
fn shrinks_from_left_at_right_edge() {
let ideal = vec![20, 20, 20];
let min = vec![4, 4, 4];
let cfg = config(&ideal, &min);
let (indices, selected) = select_viewport_columns(&cfg, &ctx(1, 31, Some(2), 1));
assert_eq!(indices, vec![1, 2]);
assert_eq!(selected, vec![10, 20]); }
#[test]
fn shrinks_multiple_columns() {
let ideal = vec![20, 20, 20];
let min = vec![4, 4, 4];
let cfg = config(&ideal, &min);
let (indices, selected) = select_viewport_columns(&cfg, &ctx(0, 30, Some(3), 1));
assert_eq!(indices, vec![0, 1, 2]);
assert_eq!(selected, vec![20, 4, 4]);
}
#[test]
fn respects_boundary() {
let ideal = vec![10, 10];
let min = vec![4, 4];
let cfg = config(&ideal, &min);
let (indices, _) = select_viewport_columns(&cfg, &ctx(0, 100, Some(5), 0));
assert_eq!(indices, vec![0, 1]);
}
#[test]
fn one_column_scroll_changes_one_column() {
let ideal = vec![10, 10, 50, 10, 10];
let min = vec![4, 4, 4, 4, 4];
let cfg = config(&ideal, &min);
let max_offset = 2;
let (idx0, _) = select_viewport_columns(&cfg, &ctx(0, 75, Some(3), max_offset));
assert_eq!(idx0, vec![0, 1, 2]);
let (idx1, _) = select_viewport_columns(&cfg, &ctx(1, 75, Some(3), max_offset));
assert_eq!(idx1, vec![1, 2, 3]);
let (idx2, _) = select_viewport_columns(&cfg, &ctx(2, 75, Some(3), max_offset));
assert_eq!(idx2, vec![2, 3, 4]);
}
}
mod max_offset {
use super::*;
#[test]
fn basic() {
assert_eq!(calculate_max_offset(5, 3), 2);
}
#[test]
fn all_fit() {
assert_eq!(calculate_max_offset(3, 3), 0);
}
#[test]
fn more_viewport_than_columns() {
assert_eq!(calculate_max_offset(2, 5), 0);
}
}
mod next_prev_offset {
use super::*;
#[test]
fn next_increments() {
assert_eq!(calculate_next_column_offset(5, 1, 3), 2);
}
#[test]
fn next_clamps_to_max() {
assert_eq!(calculate_next_column_offset(5, 2, 3), 2);
}
#[test]
fn prev_decrements() {
assert_eq!(calculate_prev_column_offset(2), 1);
}
#[test]
fn prev_clamps_to_zero() {
assert_eq!(calculate_prev_column_offset(0), 0);
}
}
mod scroll_behavior {
use super::*;
#[test]
fn one_scroll_changes_first_column_by_one() {
let ideal = vec![15, 20, 30, 10, 25];
let min = vec![8, 8, 8, 8, 8];
let cfg = config(&ideal, &min);
let max_offset = 2;
let (idx0, _) = select_viewport_columns(&cfg, &ctx(0, 80, Some(3), max_offset));
let (idx1, _) = select_viewport_columns(&cfg, &ctx(1, 80, Some(3), max_offset));
let (idx2, _) = select_viewport_columns(&cfg, &ctx(2, 80, Some(3), max_offset));
assert_eq!(idx0[0], 0);
assert_eq!(idx1[0], 1);
assert_eq!(idx2[0], 2);
assert!(idx0.len() >= 3);
assert!(idx1.len() >= 3);
assert!(idx2.len() >= 3);
}
#[test]
fn scroll_preserves_minimum_column_count() {
let ideal = vec![10, 15, 20, 12, 18];
let min = vec![6, 6, 6, 6, 6];
let cfg = config(&ideal, &min);
let max_offset = 2;
let fixed_count = 3;
for offset in 0..=max_offset {
let (indices, _) =
select_viewport_columns(&cfg, &ctx(offset, 60, Some(fixed_count), max_offset));
assert!(
indices.len() >= fixed_count,
"Column count {} below fixed {} at offset {}",
indices.len(),
fixed_count,
offset
);
}
}
}
mod header_min_width {
use super::*;
#[test]
fn columns_never_shrink_below_header_min_width() {
let ideal = vec![20, 20, 20];
let min = vec![10, 10, 10];
let cfg = config(&ideal, &min);
let (_, selected) = select_viewport_columns(&cfg, &ctx(0, 35, Some(3), 1));
for (i, (w, min_w)) in selected.iter().zip(min.iter()).enumerate() {
assert!(*w >= *min_w, "Column {i} width {w} is below min {min_w}");
}
}
#[test]
fn large_min_widths_respected_under_pressure() {
let ideal = vec![30, 30, 30];
let min = vec![15, 15, 15];
let cfg = config(&ideal, &min);
let (_, selected) = select_viewport_columns(&cfg, &ctx(0, 50, Some(3), 1));
for (w, min_w) in selected.iter().zip(min.iter()) {
assert!(*w >= *min_w);
}
}
#[test]
fn headers_never_truncated_at_any_offset() {
let ideal = vec![20, 20, 40, 20, 20];
let min = vec![8, 8, 20, 8, 8];
let available = 50;
let count = calculate_viewport_column_count(&ideal, &min, available);
let max_offset = calculate_max_offset(ideal.len(), count);
for offset in 0..=max_offset {
let cfg = config(&ideal, &min);
let (indices, widths) =
select_viewport_columns(&cfg, &ctx(offset, available, Some(count), max_offset));
for (i, &w) in widths.iter().enumerate() {
let col_idx = indices[i];
let min_w = min[col_idx];
assert!(w >= min_w, "offset={offset}, col={col_idx}: {w} < {min_w}");
}
}
}
}
mod right_edge {
use super::*;
#[test]
fn rightmost_column_not_truncated_at_right_edge() {
let ideal = vec![20, 20, 20];
let min = vec![10, 10, 10];
let cfg = config(&ideal, &min);
let max_offset = 1;
let (indices, selected) =
select_viewport_columns(&cfg, &ctx(max_offset, 35, Some(2), max_offset));
let last_idx = indices.len() - 1;
assert_eq!(
selected[last_idx], ideal[indices[last_idx]],
"Rightmost column should have its ideal width at right edge"
);
}
#[test]
fn left_columns_shrink_first_at_right_edge() {
let ideal = vec![20, 20, 20];
let min = vec![10, 10, 10];
let cfg = config(&ideal, &min);
let max_offset = 1;
let (_, selected) =
select_viewport_columns(&cfg, &ctx(max_offset, 31, Some(2), max_offset));
assert!(
selected[0] < ideal[1],
"Left column should be shrunk at right edge"
);
assert_eq!(selected[1], ideal[2], "Right column should be preserved");
}
#[test]
fn drops_leftmost_when_shrinking_not_enough() {
let ideal = vec![30, 30, 30];
let min = vec![20, 20, 20];
let cfg = config(&ideal, &min);
let max_offset = 1;
let (indices, widths) =
select_viewport_columns(&cfg, &ctx(max_offset, 35, Some(2), max_offset));
assert_eq!(indices.len(), 1, "Should drop to 1 column");
assert_eq!(indices[0], 2, "Should keep rightmost column");
assert_eq!(widths[0], 30, "Rightmost should have ideal width");
}
}
mod slack_absorption {
use super::*;
fn ctx_with_slack(
offset: usize,
width: u16,
fixed: Option<usize>,
max: usize,
policy: SlackPolicy,
) -> SelectionContext {
SelectionContext {
horizontal_offset: offset,
available_width: width,
fixed_count: fixed,
max_offset: max,
slack_policy: policy,
}
}
#[test]
fn absorbs_slack_when_max_offset_zero() {
let ideal = vec![10, 10, 10];
let min = vec![4, 4, 4];
let cfg = config(&ideal, &min);
let (indices, widths) = select_viewport_columns(
&cfg,
&ctx_with_slack(0, 50, Some(3), 0, SlackPolicy::RightmostLimited),
);
assert_eq!(indices, vec![0, 1, 2]);
assert_eq!(widths[0], 10);
assert_eq!(widths[1], 10);
assert_eq!(widths[2], 28); }
#[test]
fn no_absorption_when_policy_none() {
let ideal = vec![10, 10, 10];
let min = vec![4, 4, 4];
let cfg = config(&ideal, &min);
let (_, widths) = select_viewport_columns(
&cfg,
&ctx_with_slack(0, 50, Some(3), 0, SlackPolicy::None),
);
assert_eq!(widths, vec![10, 10, 10]);
}
#[test]
fn absorbs_all_slack_without_limit() {
let ideal = vec![40, 40];
let min = vec![10, 10];
let cfg = config(&ideal, &min);
let (_, widths) = select_viewport_columns(
&cfg,
&ctx_with_slack(0, 120, Some(2), 0, SlackPolicy::RightmostLimited),
);
assert_eq!(widths[0], 40);
assert_eq!(widths[1], 79); }
#[test]
fn viewport_plan_sets_policy_based_on_max_offset() {
let ideal = vec![10, 10, 10];
let min = vec![4, 4, 4];
let plan = ViewportPlan::calculate(&ideal, &min, 100);
assert_eq!(plan.max_offset, 0);
assert_eq!(plan.slack_policy, SlackPolicy::RightmostLimited);
let plan2 = ViewportPlan::calculate(&ideal, &min, 25);
assert!(plan2.max_offset > 0);
assert_eq!(plan2.slack_policy, SlackPolicy::None);
}
#[test]
fn one_scroll_still_changes_one_column_with_slack_none() {
let ideal = vec![15, 15, 15, 15, 15];
let min = vec![8, 8, 8, 8, 8];
let cfg = config(&ideal, &min);
let max_offset = 2;
let (idx0, _) = select_viewport_columns(
&cfg,
&ctx_with_slack(0, 60, Some(3), max_offset, SlackPolicy::None),
);
let (idx1, _) = select_viewport_columns(
&cfg,
&ctx_with_slack(1, 60, Some(3), max_offset, SlackPolicy::None),
);
let (idx2, _) = select_viewport_columns(
&cfg,
&ctx_with_slack(2, 60, Some(3), max_offset, SlackPolicy::None),
);
assert_eq!(idx0, vec![0, 1, 2]);
assert_eq!(idx1, vec![1, 2, 3]);
assert_eq!(idx2, vec![2, 3, 4]);
}
}
mod integration {
use super::*;
fn run_full_pipeline(
ideal_widths: &[u16],
min_widths: &[u16],
available_width: u16,
horizontal_offset: usize,
) -> (Vec<usize>, Vec<u16>) {
let plan = ViewportPlan::calculate(ideal_widths, min_widths, available_width);
let clamped_offset = horizontal_offset.min(plan.max_offset);
let cfg = ColumnWidthConfig {
ideal_widths,
min_widths,
};
let ctx = SelectionContext {
horizontal_offset: clamped_offset,
available_width,
fixed_count: Some(plan.column_count),
max_offset: plan.max_offset,
slack_policy: plan.slack_policy,
};
select_viewport_columns(&cfg, &ctx)
}
#[test]
fn right_edge_preserves_rightmost_column_width() {
let ideal = vec![20, 25, 30, 15, 40];
let min = vec![10, 10, 10, 10, 10];
let available = 70;
let plan = ViewportPlan::calculate(&ideal, &min, available);
let max_offset = plan.max_offset;
let (indices, widths) = run_full_pipeline(&ideal, &min, available, max_offset);
let last_idx = indices.len() - 1;
assert_eq!(
indices[last_idx],
ideal.len() - 1,
"Should include the last column at right edge"
);
assert_eq!(
widths[last_idx], ideal[indices[last_idx]],
"Rightmost column should have ideal width at right edge"
);
}
#[test]
fn headers_never_truncated_throughout_scroll() {
let ideal = vec![15, 20, 35, 18, 22];
let min = vec![8, 10, 15, 8, 10];
let available = 60;
let plan = ViewportPlan::calculate(&ideal, &min, available);
for offset in 0..=plan.max_offset {
let (indices, widths) = run_full_pipeline(&ideal, &min, available, offset);
for (i, &w) in widths.iter().enumerate() {
let col_idx = indices[i];
assert!(
w >= min[col_idx],
"offset={}, col={}: width {} < min {}",
offset,
col_idx,
w,
min[col_idx]
);
}
}
}
#[test]
fn column_count_at_least_fixed_during_scroll() {
let ideal = vec![25, 30, 20, 35, 25];
let min = vec![10, 10, 10, 10, 10];
let available = 80;
let plan = ViewportPlan::calculate(&ideal, &min, available);
let fixed_count = plan.column_count;
for offset in 0..=plan.max_offset {
let (indices, _) = run_full_pipeline(&ideal, &min, available, offset);
assert!(
indices.len() >= fixed_count,
"Column count {} below fixed {} at offset {}",
indices.len(),
fixed_count,
offset
);
}
}
#[test]
fn slack_absorbed_when_no_scroll_needed() {
let ideal = vec![15, 15, 15];
let min = vec![8, 8, 8];
let available = 80;
let plan = ViewportPlan::calculate(&ideal, &min, available);
assert_eq!(plan.max_offset, 0, "Should fit all columns");
assert_eq!(plan.slack_policy, SlackPolicy::RightmostLimited);
let (_, widths) = run_full_pipeline(&ideal, &min, available, 0);
let total: u16 = widths.iter().sum::<u16>() + (widths.len() as u16 - 1);
assert_eq!(total, available, "Should use all available width");
}
#[test]
fn realistic_result_pane_scenario() {
let ideal = vec![6, 20, 35, 22, 10]; let min = vec![4, 6, 8, 12, 8]; let available = 80;
let plan = ViewportPlan::calculate(&ideal, &min, available);
assert!(plan.column_count > 0);
assert!(plan.column_count <= ideal.len());
for offset in 0..=plan.max_offset {
let (indices, widths) = run_full_pipeline(&ideal, &min, available, offset);
assert_eq!(indices.len(), plan.column_count);
let total: u16 =
widths.iter().sum::<u16>() + (widths.len().saturating_sub(1)) as u16;
assert!(
total <= available,
"offset={offset}: total {total} > available {available}"
);
for (i, &w) in widths.iter().enumerate() {
let col_idx = indices[i];
assert!(w >= min[col_idx]);
}
}
}
}
mod bonus_column {
use super::*;
fn ctx_with_slack(
offset: usize,
width: u16,
fixed: Option<usize>,
max: usize,
policy: SlackPolicy,
) -> SelectionContext {
SelectionContext {
horizontal_offset: offset,
available_width: width,
fixed_count: fixed,
max_offset: max,
slack_policy: policy,
}
}
#[test]
fn adds_bonus_when_slack_sufficient() {
let ideal = vec![10, 10, 10, 10, 10];
let min = vec![4, 4, 4, 4, 4];
let cfg = config(&ideal, &min);
let (indices, _) = select_viewport_columns(
&cfg,
&ctx_with_slack(0, 50, Some(3), 2, SlackPolicy::None),
);
assert_eq!(indices.len(), 4, "Should add 1 bonus column");
assert_eq!(indices, vec![0, 1, 2, 3]);
}
#[test]
fn no_bonus_when_slack_insufficient() {
let ideal = vec![10, 10, 10, 20, 10];
let min = vec![4, 4, 4, 4, 4];
let cfg = config(&ideal, &min);
let (indices, _) = select_viewport_columns(
&cfg,
&ctx_with_slack(0, 40, Some(3), 2, SlackPolicy::None),
);
assert_eq!(indices.len(), 3, "Should not add bonus column");
}
#[test]
fn no_bonus_at_last_column() {
let ideal = vec![10, 10, 10];
let min = vec![4, 4, 4];
let cfg = config(&ideal, &min);
let (indices, _) = select_viewport_columns(
&cfg,
&ctx_with_slack(1, 50, Some(2), 1, SlackPolicy::None),
);
assert_eq!(indices.len(), 2, "No bonus when no more columns exist");
}
#[test]
fn no_bonus_when_max_offset_zero() {
let ideal = vec![10, 10, 10, 10];
let min = vec![4, 4, 4, 4];
let cfg = config(&ideal, &min);
let (indices, _) = select_viewport_columns(
&cfg,
&ctx_with_slack(0, 100, Some(3), 0, SlackPolicy::RightmostLimited),
);
assert_eq!(indices.len(), 3);
}
#[test]
fn one_scroll_still_changes_one_column_with_bonus() {
let ideal = vec![15, 15, 15, 15, 15, 15];
let min = vec![8, 8, 8, 8, 8, 8];
let cfg = config(&ideal, &min);
let (idx0, _) = select_viewport_columns(
&cfg,
&ctx_with_slack(0, 70, Some(3), 3, SlackPolicy::None),
);
let (idx1, _) = select_viewport_columns(
&cfg,
&ctx_with_slack(1, 70, Some(3), 3, SlackPolicy::None),
);
assert_eq!(idx0[0], 0, "First column at offset 0 should be 0");
assert_eq!(idx1[0], 1, "First column at offset 1 should be 1");
}
#[test]
fn total_width_within_available() {
let ideal = vec![20, 20, 20, 15];
let min = vec![10, 10, 10, 10];
let cfg = config(&ideal, &min);
let (indices, widths) = select_viewport_columns(
&cfg,
&ctx_with_slack(0, 80, Some(3), 1, SlackPolicy::None),
);
assert_eq!(indices.len(), 4);
let total = total_width_with_separators(&widths);
assert!(
total <= 80,
"Total width {total} should not exceed available 80"
);
}
}
}