use ratatui::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Frame,
};
#[derive(Debug, Clone)]
pub struct SimpleInput {
lines: Vec<String>,
cursor_line: usize,
cursor_col: usize,
focused: bool,
}
impl Default for SimpleInput {
fn default() -> Self {
Self::new()
}
}
impl SimpleInput {
pub fn new() -> Self {
Self {
lines: vec![String::new()],
cursor_line: 0,
cursor_col: 0,
focused: true,
}
}
pub fn insert_char(&mut self, c: char) {
if self.cursor_line < self.lines.len() {
let line = &mut self.lines[self.cursor_line];
self.cursor_col = self.cursor_col.min(line.len());
line.insert(self.cursor_col, c);
self.cursor_col += 1;
}
}
pub fn insert_newline(&mut self) {
if self.cursor_line < self.lines.len() {
let line = &mut self.lines[self.cursor_line];
self.cursor_col = self.cursor_col.min(line.len());
let remainder = line[self.cursor_col..].to_string();
line.truncate(self.cursor_col);
self.cursor_line += 1;
self.lines.insert(self.cursor_line, remainder);
self.cursor_col = 0;
}
}
pub fn backspace(&mut self) {
if self.cursor_col > 0 {
let line = &mut self.lines[self.cursor_line];
self.cursor_col -= 1;
line.remove(self.cursor_col);
} else if self.cursor_line > 0 {
let current_line = self.lines.remove(self.cursor_line);
self.cursor_line -= 1;
self.cursor_col = self.lines[self.cursor_line].len();
self.lines[self.cursor_line].push_str(¤t_line);
}
}
pub fn delete(&mut self) {
let line = &mut self.lines[self.cursor_line];
if self.cursor_col < line.len() {
line.remove(self.cursor_col);
} else if self.cursor_line + 1 < self.lines.len() {
let next_line = self.lines.remove(self.cursor_line + 1);
self.lines[self.cursor_line].push_str(&next_line);
}
}
pub fn move_left(&mut self) {
if self.cursor_col > 0 {
self.cursor_col -= 1;
} else if self.cursor_line > 0 {
self.cursor_line -= 1;
self.cursor_col = self.lines[self.cursor_line].len();
}
}
pub fn move_right(&mut self) {
let line_len = self.lines[self.cursor_line].len();
if self.cursor_col < line_len {
self.cursor_col += 1;
} else if self.cursor_line + 1 < self.lines.len() {
self.cursor_line += 1;
self.cursor_col = 0;
}
}
pub fn move_up(&mut self) {
if self.cursor_line > 0 {
self.cursor_line -= 1;
self.cursor_col = self.cursor_col.min(self.lines[self.cursor_line].len());
}
}
pub fn move_down(&mut self) {
if self.cursor_line + 1 < self.lines.len() {
self.cursor_line += 1;
self.cursor_col = self.cursor_col.min(self.lines[self.cursor_line].len());
}
}
pub fn move_home(&mut self) {
self.cursor_col = 0;
}
pub fn move_end(&mut self) {
self.cursor_col = self.lines[self.cursor_line].len();
}
pub fn text(&self) -> String {
self.lines.join("\n")
}
pub fn is_empty(&self) -> bool {
self.lines.len() == 1 && self.lines[0].is_empty()
}
pub fn clear(&mut self) {
self.lines = vec![String::new()];
self.cursor_line = 0;
self.cursor_col = 0;
}
pub fn line_count(&self) -> usize {
self.lines.len()
}
pub fn set_focused(&mut self, focused: bool) {
self.focused = focused;
}
pub fn render(&self, frame: &mut Frame, area: Rect, title: &str) {
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(if self.focused {
Color::Rgb(129, 199, 132) } else {
Color::Rgb(96, 125, 139) }))
.title(Span::styled(
format!(" {} ", title),
Style::default()
.fg(Color::Rgb(224, 247, 250))
.add_modifier(Modifier::BOLD),
));
let inner = block.inner(area);
frame.render_widget(block, area);
let mut display_lines: Vec<Line> = Vec::new();
for (i, line) in self.lines.iter().enumerate() {
if i == self.cursor_line && self.focused {
let before = &line[..self.cursor_col.min(line.len())];
let cursor_char = line.chars().nth(self.cursor_col).unwrap_or(' ');
let after = if self.cursor_col < line.len() {
&line[self.cursor_col + 1..]
} else {
""
};
display_lines.push(Line::from(vec![
Span::raw(before.to_string()),
Span::styled(
cursor_char.to_string(),
Style::default()
.bg(Color::Rgb(129, 199, 132))
.fg(Color::Black),
),
Span::raw(after.to_string()),
]));
} else {
display_lines.push(Line::from(line.as_str()));
}
}
let paragraph = Paragraph::new(display_lines);
frame.render_widget(paragraph, inner);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_insert_and_text() {
let mut input = SimpleInput::new();
input.insert_char('H');
input.insert_char('i');
assert_eq!(input.text(), "Hi");
}
#[test]
fn test_newline() {
let mut input = SimpleInput::new();
input.insert_char('a');
input.insert_newline();
input.insert_char('b');
assert_eq!(input.text(), "a\nb");
assert_eq!(input.line_count(), 2);
}
#[test]
fn test_backspace() {
let mut input = SimpleInput::new();
input.insert_char('a');
input.insert_char('b');
input.backspace();
assert_eq!(input.text(), "a");
}
#[test]
fn test_clear() {
let mut input = SimpleInput::new();
input.insert_char('x');
input.clear();
assert!(input.is_empty());
}
}