use crate::spec_ai_tui::buffer::Buffer;
use crate::spec_ai_tui::geometry::Rect;
use crate::spec_ai_tui::style::{Color, Style};
use crate::spec_ai_tui::widget::StatefulWidget;
#[derive(Debug, Clone, Default)]
pub struct InputState {
pub value: String,
pub cursor: usize,
pub scroll: usize,
pub focused: bool,
}
impl InputState {
pub fn new() -> Self {
Self {
value: String::new(),
cursor: 0,
scroll: 0,
focused: true,
}
}
pub fn with_value<S: Into<String>>(value: S) -> Self {
let value = value.into();
let cursor = value.len();
Self {
value,
cursor,
scroll: 0,
focused: true,
}
}
pub fn value(&self) -> &str {
&self.value
}
pub fn set_value<S: Into<String>>(&mut self, value: S) {
self.value = value.into();
self.cursor = self.value.len();
self.scroll = 0;
}
pub fn insert(&mut self, c: char) {
self.value.insert(self.cursor, c);
self.cursor += c.len_utf8();
}
pub fn insert_str(&mut self, s: &str) {
self.value.insert_str(self.cursor, s);
self.cursor += s.len();
}
pub fn backspace(&mut self) {
if self.cursor > 0 {
let prev = self.value[..self.cursor]
.char_indices()
.last()
.map(|(i, _)| i)
.unwrap_or(0);
self.value.remove(prev);
self.cursor = prev;
}
}
pub fn delete(&mut self) {
if self.cursor < self.value.len() {
self.value.remove(self.cursor);
}
}
pub fn move_left(&mut self) {
if self.cursor > 0 {
self.cursor = self.value[..self.cursor]
.char_indices()
.last()
.map(|(i, _)| i)
.unwrap_or(0);
}
}
pub fn move_right(&mut self) {
if self.cursor < self.value.len() {
self.cursor = self.value[self.cursor..]
.char_indices()
.nth(1)
.map(|(i, _)| self.cursor + i)
.unwrap_or(self.value.len());
}
}
pub fn move_home(&mut self) {
self.cursor = 0;
}
pub fn move_end(&mut self) {
self.cursor = self.value.len();
}
pub fn move_word_left(&mut self) {
if self.cursor == 0 {
return;
}
let before_cursor = &self.value[..self.cursor];
let trimmed_end = before_cursor.trim_end();
if trimmed_end.is_empty() {
self.cursor = 0;
return;
}
self.cursor = trimmed_end
.rfind(|c: char| c.is_whitespace())
.map(|i| i + 1)
.unwrap_or(0);
}
pub fn move_word_right(&mut self) {
if self.cursor >= self.value.len() {
return;
}
let after_cursor = &self.value[self.cursor..];
let skip_word = after_cursor
.find(|c: char| c.is_whitespace())
.unwrap_or(after_cursor.len());
let skip_space = after_cursor[skip_word..]
.find(|c: char| !c.is_whitespace())
.unwrap_or(after_cursor.len() - skip_word);
self.cursor = self.cursor + skip_word + skip_space;
}
pub fn clear(&mut self) {
self.value.clear();
self.cursor = 0;
self.scroll = 0;
}
pub fn take(&mut self) -> String {
let value = std::mem::take(&mut self.value);
self.cursor = 0;
self.scroll = 0;
value
}
pub fn cursor_char_pos(&self) -> usize {
self.value[..self.cursor].chars().count()
}
}
#[derive(Debug, Clone, Default)]
pub struct Input {
style: Style,
cursor_style: Style,
placeholder: Option<String>,
placeholder_style: Style,
mask: Option<char>,
}
impl Input {
pub fn new() -> Self {
Self {
style: Style::default(),
cursor_style: Style::new().bg(Color::White).fg(Color::Black),
placeholder: None,
placeholder_style: Style::new().fg(Color::DarkGrey),
mask: None,
}
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn cursor_style(mut self, style: Style) -> Self {
self.cursor_style = style;
self
}
pub fn placeholder<S: Into<String>>(mut self, placeholder: S) -> Self {
self.placeholder = Some(placeholder.into());
self
}
pub fn placeholder_style(mut self, style: Style) -> Self {
self.placeholder_style = style;
self
}
pub fn password(mut self) -> Self {
self.mask = Some('*');
self
}
pub fn mask(mut self, c: char) -> Self {
self.mask = Some(c);
self
}
}
impl StatefulWidget for Input {
type State = InputState;
fn render(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
if area.is_empty() {
return;
}
let width = area.width as usize;
let (display_text, display_style) = if state.value.is_empty() {
(
self.placeholder.clone().unwrap_or_default(),
self.placeholder_style,
)
} else if let Some(mask_char) = self.mask {
(
mask_char.to_string().repeat(state.value.chars().count()),
self.style,
)
} else {
(state.value.clone(), self.style)
};
let cursor_pos = if state.value.is_empty() {
0
} else if self.mask.is_some() {
state.value[..state.cursor].chars().count()
} else {
unicode_width::UnicodeWidthStr::width(&state.value[..state.cursor])
};
if cursor_pos < state.scroll {
state.scroll = cursor_pos;
} else if cursor_pos >= state.scroll + width {
state.scroll = cursor_pos - width + 1;
}
let display_chars: Vec<char> = display_text.chars().collect();
let visible_start = state.scroll.min(display_chars.len());
let visible_end = (state.scroll + width).min(display_chars.len());
let mut x = area.x;
for (i, c) in display_chars[visible_start..visible_end].iter().enumerate() {
if x >= area.right() {
break;
}
let char_pos = visible_start + i;
let is_cursor = state.focused && !state.value.is_empty() && char_pos == cursor_pos;
let style = if is_cursor {
self.cursor_style
} else {
display_style
};
if let Some(cell) = buf.get_mut(x, area.y) {
cell.symbol = c.to_string();
cell.fg = style.fg;
cell.bg = style.bg;
cell.modifier = style.modifier;
}
let char_width = unicode_width::UnicodeWidthChar::width(*c).unwrap_or(1);
x = x.saturating_add(char_width as u16);
}
if state.focused && (state.value.is_empty() || cursor_pos >= display_chars.len()) {
let cursor_x = area.x + (cursor_pos - state.scroll).min(width - 1) as u16;
if cursor_x < area.right() {
if let Some(cell) = buf.get_mut(cursor_x, area.y) {
if cell.symbol == " " || display_text.is_empty() {
cell.symbol = " ".to_string();
}
cell.fg = self.cursor_style.fg;
cell.bg = self.cursor_style.bg;
cell.modifier = self.cursor_style.modifier;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_input_state_new() {
let state = InputState::new();
assert!(state.value.is_empty());
assert_eq!(state.cursor, 0);
}
#[test]
fn test_input_state_insert() {
let mut state = InputState::new();
state.insert('H');
state.insert('i');
assert_eq!(state.value, "Hi");
assert_eq!(state.cursor, 2);
}
#[test]
fn test_input_state_backspace() {
let mut state = InputState::with_value("Hello");
state.backspace();
assert_eq!(state.value, "Hell");
assert_eq!(state.cursor, 4);
}
#[test]
fn test_input_state_move() {
let mut state = InputState::with_value("Hello");
assert_eq!(state.cursor, 5);
state.move_left();
assert_eq!(state.cursor, 4);
state.move_home();
assert_eq!(state.cursor, 0);
state.move_right();
assert_eq!(state.cursor, 1);
state.move_end();
assert_eq!(state.cursor, 5);
}
#[test]
fn test_input_state_unicode() {
let mut state = InputState::with_value("日本語");
assert_eq!(state.cursor, 9);
state.move_left();
assert_eq!(state.cursor, 6);
state.move_left();
assert_eq!(state.cursor, 3); }
#[test]
fn test_input_state_take() {
let mut state = InputState::with_value("Hello");
let value = state.take();
assert_eq!(value, "Hello");
assert!(state.value.is_empty());
assert_eq!(state.cursor, 0);
}
#[test]
fn test_input_render() {
let input = Input::new();
let area = Rect::new(0, 0, 20, 1);
let mut buf = Buffer::new(area);
let mut state = InputState::with_value("Hello");
input.render(area, &mut buf, &mut state);
assert_eq!(buf.get(0, 0).unwrap().symbol, "H");
assert_eq!(buf.get(4, 0).unwrap().symbol, "o");
}
}