use unicode_width::UnicodeWidthChar;
use crate::input::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::widgets::Widget;
#[derive(Clone, Debug)]
pub struct TextInput {
pub value: String,
pub cursor_position: usize,
pub style: Style,
pub cursor_style: Style,
pub placeholder: String,
pub placeholder_style: Style,
}
impl Default for TextInput {
fn default() -> Self {
Self {
value: String::new(),
cursor_position: 0,
style: Style::default().fg(Color::White),
cursor_style: Style::default().bg(Color::White).fg(Color::Black),
placeholder: String::new(),
placeholder_style: Style::default().fg(Color::DarkGray),
}
}
}
impl TextInput {
pub fn new() -> Self {
Self::default()
}
pub fn with_value(mut self, value: impl Into<String>) -> Self {
self.value = value.into();
self.cursor_position = self.value.len();
self
}
pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn set_value(&mut self, value: String) {
self.value = value;
self.cursor_position = self.value.len();
}
pub fn clear(&mut self) {
self.value.clear();
self.cursor_position = 0;
}
pub fn handle_event(&mut self, event: &Event) -> bool {
if let Event::Key(key) = event {
if key.kind != KeyEventKind::Press {
return false;
}
let has_control = key.modifiers.contains(KeyModifiers::CONTROL);
let has_alt = key.modifiers.contains(KeyModifiers::ALT);
match key.code {
KeyCode::Char(c) if !has_control && !has_alt => {
if c == '\x1b' {
return false;
}
self.value.insert(self.cursor_position, c);
self.cursor_position += 1;
return true;
}
KeyCode::Char('u') if has_control && self.cursor_position > 0 => {
self.value = self.value.chars().skip(self.cursor_position).collect();
self.cursor_position = 0;
return true;
}
KeyCode::Char('k') if has_control && self.cursor_position < self.value.len() => {
self.value = self.value.chars().take(self.cursor_position).collect();
return true;
}
KeyCode::Char('w') if has_control => {
return self.delete_word_backwards();
}
KeyCode::Backspace if has_control || has_alt => {
return self.delete_word_backwards();
}
KeyCode::Delete if has_control || has_alt => {
return self.delete_word_forwards();
}
KeyCode::Char('a') if has_control => {
self.cursor_position = 0;
return true;
}
KeyCode::Char('e') if has_control => {
self.cursor_position = self.value.len();
return true;
}
KeyCode::Char('f') if has_control && self.cursor_position < self.value.len() => {
self.cursor_position += self.value[self.cursor_position..]
.chars()
.next()
.map(|c| c.len_utf8())
.unwrap_or(0);
return true;
}
KeyCode::Char('b') if has_control && self.cursor_position > 0 => {
let prev = self.value[..self.cursor_position]
.chars()
.last()
.map(|c| c.len_utf8())
.unwrap_or(0);
self.cursor_position -= prev;
return true;
}
KeyCode::Backspace if self.cursor_position > 0 => {
self.value.remove(self.cursor_position - 1);
self.cursor_position -= 1;
return true;
}
KeyCode::Delete if self.cursor_position < self.value.len() => {
self.value.remove(self.cursor_position);
return true;
}
KeyCode::Left if self.cursor_position > 0 => {
let prev = self.value[..self.cursor_position]
.chars()
.last()
.map(|c| c.len_utf8())
.unwrap_or(0);
self.cursor_position -= prev;
return true;
}
KeyCode::Right if self.cursor_position < self.value.len() => {
let next = self.value[self.cursor_position..]
.chars()
.next()
.map(|c| c.len_utf8())
.unwrap_or(0);
self.cursor_position += next;
return true;
}
KeyCode::Home => {
self.cursor_position = 0;
return true;
}
KeyCode::End => {
self.cursor_position = self.value.len();
return true;
}
_ => {}
}
}
false
}
fn delete_word_backwards(&mut self) -> bool {
if self.cursor_position == 0 {
return false;
}
let mut i = self.cursor_position;
while i > 0 {
let Some(prev) = self.value[..i].chars().next_back() else {
break;
};
if prev.is_whitespace() {
i -= prev.len_utf8();
} else {
break;
}
}
while i > 0 {
let Some(prev) = self.value[..i].chars().next_back() else {
break;
};
if !prev.is_whitespace() {
i -= prev.len_utf8();
} else {
break;
}
}
let tail = self.value.split_off(self.cursor_position);
self.value.truncate(i);
self.value.push_str(&tail);
self.cursor_position = i;
true
}
fn delete_word_forwards(&mut self) -> bool {
if self.cursor_position >= self.value.len() {
return false;
}
let mut i = self.cursor_position;
while i < self.value.len() {
let Some(next) = self.value[i..].chars().next() else {
break;
};
if next.is_whitespace() {
i += next.len_utf8();
} else {
break;
}
}
while i < self.value.len() {
let Some(next) = self.value[i..].chars().next() else {
break;
};
if !next.is_whitespace() {
i += next.len_utf8();
} else {
break;
}
}
let tail = self.value.split_off(i);
self.value.truncate(self.cursor_position);
self.value.push_str(&tail);
true
}
}
impl Widget for &TextInput {
fn render(self, area: Rect, buf: &mut Buffer) {
let display_text = if self.value.is_empty() {
&self.placeholder
} else {
&self.value
};
let style = if self.value.is_empty() {
self.placeholder_style
} else {
self.style
};
buf.set_string(area.x, area.y, display_text, style);
let visual_offset = self
.value
.chars()
.take(self.cursor_position)
.map(|c| c.width().unwrap_or(1))
.sum::<usize>();
let cursor_x = area.x + visual_offset as u16;
if cursor_x < area.x + area.width {
if let Some(cell) = buf.cell_mut((cursor_x, area.y)) {
cell.set_style(self.cursor_style);
if self.cursor_position < self.value.len() {
let c = self.value.chars().nth(self.cursor_position).unwrap_or(' ');
cell.set_symbol(&c.to_string());
} else {
cell.set_symbol(" ");
}
}
}
}
}