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));
}
}