1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560
use unicode_width::UnicodeWidthStr;
use super::constraint;
use super::helper::*;
use super::{ColumnDisplayInfo, DisplayInfos};
use crate::style::*;
use crate::utils::formatting::content_split::split_line;
use crate::{Column, Table};
/// Try to find the best fit for a given content and table_width
///
/// 1. Determine the amount of available space after applying fixed columns, padding, and borders.
/// 2. Now that we know how much space we have to work with, we have to check again for
/// LowerBoundary constraints. If there are any columns that have a higher LowerBoundary,
/// we have to fix that column to this size.
/// 3. Check if there are any columns that require less space than the average
/// remaining space for the remaining columns. (This includes the MaxWidth constraint).
/// 4. Take those columns, fix their size and add the surplus in space to the remaining space.
/// 5. Repeat step 2-3 until no columns with smaller size than average remaining space are left.
/// 6. At this point, the remaining spaces is equally distributed between all columns.
/// It get's a little tricky now. Check the documentation of [optimize_space_after_split]
/// for more information.
/// 7. Divide the remaining space in relatively equal chunks.
///
/// This breaks when:
///
/// 1. A user assigns fixed sizes to a few columns, which are larger than the terminal when combined.
/// 2. A user provides more than 100% column width across a few columns.
pub fn arrange(
table: &Table,
infos: &mut DisplayInfos,
table_width: usize,
max_content_widths: &[u16],
) {
let visible_columns = count_visible_columns(&table.columns);
// Step 1
// Find out how much space there is left.
let mut remaining_width: usize =
available_content_width(table, infos, visible_columns, table_width);
let mut remaining_columns = count_remaining_columns(visible_columns, infos);
#[cfg(feature = "debug")]
println!(
"dynamic::arrange: Table width: {table_width}, Start remaining width {remaining_width}"
);
#[cfg(feature = "debug")]
println!("dynamic::arrange: Max content widths: {max_content_widths:#?}");
// Step 2.
//
// Iterate through all undecided columns and enforce LowerBoundary constraints, if they're
// bigger than the current average space.
if remaining_columns > 0 {
(remaining_width, remaining_columns) = enforce_lower_boundary_constraints(
table,
infos,
remaining_width,
remaining_columns,
visible_columns,
);
}
// Step 3-5.
// Find all columns that require less space than the average.
// Returns the remaining available width and the amount of remaining columns that need handling
let (mut remaining_width, mut remaining_columns) = find_columns_that_fit_into_average(
table,
infos,
remaining_width,
remaining_columns,
visible_columns,
max_content_widths,
);
#[cfg(feature = "debug")]
{
println!("After less than average: {infos:#?}");
println!("Remaining width {remaining_width}, column {remaining_columns}");
}
// Step 6
// All remaining columns should get an equal amount of remaining space.
// However, we check if we can save some space after the content has been split.
//
// We only do this if there are remaining columns.
if remaining_columns > 0 {
// This is where Step 5 happens.
(remaining_width, remaining_columns) = optimize_space_after_split(
table,
&table.columns,
infos,
remaining_width,
remaining_columns,
);
}
#[cfg(feature = "debug")]
{
println!("dynamic::arrange: After optimize: {infos:#?}",);
println!("dynamic::arrange: Remaining width {remaining_width}, column {remaining_columns}",);
}
// Early exit and one branch of Part 7.
//
// All columns have been successfully assigned a width.
// However, in case the user specified that the full terminal width should always be fully
// utilized, we have to equally distribute the remaining space across all columns.
if remaining_columns == 0 {
if remaining_width > 0 && matches!(table.arrangement, ContentArrangement::DynamicFullWidth)
{
use_full_width(infos, remaining_width);
#[cfg(feature = "debug")]
println!("dynamic::arrange: After full width: {infos:#?}");
}
return;
}
// Step 7. Equally distribute the remaining_width to all remaining columns
// If we have less than one space per remaining column, give at least one space per column
if remaining_width < remaining_columns {
remaining_width = remaining_columns;
}
distribute_remaining_space(&table.columns, infos, remaining_width, remaining_columns);
#[cfg(feature = "debug")]
println!("dynamic::arrange: After distribute: {infos:#?}");
}
/// Step 1
///
/// This function calculates the amount of remaining space that can be distributed between
/// all remaining columns.
///
/// Take the current terminal width and
/// - Subtract borders
/// - Subtract padding
/// - Subtract columns that already have a fixed width.
///
/// This value is converted to a i32 to handle negative values in case we work with a very small
/// terminal.
fn available_content_width(
table: &Table,
infos: &DisplayInfos,
visible_columns: usize,
mut width: usize,
) -> usize {
let border_count = count_border_columns(table, visible_columns);
width = width.saturating_sub(border_count);
// Subtract all paddings from the remaining width.
for column in table.columns.iter() {
if infos.contains_key(&column.index) {
continue;
}
// Remove the fixed padding for each column
let (left, right) = column.padding;
width = width.saturating_sub((left + right).into());
}
// Remove all already fixed sizes from the remaining_width.
for info in infos.values() {
if info.is_hidden {
continue;
}
width = width.saturating_sub(info.width().into());
}
width
}
/// Step 2-4
/// This function is part of the column width calculation process.
/// It checks if there are columns that take less space than there's currently available in average
/// for each column.
///
/// The algorithm is a while loop with a nested for loop.
/// 1. We iterate over all columns and check if there are columns that take less space.
/// 2. If we find one or more such columns, we fix their width and add the surplus space to the
/// remaining space. Due to this step, the average space per column increased. Now some other
/// column might be fixed in width as well.
/// 3. Do step 1 and 2, as long as there are columns left and as long as we find columns
/// that take up less space than the current remaining average.
///
/// Parameters:
/// - `table_width`: The absolute amount of available space.
/// - `remaining_width`: This is the amount of space that isn't yet reserved by any other column.
/// We need this to determine the average space each column has left.
/// Any columns that needs less than this average receives a fixed width.
/// The leftover space can then be used for the other columns.
/// - `visible_columns`: All visible columns that should be displayed.
///
/// Returns:
/// `(remaining_width: usize, remaining_columns: u16)`
fn find_columns_that_fit_into_average(
table: &Table,
infos: &mut DisplayInfos,
mut remaining_width: usize,
mut remaining_columns: usize,
visible_columns: usize,
max_content_widths: &[u16],
) -> (usize, usize) {
let mut found_smaller = true;
while found_smaller {
found_smaller = false;
// There are no columns left to check. Proceed to the next step
if remaining_columns == 0 {
break;
}
let mut average_space = remaining_width / remaining_columns;
// We have no space left, the terminal is either tiny or the other columns are huge.
if average_space == 0 {
break;
}
for column in table.columns.iter() {
// Ignore hidden columns
// We already checked this column, skip it
if infos.contains_key(&column.index) {
continue;
}
let max_column_width = max_content_widths[column.index];
// The column has a MaxWidth Constraint.
// we can fix the column to this max_width and mark it as checked if these
// two conditions are met:
// - The average remaining space is bigger then the MaxWidth constraint.
// - The actual max content of the column is bigger than the MaxWidth constraint.
if let Some(max_width) = constraint::max(table, &column.constraint, visible_columns) {
// Max/Min constraints always include padding!
let average_space_with_padding =
average_space + usize::from(column.padding_width());
let width_with_padding = max_column_width + column.padding_width();
// Check that both conditions mentioned above are met.
if usize::from(max_width) <= average_space_with_padding
&& width_with_padding >= max_width
{
// Save the calculated info, this column has been handled.
let width = absolute_width_with_padding(column, max_width);
let info = ColumnDisplayInfo::new(column, width);
infos.insert(column.index, info);
#[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, average_space
);
// Continue with new recalculated width
remaining_width = remaining_width.saturating_sub(width.into());
remaining_columns -= 1;
if remaining_columns == 0 {
break;
}
average_space = remaining_width / remaining_columns;
found_smaller = true;
continue;
}
}
// The column has a smaller or equal max_content_width than the average space.
// Fix the width to max_content_width and mark it as checked
if usize::from(max_column_width) <= average_space {
let info = ColumnDisplayInfo::new(column, max_column_width);
infos.insert(column.index, info);
#[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, average_space
);
// Continue with new recalculated width
remaining_width = remaining_width.saturating_sub(max_column_width.into());
remaining_columns -= 1;
if remaining_columns == 0 {
break;
}
average_space = remaining_width / remaining_columns;
found_smaller = true;
}
}
}
(remaining_width, remaining_columns)
}
/// Step 5
///
/// Determine, whether there are any columns that are allowed to occupy more width than the current
/// `average_space` via a [LowerBoundary] constraint.
///
/// These columns will then get fixed to the width specified in the [LowerBoundary] constraint.
///
/// I.e. if a column has to have at least 10 characters, but the average width left for a column is
/// only 6, we fix the column to this 10 character minimum!
fn enforce_lower_boundary_constraints(
table: &Table,
infos: &mut DisplayInfos,
mut remaining_width: usize,
mut remaining_columns: usize,
visible_columns: usize,
) -> (usize, usize) {
let mut average_space = remaining_width / remaining_columns;
for column in table.columns.iter() {
// Ignore hidden columns
// We already checked this column, skip it
if infos.contains_key(&column.index) {
continue;
}
// Check whether the column has a LowerBoundary constraint.
let min_width =
if let Some(min_width) = constraint::min(table, &column.constraint, visible_columns) {
min_width
} else {
continue;
};
// Only proceed if the average spaces is smaller than the specified lower boundary.
if average_space >= min_width.into() {
continue;
}
// This column would get smaller than the specified lower boundary.
// Fix its width!!!
let width = absolute_width_with_padding(column, min_width);
let info = ColumnDisplayInfo::new(column, width);
infos.insert(column.index, info);
#[cfg(feature = "debug")]
println!(
"dynamic::enforce_lower_boundary_constraints: Fixed column {} to min constraint width {}",
column.index, width
);
// Continue with new recalculated width
remaining_width = remaining_width.saturating_sub(width.into());
remaining_columns -= 1;
if remaining_columns == 0 {
break;
}
average_space = remaining_width / remaining_columns;
continue;
}
(remaining_width, remaining_columns)
}
/// Step 5.
///
/// Some Column's are too big and need to be split.
/// We're now going to simulate how this might look like.
/// The reason for this is the way we're splitting, which is to prefer a split at a delimiter.
/// This can lead to a column needing less space than it was initially assigned.
///
/// Example:
/// A column is allowed to have a width of 10 characters.
/// A cell's content looks like this `sometest sometest`, which is 17 chars wide.
/// After splitting at the default delimiter (space), it looks like this:
/// ```text
/// sometest
/// sometest
/// ```
/// Even though the column required 17 spaces beforehand, it can now be shrunk to 8 chars width.
///
/// By doing this for each column, we can save a lot of space in some edge-cases.
fn optimize_space_after_split(
table: &Table,
columns: &[Column],
infos: &mut DisplayInfos,
mut remaining_width: usize,
mut remaining_columns: usize,
) -> (usize, usize) {
let mut found_smaller = true;
// Calculate the average space that remains for each column.
let mut average_space = remaining_width / remaining_columns;
#[cfg(feature = "debug")]
println!(
"dynamic::optimize_space_after_split: Start with average_space {}",
average_space
);
// Do this as long as we find a smaller column
while found_smaller {
found_smaller = false;
for column in columns.iter() {
// We already checked this column, skip it
if infos.contains_key(&column.index) {
continue;
}
let longest_line = longest_line_after_split(average_space, column, table);
#[cfg(feature = "debug")]
println!(
"dynamic::optimize_space_after_split: Longest line after split for column {} is {}",
column.index, longest_line
);
// If there's a considerable amount space left after splitting, we freeze the column and
// set its content width to the calculated post-split width.
let remaining_space = average_space.saturating_sub(longest_line);
if remaining_space >= 3 {
let info =
ColumnDisplayInfo::new(column, longest_line.try_into().unwrap_or(u16::MAX));
infos.insert(column.index, info);
remaining_width = remaining_width.saturating_sub(longest_line);
remaining_columns -= 1;
if remaining_columns == 0 {
break;
}
average_space = remaining_width / remaining_columns;
#[cfg(feature = "debug")]
println!(
"dynamic::optimize_space_after_split: average_space is now {}",
average_space
);
found_smaller = true;
}
}
}
(remaining_width, remaining_columns)
}
/// Part of Step 5.
///
/// This function simulates the split of a Column's content and returns the longest
/// existing line after the split.
///
/// A lot of this logic is duplicated from the [utils::format::format_row] function.
fn longest_line_after_split(average_space: usize, column: &Column, table: &Table) -> usize {
// Collect all resulting lines of the column in a single vector.
// That way we can easily determine the longest line afterwards.
let mut column_lines = Vec::new();
// Iterate
for cell in table.column_cells_with_header_iter(column.index) {
// Only look at rows that actually contain this cell.
let cell = match cell {
Some(cell) => cell,
None => continue,
};
let delimiter = delimiter(table, column, cell);
// Create a temporary ColumnDisplayInfo with the average space as width.
// That way we can simulate how the split text will look like.
let info = ColumnDisplayInfo::new(column, average_space.try_into().unwrap_or(u16::MAX));
// Iterate over each line and split it into multiple lines, if necessary.
// Newlines added by the user will be preserved.
for line in cell.content.iter() {
if line.width() > average_space {
let mut 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
);
column_lines.append(&mut parts);
} else {
column_lines.push(line.into());
}
}
}
// Get the longest line, default to length 0 if no lines exist.
column_lines
.iter()
.map(|line| line.width())
.max()
.unwrap_or(0)
}
/// Step 6 - First branch
///
/// At this point of time, all columns have been assigned some kind of width!
/// The user wants to utilize the full width of the terminal and there's space left.
///
/// Equally distribute the remaining space between all columns.
fn use_full_width(infos: &mut DisplayInfos, remaining_width: usize) {
let visible_columns = infos.iter().filter(|(_, info)| !info.is_hidden).count();
if visible_columns == 0 {
return;
}
// Calculate the amount of average remaining space per column.
// Since we do integer division, there is most likely a little bit of non equally-divisible space.
// We then try to distribute it as fair as possible (from left to right).
let average_space = remaining_width / visible_columns;
let mut excess = remaining_width - (average_space * visible_columns);
for (_, info) in infos.iter_mut() {
// Ignore hidden columns
if info.is_hidden {
continue;
}
// Distribute the non-divisible excess from left-to right until nothing is left.
let width = if excess > 0 {
excess -= 1;
(average_space + 1).try_into().unwrap_or(u16::MAX)
} else {
average_space.try_into().unwrap_or(u16::MAX)
};
info.content_width += width;
}
}
/// Step 6 - Second branch
///
/// Not all columns have a determined width yet -> The content still doesn't fully fit into the
/// given width.
///
/// This function now equally distributes the remaining width between the remaining columns.
fn distribute_remaining_space(
columns: &[Column],
infos: &mut DisplayInfos,
remaining_width: usize,
remaining_columns: usize,
) {
// Calculate the amount of average remaining space per column.
// Since we do integer division, there is most likely a little bit of non equally-divisible space.
// We then try to distribute it as fair as possible (from left to right).
let average_space = remaining_width / remaining_columns;
let mut excess = remaining_width - (average_space * remaining_columns);
for column in columns.iter() {
// Ignore hidden columns
if infos.contains_key(&column.index) {
continue;
}
// Distribute the non-divisible excess from left-to right until nothing is left.
let width = if excess > 0 {
excess -= 1;
(average_space + 1).try_into().unwrap_or(u16::MAX)
} else {
average_space.try_into().unwrap_or(u16::MAX)
};
let info = ColumnDisplayInfo::new(column, width);
infos.insert(column.index, info);
}
}