diff --git a/src/ui/layout/components/mod.rs b/src/ui/layout/components/mod.rs
index eed68fe..cd0985a 100644
--- a/src/ui/layout/components/mod.rs
+++ b/src/ui/layout/components/mod.rs
@@ -31,4 +31,5 @@ pub mod file_list;
pub mod input;
pub mod progress_bar;
pub mod radio_group;
+pub mod table;
pub mod text;
diff --git a/src/ui/layout/components/table.rs b/src/ui/layout/components/table.rs
new file mode 100644
index 0000000..3c075ee
--- /dev/null
+++ b/src/ui/layout/components/table.rs
@@ -0,0 +1,210 @@
+//! ## TextList
+//!
+//! `TextList` component renders a radio group
+
+/*
+*
+* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
+*
+* This file is part of "TermSCP"
+*
+* TermSCP is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* TermSCP is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with TermSCP. If not, see .
+*
+*/
+
+// locals
+use super::{Component, InputEvent, Msg, Payload, Props, PropsBuilder, Render};
+// ext
+use tui::{
+ layout::Corner,
+ style::Style,
+ text::{Span, Spans},
+ widgets::{Block, BorderType, Borders, List, ListItem},
+};
+
+// NOTE: this component doesn't handle any state
+
+// -- component
+
+/// ## Table
+///
+/// Table is a table component. List n rows with n text span columns
+pub struct Table {
+ props: Props,
+}
+
+impl Table {
+ /// ### new
+ ///
+ /// Instantiate a new Table component
+ pub fn new(props: Props) -> Self {
+ Table { props }
+ }
+}
+
+impl Component for Table {
+ /// ### render
+ ///
+ /// Based on the current properties and states, return a Widget instance for the Component
+ /// Returns None if the component is hidden
+ fn render(&self) -> Option {
+ // Make a Span
+ if self.props.visible {
+ let title: String = match self.props.texts.title.as_ref() {
+ Some(t) => t.clone(),
+ None => String::new(),
+ };
+ // Make list entries
+ let list_items: Vec = match self.props.texts.table.as_ref() {
+ None => Vec::new(),
+ Some(table) => table
+ .iter()
+ .map(|row| {
+ let columns: Vec = row
+ .iter()
+ .map(|col| {
+ Span::styled(
+ col.content.clone(),
+ Style::default()
+ .add_modifier(col.get_modifiers())
+ .fg(col.fg)
+ .bg(col.bg),
+ )
+ })
+ .collect();
+ ListItem::new(Spans::from(columns))
+ })
+ .collect(), // Make List item from TextSpan
+ };
+ // Make list
+ Some(Render {
+ cursor: 0,
+ widget: Box::new(
+ List::new(list_items)
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .border_style(Style::default())
+ .border_type(BorderType::Rounded)
+ .title(title),
+ )
+ .start_corner(Corner::TopLeft),
+ ),
+ })
+ } else {
+ // Invisible
+ None
+ }
+ }
+
+ /// ### update
+ ///
+ /// Update component properties
+ /// Properties should first be retrieved through `get_props` which creates a builder from
+ /// existing properties and then edited before calling update.
+ /// Returns a Msg to the view
+ fn update(&mut self, props: Props) -> Msg {
+ self.props = props;
+ // Return None
+ Msg::None
+ }
+
+ /// ### get_props
+ ///
+ /// Returns a props builder starting from component properties.
+ /// This returns a prop builder in order to make easier to create
+ /// new properties for the element.
+ fn get_props(&self) -> PropsBuilder {
+ PropsBuilder::from(self.props.clone())
+ }
+
+ /// ### on
+ ///
+ /// Handle input event and update internal states.
+ /// Returns a Msg to the view.
+ /// Returns always None, since cannot have any focus
+ fn on(&mut self, _ev: InputEvent) -> Msg {
+ Msg::None
+ }
+
+ /// ### get_value
+ ///
+ /// Get current value from component
+ /// For this component returns always None
+ fn get_value(&self) -> Payload {
+ Payload::None
+ }
+
+ // -- events
+
+ /// ### should_umount
+ ///
+ /// The component must provide to the supervisor whether it should be umounted (destroyed)
+ /// This makes sense to be called after an `on` or after an `update`, where the states changes.
+ /// Always false for this component
+ fn should_umount(&self) -> bool {
+ false
+ }
+
+ /// ### blur
+ ///
+ /// Blur component; does nothing on this component
+ fn blur(&mut self) {}
+
+ /// ### active
+ ///
+ /// Active component; does nothing on this component
+ fn active(&mut self) {}
+}
+
+#[cfg(test)]
+mod tests {
+
+ use super::*;
+ use crate::ui::layout::props::{TableBuilder, TextParts, TextSpan};
+
+ use crossterm::event::{KeyCode, KeyEvent};
+
+ #[test]
+ fn test_ui_layout_components_table() {
+ let mut component: Table = Table::new(
+ PropsBuilder::default()
+ .with_texts(TextParts::table(
+ Some(String::from("My data")),
+ TableBuilder::default()
+ .add_col(TextSpan::from("name"))
+ .add_col(TextSpan::from("age"))
+ .add_row()
+ .add_col(TextSpan::from("omar"))
+ .add_col(TextSpan::from("24"))
+ .build(),
+ ))
+ .build(),
+ );
+ // Focus
+ component.active();
+ component.blur();
+ // Should umount
+ assert_eq!(component.should_umount(), false);
+ // Get value
+ assert_eq!(component.get_value(), Payload::None);
+ // Render
+ assert_eq!(component.render().unwrap().cursor, 0);
+ // Event
+ assert_eq!(
+ component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
+ Msg::None
+ );
+ }
+}