use unicode_width::UnicodeWidthStr;
use super::{ColumnDisplayInfo, DisplayInfos, constraint, helper::*};
use crate::{Column, Table, style::*, utils::formatting::content_split::split_line};
struct ArrangementState<'a> {
table: &'a Table,
infos: &'a mut DisplayInfos,
max_content_widths: &'a [u16],
remaining_width: usize,
remaining_columns: usize,
visible_columns: usize,
}
impl<'a> ArrangementState<'a> {
fn new(
table: &'a Table,
infos: &'a mut DisplayInfos,
visible_columns: usize,
table_width: usize,
max_content_widths: &'a [u16],
) -> Self {
let border_count = count_border_columns(table, visible_columns);
let mut remaining_width = table_width.saturating_sub(border_count);
for column in table.columns.iter() {
if infos.contains_key(&column.index) {
continue;
}
let (left, right) = column.padding;
remaining_width = remaining_width.saturating_sub((left + right).into());
}
for info in infos.values() {
if info.is_hidden {
continue;
}
remaining_width = remaining_width.saturating_sub(info.width().into());
}
let remaining_columns = count_remaining_columns(visible_columns, infos);
Self {
table,
infos,
max_content_widths,
remaining_width,
remaining_columns,
visible_columns,
}
}
fn average_space(&self) -> usize {
self.remaining_width
.checked_div(self.remaining_columns)
.unwrap_or(0)
}
fn is_decided(&self, column: &Column) -> bool {
self.infos.contains_key(&column.index)
}
fn fix_column(&mut self, column: &Column, content_width: u16) -> bool {
let info = ColumnDisplayInfo::new(column, content_width);
self.infos.insert(column.index, info);
self.remaining_width = self.remaining_width.saturating_sub(content_width.into());
self.remaining_columns -= 1;
self.remaining_columns > 0
}
}
pub fn arrange(
table: &Table,
infos: &mut DisplayInfos,
visible_columns: usize,
table_width: usize,
max_content_widths: &[u16],
) {
let mut state = ArrangementState::new(
table,
infos,
visible_columns,
table_width,
max_content_widths,
);
#[cfg(feature = "_debug")]
println!(
"dynamic::arrange: Table width: {table_width}, Start remaining width {}",
state.remaining_width
);
#[cfg(feature = "_debug")]
println!("dynamic::arrange: Max content widths: {max_content_widths:#?}");
if state.remaining_columns > 0 {
enforce_lower_boundary_constraints(&mut state);
}
find_columns_that_fit_into_average(&mut state);
#[cfg(feature = "_debug")]
{
println!("After less than average: {:#?}", state.infos);
println!(
"Remaining width {}, column {}",
state.remaining_width, state.remaining_columns
);
}
if state.remaining_columns > 0 {
optimize_space_after_split(&mut state);
}
#[cfg(feature = "_debug")]
{
println!("dynamic::arrange: After optimize: {:#?}", state.infos);
println!(
"dynamic::arrange: Remaining width {}, column {}",
state.remaining_width, state.remaining_columns
);
}
if state.remaining_columns == 0 {
if state.remaining_width > 0
&& matches!(table.arrangement, ContentArrangement::DynamicFullWidth)
{
use_full_width(&mut state);
#[cfg(feature = "_debug")]
println!("dynamic::arrange: After full width: {:#?}", state.infos);
}
return;
}
if state.remaining_width < state.remaining_columns {
state.remaining_width = state.remaining_columns;
}
distribute_remaining_space(&mut state);
#[cfg(feature = "_debug")]
println!("dynamic::arrange: After distribute: {:#?}", state.infos);
}
fn find_columns_that_fit_into_average(state: &mut ArrangementState) {
let mut found_smaller = true;
while found_smaller {
found_smaller = false;
if state.remaining_columns == 0 {
break;
}
if state.average_space() == 0 {
break;
}
for column in state.table.columns.iter() {
if state.is_decided(column) {
continue;
}
let max_column_width = state.max_content_widths[column.index];
if let Some(max_width) =
constraint::max(state.table, &column.constraint, state.visible_columns)
{
let average_space_with_padding =
state.average_space() + usize::from(column.padding_width());
let width_with_padding = max_column_width + column.padding_width();
if usize::from(max_width) <= average_space_with_padding
&& width_with_padding >= max_width
{
let width = absolute_width_with_padding(column, max_width);
#[cfg(feature = "_debug")]
println!(
"dynamic::find_columns_that_fit_into_average: Fixed column {} via MaxWidth constraint with size {}, as it's bigger than average {}",
column.index,
width,
state.average_space()
);
if !state.fix_column(column, width) {
break;
}
found_smaller = true;
continue;
}
}
if usize::from(max_column_width) <= state.average_space() {
#[cfg(feature = "_debug")]
println!(
"dynamic::find_columns_that_fit_into_average: Fixed column {} with size {}, as it's smaller than average {}",
column.index,
max_column_width,
state.average_space()
);
if !state.fix_column(column, max_column_width) {
break;
}
found_smaller = true;
}
}
}
}
fn enforce_lower_boundary_constraints(state: &mut ArrangementState) {
let mut try_again = true;
while try_again {
try_again = false;
for column in state.table.columns.iter() {
if state.is_decided(column) {
continue;
}
let Some(min_width) =
constraint::min(state.table, &column.constraint, state.visible_columns)
else {
continue;
};
if state.average_space() >= min_width.into() {
continue;
}
let width = absolute_width_with_padding(column, min_width);
#[cfg(feature = "_debug")]
println!(
"dynamic::enforce_lower_boundary_constraints: Fixed column {} to min constraint width {}",
column.index, width
);
if !state.fix_column(column, width) {
break;
}
try_again = true;
}
}
}
fn optimize_space_after_split(state: &mut ArrangementState) {
let mut found_smaller = true;
#[cfg(feature = "_debug")]
println!(
"dynamic::optimize_space_after_split: Start with average_space {}",
state.average_space()
);
while found_smaller {
found_smaller = false;
for column in state.table.columns.iter() {
if state.is_decided(column) {
continue;
}
let average_space = state.average_space();
let longest_line = longest_line_after_split(average_space, column, state.table);
#[cfg(feature = "_debug")]
println!(
"dynamic::optimize_space_after_split: Longest line after split for column {} is {}",
column.index, longest_line
);
let remaining_space = average_space.saturating_sub(longest_line);
if remaining_space >= 3 {
if !state.fix_column(column, longest_line.try_into().unwrap_or(u16::MAX)) {
break;
}
#[cfg(feature = "_debug")]
println!(
"dynamic::optimize_space_after_split: average_space is now {}",
state.average_space()
);
found_smaller = true;
}
}
}
}
fn longest_line_after_split(average_space: usize, column: &Column, table: &Table) -> usize {
let mut longest = 0;
for cell in table.column_cells_with_header_iter(column.index) {
let Some(cell) = cell else { continue };
let delimiter = delimiter(table, column, cell);
let info = ColumnDisplayInfo::new(column, average_space.try_into().unwrap_or(u16::MAX));
for line in cell.content.iter() {
if line.width() > average_space {
let parts = split_line(line, &info, delimiter);
#[cfg(feature = "_debug")]
println!(
"dynamic::longest_line_after_split: Splitting line with width {}. Original:\n {}\nSplitted:\n {:?}",
line.width(),
line,
parts
);
parts
.iter()
.for_each(|part| longest = longest.max(part.width()));
} else {
longest = longest.max(line.width())
}
}
}
longest
}
fn even_widths(width: usize, count: usize) -> impl Iterator<Item = u16> {
let average_space = width / count;
let excess = width - (average_space * count);
(0..count).map(move |index| {
let width = if index < excess {
average_space + 1
} else {
average_space
};
width.try_into().unwrap_or(u16::MAX)
})
}
fn use_full_width(state: &mut ArrangementState) {
let visible_columns = state.infos.values().filter(|info| !info.is_hidden).count();
if visible_columns == 0 {
return;
}
let mut widths = even_widths(state.remaining_width, visible_columns);
for info in state.infos.values_mut() {
if info.is_hidden {
continue;
}
let width = widths.next().expect("One width per visible column");
info.content_width += width;
}
}
fn distribute_remaining_space(state: &mut ArrangementState) {
let mut widths = even_widths(state.remaining_width, state.remaining_columns);
for column in state.table.columns.iter() {
if state.is_decided(column) {
continue;
}
let width = widths.next().expect("One width per remaining column");
let info = ColumnDisplayInfo::new(column, width);
state.infos.insert(column.index, info);
}
}