diff --git a/src/ui/layout/components/text.rs b/src/ui/layout/components/text.rs new file mode 100644 index 0000000..156a820 --- /dev/null +++ b/src/ui/layout/components/text.rs @@ -0,0 +1,195 @@ +//! ## Text +//! +//! `Text` component renders a simple readonly no event associated text + +/* +* +* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com +* +* This file is part of "TermSCP" +* +* TermSCP is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* TermSCP is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with TermSCP. If not, see . +* +*/ + +// locals +use super::{Component, InputEvent, Msg, Payload, Props, PropsBuilder, Render}; +// ext +use tui::{ + style::Style, + text::{Span, Spans, Text as TuiText}, + widgets::Paragraph, +}; + +// NOTE: this component doesn't handle any state + +// -- 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, return a Widget instance for the Component + /// Returns None if the component is hidden + fn render(&self) -> Option { + // 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| { + Span::styled( + x.content.clone(), + Style::default() + .add_modifier(x.get_modifiers()) + .fg(x.fg) + .bg(x.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) + .bg(self.props.background), + ); + Some(Render { + widget: Box::new(Paragraph::new(text)), + cursor: 0, + }) + } else { + // Invisible + None + } + } + + /// ### 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 { + Msg::None + } + + /// ### get_value + /// + /// Get current value from component + /// For this component returns always None + fn get_value(&self) -> Payload { + Payload::None + } + + // -- events + + /// ### should_umount + /// + /// The component must provide to the supervisor whether it should be umounted (destroyed) + /// This makes sense to be called after an `on` or after an `update`, where the states changes. + /// Always false for this component + fn should_umount(&self) -> bool { + false + } + + /// ### blur + /// + /// Blur component; does nothing on this component + fn blur(&mut self) {} + + /// ### active + /// + /// Active component; does nothing on this 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(), + ); + // Focus + component.active(); + component.blur(); + // Should umount + assert_eq!(component.should_umount(), false); + // Get value + assert_eq!(component.get_value(), Payload::None); + // Render + assert_eq!(component.render().unwrap().cursor, 0); + // Event + assert_eq!( + component.on(InputEvent::Key(KeyEvent::from(KeyCode::Delete))), + Msg::None + ); + } +} diff --git a/src/ui/layout/props.rs b/src/ui/layout/props.rs index 2e165ac..61f6092 100644 --- a/src/ui/layout/props.rs +++ b/src/ui/layout/props.rs @@ -282,6 +282,27 @@ impl From<&str> for TextSpan { } } +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 @@ -593,5 +614,10 @@ mod tests { 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)); } }