ratatui-kit 0.10.2

A framework for building interactive terminal user interfaces with ratatui
Documentation
use ratatui::layout::Constraint;

use super::types::{TableBorderMode, TableColumn};

fn is_column_visible(column: &TableColumn, table_width: u16) -> bool {
    column
        .min_table_width
        .is_none_or(|min_width| table_width >= min_width)
}

pub(super) fn visible_columns(columns: &[TableColumn], table_width: u16) -> Vec<TableColumn> {
    columns
        .iter()
        .filter(|column| is_column_visible(column, table_width))
        .cloned()
        .collect()
}

/// The original indices (into the full column list) of the columns that survive
/// responsive `min_table_width` filtering at the given width. Used to map a
/// `TableState::selected_column` (a full-list index) onto a visible position.
pub(super) fn visible_column_indices(columns: &[TableColumn], table_width: u16) -> Vec<usize> {
    columns
        .iter()
        .enumerate()
        .filter(|(_, column)| is_column_visible(column, table_width))
        .map(|(index, _)| index)
        .collect()
}

pub(super) fn resolve_column_widths(
    columns: &[TableColumn],
    area_width: u16,
    border_mode: TableBorderMode,
    cell_padding: u16,
    column_spacing: u16,
    gutter: u16,
) -> Vec<u16> {
    let column_count = columns.len() as u16;
    if column_count == 0 {
        return Vec::new();
    }

    let reserved = match border_mode {
        // Outer 与 Grid 一样要为 (n-1) 个列间分隔 + 外边框预留 column_count+1 格。
        // (采纳自 #12:Outer 原先只留 2,窄表时 render_border_line 会越界。)
        TableBorderMode::Outer | TableBorderMode::Grid => column_count.saturating_add(1),
        TableBorderMode::None => column_spacing.saturating_mul(column_count.saturating_sub(1)),
    }
    .saturating_add(cell_padding.saturating_mul(2).saturating_mul(column_count))
    // 选中符号的前置 gutter 也要从可用宽度里扣除,否则符号会挤占首列内容。
    .saturating_add(gutter);
    let available = area_width.saturating_sub(reserved).max(column_count) as u32;

    let mut fixed = 0u32;
    let mut fill_units = 0u32;
    let mut fill_indexes = Vec::new();
    let mut widths = vec![0u16; columns.len()];

    for (index, column) in columns.iter().enumerate() {
        match column.width {
            Constraint::Length(width) | Constraint::Min(width) | Constraint::Max(width) => {
                widths[index] = width;
                fixed += u32::from(width);
            }
            Constraint::Percentage(percent) => {
                let width = available
                    .saturating_mul(u32::from(percent))
                    .saturating_div(100) as u16;
                widths[index] = width;
                fixed += u32::from(width);
            }
            Constraint::Ratio(num, den) => {
                let width = if den == 0 {
                    0
                } else {
                    available.saturating_mul(num).saturating_div(den) as u16
                };
                widths[index] = width;
                fixed += u32::from(width);
            }
            Constraint::Fill(unit) => {
                fill_units += u32::from(unit.max(1));
                fill_indexes.push((index, u32::from(unit.max(1))));
            }
        }
    }

    let remaining = available.saturating_sub(fixed);
    for (index, unit) in fill_indexes {
        widths[index] = if fill_units == 0 {
            0
        } else {
            remaining.saturating_mul(unit).saturating_div(fill_units) as u16
        };
    }

    let total: u32 = widths.iter().map(|width| u32::from(*width)).sum();
    if total > available {
        let mut remaining = available;
        for (index, width) in widths.iter_mut().enumerate() {
            if index == columns.len() - 1 {
                *width = remaining as u16;
            } else {
                let scaled = (u32::from(*width) * available / total).max(1);
                *width = scaled as u16;
                remaining = remaining.saturating_sub(scaled);
            }
        }
    }

    widths
}

#[cfg(test)]
mod tests {
    use ratatui::layout::Constraint;

    use super::*;

    #[test]
    fn min_table_width_filters_columns() {
        let columns = vec![
            TableColumn::new("A", Constraint::Length(1)),
            TableColumn::new("B", Constraint::Length(1)).min_table_width(80),
        ];

        assert_eq!(visible_columns(&columns, 79).len(), 1);
        assert_eq!(visible_columns(&columns, 80).len(), 2);
        assert_eq!(visible_column_indices(&columns, 79), vec![0]);
        assert_eq!(visible_column_indices(&columns, 80), vec![0, 1]);
    }

    #[test]
    fn percentage_and_fill_widths_share_available_width() {
        let columns = vec![
            TableColumn::new("A", Constraint::Percentage(50)),
            TableColumn::new("B", Constraint::Fill(1)),
            TableColumn::new("C", Constraint::Fill(1)),
        ];

        let widths = resolve_column_widths(&columns, 30, TableBorderMode::None, 0, 0, 0);

        assert_eq!(widths, vec![15, 7, 7]);
    }

    #[test]
    fn gutter_reserves_leading_width() {
        let columns = vec![TableColumn::new("A", Constraint::Fill(1))];

        let without = resolve_column_widths(&columns, 20, TableBorderMode::None, 0, 0, 0);
        let with = resolve_column_widths(&columns, 20, TableBorderMode::None, 0, 0, 2);

        assert_eq!(without, vec![20]);
        assert_eq!(with, vec![18]);
    }
}