use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
prelude::*,
style::{Color, Style},
};
use ratatui_textarea::{CursorMove, TextArea};
use crate::columns::{columns_to_string, parse_columns, JobColumn};
pub enum TextFieldType {
Text(String),
Integer(usize),
Boolean(bool),
Columns(Vec<JobColumn>),
}
pub struct TextField {
pub text_area: TextArea<'static>,
pub field_type: TextFieldType,
pub active: bool,
pub focused: bool,
pub label: String,
}
impl TextField {
pub fn new(label: &str, field_type: TextFieldType) -> Self {
let mut text_field = Self {
text_area: TextArea::from([""]),
field_type,
active: false,
focused: false,
label: label.to_string(),
};
text_field.sync_v2t();
text_field
}
}
impl TextField {
pub fn sync_t2v(&mut self) {
let lines = self.text_area.lines().join("\n");
match self.field_type {
TextFieldType::Boolean(_) => {
let b = string_to_bool(lines.as_str());
self.field_type = TextFieldType::Boolean(b);
}
TextFieldType::Integer(_) => {
let u = lines.parse::<usize>().unwrap_or(0);
self.field_type = TextFieldType::Integer(u);
}
TextFieldType::Text(_) => {
self.field_type = TextFieldType::Text(lines);
}
TextFieldType::Columns(_) => {
if let Some(columns) = parse_columns(&lines) {
self.field_type = TextFieldType::Columns(columns);
}
}
}
}
pub fn sync_v2t(&mut self) {
let text_content = match self.field_type {
TextFieldType::Boolean(b) => bool_to_string(b),
TextFieldType::Integer(i) => i.to_string(),
TextFieldType::Text(ref s) => s.clone(),
TextFieldType::Columns(ref columns) => columns_to_string(columns),
};
self.text_area = TextArea::from([text_content]);
self.text_area.move_cursor(CursorMove::End);
}
pub fn on_enter(&mut self) {
match self.field_type {
TextFieldType::Boolean(old_value) => {
self.field_type = TextFieldType::Boolean(!old_value);
self.sync_v2t();
}
TextFieldType::Integer(_) => {
self.active = true;
}
TextFieldType::Text(_) => {
self.active = true;
}
TextFieldType::Columns(_) => {
self.active = true;
}
}
}
pub fn reset(&mut self) {
self.sync_v2t();
self.active = false;
}
pub fn apply(&mut self) {
let is_valid = match self.field_type {
TextFieldType::Integer(_) => {
let lines = self.text_area.lines().join("\n");
lines.parse::<usize>().is_ok()
}
TextFieldType::Columns(_) => {
let lines = self.text_area.lines().join("\n");
parse_columns(&lines).is_some()
}
_ => true,
};
if is_valid {
self.sync_t2v();
self.active = false;
} else {
self.reset();
}
}
}
fn bool_to_string(b: bool) -> String {
match b {
true => "true".to_string(),
false => "false".to_string(),
}
}
fn string_to_bool(s: &str) -> bool {
match s {
"true" => true,
"false" => false,
_ => false,
}
}
impl TextField {
pub fn render(&mut self, f: &mut Frame, area: &Rect) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(50),
Constraint::Length(1),
Constraint::Percentage(50),
])
.split(*area);
let label = format!("{}: ", self.label);
let mut line = Line::from(label)
.style(Style::default())
.alignment(Alignment::Right);
if self.focused {
line = line.style(
Style::default()
.bg(Color::Blue)
.fg(Color::Black)
.add_modifier(Modifier::BOLD),
);
};
f.render_widget(line, chunks[0]);
let mut line = Line::from(" ").style(Style::default());
if self.focused && !self.active {
line = line.style(
Style::default()
.bg(Color::Blue)
.fg(Color::Black)
.add_modifier(Modifier::BOLD),
);
};
f.render_widget(line, chunks[1]);
self.text_area.set_cursor_line_style(Style::default());
self.text_area.set_cursor_style(Style::default());
self.text_area.set_style(Style::default());
if self.focused && self.active {
self.text_area
.set_cursor_style(Style::default().bg(Color::Blue));
self.text_area.set_cursor_line_style(
Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::BOLD),
);
} else if self.focused && !self.active {
self.text_area.set_style(
Style::default()
.bg(Color::Blue)
.fg(Color::Black)
.add_modifier(Modifier::BOLD),
);
}
f.render_widget(&self.text_area, chunks[2]);
}
}
impl TextField {
pub fn input(&mut self, key_event: KeyEvent) -> bool {
match key_event.code {
KeyCode::Esc => {
self.reset();
false
}
KeyCode::Enter => {
self.apply();
true
}
_ => {
self.text_area.input(key_event);
false
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn set_text(field: &mut TextField, text: &str) {
field.text_area = TextArea::from([text.to_string()]);
}
#[test]
fn test_columns_field_applies_a_valid_list() {
let mut field = TextField::new("Job columns", TextFieldType::Columns(vec![JobColumn::Id]));
assert_eq!(field.text_area.lines().join("\n"), "id");
field.on_enter();
assert!(field.active);
set_text(&mut field, "id, priority");
field.apply();
assert!(!field.active);
match &field.field_type {
TextFieldType::Columns(columns) => {
assert_eq!(columns, &vec![JobColumn::Id, JobColumn::Priority]);
}
_ => panic!("expected a Columns field"),
}
}
#[test]
fn test_columns_field_keeps_old_value_on_invalid_input() {
let old_columns = vec![JobColumn::Id, JobColumn::Name];
let mut field = TextField::new("Job columns", TextFieldType::Columns(old_columns.clone()));
field.on_enter();
set_text(&mut field, "id, bogus");
field.apply();
assert!(!field.active);
match &field.field_type {
TextFieldType::Columns(columns) => assert_eq!(columns, &old_columns),
_ => panic!("expected a Columns field"),
}
assert_eq!(field.text_area.lines().join("\n"), "id, name");
}
}