term_table/row.rs
1use crate::table_cell::{string_width, Alignment, TableCell};
2use crate::{RowPosition, TableStyle};
3use std::cmp::max;
4use unicode_width::UnicodeWidthChar;
5
6/// A set of table cells
7#[derive(Debug, Clone)]
8pub struct Row {
9 pub cells: Vec<TableCell>,
10 /// Whether the row should have a top boarder or not
11 pub has_separator: bool,
12}
13
14impl Row {
15 pub fn new<I, T>(cells: I) -> Row
16 where
17 T: Into<TableCell>,
18 I: IntoIterator<Item = T>,
19 {
20 let mut row = Row {
21 cells: vec![],
22 has_separator: true,
23 };
24
25 for entry in cells.into_iter() {
26 row.cells.push(entry.into());
27 }
28
29 row
30 }
31
32 pub fn empty() -> Row {
33 Row {
34 cells: vec![],
35 has_separator: true,
36 }
37 }
38
39 pub fn without_separator<I, T>(cells: I) -> Row
40 where
41 T: Into<TableCell>,
42 I: IntoIterator<Item = T>,
43 {
44 let mut row = Self::new(cells);
45 row.has_separator = false;
46 row
47 }
48
49 /// Formats a row based on the provided table style
50 pub fn format(&self, column_widths: &[usize], style: &TableStyle) -> String {
51 let mut buf = String::new();
52
53 // Since a cell can span multiple columns we need to track
54 // how many columns we have actually spanned. We cannot just depend
55 // on the index of the current cell when iterating
56 let mut spanned_columns = 0;
57
58 // The height of the row determined by how many times a cell had to wrap
59 let mut row_height = 0;
60
61 // Wrapped cell content
62 let mut wrapped_cells = Vec::new();
63
64 // The first thing we do is wrap the cells if their
65 // content is greater than the max width of the column they are in
66 for cell in &self.cells {
67 let mut width = 0;
68 // Iterate from 0 to the cell's col_span and add up all the max width
69 // values for each column so we can properly pad the cell content later
70 for j in 0..cell.col_span {
71 width += column_widths[j + spanned_columns];
72 }
73 // Wrap to the total width - col_span to account for separators
74 let wrapped_cell = cell.wrapped_content(width + cell.col_span - 1);
75 row_height = max(row_height, wrapped_cell.len());
76 wrapped_cells.push(wrapped_cell);
77 spanned_columns += cell.col_span;
78 }
79
80 // reset spanned_columns so we can reuse it in the next loop
81 spanned_columns = 0;
82
83 // Row lines to combine into the final string at the end
84 let mut lines = vec![String::new(); row_height];
85
86 // We need to iterate over all of the column widths
87 // We may not have as many cells as column widths, or the cells may not even span
88 // as many columns as are in column widths. In that case weill will create empty cells
89 for col_idx in 0..column_widths.len() {
90 // Check to see if we actually have a cell for the column index
91 // Otherwise we will just need to print out empty space as filler
92 if self.cells.len() > col_idx {
93 // Number of characters spanned by column
94 let mut cell_span = 0;
95
96 // Get the cell using the column index
97 //
98 // This is a little bit confusing because cells and columns aren't always one to one
99 // We may have fewer cells than columns or some cells may span multiple columns
100 // If there are fewer cells than columns we just end drawing empty cells in the else block
101 // If there are fewer cells than columns but they span the total number of columns we just break out
102 // of the outer for loop at the end. We know how many cells we've spanned by adding the cell's col_span to spanned_columns
103 let cell = &self.cells[col_idx];
104 // Calculate the cell span by adding up the widths of the columns spanned by the cell
105 for c in 0..cell.col_span {
106 cell_span += column_widths[spanned_columns + c];
107 }
108 // Since cells can wrap we need to loop over all of the lines
109 for (line_idx, line) in lines.iter_mut().enumerate().take(row_height) {
110 // Check to see if the wrapped cell has a line for the line index
111 if wrapped_cells[col_idx].len() > line_idx {
112 // We may need to pad the cell if it's contents are not as wide as some other cell in the column
113 let mut padding = 0;
114 // We need to calculate the string_width because some characters take up extra space and we need to
115 // ignore ANSI characters
116 let str_width = string_width(&wrapped_cells[col_idx][line_idx]);
117 if cell_span >= str_width {
118 padding += cell_span - str_width;
119 // If the cols_span is greater than one we need to add extra padding for the missing vertical characters
120 if cell.col_span > 1 {
121 padding += style.vertical.width().unwrap_or_default()
122 * (cell.col_span - 1); // Subtract one since we add a vertical character to the beginning
123 }
124 }
125
126 // Finally we can push the string into the lines vec
127 line.push_str(
128 format!(
129 "{}{}",
130 style.vertical,
131 self.pad_string(
132 padding,
133 cell.alignment,
134 &wrapped_cells[col_idx][line_idx]
135 )
136 )
137 .as_str(),
138 );
139 } else {
140 // If the cell doesn't have any content for this line just fill it with empty space
141 line.push_str(
142 format!(
143 "{}{}",
144 style.vertical,
145 str::repeat(
146 " ",
147 column_widths[spanned_columns] * cell.col_span + cell.col_span
148 - 1
149 )
150 )
151 .as_str(),
152 );
153 }
154 }
155 // Keep track of how many columns we have actually spanned since
156 // cells can be wider than a single column
157 spanned_columns += cell.col_span;
158 } else {
159 // If we don't have a cell for the coulumn then we just create an empty one
160 for line in lines.iter_mut().take(row_height) {
161 line.push_str(
162 format!(
163 "{}{}",
164 style.vertical,
165 str::repeat(" ", column_widths[spanned_columns])
166 )
167 .as_str(),
168 );
169 }
170 // Add one to the spanned column since the empty space is basically a cell
171 spanned_columns += 1;
172 }
173 // If we have spanned as many columns as there are then just break out of the loop
174 if spanned_columns == column_widths.len() {
175 break;
176 }
177 }
178 // Finally add all the lines together to create the row content
179 for line in &lines {
180 buf.push_str(line.clone().as_str());
181 buf.push(style.vertical);
182 buf.push('\n');
183 }
184 buf.pop();
185
186 buf
187 }
188
189 /// Generates the top separator for a row.
190 ///
191 /// The previous seperator is used to determine junction characters
192 pub fn gen_separator(
193 &self,
194 column_widths: &[usize],
195 style: &TableStyle,
196 row_position: RowPosition,
197 previous_separator: Option<String>,
198 ) -> String {
199 let mut buf = String::new();
200
201 // If the first cell has a col_span > 1 we need to set the next
202 // intersection point to that value
203 let mut next_intersection = match self.cells.first() {
204 Some(cell) => cell.col_span,
205 None => 1,
206 };
207
208 // Push the initial char for the row
209 buf.push(style.start_for_position(row_position));
210
211 let mut current_column = 0;
212
213 for (i, column_width) in column_widths.iter().enumerate() {
214 if i == next_intersection {
215 // Draw the intersection character for the start of the column
216 buf.push(style.intersect_for_position(row_position));
217
218 current_column += 1;
219
220 // If we still have remaining cells then we use the col_span to determine
221 // when the next intersection character should be drawn
222 if self.cells.len() > current_column {
223 next_intersection += self.cells[current_column].col_span;
224 } else {
225 // Otherwise we just draw an intersection for every column
226 next_intersection += 1;
227 }
228 } else if i > 0 {
229 // This means the current cell has a col_span > 1
230 buf.push(style.horizontal);
231 }
232 // Fill in all of the horizontal space
233 buf.push_str(
234 str::repeat(style.horizontal.to_string().as_str(), *column_width).as_str(),
235 );
236 }
237
238 buf.push(style.end_for_position(row_position));
239
240 let mut out = String::new();
241
242 // Merge the previous seperator string with the current buffer
243 // This will handle cases where a cell above/below has a different col_span value
244 match previous_separator {
245 Some(prev) => {
246 for pair in buf.chars().zip(prev.chars()) {
247 if pair.0 == style.outer_left_vertical || pair.0 == style.outer_right_vertical {
248 // Always take the start and end characters of the current buffer
249 out.push(pair.0);
250 } else if pair.0 != style.horizontal || pair.1 != style.horizontal {
251 out.push(style.merge_intersection_for_position(
252 pair.1,
253 pair.0,
254 row_position,
255 ));
256 } else {
257 out.push(style.horizontal);
258 }
259 }
260 out
261 }
262 None => buf,
263 }
264 }
265
266 /// Returns a vector of split cell widths.
267 ///
268 /// A split width is the cell's total width divided by it's col_span value.
269 ///
270 /// Each cell's split width value is pushed into the resulting vector col_span times.
271 /// Returns a vec of tuples containing the cell width and the min cell width
272 pub fn split_column_widths(&self) -> Vec<(f32, usize)> {
273 let mut res = Vec::new();
274 for cell in &self.cells {
275 let val = cell.split_width();
276
277 let min = (cell.min_width() as f32 / cell.col_span as f32) as usize;
278
279 let add_one = cell.min_width() as f32 % cell.col_span as f32 > 0.001;
280 for i in 0..cell.col_span {
281 if add_one && i == cell.col_span - 1 {
282 res.push((val + 1.0, min + 1));
283 } else {
284 res.push((val, min));
285 }
286 }
287 }
288
289 res
290 }
291
292 /// Number of columns in the row.
293 ///
294 /// This is the sum of all cell's col_span values
295 pub fn num_columns(&self) -> usize {
296 self.cells.iter().map(|x| x.col_span).sum()
297 }
298
299 /// Pads a string accoding to the provided alignment
300 fn pad_string(&self, padding: usize, alignment: Alignment, text: &str) -> String {
301 match alignment {
302 Alignment::Left => return format!("{}{}", text, str::repeat(" ", padding)),
303 Alignment::Right => return format!("{}{}", str::repeat(" ", padding), text),
304 Alignment::Center => {
305 let half_padding = padding as f32 / 2.0;
306 return format!(
307 "{}{}{}",
308 str::repeat(" ", half_padding.ceil() as usize),
309 text,
310 str::repeat(" ", half_padding.floor() as usize)
311 );
312 }
313 }
314 }
315
316 /// Adds a cell to the row
317 pub fn add_cell(&mut self, cell: TableCell) {
318 self.cells.push(cell);
319 }
320
321}