use crossterm::event::KeyCode;
use std::ops::Deref;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TextInput {
value: String,
cursor: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EditResult {
Changed,
Moved,
Ignored,
}
impl TextInput {
pub fn cursor(&self) -> usize {
self.cursor
}
fn char_len(&self) -> usize {
self.value.chars().count()
}
fn byte_at(&self, i: usize) -> usize {
self.value
.char_indices()
.nth(i)
.map(|(b, _)| b)
.unwrap_or(self.value.len())
}
pub fn insert(&mut self, c: char) {
let at = self.byte_at(self.cursor);
self.value.insert(at, c);
self.cursor += 1;
}
pub fn backspace(&mut self) -> bool {
if self.cursor == 0 {
return false;
}
let start = self.byte_at(self.cursor - 1);
let end = self.byte_at(self.cursor);
self.value.replace_range(start..end, "");
self.cursor -= 1;
true
}
pub fn delete(&mut self) -> bool {
if self.cursor >= self.char_len() {
return false;
}
let start = self.byte_at(self.cursor);
let end = self.byte_at(self.cursor + 1);
self.value.replace_range(start..end, "");
true
}
pub fn left(&mut self) -> bool {
if self.cursor == 0 {
return false;
}
self.cursor -= 1;
true
}
pub fn right(&mut self) -> bool {
if self.cursor >= self.char_len() {
return false;
}
self.cursor += 1;
true
}
pub fn home(&mut self) -> bool {
if self.cursor == 0 {
return false;
}
self.cursor = 0;
true
}
pub fn end(&mut self) -> bool {
let len = self.char_len();
if self.cursor == len {
return false;
}
self.cursor = len;
true
}
pub fn clear(&mut self) {
self.value.clear();
self.cursor = 0;
}
pub fn set(&mut self, value: impl Into<String>) {
self.value = value.into();
self.cursor = self.char_len();
}
pub fn apply_edit(&mut self, code: KeyCode) -> EditResult {
match code {
KeyCode::Char(c) => {
self.insert(c);
EditResult::Changed
}
KeyCode::Backspace => bool_to_change(self.backspace()),
KeyCode::Delete => bool_to_change(self.delete()),
KeyCode::Left => bool_to_move(self.left()),
KeyCode::Right => bool_to_move(self.right()),
KeyCode::Home => bool_to_move(self.home()),
KeyCode::End => bool_to_move(self.end()),
_ => EditResult::Ignored,
}
}
}
fn bool_to_change(changed: bool) -> EditResult {
if changed {
EditResult::Changed
} else {
EditResult::Ignored
}
}
fn bool_to_move(moved: bool) -> EditResult {
if moved {
EditResult::Moved
} else {
EditResult::Ignored
}
}
impl Deref for TextInput {
type Target = str;
fn deref(&self) -> &str {
&self.value
}
}
impl std::fmt::Display for TextInput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.value)
}
}
impl From<&str> for TextInput {
fn from(value: &str) -> Self {
let mut input = Self::default();
input.set(value.to_string());
input
}
}
impl From<String> for TextInput {
fn from(value: String) -> Self {
let mut input = Self::default();
input.set(value);
input
}
}
impl PartialEq<str> for TextInput {
fn eq(&self, other: &str) -> bool {
self.value == other
}
}
impl PartialEq<&str> for TextInput {
fn eq(&self, other: &&str) -> bool {
self.value == *other
}
}
#[cfg(test)]
mod tests {
use super::*;
fn typed(s: &str) -> TextInput {
let mut input = TextInput::default();
for c in s.chars() {
input.insert(c);
}
input
}
#[test]
fn insert_at_cursor_after_moving_left() {
let mut input = typed("ac");
assert!(input.left()); input.insert('b');
assert_eq!(&*input, "abc");
assert_eq!(input.cursor(), 2);
}
#[test]
fn backspace_removes_char_before_cursor() {
let mut input = typed("abc");
assert!(input.left()); assert!(input.backspace()); assert_eq!(&*input, "ac");
assert_eq!(input.cursor(), 1);
}
#[test]
fn backspace_at_start_is_noop() {
let mut input = typed("ab");
input.home();
assert!(!input.backspace());
assert_eq!(&*input, "ab");
}
#[test]
fn delete_removes_char_at_cursor() {
let mut input = typed("abc");
input.home(); assert!(input.delete()); assert_eq!(&*input, "bc");
assert_eq!(input.cursor(), 0);
}
#[test]
fn delete_at_end_is_noop() {
let input_end = typed("ab");
let mut input = input_end.clone();
assert!(!input.delete());
assert_eq!(input, input_end);
}
#[test]
fn left_right_clamp_at_bounds() {
let mut input = typed("ab");
assert!(!input.right()); assert!(input.left());
assert!(input.left());
assert!(!input.left()); assert!(input.right());
}
#[test]
fn home_and_end_move_to_bounds() {
let mut input = typed("abc");
assert!(input.home());
assert_eq!(input.cursor(), 0);
assert!(!input.home());
assert!(input.end());
assert_eq!(input.cursor(), 3);
assert!(!input.end());
}
#[test]
fn cjk_multibyte_insert_and_backspace() {
let mut input = typed("你好");
assert_eq!(input.cursor(), 2);
assert!(input.left());
input.insert('中');
assert_eq!(&*input, "你中好");
assert_eq!(input.cursor(), 2);
assert!(input.backspace());
assert_eq!(&*input, "你好");
assert_eq!(input.cursor(), 1);
}
#[test]
fn cjk_delete_at_cursor_removes_one_char_not_one_byte() {
let mut input = typed("A你B");
input.home();
assert!(input.right()); assert!(input.delete()); assert_eq!(&*input, "AB");
assert_eq!(input.cursor(), 1);
}
#[test]
fn apply_edit_dispatches_by_key_code() {
let mut input = TextInput::default();
assert_eq!(input.apply_edit(KeyCode::Char('a')), EditResult::Changed);
assert_eq!(input.apply_edit(KeyCode::Left), EditResult::Moved);
assert_eq!(input.apply_edit(KeyCode::Left), EditResult::Ignored);
assert_eq!(input.apply_edit(KeyCode::Backspace), EditResult::Ignored);
assert_eq!(input.apply_edit(KeyCode::Tab), EditResult::Ignored);
}
#[test]
fn set_replaces_value_and_moves_cursor_to_end() {
let mut input = typed("ab");
input.set("hello");
assert_eq!(&*input, "hello");
assert_eq!(input.cursor(), 5);
}
#[test]
fn clear_resets_value_and_cursor() {
let mut input = typed("abc");
input.clear();
assert_eq!(&*input, "");
assert_eq!(input.cursor(), 0);
}
#[test]
fn deref_and_equality_treat_it_like_a_str() {
let input = typed("abc");
assert!(!input.is_empty());
assert_eq!(input, "abc");
assert_eq!(&*input, "abc");
assert_eq!(format!("{input}"), "abc");
}
#[test]
fn from_str_and_string_place_cursor_at_end() {
let from_str: TextInput = "café".into();
assert_eq!(from_str.cursor(), 4);
let from_string: TextInput = "café".to_string().into();
assert_eq!(from_string.cursor(), 4);
}
}