diff --git a/src/ui/layout/components/file_list.rs b/src/ui/layout/components/file_list.rs
new file mode 100644
index 0000000..77ad081
--- /dev/null
+++ b/src/ui/layout/components/file_list.rs
@@ -0,0 +1,260 @@
+//! ## FileList
+//!
+//! `FileList` component renders a file list tab
+
+/*
+*
+* 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, States, Widget};
+// ext
+use crossterm::event::KeyCode;
+use tui::{
+ layout::Corner,
+ style::{Color, Style},
+ text::Span,
+ widgets::{Block, Borders, List, ListItem},
+};
+
+// -- states
+
+/// ## OwnStates
+///
+/// OwnStates contains states for this component
+#[derive(Clone)]
+struct OwnStates {
+ list_index: usize, // Index of selected element in list
+ list_len: usize, // Length of file list
+}
+
+impl Default for OwnStates {
+ fn default() -> Self {
+ OwnStates {
+ list_index: 0,
+ list_len: 0,
+ }
+ }
+}
+
+impl OwnStates {
+ /// ### set_list_len
+ ///
+ /// Set list length
+ pub fn set_list_len(&mut self, len: usize) {
+ self.list_len = len;
+ }
+
+ /// ### get_list_index
+ ///
+ /// Return current value for list index
+ pub fn get_list_index(&self) -> usize {
+ self.list_index
+ }
+
+ /// ### incr_list_index
+ ///
+ /// Incremenet list index
+ pub fn incr_list_index(&mut self) {
+ // Check if index is at last element
+ if self.list_len + 1 < self.list_len {
+ self.list_len += 1;
+ }
+ }
+
+ /// ### decr_list_index
+ ///
+ /// Decrement list index
+ pub fn decr_list_index(&mut self) {
+ // Check if index is bigger than 0
+ if self.list_len > 0 {
+ self.list_len -= 1;
+ }
+ }
+
+ /// ### reset_list_index
+ ///
+ /// Reset list index to 0
+ pub fn reset_list_index(&mut self) {
+ self.list_len = 0;
+ }
+}
+
+impl States for OwnStates {}
+
+// -- Component
+
+/// ## FileList
+///
+/// File list component
+pub struct FileList {
+ props: Props,
+ states: OwnStates,
+}
+
+impl FileList {
+ /// ### new
+ ///
+ /// Instantiates a new FileList starting from Props
+ /// The method also initializes the component states.
+ pub fn new(props: Props) -> Self {
+ // Initialize states
+ let mut states: OwnStates = OwnStates::default();
+ // Set list length
+ states.set_list_len(match &props.texts.body {
+ Some(tokens) => tokens.len(),
+ None => 0,
+ });
+ FileList { props, states }
+ }
+}
+
+impl Component for FileList {
+ /// ### 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> {
+ match self.props.visible {
+ false => None,
+ true => {
+ // Make list
+ let list_item: Vec = match self.props.texts.body.as_ref() {
+ None => vec![],
+ Some(lines) => lines
+ .iter()
+ .map(|line: &String| ListItem::new(Span::from(line.to_string())))
+ .collect(),
+ };
+ let (fg, bg): (Color, Color) = match self.props.focus {
+ true => (Color::Reset, self.props.background),
+ false => (self.props.foreground, Color::Reset),
+ };
+ let title: String = match self.props.texts.title.as_ref() {
+ Some(t) => t.clone(),
+ None => String::new(),
+ };
+ // Render
+ Some(Box::new(
+ List::new(list_item)
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .border_style(match self.props.focus {
+ true => Style::default().fg(self.props.foreground),
+ false => Style::default(),
+ })
+ .title(title),
+ )
+ .start_corner(Corner::TopLeft)
+ .highlight_style(
+ Style::default()
+ .bg(bg)
+ .fg(fg)
+ .add_modifier(self.props.get_modifiers()),
+ ),
+ ))
+ }
+ }
+ }
+
+ /// ### 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
+ fn update(&mut self, props: Props) -> Msg {
+ self.props = props;
+ // re-Set list length
+ self.states.set_list_len(match &self.props.texts.body {
+ Some(tokens) => tokens.len(),
+ None => 0,
+ });
+ // Reset list index
+ self.states.reset_list_index();
+ 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_props(&self.props)
+ }
+
+ /// ### on
+ ///
+ /// Handle input event and update internal states
+ fn on(&mut self, ev: InputEvent) -> Msg {
+ // Match event
+ if let InputEvent::Key(key) = ev {
+ match key.code {
+ KeyCode::Down => {
+ // Update states
+ self.states.incr_list_index();
+ Msg::None
+ }
+ KeyCode::Up => {
+ // Update states
+ self.states.decr_list_index();
+ Msg::None
+ }
+ KeyCode::PageDown => {
+ // Update states
+ for _ in 0..8 {
+ self.states.incr_list_index();
+ }
+ Msg::None
+ }
+ KeyCode::PageUp => {
+ // Update states
+ for _ in 0..8 {
+ self.states.decr_list_index();
+ }
+ Msg::None
+ }
+ KeyCode::Enter => {
+ // Report event
+ Msg::OnSubmit(Payload::Unumber(self.states.get_list_index()))
+ }
+ _ => {
+ // Return key event to activity
+ Msg::OnKey(key)
+ }
+ }
+ } else {
+ // Unhandled event
+ Msg::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.
+ fn should_umount(&self) -> bool {
+ // Never true
+ false
+ }
+}
diff --git a/src/ui/layout/components/mod.rs b/src/ui/layout/components/mod.rs
new file mode 100644
index 0000000..0cfd141
--- /dev/null
+++ b/src/ui/layout/components/mod.rs
@@ -0,0 +1,30 @@
+//! ## Components
+//!
+//! `Components` is the module which contains the definitions for all the GUI components for TermSCP
+
+/*
+*
+* 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 .
+*
+*/
+
+// imports
+use super::{Component, InputEvent, Msg, Payload, Props, PropsBuilder, States, Widget};
+
+// exports
+pub mod file_list;
diff --git a/src/ui/layout/mod.rs b/src/ui/layout/mod.rs
index 62ca049..befc2e7 100644
--- a/src/ui/layout/mod.rs
+++ b/src/ui/layout/mod.rs
@@ -28,31 +28,34 @@ pub mod components;
pub mod props;
// locals
-use crate::ui::activities::Activity;
use props::{Props, PropsBuilder};
// ext
use crossterm::event::Event as InputEvent;
+use crossterm::event::KeyEvent;
use tui::widgets::Widget;
// -- Msg
/// ## Msg
-///
+///
/// Msg is an enum returned by an `Update` or an `On`.
/// Yep, I took inspiration from Elm.
+#[derive(std::fmt::Debug)]
pub enum Msg {
OnSubmit(Payload),
+ OnKey(KeyEvent),
None,
}
/// ## Payload
-///
+///
/// Payload describes the payload for a `Msg`
+#[derive(std::fmt::Debug)]
pub enum Payload {
Text(String),
Number(isize),
Unumber(usize),
- None
+ None,
}
// -- States
diff --git a/src/ui/layout/props.rs b/src/ui/layout/props.rs
index e7d66ff..b42d5bf 100644
--- a/src/ui/layout/props.rs
+++ b/src/ui/layout/props.rs
@@ -24,7 +24,7 @@
*/
// ext
-use tui::style::Color;
+use tui::style::{Color, Modifier};
// -- Props
@@ -35,6 +35,7 @@ use tui::style::Color;
pub struct Props {
// Values
pub visible: bool, // Is the element visible ON CREATE?
+ pub focus: bool, // Is the element focused
pub foreground: Color, // Foreground color
pub background: Color, // Background color
pub bold: bool, // Text bold
@@ -48,6 +49,7 @@ impl Default for Props {
Self {
// Values
visible: true,
+ focus: false,
foreground: Color::Reset,
background: Color::Reset,
bold: false,
@@ -58,6 +60,27 @@ impl Default for Props {
}
}
+impl Props {
+ /// ### get_modifiers
+ ///
+ /// Get text modifiers from properties
+ pub fn get_modifiers(&self) -> Modifier {
+ Modifier::empty()
+ | (match self.bold {
+ true => Modifier::BOLD,
+ false => Modifier::empty(),
+ })
+ | (match self.italic {
+ true => Modifier::ITALIC,
+ false => Modifier::empty(),
+ })
+ | (match self.underlined {
+ true => Modifier::UNDERLINED,
+ false => Modifier::empty(),
+ })
+ }
+}
+
// -- Props builder
/// ## PropsBuilder
@@ -94,6 +117,36 @@ impl PropsBuilder {
self
}
+ /// ### visible
+ ///
+ /// Initialize props with visible set to True
+ pub fn visible(&mut self) -> &mut Self {
+ if let Some(props) = self.props.as_mut() {
+ props.visible = true;
+ }
+ self
+ }
+
+ /// ### has_focus
+ ///
+ /// Initialize props with focus set to True
+ pub fn has_focus(&mut self) -> &mut Self {
+ if let Some(props) = self.props.as_mut() {
+ props.focus = true;
+ }
+ self
+ }
+
+ /// ### hasnt_focus
+ ///
+ /// Initialize props with focus set to False
+ pub fn hasnt_focus(&mut self) -> &mut Self {
+ if let Some(props) = self.props.as_mut() {
+ props.focus = false;
+ }
+ self
+ }
+
/// ### with_foreground
///
/// Set foreground color for component
@@ -204,16 +257,29 @@ mod tests {
assert_eq!(props.background, Color::Reset);
assert_eq!(props.foreground, Color::Reset);
assert_eq!(props.bold, false);
+ assert_eq!(props.focus, false);
assert_eq!(props.italic, false);
assert_eq!(props.underlined, false);
assert!(props.texts.title.is_none());
assert!(props.texts.body.is_none());
}
+ #[test]
+ fn test_ui_layout_props_modifiers() {
+ // Make properties
+ let props: Props = PropsBuilder::default().bold().italic().underlined().build();
+ // Get modifiers
+ let modifiers: Modifier = props.get_modifiers();
+ assert!(modifiers.intersects(Modifier::BOLD));
+ assert!(modifiers.intersects(Modifier::ITALIC));
+ assert!(modifiers.intersects(Modifier::UNDERLINED));
+ }
+
#[test]
fn test_ui_layout_props_builder() {
let props: Props = PropsBuilder::default()
.hidden()
+ .has_focus()
.with_background(Color::Blue)
.with_foreground(Color::Green)
.bold()
@@ -226,6 +292,7 @@ mod tests {
.build();
assert_eq!(props.background, Color::Blue);
assert_eq!(props.bold, true);
+ assert_eq!(props.focus, true);
assert_eq!(props.foreground, Color::Green);
assert_eq!(props.italic, true);
assert_eq!(props.texts.title.as_ref().unwrap().as_str(), "hello");
@@ -234,8 +301,32 @@ mod tests {
"hey"
);
assert_eq!(props.underlined, true);
- assert!(props.on_submit.is_some());
assert_eq!(props.visible, false);
+ let props: Props = PropsBuilder::default()
+ .visible()
+ .hasnt_focus()
+ .with_background(Color::Blue)
+ .with_foreground(Color::Green)
+ .bold()
+ .italic()
+ .underlined()
+ .with_texts(TextParts::new(
+ Some(String::from("hello")),
+ Some(vec![String::from("hey")]),
+ ))
+ .build();
+ assert_eq!(props.background, Color::Blue);
+ assert_eq!(props.bold, true);
+ assert_eq!(props.focus, false);
+ assert_eq!(props.foreground, Color::Green);
+ assert_eq!(props.italic, true);
+ assert_eq!(props.texts.title.as_ref().unwrap().as_str(), "hello");
+ assert_eq!(
+ props.texts.body.as_ref().unwrap().get(0).unwrap().as_str(),
+ "hey"
+ );
+ assert_eq!(props.underlined, true);
+ assert_eq!(props.visible, true);
}
#[test]
@@ -273,7 +364,7 @@ mod tests {
))
.build();
// Ok, now make a builder from properties
- let builder: PropsBuilder = PropsBuilder::from_props(props);
+ let builder: PropsBuilder = PropsBuilder::from_props(&props);
assert!(builder.props.is_some());
}