diff --git a/src/ui/layout/components/input.rs b/src/ui/layout/components/input.rs
index 084ff98..503a6ae 100644
--- a/src/ui/layout/components/input.rs
+++ b/src/ui/layout/components/input.rs
@@ -420,7 +420,7 @@ mod tests {
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'
+ component.on(InputEvent::Key(KeyEvent::from(KeyCode::Right))), // between 'e' and 'l'
Msg::None
);
assert_eq!(component.render().unwrap().cursor, 2); // Should increment
diff --git a/src/ui/layout/components/mod.rs b/src/ui/layout/components/mod.rs
index 743be25..2fbbf8c 100644
--- a/src/ui/layout/components/mod.rs
+++ b/src/ui/layout/components/mod.rs
@@ -29,3 +29,4 @@ use super::{Component, InputEvent, Msg, Payload, PropValue, Props, PropsBuilder,
// exports
pub mod file_list;
pub mod input;
+pub mod radio_group;
diff --git a/src/ui/layout/components/radio_group.rs b/src/ui/layout/components/radio_group.rs
new file mode 100644
index 0000000..0f379b1
--- /dev/null
+++ b/src/ui/layout/components/radio_group.rs
@@ -0,0 +1,335 @@
+//! ## RadioGroup
+//!
+//! `RadioGroup` component renders a radio group
+
+/*
+*
+* 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, PropValue, Props, PropsBuilder, Render};
+// ext
+use crossterm::event::KeyCode;
+use tui::{
+ style::{Color, Style},
+ text::Spans,
+ widgets::{Block, BorderType, Borders, 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;
+ }
+ }
+}
+
+// -- 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
+ states.choices = props.texts.body.clone().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, return a Widget instance for the Component
+ /// Returns None if the component is hidden
+ fn render(&self) -> Option {
+ match self.props.visible {
+ false => None,
+ true => {
+ // Make choices
+ let choices: Vec = self
+ .states
+ .choices
+ .iter()
+ .map(|x| Spans::from(x.clone()))
+ .collect();
+ // Make colors
+ let (bg, fg, block_fg): (Color, Color, Color) = match &self.states.focus {
+ true => (
+ self.props.foreground,
+ self.props.background,
+ self.props.foreground,
+ ),
+ false => (Color::Reset, Color::Reset, Color::Reset),
+ };
+ let title: String = match &self.props.texts.title {
+ Some(t) => t.clone(),
+ None => String::new(),
+ };
+ Some(Render {
+ cursor: 0,
+ widget: Box::new(
+ Tabs::new(choices)
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .border_type(BorderType::Rounded)
+ .style(Style::default().fg(block_fg))
+ .title(title),
+ )
+ .select(self.states.choice)
+ .style(Style::default())
+ .highlight_style(
+ Style::default()
+ .add_modifier(self.props.get_modifiers())
+ .fg(fg)
+ .bg(bg),
+ ),
+ ),
+ })
+ }
+ }
+ }
+
+ /// ### 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.choices = props.texts.body.clone().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_props(&self.props)
+ }
+
+ /// ### 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
+
+ /// ### 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; 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;
+
+ 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![
+ String::from("Yes!"),
+ String::from("No"),
+ String::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);
+ // Should umount
+ assert_eq!(component.should_umount(), false);
+ // Get value
+ assert_eq!(component.get_value(), Payload::Unsigned(1));
+ // Render
+ assert_eq!(component.render().unwrap().cursor, 0);
+ // 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/mod.rs b/src/ui/layout/mod.rs
index 6c55df4..dbe8f00 100644
--- a/src/ui/layout/mod.rs
+++ b/src/ui/layout/mod.rs
@@ -43,6 +43,7 @@ use tui::widgets::Widget;
#[derive(std::fmt::Debug, PartialEq)]
pub enum Msg {
OnSubmit(Payload),
+ OnChange(Payload),
OnKey(KeyEvent),
None,
}