@@ -0,0 +1,717 @@
1+use helix_tui::{
2+ backend::TestBackend,
3+ buffer::Buffer,
4+ layout::Constraint,
5+ style::{Color, Modifier, Style},
6+ text::{Span, Spans},
7+ widgets::{Block, Borders, Cell, Row, Table, TableState},
8+ Terminal,
9+};
10+
11+#[test]
12+fn widgets_table_column_spacing_can_be_changed() {
13+ let test_case = |column_spacing, expected| {
14+ let backend = TestBackend::new(30, 10);
15+ let mut terminal = Terminal::new(backend).unwrap();
16+
17+ terminal
18+ .draw(|f| {
19+ let size = f.size();
20+ let table = Table::new(vec![
21+ Row::new(vec!["Row11", "Row12", "Row13"]),
22+ Row::new(vec!["Row21", "Row22", "Row23"]),
23+ Row::new(vec!["Row31", "Row32", "Row33"]),
24+ Row::new(vec!["Row41", "Row42", "Row43"]),
25+ ])
26+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
27+ .block(Block::default().borders(Borders::ALL))
28+ .widths(&[
29+ Constraint::Length(5),
30+ Constraint::Length(5),
31+ Constraint::Length(5),
32+ ])
33+ .column_spacing(column_spacing);
34+ f.render_widget(table, size);
35+ })
36+ .unwrap();
37+ terminal.backend().assert_buffer(&expected);
38+ };
39+
40+ // no space between columns
41+ test_case(
42+ 0,
43+ Buffer::with_lines(vec![
44+ "┌────────────────────────────┐",
45+ "│Head1Head2Head3 │",
46+ "│ │",
47+ "│Row11Row12Row13 │",
48+ "│Row21Row22Row23 │",
49+ "│Row31Row32Row33 │",
50+ "│Row41Row42Row43 │",
51+ "│ │",
52+ "│ │",
53+ "└────────────────────────────┘",
54+ ]),
55+ );
56+
57+ // one space between columns
58+ test_case(
59+ 1,
60+ Buffer::with_lines(vec![
61+ "┌────────────────────────────┐",
62+ "│Head1 Head2 Head3 │",
63+ "│ │",
64+ "│Row11 Row12 Row13 │",
65+ "│Row21 Row22 Row23 │",
66+ "│Row31 Row32 Row33 │",
67+ "│Row41 Row42 Row43 │",
68+ "│ │",
69+ "│ │",
70+ "└────────────────────────────┘",
71+ ]),
72+ );
73+
74+ // enough space to just not hide the third column
75+ test_case(
76+ 6,
77+ Buffer::with_lines(vec![
78+ "┌────────────────────────────┐",
79+ "│Head1 Head2 Head3 │",
80+ "│ │",
81+ "│Row11 Row12 Row13 │",
82+ "│Row21 Row22 Row23 │",
83+ "│Row31 Row32 Row33 │",
84+ "│Row41 Row42 Row43 │",
85+ "│ │",
86+ "│ │",
87+ "└────────────────────────────┘",
88+ ]),
89+ );
90+
91+ // enough space to hide part of the third column
92+ test_case(
93+ 7,
94+ Buffer::with_lines(vec![
95+ "┌────────────────────────────┐",
96+ "│Head1 Head2 Head│",
97+ "│ │",
98+ "│Row11 Row12 Row1│",
99+ "│Row21 Row22 Row2│",
100+ "│Row31 Row32 Row3│",
101+ "│Row41 Row42 Row4│",
102+ "│ │",
103+ "│ │",
104+ "└────────────────────────────┘",
105+ ]),
106+ );
107+}
108+
109+#[test]
110+fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
111+ let test_case = |widths, expected| {
112+ let backend = TestBackend::new(30, 10);
113+ let mut terminal = Terminal::new(backend).unwrap();
114+
115+ terminal
116+ .draw(|f| {
117+ let size = f.size();
118+ let table = Table::new(vec![
119+ Row::new(vec!["Row11", "Row12", "Row13"]),
120+ Row::new(vec!["Row21", "Row22", "Row23"]),
121+ Row::new(vec!["Row31", "Row32", "Row33"]),
122+ Row::new(vec!["Row41", "Row42", "Row43"]),
123+ ])
124+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
125+ .block(Block::default().borders(Borders::ALL))
126+ .widths(widths);
127+ f.render_widget(table, size);
128+ })
129+ .unwrap();
130+ terminal.backend().assert_buffer(&expected);
131+ };
132+
133+ // columns of zero width show nothing
134+ test_case(
135+ &[
136+ Constraint::Length(0),
137+ Constraint::Length(0),
138+ Constraint::Length(0),
139+ ],
140+ Buffer::with_lines(vec![
141+ "┌────────────────────────────┐",
142+ "│ │",
143+ "│ │",
144+ "│ │",
145+ "│ │",
146+ "│ │",
147+ "│ │",
148+ "│ │",
149+ "│ │",
150+ "└────────────────────────────┘",
151+ ]),
152+ );
153+
154+ // columns of 1 width trim
155+ test_case(
156+ &[
157+ Constraint::Length(1),
158+ Constraint::Length(1),
159+ Constraint::Length(1),
160+ ],
161+ Buffer::with_lines(vec![
162+ "┌────────────────────────────┐",
163+ "│H H H │",
164+ "│ │",
165+ "│R R R │",
166+ "│R R R │",
167+ "│R R R │",
168+ "│R R R │",
169+ "│ │",
170+ "│ │",
171+ "└────────────────────────────┘",
172+ ]),
173+ );
174+
175+ // columns of large width just before pushing a column off
176+ test_case(
177+ &[
178+ Constraint::Length(8),
179+ Constraint::Length(8),
180+ Constraint::Length(8),
181+ ],
182+ Buffer::with_lines(vec![
183+ "┌────────────────────────────┐",
184+ "│Head1 Head2 Head3 │",
185+ "│ │",
186+ "│Row11 Row12 Row13 │",
187+ "│Row21 Row22 Row23 │",
188+ "│Row31 Row32 Row33 │",
189+ "│Row41 Row42 Row43 │",
190+ "│ │",
191+ "│ │",
192+ "└────────────────────────────┘",
193+ ]),
194+ );
195+}
196+
197+#[test]
198+fn widgets_table_columns_widths_can_use_percentage_constraints() {
199+ let test_case = |widths, expected| {
200+ let backend = TestBackend::new(30, 10);
201+ let mut terminal = Terminal::new(backend).unwrap();
202+
203+ terminal
204+ .draw(|f| {
205+ let size = f.size();
206+ let table = Table::new(vec![
207+ Row::new(vec!["Row11", "Row12", "Row13"]),
208+ Row::new(vec!["Row21", "Row22", "Row23"]),
209+ Row::new(vec!["Row31", "Row32", "Row33"]),
210+ Row::new(vec!["Row41", "Row42", "Row43"]),
211+ ])
212+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
213+ .block(Block::default().borders(Borders::ALL))
214+ .widths(widths)
215+ .column_spacing(0);
216+ f.render_widget(table, size);
217+ })
218+ .unwrap();
219+ terminal.backend().assert_buffer(&expected);
220+ };
221+
222+ // columns of zero width show nothing
223+ test_case(
224+ &[
225+ Constraint::Percentage(0),
226+ Constraint::Percentage(0),
227+ Constraint::Percentage(0),
228+ ],
229+ Buffer::with_lines(vec![
230+ "┌────────────────────────────┐",
231+ "│ │",
232+ "│ │",
233+ "│ │",
234+ "│ │",
235+ "│ │",
236+ "│ │",
237+ "│ │",
238+ "│ │",
239+ "└────────────────────────────┘",
240+ ]),
241+ );
242+
243+ // columns of not enough width trims the data
244+ test_case(
245+ &[
246+ Constraint::Percentage(11),
247+ Constraint::Percentage(11),
248+ Constraint::Percentage(11),
249+ ],
250+ Buffer::with_lines(vec![
251+ "┌────────────────────────────┐",
252+ "│HeaHeaHea │",
253+ "│ │",
254+ "│RowRowRow │",
255+ "│RowRowRow │",
256+ "│RowRowRow │",
257+ "│RowRowRow │",
258+ "│ │",
259+ "│ │",
260+ "└────────────────────────────┘",
261+ ]),
262+ );
263+
264+ // columns of large width just before pushing a column off
265+ test_case(
266+ &[
267+ Constraint::Percentage(33),
268+ Constraint::Percentage(33),
269+ Constraint::Percentage(33),
270+ ],
271+ Buffer::with_lines(vec![
272+ "┌────────────────────────────┐",
273+ "│Head1 Head2 Head3 │",
274+ "│ │",
275+ "│Row11 Row12 Row13 │",
276+ "│Row21 Row22 Row23 │",
277+ "│Row31 Row32 Row33 │",
278+ "│Row41 Row42 Row43 │",
279+ "│ │",
280+ "│ │",
281+ "└────────────────────────────┘",
282+ ]),
283+ );
284+
285+ // percentages summing to 100 should give equal widths
286+ test_case(
287+ &[Constraint::Percentage(50), Constraint::Percentage(50)],
288+ Buffer::with_lines(vec![
289+ "┌────────────────────────────┐",
290+ "│Head1 Head2 │",
291+ "│ │",
292+ "│Row11 Row12 │",
293+ "│Row21 Row22 │",
294+ "│Row31 Row32 │",
295+ "│Row41 Row42 │",
296+ "│ │",
297+ "│ │",
298+ "└────────────────────────────┘",
299+ ]),
300+ );
301+}
302+
303+#[test]
304+fn widgets_table_columns_widths_can_use_mixed_constraints() {
305+ let test_case = |widths, expected| {
306+ let backend = TestBackend::new(30, 10);
307+ let mut terminal = Terminal::new(backend).unwrap();
308+
309+ terminal
310+ .draw(|f| {
311+ let size = f.size();
312+ let table = Table::new(vec![
313+ Row::new(vec!["Row11", "Row12", "Row13"]),
314+ Row::new(vec!["Row21", "Row22", "Row23"]),
315+ Row::new(vec!["Row31", "Row32", "Row33"]),
316+ Row::new(vec!["Row41", "Row42", "Row43"]),
317+ ])
318+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
319+ .block(Block::default().borders(Borders::ALL))
320+ .widths(widths);
321+ f.render_widget(table, size);
322+ })
323+ .unwrap();
324+ terminal.backend().assert_buffer(&expected);
325+ };
326+
327+ // columns of zero width show nothing
328+ test_case(
329+ &[
330+ Constraint::Percentage(0),
331+ Constraint::Length(0),
332+ Constraint::Percentage(0),
333+ ],
334+ Buffer::with_lines(vec![
335+ "┌────────────────────────────┐",
336+ "│ │",
337+ "│ │",
338+ "│ │",
339+ "│ │",
340+ "│ │",
341+ "│ │",
342+ "│ │",
343+ "│ │",
344+ "└────────────────────────────┘",
345+ ]),
346+ );
347+
348+ // columns of not enough width trims the data
349+ test_case(
350+ &[
351+ Constraint::Percentage(11),
352+ Constraint::Length(20),
353+ Constraint::Percentage(11),
354+ ],
355+ Buffer::with_lines(vec![
356+ "┌────────────────────────────┐",
357+ "│Hea Head2 Hea│",
358+ "│ │",
359+ "│Row Row12 Row│",
360+ "│Row Row22 Row│",
361+ "│Row Row32 Row│",
362+ "│Row Row42 Row│",
363+ "│ │",
364+ "│ │",
365+ "└────────────────────────────┘",
366+ ]),
367+ );
368+
369+ // columns of large width just before pushing a column off
370+ test_case(
371+ &[
372+ Constraint::Percentage(33),
373+ Constraint::Length(10),
374+ Constraint::Percentage(33),
375+ ],
376+ Buffer::with_lines(vec![
377+ "┌────────────────────────────┐",
378+ "│Head1 Head2 Head3 │",
379+ "│ │",
380+ "│Row11 Row12 Row13 │",
381+ "│Row21 Row22 Row23 │",
382+ "│Row31 Row32 Row33 │",
383+ "│Row41 Row42 Row43 │",
384+ "│ │",
385+ "│ │",
386+ "└────────────────────────────┘",
387+ ]),
388+ );
389+
390+ // columns of large size (>100% total) hide the last column
391+ test_case(
392+ &[
393+ Constraint::Percentage(60),
394+ Constraint::Length(10),
395+ Constraint::Percentage(60),
396+ ],
397+ Buffer::with_lines(vec![
398+ "┌────────────────────────────┐",
399+ "│Head1 Head2 │",
400+ "│ │",
401+ "│Row11 Row12 │",
402+ "│Row21 Row22 │",
403+ "│Row31 Row32 │",
404+ "│Row41 Row42 │",
405+ "│ │",
406+ "│ │",
407+ "└────────────────────────────┘",
408+ ]),
409+ );
410+}
411+
412+#[test]
413+fn widgets_table_columns_widths_can_use_ratio_constraints() {
414+ let test_case = |widths, expected| {
415+ let backend = TestBackend::new(30, 10);
416+ let mut terminal = Terminal::new(backend).unwrap();
417+
418+ terminal
419+ .draw(|f| {
420+ let size = f.size();
421+ let table = Table::new(vec![
422+ Row::new(vec!["Row11", "Row12", "Row13"]),
423+ Row::new(vec!["Row21", "Row22", "Row23"]),
424+ Row::new(vec!["Row31", "Row32", "Row33"]),
425+ Row::new(vec!["Row41", "Row42", "Row43"]),
426+ ])
427+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
428+ .block(Block::default().borders(Borders::ALL))
429+ .widths(widths)
430+ .column_spacing(0);
431+ f.render_widget(table, size);
432+ })
433+ .unwrap();
434+ terminal.backend().assert_buffer(&expected);
435+ };
436+
437+ // columns of zero width show nothing
438+ test_case(
439+ &[
440+ Constraint::Ratio(0, 1),
441+ Constraint::Ratio(0, 1),
442+ Constraint::Ratio(0, 1),
443+ ],
444+ Buffer::with_lines(vec![
445+ "┌────────────────────────────┐",
446+ "│ │",
447+ "│ │",
448+ "│ │",
449+ "│ │",
450+ "│ │",
451+ "│ │",
452+ "│ │",
453+ "│ │",
454+ "└────────────────────────────┘",
455+ ]),
456+ );
457+
458+ // columns of not enough width trims the data
459+ test_case(
460+ &[
461+ Constraint::Ratio(1, 9),
462+ Constraint::Ratio(1, 9),
463+ Constraint::Ratio(1, 9),
464+ ],
465+ Buffer::with_lines(vec![
466+ "┌────────────────────────────┐",
467+ "│HeaHeaHea │",
468+ "│ │",
469+ "│RowRowRow │",
470+ "│RowRowRow │",
471+ "│RowRowRow │",
472+ "│RowRowRow │",
473+ "│ │",
474+ "│ │",
475+ "└────────────────────────────┘",
476+ ]),
477+ );
478+
479+ // columns of large width just before pushing a column off
480+ test_case(
481+ &[
482+ Constraint::Ratio(1, 3),
483+ Constraint::Ratio(1, 3),
484+ Constraint::Ratio(1, 3),
485+ ],
486+ Buffer::with_lines(vec![
487+ "┌────────────────────────────┐",
488+ "│Head1 Head2 Head3 │",
489+ "│ │",
490+ "│Row11 Row12 Row13 │",
491+ "│Row21 Row22 Row23 │",
492+ "│Row31 Row32 Row33 │",
493+ "│Row41 Row42 Row43 │",
494+ "│ │",
495+ "│ │",
496+ "└────────────────────────────┘",
497+ ]),
498+ );
499+
500+ // percentages summing to 100 should give equal widths
501+ test_case(
502+ &[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
503+ Buffer::with_lines(vec![
504+ "┌────────────────────────────┐",
505+ "│Head1 Head2 │",
506+ "│ │",
507+ "│Row11 Row12 │",
508+ "│Row21 Row22 │",
509+ "│Row31 Row32 │",
510+ "│Row41 Row42 │",
511+ "│ │",
512+ "│ │",
513+ "└────────────────────────────┘",
514+ ]),
515+ );
516+}
517+
518+#[test]
519+fn widgets_table_can_have_rows_with_multi_lines() {
520+ let test_case = |state: &mut TableState, expected: Buffer| {
521+ let backend = TestBackend::new(30, 8);
522+ let mut terminal = Terminal::new(backend).unwrap();
523+ terminal
524+ .draw(|f| {
525+ let size = f.size();
526+ let table = Table::new(vec![
527+ Row::new(vec!["Row11", "Row12", "Row13"]),
528+ Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
529+ Row::new(vec!["Row31", "Row32", "Row33"]),
530+ Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
531+ ])
532+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
533+ .block(Block::default().borders(Borders::ALL))
534+ .highlight_symbol(">> ")
535+ .widths(&[
536+ Constraint::Length(5),
537+ Constraint::Length(5),
538+ Constraint::Length(5),
539+ ])
540+ .column_spacing(1);
541+ f.render_stateful_widget(table, size, state);
542+ })
543+ .unwrap();
544+ terminal.backend().assert_buffer(&expected);
545+ };
546+
547+ let mut state = TableState::default();
548+ // no selection
549+ test_case(
550+ &mut state,
551+ Buffer::with_lines(vec![
552+ "┌────────────────────────────┐",
553+ "│Head1 Head2 Head3 │",
554+ "│ │",
555+ "│Row11 Row12 Row13 │",
556+ "│Row21 Row22 Row23 │",
557+ "│ │",
558+ "│Row31 Row32 Row33 │",
559+ "└────────────────────────────┘",
560+ ]),
561+ );
562+
563+ // select first
564+ state.select(Some(0));
565+ test_case(
566+ &mut state,
567+ Buffer::with_lines(vec![
568+ "┌────────────────────────────┐",
569+ "│ Head1 Head2 Head3 │",
570+ "│ │",
571+ "│>> Row11 Row12 Row13 │",
572+ "│ Row21 Row22 Row23 │",
573+ "│ │",
574+ "│ Row31 Row32 Row33 │",
575+ "└────────────────────────────┘",
576+ ]),
577+ );
578+
579+ // select second (we don't show partially the 4th row)
580+ state.select(Some(1));
581+ test_case(
582+ &mut state,
583+ Buffer::with_lines(vec![
584+ "┌────────────────────────────┐",
585+ "│ Head1 Head2 Head3 │",
586+ "│ │",
587+ "│ Row11 Row12 Row13 │",
588+ "│>> Row21 Row22 Row23 │",
589+ "│ │",
590+ "│ Row31 Row32 Row33 │",
591+ "└────────────────────────────┘",
592+ ]),
593+ );
594+
595+ // select 4th (we don't show partially the 1st row)
596+ state.select(Some(3));
597+ test_case(
598+ &mut state,
599+ Buffer::with_lines(vec![
600+ "┌────────────────────────────┐",
601+ "│ Head1 Head2 Head3 │",
602+ "│ │",
603+ "│ Row31 Row32 Row33 │",
604+ "│>> Row41 Row42 Row43 │",
605+ "│ │",
606+ "│ │",
607+ "└────────────────────────────┘",
608+ ]),
609+ );
610+}
611+
612+#[test]
613+fn widgets_table_can_have_elements_styled_individually() {
614+ let backend = TestBackend::new(30, 4);
615+ let mut terminal = Terminal::new(backend).unwrap();
616+ let mut state = TableState::default();
617+ state.select(Some(0));
618+ terminal
619+ .draw(|f| {
620+ let size = f.size();
621+ let table = Table::new(vec![
622+ Row::new(vec!["Row11", "Row12", "Row13"]).style(Style::default().fg(Color::Green)),
623+ Row::new(vec![
624+ Cell::from("Row21"),
625+ Cell::from("Row22").style(Style::default().fg(Color::Yellow)),
626+ Cell::from(Spans::from(vec![
627+ Span::raw("Row"),
628+ Span::styled("23", Style::default().fg(Color::Blue)),
629+ ]))
630+ .style(Style::default().fg(Color::Red)),
631+ ])
632+ .style(Style::default().fg(Color::LightGreen)),
633+ ])
634+ .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
635+ .block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
636+ .highlight_symbol(">> ")
637+ .highlight_style(Style::default().add_modifier(Modifier::BOLD))
638+ .widths(&[
639+ Constraint::Length(6),
640+ Constraint::Length(6),
641+ Constraint::Length(6),
642+ ])
643+ .column_spacing(1);
644+ f.render_stateful_widget(table, size, &mut state);
645+ })
646+ .unwrap();
647+
648+ let mut expected = Buffer::with_lines(vec![
649+ "│ Head1 Head2 Head3 │",
650+ "│ │",
651+ "│>> Row11 Row12 Row13 │",
652+ "│ Row21 Row22 Row23 │",
653+ ]);
654+ // First row = row color + highlight style
655+ for col in 1..=28 {
656+ expected.get_mut(col, 2).set_style(
657+ Style::default()
658+ .fg(Color::Green)
659+ .add_modifier(Modifier::BOLD),
660+ );
661+ }
662+ // Second row:
663+ // 1. row color
664+ for col in 1..=28 {
665+ expected
666+ .get_mut(col, 3)
667+ .set_style(Style::default().fg(Color::LightGreen));
668+ }
669+ // 2. cell color
670+ for col in 11..=16 {
671+ expected
672+ .get_mut(col, 3)
673+ .set_style(Style::default().fg(Color::Yellow));
674+ }
675+ for col in 18..=23 {
676+ expected
677+ .get_mut(col, 3)
678+ .set_style(Style::default().fg(Color::Red));
679+ }
680+ // 3. text color
681+ for col in 21..=22 {
682+ expected
683+ .get_mut(col, 3)
684+ .set_style(Style::default().fg(Color::Blue));
685+ }
686+ terminal.backend().assert_buffer(&expected);
687+}
688+
689+#[test]
690+fn widgets_table_should_render_even_if_empty() {
691+ let backend = TestBackend::new(30, 4);
692+ let mut terminal = Terminal::new(backend).unwrap();
693+ terminal
694+ .draw(|f| {
695+ let size = f.size();
696+ let table = Table::new(vec![])
697+ .header(Row::new(vec!["Head1", "Head2", "Head3"]))
698+ .block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
699+ .widths(&[
700+ Constraint::Length(6),
701+ Constraint::Length(6),
702+ Constraint::Length(6),
703+ ])
704+ .column_spacing(1);
705+ f.render_widget(table, size);
706+ })
707+ .unwrap();
708+
709+ let expected = Buffer::with_lines(vec![
710+ "│Head1 Head2 Head3 │",
711+ "│ │",
712+ "│ │",
713+ "│ │",
714+ ]);
715+
716+ terminal.backend().assert_buffer(&expected);
717+}