use ratatui::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
Frame,
};
use crate::input::{Key, Modifiers};
use crate::theme::Theme;
pub fn key_to_op(key: crate::input::KeyEvent) -> Option<InputFieldOp> {
match (key.modifiers, key.key) {
(m, Key::Char(c)) if !m.contains(Modifiers::CTRL) && !m.contains(Modifiers::ALT) => {
Some(InputFieldOp::InsertChar(c))
}
(_, Key::Backspace) => Some(InputFieldOp::Backspace),
(_, Key::Delete) => Some(InputFieldOp::DeleteForward),
(_, Key::ArrowLeft) => Some(InputFieldOp::CursorLeft),
(_, Key::ArrowRight) => Some(InputFieldOp::CursorRight),
(_, Key::Home) => Some(InputFieldOp::CursorHome),
(_, Key::End) => Some(InputFieldOp::CursorEnd),
_ => None,
}
}
#[derive(Debug, Default, Clone)]
pub struct InputField {
text: String,
cursor: usize,
label: String,
}
#[derive(Debug, Clone)]
pub enum InputFieldOp {
InsertChar(char),
Backspace,
DeleteForward,
CursorLeft,
CursorRight,
CursorHome,
CursorEnd,
SetText(String),
}
impl InputField {
pub fn new(label: impl Into<String>) -> Self {
Self {
text: String::new(),
cursor: 0,
label: label.into(),
}
}
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.text = text.into();
self.cursor = self.text.len();
self
}
pub fn text(&self) -> &str {
&self.text
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn label(&self) -> &str {
&self.label
}
pub fn set_label(&mut self, label: impl Into<String>) {
self.label = label.into();
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub fn insert_char(&mut self, c: char) {
self.text.insert(self.cursor, c);
self.cursor += c.len_utf8();
}
pub fn backspace(&mut self) {
if self.cursor == 0 {
return;
}
let prev = self.text[..self.cursor]
.char_indices()
.next_back()
.map(|(i, _)| i)
.unwrap_or(0);
self.text.drain(prev..self.cursor);
self.cursor = prev;
}
pub fn delete_forward(&mut self) {
if self.cursor >= self.text.len() {
return;
}
let next = self.text[self.cursor..]
.char_indices()
.nth(1)
.map(|(i, _)| self.cursor + i)
.unwrap_or(self.text.len());
self.text.drain(self.cursor..next);
}
pub fn cursor_left(&mut self) {
if self.cursor == 0 {
return;
}
self.cursor = self.text[..self.cursor]
.char_indices()
.next_back()
.map(|(i, _)| i)
.unwrap_or(0);
}
pub fn cursor_right(&mut self) {
if self.cursor >= self.text.len() {
return;
}
self.cursor = self.text[self.cursor..]
.char_indices()
.nth(1)
.map(|(i, _)| self.cursor + i)
.unwrap_or(self.text.len());
}
pub fn cursor_home(&mut self) {
self.cursor = 0;
}
pub fn cursor_end(&mut self) {
self.cursor = self.text.len();
}
pub fn set_text(&mut self, text: String) {
self.text = text;
self.cursor = self.text.len();
}
pub fn apply(&mut self, op: &InputFieldOp) {
match op {
InputFieldOp::InsertChar(c) => self.insert_char(*c),
InputFieldOp::Backspace => self.backspace(),
InputFieldOp::DeleteForward => self.delete_forward(),
InputFieldOp::CursorLeft => self.cursor_left(),
InputFieldOp::CursorRight => self.cursor_right(),
InputFieldOp::CursorHome => self.cursor_home(),
InputFieldOp::CursorEnd => self.cursor_end(),
InputFieldOp::SetText(t) => self.set_text(t.clone()),
}
}
pub fn render(&self, frame: &mut Frame, area: Rect, focused: bool, theme: &Theme) {
if area.height == 0 || area.width == 0 {
return;
}
let bg = if focused {
theme.bg_active()
} else {
theme.bg_inactive()
};
let fg = if focused {
theme.fg_active()
} else {
theme.fg_dim()
};
let accent = theme.accent();
let mut spans = Vec::new();
if !self.label.is_empty() {
spans.push(Span::styled(
format!(" {}: ", self.label),
Style::new().fg(accent).add_modifier(Modifier::BOLD),
));
}
if focused {
let (before, after) = self.text.split_at(self.cursor);
spans.push(Span::styled(before.to_string(), Style::new().fg(fg).bg(bg)));
let cursor_char = after.chars().next().unwrap_or(' ');
spans.push(Span::styled(
cursor_char.to_string(),
Style::new()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD),
));
if !after.is_empty() {
let rest_start = cursor_char.len_utf8();
if rest_start < after.len() {
spans.push(Span::styled(
after[rest_start..].to_string(),
Style::new().fg(fg).bg(bg),
));
}
}
} else {
spans.push(Span::styled(
self.text.clone(),
Style::new().fg(fg).bg(bg),
));
}
frame.render_widget(
Paragraph::new(Line::from(spans)).style(Style::new().bg(bg)),
area,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_empty() {
let field = InputField::new("Filter");
assert_eq!(field.text(), "");
assert_eq!(field.cursor(), 0);
assert_eq!(field.label(), "Filter");
assert!(field.is_empty());
}
#[test]
fn test_with_text() {
let field = InputField::new("").with_text("hello");
assert_eq!(field.text(), "hello");
assert_eq!(field.cursor(), 5);
}
#[test]
fn test_insert_char() {
let mut field = InputField::new("");
field.insert_char('a');
field.insert_char('b');
assert_eq!(field.text(), "ab");
assert_eq!(field.cursor(), 2);
}
#[test]
fn test_insert_char_middle() {
let mut field = InputField::new("").with_text("ac");
field.cursor_left();
field.insert_char('b');
assert_eq!(field.text(), "abc");
assert_eq!(field.cursor(), 2);
}
#[test]
fn test_backspace() {
let mut field = InputField::new("").with_text("abc");
field.backspace();
assert_eq!(field.text(), "ab");
assert_eq!(field.cursor(), 2);
}
#[test]
fn test_backspace_at_start() {
let mut field = InputField::new("").with_text("abc");
field.cursor_home();
field.backspace();
assert_eq!(field.text(), "abc");
assert_eq!(field.cursor(), 0);
}
#[test]
fn test_backspace_empty() {
let mut field = InputField::new("");
field.backspace();
assert_eq!(field.text(), "");
assert_eq!(field.cursor(), 0);
}
#[test]
fn test_delete_forward() {
let mut field = InputField::new("").with_text("abc");
field.cursor_home();
field.delete_forward();
assert_eq!(field.text(), "bc");
assert_eq!(field.cursor(), 0);
}
#[test]
fn test_delete_forward_at_end() {
let mut field = InputField::new("").with_text("abc");
field.delete_forward();
assert_eq!(field.text(), "abc");
assert_eq!(field.cursor(), 3);
}
#[test]
fn test_cursor_left_right() {
let mut field = InputField::new("").with_text("abc");
assert_eq!(field.cursor(), 3);
field.cursor_left();
assert_eq!(field.cursor(), 2);
field.cursor_left();
assert_eq!(field.cursor(), 1);
field.cursor_right();
assert_eq!(field.cursor(), 2);
}
#[test]
fn test_cursor_left_at_start() {
let mut field = InputField::new("").with_text("a");
field.cursor_home();
field.cursor_left();
assert_eq!(field.cursor(), 0);
}
#[test]
fn test_cursor_right_at_end() {
let mut field = InputField::new("").with_text("a");
field.cursor_right();
assert_eq!(field.cursor(), 1);
}
#[test]
fn test_cursor_home_end() {
let mut field = InputField::new("").with_text("hello");
field.cursor_home();
assert_eq!(field.cursor(), 0);
field.cursor_end();
assert_eq!(field.cursor(), 5);
}
#[test]
fn test_set_text() {
let mut field = InputField::new("");
field.set_text("new value".to_string());
assert_eq!(field.text(), "new value");
assert_eq!(field.cursor(), 9);
}
#[test]
fn test_utf8_multibyte() {
let mut field = InputField::new("");
field.insert_char('é');
field.insert_char('ñ');
assert_eq!(field.text(), "éñ");
assert_eq!(field.cursor(), 4);
field.cursor_left();
assert_eq!(field.cursor(), 2);
field.backspace();
assert_eq!(field.text(), "ñ");
assert_eq!(field.cursor(), 0);
}
#[test]
fn test_utf8_emoji() {
let mut field = InputField::new("");
field.insert_char('🎉');
assert_eq!(field.text(), "🎉");
assert_eq!(field.cursor(), 4); field.backspace();
assert_eq!(field.text(), "");
assert_eq!(field.cursor(), 0);
}
#[test]
fn test_apply_op() {
let mut field = InputField::new("");
field.apply(&InputFieldOp::InsertChar('h'));
field.apply(&InputFieldOp::InsertChar('i'));
assert_eq!(field.text(), "hi");
field.apply(&InputFieldOp::CursorLeft);
field.apply(&InputFieldOp::Backspace);
assert_eq!(field.text(), "i");
field.apply(&InputFieldOp::CursorHome);
field.apply(&InputFieldOp::DeleteForward);
assert_eq!(field.text(), "");
}
#[test]
fn test_apply_set_text() {
let mut field = InputField::new("");
field.apply(&InputFieldOp::SetText("replaced".to_string()));
assert_eq!(field.text(), "replaced");
assert_eq!(field.cursor(), 8);
}
#[test]
fn test_delete_forward_middle() {
let mut field = InputField::new("").with_text("abc");
field.cursor_home();
field.cursor_right();
field.delete_forward();
assert_eq!(field.text(), "ac");
assert_eq!(field.cursor(), 1);
}
#[test]
fn test_backspace_middle() {
let mut field = InputField::new("").with_text("abc");
field.cursor_left();
field.backspace();
assert_eq!(field.text(), "ac");
assert_eq!(field.cursor(), 1);
}
}