File list component

This commit is contained in:
veeso 2021-03-03 22:02:58 +01:00
parent b57763e688
commit e61e0c018c
4 changed files with 391 additions and 7 deletions

View file

@ -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 <http://www.gnu.org/licenses/>.
*
*/
// 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<Box<dyn Widget>> {
match self.props.visible {
false => None,
true => {
// Make list
let list_item: Vec<ListItem> = 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
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*
*/
// imports
use super::{Component, InputEvent, Msg, Payload, Props, PropsBuilder, States, Widget};
// exports
pub mod file_list;

View file

@ -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

View file

@ -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());
}