use super::props::{INPUT_INVALID_STYLE, INPUT_PLACEHOLDER, INPUT_PLACEHOLDER_STYLE};
use crate::utils::calc_utf8_cursor_position;
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::props::{
Alignment, AttrValue, Attribute, Borders, Color, InputType, Props, Style, TextModifiers,
};
use tuirealm::ratatui::{layout::Rect, widgets::Paragraph};
use tuirealm::{Frame, MockComponent, State, StateValue};
#[derive(Default)]
pub struct InputStates {
pub input: Vec<char>, pub cursor: usize, }
impl InputStates {
pub fn append(&mut self, ch: char, itype: &InputType, max_len: Option<usize>) {
if self.input.len() < max_len.unwrap_or(usize::MAX) {
if itype.char_valid(self.input.iter().collect::<String>().as_str(), ch) {
self.input.insert(self.cursor, ch);
self.incr_cursor();
}
}
}
pub fn backspace(&mut self) {
if self.cursor > 0 && !self.input.is_empty() {
self.input.remove(self.cursor - 1);
self.cursor -= 1;
}
}
pub fn delete(&mut self) {
if self.cursor < self.input.len() {
self.input.remove(self.cursor);
}
}
pub fn incr_cursor(&mut self) {
if self.cursor < self.input.len() {
self.cursor += 1;
}
}
pub fn cursor_at_begin(&mut self) {
self.cursor = 0;
}
pub fn cursor_at_end(&mut self) {
self.cursor = self.input.len();
}
pub fn decr_cursor(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
}
}
pub fn render_value(&self, itype: InputType) -> String {
self.render_value_chars(itype).iter().collect::<String>()
}
pub fn render_value_chars(&self, itype: InputType) -> Vec<char> {
match itype {
InputType::Password(ch) | InputType::CustomPassword(ch, _, _) => {
(0..self.input.len()).map(|_| ch).collect()
}
_ => self.input.clone(),
}
}
pub fn get_value(&self) -> String {
self.input.iter().collect()
}
}
#[derive(Default)]
pub struct Input {
props: Props,
pub states: InputStates,
}
impl Input {
pub fn foreground(mut self, fg: Color) -> Self {
self.attr(Attribute::Foreground, AttrValue::Color(fg));
self
}
pub fn background(mut self, bg: Color) -> Self {
self.attr(Attribute::Background, AttrValue::Color(bg));
self
}
pub fn inactive(mut self, s: Style) -> Self {
self.attr(Attribute::FocusStyle, AttrValue::Style(s));
self
}
pub fn borders(mut self, b: Borders) -> Self {
self.attr(Attribute::Borders, AttrValue::Borders(b));
self
}
pub fn title<S: AsRef<str>>(mut self, t: S, a: Alignment) -> Self {
self.attr(
Attribute::Title,
AttrValue::Title((t.as_ref().to_string(), a)),
);
self
}
pub fn input_type(mut self, itype: InputType) -> Self {
self.attr(Attribute::InputType, AttrValue::InputType(itype));
self
}
pub fn input_len(mut self, ilen: usize) -> Self {
self.attr(Attribute::InputLength, AttrValue::Length(ilen));
self
}
pub fn value<S: AsRef<str>>(mut self, s: S) -> Self {
self.attr(Attribute::Value, AttrValue::String(s.as_ref().to_string()));
self
}
pub fn invalid_style(mut self, s: Style) -> Self {
self.attr(Attribute::Custom(INPUT_INVALID_STYLE), AttrValue::Style(s));
self
}
pub fn placeholder<S: AsRef<str>>(mut self, placeholder: S, style: Style) -> Self {
self.attr(
Attribute::Custom(INPUT_PLACEHOLDER),
AttrValue::String(placeholder.as_ref().to_string()),
);
self.attr(
Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
AttrValue::Style(style),
);
self
}
fn get_input_len(&self) -> Option<usize> {
self.props
.get(Attribute::InputLength)
.map(|x| x.unwrap_length())
}
fn get_input_type(&self) -> InputType {
self.props
.get_or(Attribute::InputType, AttrValue::InputType(InputType::Text))
.unwrap_input_type()
}
fn is_valid(&self) -> bool {
let value = self.states.get_value();
self.get_input_type().validate(value.as_str())
}
}
impl MockComponent for Input {
fn view(&mut self, render: &mut Frame, area: Rect) {
if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
let mut foreground = self
.props
.get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
.unwrap_color();
let mut background = self
.props
.get_or(Attribute::Background, AttrValue::Color(Color::Reset))
.unwrap_color();
let modifiers = self
.props
.get_or(
Attribute::TextProps,
AttrValue::TextModifiers(TextModifiers::empty()),
)
.unwrap_text_modifiers();
let title = self
.props
.get_or(
Attribute::Title,
AttrValue::Title((String::default(), Alignment::Center)),
)
.unwrap_title();
let borders = self
.props
.get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
.unwrap_borders();
let focus = self
.props
.get_or(Attribute::Focus, AttrValue::Flag(false))
.unwrap_flag();
let inactive_style = self
.props
.get(Attribute::FocusStyle)
.map(|x| x.unwrap_style());
let itype = self.get_input_type();
let mut block = crate::utils::get_block(borders, Some(title), focus, inactive_style);
if focus && !self.is_valid() {
if let Some(style) = self
.props
.get(Attribute::Custom(INPUT_INVALID_STYLE))
.map(|x| x.unwrap_style())
{
let borders = self
.props
.get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
.unwrap_borders()
.color(style.fg.unwrap_or(Color::Reset));
let title = self
.props
.get_or(
Attribute::Title,
AttrValue::Title((String::default(), Alignment::Center)),
)
.unwrap_title();
block = crate::utils::get_block(borders, Some(title), focus, None);
foreground = style.fg.unwrap_or(Color::Reset);
background = style.bg.unwrap_or(Color::Reset);
}
}
let text_to_display = self.states.render_value(self.get_input_type());
let show_placeholder = text_to_display.is_empty();
let text_to_display = match show_placeholder {
true => self
.props
.get_or(
Attribute::Custom(INPUT_PLACEHOLDER),
AttrValue::String(String::new()),
)
.unwrap_string(),
false => text_to_display,
};
let paragraph_style = match focus {
true => Style::default()
.fg(foreground)
.bg(background)
.add_modifier(modifiers),
false => inactive_style.unwrap_or_default(),
};
let paragraph_style = match show_placeholder {
true => self
.props
.get_or(
Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
AttrValue::Style(paragraph_style),
)
.unwrap_style(),
false => paragraph_style,
};
let block_inner_area = block.inner(area);
let p: Paragraph = Paragraph::new(text_to_display)
.style(paragraph_style)
.block(block);
render.render_widget(p, area);
if focus {
let x: u16 = block_inner_area.x
+ calc_utf8_cursor_position(
&self.states.render_value_chars(itype)[0..self.states.cursor],
);
render
.set_cursor_position(tuirealm::ratatui::prelude::Position { x, y: area.y + 1 });
}
}
}
fn query(&self, attr: Attribute) -> Option<AttrValue> {
self.props.get(attr)
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
let sanitize_input = matches!(
attr,
Attribute::InputLength | Attribute::InputType | Attribute::Value
);
let new_input = match attr {
Attribute::Value => Some(value.clone().unwrap_string()),
_ => None,
};
self.props.set(attr, value);
if sanitize_input {
let input = match new_input {
None => self.states.input.clone(),
Some(v) => v.chars().collect(),
};
self.states.input = Vec::new();
self.states.cursor = 0;
let itype = self.get_input_type();
let max_len = self.get_input_len();
for ch in input.into_iter() {
self.states.append(ch, &itype, max_len);
}
}
}
fn state(&self) -> State {
if self.is_valid() {
State::One(StateValue::String(self.states.get_value()))
} else {
State::None
}
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
match cmd {
Cmd::Delete => {
let prev_input = self.states.input.clone();
self.states.backspace();
if prev_input != self.states.input {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
Cmd::Cancel => {
let prev_input = self.states.input.clone();
self.states.delete();
if prev_input != self.states.input {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
Cmd::Submit => CmdResult::Submit(self.state()),
Cmd::Move(Direction::Left) => {
self.states.decr_cursor();
CmdResult::None
}
Cmd::Move(Direction::Right) => {
self.states.incr_cursor();
CmdResult::None
}
Cmd::GoTo(Position::Begin) => {
self.states.cursor_at_begin();
CmdResult::None
}
Cmd::GoTo(Position::End) => {
self.states.cursor_at_end();
CmdResult::None
}
Cmd::Type(ch) => {
let prev_input = self.states.input.clone();
self.states
.append(ch, &self.get_input_type(), self.get_input_len());
if prev_input != self.states.input {
CmdResult::Changed(self.state())
} else {
CmdResult::None
}
}
_ => CmdResult::None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_components_input_states() {
let mut states: InputStates = InputStates::default();
states.append('a', &InputType::Text, Some(3));
assert_eq!(states.input, vec!['a']);
states.append('b', &InputType::Text, Some(3));
assert_eq!(states.input, vec!['a', 'b']);
states.append('c', &InputType::Text, Some(3));
assert_eq!(states.input, vec!['a', 'b', 'c']);
states.append('d', &InputType::Text, Some(3));
assert_eq!(states.input, vec!['a', 'b', 'c']);
states.append('d', &InputType::Number, None);
assert_eq!(states.input, vec!['a', 'b', 'c']);
states.decr_cursor();
assert_eq!(states.cursor, 2);
states.cursor = 1;
states.decr_cursor();
assert_eq!(states.cursor, 0);
states.decr_cursor();
assert_eq!(states.cursor, 0);
states.incr_cursor();
assert_eq!(states.cursor, 1);
states.incr_cursor();
assert_eq!(states.cursor, 2);
states.incr_cursor();
assert_eq!(states.cursor, 3);
assert_eq!(states.render_value(InputType::Text).as_str(), "abc");
assert_eq!(
states.render_value(InputType::Password('*')).as_str(),
"***"
);
}
#[test]
fn test_components_input_text() {
let mut component: Input = Input::default()
.background(Color::Yellow)
.borders(Borders::default())
.foreground(Color::Cyan)
.inactive(Style::default())
.input_len(5)
.input_type(InputType::Text)
.title("pippo", Alignment::Center)
.value("home");
assert_eq!(component.states.cursor, 4);
assert_eq!(component.states.input.len(), 4);
assert_eq!(
component.state(),
State::One(StateValue::String(String::from("home")))
);
assert_eq!(
component.perform(Cmd::Type('/')),
CmdResult::Changed(State::One(StateValue::String(String::from("home/"))))
);
assert_eq!(
component.state(),
State::One(StateValue::String(String::from("home/")))
);
assert_eq!(component.states.cursor, 5);
assert_eq!(component.perform(Cmd::Type('a')), CmdResult::None);
assert_eq!(
component.state(),
State::One(StateValue::String(String::from("home/")))
);
assert_eq!(component.states.cursor, 5);
assert_eq!(
component.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::String(String::from("home/"))))
);
assert_eq!(
component.perform(Cmd::Delete),
CmdResult::Changed(State::One(StateValue::String(String::from("home"))))
);
assert_eq!(
component.state(),
State::One(StateValue::String(String::from("home")))
);
assert_eq!(component.states.cursor, 4);
component.states.input = vec!['h'];
component.states.cursor = 1;
assert_eq!(
component.perform(Cmd::Delete),
CmdResult::Changed(State::One(StateValue::String(String::from(""))))
);
assert_eq!(
component.state(),
State::One(StateValue::String(String::from("")))
);
assert_eq!(component.states.cursor, 0);
assert_eq!(component.perform(Cmd::Delete), CmdResult::None);
assert_eq!(
component.state(),
State::One(StateValue::String(String::from("")))
);
assert_eq!(component.states.cursor, 0);
assert_eq!(component.perform(Cmd::Cancel), CmdResult::None);
assert_eq!(
component.state(),
State::One(StateValue::String(String::from("")))
);
assert_eq!(component.states.cursor, 0);
component.states.input = vec!['h', 'e'];
component.states.cursor = 1;
assert_eq!(
component.perform(Cmd::Cancel),
CmdResult::Changed(State::One(StateValue::String(String::from("h"))))
);
assert_eq!(
component.state(),
State::One(StateValue::String(String::from("h")))
);
assert_eq!(component.states.cursor, 1);
assert_eq!(component.perform(Cmd::Cancel), CmdResult::None);
assert_eq!(
component.state(),
State::One(StateValue::String(String::from("h")))
);
assert_eq!(component.states.cursor, 1);
component.states.input = vec!['h', 'e', 'l', 'l', 'o'];
component.attr(Attribute::InputLength, AttrValue::Length(16));
component.states.cursor = 1;
assert_eq!(
component.perform(Cmd::Move(Direction::Right)), CmdResult::None
);
assert_eq!(component.states.cursor, 2);
assert_eq!(
component.perform(Cmd::Type('a')),
CmdResult::Changed(State::One(StateValue::String(String::from("heallo"))))
);
assert_eq!(
component.state(),
State::One(StateValue::String(String::from("heallo")))
);
assert_eq!(component.states.cursor, 3);
assert_eq!(
component.perform(Cmd::Move(Direction::Left)),
CmdResult::None
);
assert_eq!(component.states.cursor, 2);
component.states.cursor = 6;
assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
assert_eq!(component.states.cursor, 6);
assert_eq!(
component.perform(Cmd::Move(Direction::Left)),
CmdResult::None
);
assert_eq!(component.states.cursor, 5);
component.states.cursor = 0;
assert_eq!(
component.perform(Cmd::Move(Direction::Left)),
CmdResult::None
);
assert_eq!(component.states.cursor, 0);
assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
assert_eq!(component.states.cursor, 6);
assert_eq!(
component.perform(Cmd::GoTo(Position::Begin)),
CmdResult::None
);
assert_eq!(component.states.cursor, 0);
component.attr(Attribute::Value, AttrValue::String("new-value".to_string()));
assert_eq!(
component.state(),
State::One(StateValue::String(String::from("new-value")))
);
component.attr(
Attribute::InputType,
AttrValue::InputType(InputType::Number),
);
assert_eq!(component.state(), State::None);
}
}