use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
use ratatui::{
Frame,
layout::{Alignment, Position, Rect},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Paragraph},
};
use crate::tui::SEETui;
pub(crate) struct TuiInput {
title: String,
prefix: String,
pub input: String,
character_index: usize,
pub focused: bool,
}
impl TuiInput {
pub fn new(title: String, prefix: String) -> Self {
Self {
title: title,
prefix: prefix,
input: String::new(),
character_index: 0,
focused: false,
}
}
fn move_cursor_left(&mut self) {
let cursor_moved_left = self.character_index.saturating_sub(1);
self.character_index = self.clamp_cursor(cursor_moved_left);
}
fn move_cursor_right(&mut self) {
let cursor_moved_right = self.character_index.saturating_add(1);
self.character_index = self.clamp_cursor(cursor_moved_right);
}
fn enter_char(&mut self, new_char: char) {
let index = self.byte_index();
self.input.insert(index, new_char);
self.move_cursor_right();
}
fn byte_index(&self) -> usize {
self.input
.char_indices()
.map(|(i, _)| i)
.nth(self.character_index)
.unwrap_or(self.input.len())
}
fn delete_char(&mut self) {
let is_not_cursor_leftmost = self.character_index != 0;
if is_not_cursor_leftmost {
let current_index = self.character_index;
let from_left_to_current_index = current_index - 1;
let before_char_to_delete = self.input.chars().take(from_left_to_current_index);
let after_char_to_delete = self.input.chars().skip(current_index);
self.input = before_char_to_delete.chain(after_char_to_delete).collect();
self.move_cursor_left();
}
}
fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
new_cursor_pos.clamp(0, self.input.chars().count())
}
const fn reset_cursor(&mut self) {
self.character_index = 0;
}
fn submit_message(&mut self) {
self.input.clear();
self.reset_cursor();
}
pub fn render_input(&mut self, area: Rect, frame: &mut Frame, keye: Option<KeyEvent>) -> bool {
self.render(area, frame);
let mut valuechanged = false;
if let Some(key) = keye
&& self.focused
{
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Enter => self.submit_message(),
KeyCode::Char(to_insert) => {
self.enter_char(to_insert);
valuechanged = true;
}
KeyCode::Backspace => {
valuechanged = true;
self.delete_char();
}
KeyCode::Left => self.move_cursor_left(),
KeyCode::Right => self.move_cursor_right(),
_ => {}
}
}
}
valuechanged
}
fn render(&self, input_area: Rect, frame: &mut Frame) {
let cool_block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(if self.focused {
SEETui::FOCUSED_COLOR
} else {
SEETui::UNFOCUSED_COLOR
})
.title(Line::from(vec![Span::styled(
&self.title,
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)]))
.title_alignment(Alignment::Left);
let is_empty = str::is_empty(self.input.as_str());
let input = Paragraph::new(if is_empty {
self.prefix.as_str()
} else {
self.input.as_str()
})
.block(cool_block)
.fg(if is_empty { Color::Gray } else { Color::White });
frame.render_widget(input, input_area);
if self.focused {
#[expect(clippy::cast_possible_truncation)]
frame.set_cursor_position(Position::new(
input_area.x + self.character_index as u16 + 1,
input_area.y + 1,
));
}
}
}