From e61e0c018c7b6b397af02270d4df475ceb9038b5 Mon Sep 17 00:00:00 2001 From: veeso Date: Wed, 3 Mar 2021 22:02:58 +0100 Subject: [PATCH] File list component --- src/ui/layout/components/file_list.rs | 260 ++++++++++++++++++++++++++ src/ui/layout/components/mod.rs | 30 +++ src/ui/layout/mod.rs | 11 +- src/ui/layout/props.rs | 97 +++++++++- 4 files changed, 391 insertions(+), 7 deletions(-) create mode 100644 src/ui/layout/components/file_list.rs create mode 100644 src/ui/layout/components/mod.rs 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()); }