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 + ); + } +}