on 2158366 help sign in
534 lines 17 kB history raw
1use crate::{
2 buffer::Buffer,
3 layout::Constraint,
4 text::Text,
5 widgets::{Block, Widget},
6};
7use cassowary::{
8 strength::{MEDIUM, REQUIRED, WEAK},
9 WeightedRelation::*,
10 {Expression, Solver},
11};
12use helix_core::unicode::width::UnicodeWidthStr;
13use helix_view::graphics::{Rect, Style};
14use std::collections::HashMap;
16/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
17///
18/// It can be created from anything that can be converted to a [`Text`].
19/// ```rust
20/// # use helix_tui::widgets::Cell;
21/// # use helix_tui::text::{Span, Spans, Text};
22/// # use helix_view::graphics::{Style, Modifier};
23/// Cell::from("simple string");
24///
25/// Cell::from(Span::from("span"));
26///
27/// Cell::from(Spans::from(vec![
28/// Span::raw("a vec of "),
29/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD))
30/// ]));
31///
32/// Cell::from(Text::from("a text"));
33/// ```
34///
35/// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling
36/// capabilities of [`Text`].
37#[derive(Debug, Clone, PartialEq, Default)]
38pub struct Cell<'a> {
39 pub content: Text<'a>,
40 style: Style,
43impl<'a> Cell<'a> {
44 /// Set the `Style` of this cell.
45 pub fn style(mut self, style: Style) -> Self {
46 self.style = style;
47 self
48 }
51impl<'a, T> From<T> for Cell<'a>
52where
53 T: Into<Text<'a>>,
55 fn from(content: T) -> Cell<'a> {
56 Cell {
57 content: content.into(),
58 style: Style::default(),
59 }
60 }
63/// Holds data to be displayed in a [`Table`] widget.
64///
65/// A [`Row`] is a collection of cells. It can be created from simple strings:
66/// ```rust
67/// # use helix_tui::widgets::Row;
68/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
69/// ```
70///
71/// But if you need a bit more control over individual cells, you can explicity create [`Cell`]s:
72/// ```rust
73/// # use helix_tui::widgets::{Row, Cell};
74/// # use helix_view::graphics::{Style, Color};
75/// Row::new(vec![
76/// Cell::from("Cell1"),
77/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
78/// ]);
79/// ```
80///
81/// By default, a row has a height of 1 but you can change this using [`Row::height`].
82#[derive(Debug, Clone, PartialEq, Default)]
83pub struct Row<'a> {
84 pub cells: Vec<Cell<'a>>,
85 height: u16,
86 style: Style,
87 bottom_margin: u16,
90impl<'a> Row<'a> {
91 /// Creates a new [`Row`] from an iterator where items can be converted to a [`Cell`].
92 pub fn new<T>(cells: T) -> Self
93 where
94 T: IntoIterator,
95 T::Item: Into<Cell<'a>>,
96 {
97 Self {
98 height: 1,
99 cells: cells.into_iter().map(|c| c.into()).collect(),
100 style: Style::default(),
101 bottom_margin: 0,
102 }
103 }
105 /// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this
106 /// height will see its content truncated.
107 pub fn height(mut self, height: u16) -> Self {
108 self.height = height;
109 self
110 }
112 /// Set the [`Style`] of the entire row. This [`Style`] can be overriden by the [`Style`] of a
113 /// any individual [`Cell`] or event by their [`Text`] content.
114 pub fn style(mut self, style: Style) -> Self {
115 self.style = style;
116 self
117 }
119 /// Set the bottom margin. By default, the bottom margin is `0`.
120 pub fn bottom_margin(mut self, margin: u16) -> Self {
121 self.bottom_margin = margin;
122 self
123 }
125 /// Returns the total height of the row.
126 fn total_height(&self) -> u16 {
127 self.height.saturating_add(self.bottom_margin)
128 }
131/// A widget to display data in formatted columns.
132///
133/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
134/// ```rust
135/// # use helix_tui::widgets::{Block, Borders, Table, Row, Cell};
136/// # use helix_tui::layout::Constraint;
137/// # use helix_view::graphics::{Style, Color, Modifier};
138/// # use helix_tui::text::{Text, Spans, Span};
139/// Table::new(vec![
140/// // Row can be created from simple strings.
141/// Row::new(vec!["Row11", "Row12", "Row13"]),
142/// // You can style the entire row.
143/// Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)),
144/// // If you need more control over the styling you may need to create Cells directly
145/// Row::new(vec![
146/// Cell::from("Row31"),
147/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
148/// Cell::from(Spans::from(vec![
149/// Span::raw("Row"),
150/// Span::styled("33", Style::default().fg(Color::Green))
151/// ])),
152/// ]),
153/// // If a Row need to display some content over multiple lines, you just have to change
154/// // its height.
155/// Row::new(vec![
156/// Cell::from("Row\n41"),
157/// Cell::from("Row\n42"),
158/// Cell::from("Row\n43"),
159/// ]).height(2),
160/// ])
161/// // You can set the style of the entire Table.
162/// .style(Style::default().fg(Color::White))
163/// // It has an optional header, which is simply a Row always visible at the top.
164/// .header(
165/// Row::new(vec!["Col1", "Col2", "Col3"])
166/// .style(Style::default().fg(Color::Yellow))
167/// // If you want some space between the header and the rest of the rows, you can always
168/// // specify some margin at the bottom.
169/// .bottom_margin(1)
170/// )
171/// // As any other widget, a Table can be wrapped in a Block.
172/// .block(Block::default().title("Table"))
173/// // Columns widths are constrained in the same way as Layout...
174/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
175/// // ...and they can be separated by a fixed spacing.
176/// .column_spacing(1)
177/// // If you wish to highlight a row in any specific way when it is selected...
178/// .highlight_style(Style::default().add_modifier(Modifier::BOLD))
179/// // ...and potentially show a symbol in front of the selection.
180/// .highlight_symbol(">>");
181/// ```
182#[derive(Debug, Clone, PartialEq)]
183pub struct Table<'a> {
184 /// A block to wrap the widget in
185 block: Option<Block<'a>>,
186 /// Base style for the widget
187 style: Style,
188 /// Width constraints for each column
189 widths: &'a [Constraint],
190 /// Space between each column
191 column_spacing: u16,
192 /// Style used to render the selected row
193 highlight_style: Style,
194 /// Symbol in front of the selected rom
195 highlight_symbol: Option<&'a str>,
196 /// Optional header
197 header: Option<Row<'a>>,
198 /// Data to display in each row
199 rows: Vec<Row<'a>>,
202impl<'a> Table<'a> {
203 pub fn new<T>(rows: T) -> Self
204 where
205 T: IntoIterator<Item = Row<'a>>,
206 {
207 Self {
208 block: None,
209 style: Style::default(),
210 widths: &[],
211 column_spacing: 1,
212 highlight_style: Style::default(),
213 highlight_symbol: None,
214 header: None,
215 rows: rows.into_iter().collect(),
216 }
217 }
219 pub fn block(mut self, block: Block<'a>) -> Self {
220 self.block = Some(block);
221 self
222 }
224 pub fn header(mut self, header: Row<'a>) -> Self {
225 self.header = Some(header);
226 self
227 }
229 pub fn widths(mut self, widths: &'a [Constraint]) -> Self {
230 let between_0_and_100 = |&w| match w {
231 Constraint::Percentage(p) => p <= 100,
232 _ => true,
233 };
234 assert!(
235 widths.iter().all(between_0_and_100),
236 "Percentages should be between 0 and 100 inclusively."
237 );
238 self.widths = widths;
239 self
240 }
242 pub fn style(mut self, style: Style) -> Self {
243 self.style = style;
244 self
245 }
247 pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
248 self.highlight_symbol = Some(highlight_symbol);
249 self
250 }
252 pub fn highlight_style(mut self, highlight_style: Style) -> Self {
253 self.highlight_style = highlight_style;
254 self
255 }
257 pub fn column_spacing(mut self, spacing: u16) -> Self {
258 self.column_spacing = spacing;
259 self
260 }
262 fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
263 let mut solver = Solver::new();
264 let mut var_indices = HashMap::new();
265 let mut ccs = Vec::new();
266 let mut variables = Vec::new();
267 for i in 0..self.widths.len() {
268 let var = cassowary::Variable::new();
269 variables.push(var);
270 var_indices.insert(var, i);
271 }
272 let spacing_width = (variables.len() as u16).saturating_sub(1) * self.column_spacing;
273 let mut available_width = max_width.saturating_sub(spacing_width);
274 if has_selection {
275 let highlight_symbol_width =
276 self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0);
277 available_width = available_width.saturating_sub(highlight_symbol_width);
278 }
279 for (i, constraint) in self.widths.iter().enumerate() {
280 ccs.push(variables[i] | GE(WEAK) | 0.);
281 ccs.push(match *constraint {
282 Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v),
283 Constraint::Percentage(v) => {
284 variables[i] | EQ(WEAK) | (f64::from(v * available_width) / 100.0)
285 }
286 Constraint::Ratio(n, d) => {
287 variables[i]
288 | EQ(WEAK)
289 | (f64::from(available_width) * f64::from(n) / f64::from(d))
290 }
291 Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v),
292 Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v),
293 })
294 }
295 solver
296 .add_constraint(
297 variables
298 .iter()
299 .fold(Expression::from_constant(0.), |acc, v| acc + *v)
300 | LE(REQUIRED)
301 | f64::from(available_width),
302 )
303 .unwrap();
304 solver.add_constraints(&ccs).unwrap();
305 let mut widths = vec![0; variables.len()];
306 for &(var, value) in solver.fetch_changes() {
307 let index = var_indices[&var];
308 let value = if value.is_sign_negative() {
309 0
310 } else {
311 value.round() as u16
312 };
313 widths[index] = value;
314 }
315 // Cassowary could still return columns widths greater than the max width when there are
316 // fixed length constraints that cannot be satisfied. Therefore, we clamp the widths from
317 // left to right.
318 let mut available_width = max_width;
319 for w in &mut widths {
320 *w = available_width.min(*w);
321 available_width = available_width
322 .saturating_sub(*w)
323 .saturating_sub(self.column_spacing);
324 }
325 widths
326 }
328 fn get_row_bounds(
329 &self,
330 selected: Option<usize>,
331 offset: usize,
332 max_height: u16,
333 ) -> (usize, usize) {
334 let mut start = offset;
335 let mut end = offset;
336 let mut height = 0;
337 for item in self.rows.iter().skip(offset) {
338 if height + item.height > max_height {
339 break;
340 }
341 height += item.total_height();
342 end += 1;
343 }
345 let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
346 while selected >= end {
347 height = height.saturating_add(self.rows[end].total_height());
348 end += 1;
349 while height > max_height {
350 height = height.saturating_sub(self.rows[start].total_height());
351 start += 1;
352 }
353 }
354 while selected < start {
355 start -= 1;
356 height = height.saturating_add(self.rows[start].total_height());
357 while height > max_height {
358 end -= 1;
359 height = height.saturating_sub(self.rows[end].total_height());
360 }
361 }
362 (start, end)
363 }
366#[derive(Debug, Clone)]
367pub struct TableState {
368 pub offset: usize,
369 pub selected: Option<usize>,
372impl Default for TableState {
373 fn default() -> TableState {
374 TableState {
375 offset: 0,
376 selected: None,
377 }
378 }
381impl TableState {
382 pub fn selected(&self) -> Option<usize> {
383 self.selected
384 }
386 pub fn select(&mut self, index: Option<usize>) {
387 self.selected = index;
388 if index.is_none() {
389 self.offset = 0;
390 }
391 }
394// impl<'a> StatefulWidget for Table<'a> {
395impl<'a> Table<'a> {
396 // type State = TableState;
398 pub fn render_table(mut self, area: Rect, buf: &mut Buffer, state: &mut TableState) {
399 if area.area() == 0 {
400 return;
401 }
402 buf.set_style(area, self.style);
403 let table_area = match self.block.take() {
404 Some(b) => {
405 let inner_area = b.inner(area);
406 b.render(area, buf);
407 inner_area
408 }
409 None => area,
410 };
412 let has_selection = state.selected.is_some();
413 let columns_widths = self.get_columns_widths(table_area.width, has_selection);
414 let highlight_symbol = self.highlight_symbol.unwrap_or("");
415 let blank_symbol = " ".repeat(highlight_symbol.width());
416 let mut current_height = 0;
417 let mut rows_height = table_area.height;
419 // Draw header
420 if let Some(ref header) = self.header {
421 let max_header_height = table_area.height.min(header.total_height());
422 buf.set_style(
423 Rect {
424 x: table_area.left(),
425 y: table_area.top(),
426 width: table_area.width,
427 height: table_area.height.min(header.height),
428 },
429 header.style,
430 );
431 let mut col = table_area.left();
432 if has_selection {
433 col += (highlight_symbol.width() as u16).min(table_area.width);
434 }
435 for (width, cell) in columns_widths.iter().zip(header.cells.iter()) {
436 render_cell(
437 buf,
438 cell,
439 Rect {
440 x: col,
441 y: table_area.top(),
442 width: *width,
443 height: max_header_height,
444 },
445 );
446 col += *width + self.column_spacing;
447 }
448 current_height += max_header_height;
449 rows_height = rows_height.saturating_sub(max_header_height);
450 }
452 // Draw rows
453 if self.rows.is_empty() {
454 return;
455 }
456 let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height);
457 state.offset = start;
458 for (i, table_row) in self
459 .rows
460 .iter_mut()
461 .enumerate()
462 .skip(state.offset)
463 .take(end - start)
464 {
465 let (row, col) = (table_area.top() + current_height, table_area.left());
466 current_height += table_row.total_height();
467 let table_row_area = Rect {
468 x: col,
469 y: row,
470 width: table_area.width,
471 height: table_row.height,
472 };
473 buf.set_style(table_row_area, table_row.style);
474 let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
475 let table_row_start_col = if has_selection {
476 let symbol = if is_selected {
477 highlight_symbol
478 } else {
479 &blank_symbol
480 };
481 let (col, _) =
482 buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
483 col
484 } else {
485 col
486 };
487 let mut col = table_row_start_col;
488 for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
489 render_cell(
490 buf,
491 cell,
492 Rect {
493 x: col,
494 y: row,
495 width: *width,
496 height: table_row.height,
497 },
498 );
499 col += *width + self.column_spacing;
500 }
501 if is_selected {
502 buf.set_style(table_row_area, self.highlight_style);
503 }
504 }
505 }
508fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
509 buf.set_style(area, cell.style);
510 for (i, spans) in cell.content.lines.iter().enumerate() {
511 if i as u16 >= area.height {
512 break;
513 }
514 buf.set_spans(area.x, area.y + i as u16, spans, area.width);
515 }
518impl<'a> Widget for Table<'a> {
519 fn render(self, area: Rect, buf: &mut Buffer) {
520 let mut state = TableState::default();
521 Table::render_table(self, area, buf, &mut state);
522 }
525#[cfg(test)]
526mod tests {
527 use super::*;
529 #[test]
530 #[should_panic]
531 fn table_invalid_percentages() {
532 Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
533 }