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