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