diff --git a/CHANGELOG.md b/CHANGELOG.md index e6e7b9e..b1580e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ Released on FIXME: ?? +- Bugfix: + - Fixed wrong text wrap in log box +- Dependencies: + - Added `tui-realm 0.1.0` + - Removed `tui` + ## 0.4.2 Released on 13/04/2021 diff --git a/Cargo.lock b/Cargo.lock index 56676e5..0ef3554 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,18 +40,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "arrayref" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - [[package]] name = "autocfg" version = "1.0.1" @@ -70,17 +58,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -[[package]] -name = "blake2b_simd" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" -dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq", -] - [[package]] name = "block-buffer" version = "0.9.0" @@ -186,12 +163,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - [[package]] name = "content_inspector" version = "0.2.4" @@ -241,24 +212,13 @@ checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" [[package]] name = "crc-any" -version = "2.3.5" +version = "2.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3784befdf9469f4d51c69ef0b774f6a99de6bcc655285f746f16e0dd63d9007" +checksum = "0d98be01088633be44a2a82b55a96dca49b226d65297428a3c44d33de07528ff" dependencies = [ "debug-helper", ] -[[package]] -name = "crossbeam-utils" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" -dependencies = [ - "autocfg", - "cfg-if 1.0.0", - "lazy_static", -] - [[package]] name = "crossterm" version = "0.18.2" @@ -330,9 +290,9 @@ dependencies = [ [[package]] name = "debug-helper" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a5bb894f24f42c247f19b25928a88e31867c0f84552c05df41a9dd527435e" +checksum = "4460596867846f73bddca51f7403b6a29f5315125be10a1640259b4db5b9494c" [[package]] name = "des" @@ -356,18 +316,18 @@ dependencies = [ [[package]] name = "dirs" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" dependencies = [ "libc", "redox_users", @@ -501,9 +461,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ "matches", "unicode-bidi", @@ -554,9 +514,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714" +checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" [[package]] name = "libssh2-sys" @@ -613,9 +573,9 @@ dependencies = [ [[package]] name = "magic-crypt" -version = "3.1.7" +version = "3.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a7d8d3790b76ab76cc459a707e09009fcd8ef8da8999d7a99c9bb9b9bef8890" +checksum = "d3c94f1281833c690f81e6a00c545063b4f034509d3af6d29b58d48e39aa64c9" dependencies = [ "aes-soft", "base64", @@ -871,7 +831,7 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.2.5", + "redox_syscall 0.2.6", "smallvec", "winapi", ] @@ -1007,29 +967,28 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041" dependencies = [ "bitflags", ] [[package]] name = "redox_users" -version = "0.3.5" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ - "getrandom 0.1.16", - "redox_syscall 0.1.57", - "rust-argon2", + "getrandom 0.2.2", + "redox_syscall 0.2.6", ] [[package]] name = "regex" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" +checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" dependencies = [ "aho-corasick", "memchr", @@ -1076,23 +1035,11 @@ dependencies = [ "winapi", ] -[[package]] -name = "rust-argon2" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" -dependencies = [ - "base64", - "blake2b_simd", - "constant_time_eq", - "crossbeam-utils", -] - [[package]] name = "rustls" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ "base64", "log", @@ -1125,9 +1072,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "sct" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ "ring", "untrusted", @@ -1297,9 +1244,9 @@ checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" [[package]] name = "syn" -version = "1.0.68" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87" +checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883" dependencies = [ "proc-macro2", "quote", @@ -1315,7 +1262,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "rand 0.8.3", - "redox_syscall 0.2.5", + "redox_syscall 0.2.6", "remove_dir_all", "winapi", ] @@ -1347,7 +1294,7 @@ dependencies = [ "textwrap", "thiserror", "toml", - "tui", + "tuirealm", "ureq", "users", "whoami", @@ -1408,9 +1355,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023" +checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" dependencies = [ "tinyvec_macros", ] @@ -1443,6 +1390,18 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "tuirealm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feb016a1b4aa98d5488cd109d568c31ef0f3f34196b6f3b551deefab01443f83" +dependencies = [ + "crossterm 0.19.0", + "textwrap", + "tui", + "unicode-width", +] + [[package]] name = "typenum" version = "1.13.0" @@ -1451,9 +1410,9 @@ checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" [[package]] name = "unicode-bidi" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" dependencies = [ "matches", ] @@ -1533,9 +1492,9 @@ dependencies = [ [[package]] name = "vcpkg" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" +checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d" [[package]] name = "version_check" @@ -1650,9 +1609,9 @@ dependencies = [ [[package]] name = "whoami" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e296f550993cba2c5c3eba5da0fb335562b2fa3d97b7a8ac9dc91f40a3abc70" +checksum = "4abacf325c958dfeaf1046931d37f2a901b6dfe0968ee965a29e94c6766b2af6" dependencies = [ "wasm-bindgen", "web-sys", @@ -1660,9 +1619,9 @@ dependencies = [ [[package]] name = "wildmatch" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ae7ce410f81ba679081aac1d4874f3b1c328535b630209aa5b4cdaaf895e20" +checksum = "d6c48bd20df7e4ced539c12f570f937c6b4884928a87fee70a479d72f031d4e0" [[package]] name = "winapi" diff --git a/Cargo.toml b/Cargo.toml index d0bdcb2..c18db0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ tempfile = "3.1.0" textwrap = "0.13.4" thiserror = "^1.0.0" toml = "0.5.8" +tuirealm = { version = "0.1.0", features = [ "with-components" ] } whoami = "1.1.1" wildmatch = "2.0.0" @@ -57,11 +58,6 @@ version = "^4.0.2" features = ["derive"] version = "^1.0.0" -[dependencies.tui] -default-features = false -features = ["crossterm"] -version = "0.14.0" - [dependencies.ureq] features = ["json"] version = "2.1.0" diff --git a/README.md b/README.md index 6e0ff91..5ef796f 100644 --- a/README.md +++ b/README.md @@ -426,6 +426,7 @@ TermSCP is powered by these aweseome projects: - [ssh2-rs](https://github.com/alexcrichton/ssh2-rs) - [textwrap](https://github.com/mgeisler/textwrap) - [tui-rs](https://github.com/fdehau/tui-rs) +- [tui-realm](https://github.com/veeso/tui-realm) - [whoami](https://github.com/libcala/whoami) --- diff --git a/src/ui/activities/auth_activity/bookmarks.rs b/src/ui/activities/auth_activity/bookmarks.rs index 6e131d7..ef7edc3 100644 --- a/src/ui/activities/auth_activity/bookmarks.rs +++ b/src/ui/activities/auth_activity/bookmarks.rs @@ -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 = 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, ) { // 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); } } diff --git a/src/ui/activities/auth_activity/mod.rs b/src/ui/activities/auth_activity/mod.rs index f190e25..6ab7b60 100644 --- a/src/ui/activities/auth_activity/mod.rs +++ b/src/ui/activities/auth_activity/mod.rs @@ -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"; diff --git a/src/ui/activities/auth_activity/update.rs b/src/ui/activities/auth_activity/update.rs index bc789d3..6e3569e 100644 --- a/src/ui/activities/auth_activity/update.rs +++ b/src/ui/activities/auth_activity/update.rs @@ -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 { } // 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 diff --git a/src/ui/activities/auth_activity/view.rs b/src/ui/activities/auth_activity/view.rs index 18a7065..0718027 100644 --- a/src/ui/activities/auth_activity/view.rs +++ b/src/ui/activities/auth_activity/view.rs @@ -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("") - .bold() - .with_foreground(Color::Cyan) - .build(), - TextSpanBuilder::new(" to show keybindings; ") - .bold() - .build(), - TextSpanBuilder::new("") - .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("") + .bold() + .with_foreground(Color::Cyan) + .build(), + TextSpanBuilder::new(" to show keybindings; ") + .bold() + .build(), + TextSpanBuilder::new("") + .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 ", version))]))) + .with_spans( + vec![ + TextSpan::from("TermSCP "), + TextSpanBuilder::new(version).underlined().bold().build(), + TextSpan::from(" is now available! Download it from ") + ] + ) .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 = self + let bookmarks: Vec = 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 = self + let bookmarks: Vec = 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(), }; diff --git a/src/ui/activities/filetransfer_activity/actions.rs b/src/ui/activities/filetransfer_activity/actions.rs index ebbb778..72bf039 100644 --- a/src/ui/activities/filetransfer_activity/actions.rs +++ b/src/ui/activities/filetransfer_activity/actions.rs @@ -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 { - 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 { - 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, } diff --git a/src/ui/activities/filetransfer_activity/mod.rs b/src/ui/activities/filetransfer_activity/mod.rs index 6831d16..26f7545 100644 --- a/src/ui/activities/filetransfer_activity/mod.rs +++ b/src/ui/activities/filetransfer_activity/mod.rs @@ -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 diff --git a/src/ui/activities/filetransfer_activity/update.rs b/src/ui/activities/filetransfer_activity/update.rs index 62e971a..fcbba61 100644 --- a/src/ui/activities/filetransfer_activity/update.rs +++ b/src/ui/activities/filetransfer_activity/update.rs @@ -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 = self + let files: Vec = 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 = self + let files: Vec = 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 = self + let files: Vec = 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) } diff --git a/src/ui/activities/filetransfer_activity/view.rs b/src/ui/activities/filetransfer_activity/view.rs index 15269a2..84d2f58 100644 --- a/src/ui/activities/filetransfer_activity/view.rs +++ b/src/ui/activities/filetransfer_activity/view.rs @@ -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(), )), ); diff --git a/src/ui/activities/keymap.rs b/src/ui/activities/keymap.rs index 84425c0..4de26bc 100644 --- a/src/ui/activities/keymap.rs +++ b/src/ui/activities/keymap.rs @@ -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 diff --git a/src/ui/activities/setup_activity/actions.rs b/src/ui/activities/setup_activity/actions.rs index db7a45a..52369e9 100644 --- a/src/ui/activities/setup_activity/actions.rs +++ b/src/ui/activities/setup_activity/actions.rs @@ -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 = match self.view.get_value(super::COMPONENT_LIST_SSH_KEYS) { + let idx: Option = 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(), }; diff --git a/src/ui/activities/setup_activity/mod.rs b/src/ui/activities/setup_activity/mod.rs index 6bea45f..0ec7d00 100644 --- a/src/ui/activities/setup_activity/mod.rs +++ b/src/ui/activities/setup_activity/mod.rs @@ -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"; diff --git a/src/ui/activities/setup_activity/update.rs b/src/ui/activities/setup_activity/update.rs index 39e699c..036907b 100644 --- a/src/ui/activities/setup_activity/update.rs +++ b/src/ui/activities/setup_activity/update.rs @@ -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 diff --git a/src/ui/activities/setup_activity/view.rs b/src/ui/activities/setup_activity/view.rs index a4176ac..d5d06d8 100644 --- a/src/ui/activities/setup_activity/view.rs +++ b/src/ui/activities/setup_activity/view.rs @@ -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("") - .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("") + .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("") - .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("") + .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 = 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 = cli + let keys: Vec = 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); } diff --git a/src/ui/layout/components/bookmark_list.rs b/src/ui/components/bookmark_list.rs similarity index 62% rename from src/ui/layout/components/bookmark_list.rs rename to src/ui/components/bookmark_list.rs index b64218f..02d35fb 100644 --- a/src/ui/layout/components/bookmark_list.rs +++ b/src/ui/components/bookmark_list.rs @@ -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, +} + +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 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, bookmarks: Vec) -> &mut Self { + if let Some(props) = self.props.as_mut() { + let bookmarks: Vec = 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 = match self.props.texts.rows.as_ref() { + let list_item: Vec = 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)) ); } diff --git a/src/ui/layout/components/file_list.rs b/src/ui/components/file_list.rs similarity index 63% rename from src/ui/layout/components/file_list.rs rename to src/ui/components/file_list.rs index ed3c094..314e1de 100644 --- a/src/ui/layout/components/file_list.rs +++ b/src/ui/components/file_list.rs @@ -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, +} + +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 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, files: Vec) -> &mut Self { + if let Some(props) = self.props.as_mut() { + let files: Vec = 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 = match self.props.texts.rows.as_ref() { + let list_item: Vec = 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)) ); } diff --git a/src/ui/layout/components/logbox.rs b/src/ui/components/logbox.rs similarity index 65% rename from src/ui/layout/components/logbox.rs rename to src/ui/components/logbox.rs index e785a41..7e2924f 100644 --- a/src/ui/layout/components/logbox.rs +++ b/src/ui/components/logbox.rs @@ -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, +} + +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 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, 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 = 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 = Vec::with_capacity(columns.len() - 4); - // Get first row - let mut first_row: Vec = 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)) ); } diff --git a/src/ui/layout/components/mod.rs b/src/ui/components/mod.rs similarity index 86% rename from src/ui/layout/components/mod.rs rename to src/ui/components/mod.rs index 297d16d..2a8fb31 100644 --- a/src/ui/layout/components/mod.rs +++ b/src/ui/components/mod.rs @@ -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; diff --git a/src/ui/layout/components/msgbox.rs b/src/ui/components/msgbox.rs similarity index 50% rename from src/ui/layout/components/msgbox.rs rename to src/ui/components/msgbox.rs index bee1260..e61d5f7 100644 --- a/src/ui/layout/components/msgbox.rs +++ b/src/ui/components/msgbox.rs @@ -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, +} + +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 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, texts: Vec) -> &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 = match self.props.texts.rows.as_ref() { + let lines: Vec = match self.props.texts.spans.as_ref() { None => Vec::new(), Some(rows) => { let mut lines: Vec = 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("") .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)) ); } diff --git a/src/ui/context.rs b/src/ui/context.rs index 3c7486b..327761f 100644 --- a/src/ui/context.rs +++ b/src/ui/context.rs @@ -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 /// diff --git a/src/ui/layout/components/input.rs b/src/ui/layout/components/input.rs deleted file mode 100644 index 632f349..0000000 --- a/src/ui/layout/components/input.rs +++ /dev/null @@ -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, // 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) { - // 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::().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); - } -} diff --git a/src/ui/layout/components/progress_bar.rs b/src/ui/layout/components/progress_bar.rs deleted file mode 100644 index 585f142..0000000 --- a/src/ui/layout/components/progress_bar.rs +++ /dev/null @@ -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)) - ); - } -} diff --git a/src/ui/layout/components/radio_group.rs b/src/ui/layout/components/radio_group.rs deleted file mode 100644 index 7132425..0000000 --- a/src/ui/layout/components/radio_group.rs +++ /dev/null @@ -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, // 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 = 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'))), - ); - } -} diff --git a/src/ui/layout/components/table.rs b/src/ui/layout/components/table.rs deleted file mode 100644 index 9062307..0000000 --- a/src/ui/layout/components/table.rs +++ /dev/null @@ -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 = match self.props.texts.table.as_ref() { - None => Vec::new(), - Some(table) => table - .iter() - .map(|row| { - let columns: Vec = row - .iter() - .map(|col| { - Span::styled( - col.content.clone(), - Style::default() - .add_modifier(col.get_modifiers()) - .fg(col.fg) - .bg(col.bg), - ) - }) - .collect(); - ListItem::new(Spans::from(columns)) - }) - .collect(), // Make List item from TextSpan - }; - // Make list - 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)) - ); - } -} diff --git a/src/ui/layout/components/text.rs b/src/ui/layout/components/text.rs deleted file mode 100644 index 8aff07b..0000000 --- a/src/ui/layout/components/text.rs +++ /dev/null @@ -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 = 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("") - .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)) - ); - } -} diff --git a/src/ui/layout/components/title.rs b/src/ui/layout/components/title.rs deleted file mode 100644 index 52e3091..0000000 --- a/src/ui/layout/components/title.rs +++ /dev/null @@ -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)) - ); - } -} diff --git a/src/ui/layout/mod.rs b/src/ui/layout/mod.rs deleted file mode 100644 index 9151c3f..0000000 --- a/src/ui/layout/mod.rs +++ /dev/null @@ -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; -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); -} diff --git a/src/ui/layout/props.rs b/src/ui/layout/props.rs deleted file mode 100644 index cc1a50c..0000000 --- a/src/ui/layout/props.rs +++ /dev/null @@ -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, // 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, -} - -#[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 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>; - -/// ## TextParts -/// -/// TextParts holds optional component for the text displayed by a component -#[derive(Clone)] -pub struct TextParts { - pub title: Option, - pub rows: Option>, - pub table: Option, // First vector is rows, inner vec is column -} - -impl TextParts { - /// ### new - /// - /// Instantiates a new TextParts entity - pub fn new(title: Option, rows: Option>) -> Self { - TextParts { - title, - rows, - table: None, - } - } - - /// ### table - /// - /// Instantiates a new TextParts as a Table - pub fn table(title: Option, 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
, -} - -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 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, -} - -#[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)); - } -} diff --git a/src/ui/layout/view.rs b/src/ui/layout/view.rs deleted file mode 100644 index ed8a3d9..0000000 --- a/src/ui/layout/view.rs +++ /dev/null @@ -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>, // all the components in the view - focus: Option, // Current active component - focus_stack: Vec, // 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) { - 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 { - 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 { - 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 = 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 { - 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 { - Box::new(Text::new( - PropsBuilder::default() - .with_texts(TextParts::new( - None, - Some(vec![TextSpan::from("Sample text")]), - )) - .build(), - )) - } -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 2b23a1b..275a0b6 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -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; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 3daed62..d3028c6 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -31,3 +31,4 @@ pub mod fmt; pub mod git; pub mod parser; pub mod random; +pub mod ui; diff --git a/src/ui/layout/utils.rs b/src/utils/ui.rs similarity index 95% rename from src/ui/layout/utils.rs rename to src/utils/ui.rs index 6968d00..9d7f60a 100644 --- a/src/ui/layout/utils.rs +++ b/src/utils/ui.rs @@ -25,7 +25,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -use tui::layout::{Constraint, Direction, Layout, Rect}; +use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect}; /// ### draw_area_in /// @@ -61,7 +61,7 @@ mod tests { use super::*; #[test] - fn test_ui_layout_utils_draw_area_in() { + fn test_utils_ui_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);