diff --git a/src/ui/layout/components/file_list.rs b/src/ui/layout/components/file_list.rs index d1379b3..eb93d4c 100644 --- a/src/ui/layout/components/file_list.rs +++ b/src/ui/layout/components/file_list.rs @@ -24,7 +24,7 @@ */ // locals -use super::{Component, InputEvent, Msg, Payload, Props, PropsBuilder, Render, States}; +use super::{Component, InputEvent, Msg, Payload, Props, PropsBuilder, Render}; // ext use crossterm::event::KeyCode; use tui::{ @@ -97,8 +97,6 @@ impl OwnStates { } } -impl States for OwnStates {} - // -- Component /// ## FileList diff --git a/src/ui/layout/components/input.rs b/src/ui/layout/components/input.rs new file mode 100644 index 0000000..68118ce --- /dev/null +++ b/src/ui/layout/components/input.rs @@ -0,0 +1,302 @@ +//! ## Input +//! +//! `Input` component renders an input box + +/* +* +* 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::super::props::InputType; +use super::{Component, InputEvent, Msg, Payload, Props, PropsBuilder, Render}; +// ext +use crossterm::event::{KeyCode, KeyModifiers}; +use tui::{ + style::Style, + widgets::{Block, BorderType, Borders, Paragraph}, +}; + +// -- states + +/// ## OwnStates +/// +/// OwnStates contains states for this component +#[derive(Clone)] +struct OwnStates { + input: Vec, // Current input + cursor: usize, // Input position +} + +impl Default for OwnStates { + fn default() -> Self { + OwnStates { + input: Vec::new(), + cursor: 0, + } + } +} + +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) { + match itype { + InputType::Number => { + if ch.is_digit(10) { + // Must be digit + self.input.push(ch); + // Increment cursor + self.cursor += 1; + } + } + _ => { + // No rule + self.input.push(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.len() > 0 { + self.input.remove(self.cursor - 1); + // Decrement cursor + self.cursor -= 1; + } + } + + /// ### delete + /// + /// Delete element at cursor + pub fn delete(&mut self) { + if self.cursor + 1 < 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 + 1 < self.input.len() { + self.cursor += 1; + } + } + + /// ### 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 Some(val) = props.value.as_ref() { + for ch in val.chars() { + states.append(ch, props.input_type); + } + } + Input { props, states } + } +} + +impl Component for Input { + /// ### 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 { + 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.get_value()) + .style(match self.props.focus { + true => Style::default().fg(self.props.foreground), + false => Style::default(), + }) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title(title), + ); + Some(Render { + widget: Box::new(p), + cursor: self.states.cursor, + }) + } else { + 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; + // Don't reset value + 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 { + 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::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); + // 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::Unumber(self.states.get_value().parse::().ok().unwrap_or(0)) + } + _ => Payload::Text(self.states.get_value()), + } + } + + // -- 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. + /// This component never umounts + fn should_umount(&self) -> bool { + false + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::ui::layout::props::TextParts; + + use crossterm::event::KeyEvent; + + #[test] + fn test_ui_layout_components_input_text() { + + } + +} \ No newline at end of file diff --git a/src/ui/layout/components/mod.rs b/src/ui/layout/components/mod.rs index e30eb07..678fa28 100644 --- a/src/ui/layout/components/mod.rs +++ b/src/ui/layout/components/mod.rs @@ -24,7 +24,8 @@ */ // imports -use super::{Component, InputEvent, Msg, Payload, Props, PropsBuilder, Render, States}; +use super::{Component, InputEvent, Msg, Payload, Props, PropsBuilder, Render}; // exports pub mod file_list; +pub mod input; diff --git a/src/ui/layout/mod.rs b/src/ui/layout/mod.rs index d317cb4..f2add3b 100644 --- a/src/ui/layout/mod.rs +++ b/src/ui/layout/mod.rs @@ -68,14 +68,6 @@ pub struct Render { pub cursor: usize, // Cursor position } -// -- States - -/// ## States -/// -/// States is a trait which defines the behaviours for the states model for the different component. -/// A state contains internal values for each component. -pub(crate) trait States {} - // -- Component /// ## Component diff --git a/src/ui/layout/props.rs b/src/ui/layout/props.rs index 9d909f7..7d9816d 100644 --- a/src/ui/layout/props.rs +++ b/src/ui/layout/props.rs @@ -286,7 +286,7 @@ impl Default for TextParts { /// ## InputType /// /// Input type for text inputs -#[derive(Clone, PartialEq, std::fmt::Debug)] +#[derive(Clone, Copy, PartialEq, std::fmt::Debug)] pub enum InputType { Text, Number,