From 55f74a82441d66429474f9177aa67040977a6bb3 Mon Sep 17 00:00:00 2001 From: veeso Date: Sat, 6 Mar 2021 20:34:32 +0100 Subject: [PATCH] TextSpan instead of strings --- src/ui/layout/components/file_list.rs | 18 ++-- src/ui/layout/components/radio_group.rs | 23 +++-- src/ui/layout/props.rs | 116 +++++++++++++++++++++--- 3 files changed, 126 insertions(+), 31 deletions(-) diff --git a/src/ui/layout/components/file_list.rs b/src/ui/layout/components/file_list.rs index d270726..3dd1ada 100644 --- a/src/ui/layout/components/file_list.rs +++ b/src/ui/layout/components/file_list.rs @@ -118,7 +118,7 @@ impl FileList { // Initialize states let mut states: OwnStates = OwnStates::default(); // Set list length - states.set_list_len(match &props.texts.body { + states.set_list_len(match &props.texts.rows { Some(tokens) => tokens.len(), None => 0, }); @@ -136,11 +136,11 @@ impl Component for FileList { false => None, true => { // Make list - let list_item: Vec = match self.props.texts.body.as_ref() { + let list_item: Vec = match self.props.texts.rows.as_ref() { None => vec![], Some(lines) => lines .iter() - .map(|line: &String| ListItem::new(Span::from(line.to_string()))) + .map(|line| ListItem::new(Span::from(line.content.to_string()))) .collect(), }; let (fg, bg): (Color, Color) = match self.states.focus { @@ -186,7 +186,7 @@ impl Component for FileList { fn update(&mut self, props: Props) -> Msg { self.props = props; // re-Set list length - self.states.set_list_len(match &self.props.texts.body { + self.states.set_list_len(match &self.props.texts.rows { Some(tokens) => tokens.len(), None => 0, }); @@ -287,7 +287,7 @@ impl Component for FileList { mod tests { use super::*; - use crate::ui::layout::props::TextParts; + use crate::ui::layout::props::{TextParts, TextSpan}; use crossterm::event::KeyEvent; @@ -298,7 +298,7 @@ mod tests { PropsBuilder::default() .with_texts(TextParts::new( Some(String::from("filelist")), - Some(vec![String::from("file1"), String::from("file2")]), + Some(vec![TextSpan::from("file1"), TextSpan::from("file2")]), )) .build(), ); @@ -323,9 +323,9 @@ mod tests { .with_texts(TextParts::new( Some(String::from("filelist")), Some(vec![ - String::from("file1"), - String::from("file2"), - String::from("file3"), + TextSpan::from("file1"), + TextSpan::from("file2"), + TextSpan::from("file3"), ]), )) .build(), diff --git a/src/ui/layout/components/radio_group.rs b/src/ui/layout/components/radio_group.rs index e53caae..abe7e78 100644 --- a/src/ui/layout/components/radio_group.rs +++ b/src/ui/layout/components/radio_group.rs @@ -24,6 +24,7 @@ */ // locals +use super::super::props::TextSpan; use super::{Component, InputEvent, Msg, Payload, PropValue, Props, PropsBuilder, Render}; // ext use crossterm::event::KeyCode; @@ -73,6 +74,13 @@ impl OwnStates { self.choice -= 1; } } + + /// ### make_choices + /// + /// Set OwnStates choices from a vector of text spans + pub fn make_choices(&mut self, spans: &Vec) { + self.choices = spans.iter().map(|x| x.content.clone()).collect(); + } } // -- component @@ -92,8 +100,8 @@ impl RadioGroup { pub fn new(props: Props) -> Self { // Make states let mut states: OwnStates = OwnStates::default(); - // Update choices - states.choices = props.texts.body.clone().unwrap_or(Vec::new()); + // 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; @@ -164,7 +172,8 @@ impl Component for RadioGroup { /// Returns a Msg to the view fn update(&mut self, props: Props) -> Msg { // Reset choices - self.states.choices = props.texts.body.clone().unwrap_or(Vec::new()); + 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; @@ -256,7 +265,7 @@ impl Component for RadioGroup { mod tests { use super::*; - use crate::ui::layout::props::TextParts; + use crate::ui::layout::props::{TextParts, TextSpan}; use crossterm::event::KeyEvent; @@ -268,9 +277,9 @@ mod tests { .with_texts(TextParts::new( Some(String::from("yes or no?")), Some(vec![ - String::from("Yes!"), - String::from("No"), - String::from("Maybe"), + TextSpan::from("Yes!"), + TextSpan::from("No"), + TextSpan::from("Maybe"), ]), )) .with_value(PropValue::Unsigned(1)) diff --git a/src/ui/layout/props.rs b/src/ui/layout/props.rs index 6c506af..91b912a 100644 --- a/src/ui/layout/props.rs +++ b/src/ui/layout/props.rs @@ -235,15 +235,15 @@ impl Default for PropsBuilder { #[derive(Clone)] pub struct TextParts { pub title: Option, - pub body: Option>, + pub rows: Option>, } impl TextParts { /// ### new /// /// Instantiates a new TextParts entity - pub fn new(title: Option, body: Option>) -> Self { - TextParts { title, body } + pub fn new(title: Option, rows: Option>) -> Self { + TextParts { title, rows } } } @@ -251,7 +251,46 @@ impl Default for TextParts { fn default() -> Self { TextParts { title: None, - body: None, + rows: None, + } + } +} + +/// ### TextSpan +/// +/// TextSpan is a "cell" of text with its attributes +#[derive(Clone, std::fmt::Debug)] +pub struct TextSpan { + pub content: String, + pub color: Color, + pub bold: bool, + pub italic: bool, + pub underlined: bool, +} + +impl TextSpan { + /// ### new + /// + /// Instantiates a new TextSpan with all the attributes + pub fn new(content: String, color: Color, bold: bool, italic: bool, underlined: bool) -> Self { + TextSpan { + content, + color, + bold, + italic, + underlined, + } + } +} + +impl From<&str> for TextSpan { + fn from(txt: &str) -> Self { + TextSpan { + content: txt.to_string(), + color: Color::Reset, + bold: false, + italic: false, + underlined: false, } } } @@ -301,7 +340,7 @@ mod tests { assert_eq!(props.input_type, InputType::Text); assert!(props.input_len.is_none()); assert_eq!(props.value, PropValue::None); - assert!(props.texts.body.is_none()); + assert!(props.texts.rows.is_none()); } #[test] @@ -326,7 +365,7 @@ mod tests { .underlined() .with_texts(TextParts::new( Some(String::from("hello")), - Some(vec![String::from("hey")]), + Some(vec![TextSpan::from("hey")]), )) .with_input(InputType::Password) .with_input_len(16) @@ -345,7 +384,15 @@ mod tests { panic!("Expected value to be a string"); } assert_eq!( - props.texts.body.as_ref().unwrap().get(0).unwrap().as_str(), + props + .texts + .rows + .as_ref() + .unwrap() + .get(0) + .unwrap() + .content + .as_str(), "hey" ); assert_eq!(props.underlined, true); @@ -359,7 +406,7 @@ mod tests { .underlined() .with_texts(TextParts::new( Some(String::from("hello")), - Some(vec![String::from("hey")]), + Some(vec![TextSpan::from("hey")]), )) .build(); assert_eq!(props.background, Color::Blue); @@ -368,7 +415,15 @@ mod tests { assert_eq!(props.italic, true); assert_eq!(props.texts.title.as_ref().unwrap().as_str(), "hello"); assert_eq!( - props.texts.body.as_ref().unwrap().get(0).unwrap().as_str(), + props + .texts + .rows + .as_ref() + .unwrap() + .get(0) + .unwrap() + .content + .as_str(), "hey" ); assert_eq!(props.underlined, true); @@ -389,7 +444,7 @@ mod tests { .underlined() .with_texts(TextParts::new( Some(String::from("hello")), - Some(vec![String::from("hey")]), + Some(vec![TextSpan::from("hey")]), )); // Rebuild let _ = builder.build(); @@ -406,7 +461,7 @@ mod tests { .underlined() .with_texts(TextParts::new( Some(String::from("hello")), - Some(vec![String::from("hey")]), + Some(vec![TextSpan::from("hey")]), )) .build(); // Ok, now make a builder from properties @@ -418,15 +473,29 @@ mod tests { fn test_ui_layout_props_text_parts_with_values() { let parts: TextParts = TextParts::new( Some(String::from("Hello world!")), - Some(vec![String::from("row1"), String::from("row2")]), + Some(vec![TextSpan::from("row1"), TextSpan::from("row2")]), ); assert_eq!(parts.title.as_ref().unwrap().as_str(), "Hello world!"); assert_eq!( - parts.body.as_ref().unwrap().get(0).unwrap().as_str(), + parts + .rows + .as_ref() + .unwrap() + .get(0) + .unwrap() + .content + .as_str(), "row1" ); assert_eq!( - parts.body.as_ref().unwrap().get(1).unwrap().as_str(), + parts + .rows + .as_ref() + .unwrap() + .get(1) + .unwrap() + .content + .as_str(), "row2" ); } @@ -435,6 +504,23 @@ mod tests { fn test_ui_layout_props_text_parts_default() { let parts: TextParts = TextParts::default(); assert!(parts.title.is_none()); - assert!(parts.body.is_none()); + assert!(parts.rows.is_none()); + } + + #[test] + fn test_ui_layout_props_text_span() { + let span: TextSpan = TextSpan::from("Hello!"); + assert_eq!(span.content.as_str(), "Hello!"); + assert_eq!(span.bold, false); + assert_eq!(span.color, Color::Reset); + assert_eq!(span.italic, false); + assert_eq!(span.underlined, false); + // With attributes + let span: TextSpan = TextSpan::new(String::from("Error"), Color::Red, true, true, true); + assert_eq!(span.content.as_str(), "Error"); + assert_eq!(span.bold, true); + assert_eq!(span.color, Color::Red); + assert_eq!(span.italic, true); + assert_eq!(span.underlined, true); } }