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