tui_realm_stdlib/components/
table.rs1use std::cmp::max;
4
5use tuirealm::command::{Cmd, CmdResult, Direction, Position};
6use tuirealm::component::Component;
7use tuirealm::props::{
8 AttrValue, Attribute, Borders, Color, LineStatic, PropPayload, PropValue, Props, QueryResult,
9 Style, Table as PropTable, TextModifiers, Title,
10};
11use tuirealm::ratatui::Frame;
12use tuirealm::ratatui::layout::{Constraint, Rect};
13use tuirealm::ratatui::text::Line;
14use tuirealm::ratatui::widgets::{Cell, Row, Table as TuiTable, TableState};
15use tuirealm::state::{State, StateValue};
16
17use super::props::TABLE_COLUMN_SPACING;
18use crate::prop_ext::{CommonHighlight, CommonProps};
19use crate::utils;
20
21#[derive(Default)]
25pub struct TableStates {
26 pub list_index: usize,
28 pub list_len: usize,
30}
31
32impl TableStates {
33 pub fn set_list_len(&mut self, len: usize) {
35 self.list_len = len;
36 }
37
38 pub fn incr_list_index(&mut self, rewind: bool) {
40 if self.list_index + 1 < self.list_len {
42 self.list_index += 1;
43 } else if rewind {
44 self.list_index = 0;
45 }
46 }
47
48 pub fn decr_list_index(&mut self, rewind: bool) {
50 if self.list_index > 0 {
52 self.list_index -= 1;
53 } else if rewind && self.list_len > 0 {
54 self.list_index = self.list_len - 1;
55 }
56 }
57
58 pub fn fix_list_index(&mut self) {
60 if self.list_index >= self.list_len && self.list_len > 0 {
61 self.list_index = self.list_len - 1;
62 } else if self.list_len == 0 {
63 self.list_index = 0;
64 }
65 }
66
67 pub fn list_index_at_first(&mut self) {
69 self.list_index = 0;
70 }
71
72 pub fn list_index_at_last(&mut self) {
74 if self.list_len > 0 {
75 self.list_index = self.list_len - 1;
76 } else {
77 self.list_index = 0;
78 }
79 }
80
81 #[must_use]
83 pub fn calc_max_step_ahead(&self, max: usize) -> usize {
84 let remaining: usize = match self.list_len {
85 0 => 0,
86 len => len - 1 - self.list_index,
87 };
88 if remaining > max { max } else { remaining }
89 }
90
91 #[must_use]
93 pub fn calc_max_step_behind(&self, max: usize) -> usize {
94 if self.list_index > max {
95 max
96 } else {
97 self.list_index
98 }
99 }
100}
101
102#[derive(Default)]
106#[must_use]
107pub struct Table {
108 common: CommonProps,
109 common_hg: CommonHighlight,
110 props: Props,
111 pub states: TableStates,
112}
113
114impl Table {
115 pub fn foreground(mut self, fg: Color) -> Self {
117 self.attr(Attribute::Foreground, AttrValue::Color(fg));
118 self
119 }
120
121 pub fn background(mut self, bg: Color) -> Self {
123 self.attr(Attribute::Background, AttrValue::Color(bg));
124 self
125 }
126
127 pub fn modifiers(mut self, m: TextModifiers) -> Self {
129 self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
130 self
131 }
132
133 pub fn style(mut self, style: Style) -> Self {
137 self.attr(Attribute::Style, AttrValue::Style(style));
138 self
139 }
140
141 pub fn inactive(mut self, s: Style) -> Self {
143 self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
144 self
145 }
146
147 pub fn borders(mut self, b: Borders) -> Self {
149 self.attr(Attribute::Borders, AttrValue::Borders(b));
150 self
151 }
152
153 pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
155 self.attr(Attribute::Title, AttrValue::Title(title.into()));
156 self
157 }
158
159 pub fn step(mut self, step: usize) -> Self {
161 self.attr(Attribute::ScrollStep, AttrValue::Length(step));
162 self
163 }
164
165 pub fn scroll(mut self, scrollable: bool) -> Self {
167 self.attr(Attribute::Scroll, AttrValue::Flag(scrollable));
168 self
169 }
170
171 pub fn highlight_str<S: Into<LineStatic>>(mut self, s: S) -> Self {
173 self.attr(Attribute::HighlightedStr, AttrValue::TextLine(s.into()));
174 self
175 }
176
177 pub fn highlight_style(mut self, s: Style) -> Self {
181 self.attr(Attribute::HighlightStyle, AttrValue::Style(s));
182 self
183 }
184
185 pub fn column_spacing(mut self, w: u16) -> Self {
187 self.attr(Attribute::Custom(TABLE_COLUMN_SPACING), AttrValue::Size(w));
188 self
189 }
190
191 pub fn row_height(mut self, h: u16) -> Self {
195 self.attr(Attribute::Height, AttrValue::Size(h));
196 self
197 }
198
199 pub fn widths(mut self, w: &[u16]) -> Self {
201 self.attr(
203 Attribute::Width,
204 AttrValue::Payload(PropPayload::Vec(
205 w.iter().map(|x| PropValue::U16(*x)).collect(),
206 )),
207 );
208 self
209 }
210
211 pub fn headers<S: Into<String>>(mut self, headers: impl IntoIterator<Item = S>) -> Self {
213 self.attr(
215 Attribute::Text,
216 AttrValue::Payload(PropPayload::Vec(
217 headers
218 .into_iter()
219 .map(|v| PropValue::Str(v.into()))
220 .collect(),
221 )),
222 );
223 self
224 }
225
226 pub fn table(mut self, t: PropTable) -> Self {
228 self.attr(Attribute::Content, AttrValue::Table(t));
229 self
230 }
231
232 pub fn rewind(mut self, r: bool) -> Self {
234 self.attr(Attribute::Rewind, AttrValue::Flag(r));
235 self
236 }
237
238 pub fn selected_line(mut self, line: usize) -> Self {
240 self.attr(
241 Attribute::Value,
242 AttrValue::Payload(PropPayload::Single(PropValue::Usize(line))),
243 );
244 self
245 }
246
247 pub fn always_active(mut self) -> Self {
249 self.attr(Attribute::AlwaysActive, AttrValue::Flag(true));
250 self
251 }
252
253 fn is_scrollable(&self) -> bool {
257 self.props
258 .get(Attribute::Scroll)
259 .and_then(AttrValue::as_flag)
260 .unwrap_or_default()
261 }
262
263 fn rewindable(&self) -> bool {
264 self.props
265 .get(Attribute::Rewind)
266 .and_then(AttrValue::as_flag)
267 .unwrap_or_default()
268 }
269
270 fn layout(&self) -> Vec<Constraint> {
275 if let Some(widths) = self
276 .props
277 .get(Attribute::Width)
278 .and_then(AttrValue::as_payload)
279 .and_then(PropPayload::as_vec)
280 {
281 widths
282 .iter()
283 .cloned()
284 .map(|x| x.unwrap_u16())
285 .map(Constraint::Percentage)
286 .collect()
287 } else {
288 let columns: usize = self
290 .props
291 .get(Attribute::Content)
292 .and_then(AttrValue::as_table)
293 .and_then(|rows| rows.iter().map(|col| col.len()).max())
294 .unwrap_or(1);
295 let width: u16 = (100 / max(columns, 1)) as u16;
297 (0..columns)
298 .map(|_| Constraint::Percentage(width))
299 .collect()
300 }
301 }
302
303 fn make_rows(&self, row_height: u16) -> Vec<Row<'_>> {
305 let Some(table) = self
306 .props
307 .get(Attribute::Content)
308 .and_then(|x| x.as_table())
309 else {
310 return Vec::new();
311 };
312
313 table
314 .iter()
315 .map(|row| {
316 let columns: Vec<Cell> = row
317 .iter()
318 .map(|col| {
319 let line = Line::from(
320 col.spans
321 .iter()
322 .map(utils::borrow_clone_span)
323 .collect::<Vec<_>>(),
324 );
325 Cell::from(line)
326 })
327 .collect();
328 Row::new(columns).height(row_height)
329 })
330 .collect() }
332}
333
334impl Component for Table {
335 fn view(&mut self, render: &mut Frame, area: Rect) {
336 if !self.common.display {
337 return;
338 }
339
340 let row_height = self
341 .props
342 .get(Attribute::Height)
343 .and_then(AttrValue::as_size)
344 .unwrap_or(1);
345 let rows: Vec<Row> = self.make_rows(row_height);
347 let widths: Vec<Constraint> = self.layout();
348
349 let mut widget = TuiTable::new(rows, &widths).style(self.common.style);
350
351 if self.common.is_active() {
352 widget = widget.row_highlight_style(self.common_hg.get_style(self.common.style));
353 }
354
355 if let Some(block) = self.common.get_block() {
356 widget = widget.block(block);
357 }
358
359 if let Some(symbol) = self.common_hg.get_symbol() {
361 widget = widget.highlight_symbol(symbol);
362 }
363
364 if let Some(spacing) = self
366 .props
367 .get(Attribute::Custom(TABLE_COLUMN_SPACING))
368 .and_then(AttrValue::as_size)
369 {
370 widget = widget.column_spacing(spacing);
371 }
372 let headers: Vec<&str> = self
374 .props
375 .get(Attribute::Text)
376 .and_then(AttrValue::as_payload)
377 .and_then(PropPayload::as_vec)
378 .map(|v| {
379 v.iter()
380 .filter_map(|v| v.as_str().map(|v| v.as_str()))
381 .collect()
382 })
383 .unwrap_or_default();
384 if !headers.is_empty() {
385 widget = widget.header(
386 Row::new(headers)
387 .style(self.common.style)
388 .height(row_height),
389 );
390 }
391 if self.is_scrollable() {
392 let mut state: TableState = TableState::default();
393 state.select(Some(self.states.list_index));
394 render.render_stateful_widget(widget, area, &mut state);
395 } else {
396 render.render_widget(widget, area);
397 }
398 }
399
400 fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
401 if let Some(value) = self
402 .common
403 .get_for_query(attr)
404 .or_else(|| self.common_hg.get_for_query(attr))
405 {
406 return Some(value);
407 }
408
409 self.props.get_for_query(attr)
410 }
411
412 fn attr(&mut self, attr: Attribute, value: AttrValue) {
413 if let Some(value) = self
414 .common
415 .set(attr, value)
416 .and_then(|value| self.common_hg.set(attr, value))
417 {
418 self.props.set(attr, value);
419 if matches!(attr, Attribute::Content) {
420 self.states.set_list_len(
422 self.props
423 .get(Attribute::Content)
424 .and_then(AttrValue::as_table)
425 .map(|spans| spans.len())
426 .unwrap_or_default(),
427 );
428 self.states.fix_list_index();
429 } else if matches!(attr, Attribute::Value) && self.is_scrollable() {
430 self.states.list_index = self
431 .props
432 .get(Attribute::Value)
433 .and_then(AttrValue::as_payload)
434 .and_then(PropPayload::as_single)
435 .and_then(PropValue::as_usize)
436 .unwrap_or_default();
437 self.states.fix_list_index();
438 }
439 }
440 }
441
442 fn state(&self) -> State {
443 if self.is_scrollable() {
444 State::Single(StateValue::Usize(self.states.list_index))
445 } else {
446 State::None
447 }
448 }
449
450 fn perform(&mut self, cmd: Cmd) -> CmdResult {
451 match cmd {
452 Cmd::Move(Direction::Down) => {
453 let prev = self.states.list_index;
454 self.states.incr_list_index(self.rewindable());
455 if prev == self.states.list_index {
456 CmdResult::NoChange
457 } else {
458 CmdResult::Changed(self.state())
459 }
460 }
461 Cmd::Move(Direction::Up) => {
462 let prev = self.states.list_index;
463 self.states.decr_list_index(self.rewindable());
464 if prev == self.states.list_index {
465 CmdResult::NoChange
466 } else {
467 CmdResult::Changed(self.state())
468 }
469 }
470 Cmd::Scroll(Direction::Down) => {
471 let prev = self.states.list_index;
472 let step = self
473 .props
474 .get(Attribute::ScrollStep)
475 .and_then(AttrValue::as_length)
476 .unwrap_or(8);
477 let step: usize = self.states.calc_max_step_ahead(step);
478 (0..step).for_each(|_| self.states.incr_list_index(false));
479 if prev == self.states.list_index {
480 CmdResult::NoChange
481 } else {
482 CmdResult::Changed(self.state())
483 }
484 }
485 Cmd::Scroll(Direction::Up) => {
486 let prev = self.states.list_index;
487 let step = self
488 .props
489 .get(Attribute::ScrollStep)
490 .and_then(AttrValue::as_length)
491 .unwrap_or(8);
492 let step: usize = self.states.calc_max_step_behind(step);
493 (0..step).for_each(|_| self.states.decr_list_index(false));
494 if prev == self.states.list_index {
495 CmdResult::NoChange
496 } else {
497 CmdResult::Changed(self.state())
498 }
499 }
500 Cmd::GoTo(Position::Begin) => {
501 let prev = self.states.list_index;
502 self.states.list_index_at_first();
503 if prev == self.states.list_index {
504 CmdResult::NoChange
505 } else {
506 CmdResult::Changed(self.state())
507 }
508 }
509 Cmd::GoTo(Position::End) => {
510 let prev = self.states.list_index;
511 self.states.list_index_at_last();
512 if prev == self.states.list_index {
513 CmdResult::NoChange
514 } else {
515 CmdResult::Changed(self.state())
516 }
517 }
518 _ => CmdResult::Invalid(cmd),
519 }
520 }
521}
522
523#[cfg(test)]
524mod tests {
525
526 use pretty_assertions::assert_eq;
527 use tuirealm::props::{HorizontalAlignment, TableBuilder};
528
529 use super::*;
530
531 #[test]
532 fn table_states() {
533 let mut states = TableStates::default();
534 assert_eq!(states.list_index, 0);
535 assert_eq!(states.list_len, 0);
536 states.set_list_len(5);
537 assert_eq!(states.list_index, 0);
538 assert_eq!(states.list_len, 5);
539 states.incr_list_index(true);
541 assert_eq!(states.list_index, 1);
542 states.list_index = 4;
543 states.incr_list_index(false);
544 assert_eq!(states.list_index, 4);
545 states.incr_list_index(true);
546 assert_eq!(states.list_index, 0);
547 states.decr_list_index(false);
549 assert_eq!(states.list_index, 0);
550 states.decr_list_index(true);
551 assert_eq!(states.list_index, 4);
552 states.decr_list_index(true);
553 assert_eq!(states.list_index, 3);
554 states.list_index_at_first();
556 assert_eq!(states.list_index, 0);
557 states.list_index_at_last();
558 assert_eq!(states.list_index, 4);
559 states.set_list_len(3);
561 states.fix_list_index();
562 assert_eq!(states.list_index, 2);
563 }
564
565 #[test]
566 fn test_component_table_scrolling() {
567 let mut component = Table::default()
569 .foreground(Color::Red)
570 .background(Color::Blue)
571 .highlight_style(Style::new().fg(Color::Yellow))
572 .highlight_str("🚀")
573 .modifiers(TextModifiers::BOLD)
574 .scroll(true)
575 .step(4)
576 .borders(Borders::default())
577 .title(Title::from("events").alignment(HorizontalAlignment::Center))
578 .column_spacing(4)
579 .widths(&[25, 25, 25, 25])
580 .row_height(3)
581 .headers(["Event", "Message", "Behaviour", "???"])
582 .table(
583 TableBuilder::default()
584 .add_col(Line::from("KeyCode::Down"))
585 .add_col(Line::from("OnKey"))
586 .add_col(Line::from("Move cursor down"))
587 .add_row()
588 .add_col(Line::from("KeyCode::Up"))
589 .add_col(Line::from("OnKey"))
590 .add_col(Line::from("Move cursor up"))
591 .add_row()
592 .add_col(Line::from("KeyCode::PageDown"))
593 .add_col(Line::from("OnKey"))
594 .add_col(Line::from("Move cursor down by 8"))
595 .add_row()
596 .add_col(Line::from("KeyCode::PageUp"))
597 .add_col(Line::from("OnKey"))
598 .add_col(Line::from("ove cursor up by 8"))
599 .add_row()
600 .add_col(Line::from("KeyCode::End"))
601 .add_col(Line::from("OnKey"))
602 .add_col(Line::from("Move cursor to last item"))
603 .add_row()
604 .add_col(Line::from("KeyCode::Home"))
605 .add_col(Line::from("OnKey"))
606 .add_col(Line::from("Move cursor to first item"))
607 .add_row()
608 .add_col(Line::from("KeyCode::Char(_)"))
609 .add_col(Line::from("OnKey"))
610 .add_col(Line::from("Return pressed key"))
611 .add_col(Line::from("4th mysterious columns"))
612 .build(),
613 );
614 assert_eq!(component.states.list_len, 7);
615 assert_eq!(component.states.list_index, 0);
616 assert_eq!(component.layout().len(), 4);
618 component.states.list_index += 1;
620 assert_eq!(component.states.list_index, 1);
621 assert_eq!(
624 component.perform(Cmd::Move(Direction::Down)),
625 CmdResult::Changed(State::Single(StateValue::Usize(2)))
626 );
627 assert_eq!(component.states.list_index, 2);
629 assert_eq!(
631 component.perform(Cmd::Move(Direction::Up)),
632 CmdResult::Changed(State::Single(StateValue::Usize(1)))
633 );
634 assert_eq!(component.states.list_index, 1);
636 assert_eq!(
638 component.perform(Cmd::Scroll(Direction::Down)),
639 CmdResult::Changed(State::Single(StateValue::Usize(5)))
640 );
641 assert_eq!(component.states.list_index, 5);
643 assert_eq!(
644 component.perform(Cmd::Scroll(Direction::Down)),
645 CmdResult::Changed(State::Single(StateValue::Usize(6)))
646 );
647 assert_eq!(component.states.list_index, 6);
649 assert_eq!(
651 component.perform(Cmd::Scroll(Direction::Up)),
652 CmdResult::Changed(State::Single(StateValue::Usize(2)))
653 );
654 assert_eq!(component.states.list_index, 2);
655 assert_eq!(
656 component.perform(Cmd::Scroll(Direction::Up)),
657 CmdResult::Changed(State::Single(StateValue::Usize(0)))
658 );
659 assert_eq!(component.states.list_index, 0);
660 assert_eq!(
662 component.perform(Cmd::GoTo(Position::End)),
663 CmdResult::Changed(State::Single(StateValue::Usize(6)))
664 );
665 assert_eq!(component.states.list_index, 6);
666 assert_eq!(
668 component.perform(Cmd::GoTo(Position::Begin)),
669 CmdResult::Changed(State::Single(StateValue::Usize(0)))
670 );
671 assert_eq!(component.states.list_index, 0);
672 component.attr(
674 Attribute::Content,
675 AttrValue::Table(
676 TableBuilder::default()
677 .add_col(Line::from("name"))
678 .add_col(Line::from("age"))
679 .add_col(Line::from("birthdate"))
680 .build(),
681 ),
682 );
683 assert_eq!(component.states.list_len, 1);
684 assert_eq!(component.states.list_index, 0);
685 assert_eq!(component.state(), State::Single(StateValue::Usize(0)));
687 }
688
689 #[test]
690 fn test_component_table_with_empty_rows_and_no_width_set() {
691 let component = Table::default().table(TableBuilder::default().build());
693
694 assert_eq!(component.states.list_len, 1);
695 assert_eq!(component.states.list_index, 0);
696 assert_eq!(component.layout().len(), 0);
698 }
699
700 #[test]
701 fn test_components_table() {
702 let component = Table::default()
704 .foreground(Color::Red)
705 .background(Color::Blue)
706 .highlight_style(Style::new().fg(Color::Yellow))
707 .highlight_str("🚀")
708 .modifiers(TextModifiers::BOLD)
709 .borders(Borders::default())
710 .title(Title::from("events").alignment(HorizontalAlignment::Center))
711 .column_spacing(4)
712 .widths(&[33, 33, 33])
713 .row_height(3)
714 .headers(["Event", "Message", "Behaviour"])
715 .table(
716 TableBuilder::default()
717 .add_col(Line::from("KeyCode::Down"))
718 .add_col(Line::from("OnKey"))
719 .add_col(Line::from("Move cursor down"))
720 .add_row()
721 .add_col(Line::from("KeyCode::Up"))
722 .add_col(Line::from("OnKey"))
723 .add_col(Line::from("Move cursor up"))
724 .add_row()
725 .add_col(Line::from("KeyCode::PageDown"))
726 .add_col(Line::from("OnKey"))
727 .add_col(Line::from("Move cursor down by 8"))
728 .add_row()
729 .add_col(Line::from("KeyCode::PageUp"))
730 .add_col(Line::from("OnKey"))
731 .add_col(Line::from("ove cursor up by 8"))
732 .add_row()
733 .add_col(Line::from("KeyCode::End"))
734 .add_col(Line::from("OnKey"))
735 .add_col(Line::from("Move cursor to last item"))
736 .add_row()
737 .add_col(Line::from("KeyCode::Home"))
738 .add_col(Line::from("OnKey"))
739 .add_col(Line::from("Move cursor to first item"))
740 .add_row()
741 .add_col(Line::from("KeyCode::Char(_)"))
742 .add_col(Line::from("OnKey"))
743 .add_col(Line::from("Return pressed key"))
744 .build(),
745 );
746 assert_eq!(component.state(), State::None);
748 }
749
750 #[test]
751 fn should_init_list_value() {
752 let mut component = Table::default()
753 .foreground(Color::Red)
754 .background(Color::Blue)
755 .highlight_style(Style::new().fg(Color::Yellow))
756 .highlight_str("🚀")
757 .modifiers(TextModifiers::BOLD)
758 .borders(Borders::default())
759 .title(Title::from("events").alignment(HorizontalAlignment::Center))
760 .table(
761 TableBuilder::default()
762 .add_col(Line::from("KeyCode::Down"))
763 .add_col(Line::from("OnKey"))
764 .add_col(Line::from("Move cursor down"))
765 .add_row()
766 .add_col(Line::from("KeyCode::Up"))
767 .add_col(Line::from("OnKey"))
768 .add_col(Line::from("Move cursor up"))
769 .add_row()
770 .add_col(Line::from("KeyCode::PageDown"))
771 .add_col(Line::from("OnKey"))
772 .add_col(Line::from("Move cursor down by 8"))
773 .add_row()
774 .add_col(Line::from("KeyCode::PageUp"))
775 .add_col(Line::from("OnKey"))
776 .add_col(Line::from("ove cursor up by 8"))
777 .add_row()
778 .add_col(Line::from("KeyCode::End"))
779 .add_col(Line::from("OnKey"))
780 .add_col(Line::from("Move cursor to last item"))
781 .add_row()
782 .add_col(Line::from("KeyCode::Home"))
783 .add_col(Line::from("OnKey"))
784 .add_col(Line::from("Move cursor to first item"))
785 .add_row()
786 .add_col(Line::from("KeyCode::Char(_)"))
787 .add_col(Line::from("OnKey"))
788 .add_col(Line::from("Return pressed key"))
789 .build(),
790 )
791 .scroll(true)
792 .selected_line(2);
793 assert_eq!(component.states.list_index, 2);
794 component.attr(
796 Attribute::Value,
797 AttrValue::Payload(PropPayload::Single(PropValue::Usize(50))),
798 );
799 assert_eq!(component.states.list_index, 6);
800 }
801
802 #[test]
803 fn various_header_types() {
804 let _ = Table::default().headers(["hello"]);
806 let _ = Table::default().headers(["hello".to_string()]);
808 let _ = Table::default().headers(vec!["hello"]);
810 let _ = Table::default().headers(vec!["hello".to_string()]);
812 let _ = Table::default().headers(vec!["hello"].into_boxed_slice());
814 let _ = Table::default().headers(vec!["hello".to_string()].into_boxed_slice());
816 }
817}