Removed tui; added tuirealm

This commit is contained in:
veeso
2021-04-24 11:38:28 +02:00
parent 84c36f1789
commit 8bb66b53e0
35 changed files with 1191 additions and 4066 deletions

View File

@@ -32,11 +32,11 @@ extern crate dirs;
use super::{AuthActivity, FileTransferProtocol};
use crate::system::bookmarks_client::BookmarksClient;
use crate::system::environment;
use crate::ui::layout::props::PropValue;
use crate::ui::layout::Payload;
// Ext
use std::path::PathBuf;
use tuirealm::components::{input::InputPropsBuilder, radio::RadioPropsBuilder};
use tuirealm::{Payload, PropsBuilder};
impl AuthActivity {
/// ### del_bookmark
@@ -83,7 +83,7 @@ impl AuthActivity {
let password: Option<String> = match save_password {
true => match self
.view
.get_value(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
.get_state(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
{
Some(Payload::Unsigned(0)) => Some(password), // Yes
_ => None, // No such component / No
@@ -246,32 +246,34 @@ impl AuthActivity {
password: Option<String>,
) {
// Load parameters into components
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_ADDR) {
let props = props.with_value(PropValue::Str(addr)).build();
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_ADDR) {
let props = InputPropsBuilder::from(props).with_value(addr).build();
self.view.update(super::COMPONENT_INPUT_ADDR, props);
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_PORT) {
let props = props.with_value(PropValue::Str(port.to_string())).build();
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PORT) {
let props = InputPropsBuilder::from(props)
.with_value(port.to_string())
.build();
self.view.update(super::COMPONENT_INPUT_PORT, props);
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
let props = props
.with_value(PropValue::Unsigned(match protocol {
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
let props = RadioPropsBuilder::from(props)
.with_value(match protocol {
FileTransferProtocol::Sftp => 0,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
}))
})
.build();
self.view.update(super::COMPONENT_RADIO_PROTOCOL, props);
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_USERNAME) {
let props = props.with_value(PropValue::Str(username)).build();
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_USERNAME) {
let props = InputPropsBuilder::from(props).with_value(username).build();
self.view.update(super::COMPONENT_INPUT_USERNAME, props);
}
if let Some(password) = password {
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
let props = props.with_value(PropValue::Str(password)).build();
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
let props = InputPropsBuilder::from(props).with_value(password).build();
self.view.update(super::COMPONENT_INPUT_PASSWORD, props);
}
}

View File

@@ -32,18 +32,18 @@ mod view;
// Dependencies
extern crate crossterm;
extern crate tui;
extern crate tuirealm;
// locals
use super::{Activity, Context, ExitReason};
use crate::filetransfer::FileTransferProtocol;
use crate::system::bookmarks_client::BookmarksClient;
use crate::ui::context::FileTransferParams;
use crate::ui::layout::view::View;
use crate::utils::git;
// Includes
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use tuirealm::View;
// -- components
const COMPONENT_TEXT_HEADER: &str = "TEXT_HEADER";

View File

@@ -35,7 +35,7 @@ use super::{
COMPONENT_TEXT_HELP,
};
use crate::ui::activities::keymap::*;
use crate::ui::layout::{Msg, Payload};
use tuirealm::{Msg, Payload};
// -- update
@@ -164,7 +164,7 @@ impl AuthActivity {
match *index {
0 => {
// Get selected bookmark
match self.view.get_value(COMPONENT_BOOKMARKS_LIST) {
match self.view.get_state(COMPONENT_BOOKMARKS_LIST) {
Some(Payload::Unsigned(index)) => {
// Delete bookmark
self.del_bookmark(index);
@@ -184,7 +184,7 @@ impl AuthActivity {
match *index {
0 => {
// Get selected bookmark
match self.view.get_value(COMPONENT_RECENTS_LIST) {
match self.view.get_state(COMPONENT_RECENTS_LIST) {
Some(Payload::Unsigned(index)) => {
// Delete recent
self.del_recent(index);
@@ -199,36 +199,12 @@ impl AuthActivity {
}
// <ESC> hide tab
(COMPONENT_RADIO_BOOKMARK_DEL_RECENT, &MSG_KEY_ESC) => {
match self
.view
.get_props(COMPONENT_RADIO_BOOKMARK_DEL_RECENT)
.as_mut()
{
Some(props) => {
let msg = self.view.update(
COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
props.hidden().build(),
);
self.update(msg)
}
None => None,
}
self.umount_recent_del_dialog();
None
}
(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, &MSG_KEY_ESC) => {
match self
.view
.get_props(COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK)
.as_mut()
{
Some(props) => {
let msg = self.view.update(
COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
props.hidden().build(),
);
self.update(msg)
}
None => None,
}
self.umount_bookmark_del_dialog();
None
}
// Help
(_, &MSG_KEY_CTRL_H) => {
@@ -269,12 +245,12 @@ impl AuthActivity {
| (COMPONENT_RADIO_BOOKMARK_SAVE_PWD, Msg::OnSubmit(_)) => {
// Get values
let bookmark_name: String =
match self.view.get_value(COMPONENT_INPUT_BOOKMARK_NAME) {
match self.view.get_state(COMPONENT_INPUT_BOOKMARK_NAME) {
Some(Payload::Text(s)) => s,
_ => String::new(),
};
let save_pwd: bool = matches!(
self.view.get_value(COMPONENT_RADIO_BOOKMARK_SAVE_PWD),
self.view.get_state(COMPONENT_RADIO_BOOKMARK_SAVE_PWD),
Some(Payload::Unsigned(0))
);
// Save bookmark

View File

@@ -27,20 +27,27 @@
*/
// Locals
use super::{AuthActivity, Context, FileTransferProtocol};
use crate::ui::layout::components::{
bookmark_list::BookmarkList, input::Input, msgbox::MsgBox, radio_group::RadioGroup,
table::Table, text::Text, title::Title,
use crate::ui::components::{
bookmark_list::{BookmarkList, BookmarkListPropsBuilder},
msgbox::{MsgBox, MsgBoxPropsBuilder},
};
use crate::ui::layout::props::{
InputType, PropValue, PropsBuilder, TableBuilder, TextParts, TextSpan, TextSpanBuilder,
};
use crate::ui::layout::utils::draw_area_in;
use crate::ui::layout::{Msg, Payload};
use crate::utils::ui::draw_area_in;
// Ext
use tui::{
use tuirealm::components::{
input::{Input, InputPropsBuilder},
label::{Label, LabelPropsBuilder},
radio::{Radio, RadioPropsBuilder},
span::{Span, SpanPropsBuilder},
table::{Table, TablePropsBuilder},
};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
widgets::Clear,
widgets::{BorderType, Borders, Clear},
};
use tuirealm::{
props::{InputType, PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder},
Msg, Payload,
};
impl AuthActivity {
@@ -50,35 +57,32 @@ impl AuthActivity {
pub(super) fn init(&mut self) {
// Header
self.view.mount(super::COMPONENT_TEXT_HEADER, Box::new(
Title::new(
PropsBuilder::default().with_foreground(Color::White).with_texts(
TextParts::new(Some(String::from(" _____ ____ ____ ____ \n|_ _|__ _ __ _ __ ___ / ___| / ___| _ \\ \n | |/ _ \\ '__| '_ ` _ \\\\___ \\| | | |_) |\n | | __/ | | | | | | |___) | |___| __/ \n |_|\\___|_| |_| |_| |_|____/ \\____|_| \n")), None)
Label::new(
LabelPropsBuilder::default().with_foreground(Color::White).with_text(
String::from(" _____ ____ ____ ____ \n|_ _|__ _ __ _ __ ___ / ___| / ___| _ \\ \n | |/ _ \\ '__| '_ ` _ \\\\___ \\| | | |_) |\n | | __/ | | | | | | |___) | |___| __/ \n |_|\\___|_| |_| |_| |_|____/ \\____|_| \n")
).bold().build()
)
));
// Footer
self.view.mount(
super::COMPONENT_TEXT_FOOTER,
Box::new(Text::new(
PropsBuilder::default()
.with_texts(TextParts::new(
None,
Some(vec![
TextSpanBuilder::new("Press ").bold().build(),
TextSpanBuilder::new("<CTRL+H>")
.bold()
.with_foreground(Color::Cyan)
.build(),
TextSpanBuilder::new(" to show keybindings; ")
.bold()
.build(),
TextSpanBuilder::new("<CTRL+C>")
.bold()
.with_foreground(Color::Cyan)
.build(),
TextSpanBuilder::new(" to enter setup").bold().build(),
]),
))
Box::new(Span::new(
SpanPropsBuilder::default()
.with_spans(vec![
TextSpanBuilder::new("Press ").bold().build(),
TextSpanBuilder::new("<CTRL+H>")
.bold()
.with_foreground(Color::Cyan)
.build(),
TextSpanBuilder::new(" to show keybindings; ")
.bold()
.build(),
TextSpanBuilder::new("<CTRL+C>")
.bold()
.with_foreground(Color::Cyan)
.build(),
TextSpanBuilder::new(" to enter setup").bold().build(),
])
.build(),
)),
);
@@ -86,9 +90,10 @@ impl AuthActivity {
self.view.mount(
super::COMPONENT_INPUT_ADDR,
Box::new(Input::new(
PropsBuilder::default()
InputPropsBuilder::default()
.with_foreground(Color::Yellow)
.with_texts(TextParts::new(Some(String::from("Remote address")), None))
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
.with_label(String::from("Remote address"))
.build(),
)),
);
@@ -96,31 +101,33 @@ impl AuthActivity {
self.view.mount(
super::COMPONENT_INPUT_PORT,
Box::new(Input::new(
PropsBuilder::default()
InputPropsBuilder::default()
.with_foreground(Color::LightCyan)
.with_texts(TextParts::new(Some(String::from("Port number")), None))
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
.with_label(String::from("Port number"))
.with_input(InputType::Number)
.with_input_len(5)
.with_value(PropValue::Str(String::from("22")))
.with_value(String::from("22"))
.build(),
)),
);
// Protocol
self.view.mount(
super::COMPONENT_RADIO_PROTOCOL,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::LightGreen)
.with_background(Color::Black)
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightGreen)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
.with_options(
Some(String::from("Protocol")),
Some(vec![
TextSpan::from("SFTP"),
TextSpan::from("SCP"),
TextSpan::from("FTP"),
TextSpan::from("FTPS"),
]),
))
vec![
String::from("SFTP"),
String::from("SCP"),
String::from("FTP"),
String::from("FTPS"),
],
)
.build(),
)),
);
@@ -128,9 +135,10 @@ impl AuthActivity {
self.view.mount(
super::COMPONENT_INPUT_USERNAME,
Box::new(Input::new(
PropsBuilder::default()
InputPropsBuilder::default()
.with_foreground(Color::LightMagenta)
.with_texts(TextParts::new(Some(String::from("Username")), None))
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
.with_label(String::from("Username"))
.build(),
)),
);
@@ -138,9 +146,10 @@ impl AuthActivity {
self.view.mount(
super::COMPONENT_INPUT_PASSWORD,
Box::new(Input::new(
PropsBuilder::default()
InputPropsBuilder::default()
.with_foreground(Color::LightBlue)
.with_texts(TextParts::new(Some(String::from("Password")), None))
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue)
.with_label(String::from("Password"))
.with_input(InputType::Password)
.build(),
)),
@@ -155,10 +164,16 @@ impl AuthActivity {
{
self.view.mount(
super::COMPONENT_TEXT_NEW_VERSION,
Box::new(Text::new(
PropsBuilder::default()
Box::new(Span::new(
SpanPropsBuilder::default()
.with_foreground(Color::Yellow)
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(format!("TermSCP {} is now available! Download it from <https://github.com/veeso/termscp/releases/latest>", version))])))
.with_spans(
vec![
TextSpan::from("TermSCP "),
TextSpanBuilder::new(version).underlined().bold().build(),
TextSpan::from(" is now available! Download it from <https://github.com/veeso/termscp/releases/latest>")
]
)
.build()
))
);
@@ -167,10 +182,11 @@ impl AuthActivity {
self.view.mount(
super::COMPONENT_BOOKMARKS_LIST,
Box::new(BookmarkList::new(
PropsBuilder::default()
BookmarkListPropsBuilder::default()
.with_background(Color::LightGreen)
.with_foreground(Color::Black)
.with_texts(TextParts::new(Some(String::from("Bookmarks")), None))
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
.with_bookmarks(Some(String::from("Bookmarks")), vec![])
.build(),
)),
);
@@ -179,13 +195,11 @@ impl AuthActivity {
self.view.mount(
super::COMPONENT_RECENTS_LIST,
Box::new(BookmarkList::new(
PropsBuilder::default()
BookmarkListPropsBuilder::default()
.with_background(Color::LightBlue)
.with_foreground(Color::Black)
.with_texts(TextParts::new(
Some(String::from("Recent connections")),
None,
))
.with_borders(Borders::ALL, BorderType::Plain, Color::LightBlue)
.with_bookmarks(Some(String::from("Recent connections")), vec![])
.build(),
)),
);
@@ -258,27 +272,27 @@ impl AuthActivity {
self.view
.render(super::COMPONENT_RECENTS_LIST, f, bookmark_chunks[1]);
// Popups
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
}
}
if let Some(mut props) = self
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK)
{
if props.build().visible {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
@@ -286,11 +300,11 @@ impl AuthActivity {
.render(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK, f, popup);
}
}
if let Some(mut props) = self
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT)
{
if props.build().visible {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
@@ -298,19 +312,19 @@ impl AuthActivity {
.render(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 50, 70);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
}
}
if let Some(mut props) = self
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
{
if props.build().visible {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 20, 20);
f.render_widget(Clear, popup);
@@ -340,7 +354,7 @@ impl AuthActivity {
///
/// Make text span from bookmarks
pub(super) fn view_bookmarks(&mut self) -> Option<(String, Msg)> {
let bookmarks: Vec<TextSpan> = self
let bookmarks: Vec<String> = self
.bookmarks_list
.iter()
.map(|x| {
@@ -350,33 +364,23 @@ impl AuthActivity {
.unwrap()
.get_bookmark(x)
.unwrap();
TextSpan::from(
format!(
"{} ({}://{}@{}:{})",
x,
entry.2.to_string().to_lowercase(),
entry.3,
entry.0,
entry.1
)
.as_str(),
format!(
"{} ({}://{}@{}:{})",
x,
entry.2.to_string().to_lowercase(),
entry.3,
entry.0,
entry.1
)
})
.collect();
match self
.view
.get_props(super::COMPONENT_BOOKMARKS_LIST)
.as_mut()
{
match self.view.get_props(super::COMPONENT_BOOKMARKS_LIST) {
None => None,
Some(props) => {
let msg = self.view.update(
super::COMPONENT_BOOKMARKS_LIST,
props
.with_texts(TextParts::new(
Some(String::from("Bookmarks")),
Some(bookmarks),
))
BookmarkListPropsBuilder::from(props)
.with_bookmarks(Some(String::from("Bookmarks")), bookmarks)
.build(),
);
msg
@@ -388,7 +392,7 @@ impl AuthActivity {
///
/// View recent connections
pub(super) fn view_recent_connections(&mut self) -> Option<(String, Msg)> {
let bookmarks: Vec<TextSpan> = self
let bookmarks: Vec<String> = self
.recents_list
.iter()
.map(|x| {
@@ -398,28 +402,23 @@ impl AuthActivity {
.unwrap()
.get_recent(x)
.unwrap();
TextSpan::from(
format!(
"{}://{}@{}:{}",
entry.2.to_string().to_lowercase(),
entry.3,
entry.0,
entry.1
)
.as_str(),
format!(
"{}://{}@{}:{}",
entry.2.to_string().to_lowercase(),
entry.3,
entry.0,
entry.1
)
})
.collect();
match self.view.get_props(super::COMPONENT_RECENTS_LIST).as_mut() {
match self.view.get_props(super::COMPONENT_RECENTS_LIST) {
None => None,
Some(props) => {
let msg = self.view.update(
super::COMPONENT_RECENTS_LIST,
props
.with_texts(TextParts::new(
Some(String::from("Recent connections")),
Some(bookmarks),
))
BookmarkListPropsBuilder::from(props)
.with_bookmarks(Some(String::from("Recent connections")), bookmarks)
.build(),
);
msg
@@ -437,10 +436,11 @@ impl AuthActivity {
self.view.mount(
super::COMPONENT_TEXT_ERROR,
Box::new(MsgBox::new(
PropsBuilder::default()
MsgBoxPropsBuilder::default()
.with_foreground(Color::Red)
.with_borders(Borders::ALL, BorderType::Thick, Color::Red)
.bold()
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)])))
.with_texts(None, vec![TextSpan::from(text)])
.build(),
)),
);
@@ -462,14 +462,15 @@ impl AuthActivity {
// Protocol
self.view.mount(
super::COMPONENT_RADIO_QUIT,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::Yellow)
.with_background(Color::Black)
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::Yellow)
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
.with_inverted_color(Color::Black)
.with_options(
Some(String::from("Quit TermSCP?")),
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
))
vec![String::from("Yes"), String::from("No")],
)
.build(),
)),
);
@@ -489,15 +490,16 @@ impl AuthActivity {
pub(super) fn mount_bookmark_del_dialog(&mut self) {
self.view.mount(
super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::Yellow)
.with_background(Color::Black)
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::Yellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
.with_options(
Some(String::from("Delete bookmark?")),
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
))
.with_value(PropValue::Unsigned(1))
vec![String::from("Yes"), String::from("No")],
)
.with_value(1)
.build(),
)),
);
@@ -520,15 +522,16 @@ impl AuthActivity {
pub(super) fn mount_recent_del_dialog(&mut self) {
self.view.mount(
super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::Yellow)
.with_background(Color::Black)
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::Yellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
.with_options(
Some(String::from("Delete bookmark?")),
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
))
.with_value(PropValue::Unsigned(1))
vec![String::from("Yes"), String::from("No")],
)
.with_value(1)
.build(),
)),
);
@@ -550,27 +553,31 @@ impl AuthActivity {
self.view.mount(
super::COMPONENT_INPUT_BOOKMARK_NAME,
Box::new(Input::new(
PropsBuilder::default()
InputPropsBuilder::default()
.with_foreground(Color::LightCyan)
.with_texts(TextParts::new(
Some(String::from("Save bookmark as...")),
None,
))
//.with_borders(Borders::TOP | Borders::RIGHT | Borders::LEFT)
.with_label(String::from("Save bookmark as..."))
.with_borders(
Borders::TOP | Borders::RIGHT | Borders::LEFT,
BorderType::Rounded,
Color::Reset,
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::Red)
//.with_borders(Borders::BOTTOM | Borders::RIGHT | Borders::LEFT)
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::Red)
.with_borders(
Borders::BOTTOM | Borders::RIGHT | Borders::LEFT,
BorderType::Rounded,
Color::Reset,
)
.with_options(
Some(String::from("Save password?")),
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
))
//.with_value(PropValue::Unsigned(1))
vec![String::from("Yes"), String::from("No")],
)
.build(),
)),
);
@@ -593,8 +600,9 @@ impl AuthActivity {
self.view.mount(
super::COMPONENT_TEXT_HELP,
Box::new(Table::new(
PropsBuilder::default()
.with_texts(TextParts::table(
TablePropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_table(
Some(String::from("Help")),
TableBuilder::default()
.add_col(
@@ -661,7 +669,7 @@ impl AuthActivity {
)
.add_col(TextSpan::from(" Save bookmark"))
.build(),
))
)
.build(),
)),
);
@@ -680,16 +688,16 @@ impl AuthActivity {
///
/// Collect input values from view
pub(super) fn get_input(&self) -> (String, u16, FileTransferProtocol, String, String) {
let addr: String = match self.view.get_value(super::COMPONENT_INPUT_ADDR) {
let addr: String = match self.view.get_state(super::COMPONENT_INPUT_ADDR) {
Some(Payload::Text(a)) => a,
_ => String::new(),
};
let port: u16 = match self.view.get_value(super::COMPONENT_INPUT_PORT) {
let port: u16 = match self.view.get_state(super::COMPONENT_INPUT_PORT) {
Some(Payload::Unsigned(p)) => p as u16,
_ => 0,
};
let protocol: FileTransferProtocol =
match self.view.get_value(super::COMPONENT_RADIO_PROTOCOL) {
match self.view.get_state(super::COMPONENT_RADIO_PROTOCOL) {
Some(Payload::Unsigned(p)) => match p {
1 => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false),
@@ -698,11 +706,11 @@ impl AuthActivity {
},
_ => FileTransferProtocol::Sftp,
};
let username: String = match self.view.get_value(super::COMPONENT_INPUT_USERNAME) {
let username: String = match self.view.get_state(super::COMPONENT_INPUT_USERNAME) {
Some(Payload::Text(a)) => a,
_ => String::new(),
};
let password: String = match self.view.get_value(super::COMPONENT_INPUT_PASSWORD) {
let password: String = match self.view.get_state(super::COMPONENT_INPUT_PASSWORD) {
Some(Payload::Text(a)) => a,
_ => String::new(),
};

View File

@@ -27,7 +27,7 @@
*/
// locals
use super::{FileExplorerTab, FileTransferActivity, FsEntry, LogLevel};
use crate::ui::layout::Payload;
use tuirealm::Payload;
// externals
use std::path::PathBuf;
@@ -622,7 +622,7 @@ impl FileTransferActivity {
///
/// Get index of selected file in the local tab
fn get_local_file_idx(&self) -> Option<usize> {
match self.view.get_value(super::COMPONENT_EXPLORER_LOCAL) {
match self.view.get_state(super::COMPONENT_EXPLORER_LOCAL) {
Some(Payload::Unsigned(idx)) => Some(idx),
_ => None,
}
@@ -632,7 +632,7 @@ impl FileTransferActivity {
///
/// Get index of selected file in the remote file
fn get_remote_file_idx(&self) -> Option<usize> {
match self.view.get_value(super::COMPONENT_EXPLORER_REMOTE) {
match self.view.get_state(super::COMPONENT_EXPLORER_REMOTE) {
Some(Payload::Unsigned(idx)) => Some(idx),
_ => None,
}

View File

@@ -36,7 +36,7 @@ mod view;
extern crate chrono;
extern crate crossterm;
extern crate textwrap;
extern crate tui;
extern crate tuirealm;
// locals
use super::{Activity, Context, ExitReason};
@@ -47,7 +47,6 @@ use crate::filetransfer::{FileTransfer, FileTransferProtocol};
use crate::fs::explorer::FileExplorer;
use crate::fs::FsEntry;
use crate::system::config_client::ConfigClient;
use crate::ui::layout::view::View;
// Includes
use chrono::{DateTime, Local};
@@ -55,6 +54,7 @@ use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::collections::VecDeque;
use std::path::PathBuf;
use std::time::Instant;
use tuirealm::View;
// -- Storage keys

View File

@@ -40,14 +40,16 @@ use super::{
use crate::fs::explorer::FileSorting;
use crate::fs::FsEntry;
use crate::ui::activities::keymap::*;
use crate::ui::layout::props::{
PropValue, PropsBuilder, TableBuilder, TextParts, TextSpan, TextSpanBuilder,
};
use crate::ui::layout::{Msg, Payload};
use crate::ui::components::{file_list::FileListPropsBuilder, logbox::LogboxPropsBuilder};
// externals
use bytesize::ByteSize;
use std::path::{Path, PathBuf};
use tui::style::Color;
use tuirealm::{
components::progress_bar::ProgressBarPropsBuilder,
props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder},
tui::style::Color,
Msg, Payload,
};
impl FileTransferActivity {
// -- update
@@ -394,7 +396,7 @@ impl FileTransferActivity {
}
(COMPONENT_EXPLORER_FIND, &MSG_KEY_SPACE) => {
// Get entry
match self.view.get_value(COMPONENT_EXPLORER_FIND) {
match self.view.get_state(COMPONENT_EXPLORER_FIND) {
Some(Payload::Unsigned(idx)) => {
self.action_find_transfer(idx, None);
// Reload files
@@ -585,7 +587,7 @@ impl FileTransferActivity {
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
// Get entry
if let Some(Payload::Unsigned(idx)) =
self.view.get_value(COMPONENT_EXPLORER_FIND)
self.view.get_state(COMPONENT_EXPLORER_FIND)
{
self.action_find_transfer(idx, Some(input.to_string()));
}
@@ -621,7 +623,7 @@ impl FileTransferActivity {
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
// Get entry
if let Some(Payload::Unsigned(idx)) =
self.view.get_value(COMPONENT_EXPLORER_FIND)
self.view.get_state(COMPONENT_EXPLORER_FIND)
{
self.action_find_delete(idx);
// Reload entries
@@ -662,11 +664,12 @@ impl FileTransferActivity {
None
}
// -- sorting
(COMPONENT_RADIO_SORTING, &MSG_KEY_ESC) => {
(COMPONENT_RADIO_SORTING, &MSG_KEY_ESC)
| (COMPONENT_RADIO_SORTING, Msg::OnSubmit(_)) => {
self.umount_file_sorting();
None
}
(COMPONENT_RADIO_SORTING, Msg::OnSubmit(Payload::Unsigned(mode))) => {
(COMPONENT_RADIO_SORTING, Msg::OnChange(Payload::Unsigned(mode))) => {
// Get sorting mode
let sorting: FileSorting = match mode {
1 => FileSorting::ByModifyTime,
@@ -679,7 +682,6 @@ impl FileTransferActivity {
FileExplorerTab::Remote => self.remote.sort_by(sorting),
_ => panic!("Found result doesn't support SORTING"),
}
self.umount_file_sorting();
// Reload files
match self.tab {
FileExplorerTab::Local => self.update_local_filelist(),
@@ -718,11 +720,7 @@ impl FileTransferActivity {
///
/// Update local file list
pub(super) fn update_local_filelist(&mut self) -> Option<(String, Msg)> {
match self
.view
.get_props(super::COMPONENT_EXPLORER_LOCAL)
.as_mut()
{
match self.view.get_props(super::COMPONENT_EXPLORER_LOCAL) {
Some(props) => {
// Get width
let width: usize = self
@@ -750,14 +748,14 @@ impl FileTransferActivity {
)
.display()
);
let files: Vec<TextSpan> = self
let files: Vec<String> = self
.local
.iter_files()
.map(|x: &FsEntry| TextSpan::from(self.local.fmt_file(x)))
.map(|x: &FsEntry| self.local.fmt_file(x))
.collect();
// Update
let props = props
.with_texts(TextParts::new(Some(hostname), Some(files)))
let props = FileListPropsBuilder::from(props)
.with_files(Some(hostname), files)
.build();
// Update
self.view.update(super::COMPONENT_EXPLORER_LOCAL, props)
@@ -770,11 +768,7 @@ impl FileTransferActivity {
///
/// Update remote file list
pub(super) fn update_remote_filelist(&mut self) -> Option<(String, Msg)> {
match self
.view
.get_props(super::COMPONENT_EXPLORER_REMOTE)
.as_mut()
{
match self.view.get_props(super::COMPONENT_EXPLORER_REMOTE) {
Some(props) => {
// Get width
let width: usize = self
@@ -795,14 +789,14 @@ impl FileTransferActivity {
)
.display()
);
let files: Vec<TextSpan> = self
let files: Vec<String> = self
.remote
.iter_files()
.map(|x: &FsEntry| TextSpan::from(self.remote.fmt_file(x)))
.map(|x: &FsEntry| self.remote.fmt_file(x))
.collect();
// Update
let props = props
.with_texts(TextParts::new(Some(hostname), Some(files)))
let props = FileListPropsBuilder::from(props)
.with_files(Some(hostname), files)
.build();
self.view.update(super::COMPONENT_EXPLORER_REMOTE, props)
}
@@ -814,7 +808,7 @@ impl FileTransferActivity {
///
/// Update log box
pub(super) fn update_logbox(&mut self) -> Option<(String, Msg)> {
match self.view.get_props(super::COMPONENT_LOG_BOX).as_mut() {
match self.view.get_props(super::COMPONENT_LOG_BOX) {
Some(props) => {
// Get width
let width: usize = self
@@ -876,8 +870,8 @@ impl FileTransferActivity {
}
}
let table = table.build();
let props = props
.with_texts(TextParts::table(Some(String::from("Log")), table))
let props = LogboxPropsBuilder::from(props)
.with_log(Some(String::from("Log")), table)
.build();
self.view.update(super::COMPONENT_LOG_BOX, props)
}
@@ -886,7 +880,7 @@ impl FileTransferActivity {
}
pub(super) fn update_progress_bar(&mut self, text: String) -> Option<(String, Msg)> {
match self.view.get_props(COMPONENT_PROGRESS_BAR).as_mut() {
match self.view.get_props(COMPONENT_PROGRESS_BAR) {
Some(props) => {
// Calculate ETA
let elapsed_secs: u64 = self.transfer.started.elapsed().as_secs();
@@ -905,12 +899,9 @@ impl FileTransferActivity {
eta,
ByteSize(self.transfer.bytes_per_second())
);
let props = props
.with_texts(TextParts::new(
Some(text),
Some(vec![TextSpan::from(label)]),
))
.with_value(PropValue::Float(self.transfer.progress / 100.0))
let props = ProgressBarPropsBuilder::from(props)
.with_texts(Some(text), label)
.with_progress(self.transfer.progress / 100.0)
.build();
self.view.update(COMPONENT_PROGRESS_BAR, props)
}
@@ -933,22 +924,20 @@ impl FileTransferActivity {
}
fn update_find_list(&mut self) -> Option<(String, Msg)> {
match self.view.get_props(COMPONENT_EXPLORER_FIND).as_mut() {
match self.view.get_props(COMPONENT_EXPLORER_FIND) {
None => None,
Some(props) => {
let props = props.build();
let title: String = props.texts.title.clone().unwrap_or_default();
let mut props = PropsBuilder::from(props);
// Prepare files
let file_texts: Vec<TextSpan> = self
let files: Vec<String> = self
.found
.as_ref()
.unwrap()
.iter_files()
.map(|x: &FsEntry| TextSpan::from(self.found.as_ref().unwrap().fmt_file(x)))
.map(|x: &FsEntry| self.found.as_ref().unwrap().fmt_file(x))
.collect();
let props = props
.with_texts(TextParts::new(Some(title), Some(file_texts)))
let props = FileListPropsBuilder::from(props)
.with_files(Some(title), files)
.build();
self.view.update(COMPONENT_EXPLORER_FIND, props)
}

View File

@@ -34,23 +34,28 @@ extern crate users;
use super::{Context, FileExplorerTab, FileTransferActivity};
use crate::fs::explorer::FileSorting;
use crate::fs::FsEntry;
use crate::ui::layout::components::{
file_list::FileList, input::Input, logbox::LogBox, msgbox::MsgBox, progress_bar::ProgressBar,
radio_group::RadioGroup, table::Table,
use crate::ui::components::{
file_list::{FileList, FileListPropsBuilder},
logbox::{LogBox, LogboxPropsBuilder},
msgbox::{MsgBox, MsgBoxPropsBuilder},
};
use crate::ui::layout::props::{
PropValue, PropsBuilder, TableBuilder, TextParts, TextSpan, TextSpanBuilder,
};
use crate::ui::layout::utils::draw_area_in;
use crate::ui::store::Store;
use crate::utils::fmt::fmt_time;
use crate::utils::ui::draw_area_in;
// Ext
use bytesize::ByteSize;
use std::path::PathBuf;
use tui::{
use tuirealm::components::{
input::{Input, InputPropsBuilder},
progress_bar::{ProgressBar, ProgressBarPropsBuilder},
radio::{Radio, RadioPropsBuilder},
table::{Table, TablePropsBuilder},
};
use tuirealm::props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
widgets::Clear,
widgets::{BorderType, Borders, Clear},
};
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
use users::{get_group_by_gid, get_user_by_uid};
@@ -66,9 +71,10 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_EXPLORER_LOCAL,
Box::new(FileList::new(
PropsBuilder::default()
FileListPropsBuilder::default()
.with_background(Color::Yellow)
.with_foreground(Color::Yellow)
.with_borders(Borders::ALL, BorderType::Plain, Color::Yellow)
.build(),
)),
);
@@ -76,9 +82,10 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_EXPLORER_REMOTE,
Box::new(FileList::new(
PropsBuilder::default()
FileListPropsBuilder::default()
.with_background(Color::LightBlue)
.with_foreground(Color::LightBlue)
.with_borders(Borders::ALL, BorderType::Plain, Color::LightBlue)
.build(),
)),
);
@@ -86,9 +93,8 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_LOG_BOX,
Box::new(LogBox::new(
PropsBuilder::default()
.with_foreground(Color::LightGreen)
.bold()
LogboxPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
.build(),
)),
);
@@ -156,96 +162,96 @@ impl FileTransferActivity {
// Draw log box
self.view.render(super::COMPONENT_LOG_BOX, f, chunks[1]);
// @! Draw popups
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_COPY) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_COPY) {
if props.visible {
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_INPUT_COPY, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_FIND) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_FIND) {
if props.visible {
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_INPUT_FIND, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_GOTO) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_GOTO) {
if props.visible {
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_INPUT_GOTO, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_MKDIR) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_MKDIR) {
if props.visible {
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_INPUT_MKDIR, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_NEWFILE) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_NEWFILE) {
if props.visible {
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_INPUT_NEWFILE, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_RENAME) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_RENAME) {
if props.visible {
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_INPUT_RENAME, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_SAVEAS) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_SAVEAS) {
if props.visible {
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_INPUT_SAVEAS, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_EXEC) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_EXEC) {
if props.visible {
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_INPUT_EXEC, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_LIST_FILEINFO) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_FILEINFO) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 50);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_LIST_FILEINFO, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_PROGRESS_BAR) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_PROGRESS_BAR) {
if props.visible {
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_PROGRESS_BAR, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_DELETE) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DELETE) {
if props.visible {
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_RADIO_DELETE, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_DISCONNECT) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DISCONNECT) {
if props.visible {
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
// make popup
@@ -253,48 +259,48 @@ impl FileTransferActivity {
.render(super::COMPONENT_RADIO_DISCONNECT, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
if props.visible {
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_SORTING) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SORTING) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_RADIO_SORTING, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_FATAL) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_FATAL) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_TEXT_FATAL, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_WAIT) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_WAIT) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_TEXT_WAIT, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 80);
f.render_widget(Clear, popup);
// make popup
@@ -316,10 +322,11 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_TEXT_ERROR,
Box::new(MsgBox::new(
PropsBuilder::default()
MsgBoxPropsBuilder::default()
.with_foreground(Color::Red)
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
.bold()
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)])))
.with_texts(None, vec![TextSpan::from(text)])
.build(),
)),
);
@@ -339,10 +346,11 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_TEXT_FATAL,
Box::new(MsgBox::new(
PropsBuilder::default()
MsgBoxPropsBuilder::default()
.with_foreground(Color::Red)
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
.bold()
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)])))
.with_texts(None, vec![TextSpan::from(text)])
.build(),
)),
);
@@ -355,10 +363,11 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_TEXT_WAIT,
Box::new(MsgBox::new(
PropsBuilder::default()
MsgBoxPropsBuilder::default()
.with_foreground(Color::White)
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.bold()
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)])))
.with_texts(None, vec![TextSpan::from(text)])
.build(),
)),
);
@@ -377,14 +386,15 @@ impl FileTransferActivity {
// Protocol
self.view.mount(
super::COMPONENT_RADIO_QUIT,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::Yellow)
.with_background(Color::Black)
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::Yellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
.with_options(
Some(String::from("Are you sure you want to quit?")),
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
))
vec![String::from("Yes"), String::from("No")],
)
.build(),
)),
);
@@ -405,14 +415,15 @@ impl FileTransferActivity {
// Protocol
self.view.mount(
super::COMPONENT_RADIO_DISCONNECT,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::Yellow)
.with_background(Color::Black)
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::Yellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::Yellow)
.with_options(
Some(String::from("Are you sure you want to disconnect?")),
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
))
vec![String::from("Yes"), String::from("No")],
)
.build(),
)),
);
@@ -430,11 +441,9 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_INPUT_COPY,
Box::new(Input::new(
PropsBuilder::default()
.with_texts(TextParts::new(
Some(String::from("Insert destination name")),
None,
))
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_label(String::from("Insert destination name"))
.build(),
)),
);
@@ -449,8 +458,9 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_INPUT_EXEC,
Box::new(Input::new(
PropsBuilder::default()
.with_texts(TextParts::new(Some(String::from("Execute command")), None))
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Plain, Color::White)
.with_label(String::from("Execute command"))
.build(),
)),
);
@@ -471,11 +481,9 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_EXPLORER_FIND,
Box::new(FileList::new(
PropsBuilder::default()
.with_texts(TextParts::new(
Some(format!("Search results for \"{}\"", search)),
Some(vec![]),
))
FileListPropsBuilder::default()
.with_files(Some(format!("Search results for \"{}\"", search)), vec![])
.with_borders(Borders::ALL, BorderType::Plain, color)
.with_background(color)
.with_foreground(color)
.build(),
@@ -493,11 +501,9 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_INPUT_FIND,
Box::new(Input::new(
PropsBuilder::default()
.with_texts(TextParts::new(
Some(String::from("Search files by name")),
None,
))
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_label(String::from("Search files by name"))
.build(),
)),
);
@@ -514,11 +520,9 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_INPUT_GOTO,
Box::new(Input::new(
PropsBuilder::default()
.with_texts(TextParts::new(
Some(String::from("Change working directory")),
None,
))
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_label(String::from("Change working directory"))
.build(),
)),
);
@@ -533,11 +537,9 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_INPUT_MKDIR,
Box::new(Input::new(
PropsBuilder::default()
.with_texts(TextParts::new(
Some(String::from("Insert directory name")),
None,
))
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_label(String::from("Insert directory name"))
.build(),
)),
);
@@ -552,8 +554,9 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_INPUT_NEWFILE,
Box::new(Input::new(
PropsBuilder::default()
.with_texts(TextParts::new(Some(String::from("New file name")), None))
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_label(String::from("New file name"))
.build(),
)),
);
@@ -568,8 +571,9 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_INPUT_RENAME,
Box::new(Input::new(
PropsBuilder::default()
.with_texts(TextParts::new(Some(String::from("Insert new name")), None))
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_label(String::from("Insert new name"))
.build(),
)),
);
@@ -584,8 +588,9 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_INPUT_SAVEAS,
Box::new(Input::new(
PropsBuilder::default()
.with_texts(TextParts::new(Some(String::from("Save as...")), None))
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_label(String::from("Save as..."))
.build(),
)),
);
@@ -600,10 +605,11 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_PROGRESS_BAR,
Box::new(ProgressBar::new(
PropsBuilder::default()
.with_foreground(Color::LightGreen)
ProgressBarPropsBuilder::default()
.with_progbar_color(Color::LightGreen)
.with_background(Color::Black)
.with_texts(TextParts::new(Some(String::from("Please wait")), None))
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
.with_texts(Some(String::from("Please wait")), String::new())
.build(),
)),
);
@@ -628,20 +634,21 @@ impl FileTransferActivity {
};
self.view.mount(
super::COMPONENT_RADIO_SORTING,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::LightMagenta)
.with_background(Color::Black)
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightMagenta)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
.with_options(
Some(String::from("Sort files by")),
Some(vec![
TextSpan::from("Name"),
TextSpan::from("Modify time"),
TextSpan::from("Creation time"),
TextSpan::from("Size"),
]),
))
.with_value(PropValue::Unsigned(index))
vec![
String::from("Name"),
String::from("Modify time"),
String::from("Creation time"),
String::from("Size"),
],
)
.with_value(index)
.build(),
)),
);
@@ -655,15 +662,16 @@ impl FileTransferActivity {
pub(super) fn mount_radio_delete(&mut self) {
self.view.mount(
super::COMPONENT_RADIO_DELETE,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::Red)
.with_background(Color::Black)
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::Red)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Plain, Color::Red)
.with_options(
Some(String::from("Delete file")),
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
))
.with_value(PropValue::Unsigned(1))
vec![String::from("Yes"), String::from("No")],
)
.with_value(1)
.build(),
)),
);
@@ -772,11 +780,9 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_LIST_FILEINFO,
Box::new(Table::new(
PropsBuilder::default()
.with_texts(TextParts::table(
Some(file.get_name().to_string()),
texts.build(),
))
TablePropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_table(Some(file.get_name().to_string()), texts.build())
.build(),
)),
);
@@ -794,8 +800,9 @@ impl FileTransferActivity {
self.view.mount(
super::COMPONENT_TEXT_HELP,
Box::new(Table::new(
PropsBuilder::default()
.with_texts(TextParts::table(
TablePropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_table(
Some(String::from("Help")),
TableBuilder::default()
.add_col(
@@ -984,7 +991,7 @@ impl FileTransferActivity {
)
.add_col(TextSpan::from(" Interrupt file transfer"))
.build(),
))
)
.build(),
)),
);

View File

@@ -25,8 +25,8 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use crate::ui::layout::Msg;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tuirealm::event::{KeyCode, KeyEvent, KeyModifiers};
use tuirealm::Msg;
// -- Special keys

View File

@@ -28,10 +28,10 @@
*/
// Locals
use super::SetupActivity;
use crate::ui::layout::Payload;
// Ext
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::env;
use tuirealm::Payload;
impl SetupActivity {
/// ### action_save_config
@@ -63,7 +63,7 @@ impl SetupActivity {
// Get key
if let Some(config_cli) = self.context.as_mut().unwrap().config_client.as_mut() {
// get index
let idx: Option<usize> = match self.view.get_value(super::COMPONENT_LIST_SSH_KEYS) {
let idx: Option<usize> = match self.view.get_state(super::COMPONENT_LIST_SSH_KEYS) {
Some(Payload::Unsigned(idx)) => Some(idx),
_ => None,
};
@@ -99,11 +99,11 @@ impl SetupActivity {
pub(super) fn action_new_ssh_key(&mut self) {
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
// get parameters
let host: String = match self.view.get_value(super::COMPONENT_INPUT_SSH_HOST) {
let host: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_HOST) {
Some(Payload::Text(host)) => host,
_ => String::new(),
};
let username: String = match self.view.get_value(super::COMPONENT_INPUT_SSH_USERNAME) {
let username: String = match self.view.get_state(super::COMPONENT_INPUT_SSH_USERNAME) {
Some(Payload::Text(user)) => user,
_ => String::new(),
};

View File

@@ -34,13 +34,13 @@ mod view;
// Deps
extern crate crossterm;
extern crate tui;
extern crate tuirealm;
// Locals
use super::{Activity, Context, ExitReason};
use crate::ui::layout::view::View;
// Ext
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use tuirealm::View;
// -- components
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";

View File

@@ -35,7 +35,9 @@ use super::{
COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP,
};
use crate::ui::activities::keymap::*;
use crate::ui::layout::{Msg, Payload};
// ext
use tuirealm::{Msg, Payload};
impl SetupActivity {
/// ### update

View File

@@ -30,22 +30,27 @@
use super::{Context, SetupActivity, ViewLayout};
use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs;
use crate::ui::layout::components::{
bookmark_list::BookmarkList, input::Input, msgbox::MsgBox, radio_group::RadioGroup,
table::Table, text::Text,
use crate::ui::components::{
bookmark_list::{BookmarkList, BookmarkListPropsBuilder},
msgbox::{MsgBox, MsgBoxPropsBuilder},
};
use crate::ui::layout::props::{
PropValue, PropsBuilder, TableBuilder, TextParts, TextSpan, TextSpanBuilder,
};
use crate::ui::layout::utils::draw_area_in;
use crate::ui::layout::view::View;
use crate::ui::layout::Payload;
use crate::utils::ui::draw_area_in;
// Ext
use std::path::PathBuf;
use tui::{
use tuirealm::components::{
input::{Input, InputPropsBuilder},
radio::{Radio, RadioPropsBuilder},
span::{Span, SpanPropsBuilder},
table::{Table, TablePropsBuilder},
};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
widgets::{Borders, Clear},
widgets::{BorderType, Borders, Clear},
};
use tuirealm::{
props::{PropsBuilder, TableBuilder, TextSpan, TextSpanBuilder},
Payload, View,
};
impl SetupActivity {
@@ -61,38 +66,32 @@ impl SetupActivity {
// Radio tab
self.view.mount(
super::COMPONENT_RADIO_TAB,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::LightYellow)
.with_background(Color::Black)
.with_borders(Borders::BOTTOM)
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::BOTTOM, BorderType::Thick, Color::White)
.with_options(
None,
Some(vec![
TextSpan::from("User Interface"),
TextSpan::from("SSH Keys"),
]),
))
.with_value(PropValue::Unsigned(0))
vec![String::from("User Interface"), String::from("SSH Keys")],
)
.with_value(0)
.build(),
)),
);
// Footer
self.view.mount(
super::COMPONENT_TEXT_FOOTER,
Box::new(Text::new(
PropsBuilder::default()
.with_texts(TextParts::new(
None,
Some(vec![
TextSpanBuilder::new("Press ").bold().build(),
TextSpanBuilder::new("<CTRL+H>")
.bold()
.with_foreground(Color::Cyan)
.build(),
TextSpanBuilder::new(" to show keybindings").bold().build(),
]),
))
Box::new(Span::new(
SpanPropsBuilder::default()
.with_spans(vec![
TextSpanBuilder::new("Press ").bold().build(),
TextSpanBuilder::new("<CTRL+H>")
.bold()
.with_foreground(Color::Cyan)
.build(),
TextSpanBuilder::new(" to show keybindings").bold().build(),
])
.build(),
)),
);
@@ -100,83 +99,86 @@ impl SetupActivity {
self.view.mount(
super::COMPONENT_INPUT_TEXT_EDITOR,
Box::new(Input::new(
PropsBuilder::default()
InputPropsBuilder::default()
.with_foreground(Color::LightGreen)
.with_texts(TextParts::new(Some(String::from("Text editor")), None))
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
.with_label(String::from("Text editor"))
.build(),
)),
);
self.view.active(super::COMPONENT_INPUT_TEXT_EDITOR); // <-- Focus
self.view.mount(
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::LightCyan)
.with_background(Color::Black)
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightCyan)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
.with_options(
Some(String::from("Default file transfer protocol")),
Some(vec![
TextSpan::from("SFTP"),
TextSpan::from("SCP"),
TextSpan::from("FTP"),
TextSpan::from("FTPS"),
]),
))
vec![
String::from("SFTP"),
String::from("SCP"),
String::from("FTP"),
String::from("FTPS"),
],
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_RADIO_HIDDEN_FILES,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::LightRed)
.with_background(Color::Black)
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightRed)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
.with_options(
Some(String::from("Show hidden files (by default)")),
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
))
vec![String::from("Yes"), String::from("No")],
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_RADIO_UPDATES,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::LightYellow)
.with_background(Color::Black)
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
.with_options(
Some(String::from("Check for updates?")),
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
))
vec![String::from("Yes"), String::from("No")],
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_RADIO_GROUP_DIRS,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::LightMagenta)
.with_background(Color::Black)
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightMagenta)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
.with_options(
Some(String::from("Group directories")),
Some(vec![
TextSpan::from("Display first"),
TextSpan::from("Display Last"),
TextSpan::from("No"),
]),
))
vec![
String::from("Display first"),
String::from("Display Last"),
String::from("No"),
],
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_INPUT_FILE_FMT,
Box::new(Input::new(
PropsBuilder::default()
InputPropsBuilder::default()
.with_foreground(Color::LightBlue)
.with_texts(TextParts::new(
Some(String::from("File formatter syntax")),
None,
))
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue)
.with_label(String::from("File formatter syntax"))
.build(),
)),
);
@@ -196,46 +198,41 @@ impl SetupActivity {
// Radio tab
self.view.mount(
super::COMPONENT_RADIO_TAB,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::LightYellow)
.with_background(Color::Black)
.with_borders(Borders::BOTTOM)
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::BOTTOM, BorderType::Thick, Color::LightYellow)
.with_options(
None,
Some(vec![
TextSpan::from("User Interface"),
TextSpan::from("SSH Keys"),
]),
))
.with_value(PropValue::Unsigned(1))
vec![String::from("User Interface"), String::from("SSH Keys")],
)
.with_value(1)
.build(),
)),
);
// Footer
self.view.mount(
super::COMPONENT_TEXT_FOOTER,
Box::new(Text::new(
PropsBuilder::default()
.with_texts(TextParts::new(
None,
Some(vec![
TextSpanBuilder::new("Press ").bold().build(),
TextSpanBuilder::new("<CTRL+H>")
.bold()
.with_foreground(Color::Cyan)
.build(),
TextSpanBuilder::new(" to show keybindings").bold().build(),
]),
))
Box::new(Span::new(
SpanPropsBuilder::default()
.with_spans(vec![
TextSpanBuilder::new("Press ").bold().build(),
TextSpanBuilder::new("<CTRL+H>")
.bold()
.with_foreground(Color::Cyan)
.build(),
TextSpanBuilder::new(" to show keybindings").bold().build(),
])
.build(),
)),
);
self.view.mount(
super::COMPONENT_LIST_SSH_KEYS,
Box::new(BookmarkList::new(
PropsBuilder::default()
.with_texts(TextParts::new(Some(String::from("SSH Keys")), Some(vec![])))
BookmarkListPropsBuilder::default()
.with_bookmarks(Some(String::from("SSH Keys")), vec![])
.with_borders(Borders::ALL, BorderType::Plain, Color::LightGreen)
.with_background(Color::LightGreen)
.with_foreground(Color::Black)
.build(),
@@ -312,40 +309,40 @@ impl SetupActivity {
}
}
// Popups
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 40, 10);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_RADIO_QUIT, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 50, 70);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_TEXT_HELP, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_SAVE) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
self.view.render(super::COMPONENT_RADIO_SAVE, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_RADIO_DEL_SSH_KEY) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEL_SSH_KEY) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 30, 10);
f.render_widget(Clear, popup);
@@ -353,8 +350,8 @@ impl SetupActivity {
.render(super::COMPONENT_RADIO_DEL_SSH_KEY, f, popup);
}
}
if let Some(mut props) = self.view.get_props(super::COMPONENT_INPUT_SSH_HOST) {
if props.build().visible {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_SSH_HOST) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 50, 20);
f.render_widget(Clear, popup);
@@ -389,10 +386,11 @@ impl SetupActivity {
self.view.mount(
super::COMPONENT_TEXT_ERROR,
Box::new(MsgBox::new(
PropsBuilder::default()
MsgBoxPropsBuilder::default()
.with_foreground(Color::Red)
.bold()
.with_texts(TextParts::new(None, Some(vec![TextSpan::from(text)])))
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
.with_texts(None, vec![TextSpan::from(text)])
.build(),
)),
);
@@ -413,15 +411,16 @@ impl SetupActivity {
pub(super) fn mount_del_ssh_key(&mut self) {
self.view.mount(
super::COMPONENT_RADIO_DEL_SSH_KEY,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::LightRed)
.bold()
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightRed)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
.with_options(
Some(String::from("Delete key?")),
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
))
.with_value(PropValue::Unsigned(1)) // Default: No
vec![String::from("Yes"), String::from("No")],
)
.with_value(1) // Default: No
.build(),
)),
);
@@ -443,21 +442,26 @@ impl SetupActivity {
self.view.mount(
super::COMPONENT_INPUT_SSH_HOST,
Box::new(Input::new(
PropsBuilder::default()
.with_texts(TextParts::new(
Some(String::from("Hostname or address")),
None,
))
.with_borders(Borders::TOP | Borders::RIGHT | Borders::LEFT)
InputPropsBuilder::default()
.with_label(String::from("Hostname or address"))
.with_borders(
Borders::TOP | Borders::RIGHT | Borders::LEFT,
BorderType::Plain,
Color::Reset,
)
.build(),
)),
);
self.view.mount(
super::COMPONENT_INPUT_SSH_USERNAME,
Box::new(Input::new(
PropsBuilder::default()
.with_texts(TextParts::new(Some(String::from("Username")), None))
.with_borders(Borders::BOTTOM | Borders::RIGHT | Borders::LEFT)
InputPropsBuilder::default()
.with_label(String::from("Username"))
.with_borders(
Borders::ALL | Borders::RIGHT | Borders::LEFT,
BorderType::Plain,
Color::Reset,
)
.build(),
)),
);
@@ -478,18 +482,19 @@ impl SetupActivity {
pub(super) fn mount_quit(&mut self) {
self.view.mount(
super::COMPONENT_RADIO_QUIT,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::LightRed)
.bold()
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightRed)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
.with_options(
Some(String::from("Exit setup?")),
Some(vec![
TextSpan::from("Save"),
TextSpan::from("Don't save"),
TextSpan::from("Cancel"),
]),
))
vec![
String::from("Save"),
String::from("Don't save"),
String::from("Cancel"),
],
)
.build(),
)),
);
@@ -510,14 +515,15 @@ impl SetupActivity {
pub(super) fn mount_save_popup(&mut self) {
self.view.mount(
super::COMPONENT_RADIO_SAVE,
Box::new(RadioGroup::new(
PropsBuilder::default()
.with_foreground(Color::LightYellow)
.bold()
.with_texts(TextParts::new(
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
.with_options(
Some(String::from("Save changes?")),
Some(vec![TextSpan::from("Yes"), TextSpan::from("No")]),
))
vec![String::from("Yes"), String::from("No")],
)
.build(),
)),
);
@@ -539,8 +545,9 @@ impl SetupActivity {
self.view.mount(
super::COMPONENT_TEXT_HELP,
Box::new(Table::new(
PropsBuilder::default()
.with_texts(TextParts::table(
TablePropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_table(
Some(String::from("Help")),
TableBuilder::default()
.add_col(
@@ -615,7 +622,7 @@ impl SetupActivity {
)
.add_col(TextSpan::from(" Save configuration"))
.build(),
))
)
.build(),
)),
);
@@ -636,77 +643,59 @@ impl SetupActivity {
pub(super) fn load_input_values(&mut self) {
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
// Text editor
if let Some(props) = self
.view
.get_props(super::COMPONENT_INPUT_TEXT_EDITOR)
.as_mut()
{
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_TEXT_EDITOR) {
let text_editor: String =
String::from(cli.get_text_editor().as_path().to_string_lossy());
let props = props.with_value(PropValue::Str(text_editor)).build();
let props = InputPropsBuilder::from(props)
.with_value(text_editor)
.build();
let _ = self.view.update(super::COMPONENT_INPUT_TEXT_EDITOR, props);
}
// Protocol
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
.as_mut()
{
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DEFAULT_PROTOCOL) {
let protocol: usize = match cli.get_default_protocol() {
FileTransferProtocol::Sftp => 0,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
};
let props = props.with_value(PropValue::Unsigned(protocol)).build();
let props = RadioPropsBuilder::from(props).with_value(protocol).build();
let _ = self
.view
.update(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, props);
}
// Hidden files
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_HIDDEN_FILES)
.as_mut()
{
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_HIDDEN_FILES) {
let hidden: usize = match cli.get_show_hidden_files() {
true => 0,
false => 1,
};
let props = props.with_value(PropValue::Unsigned(hidden)).build();
let props = RadioPropsBuilder::from(props).with_value(hidden).build();
let _ = self.view.update(super::COMPONENT_RADIO_HIDDEN_FILES, props);
}
// Updates
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_UPDATES).as_mut() {
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_UPDATES) {
let updates: usize = match cli.get_check_for_updates() {
true => 0,
false => 1,
};
let props = props.with_value(PropValue::Unsigned(updates)).build();
let props = RadioPropsBuilder::from(props).with_value(updates).build();
let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props);
}
// Group dirs
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_GROUP_DIRS)
.as_mut()
{
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_GROUP_DIRS) {
let dirs: usize = match cli.get_group_dirs() {
Some(GroupDirs::First) => 0,
Some(GroupDirs::Last) => 1,
None => 2,
};
let props = props.with_value(PropValue::Unsigned(dirs)).build();
let props = RadioPropsBuilder::from(props).with_value(dirs).build();
let _ = self.view.update(super::COMPONENT_RADIO_GROUP_DIRS, props);
}
// File Fmt
if let Some(props) = self
.view
.get_props(super::COMPONENT_INPUT_FILE_FMT)
.as_mut()
{
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_FILE_FMT) {
let file_fmt: String = cli.get_file_fmt().unwrap_or_default();
let props = props.with_value(PropValue::Str(file_fmt)).build();
let props = InputPropsBuilder::from(props).with_value(file_fmt).build();
let _ = self.view.update(super::COMPONENT_INPUT_FILE_FMT, props);
}
}
@@ -718,12 +707,12 @@ impl SetupActivity {
pub(super) fn collect_input_values(&mut self) {
if let Some(cli) = self.context.as_mut().unwrap().config_client.as_mut() {
if let Some(Payload::Text(editor)) =
self.view.get_value(super::COMPONENT_INPUT_TEXT_EDITOR)
self.view.get_state(super::COMPONENT_INPUT_TEXT_EDITOR)
{
cli.set_text_editor(PathBuf::from(editor.as_str()));
}
if let Some(Payload::Unsigned(protocol)) =
self.view.get_value(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
self.view.get_state(super::COMPONENT_RADIO_DEFAULT_PROTOCOL)
{
let protocol: FileTransferProtocol = match protocol {
1 => FileTransferProtocol::Scp,
@@ -734,22 +723,22 @@ impl SetupActivity {
cli.set_default_protocol(protocol);
}
if let Some(Payload::Unsigned(opt)) =
self.view.get_value(super::COMPONENT_RADIO_HIDDEN_FILES)
self.view.get_state(super::COMPONENT_RADIO_HIDDEN_FILES)
{
let show: bool = matches!(opt, 0);
cli.set_show_hidden_files(show);
}
if let Some(Payload::Unsigned(opt)) =
self.view.get_value(super::COMPONENT_RADIO_UPDATES)
self.view.get_state(super::COMPONENT_RADIO_UPDATES)
{
let check: bool = matches!(opt, 0);
cli.set_check_for_updates(check);
}
if let Some(Payload::Text(fmt)) = self.view.get_value(super::COMPONENT_INPUT_FILE_FMT) {
if let Some(Payload::Text(fmt)) = self.view.get_state(super::COMPONENT_INPUT_FILE_FMT) {
cli.set_file_fmt(fmt);
}
if let Some(Payload::Unsigned(opt)) =
self.view.get_value(super::COMPONENT_RADIO_GROUP_DIRS)
self.view.get_state(super::COMPONENT_RADIO_GROUP_DIRS)
{
let dirs: Option<GroupDirs> = match opt {
0 => Some(GroupDirs::First),
@@ -767,17 +756,17 @@ impl SetupActivity {
pub(super) fn reload_ssh_keys(&mut self) {
if let Some(cli) = self.context.as_ref().unwrap().config_client.as_ref() {
// get props
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_SSH_KEYS).as_mut() {
if let Some(props) = self.view.get_props(super::COMPONENT_LIST_SSH_KEYS) {
// Create texts
let keys: Vec<TextSpan> = cli
let keys: Vec<String> = cli
.iter_ssh_keys()
.map(|x| {
let (addr, username, _) = cli.get_ssh_key(x).ok().unwrap().unwrap();
TextSpan::from(format!("{} at {}", addr, username).as_str())
format!("{} at {}", addr, username)
})
.collect();
let props = props
.with_texts(TextParts::new(Some(String::from("SSH Keys")), Some(keys)))
let props = BookmarkListPropsBuilder::from(props)
.with_bookmarks(Some(String::from("SSH Keys")), keys)
.build();
self.view.update(super::COMPONENT_LIST_SSH_KEYS, props);
}

View File

@@ -25,16 +25,106 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
// ext
use crossterm::event::KeyCode;
use tui::{
use tuirealm::components::utils::get_block;
use tuirealm::event::{Event, KeyCode};
use tuirealm::props::{BordersProps, Props, PropsBuilder, TextParts, TextSpan};
use tuirealm::tui::{
layout::{Corner, Rect},
style::{Color, Style},
text::Span,
widgets::{Block, List, ListItem, ListState},
widgets::{BorderType, Borders, List, ListItem, ListState},
};
use tuirealm::{Canvas, Component, Msg, Payload};
// -- props
pub struct BookmarkListPropsBuilder {
props: Option<Props>,
}
impl Default for BookmarkListPropsBuilder {
fn default() -> Self {
BookmarkListPropsBuilder {
props: Some(Props::default()),
}
}
}
impl PropsBuilder for BookmarkListPropsBuilder {
fn build(&mut self) -> Props {
self.props.take().unwrap()
}
fn hidden(&mut self) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.visible = false;
}
self
}
fn visible(&mut self) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.visible = true;
}
self
}
}
impl From<Props> for BookmarkListPropsBuilder {
fn from(props: Props) -> Self {
BookmarkListPropsBuilder { props: Some(props) }
}
}
impl BookmarkListPropsBuilder {
/// ### with_foreground
///
/// Set foreground color for area
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.foreground = color;
}
self
}
/// ### with_background
///
/// Set background color for area
pub fn with_background(&mut self, color: Color) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.background = color;
}
self
}
/// ### with_borders
///
/// Set component borders style
pub fn with_borders(
&mut self,
borders: Borders,
variant: BorderType,
color: Color,
) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.borders = BordersProps {
borders,
variant,
color,
}
}
self
}
pub fn with_bookmarks(&mut self, title: Option<String>, bookmarks: Vec<String>) -> &mut Self {
if let Some(props) = self.props.as_mut() {
let bookmarks: Vec<TextSpan> = bookmarks.into_iter().map(TextSpan::from).collect();
props.texts = TextParts::new(title, Some(bookmarks));
}
self
}
}
// -- states
@@ -120,7 +210,7 @@ impl BookmarkList {
// Initialize states
let mut states: OwnStates = OwnStates::default();
// Set list length
states.set_list_len(match &props.texts.rows {
states.set_list_len(match &props.texts.spans {
Some(tokens) => tokens.len(),
None => 0,
});
@@ -129,15 +219,11 @@ impl BookmarkList {
}
impl Component for BookmarkList {
/// ### render
///
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
/// If focused, cursor is also set (if supported by widget)
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Canvas, area: Rect) {
if self.props.visible {
// Make list
let list_item: Vec<ListItem> = match self.props.texts.rows.as_ref() {
let list_item: Vec<ListItem> = match self.props.texts.spans.as_ref() {
None => vec![],
Some(lines) => lines
.iter()
@@ -148,30 +234,22 @@ impl Component for BookmarkList {
true => (self.props.foreground, self.props.background),
false => (Color::Reset, Color::Reset),
};
let title: String = match self.props.texts.title.as_ref() {
Some(t) => t.clone(),
None => String::new(),
};
// Render
let mut state: ListState = ListState::default();
state.select(Some(self.states.list_index));
render.render_stateful_widget(
List::new(list_item)
.block(
Block::default()
.borders(self.props.borders)
.border_style(match self.states.focus {
true => Style::default().fg(self.props.background),
false => Style::default(),
})
.title(title),
)
.block(get_block(
&self.props.borders,
&self.props.texts.title,
self.states.focus,
))
.start_corner(Corner::TopLeft)
.highlight_style(
Style::default()
.bg(bg)
.fg(fg)
.add_modifier(self.props.get_modifiers()),
.add_modifier(self.props.modifiers),
),
area,
&mut state,
@@ -179,15 +257,10 @@ impl Component for BookmarkList {
}
}
/// ### 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.rows {
self.states.set_list_len(match &self.props.texts.spans {
Some(tokens) => tokens.len(),
None => 0,
});
@@ -196,21 +269,13 @@ impl Component for BookmarkList {
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())
fn get_props(&self) -> Props {
self.props.clone()
}
/// ### on
///
/// Handle input event and update internal states
fn on(&mut self, ev: InputEvent) -> Msg {
fn on(&mut self, ev: Event) -> Msg {
// Match event
if let InputEvent::Key(key) = ev {
if let Event::Key(key) = ev {
match key.code {
KeyCode::Down => {
// Update states
@@ -238,7 +303,7 @@ impl Component for BookmarkList {
}
KeyCode::Enter => {
// Report event
Msg::OnSubmit(self.get_value())
Msg::OnSubmit(self.get_state())
}
_ => {
// Return key event to activity
@@ -251,25 +316,14 @@ impl Component for BookmarkList {
}
}
/// ### get_value
///
/// Return component value. File list return index
fn get_value(&self) -> Payload {
fn get_state(&self) -> Payload {
Payload::Unsigned(self.states.get_list_index())
}
// -- events
/// ### blur
///
/// Blur component; basically remove focus
fn blur(&mut self) {
self.states.focus = false;
}
/// ### active
///
/// Active component; basically give focus
fn active(&mut self) {
self.states.focus = true;
}
@@ -279,21 +333,32 @@ impl Component for BookmarkList {
mod tests {
use super::*;
use crate::ui::layout::props::{TextParts, TextSpan};
use crossterm::event::KeyEvent;
use tuirealm::event::KeyEvent;
#[test]
fn test_ui_layout_components_bookmarks_list() {
fn test_ui_components_bookmarks_list() {
// Make component
let mut component: BookmarkList = BookmarkList::new(
PropsBuilder::default()
.with_texts(TextParts::new(
BookmarkListPropsBuilder::default()
.hidden()
.visible()
.with_foreground(Color::Red)
.with_background(Color::Blue)
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
.with_bookmarks(
Some(String::from("filelist")),
Some(vec![TextSpan::from("file1"), TextSpan::from("file2")]),
))
vec![String::from("file1"), String::from("file2")],
)
.build(),
);
assert_eq!(component.props.foreground, Color::Red);
assert_eq!(component.props.background, Color::Blue);
assert_eq!(component.props.visible, true);
assert_eq!(
component.props.texts.title.as_ref().unwrap().as_str(),
"filelist"
);
assert_eq!(component.props.texts.spans.as_ref().unwrap().len(), 2);
// Verify states
assert_eq!(component.states.list_index, 0);
assert_eq!(component.states.list_len, 2);
@@ -304,69 +369,72 @@ mod tests {
component.blur();
assert_eq!(component.states.focus, false);
// Update
let props = component.get_props().with_foreground(Color::Red).build();
let props = BookmarkListPropsBuilder::from(component.get_props())
.with_foreground(Color::Yellow)
.hidden()
.build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
assert_eq!(component.props.foreground, Color::Yellow);
assert_eq!(component.props.visible, false);
// Increment list index
component.states.list_index += 1;
assert_eq!(component.states.list_index, 1);
// Update
component.update(
component
.get_props()
.with_texts(TextParts::new(
BookmarkListPropsBuilder::from(component.get_props())
.with_bookmarks(
Some(String::from("filelist")),
Some(vec![
TextSpan::from("file1"),
TextSpan::from("file2"),
TextSpan::from("file3"),
]),
))
vec![
String::from("file1"),
String::from("file2"),
String::from("file3"),
],
)
.build(),
);
// Verify states
assert_eq!(component.states.list_index, 0);
assert_eq!(component.states.list_len, 3);
// get value
assert_eq!(component.get_value(), Payload::Unsigned(0));
assert_eq!(component.get_state(), Payload::Unsigned(0));
// Render
assert_eq!(component.states.list_index, 0);
// Handle inputs
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Down))),
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
Msg::None
);
// Index should be incremented
assert_eq!(component.states.list_index, 1);
// Index should be decremented
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Up))),
component.on(Event::Key(KeyEvent::from(KeyCode::Up))),
Msg::None
);
// Index should be incremented
assert_eq!(component.states.list_index, 0);
// Index should be 2
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageDown))),
component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))),
Msg::None
);
// Index should be incremented
assert_eq!(component.states.list_index, 2);
// Index should be 0
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageUp))),
component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))),
Msg::None
);
// Index should be incremented
assert_eq!(component.states.list_index, 0);
// Enter
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter))),
component.on(Event::Key(KeyEvent::from(KeyCode::Enter))),
Msg::OnSubmit(Payload::Unsigned(0))
);
// On key
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
);
}

View File

@@ -25,16 +25,106 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
// ext
use crossterm::event::KeyCode;
use tui::{
use tuirealm::components::utils::get_block;
use tuirealm::event::{Event, KeyCode};
use tuirealm::props::{BordersProps, Props, PropsBuilder, TextParts, TextSpan};
use tuirealm::tui::{
layout::{Corner, Rect},
style::{Color, Style},
text::Span,
widgets::{Block, List, ListItem, ListState},
widgets::{BorderType, Borders, List, ListItem, ListState},
};
use tuirealm::{Canvas, Component, Msg, Payload};
// -- props
pub struct FileListPropsBuilder {
props: Option<Props>,
}
impl Default for FileListPropsBuilder {
fn default() -> Self {
FileListPropsBuilder {
props: Some(Props::default()),
}
}
}
impl PropsBuilder for FileListPropsBuilder {
fn build(&mut self) -> Props {
self.props.take().unwrap()
}
fn hidden(&mut self) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.visible = false;
}
self
}
fn visible(&mut self) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.visible = true;
}
self
}
}
impl From<Props> for FileListPropsBuilder {
fn from(props: Props) -> Self {
FileListPropsBuilder { props: Some(props) }
}
}
impl FileListPropsBuilder {
/// ### with_foreground
///
/// Set foreground color for area
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.foreground = color;
}
self
}
/// ### with_background
///
/// Set background color for area
pub fn with_background(&mut self, color: Color) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.background = color;
}
self
}
/// ### with_borders
///
/// Set component borders style
pub fn with_borders(
&mut self,
borders: Borders,
variant: BorderType,
color: Color,
) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.borders = BordersProps {
borders,
variant,
color,
}
}
self
}
pub fn with_files(&mut self, title: Option<String>, files: Vec<String>) -> &mut Self {
if let Some(props) = self.props.as_mut() {
let files: Vec<TextSpan> = files.into_iter().map(TextSpan::from).collect();
props.texts = TextParts::new(title, Some(files));
}
self
}
}
// -- states
@@ -124,7 +214,7 @@ impl FileList {
// Initialize states
let mut states: OwnStates = OwnStates::default();
// Set list length
states.set_list_len(match &props.texts.rows {
states.set_list_len(match &props.texts.spans {
Some(tokens) => tokens.len(),
None => 0,
});
@@ -133,15 +223,11 @@ impl FileList {
}
impl Component for FileList {
/// ### render
///
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
/// If focused, cursor is also set (if supported by widget)
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Canvas, area: Rect) {
if self.props.visible {
// Make list
let list_item: Vec<ListItem> = match self.props.texts.rows.as_ref() {
let list_item: Vec<ListItem> = match self.props.texts.spans.as_ref() {
None => vec![],
Some(lines) => lines
.iter()
@@ -152,30 +238,22 @@ impl Component for FileList {
true => (Color::Black, 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
let mut state: ListState = ListState::default();
state.select(Some(self.states.list_index));
render.render_stateful_widget(
List::new(list_item)
.block(
Block::default()
.borders(self.props.borders)
.border_style(match self.states.focus {
true => Style::default().fg(self.props.foreground),
false => Style::default(),
})
.title(title),
)
.block(get_block(
&self.props.borders,
&self.props.texts.title,
self.states.focus,
))
.start_corner(Corner::TopLeft)
.highlight_style(
Style::default()
.bg(bg)
.fg(fg)
.add_modifier(self.props.get_modifiers()),
.add_modifier(self.props.modifiers),
),
area,
&mut state,
@@ -183,15 +261,10 @@ impl Component for FileList {
}
}
/// ### 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.rows {
self.states.set_list_len(match &self.props.texts.spans {
Some(tokens) => tokens.len(),
None => 0,
});
@@ -200,21 +273,13 @@ impl Component for FileList {
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())
fn get_props(&self) -> Props {
self.props.clone()
}
/// ### on
///
/// Handle input event and update internal states
fn on(&mut self, ev: InputEvent) -> Msg {
fn on(&mut self, ev: Event) -> Msg {
// Match event
if let InputEvent::Key(key) = ev {
if let Event::Key(key) = ev {
match key.code {
KeyCode::Down => {
// Update states
@@ -242,7 +307,7 @@ impl Component for FileList {
}
KeyCode::Enter => {
// Report event
Msg::OnSubmit(self.get_value())
Msg::OnSubmit(self.get_state())
}
_ => {
// Return key event to activity
@@ -255,10 +320,7 @@ impl Component for FileList {
}
}
/// ### get_value
///
/// Return component value. File list return index
fn get_value(&self) -> Payload {
fn get_state(&self) -> Payload {
Payload::Unsigned(self.states.get_list_index())
}
@@ -283,21 +345,32 @@ impl Component for FileList {
mod tests {
use super::*;
use crate::ui::layout::props::{TextParts, TextSpan};
use crossterm::event::KeyEvent;
use tuirealm::event::KeyEvent;
#[test]
fn test_ui_layout_components_file_list() {
fn test_ui_components_file_list() {
// Make component
let mut component: FileList = FileList::new(
PropsBuilder::default()
.with_texts(TextParts::new(
Some(String::from("filelist")),
Some(vec![TextSpan::from("file1"), TextSpan::from("file2")]),
))
FileListPropsBuilder::default()
.hidden()
.visible()
.with_foreground(Color::Red)
.with_background(Color::Blue)
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
.with_files(
Some(String::from("files")),
vec![String::from("file1"), String::from("file2")],
)
.build(),
);
assert_eq!(component.props.foreground, Color::Red);
assert_eq!(component.props.background, Color::Blue);
assert_eq!(component.props.visible, true);
assert_eq!(
component.props.texts.title.as_ref().unwrap().as_str(),
"files"
);
assert_eq!(component.props.texts.spans.as_ref().unwrap().len(), 2);
// Verify states
assert_eq!(component.states.list_index, 0);
assert_eq!(component.states.list_len, 2);
@@ -308,69 +381,72 @@ mod tests {
component.blur();
assert_eq!(component.states.focus, false);
// Update
let props = component.get_props().with_foreground(Color::Red).build();
let props = FileListPropsBuilder::from(component.get_props())
.with_foreground(Color::Yellow)
.hidden()
.build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
assert_eq!(component.props.visible, false);
assert_eq!(component.props.foreground, Color::Yellow);
// Increment list index
component.states.list_index += 1;
assert_eq!(component.states.list_index, 1);
// Update
component.update(
component
.get_props()
.with_texts(TextParts::new(
FileListPropsBuilder::from(component.get_props())
.with_files(
Some(String::from("filelist")),
Some(vec![
TextSpan::from("file1"),
TextSpan::from("file2"),
TextSpan::from("file3"),
]),
))
vec![
String::from("file1"),
String::from("file2"),
String::from("file3"),
],
)
.build(),
);
// Verify states
assert_eq!(component.states.list_index, 1); // Kept
assert_eq!(component.states.list_len, 3);
// get value
assert_eq!(component.get_value(), Payload::Unsigned(1));
assert_eq!(component.get_state(), Payload::Unsigned(1));
// Render
assert_eq!(component.states.list_index, 1);
// Handle inputs
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Down))),
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
Msg::None
);
// Index should be incremented
assert_eq!(component.states.list_index, 2);
// Index should be decremented
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Up))),
component.on(Event::Key(KeyEvent::from(KeyCode::Up))),
Msg::None
);
// Index should be incremented
assert_eq!(component.states.list_index, 1);
// Index should be 2
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageDown))),
component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))),
Msg::None
);
// Index should be incremented
assert_eq!(component.states.list_index, 2);
// Index should be 0
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageUp))),
component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))),
Msg::None
);
// Index should be incremented
assert_eq!(component.states.list_index, 0);
// Enter
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter))),
component.on(Event::Key(KeyEvent::from(KeyCode::Enter))),
Msg::OnSubmit(Payload::Unsigned(0))
);
// On key
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
);
}

View File

@@ -25,17 +25,84 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
// ext
use crossterm::event::KeyCode;
use std::collections::VecDeque;
use tui::{
use tuirealm::components::utils::{get_block, wrap_spans};
use tuirealm::event::{Event, KeyCode};
use tuirealm::props::{BordersProps, Props, PropsBuilder, Table as TextTable, TextParts};
use tuirealm::tui::{
layout::{Corner, Rect},
style::Style,
text::{Span, Spans},
widgets::{Block, List, ListItem, ListState},
style::{Color, Style},
widgets::{BorderType, Borders, List, ListItem, ListState},
};
use tuirealm::{Canvas, Component, Msg, Payload};
// -- props
pub struct LogboxPropsBuilder {
props: Option<Props>,
}
impl Default for LogboxPropsBuilder {
fn default() -> Self {
LogboxPropsBuilder {
props: Some(Props::default()),
}
}
}
impl PropsBuilder for LogboxPropsBuilder {
fn build(&mut self) -> Props {
self.props.take().unwrap()
}
fn hidden(&mut self) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.visible = false;
}
self
}
fn visible(&mut self) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.visible = true;
}
self
}
}
impl From<Props> for LogboxPropsBuilder {
fn from(props: Props) -> Self {
LogboxPropsBuilder { props: Some(props) }
}
}
impl LogboxPropsBuilder {
/// ### with_borders
///
/// Set component borders style
pub fn with_borders(
&mut self,
borders: Borders,
variant: BorderType,
color: Color,
) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.borders = BordersProps {
borders,
variant,
color,
}
}
self
}
pub fn with_log(&mut self, title: Option<String>, table: TextTable) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.texts = TextParts::table(title, table);
}
self
}
}
// -- states
@@ -132,10 +199,6 @@ impl LogBox {
}
impl Component for LogBox {
/// ### render
///
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
/// If focused, cursor is also set (if supported by widget)
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Canvas, area: Rect) {
if self.props.visible {
@@ -144,70 +207,24 @@ impl Component for LogBox {
None => Vec::new(),
Some(table) => table
.iter()
.map(|row| {
let mut columns: VecDeque<Span> = row
.iter()
.map(|col| {
Span::styled(
col.content.clone(),
Style::default()
.add_modifier(col.get_modifiers())
.fg(col.fg)
.bg(col.bg),
)
})
.collect();
// Let's convert column spans into Spans rows NOTE: -4 because first line is always made by 5 columns; but there's always 1
let mut rows: Vec<Spans> = Vec::with_capacity(columns.len() - 4);
// Get first row
let mut first_row: Vec<Span> = Vec::with_capacity(5);
for _ in 0..5 {
if let Some(col) = columns.pop_front() {
first_row.push(col);
}
}
rows.push(Spans::from(first_row));
// Fill remaining rows
let cycles: usize = columns.len();
for _ in 0..cycles {
if let Some(col) = columns.pop_front() {
rows.push(Spans::from(vec![col]));
}
}
ListItem::new(rows)
})
.map(|row| ListItem::new(wrap_spans(row, area.width.into(), &self.props)))
.collect(), // Make List item from TextSpan
};
let title: String = match self.props.texts.title.as_ref() {
Some(t) => t.clone(),
None => String::new(),
};
// Render
let w = List::new(list_items)
.block(
Block::default()
.borders(self.props.borders)
.border_style(match self.states.focus {
true => Style::default().fg(self.props.foreground),
false => Style::default(),
})
.title(title),
)
.block(get_block(
&self.props.borders,
&self.props.texts.title,
self.states.focus,
))
.start_corner(Corner::BottomLeft)
.highlight_symbol(">> ")
.highlight_style(Style::default().add_modifier(self.props.get_modifiers()));
.highlight_style(Style::default().add_modifier(self.props.modifiers));
let mut state: ListState = ListState::default();
state.select(Some(self.states.list_index));
render.render_stateful_widget(w, area, &mut state);
}
}
/// ### 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
@@ -220,21 +237,13 @@ impl Component for LogBox {
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())
fn get_props(&self) -> Props {
self.props.clone()
}
/// ### on
///
/// Handle input event and update internal states
fn on(&mut self, ev: InputEvent) -> Msg {
fn on(&mut self, ev: Event) -> Msg {
// Match event
if let InputEvent::Key(key) = ev {
if let Event::Key(key) = ev {
match key.code {
KeyCode::Up => {
// Update states
@@ -271,25 +280,14 @@ impl Component for LogBox {
}
}
/// ### get_value
///
/// Return component value. File list return index
fn get_value(&self) -> Payload {
fn get_state(&self) -> Payload {
Payload::Unsigned(self.states.get_list_index())
}
// -- events
/// ### blur
///
/// Blur component; basically remove focus
fn blur(&mut self) {
self.states.focus = false;
}
/// ### active
///
/// Active component; basically give focus
fn active(&mut self) {
self.states.focus = true;
}
@@ -299,16 +297,18 @@ impl Component for LogBox {
mod tests {
use super::*;
use crate::ui::layout::props::{TableBuilder, TextParts, TextSpan};
use crossterm::event::{KeyCode, KeyEvent};
use tui::style::Color;
use tuirealm::event::{KeyCode, KeyEvent};
use tuirealm::props::{TableBuilder, TextSpan};
use tuirealm::tui::style::Color;
#[test]
fn test_ui_layout_components_logbox() {
fn test_ui_components_logbox() {
let mut component: LogBox = LogBox::new(
PropsBuilder::default()
.with_texts(TextParts::table(
LogboxPropsBuilder::default()
.hidden()
.visible()
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
.with_log(
Some(String::from("Log")),
TableBuilder::default()
.add_col(TextSpan::from("12:29"))
@@ -317,9 +317,15 @@ mod tests {
.add_col(TextSpan::from("12:38"))
.add_col(TextSpan::from("system alive"))
.build(),
))
)
.build(),
);
assert_eq!(component.props.visible, true);
assert_eq!(
component.props.texts.title.as_ref().unwrap().as_str(),
"Log"
);
assert_eq!(component.props.texts.table.as_ref().unwrap().len(), 2);
// Verify states
assert_eq!(component.states.list_index, 0);
assert_eq!(component.states.list_len, 2);
@@ -330,17 +336,18 @@ mod tests {
component.blur();
assert_eq!(component.states.focus, false);
// Update
let props = component.get_props().with_foreground(Color::Red).build();
let props = LogboxPropsBuilder::from(component.get_props())
.hidden()
.build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
assert_eq!(component.props.visible, false);
// Increment list index
component.states.list_index += 1;
assert_eq!(component.states.list_index, 1);
// Update
component.update(
component
.get_props()
.with_texts(TextParts::table(
LogboxPropsBuilder::from(component.get_props())
.with_log(
Some(String::from("Log")),
TableBuilder::default()
.add_col(TextSpan::from("12:29"))
@@ -352,49 +359,49 @@ mod tests {
.add_col(TextSpan::from("12:41"))
.add_col(TextSpan::from("system is going down for REBOOT"))
.build(),
))
)
.build(),
);
// Verify states
assert_eq!(component.states.list_index, 0); // Last item
assert_eq!(component.states.list_len, 3);
// get value
assert_eq!(component.get_value(), Payload::Unsigned(0));
assert_eq!(component.get_state(), Payload::Unsigned(0));
// RenderData
assert_eq!(component.states.list_index, 0);
// Set cursor to 0
component.states.list_index = 0;
// Handle inputs
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Up))),
component.on(Event::Key(KeyEvent::from(KeyCode::Up))),
Msg::None
);
// Index should be incremented
assert_eq!(component.states.list_index, 1);
// Index should be decremented
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Down))),
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
Msg::None
);
// Index should be incremented
assert_eq!(component.states.list_index, 0);
// Index should be 2
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageUp))),
component.on(Event::Key(KeyEvent::from(KeyCode::PageUp))),
Msg::None
);
// Index should be incremented
assert_eq!(component.states.list_index, 2);
// Index should be 0
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::PageDown))),
component.on(Event::Key(KeyEvent::from(KeyCode::PageDown))),
Msg::None
);
// Index should be incremented
assert_eq!(component.states.list_index, 0);
// On key
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
);
}

View File

@@ -25,17 +25,8 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// imports
use super::{Canvas, Component, InputEvent, Msg, Payload, PropValue, Props, PropsBuilder};
// exports
pub mod bookmark_list;
pub mod file_list;
pub mod input;
pub mod logbox;
pub mod msgbox;
pub mod progress_bar;
pub mod radio_group;
pub mod table;
pub mod text;
pub mod title;

View File

@@ -27,16 +27,99 @@
*/
// deps
extern crate textwrap;
extern crate tuirealm;
// locals
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
use crate::utils::fmt::align_text_center;
// ext
use tui::{
use tuirealm::components::utils::{get_block, use_or_default_styles};
use tuirealm::event::Event;
use tuirealm::props::{BordersProps, Props, PropsBuilder, TextParts, TextSpan};
use tuirealm::tui::{
layout::{Corner, Rect},
style::{Color, Style},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, BorderType, List, ListItem},
widgets::{BorderType, Borders, List, ListItem},
};
use tuirealm::{Canvas, Component, Msg, Payload};
// -- Props
pub struct MsgBoxPropsBuilder {
props: Option<Props>,
}
impl Default for MsgBoxPropsBuilder {
fn default() -> Self {
MsgBoxPropsBuilder {
props: Some(Props::default()),
}
}
}
impl PropsBuilder for MsgBoxPropsBuilder {
fn build(&mut self) -> Props {
self.props.take().unwrap()
}
fn hidden(&mut self) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.visible = false;
}
self
}
fn visible(&mut self) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.visible = true;
}
self
}
}
impl From<Props> for MsgBoxPropsBuilder {
fn from(props: Props) -> Self {
MsgBoxPropsBuilder { props: Some(props) }
}
}
impl MsgBoxPropsBuilder {
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.foreground = color;
}
self
}
pub fn bold(&mut self) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.modifiers |= Modifier::BOLD;
}
self
}
pub fn with_borders(
&mut self,
borders: Borders,
variant: BorderType,
color: Color,
) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.borders = BordersProps {
borders,
variant,
color,
}
}
self
}
pub fn with_texts(&mut self, title: Option<String>, texts: Vec<TextSpan>) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.texts = TextParts::new(title, Some(texts));
}
self
}
}
// -- component
@@ -54,56 +137,36 @@ impl MsgBox {
}
impl Component for MsgBox {
/// ### render
///
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
/// If focused, cursor is also set (if supported by widget)
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Canvas, area: Rect) {
// Make a Span
if self.props.visible {
let lines: Vec<ListItem> = match self.props.texts.rows.as_ref() {
let lines: Vec<ListItem> = match self.props.texts.spans.as_ref() {
None => Vec::new(),
Some(rows) => {
let mut lines: Vec<ListItem> = Vec::new();
for line in rows.iter() {
// Keep line color, or use default
let line_fg: Color = match line.fg {
Color::Reset => self.props.foreground,
_ => line.fg,
};
let line_bg: Color = match line.bg {
Color::Reset => self.props.background,
_ => line.bg,
};
let (fg, bg, modifiers) = use_or_default_styles(&self.props, line);
let message_row =
textwrap::wrap(line.content.as_str(), area.width as usize);
for msg in message_row.iter() {
lines.push(ListItem::new(Spans::from(vec![Span::styled(
align_text_center(msg, area.width),
Style::default()
.add_modifier(line.get_modifiers())
.fg(line_fg)
.bg(line_bg),
Style::default().add_modifier(modifiers).fg(fg).bg(bg),
)])));
}
}
lines
}
};
let title: String = match self.props.texts.title.as_ref() {
Some(t) => t.clone(),
None => String::new(),
};
render.render_widget(
List::new(lines)
.block(
Block::default()
.borders(self.props.borders)
.border_style(Style::default().fg(self.props.foreground))
.border_type(BorderType::Rounded)
.title(title),
)
.block(get_block(
&self.props.borders,
&self.props.texts.title,
true,
))
.start_corner(Corner::TopLeft)
.style(
Style::default()
@@ -115,59 +178,31 @@ impl Component for MsgBox {
}
}
/// ### 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())
fn get_props(&self) -> Props {
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 {
fn on(&mut self, ev: Event) -> Msg {
// Return key
if let InputEvent::Key(key) = ev {
if let Event::Key(key) = ev {
Msg::OnKey(key)
} else {
Msg::None
}
}
/// ### get_value
///
/// Get current value from component
/// For this component returns always None
fn get_value(&self) -> Payload {
fn get_state(&self) -> Payload {
Payload::None
}
// -- events
/// ### blur
///
/// Blur component
fn blur(&mut self) {}
/// ### active
///
/// Active component
fn active(&mut self) {}
}
@@ -175,39 +210,51 @@ impl Component for MsgBox {
mod tests {
use super::*;
use crate::ui::layout::props::{TextParts, TextSpan, TextSpanBuilder};
use crossterm::event::{KeyCode, KeyEvent};
use tui::style::Color;
use tuirealm::event::{KeyCode, KeyEvent};
use tuirealm::props::{TextSpan, TextSpanBuilder};
use tuirealm::tui::style::Color;
#[test]
fn test_ui_layout_components_msgbox() {
fn test_ui_components_msgbox() {
let mut component: MsgBox = MsgBox::new(
PropsBuilder::default()
.with_texts(TextParts::new(
MsgBoxPropsBuilder::default()
.hidden()
.visible()
.with_foreground(Color::Red)
.bold()
.with_borders(Borders::ALL, BorderType::Double, Color::Red)
.with_texts(
None,
Some(vec![
vec![
TextSpan::from("Press "),
TextSpanBuilder::new("<ESC>")
.with_foreground(Color::Cyan)
.bold()
.build(),
TextSpan::from(" to quit"),
]),
))
],
)
.build(),
);
assert_eq!(component.props.foreground, Color::Red);
assert!(component.props.modifiers.intersects(Modifier::BOLD));
assert_eq!(component.props.visible, true);
assert_eq!(component.props.texts.spans.as_ref().unwrap().len(), 3);
component.active();
component.blur();
// Update
let props = component.get_props().with_foreground(Color::Red).build();
let props = MsgBoxPropsBuilder::from(component.get_props())
.hidden()
.with_foreground(Color::Yellow)
.build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
assert_eq!(component.props.visible, false);
assert_eq!(component.props.foreground, Color::Yellow);
// Get value
assert_eq!(component.get_value(), Payload::None);
assert_eq!(component.get_state(), Payload::None);
// Event
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
component.on(Event::Key(KeyEvent::from(KeyCode::Delete))),
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
);
}

View File

@@ -27,7 +27,7 @@
*/
// Dependencies
extern crate crossterm;
extern crate tui;
extern crate tuirealm;
// Locals
use super::input::InputHandler;
@@ -42,8 +42,8 @@ use crossterm::execute;
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use std::io::{stdout, Stdout};
use std::path::PathBuf;
use tui::backend::CrosstermBackend;
use tui::Terminal;
use tuirealm::tui::backend::CrosstermBackend;
use tuirealm::tui::Terminal;
/// ## Context
///

View File

@@ -1,564 +0,0 @@
//! ## Input
//!
//! `Input` component renders an input box
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::super::props::InputType;
use super::{Canvas, Component, InputEvent, Msg, Payload, PropValue, Props, PropsBuilder};
// ext
use crossterm::event::{KeyCode, KeyModifiers};
use tui::{
layout::Rect,
style::Style,
widgets::{Block, BorderType, Paragraph},
};
// -- states
/// ## OwnStates
///
/// OwnStates contains states for this component
#[derive(Clone)]
struct OwnStates {
input: Vec<char>, // Current input
cursor: usize, // Input position
focus: bool, // Focus
}
impl Default for OwnStates {
fn default() -> Self {
OwnStates {
input: Vec::new(),
cursor: 0,
focus: false,
}
}
}
impl OwnStates {
/// ### append
///
/// Append, if possible according to input type, the character to the input vec
pub fn append(&mut self, ch: char, itype: InputType, max_len: Option<usize>) {
// Check if max length has been reached
if self.input.len() < max_len.unwrap_or(usize::MAX) {
match itype {
InputType::Number => {
if ch.is_digit(10) {
// Must be digit
self.input.insert(self.cursor, ch);
// Increment cursor
self.cursor += 1;
}
}
_ => {
// No rule
self.input.insert(self.cursor, ch);
// Increment cursor
self.cursor += 1;
}
}
}
}
/// ### backspace
///
/// Delete element at cursor -1; then decrement cursor by 1
pub fn backspace(&mut self) {
if self.cursor > 0 && !self.input.is_empty() {
self.input.remove(self.cursor - 1);
// Decrement cursor
self.cursor -= 1;
}
}
/// ### delete
///
/// Delete element at cursor
pub fn delete(&mut self) {
if self.cursor < self.input.len() {
self.input.remove(self.cursor);
}
}
/// ### incr_cursor
///
/// Increment cursor value by one if possible
pub fn incr_cursor(&mut self) {
if self.cursor < self.input.len() {
self.cursor += 1;
}
}
/// ### cursoro_at_begin
///
/// Place cursor at the begin of the input
pub fn cursor_at_begin(&mut self) {
self.cursor = 0;
}
/// ### cursor_at_end
///
/// Place cursor at the end of the input
pub fn cursor_at_end(&mut self) {
self.cursor = self.input.len();
}
/// ### decr_cursor
///
/// Decrement cursor value by one if possible
pub fn decr_cursor(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
}
}
/// ### render_value
///
/// Get value as string to render
pub fn render_value(&self, itype: InputType) -> String {
match itype {
InputType::Password => (0..self.input.len()).map(|_| '*').collect(),
_ => self.get_value(),
}
}
/// ### get_value
///
/// Get value as string
pub fn get_value(&self) -> String {
self.input.iter().collect()
}
}
// -- Component
/// ## FileList
///
/// File list component
pub struct Input {
props: Props,
states: OwnStates,
}
impl Input {
/// ### new
///
/// Instantiates a new Input 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 state value from props
if let PropValue::Str(val) = props.value.clone() {
for ch in val.chars() {
states.append(ch, props.input_type, props.input_len);
}
}
Input { props, states }
}
}
impl Component for Input {
/// ### render
///
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
/// If focused, cursor is also set (if supported by widget)
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Canvas, area: Rect) {
if self.props.visible {
let title: String = match self.props.texts.title.as_ref() {
Some(t) => t.clone(),
None => String::new(),
};
let p: Paragraph = Paragraph::new(self.states.render_value(self.props.input_type))
.style(match self.states.focus {
true => Style::default().fg(self.props.foreground),
false => Style::default(),
})
.block(
Block::default()
.borders(self.props.borders)
.border_type(BorderType::Rounded)
.title(title),
);
render.render_widget(p, area);
// Set cursor, if focus
if self.states.focus {
let x: u16 = area.x + (self.states.cursor as u16) + 1;
render.set_cursor(x, area.y + 1);
}
}
}
/// ### 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;
// Set value from props
if let PropValue::Str(val) = self.props.value.clone() {
self.states.input = Vec::new();
self.states.cursor = 0;
for ch in val.chars() {
self.states
.append(ch, self.props.input_type, self.props.input_len);
}
}
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 {
// Make properties with value from states
let mut props: Props = self.props.clone();
props.value = PropValue::Str(self.states.get_value());
PropsBuilder::from(props)
}
/// ### on
///
/// Handle input event and update internal states.
/// Returns a Msg to the view
fn on(&mut self, ev: InputEvent) -> Msg {
if let InputEvent::Key(key) = ev {
match key.code {
KeyCode::Backspace => {
// Backspace and None
self.states.backspace();
Msg::None
}
KeyCode::Delete => {
// Delete and None
self.states.delete();
Msg::None
}
KeyCode::Enter => Msg::OnSubmit(self.get_value()),
KeyCode::Left => {
// Move cursor left; msg None
self.states.decr_cursor();
Msg::None
}
KeyCode::Right => {
// Move cursor right; Msg None
self.states.incr_cursor();
Msg::None
}
KeyCode::End => {
// Cursor at last position
self.states.cursor_at_end();
Msg::None
}
KeyCode::Home => {
// Cursor at first positon
self.states.cursor_at_begin();
Msg::None
}
KeyCode::Char(ch) => {
// Check if modifiers is NOT CTRL OR ALT
if !key.modifiers.intersects(KeyModifiers::CONTROL)
&& !key.modifiers.intersects(KeyModifiers::ALT)
{
// Push char to input
self.states
.append(ch, self.props.input_type, self.props.input_len);
// Message none
Msg::None
} else {
// Return key
Msg::OnKey(key)
}
}
_ => Msg::OnKey(key),
}
} else {
Msg::None
}
}
/// ### get_value
///
/// Get current value from component
/// Returns the value as string or as a number based on the input value
fn get_value(&self) -> Payload {
match self.props.input_type {
InputType::Number => {
Payload::Unsigned(self.states.get_value().parse::<usize>().ok().unwrap_or(0))
}
_ => Payload::Text(self.states.get_value()),
}
}
// -- events
/// ### blur
///
/// Blur component; basically remove focus
fn blur(&mut self) {
self.states.focus = false;
}
/// ### active
///
/// Active component; basically give focus
fn active(&mut self) {
self.states.focus = true;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyEvent;
use tui::style::Color;
#[test]
fn test_ui_layout_components_input_text() {
// Instantiate Input with value
let mut component: Input = Input::new(
PropsBuilder::default()
.with_input(InputType::Text)
.with_input_len(5)
.with_value(PropValue::Str(String::from("home")))
.build(),
);
// Verify initial state
assert_eq!(component.states.cursor, 4);
assert_eq!(component.states.input.len(), 4);
// Focus
assert_eq!(component.states.focus, false);
component.active();
assert_eq!(component.states.focus, true);
component.blur();
assert_eq!(component.states.focus, false);
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Get value
assert_eq!(component.get_value(), Payload::Text(String::from("home")));
// RenderData
//assert_eq!(component.render().unwrap().cursor, 4);
assert_eq!(component.states.cursor, 4);
// Handle events
// Try key with ctrl
assert_eq!(
component.on(InputEvent::Key(KeyEvent::new(
KeyCode::Char('a'),
KeyModifiers::CONTROL
))),
Msg::OnKey(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)),
);
// String shouldn't have changed
assert_eq!(component.get_value(), Payload::Text(String::from("home")));
//assert_eq!(component.render().unwrap().cursor, 4);
assert_eq!(component.states.cursor, 4);
// Character
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('/')))),
Msg::None
);
assert_eq!(component.get_value(), Payload::Text(String::from("home/")));
//assert_eq!(component.render().unwrap().cursor, 5);
assert_eq!(component.states.cursor, 5);
// Verify max length (shouldn't push any character)
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('a')))),
Msg::None
);
assert_eq!(component.get_value(), Payload::Text(String::from("home/")));
//assert_eq!(component.render().unwrap().cursor, 5);
assert_eq!(component.states.cursor, 5);
// Enter
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter))),
Msg::OnSubmit(Payload::Text(String::from("home/")))
);
// Backspace
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
Msg::None
);
assert_eq!(component.get_value(), Payload::Text(String::from("home")));
//assert_eq!(component.render().unwrap().cursor, 4);
assert_eq!(component.states.cursor, 4);
// Check backspace at 0
component.states.input = vec!['h'];
component.states.cursor = 1;
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
Msg::None
);
assert_eq!(component.get_value(), Payload::Text(String::from("")));
//assert_eq!(component.render().unwrap().cursor, 0);
assert_eq!(component.states.cursor, 0);
// Another one...
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Backspace))),
Msg::None
);
assert_eq!(component.get_value(), Payload::Text(String::from("")));
//assert_eq!(component.render().unwrap().cursor, 0);
assert_eq!(component.states.cursor, 0);
// See del behaviour here
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
Msg::None
);
assert_eq!(component.get_value(), Payload::Text(String::from("")));
//assert_eq!(component.render().unwrap().cursor, 0);
assert_eq!(component.states.cursor, 0);
// Check del behaviour
component.states.input = vec!['h', 'e'];
component.states.cursor = 1;
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
Msg::None
);
assert_eq!(component.get_value(), Payload::Text(String::from("h")));
//assert_eq!(component.render().unwrap().cursor, 1); // Shouldn't move
assert_eq!(component.states.cursor, 1);
// Another one (should do nothing)
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
Msg::None
);
assert_eq!(component.get_value(), Payload::Text(String::from("h")));
//assert_eq!(component.render().unwrap().cursor, 1); // Shouldn't move
assert_eq!(component.states.cursor, 1);
// Move cursor right
component.states.input = vec!['h', 'e', 'l', 'l', 'o'];
component.states.cursor = 1;
component.props.input_len = Some(16); // Let's change length
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))), // between 'e' and 'l'
Msg::None
);
//assert_eq!(component.render().unwrap().cursor, 2); // Should increment
assert_eq!(component.states.cursor, 2);
// Put a character here
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('a')))),
Msg::None
);
assert_eq!(component.get_value(), Payload::Text(String::from("heallo")));
//assert_eq!(component.render().unwrap().cursor, 3);
assert_eq!(component.states.cursor, 3);
// Move left
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Left))),
Msg::None
);
//assert_eq!(component.render().unwrap().cursor, 2); // Should decrement
assert_eq!(component.states.cursor, 2);
// Go at the end
component.states.cursor = 6;
// Move right
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))),
Msg::None
);
//assert_eq!(component.render().unwrap().cursor, 6); // Should stay
assert_eq!(component.states.cursor, 6);
// Move left
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Left))),
Msg::None
);
//assert_eq!(component.render().unwrap().cursor, 5); // Should decrement
assert_eq!(component.states.cursor, 5);
// Go at the beginning
component.states.cursor = 0;
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Left))),
Msg::None
);
//assert_eq!(component.render().unwrap().cursor, 0); // Should stay
assert_eq!(component.states.cursor, 0);
// End - begin
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::End))),
Msg::None
);
assert_eq!(component.states.cursor, 6);
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Home))),
Msg::None
);
assert_eq!(component.states.cursor, 0);
// Update value
component.update(
component
.get_props()
.with_value(PropValue::Str("new-value".to_string()))
.build(),
);
assert_eq!(
component.get_value(),
Payload::Text(String::from("new-value"))
);
}
#[test]
fn test_ui_layout_components_input_number() {
// Instantiate Input with value
let mut component: Input = Input::new(
PropsBuilder::default()
.with_input(InputType::Number)
.with_input_len(5)
.with_value(PropValue::Str(String::from("3000")))
.build(),
);
// Verify initial state
assert_eq!(component.states.cursor, 4);
assert_eq!(component.states.input.len(), 4);
// Push a non numeric value
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('a')))),
Msg::None
);
assert_eq!(component.get_value(), Payload::Unsigned(3000));
//assert_eq!(component.render().unwrap().cursor, 4);
assert_eq!(component.states.cursor, 4);
// Push a number
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('1')))),
Msg::None
);
assert_eq!(component.get_value(), Payload::Unsigned(30001));
//assert_eq!(component.render().unwrap().cursor, 5);
assert_eq!(component.states.cursor, 5);
}
}

View File

@@ -1,184 +0,0 @@
//! ## ProgressBar
//!
//! `ProgressBar` component renders a progress bar
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{Canvas, Component, InputEvent, Msg, Payload, PropValue, Props, PropsBuilder};
// ext
use tui::{
layout::Rect,
style::Style,
widgets::{Block, Gauge},
};
// -- component
pub struct ProgressBar {
props: Props,
}
impl ProgressBar {
/// ### new
///
/// Instantiate a new Progress Bar
pub fn new(props: Props) -> Self {
ProgressBar { props }
}
}
impl Component for ProgressBar {
/// ### render
///
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
/// If focused, cursor is also set (if supported by widget)
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Canvas, area: Rect) {
// Make a Span
if self.props.visible {
let title: String = match self.props.texts.title.as_ref() {
Some(t) => t.clone(),
None => String::new(),
};
// Text
let label: String = match self.props.texts.rows.as_ref() {
Some(rows) => match rows.get(0) {
Some(label) => label.content.clone(),
None => String::new(),
},
None => String::new(),
};
// Get percentage
let percentage: f64 = match self.props.value {
PropValue::Float(ratio) => ratio,
_ => 0.0,
};
// Make progress bar
render.render_widget(
Gauge::default()
.block(Block::default().borders(self.props.borders).title(title))
.gauge_style(
Style::default()
.fg(self.props.foreground)
.bg(self.props.background)
.add_modifier(self.props.get_modifiers()),
)
.label(label)
.ratio(percentage),
area,
);
}
}
/// ### 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 {
// Return key
if let InputEvent::Key(key) = ev {
Msg::OnKey(key)
} else {
Msg::None
}
}
/// ### get_value
///
/// Get current value from component
/// For this component returns always None
fn get_value(&self) -> Payload {
Payload::None
}
// -- events
/// ### blur
///
/// Blur component
fn blur(&mut self) {}
/// ### active
///
/// Active component
fn active(&mut self) {}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::layout::props::{TextParts, TextSpan};
use crossterm::event::{KeyCode, KeyEvent};
use tui::style::Color;
#[test]
fn test_ui_layout_components_progress_bar() {
let mut component: ProgressBar = ProgressBar::new(
PropsBuilder::default()
.with_texts(TextParts::new(
Some(String::from("Uploading file...")),
Some(vec![TextSpan::from("96.5% - ETA 00:02 (4.2MB/s)")]),
))
.build(),
);
// Get value
assert_eq!(component.get_value(), Payload::None);
component.active();
component.blur();
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Event
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
);
}
}

View File

@@ -1,334 +0,0 @@
//! ## RadioGroup
//!
//! `RadioGroup` component renders a radio group
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::super::props::TextSpan;
use super::{Canvas, Component, InputEvent, Msg, Payload, PropValue, Props, PropsBuilder};
// ext
use crossterm::event::KeyCode;
use tui::{
layout::Rect,
style::{Color, Style},
text::Spans,
widgets::{Block, BorderType, Tabs},
};
// -- states
/// ## OwnStates
///
/// OwnStates contains states for this component
#[derive(Clone)]
struct OwnStates {
choice: usize, // Selected option
choices: Vec<String>, // Available choices
focus: bool, // has focus?
}
impl Default for OwnStates {
fn default() -> Self {
OwnStates {
choice: 0,
choices: Vec::new(),
focus: false,
}
}
}
impl OwnStates {
/// ### next_choice
///
/// Move choice index to next choice
pub fn next_choice(&mut self) {
if self.choice + 1 < self.choices.len() {
self.choice += 1;
}
}
/// ### prev_choice
///
/// Move choice index to previous choice
pub fn prev_choice(&mut self) {
if self.choice > 0 {
self.choice -= 1;
}
}
/// ### make_choices
///
/// Set OwnStates choices from a vector of text spans
pub fn make_choices(&mut self, spans: &[TextSpan]) {
self.choices = spans.iter().map(|x| x.content.clone()).collect();
}
}
// -- component
/// ## RadioGroup
///
/// RadioGroup component represents a group of tabs to select from
pub struct RadioGroup {
props: Props,
states: OwnStates,
}
impl RadioGroup {
/// ### new
///
/// Instantiate a new Radio Group component
pub fn new(props: Props) -> Self {
// Make states
let mut states: OwnStates = OwnStates::default();
// Update choices (vec of TextSpan to String)
states.make_choices(props.texts.rows.as_ref().unwrap_or(&Vec::new()));
// Get value
if let PropValue::Unsigned(choice) = props.value {
states.choice = choice;
}
RadioGroup { props, states }
}
}
impl Component for RadioGroup {
/// ### render
///
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
/// If focused, cursor is also set (if supported by widget)
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Canvas, area: Rect) {
if self.props.visible {
// Make choices
let choices: Vec<Spans> = self
.states
.choices
.iter()
.map(|x| Spans::from(x.clone()))
.collect();
// Make colors
let (bg, fg, block_color): (Color, Color, Color) = match &self.states.focus {
true => (
self.props.foreground,
self.props.background,
self.props.foreground,
),
false => (Color::Reset, self.props.foreground, Color::Reset),
};
let title: String = match &self.props.texts.title {
Some(t) => t.clone(),
None => String::new(),
};
render.render_widget(
Tabs::new(choices)
.block(
Block::default()
.borders(self.props.borders)
.border_type(BorderType::Rounded)
.style(Style::default())
.title(title),
)
.select(self.states.choice)
.style(Style::default().fg(block_color))
.highlight_style(
Style::default()
.add_modifier(self.props.get_modifiers())
.fg(fg)
.bg(bg),
),
area,
);
}
}
/// ### 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 {
// Reset choices
self.states
.make_choices(props.texts.rows.as_ref().unwrap_or(&Vec::new()));
// Get value
if let PropValue::Unsigned(choice) = props.value {
self.states.choice = choice;
}
self.props = props;
// Msg 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
fn on(&mut self, ev: InputEvent) -> Msg {
// Match event
if let InputEvent::Key(key) = ev {
match key.code {
KeyCode::Right => {
// Increment choice
self.states.next_choice();
// Return Msg On Change
Msg::OnChange(self.get_value())
}
KeyCode::Left => {
// Decrement choice
self.states.prev_choice();
// Return Msg On Change
Msg::OnChange(self.get_value())
}
KeyCode::Enter => {
// Return Submit
Msg::OnSubmit(self.get_value())
}
_ => {
// Return key event to activity
Msg::OnKey(key)
}
}
} else {
// Ignore event
Msg::None
}
}
/// ### get_value
///
/// Get current value from component
/// Returns the selected option
fn get_value(&self) -> Payload {
Payload::Unsigned(self.states.choice)
}
// -- events
/// ### blur
///
/// Blur component; basically remove focus
fn blur(&mut self) {
self.states.focus = false;
}
/// ### active
///
/// Active component; basically give focus
fn active(&mut self) {
self.states.focus = true;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::layout::props::{TextParts, TextSpan};
use crossterm::event::KeyEvent;
#[test]
fn test_ui_layout_components_radio() {
// Make component
let mut component: RadioGroup = RadioGroup::new(
PropsBuilder::default()
.with_texts(TextParts::new(
Some(String::from("yes or no?")),
Some(vec![
TextSpan::from("Yes!"),
TextSpan::from("No"),
TextSpan::from("Maybe"),
]),
))
.with_value(PropValue::Unsigned(1))
.build(),
);
// Verify states
assert_eq!(component.states.choice, 1);
assert_eq!(component.states.choices.len(), 3);
// Focus
assert_eq!(component.states.focus, false);
component.active();
assert_eq!(component.states.focus, true);
component.blur();
assert_eq!(component.states.focus, false);
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Get value
assert_eq!(component.get_value(), Payload::Unsigned(1));
// Handle events
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Left))),
Msg::OnChange(Payload::Unsigned(0)),
);
assert_eq!(component.get_value(), Payload::Unsigned(0));
// Left again
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Left))),
Msg::OnChange(Payload::Unsigned(0)),
);
assert_eq!(component.get_value(), Payload::Unsigned(0));
// Right
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))),
Msg::OnChange(Payload::Unsigned(1)),
);
assert_eq!(component.get_value(), Payload::Unsigned(1));
// Right again
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))),
Msg::OnChange(Payload::Unsigned(2)),
);
assert_eq!(component.get_value(), Payload::Unsigned(2));
// Right again
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))),
Msg::OnChange(Payload::Unsigned(2)),
);
assert_eq!(component.get_value(), Payload::Unsigned(2));
// Submit
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter))),
Msg::OnSubmit(Payload::Unsigned(2)),
);
// Any key
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('a')))),
Msg::OnKey(KeyEvent::from(KeyCode::Char('a'))),
);
}
}

View File

@@ -1,202 +0,0 @@
//! ## TextList
//!
//! `TextList` component renders a radio group
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
// ext
use tui::{
layout::{Corner, Rect},
style::Style,
text::{Span, Spans},
widgets::{Block, BorderType, List, ListItem},
};
// -- 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, renders a widget using the provided render engine in the provided Area
/// If focused, cursor is also set (if supported by widget)
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Canvas, area: Rect) {
// 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<ListItem> = match self.props.texts.table.as_ref() {
None => Vec::new(),
Some(table) => table
.iter()
.map(|row| {
let columns: Vec<Span> = 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
render.render_widget(
List::new(list_items)
.block(
Block::default()
.borders(self.props.borders)
.border_style(Style::default())
.border_type(BorderType::Rounded)
.title(title),
)
.start_corner(Corner::TopLeft),
area,
);
}
}
/// ### 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 {
// Return key
if let InputEvent::Key(key) = ev {
Msg::OnKey(key)
} else {
Msg::None
}
}
/// ### get_value
///
/// Get current value from component
/// For this component returns always None
fn get_value(&self) -> Payload {
Payload::None
}
// -- events
/// ### blur
///
/// Blur component
fn blur(&mut self) {}
/// ### active
///
/// Active component
fn active(&mut self) {}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::layout::props::{TableBuilder, TextParts, TextSpan};
use crossterm::event::{KeyCode, KeyEvent};
use tui::style::Color;
#[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(),
);
component.active();
component.blur();
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Get value
assert_eq!(component.get_value(), Payload::None);
// Event
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
);
}
}

View File

@@ -1,195 +0,0 @@
//! ## Text
//!
//! `Text` component renders a simple readonly no event associated text
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
// ext
use tui::{
layout::Rect,
style::{Color, Style},
text::{Span, Spans, Text as TuiText},
widgets::Paragraph,
};
// -- component
pub struct Text {
props: Props,
}
impl Text {
/// ### new
///
/// Instantiate a new Text component
pub fn new(props: Props) -> Self {
Text { props }
}
}
impl Component for Text {
/// ### render
///
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
/// If focused, cursor is also set (if supported by widget)
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Canvas, area: Rect) {
// Make a Span
if self.props.visible {
let spans: Vec<Span> = match self.props.texts.rows.as_ref() {
None => Vec::new(),
Some(rows) => rows
.iter()
.map(|x| {
// Keep line color, or use default
let fg: Color = match x.fg {
Color::Reset => self.props.foreground,
_ => x.fg,
};
let bg: Color = match x.bg {
Color::Reset => self.props.background,
_ => x.bg,
};
Span::styled(
x.content.clone(),
Style::default()
.add_modifier(x.get_modifiers())
.fg(fg)
.bg(bg),
)
})
.collect(),
};
// Make text
let mut text: TuiText = TuiText::from(Spans::from(spans));
// Apply style
text.patch_style(
Style::default()
//.add_modifier(self.props.get_modifiers())
//.fg(self.props.foreground) NOTE: don't style twice !!!
//.bg(self.props.background),
);
render.render_widget(Paragraph::new(text), area);
}
}
/// ### 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 {
// Return key
if let InputEvent::Key(key) = ev {
Msg::OnKey(key)
} else {
Msg::None
}
}
/// ### get_value
///
/// Get current value from component
/// For this component returns always None
fn get_value(&self) -> Payload {
Payload::None
}
// -- events
/// ### blur
///
/// Blur component
fn blur(&mut self) {}
/// ### active
///
/// Active component
fn active(&mut self) {}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::layout::props::{TextParts, TextSpan, TextSpanBuilder};
use crossterm::event::{KeyCode, KeyEvent};
use tui::style::Color;
#[test]
fn test_ui_layout_components_text() {
let mut component: Text = Text::new(
PropsBuilder::default()
.with_texts(TextParts::new(
None,
Some(vec![
TextSpan::from("Press "),
TextSpanBuilder::new("<ESC>")
.with_foreground(Color::Cyan)
.bold()
.build(),
TextSpan::from(" to quit"),
]),
))
.build(),
);
component.active();
component.blur();
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Get value
assert_eq!(component.get_value(), Payload::None);
// Event
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
);
}
}

View File

@@ -1,159 +0,0 @@
//! ## Title
//!
//! `Title` component renders a simple readonly no event associated title
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder};
// ext
use tui::{layout::Rect, style::Style, widgets::Paragraph};
// -- component
pub struct Title {
props: Props,
}
impl Title {
/// ### new
///
/// Instantiate a new Title component
pub fn new(props: Props) -> Self {
Title { props }
}
}
impl Component for Title {
/// ### render
///
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
/// If focused, cursor is also set (if supported by widget)
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Canvas, area: Rect) {
// Make a Span
if self.props.visible {
let title: String = match self.props.texts.title.as_ref() {
None => String::new(),
Some(t) => t.clone(),
};
render.render_widget(
Paragraph::new(title).style(
Style::default()
.fg(self.props.foreground)
.bg(self.props.background)
.add_modifier(self.props.get_modifiers()),
),
area,
);
}
}
/// ### 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 {
// Return key
if let InputEvent::Key(key) = ev {
Msg::OnKey(key)
} else {
Msg::None
}
}
/// ### get_value
///
/// Get current value from component
/// For this component returns always None
fn get_value(&self) -> Payload {
Payload::None
}
// -- events
/// ### blur
///
/// Blur component
fn blur(&mut self) {}
/// ### active
///
/// Active component
fn active(&mut self) {}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::layout::props::TextParts;
use crossterm::event::{KeyCode, KeyEvent};
use tui::style::Color;
#[test]
fn test_ui_layout_components_title() {
let mut component: Title = Title::new(
PropsBuilder::default()
.with_texts(TextParts::new(Some(String::from("Title")), None))
.build(),
);
component.active();
component.blur();
// Update
let props = component.get_props().with_foreground(Color::Red).build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.props.foreground, Color::Red);
// Get value
assert_eq!(component.get_value(), Payload::None);
// Event
assert_eq!(
component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))),
Msg::OnKey(KeyEvent::from(KeyCode::Delete))
);
}
}

View File

@@ -1,122 +0,0 @@
//! ## Layout
//!
//! `Layout` is the module which provides components, view, state and properties to create layouts
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Modules
pub mod components;
pub mod props;
pub mod utils;
pub mod view;
// locals
use props::{PropValue, Props, PropsBuilder};
// ext
use crossterm::event::Event as InputEvent;
use crossterm::event::KeyEvent;
use std::io::Stdout;
use tui::backend::CrosstermBackend;
use tui::layout::Rect;
use tui::Frame;
type Backend = CrosstermBackend<Stdout>;
pub(crate) type Canvas<'a> = Frame<'a, Backend>;
// -- Msg
/// ## Msg
///
/// Msg is an enum returned after an event is raised for a certain component
/// Yep, I took inspiration from Elm.
#[derive(std::fmt::Debug, PartialEq, Eq)]
pub enum Msg {
OnSubmit(Payload),
OnChange(Payload),
OnKey(KeyEvent),
None,
}
/// ## Payload
///
/// Payload describes a component value
#[derive(std::fmt::Debug, PartialEq, Eq)]
pub enum Payload {
Text(String),
//Signed(isize),
Unsigned(usize),
None,
}
// -- Component
/// ## Component
///
/// Component is a trait which defines the behaviours for a Layout component.
/// All layout components must implement a method to render and one to update
pub trait Component {
/// ### render
///
/// Based on the current properties and states, renders the component in the provided area frame
#[cfg(not(tarpaulin_include))]
fn render(&self, frame: &mut Canvas, area: Rect);
/// ### 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;
/// ### 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;
/// ### on
///
/// Handle input event and update internal states.
/// Returns a Msg to the view
fn on(&mut self, ev: InputEvent) -> Msg;
/// ### get_value
///
/// Get current value from component
fn get_value(&self) -> Payload;
// -- events
/// ### blur
///
/// Blur component; basically remove focus
fn blur(&mut self);
/// ### active
///
/// Active component; basically give focus
fn active(&mut self);
}

View File

@@ -1,779 +0,0 @@
//! ## Props
//!
//! `Props` is the module which defines properties for layout components
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// ext
use tui::style::{Color, Modifier};
use tui::widgets::Borders;
// -- Props
/// ## Props
///
/// Props holds all the possible properties for a layout component
#[derive(Clone)]
pub struct Props {
// Values
pub visible: bool, // Is the element visible ON CREATE?
pub foreground: Color, // Foreground color
pub background: Color, // Background color
pub borders: Borders, // Borders
pub bold: bool, // Text bold
pub italic: bool, // Italic
pub underlined: bool, // Underlined
pub input_type: InputType, // Input type
pub input_len: Option<usize>, // max input len
pub texts: TextParts, // text parts
pub value: PropValue, // Initial value
}
impl Default for Props {
fn default() -> Self {
Self {
// Values
visible: true,
foreground: Color::Reset,
background: Color::Reset,
borders: Borders::ALL,
bold: false,
italic: false,
underlined: false,
input_type: InputType::Text,
input_len: None,
texts: TextParts::default(),
value: PropValue::None,
}
}
}
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
///
/// Chain constructor for `Props`
pub struct PropsBuilder {
props: Option<Props>,
}
#[allow(dead_code)]
impl PropsBuilder {
/// ### build
///
/// Build Props from builder
/// Don't call this method twice for any reasons!
pub fn build(&mut self) -> Props {
self.props.take().unwrap()
}
/// ### hidden
///
/// Initialize props with visible set to False
pub fn hidden(&mut self) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.visible = false;
}
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
}
/// ### with_foreground
///
/// Set foreground color for component
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.foreground = color;
}
self
}
/// ### with_background
///
/// Set background color for component
pub fn with_background(&mut self, color: Color) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.background = color;
}
self
}
/// ### with_borders
///
/// Set component borders style
pub fn with_borders(&mut self, borders: Borders) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.borders = borders;
}
self
}
/// ### bold
///
/// Set bold property for component
pub fn bold(&mut self) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.bold = true;
}
self
}
/// ### italic
///
/// Set italic property for component
pub fn italic(&mut self) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.italic = true;
}
self
}
/// ### underlined
///
/// Set underlined property for component
pub fn underlined(&mut self) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.underlined = true;
}
self
}
/// ### with_texts
///
/// Set texts for component
pub fn with_texts(&mut self, texts: TextParts) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.texts = texts;
}
self
}
/// ### with_input
///
/// Set input type for component
pub fn with_input(&mut self, input_type: InputType) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.input_type = input_type;
}
self
}
/// ### with_input_len
///
/// Set max input len
pub fn with_input_len(&mut self, len: usize) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.input_len = Some(len);
}
self
}
/// ### with_value
///
/// Set initial value for component
pub fn with_value(&mut self, value: PropValue) -> &mut Self {
if let Some(props) = self.props.as_mut() {
props.value = value;
}
self
}
}
impl From<Props> for PropsBuilder {
fn from(props: Props) -> Self {
PropsBuilder { props: Some(props) }
}
}
impl Default for PropsBuilder {
fn default() -> Self {
PropsBuilder {
props: Some(Props::default()),
}
}
}
// -- Text parts
/// ## Table
///
/// Table represents a list of rows with a list of columns of text spans
pub type Table = Vec<Vec<TextSpan>>;
/// ## TextParts
///
/// TextParts holds optional component for the text displayed by a component
#[derive(Clone)]
pub struct TextParts {
pub title: Option<String>,
pub rows: Option<Vec<TextSpan>>,
pub table: Option<Table>, // First vector is rows, inner vec is column
}
impl TextParts {
/// ### new
///
/// Instantiates a new TextParts entity
pub fn new(title: Option<String>, rows: Option<Vec<TextSpan>>) -> Self {
TextParts {
title,
rows,
table: None,
}
}
/// ### table
///
/// Instantiates a new TextParts as a Table
pub fn table(title: Option<String>, table: Table) -> Self {
TextParts {
title,
rows: None,
table: Some(table),
}
}
}
impl Default for TextParts {
fn default() -> Self {
TextParts {
title: None,
rows: None,
table: None,
}
}
}
/// ## TableBuilder
///
/// Table builder is a helper to make it easier to build text tables
pub struct TableBuilder {
table: Option<Table>,
}
impl TableBuilder {
/// ### add_col
///
/// Add a column to the last row
pub fn add_col(&mut self, span: TextSpan) -> &mut Self {
if let Some(table) = self.table.as_mut() {
if let Some(row) = table.last_mut() {
row.push(span);
}
}
self
}
/// ### add_row
///
/// Add a new row to the table
pub fn add_row(&mut self) -> &mut Self {
if let Some(table) = self.table.as_mut() {
table.push(vec![]);
}
self
}
/// ### build
///
/// Take table out of builder
/// Don't call this method twice for any reasons!
pub fn build(&mut self) -> Table {
self.table.take().unwrap()
}
}
impl Default for TableBuilder {
fn default() -> Self {
TableBuilder {
table: Some(vec![vec![]]),
}
}
}
/// ### TextSpan
///
/// TextSpan is a "cell" of text with its attributes
#[derive(Clone, std::fmt::Debug)]
pub struct TextSpan {
pub content: String,
pub fg: Color,
pub bg: Color,
pub bold: bool,
pub italic: bool,
pub underlined: bool,
}
impl From<&str> for TextSpan {
fn from(txt: &str) -> Self {
TextSpan {
content: txt.to_string(),
fg: Color::Reset,
bg: Color::Reset,
bold: false,
italic: false,
underlined: false,
}
}
}
impl From<String> for TextSpan {
fn from(content: String) -> Self {
TextSpan {
content,
fg: Color::Reset,
bg: Color::Reset,
bold: false,
italic: false,
underlined: false,
}
}
}
impl TextSpan {
/// ### 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(),
})
}
}
// -- TextSpan builder
/// ## TextSpanBuilder
///
/// TextSpanBuilder is a struct which helps building quickly a TextSpan
pub struct TextSpanBuilder {
text: Option<TextSpan>,
}
#[allow(dead_code)]
impl TextSpanBuilder {
/// ### new
///
/// Instantiate a new TextSpanBuilder
pub fn new(text: &str) -> Self {
TextSpanBuilder {
text: Some(TextSpan::from(text)),
}
}
/// ### with_foreground
///
/// Set foreground for text span
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
if let Some(text) = self.text.as_mut() {
text.fg = color;
}
self
}
/// ### with_background
///
/// Set background for text span
pub fn with_background(&mut self, color: Color) -> &mut Self {
if let Some(text) = self.text.as_mut() {
text.bg = color;
}
self
}
/// ### italic
///
/// Set italic for text span
pub fn italic(&mut self) -> &mut Self {
if let Some(text) = self.text.as_mut() {
text.italic = true;
}
self
}
/// ### bold
///
/// Set bold for text span
pub fn bold(&mut self) -> &mut Self {
if let Some(text) = self.text.as_mut() {
text.bold = true;
}
self
}
/// ### underlined
///
/// Set underlined for text span
pub fn underlined(&mut self) -> &mut Self {
if let Some(text) = self.text.as_mut() {
text.underlined = true;
}
self
}
/// ### build
///
/// Make TextSpan out of builder
/// Don't call this method twice for any reasons!
pub fn build(&mut self) -> TextSpan {
self.text.take().unwrap()
}
}
// -- Prop value
/// ### PropValue
///
/// PropValue describes a property initial value
#[derive(Clone, PartialEq, std::fmt::Debug)]
#[allow(dead_code)]
pub enum PropValue {
Str(String),
Unsigned(usize),
Signed(isize),
Float(f64),
Boolean(bool),
None,
}
// -- Input Type
/// ## InputType
///
/// Input type for text inputs
#[derive(Clone, Copy, PartialEq, std::fmt::Debug)]
pub enum InputType {
Text,
Number,
Password,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ui_layout_props_default() {
let props: Props = Props::default();
assert_eq!(props.visible, true);
assert_eq!(props.background, Color::Reset);
assert_eq!(props.foreground, Color::Reset);
assert_eq!(props.borders, Borders::ALL);
assert_eq!(props.bold, false);
assert_eq!(props.italic, false);
assert_eq!(props.underlined, false);
assert!(props.texts.title.is_none());
assert_eq!(props.input_type, InputType::Text);
assert!(props.input_len.is_none());
assert_eq!(props.value, PropValue::None);
assert!(props.texts.rows.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()
.with_background(Color::Blue)
.with_foreground(Color::Green)
.with_borders(Borders::BOTTOM)
.bold()
.italic()
.underlined()
.with_texts(TextParts::new(
Some(String::from("hello")),
Some(vec![TextSpan::from("hey")]),
))
.with_input(InputType::Password)
.with_input_len(16)
.with_value(PropValue::Str(String::from("Hello")))
.build();
assert_eq!(props.background, Color::Blue);
assert_eq!(props.borders, Borders::BOTTOM);
assert_eq!(props.bold, true);
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.input_type, InputType::Password);
assert_eq!(*props.input_len.as_ref().unwrap(), 16);
if let PropValue::Str(s) = props.value {
assert_eq!(s.as_str(), "Hello");
} else {
panic!("Expected value to be a string");
}
assert_eq!(
props
.texts
.rows
.as_ref()
.unwrap()
.get(0)
.unwrap()
.content
.as_str(),
"hey"
);
assert_eq!(props.underlined, true);
assert_eq!(props.visible, false);
let props: Props = PropsBuilder::default()
.visible()
.with_background(Color::Blue)
.with_foreground(Color::Green)
.bold()
.italic()
.underlined()
.with_texts(TextParts::new(
Some(String::from("hello")),
Some(vec![TextSpan::from("hey")]),
))
.build();
assert_eq!(props.background, Color::Blue);
assert_eq!(props.bold, true);
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
.rows
.as_ref()
.unwrap()
.get(0)
.unwrap()
.content
.as_str(),
"hey"
);
assert_eq!(props.underlined, true);
assert_eq!(props.visible, true);
}
#[test]
#[should_panic]
fn test_ui_layout_props_build_twice() {
let mut builder: PropsBuilder = PropsBuilder::default();
let _ = builder.build();
builder
.hidden()
.with_background(Color::Blue)
.with_foreground(Color::Green)
.bold()
.italic()
.underlined()
.with_texts(TextParts::new(
Some(String::from("hello")),
Some(vec![TextSpan::from("hey")]),
));
// Rebuild
let _ = builder.build();
}
#[test]
fn test_ui_layout_props_builder_from_props() {
let props: Props = PropsBuilder::default()
.hidden()
.with_background(Color::Blue)
.with_foreground(Color::Green)
.bold()
.italic()
.underlined()
.with_texts(TextParts::new(
Some(String::from("hello")),
Some(vec![TextSpan::from("hey")]),
))
.build();
// Ok, now make a builder from properties
let builder: PropsBuilder = PropsBuilder::from(props);
assert!(builder.props.is_some());
}
#[test]
fn test_ui_layout_props_text_parts_with_values() {
let parts: TextParts = TextParts::new(
Some(String::from("Hello world!")),
Some(vec![TextSpan::from("row1"), TextSpan::from("row2")]),
);
assert_eq!(parts.title.as_ref().unwrap().as_str(), "Hello world!");
assert_eq!(
parts
.rows
.as_ref()
.unwrap()
.get(0)
.unwrap()
.content
.as_str(),
"row1"
);
assert_eq!(
parts
.rows
.as_ref()
.unwrap()
.get(1)
.unwrap()
.content
.as_str(),
"row2"
);
}
#[test]
fn test_ui_layout_props_text_parts_default() {
let parts: TextParts = TextParts::default();
assert!(parts.title.is_none());
assert!(parts.rows.is_none());
}
#[test]
fn test_ui_layout_props_text_parts_table() {
let table: TextParts = 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("christian"))
.add_col(TextSpan::from("23"))
.add_row()
.add_col(TextSpan::from("omar"))
.add_col(TextSpan::from("25"))
.add_row()
.add_row()
.add_col(TextSpan::from("pippo"))
.build(),
);
// Verify table
assert_eq!(table.title.as_ref().unwrap().as_str(), "my data");
assert!(table.rows.is_none());
assert_eq!(table.table.as_ref().unwrap().len(), 5); // 5 rows
assert_eq!(table.table.as_ref().unwrap().get(0).unwrap().len(), 2); // 2 cols
assert_eq!(table.table.as_ref().unwrap().get(1).unwrap().len(), 2); // 2 cols
assert_eq!(
table
.table
.as_ref()
.unwrap()
.get(1)
.unwrap()
.get(0)
.unwrap()
.content
.as_str(),
"christian"
); // check content
assert_eq!(table.table.as_ref().unwrap().get(2).unwrap().len(), 2); // 2 cols
assert_eq!(table.table.as_ref().unwrap().get(3).unwrap().len(), 0); // 0 cols
assert_eq!(table.table.as_ref().unwrap().get(4).unwrap().len(), 1); // 1 cols
}
#[test]
fn test_ui_layout_props_text_span() {
// from str
let span: TextSpan = TextSpan::from("Hello!");
assert_eq!(span.content.as_str(), "Hello!");
assert_eq!(span.bold, false);
assert_eq!(span.fg, Color::Reset);
assert_eq!(span.bg, Color::Reset);
assert_eq!(span.italic, false);
assert_eq!(span.underlined, false);
// From String
let span: TextSpan = TextSpan::from(String::from("omar"));
assert_eq!(span.content.as_str(), "omar");
assert_eq!(span.bold, false);
assert_eq!(span.fg, Color::Reset);
assert_eq!(span.bg, Color::Reset);
assert_eq!(span.italic, false);
assert_eq!(span.underlined, false);
// With attributes
let span: TextSpan = TextSpanBuilder::new("Error")
.with_background(Color::Red)
.with_foreground(Color::Black)
.bold()
.italic()
.underlined()
.build();
assert_eq!(span.content.as_str(), "Error");
assert_eq!(span.bold, true);
assert_eq!(span.fg, Color::Black);
assert_eq!(span.bg, Color::Red);
assert_eq!(span.italic, true);
assert_eq!(span.underlined, true);
// Check modifiers
let modifiers: Modifier = span.get_modifiers();
assert!(modifiers.intersects(Modifier::BOLD));
assert!(modifiers.intersects(Modifier::ITALIC));
assert!(modifiers.intersects(Modifier::UNDERLINED));
}
}

View File

@@ -1,72 +0,0 @@
//! ## Utils
//!
//! `Utils` implements utilities functions to work with layouts
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use tui::layout::{Constraint, Direction, Layout, Rect};
/// ### draw_area_in
///
/// Draw an area (WxH / 3) in the middle of the parent area
pub fn draw_area_in(parent: Rect, width: u16, height: u16) -> Rect {
let new_area = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage((100 - height) / 2),
Constraint::Percentage(height),
Constraint::Percentage((100 - height) / 2),
]
.as_ref(),
)
.split(parent);
Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Percentage((100 - width) / 2),
Constraint::Percentage(width),
Constraint::Percentage((100 - width) / 2),
]
.as_ref(),
)
.split(new_area[1])[1]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ui_layout_utils_draw_area_in() {
let area: Rect = Rect::new(0, 0, 1024, 512);
let child: Rect = draw_area_in(area, 75, 30);
assert_eq!(child.x, 43);
assert_eq!(child.y, 63);
assert_eq!(child.width, 271);
assert_eq!(child.height, 54);
}
}

View File

@@ -1,461 +0,0 @@
//! ## View
//!
//! `View` is the module which handles layout components
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// imports
use super::{Canvas, Component, InputEvent, Msg, Payload, Props, PropsBuilder, Rect};
// ext
use std::collections::HashMap;
/// ## View
///
/// View is the wrapper and manager for all the components.
/// A View is a container for all the components in a certain layout.
/// Each View can have only one focused component.
pub struct View {
components: HashMap<String, Box<dyn Component>>, // all the components in the view
focus: Option<String>, // Current active component
focus_stack: Vec<String>, // Focus stack; used to give focus in case the current element loses focus
}
// -- view
impl View {
/// ### init
///
/// Initialize a new `View`
pub fn init() -> Self {
View {
components: HashMap::new(),
focus: None,
focus_stack: Vec::new(),
}
}
// -- mount / umount
/// ### mount
///
/// Mount a new component in the view
pub fn mount(&mut self, id: &str, component: Box<dyn Component>) {
self.components.insert(id.to_string(), component);
}
/// ### umount
///
/// Umount a component from the view.
/// If component has focus, blur component and remove it from the stack
pub fn umount(&mut self, id: &str) {
// Check if component has focus
if let Some(focus) = self.focus.as_ref() {
// If has focus, blur component
if focus == id {
self.blur();
}
}
// Remove component from focus stack
self.pop_from_stack(id);
self.components.remove(id);
}
// -- render
/// ### render
///
/// RenderData component with the provided id
#[cfg(not(tarpaulin_include))]
pub fn render(&self, id: &str, frame: &mut Canvas, area: Rect) {
if let Some(component) = self.components.get(id) {
component.render(frame, area);
}
}
// -- props
/// ### get_props
///
/// Get component properties
pub fn get_props(&self, id: &str) -> Option<PropsBuilder> {
self.components.get(id).map(|cmp| cmp.get_props())
}
/// update
///
/// Update component properties
/// Returns `None` if component doesn't exist
pub fn update(&mut self, id: &str, props: Props) -> Option<(String, Msg)> {
self.components
.get_mut(id)
.map(|cmp| (id.to_string(), cmp.update(props)))
}
// -- state
/// ### get_value
///
/// Get component value
pub fn get_value(&self, id: &str) -> Option<Payload> {
self.components.get(id).map(|cmp| cmp.get_value())
}
// -- events
/// ### on
///
/// Handle event for the focused component (if any)
/// Returns `None` if no component is focused
pub fn on(&mut self, ev: InputEvent) -> Option<(String, Msg)> {
match self.focus.as_ref() {
None => None,
Some(id) => self
.components
.get_mut(id)
.map(|cmp| (id.to_string(), cmp.on(ev))),
}
}
// -- focus
/// ### blur
///
/// Blur selected element AND DON'T PUSH CURRENT ACTIVE ELEMENT INTO THE STACK
/// Last element in stack becomes active and is removed from the stack
pub fn blur(&mut self) {
if let Some(component) = self.focus.take() {
// Blur component
if let Some(cmp) = self.components.get_mut(component.as_str()) {
cmp.blur();
}
// Set last element in the stack as active
let mut new: Option<String> = None;
if let Some(last) = self.focus_stack.last() {
// Set focus to last element
new = Some(last.clone());
self.focus = Some(last.clone());
// Active
if let Some(new) = self.components.get_mut(last) {
new.active();
}
}
// Pop element from stack
if let Some(new) = new {
self.pop_from_stack(new.as_str());
}
}
}
/// ### active
///
/// Active provided element
/// Current active component, if any, GETS PUSHED to the STACK
pub fn active(&mut self, component: &str) {
// Active component if exists
if let Some(cmp) = self.components.get_mut(component) {
// Active component
cmp.active();
// Put current focus if any, into the stack
if let Some(active_component) = self.focus.take() {
if active_component != component {
// Blur active component if are different
if let Some(active_component) =
self.components.get_mut(active_component.as_str())
{
active_component.blur();
}
}
self.push_to_stack(active_component.as_str());
}
// Give focus to component
self.focus = Some(component.to_string());
// Remove new component from the stack
self.pop_from_stack(component);
}
}
// -- private
/// ### push_to_stack
///
/// Push component to stack; first remove it from the stack if any
fn push_to_stack(&mut self, name: &str) {
self.pop_from_stack(name);
self.focus_stack.push(name.to_string());
}
/// ### pop_from_stack
///
/// Pop element from focus stack
fn pop_from_stack(&mut self, name: &str) {
self.focus_stack.retain(|c| c.as_str() != name);
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::components::input::Input;
use super::super::components::text::Text;
use super::super::props::{PropValue, TextParts, TextSpan};
use crossterm::event::{KeyCode, KeyEvent};
#[test]
fn test_ui_layout_view_init() {
let view: View = View::init();
// Verify view
assert_eq!(view.components.len(), 0);
assert!(view.focus.is_none());
assert_eq!(view.focus_stack.len(), 0);
}
#[test]
fn test_ui_layout_view_mount_umount() {
let mut view: View = View::init();
// Mount component
let input: &str = "INPUT";
view.mount(input, make_component_input());
// Verify is mounted
assert!(view.components.get(input).is_some());
// Mount another
let text: &str = "TEXT";
view.mount(text, make_component_text());
assert!(view.components.get(text).is_some());
assert_eq!(view.components.len(), 2);
// Verify you cannot have duplicates
view.mount(input, make_component_input());
assert_eq!(view.components.len(), 2); // length should still be 2
// Umount
view.umount(text);
assert_eq!(view.components.len(), 1);
assert!(view.components.get(text).is_none());
}
/*
#[test]
fn test_ui_layout_view_mount_render() {
let mut view: View = View::init();
// Mount component
let input: &str = "INPUT";
view.mount(input, make_component_input());
assert!(view.render(input).is_some());
assert!(view.render("unexisting").is_none());
}
*/
#[test]
fn test_ui_layout_view_focus() {
let mut view: View = View::init();
// Prepare ids
let input1: &str = "INPUT_1";
let input2: &str = "INPUT_2";
let input3: &str = "INPUT_3";
let text1: &str = "TEXT_1";
let text2: &str = "TEXT_2";
// Mount components
view.mount(input1, make_component_input());
view.mount(input2, make_component_input());
view.mount(input3, make_component_input());
view.mount(text1, make_component_text());
view.mount(text2, make_component_text());
// Verify focus
assert!(view.focus.is_none());
assert_eq!(view.focus_stack.len(), 0);
// Blur when nothing is selected
view.blur();
assert!(view.focus.is_none());
assert_eq!(view.focus_stack.len(), 0);
// Active unexisting component
view.active("UNEXISTING-COMPONENT");
assert!(view.focus.is_none());
assert_eq!(view.focus_stack.len(), 0);
// Give focus to a component
view.active(input1);
// Check focus
assert_eq!(view.focus.as_ref().unwrap().as_str(), input1);
assert_eq!(view.focus_stack.len(), 0); // NOTE: stack is empty until a focus gets blurred
// Active a new component
view.active(input2);
// Now focus should be on input2, but input 1 should be in the focus stack
assert_eq!(view.focus.as_ref().unwrap().as_str(), input2);
assert_eq!(view.focus_stack.len(), 1);
assert_eq!(view.focus_stack[0].as_str(), input1);
// Active input 3
view.active(input3);
// now focus should be hold by input3, and stack should have len 2
assert_eq!(view.focus.as_ref().unwrap().as_str(), input3);
assert_eq!(view.focus_stack.len(), 2);
assert_eq!(view.focus_stack[0].as_str(), input1);
assert_eq!(view.focus_stack[1].as_str(), input2);
// blur
view.blur();
// Focus should now be hold by input2; input 3 should NOT be in the stack
assert_eq!(view.focus.as_ref().unwrap().as_str(), input2);
assert_eq!(view.focus_stack.len(), 1);
assert_eq!(view.focus_stack[0].as_str(), input1);
// Active twice
view.active(input2);
// Nothing should have changed
assert_eq!(view.focus.as_ref().unwrap().as_str(), input2);
assert_eq!(view.focus_stack.len(), 1);
assert_eq!(view.focus_stack[0].as_str(), input1);
// Blur again; stack should become empty, whether focus should then be hold by input 1
view.blur();
assert_eq!(view.focus.as_ref().unwrap().as_str(), input1);
assert_eq!(view.focus_stack.len(), 0);
// Blur again; now everything should be none
view.blur();
assert!(view.focus.is_none());
assert_eq!(view.focus_stack.len(), 0);
}
#[test]
fn test_ui_layout_view_focus_umount() {
let mut view: View = View::init();
// Mount component
let input: &str = "INPUT";
let text: &str = "TEXT";
let text2: &str = "TEXT2";
view.mount(input, make_component_input());
view.mount(text, make_component_text());
view.mount(text2, make_component_text());
// Give focus to input
view.active(input);
// Give focus to text
view.active(text);
view.active(text2);
// Stack should have 1 element
assert_eq!(view.focus_stack.len(), 2);
// Focus should be some
assert!(view.focus.is_some());
// Umount text
view.umount(text2);
// Focus should now be hold by 'text'; stack should now have size 1
assert_eq!(view.focus.as_ref().unwrap(), text);
assert_eq!(view.focus_stack.len(), 1);
// Umount input
view.umount(input);
assert_eq!(view.focus.as_ref().unwrap(), text);
assert_eq!(view.focus_stack.len(), 0);
// Umount text
view.umount(text);
assert!(view.focus.is_none());
assert_eq!(view.focus_stack.len(), 0);
}
#[test]
fn test_ui_layout_view_update() {
let mut view: View = View::init();
// Prepare ids
let text: &str = "TEXT";
// Mount text
view.mount(text, make_component_text());
// Get properties and update
let props: Props = view.get_props(text).unwrap().build();
// Verify bold is false
assert_eq!(props.bold, false);
// Update properties and set bold to true
let mut builder = view.get_props(text).unwrap();
let (id, msg) = view.update(text, builder.bold().build()).unwrap();
// Verify return values
assert_eq!(id, text);
assert_eq!(msg, Msg::None);
// Verify bold is now true
let props: Props = view.get_props(text).unwrap().build();
// Verify bold is false
assert_eq!(props.bold, true);
// Get properties for unexisting component
assert!(view.update("foobar", props).is_none());
}
#[test]
fn test_ui_layout_view_on() {
let mut view: View = View::init();
// Prepare ids
let text: &str = "TEXT";
let input: &str = "INPUT";
// Mount
view.mount(text, make_component_text());
view.mount(input, make_component_input());
// Verify current value
assert_eq!(view.get_value(text).unwrap(), Payload::None); // Text value is Nothing
assert_eq!(
view.get_value(input).unwrap(),
Payload::Text(String::from("text"))
); // Defined in `make_component_input`
// Handle events WITHOUT ANY ACTIVE ELEMENT
assert!(view
.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter)))
.is_none());
// Active input
view.active(input);
// Now handle events on input
// Try char
assert_eq!(
view.on(InputEvent::Key(KeyEvent::from(KeyCode::Char('1'))))
.unwrap(),
(input.to_string(), Msg::None)
);
// Verify new value
assert_eq!(
view.get_value(input).unwrap(),
Payload::Text(String::from("text1"))
);
// Verify enter
assert_eq!(
view.on(InputEvent::Key(KeyEvent::from(KeyCode::Enter)))
.unwrap(),
(
input.to_string(),
Msg::OnSubmit(Payload::Text(String::from("text1")))
)
);
}
/// ### make_component
///
/// Make a new component; we'll use Input, which uses a rich set of events
fn make_component_input() -> Box<dyn Component> {
Box::new(Input::new(
PropsBuilder::default()
.with_texts(TextParts::new(Some(String::from("mytext")), None))
.with_value(PropValue::Str(String::from("text")))
.build(),
))
}
fn make_component_text() -> Box<dyn Component> {
Box::new(Text::new(
PropsBuilder::default()
.with_texts(TextParts::new(
None,
Some(vec![TextSpan::from("Sample text")]),
))
.build(),
))
}
}

View File

@@ -27,7 +27,7 @@
*/
// Modules
pub mod activities;
pub(crate) mod components;
pub mod context;
pub(crate) mod input;
pub(crate) mod layout;
pub(crate) mod store;