summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorcyqsimon <28627918+cyqsimon@users.noreply.github.com>2023-10-19 16:55:04 +0800
committerGitHub <noreply@github.com>2023-10-19 16:55:04 +0800
commit0c4987aa86dc5210c11024b74f3f8947ba1358ac (patch)
treed133948be851cb4209a006237dd99fda25d1909c /src
parentd9cc84b3732fe9cb0c819b93d67d2a6420f6a065 (diff)
Table formatting logic overhaul (#305)
* Table formatting logic overhaul - Columns now auto-expand and auto-shrink proportionally - Data column selection logic is now set per-table - Necessary boilerplate added to allow tables with more (or fewer) columns in the future * Better naming: `TableLayout` -> `DisplayLayout` * Fix clippy complaints * Optimise layout cutoff widths - These values are pretty much arbitrary. I'm open to further optimising them in the future. * Updated test snapshots to match new layout settings * Remove unnecessary logging * Correct `debug_fn` impl for `column_selector` * Further optimise bandwidth column display * Update test snapshots * Layout width preset minor adjustment
Diffstat (limited to 'src')
-rw-r--r--src/display/components/display_bandwidth.rs28
-rw-r--r--src/display/components/header_details.rs26
-rw-r--r--src/display/components/layout.rs2
-rw-r--r--src/display/components/table.rs559
-rw-r--r--src/display/ui.rs2
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__basic_only_addresses.snap4
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__basic_only_connections.snap4
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__basic_only_processes.snap4
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__basic_processes_with_dns_queries.snap4
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__basic_startup.snap6
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__bi_directional_traffic.snap12
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__layout-full-width-under-30-height-draw_events.snap14
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__layout-under-120-width-full-height-draw_events.snap24
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__layout-under-120-width-under-30-height-draw_events.snap14
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__layout-under-50-width-under-50-height-draw_events.snap24
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__layout-under-70-width-under-30-height-draw_events.snap14
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__multiple_connections_from_remote_address.snap14
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__multiple_packets_of_traffic_from_different_connections.snap16
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__multiple_packets_of_traffic_from_single_connection.snap12
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__multiple_processes_with_multiple_connections.snap24
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__no_resolve_mode.snap26
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__one_packet_of_traffic.snap12
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__one_process_with_multiple_connections.snap14
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__pause_by_space.snap8
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__rearranged_by_tab.snap32
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes.snap26
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes_bi_directional.snap26
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes_bi_directional_total.snap26
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__sustained_traffic_from_multiple_processes_total.snap26
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__sustained_traffic_from_one_process.snap18
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__sustained_traffic_from_one_process_total.snap18
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__traffic_with_host_names.snap26
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__traffic_with_winch_event.snap12
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__truncate_long_hostnames.snap26
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__two_packets_only_addresses.snap8
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__two_packets_only_connections.snap8
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__two_packets_only_processes.snap8
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__two_windows_split_horizontally.snap6
-rw-r--r--src/tests/cases/snapshots/bandwhich__tests__cases__ui__two_windows_split_vertically.snap4
39 files changed, 623 insertions, 514 deletions
diff --git a/src/display/components/display_bandwidth.rs b/src/display/components/display_bandwidth.rs
index d34ffe1..4c7e010 100644
--- a/src/display/components/display_bandwidth.rs
+++ b/src/display/components/display_bandwidth.rs
@@ -2,25 +2,23 @@ use std::fmt;
pub struct DisplayBandwidth {
pub bandwidth: f64,
- pub as_rate: bool,
}
impl fmt::Display for DisplayBandwidth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let suffix = if self.as_rate { "ps" } else { "" };
- if self.bandwidth > 999_999_999_999.0 {
- // 1024 * 1024 * 1024 * 1024
- write!(f, "{:.2}TiB{suffix}", self.bandwidth / 1_099_511_627_776.0,)
- } else if self.bandwidth > 999_999_999.0 {
- // 1024 * 1024 * 1024
- write!(f, "{:.2}GiB{suffix}", self.bandwidth / 1_073_741_824.0)
- } else if self.bandwidth > 999_999.0 {
- // 1024 * 1024
- write!(f, "{:.2}MiB{suffix}", self.bandwidth / 1_048_576.0)
- } else if self.bandwidth > 999.0 {
- write!(f, "{:.2}KiB{suffix}", self.bandwidth / 1024.0)
+ // see https://github.com/rust-lang/rust/issues/41620
+ let (div, suffix) = if self.bandwidth >= 1e12 {
+ (1_099_511_627_776.0, "TiB")
+ } else if self.bandwidth >= 1e9 {
+ (1_073_741_824.0, "GiB")
+ } else if self.bandwidth >= 1e6 {
+ (1_048_576.0, "MiB")
+ } else if self.bandwidth >= 1e3 {
+ (1024.0, "KiB")
} else {
- write!(f, "{}B{suffix}", self.bandwidth)
- }
+ (1.0, "B")
+ };
+
+ write!(f, "{:.2}{suffix}", self.bandwidth / div)
}
}
diff --git a/src/display/components/header_details.rs b/src/display/components/header_details.rs
index e997406..3f68984 100644
--- a/src/display/components/header_details.rs
+++ b/src/display/components/header_details.rs
@@ -80,19 +80,19 @@ impl<'a> HeaderDetails<'a> {
}
fn bandwidth_string(&self) -> String {
- let c_mode = self.state.cumulative_mode;
- format!(
- " Total Up / Down: {} / {}{}",
- DisplayBandwidth {
- bandwidth: self.state.total_bytes_uploaded as f64,
- as_rate: !c_mode,
- },
- DisplayBandwidth {
- bandwidth: self.state.total_bytes_downloaded as f64,
- as_rate: !c_mode,
- },
- if self.paused { " [PAUSED]" } else { "" }
- )
+ let t = if self.state.cumulative_mode {
+ "Data"
+ } else {
+ "Rate"
+ };
+ let up = DisplayBandwidth {
+ bandwidth: self.state.total_bytes_uploaded as f64,
+ };
+ let down = DisplayBandwidth {
+ bandwidth: self.state.total_bytes_downloaded as f64,
+ };
+ let paused = if self.paused { " [PAUSED]" } else { "" };
+ format!(" Total {t} (Up / Down): {up} / {down}{paused}")
}
fn render_elapsed_time(
diff --git a/src/display/components/layout.rs b/src/display/components/layout.rs
index 928be3d..60b7f23 100644
--- a/src/display/components/layout.rs
+++ b/src/display/components/layout.rs
@@ -27,7 +27,7 @@ fn top_app_and_bottom_split(rect: Rect) -> (Rect, Rect, Rect) {
pub struct Layout<'a> {
pub header: HeaderDetails<'a>,
- pub children: Vec<Table<'a>>,
+ pub children: Vec<Table>,
pub footer: HelpText,
}
diff --git a/src/display/components/table.rs b/src/display/components/table.rs
index 70e1b51..ed97349 100644
--- a/src/display/components/table.rs
+++ b/src/display/components/table.rs
@@ -1,9 +1,7 @@
-use std::{
- collections::{BTreeMap, HashMap},
- iter::FromIterator,
- net::IpAddr,
-};
+use std::{collections::HashMap, fmt, iter::FromIterator, net::IpAddr, ops::Index, rc::Rc};
+use derivative::Derivative;
+use itertools::Itertools;
use ratatui::{
backend::Backend,
layout::{Constraint, Rect},
@@ -11,95 +9,195 @@ use ratatui::{
terminal::Frame,
widgets::{Block, Borders, Row},
};
-use unicode_width::UnicodeWidthChar;
+use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::{
display::{Bandwidth, DisplayBandwidth, UIState},
network::{display_connection_string, display_ip_or_host},
};
-fn display_upload_and_download(bandwidth: &impl Bandwidth, total: bool) -> String {
- format!(
- "{} / {}",
- DisplayBandwidth {
- bandwidth: bandwidth.get_total_bytes_uploaded() as f64,
- as_rate: !total,
- },
- DisplayBandwidth {
- bandwidth: bandwidth.get_total_bytes_downloaded() as f64,
- as_rate: !total,
- },
- )
-}
-
-pub enum ColumnCount {
- Two,
- Three,
+/// The displayed layout choice of a table.
+/// Each value in the array is the width of each column.
+///
+/// Note that this only determines how a table is displayed, not what data it contains.
+///
+/// If we intend to display different number of columns in the future,
+/// then new variants should be added.
+#[derive(Copy, Clone, Debug)]
+pub enum DisplayLayout {
+ /// Show 2 columns.
+ C2([u16; 2]),
+ /// Show 3 columns.
+ C3([u16; 3]),
}
+impl Index<usize> for DisplayLayout {
+ type Output = u16;
-impl ColumnCount {
- pub fn as_u16(&self) -> u16 {
- match &self {
- ColumnCount::Two => 2,
- ColumnCount::Three => 3,
+ fn index(&self, i: usize) -> &Self::Output {
+ match self {
+ Self::C2(arr) => &arr[i],
+ Self::C3(arr) => &arr[i],
}
}
}
+impl DisplayLayout {
+ #[inline]
+ fn columns_count(&self) -> usize {
+ match self {
+ Self::C2(_) => 2,
+ Self::C3(_) => 3,
+ }
+ }
+ #[inline]
+ fn iter(&self) -> impl Iterator<Item = &u16> {
+ match self {
+ Self::C2(ws) => ws.iter(),
+ Self::C3(ws) => ws.iter(),
+ }
+ }
+ #[inline]
+ fn widths_sum(&self) -> u16 {
+ self.iter().sum()
+ }
+ /// Returns the computed actual width and the spacer width.
+ ///
+ /// See [`Table`] for layout rules.
+ fn compute_actual_widths(&self, available: u16) -> (Self, u16) {
+ let columns_count = self.columns_count() as u16;
+ let desired_min = self.widths_sum();
-pub struct ColumnData {
- column_count: ColumnCount,
- column_widths: Vec<u16>,
-}
+ // spacer max width is 2
+ let spacer = if available > desired_min {
+ ((available - desired_min) / (columns_count - 1)).min(2)
+ } else {
+ 0
+ };
+ let available_without_spacers = available - spacer * (columns_count - 1);
-pub struct Table<'a> {
- title: &'a str,
- column_names: &'a [&'a str],
- rows: Vec<Vec<String>>,
- breakpoints: BTreeMap<u16, ColumnData>,
-}
+ // multiplier
+ let m = available_without_spacers as f64 / desired_min as f64;
-fn truncate_iter_to_unicode_width<Input, Collect>(iter: Input, width: usize) -> Collect
-where
- Input: Iterator<Item = char>,
- Collect: FromIterator<char>,
-{
- let mut chunk_width = 0;
- iter.take_while(|ch| {
- chunk_width += ch.width().unwrap_or(0);
- chunk_width <= width
- })
- .collect()
+ // remainder width is arbitrarily given to column 0
+ let computed = match *self {
+ Self::C2([_w0, w1]) => {
+ let w1_new = (w1 as f64 * m).trunc() as u16;
+ Self::C2([available_without_spacers - w1_new, w1_new])
+ }
+ Self::C3([_w0, w1, w2]) => {
+ let w1_new = (w1 as f64 * m).trunc() as u16;
+ let w2_new = (w2 as f64 * m).trunc() as u16;
+ Self::C3([available_without_spacers - w1_new - w2_new, w1_new, w2_new])
+ }
+ };
+
+ (computed, spacer)
+ }
}
-fn truncate_middle(row: &str, max_length: u16) -> String {
- if max_length < 6 {
- truncate_iter_to_unicode_width(row.chars(), max_length as usize)
- } else if row.len() as u16 > max_length {
- let split_point = (max_length as usize / 2) - 3;
- // why 3? 5 is the max size of the truncation text ([...] or [..]), 3 is ~5/2
- let first_slice = truncate_iter_to_unicode_width::<_, String>(row.chars(), split_point);
- let second_slice =
- truncate_iter_to_unicode_width::<_, Vec<_>>(row.chars().rev(), split_point)
- .into_iter()
- .rev()
- .collect::<String>();
- if max_length % 2 == 0 {
- format!("{first_slice}[...]{second_slice}")
- } else {
- format!("{first_slice}[..]{second_slice}")
+/// All data of a table.
+///
+/// If tables with different number of columns are added in the future,
+/// then new variants should be added.
+#[derive(Clone, Debug)]
+enum TableData {
+ /// A table with 3 columns.
+ C3(NColsTableData<3>),
+}
+impl From<NColsTableData<3>> for TableData {
+ fn from(data: NColsTableData<3>) -> Self {
+ Self::C3(data)
+ }
+}
+impl TableData {
+ fn column_names(&self) -> &[&str] {
+ match self {
+ Self::C3(inner) => &inner.column_names,
+ }
+ }
+ fn rows(&self) -> Vec<&[String]> {
+ match self {
+ Self::C3(inner) => inner.rows.iter().map(|r| r.as_slice()).collect(),
}
- } else {
- row.to_string()
}
+ fn column_selector(&self) -> &dyn Fn(&DisplayLayout) -> Vec<usize> {
+ match self {
+ Self::C3(inner) => inner.column_selector.as_ref(),
+ }
+ }
+}
+
+/// All data of a table with `C` columns.
+///
+/// Note that the number of columns here is independent of the number of columns
+/// being actually shown. If width-constrained, we might only show some of the columns.
+#[derive(Clone, Derivative)]
+#[derivative(Debug)]
+struct NColsTableData<const C: usize> {
+ /// The name of each column.
+ column_names: [&'static str; C],
+ /// All rows of data.
+ rows: Vec<[String; C]>,
+ /// Function to determine which columns to show for a given layout.
+ ///
+ /// This function should return a vector of column indices.
+ /// The indices should be less than `C`; otherwise this will cause a runtime panic.
+ #[derivative(Debug(format_with = "debug_fn::<C>"))]
+ column_selector: Rc<ColumnSelectorFn>,
+}
+
+/// Clippy wanted me to write this. 💢
+type ColumnSelectorFn = dyn Fn(&DisplayLayout) -> Vec<usize>;
+
+fn debug_fn<const C: usize>(
+ _func: &Rc<ColumnSelectorFn>,
+ f: &mut fmt::Formatter,
+) -> Result<(), fmt::Error> {
+ write!(f, "Rc</* function pointer */>")
}
-impl<'a> Table<'a> {
+/// A table displayed by bandwhich.
+#[derive(Clone, Debug)]
+pub struct Table {
+ title: &'static str,
+ /// A layout mapping between minimum available width and the width of each column.
+ ///
+ /// Note that the width of each column here is the "desired minimum width".
+ ///
+ /// - Wt = available width of table
+ /// - Wd = sum of desired minimum width of each column
+ ///
+ /// - If `Wt >= Wd`, spacers with a maximum width of `2` will be inserted
+ /// between columns; and then the columns will proportionally expand.
+ /// - If `Wt < Wd`, columns will proportionally shrink.
+ width_cutoffs: Vec<(u16, DisplayLayout)>,
+ data: TableData,
+}
+impl Table {
pub fn create_connections_table(state: &UIState, ip_to_host: &HashMap<IpAddr, String>) -> Self {
- let connections_rows = state
+ use DisplayLayout as D;
+
+ let title = "Utilization by connection";
+ let width_cutoffs = vec![
+ (0, D::C2([32, 18])),
+ (80, D::C3([36, 12, 18])),
+ (100, D::C3([54, 18, 22])),
+ (120, D::C3([72, 24, 22])),
+ ];
+
+ let column_names = [
+ "Connection",
+ "Process",
+ if state.cumulative_mode {
+ "Data (Up / Down)"
+ } else {
+ "Rate (Up / Down)"
+ },
+ ];
+ let rows = state
.connections
.iter()
.map(|(connection, connection_data)| {
- vec![
+ [
display_connection_string(
connection,
ip_to_host,
@@ -110,199 +208,212 @@ impl<'a> Table<'a> {
]
})
.collect();
- let connections_title = "Utilization by connection";
- let connections_column_names = &["Connection", "Process", "Up / Down"];
- let mut breakpoints = BTreeMap::new();
- breakpoints.insert(
- 0,
- ColumnData {
- column_count: ColumnCount::Two,
- column_widths: vec![20, 23],
- },
- );
- breakpoints.insert(
- 70,
- ColumnData {
- column_count: ColumnCount::Three,
- column_widths: vec![30, 12, 23],
- },
- );
- breakpoints.insert(
- 100,
- ColumnData {
- column_count: ColumnCount::Three,
- column_widths: vec![60, 12, 23],
- },
- );
- breakpoints.insert(
- 140,
- ColumnData {
- column_count: ColumnCount::Three,
- column_widths: vec![100, 12, 23],
- },
- );
+ let column_selector = Rc::new(|layout: &D| match layout {
+ D::C2(_) => vec![0, 2],
+ D::C3(_) => vec![0, 1, 2],
+ });
+
Table {
- title: connections_title,
- column_names: connections_column_names,
- rows: connections_rows,
- breakpoints,
+ title,
+ width_cutoffs,
+ data: NColsTableData {
+ column_names,
+ rows,
+ column_selector,
+ }
+ .into(),
}
}
+
pub fn create_processes_table(state: &UIState) -> Self {
- let processes_rows = state
+ use DisplayLayout as D;
+
+ let title = "Utilization by process name";
+ let width_cutoffs = vec![
+ (0, D::C2([16, 18])),
+ (50, D::C3([16, 12, 20])),
+ (60, D::C3([24, 12, 20])),
+ (80, D::C3([36, 16, 24])),
+ ];
+
+ let column_names = [
+ "Process",
+ "Connections",
+ if state.cumulative_mode {
+ "Data (Up / Down)"
+ } else {
+ "Rate (Up / Down)"
+ },
+ ];
+ let rows = state
.processes
.iter()
.map(|(process_name, data_for_process)| {
- vec![
+ [
(*process_name).to_string(),
data_for_process.connection_count.to_string(),
display_upload_and_download(data_for_process, state.cumulative_mode),
]
})
.collect();
- let processes_title = "Utilization by process name";
- let processes_column_names = &["Process", "Connections", "Up / Down"];
- let mut breakpoints = BTreeMap::new();
- breakpoints.insert(
- 0,
- ColumnData {
- column_count: ColumnCount::Two,
- column_widths: vec![12, 23],
- },
- );
- breakpoints.insert(
- 50,
- ColumnData {
- column_count: ColumnCount::Three,
- column_widths: vec![12, 12, 23],
- },
- );
- breakpoints.insert(
- 100,
- ColumnData {
- column_count: ColumnCount::Three,
- column_widths: vec![40, 12, 23],
- },
- );
- breakpoints.insert(
- 140,
- ColumnData {
- column_count: ColumnCount::Three,
- column_widths: vec![40, 12, 23],
- },
- );
+ let column_selector = Rc::new(|layout: &D| match layout {
+ D::C2(_) => vec![0, 2],
+ D::C3(_) => vec![0, 1, 2],
+ });
+
Table {
- title: processes_title,
- column_names: processes_column_names,
- rows: processes_rows,
- breakpoints,
+ title,
+ width_cutoffs,
+ data: NColsTableData {
+ column_names,
+ rows,
+ column_selector,
+ }
+ .into(),
}
}
+
pub fn create_remote_addresses_table(
state: &UIState,
ip_to_host: &HashMap<IpAddr, String>,
) -> Self {
- let remote_addresses_rows = state
+ use DisplayLayout as D;
+
+ let title = "Utilization by remote address";
+ let width_cutoffs = vec![
+ (0, D::C2([16, 16])),
+ (40, D::C2([20, 16])),
+ (60, D::C3([24, 10, 20])),
+ (100, D::C3([54, 16, 24])),
+ ];
+
+ let column_names = [
+ "Remote Address",
+ "Connections",
+ if state.cumulative_mode {
+ "Data (Up / Down)"
+ } else {
+ "Rate (Up / Down)"
+ },
+ ];
+ let rows = state
.remote_addresses
.iter()
.map(|(remote_address, data_for_remote_address)| {
let remote_address = display_ip_or_host(*remote_address, ip_to_host);
- vec![
+ [
remote_address,
data_for_remote_address.connection_count.to_string(),
display_upload_and_download(data_for_remote_address, state.cumulative_mode),
]
})
.collect();
- let remote_addresses_title = "Utilization by remote address";
- let remote_addresses_column_names = &["Remote Address", "Connections", "Up / Down"];
- let mut breakpoints = BTreeMap::new();
- breakpoints.insert(
- 0,
- ColumnData {
- column_count: ColumnCount::Two,
- column_widths: vec![15, 20],
- },
- );
- breakpoints.insert(
- 70,
- ColumnData {
- column_count: ColumnCount::Three,
- column_widths: vec![30, 12, 23],
- },
- );
- breakpoints.insert(
- 100,
- ColumnData {
- column_count: ColumnCount::Three,
- column_widths: vec![60, 12, 23],
- },
- );
- breakpoints.insert(
- 140,
- ColumnData {
- column_count: ColumnCount::Three,
- column_widths: vec![100, 12, 23],
- },
- );
+ let column_selector = Rc::new(|layout: &D| match layout {
+ D::C2(_) => vec![0, 2],
+ D::C3(_) => vec![0, 1, 2],
+ });
+
Table {
- title: remote_addresses_title,
- column_names: remote_addresses_column_names,
- rows: remote_addresses_rows,
- breakpoints,
- }
- }
- pub fn render(&self, frame: &mut Frame<impl Backend>, rect: Rect) {
- let mut column_spacing: u16 = 0;
- let mut widths = &vec![];
- let mut column_count: &ColumnCount = &ColumnCount::Three;
-
- for (width_breakpoint, column_data) in self.breakpoints.iter() {
- if *width_breakpoint < rect.width {
- widths = &column_data.column_widths;
- column_count = &column_data.column_count;
-
- let total_column_width: u16 = widths.iter().sum();
- if rect.width < total_column_width - column_count.as_u16() {
- column_spacing = 0;
- } else {
- column_spacing = (rect.width - total_column_width) / column_count.as_u16();
- }
+ title,
+ width_cutoffs,
+ data: NColsTableData {
+ column_names,
+ rows,
+ column_selector,
}
+ .into(),
}
+ }
- let column_names = match column_count {
- ColumnCount::Two => {
- vec![self.column_names[0], self.column_names[2]] // always lose the middle column when needed
- }
- ColumnCount::Three => vec![
- self.column_names[0],
- self.column_names[1],
- self.column_names[2],
- ],
+ /// See [`Table`] for layout rules.
+ pub fn render(&self, frame: &mut Frame<impl Backend>, rect: Rect) {
+ let (computed_layout, spacer_width) = {
+ // pick the largest possible layout, constrained by the available width
+ let &(_, layout) = self
+ .width_cutoffs
+ .iter()
+ .rev()
+ .find(|(cutoff, _)| rect.width > *cutoff)
+ .unwrap(); // all cutoff tables have a 0-width entry
+ layout.compute_actual_widths(rect.width)
};
- let rows = self.rows.iter().map(|row| match column_count {
- ColumnCount::Two => vec![
- truncate_middle(&row[0], widths[0]),
- truncate_middle(&row[2], widths[1]),
- ],
- ColumnCount::Three => vec![
- truncate_middle(&row[0], widths[0]),
- truncate_middle(&row[1], widths[1]),
- truncate_middle(&row[2], widths[2]),
- ],
- });
+ let columns_to_show = self.data.column_selector()(&computed_layout);
+ let column_names: Vec<_> = columns_to_show
+ .iter()
+ .copied()
+ .map(|i| self.data.column_names()[i])
+ .collect();
+
+ // text needs to react to column widths
+