use crate::data::data_view::DataView;
pub const DEFAULT_COL_WIDTH: u16 = 15;
pub const MIN_COL_WIDTH: u16 = 3;
pub const MAX_COL_WIDTH: u16 = 50;
pub const MAX_COL_WIDTH_DATA_FOCUS: u16 = 100;
pub const COLUMN_PADDING: u16 = 2;
pub const MIN_HEADER_WIDTH_DATA_FOCUS: u16 = 5;
pub const MAX_HEADER_TO_DATA_RATIO: f32 = 1.5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColumnPackingMode {
DataFocus,
HeaderFocus,
Balanced,
}
impl ColumnPackingMode {
#[must_use]
pub fn cycle(&self) -> Self {
match self {
ColumnPackingMode::Balanced => ColumnPackingMode::DataFocus,
ColumnPackingMode::DataFocus => ColumnPackingMode::HeaderFocus,
ColumnPackingMode::HeaderFocus => ColumnPackingMode::Balanced,
}
}
#[must_use]
pub fn display_name(&self) -> &'static str {
match self {
ColumnPackingMode::Balanced => "Balanced",
ColumnPackingMode::DataFocus => "Data Focus",
ColumnPackingMode::HeaderFocus => "Header Focus",
}
}
}
pub type ColumnWidthDebugInfo = (String, u16, u16, u16, u32);
pub struct ColumnWidthCalculator {
column_widths: Vec<u16>,
packing_mode: ColumnPackingMode,
column_width_debug: Vec<ColumnWidthDebugInfo>,
cache_dirty: bool,
}
impl ColumnWidthCalculator {
#[must_use]
pub fn new() -> Self {
Self {
column_widths: Vec::new(),
packing_mode: ColumnPackingMode::Balanced,
column_width_debug: Vec::new(),
cache_dirty: true,
}
}
#[must_use]
pub fn get_packing_mode(&self) -> ColumnPackingMode {
self.packing_mode
}
pub fn set_packing_mode(&mut self, mode: ColumnPackingMode) {
if self.packing_mode != mode {
self.packing_mode = mode;
self.cache_dirty = true;
}
}
pub fn cycle_packing_mode(&mut self) {
self.set_packing_mode(self.packing_mode.cycle());
}
#[must_use]
pub fn get_debug_info(&self) -> &[ColumnWidthDebugInfo] {
&self.column_width_debug
}
pub fn mark_dirty(&mut self) {
self.cache_dirty = true;
}
pub fn calculate_with_terminal_width(
&mut self,
dataview: &DataView,
viewport_rows: &std::ops::Range<usize>,
terminal_width: u16,
) {
self.recalculate_column_widths(dataview, viewport_rows);
let total_ideal_width: u16 = self.column_widths.iter().sum();
let separators_width = (self.column_widths.len() as u16).saturating_sub(1);
let borders_width = 4u16;
let total_needed = total_ideal_width + separators_width + borders_width;
if total_needed < terminal_width {
let extra_space = terminal_width - total_needed;
let num_columns = self.column_widths.len() as u16;
let space_per_column = if num_columns > 0 {
extra_space / num_columns
} else {
0
};
for (idx, width) in self.column_widths.iter_mut().enumerate() {
if *width <= 10 && idx < self.column_width_debug.len() {
let (_, header_w, data_w, _, _) = &self.column_width_debug[idx];
let ideal = (*header_w).max(*data_w) + COLUMN_PADDING;
if ideal > *width && ideal <= 15 {
*width = ideal.min(*width + space_per_column);
}
}
}
}
}
pub fn get_column_width(
&mut self,
dataview: &DataView,
viewport_rows: &std::ops::Range<usize>,
col_idx: usize,
) -> u16 {
if self.cache_dirty {
self.recalculate_column_widths(dataview, viewport_rows);
}
self.column_widths
.get(col_idx)
.copied()
.unwrap_or(DEFAULT_COL_WIDTH)
}
pub fn get_all_column_widths(
&mut self,
dataview: &DataView,
viewport_rows: &std::ops::Range<usize>,
) -> &[u16] {
if self.cache_dirty {
self.recalculate_column_widths(dataview, viewport_rows);
}
&self.column_widths
}
fn recalculate_column_widths(
&mut self,
dataview: &DataView,
viewport_rows: &std::ops::Range<usize>,
) {
let col_count = dataview.column_count();
self.column_widths.resize(col_count, DEFAULT_COL_WIDTH);
self.column_width_debug.clear();
let headers = dataview.column_names();
let mut ideal_widths = Vec::with_capacity(col_count);
let mut header_widths = Vec::with_capacity(col_count);
let mut max_data_widths = Vec::with_capacity(col_count);
for col_idx in 0..col_count {
let header_width = headers.get(col_idx).map_or(0, |h| h.len() as u16);
header_widths.push(header_width);
let mut max_data_width = 0u16;
let sample_size = 100.min(viewport_rows.len());
let sample_step = if viewport_rows.len() > sample_size {
viewport_rows.len() / sample_size
} else {
1
};
for (i, row_idx) in viewport_rows.clone().enumerate() {
if i % sample_step != 0 && i != 0 && i != viewport_rows.len() - 1 {
continue;
}
if let Some(row) = dataview.get_row(row_idx) {
if col_idx < row.values.len() {
let cell_str = row.values[col_idx].to_string();
let cell_width = cell_str.len() as u16;
max_data_width = max_data_width.max(cell_width);
}
}
}
max_data_widths.push(max_data_width);
let ideal_width = header_width.max(max_data_width) + COLUMN_PADDING;
ideal_widths.push(ideal_width);
}
for col_idx in 0..col_count {
let header_width = header_widths[col_idx];
let max_data_width = max_data_widths[col_idx];
let ideal_width = ideal_widths[col_idx];
let final_width = if ideal_width <= 10 {
ideal_width
} else {
let data_samples = u32::from(max_data_width > 0);
let optimal_width = self.calculate_optimal_width_for_mode(
header_width,
max_data_width,
data_samples,
);
let (min_width, max_width) = match self.packing_mode {
ColumnPackingMode::DataFocus => (MIN_COL_WIDTH, MAX_COL_WIDTH_DATA_FOCUS),
_ => (MIN_COL_WIDTH, MAX_COL_WIDTH),
};
optimal_width.clamp(min_width, max_width)
};
self.column_widths[col_idx] = final_width;
let column_name = headers
.get(col_idx)
.cloned()
.unwrap_or_else(|| format!("col_{col_idx}"));
self.column_width_debug.push((
column_name,
header_width,
max_data_width,
final_width,
1, ));
}
self.cache_dirty = false;
}
fn calculate_optimal_width_for_mode(
&self,
header_width: u16,
max_data_width: u16,
data_samples: u32,
) -> u16 {
match self.packing_mode {
ColumnPackingMode::DataFocus => {
if data_samples > 0 {
if max_data_width <= 3 {
max_data_width + COLUMN_PADDING
} else if max_data_width <= 10 && header_width > max_data_width * 2 {
(max_data_width + COLUMN_PADDING).max(MIN_HEADER_WIDTH_DATA_FOCUS)
} else {
let data_width =
(max_data_width + COLUMN_PADDING).min(MAX_COL_WIDTH_DATA_FOCUS);
data_width.max(MIN_HEADER_WIDTH_DATA_FOCUS)
}
} else {
header_width
.min(DEFAULT_COL_WIDTH)
.max(MIN_HEADER_WIDTH_DATA_FOCUS)
}
}
ColumnPackingMode::HeaderFocus => {
let header_with_padding = header_width + COLUMN_PADDING;
if data_samples > 0 {
header_with_padding.max(max_data_width.min(MAX_COL_WIDTH))
} else {
header_with_padding
}
}
ColumnPackingMode::Balanced => {
if data_samples > 0 {
let data_based_width = max_data_width + COLUMN_PADDING;
if header_width > max_data_width {
let max_allowed_header =
(f32::from(max_data_width) * MAX_HEADER_TO_DATA_RATIO) as u16;
data_based_width.max(header_width.min(max_allowed_header))
} else {
data_based_width.max(header_width)
}
} else {
header_width.max(DEFAULT_COL_WIDTH)
}
}
}
}
}
impl Default for ColumnWidthCalculator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::datatable::{DataColumn, DataRow, DataTable, DataValue};
use std::sync::Arc;
fn create_test_dataview() -> DataView {
let mut table = DataTable::new("test");
table.add_column(DataColumn::new("short"));
table.add_column(DataColumn::new("very_long_header_name"));
table.add_column(DataColumn::new("normal"));
for i in 0..5 {
let values = vec![
DataValue::String("A".to_string()), DataValue::String("X".to_string()), DataValue::String(format!("Value{i}")), ];
table.add_row(DataRow::new(values)).unwrap();
}
DataView::new(Arc::new(table))
}
#[test]
fn test_column_width_calculator_creation() {
let calculator = ColumnWidthCalculator::new();
assert_eq!(calculator.get_packing_mode(), ColumnPackingMode::Balanced);
assert!(calculator.cache_dirty);
}
#[test]
fn test_packing_mode_cycle() {
let mut calculator = ColumnWidthCalculator::new();
assert_eq!(calculator.get_packing_mode(), ColumnPackingMode::Balanced);
calculator.cycle_packing_mode();
assert_eq!(calculator.get_packing_mode(), ColumnPackingMode::DataFocus);
calculator.cycle_packing_mode();
assert_eq!(
calculator.get_packing_mode(),
ColumnPackingMode::HeaderFocus
);
calculator.cycle_packing_mode();
assert_eq!(calculator.get_packing_mode(), ColumnPackingMode::Balanced);
}
#[test]
fn test_width_calculation_different_modes() {
let dataview = create_test_dataview();
let viewport_rows = 0..5;
let mut calculator = ColumnWidthCalculator::new();
calculator.set_packing_mode(ColumnPackingMode::Balanced);
let balanced_widths = calculator
.get_all_column_widths(&dataview, &viewport_rows)
.to_vec();
calculator.set_packing_mode(ColumnPackingMode::DataFocus);
let data_focus_widths = calculator
.get_all_column_widths(&dataview, &viewport_rows)
.to_vec();
calculator.set_packing_mode(ColumnPackingMode::HeaderFocus);
let header_focus_widths = calculator
.get_all_column_widths(&dataview, &viewport_rows)
.to_vec();
assert_eq!(balanced_widths.len(), 3);
assert_eq!(data_focus_widths.len(), 3);
assert_eq!(header_focus_widths.len(), 3);
assert!(header_focus_widths[1] >= data_focus_widths[1]);
}
}