use crate::host::FormFieldHost;
use crate::validate::Validator;
use hjkl_buffer::Buffer;
use hjkl_engine::{Editor, Host, Input, Key, Options, VimMode, step};
pub struct FieldMeta {
pub label: String,
pub required: bool,
pub error: Option<String>,
pub placeholder: Option<String>,
}
impl FieldMeta {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
required: false,
error: None,
placeholder: None,
}
}
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn placeholder(mut self, text: impl Into<String>) -> Self {
self.placeholder = Some(text.into());
self
}
}
pub struct TextFieldEditor {
pub meta: FieldMeta,
pub editor: Editor<Buffer, FormFieldHost>,
pub validator: Option<Validator>,
pub rows: u16,
pub(crate) single_line: bool,
pub(crate) enter_gen: u64,
}
impl TextFieldEditor {
pub fn new(single_line: bool) -> Self {
let buffer = Buffer::new();
let host = FormFieldHost::new();
let editor = Editor::new(buffer, host, Options::default());
Self {
meta: FieldMeta::new(""),
editor,
validator: None,
rows: if single_line { 1 } else { 3 },
single_line,
enter_gen: 0,
}
}
pub fn with_text(text: &str, single_line: bool) -> Self {
let mut me = Self::new(single_line);
me.set_text(text);
me
}
pub fn with_meta(meta: FieldMeta, rows: u16) -> Self {
let buffer = Buffer::new();
let host = FormFieldHost::new();
let editor = Editor::new(buffer, host, Options::default());
Self {
meta,
editor,
validator: None,
single_line: rows <= 1,
rows,
enter_gen: 0,
}
}
pub fn with_validator(mut self, validator: Validator) -> Self {
self.validator = Some(validator);
self
}
pub fn with_initial(mut self, text: &str) -> Self {
let buffer = Buffer::from_str(text);
let host = FormFieldHost::new();
self.editor = Editor::new(buffer, host, Options::default());
self
}
pub fn buffer(&self) -> &Buffer {
self.editor.buffer()
}
pub fn buffer_mut(&mut self) -> &mut Buffer {
self.editor.buffer_mut()
}
pub fn text(&self) -> String {
self.editor.buffer().as_string()
}
pub fn set_text(&mut self, text: &str) {
let buffer = Buffer::from_str(text);
let host = FormFieldHost::new();
self.editor = Editor::new(buffer, host, Options::default());
let lines = self.editor.buffer().lines();
if let Some(last) = lines.last() {
let row = lines.len().saturating_sub(1);
let col = last.chars().count();
self.editor
.buffer_mut()
.set_cursor(hjkl_buffer::Position::new(row, col));
}
}
pub fn cursor(&self) -> (usize, usize) {
self.editor.cursor()
}
pub fn vim_mode(&self) -> VimMode {
self.editor.vim_mode()
}
pub fn enter_insert_at_end(&mut self) {
let lines = self.editor.buffer().lines().to_vec();
let row = lines.len().saturating_sub(1);
let col = lines.last().map(|s| s.chars().count()).unwrap_or(0);
self.editor
.buffer_mut()
.set_cursor(hjkl_buffer::Position::new(row, col));
self.editor.force_normal();
let _ = step(
&mut self.editor,
Input {
key: Key::Char('A'),
shift: true,
..Input::default()
},
);
}
pub fn enter_normal(&mut self) {
self.editor.force_normal();
}
pub fn handle_input(&mut self, input: Input) -> bool {
if self.single_line && input.key == Key::Enter && self.editor.vim_mode() == VimMode::Insert
{
return false;
}
let before = self.editor.buffer().dirty_gen();
let _ = step(&mut self.editor, input);
self.editor.buffer().dirty_gen() != before
}
pub fn dirty_gen(&self) -> u64 {
self.editor.buffer().dirty_gen()
}
pub fn set_viewport_width(&mut self, width: u16) {
self.editor.host_mut().viewport_mut().width = width;
}
pub fn set_viewport_height(&mut self, height: u16) {
self.editor.host_mut().viewport_mut().height = height;
}
}
pub struct CheckboxField {
pub meta: FieldMeta,
pub value: bool,
}
impl CheckboxField {
pub fn new(meta: FieldMeta) -> Self {
Self { meta, value: false }
}
pub fn with_value(mut self, value: bool) -> Self {
self.value = value;
self
}
}
pub struct SelectField {
pub meta: FieldMeta,
pub options: Vec<String>,
pub index: usize,
}
impl SelectField {
pub fn new(meta: FieldMeta, options: Vec<String>) -> Self {
Self {
meta,
options,
index: 0,
}
}
pub fn selected(&self) -> Option<&str> {
self.options.get(self.index).map(String::as_str)
}
}
pub struct SubmitField {
pub meta: FieldMeta,
}
impl SubmitField {
pub fn new(meta: FieldMeta) -> Self {
Self { meta }
}
}
pub enum Field {
SingleLineText(TextFieldEditor),
MultiLineText(TextFieldEditor),
Select(SelectField),
Checkbox(CheckboxField),
Submit(SubmitField),
}
impl Field {
pub fn meta(&self) -> &FieldMeta {
match self {
Field::SingleLineText(f) | Field::MultiLineText(f) => &f.meta,
Field::Select(f) => &f.meta,
Field::Checkbox(f) => &f.meta,
Field::Submit(f) => &f.meta,
}
}
pub fn meta_mut(&mut self) -> &mut FieldMeta {
match self {
Field::SingleLineText(f) | Field::MultiLineText(f) => &mut f.meta,
Field::Select(f) => &mut f.meta,
Field::Checkbox(f) => &mut f.meta,
Field::Submit(f) => &mut f.meta,
}
}
pub fn is_text(&self) -> bool {
matches!(self, Field::SingleLineText(_) | Field::MultiLineText(_))
}
pub fn is_single_line_text(&self) -> bool {
matches!(self, Field::SingleLineText(_))
}
pub fn is_focusable(&self) -> bool {
let _ = self;
true
}
}
#[cfg(test)]
mod standalone_tests {
use super::*;
fn ki(c: char) -> Input {
Input {
key: Key::Char(c),
..Input::default()
}
}
#[test]
fn text_round_trips_via_set_text() {
let mut f = TextFieldEditor::new(true);
f.set_text("hello");
assert_eq!(f.text(), "hello");
}
#[test]
fn with_text_constructor_pre_populates() {
let f = TextFieldEditor::with_text("abc", true);
assert_eq!(f.text(), "abc");
}
#[test]
fn handle_input_i_enters_insert() {
let mut f = TextFieldEditor::new(true);
f.handle_input(ki('i'));
assert_eq!(f.vim_mode(), VimMode::Insert);
}
#[test]
fn handle_input_types_and_esc_returns_to_normal() {
let mut f = TextFieldEditor::new(true);
f.handle_input(ki('i'));
f.handle_input(ki('h'));
f.handle_input(ki('i'));
f.handle_input(Input {
key: Key::Esc,
..Input::default()
});
assert_eq!(f.text(), "hi");
assert_eq!(f.vim_mode(), VimMode::Normal);
}
#[test]
fn dirty_gen_advances_after_insert() {
let mut f = TextFieldEditor::new(true);
let before = f.dirty_gen();
f.handle_input(ki('i'));
f.handle_input(ki('x'));
assert!(f.dirty_gen() > before);
}
#[test]
fn enter_insert_at_end_lands_cursor_at_eol() {
let mut f = TextFieldEditor::with_text("abc", true);
f.enter_insert_at_end();
let (row, col) = f.cursor();
assert_eq!(row, 0);
assert_eq!(col, 3);
assert_eq!(f.vim_mode(), VimMode::Insert);
}
#[test]
fn single_line_swallows_enter_in_insert() {
let mut f = TextFieldEditor::new(true);
f.enter_insert_at_end();
let dirty = f.handle_input(Input {
key: Key::Enter,
..Input::default()
});
assert!(!dirty, "Enter must not mutate buffer in single-line Insert");
assert_eq!(f.text(), "");
}
#[test]
fn multi_line_accepts_enter_in_insert() {
let mut f = TextFieldEditor::new(false);
f.enter_insert_at_end();
f.handle_input(ki('a'));
f.handle_input(Input {
key: Key::Enter,
..Input::default()
});
f.handle_input(ki('b'));
assert_eq!(f.text(), "a\nb");
}
}