use chrono::NaiveDate;
use super::{input::InputField, textarea::TextArea};
mod render;
mod run;
const MULTILINE_ROWS: u16 = 4;
const LABEL_WIDTH: u16 = 14;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FieldValue {
Text(String),
Multiline(String),
Bool(bool),
Choice(String),
Date(Option<NaiveDate>),
}
enum FieldState {
Text {
input: InputField,
initial: String,
},
Multiline {
area: TextArea,
initial: String,
},
Bool {
value: bool,
initial: bool,
},
Choice {
index: usize,
initial: usize,
options: Vec<String>,
},
Date {
value: Option<NaiveDate>,
initial: Option<NaiveDate>,
},
}
pub struct Field {
label: String,
state: FieldState,
}
impl Field {
pub fn text(label: impl Into<String>, initial: &str) -> Self {
Self {
label: label.into(),
state: FieldState::Text {
input: InputField::new(initial),
initial: initial.to_string(),
},
}
}
pub fn multiline(label: impl Into<String>, initial: &str) -> Self {
Self {
label: label.into(),
state: FieldState::Multiline {
area: TextArea::new(initial),
initial: initial.to_string(),
},
}
}
pub fn checkbox(label: impl Into<String>, initial: bool) -> Self {
Self {
label: label.into(),
state: FieldState::Bool {
value: initial,
initial,
},
}
}
pub fn choice(
label: impl Into<String>,
options: Vec<String>,
initial: usize,
) -> Self {
Self {
label: label.into(),
state: FieldState::Choice {
index: initial.min(options.len().saturating_sub(1)),
initial,
options,
},
}
}
pub fn date(label: impl Into<String>, initial: Option<NaiveDate>) -> Self {
Self {
label: label.into(),
state: FieldState::Date {
value: initial,
initial,
},
}
}
pub fn label(&self) -> &str {
&self.label
}
pub fn value(&self) -> FieldValue {
match &self.state {
FieldState::Text { input, .. } => {
FieldValue::Text(input.value().to_string())
}
FieldState::Multiline { area, .. } => {
FieldValue::Multiline(area.text().to_string())
}
FieldState::Bool { value, .. } => FieldValue::Bool(*value),
FieldState::Choice { index, options, .. } => {
FieldValue::Choice(options[*index].clone())
}
FieldState::Date { value, .. } => FieldValue::Date(*value),
}
}
pub fn is_dirty(&self) -> bool {
match &self.state {
FieldState::Text { input, initial } => input.value() != initial,
FieldState::Multiline { area, initial } => area.text() != initial,
FieldState::Bool { value, initial } => value != initial,
FieldState::Choice { index, initial, .. } => index != initial,
FieldState::Date { value, initial } => value != initial,
}
}
fn height(&self) -> u16 {
match self.state {
FieldState::Multiline { .. } => MULTILINE_ROWS + 1,
_ => 1,
}
}
}
pub enum FormOutcome {
Saved,
Cancelled,
Quit,
}
pub struct Form {
title: String,
fields: Vec<Field>,
focus: usize,
}
impl Form {
pub fn new(title: impl Into<String>, fields: Vec<Field>) -> Self {
Self {
title: title.into(),
fields,
focus: 0,
}
}
pub fn fields(&self) -> &[Field] {
&self.fields
}
}
#[cfg(test)]
mod tests {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::{run::toggles_checkbox, *};
#[test]
fn only_a_bare_space_toggles_a_checkbox() {
for modified in [
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL),
KeyEvent::new(
KeyCode::Char(' '),
KeyModifiers::CONTROL | KeyModifiers::ALT,
),
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::ALT),
] {
assert!(
!toggles_checkbox(modified),
"{modified:?} must not toggle the checkbox"
);
}
assert!(toggles_checkbox(KeyEvent::new(
KeyCode::Char(' '),
KeyModifiers::NONE
)));
assert!(toggles_checkbox(KeyEvent::new(
KeyCode::Enter,
KeyModifiers::NONE
)));
}
#[test]
fn dirty_tracks_changes() {
let field = Field::checkbox("done", false);
assert!(!field.is_dirty());
let mut field = Field::choice("p", vec!["a".into(), "b".into()], 0);
assert!(!field.is_dirty());
if let FieldState::Choice { index, .. } = &mut field.state {
*index = 1;
}
assert!(field.is_dirty());
assert_eq!(field.value(), FieldValue::Choice("b".to_string()));
}
}