use crate::element::{Component, Element};
use crate::style::{Color, Modifier, Style};
#[derive(Debug, Clone)]
pub struct TextInputProps {
pub value: String,
pub placeholder: Option<String>,
pub cursor: usize,
pub selection_anchor: Option<usize>,
pub focused: bool,
pub mask: bool,
pub mask_char: char,
pub color: Option<Color>,
pub placeholder_color: Option<Color>,
pub cursor_color: Option<Color>,
pub selection_color: Option<Color>,
pub bold: bool,
pub dim: bool,
pub min_width: Option<usize>,
}
impl Default for TextInputProps {
fn default() -> Self {
Self {
value: String::new(),
placeholder: None,
cursor: 0,
selection_anchor: None,
focused: true,
mask: false,
mask_char: '•',
color: None,
placeholder_color: None,
cursor_color: None,
selection_color: None,
bold: false,
dim: false,
min_width: None,
}
}
}
impl TextInputProps {
pub fn new(value: impl Into<String>) -> Self {
let value = value.into();
let cursor = value.len();
Self {
value,
cursor,
..Default::default()
}
}
#[must_use]
pub fn value(mut self, value: impl Into<String>) -> Self {
self.value = value.into();
self.cursor = self.cursor.min(self.value.len());
self
}
#[must_use]
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
#[must_use]
pub fn cursor(mut self, cursor: usize) -> Self {
self.cursor = cursor.min(self.value.len());
self
}
#[must_use]
pub fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
#[must_use]
pub fn mask(mut self) -> Self {
self.mask = true;
self
}
#[must_use]
pub fn mask_char(mut self, c: char) -> Self {
self.mask_char = c;
self
}
#[must_use]
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
#[must_use]
pub fn placeholder_color(mut self, color: Color) -> Self {
self.placeholder_color = Some(color);
self
}
#[must_use]
pub fn cursor_color(mut self, color: Color) -> Self {
self.cursor_color = Some(color);
self
}
#[must_use]
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
#[must_use]
pub fn dim(mut self) -> Self {
self.dim = true;
self
}
#[must_use]
pub fn min_width(mut self, width: usize) -> Self {
self.min_width = Some(width);
self
}
#[must_use]
pub fn selection(mut self, anchor: Option<usize>) -> Self {
self.selection_anchor = anchor;
self
}
pub fn selection_range(&self) -> Option<(usize, usize)> {
self.selection_anchor.and_then(|anchor| {
if anchor == self.cursor {
None } else if anchor <= self.cursor {
Some((anchor, self.cursor))
} else {
Some((self.cursor, anchor))
}
})
}
pub fn render_string(&self) -> String {
if self.value.is_empty() {
if let Some(ref placeholder) = self.placeholder {
if self.focused {
format!("▏{}", placeholder)
} else {
format!(" {}", placeholder)
}
} else if self.focused {
"▏".to_string()
} else {
" ".to_string()
}
} else {
let display_value = if self.mask {
self.mask_char
.to_string()
.repeat(self.value.chars().count())
} else {
self.value.clone()
};
if self.focused {
let char_count = display_value.chars().count();
let cursor_pos = self.cursor.min(char_count);
if let Some((sel_start, sel_end)) = self.selection_range() {
let sel_start = sel_start.min(char_count);
let sel_end = sel_end.min(char_count);
let before: String = display_value.chars().take(sel_start).collect();
let selected: String = display_value
.chars()
.skip(sel_start)
.take(sel_end - sel_start)
.collect();
let after: String = display_value.chars().skip(sel_end).collect();
if cursor_pos == sel_end {
format!("{}[{}]▏{}", before, selected, after)
} else {
format!("{}▏[{}]{}", before, selected, after)
}
} else {
let before: String = display_value.chars().take(cursor_pos).collect();
let after: String = display_value.chars().skip(cursor_pos).collect();
format!("{}▏{}", before, after)
}
} else {
display_value
}
}
}
}
pub struct TextInput;
impl Component for TextInput {
type Props = TextInputProps;
fn render(props: &Self::Props) -> Element {
let content = props.render_string();
let mut style = Style::new();
if props.value.is_empty() && props.placeholder.is_some() && !props.focused {
if let Some(color) = props.placeholder_color {
style = style.fg(color);
} else {
style = style.add_modifier(Modifier::DIM);
}
} else {
if let Some(color) = props.color {
style = style.fg(color);
}
if props.bold {
style = style.add_modifier(Modifier::BOLD);
}
if props.dim {
style = style.add_modifier(Modifier::DIM);
}
}
Element::styled_text(&content, style)
}
}
#[derive(Debug, Clone, Default)]
pub struct TextInputState {
pub value: String,
pub cursor: usize,
pub selection_anchor: Option<usize>,
}
impl TextInputState {
pub fn new() -> Self {
Self::default()
}
pub fn with_value(value: impl Into<String>) -> Self {
let value = value.into();
let cursor = value.len();
Self {
value,
cursor,
selection_anchor: None,
}
}
pub fn insert(&mut self, c: char) {
self.delete_selection();
self.value.insert(self.cursor, c);
self.cursor += 1;
}
pub fn insert_str(&mut self, s: &str) {
self.delete_selection();
self.value.insert_str(self.cursor, s);
self.cursor += s.len();
}
pub fn backspace(&mut self) -> bool {
if self.has_selection() {
self.delete_selection();
return true;
}
if self.cursor > 0 {
self.cursor -= 1;
self.value.remove(self.cursor);
true
} else {
false
}
}
pub fn delete(&mut self) -> bool {
if self.has_selection() {
self.delete_selection();
return true;
}
if self.cursor < self.value.len() {
self.value.remove(self.cursor);
true
} else {
false
}
}
pub fn move_left(&mut self) -> bool {
self.clear_selection();
if self.cursor > 0 {
self.cursor -= 1;
true
} else {
false
}
}
pub fn move_right(&mut self) -> bool {
self.clear_selection();
if self.cursor < self.value.len() {
self.cursor += 1;
true
} else {
false
}
}
pub fn move_home(&mut self) {
self.clear_selection();
self.cursor = 0;
}
pub fn move_end(&mut self) {
self.clear_selection();
self.cursor = self.value.len();
}
pub fn clear(&mut self) {
self.value.clear();
self.cursor = 0;
}
pub fn set_value(&mut self, value: impl Into<String>) {
self.value = value.into();
self.cursor = self.value.len();
}
pub fn value(&self) -> &str {
&self.value
}
pub fn is_empty(&self) -> bool {
self.value.is_empty()
}
pub fn has_selection(&self) -> bool {
self.selection_anchor.is_some() && self.selection_anchor != Some(self.cursor)
}
pub fn selection_range(&self) -> Option<(usize, usize)> {
self.selection_anchor.map(|anchor| {
if anchor <= self.cursor {
(anchor, self.cursor)
} else {
(self.cursor, anchor)
}
})
}
pub fn selected_text(&self) -> Option<&str> {
self.selection_range()
.map(|(start, end)| &self.value[start..end])
}
pub fn clear_selection(&mut self) {
self.selection_anchor = None;
}
pub fn delete_selection(&mut self) -> Option<String> {
if let Some((start, end)) = self.selection_range() {
if start != end {
let deleted: String = self.value.drain(start..end).collect();
self.cursor = start;
self.selection_anchor = None;
return Some(deleted);
}
}
self.selection_anchor = None;
None
}
pub fn select_all(&mut self) {
self.selection_anchor = Some(0);
self.cursor = self.value.len();
}
pub fn select_left(&mut self) -> bool {
if self.selection_anchor.is_none() {
self.selection_anchor = Some(self.cursor);
}
if self.cursor > 0 {
self.cursor -= 1;
true
} else {
false
}
}
pub fn select_right(&mut self) -> bool {
if self.selection_anchor.is_none() {
self.selection_anchor = Some(self.cursor);
}
if self.cursor < self.value.len() {
self.cursor += 1;
true
} else {
false
}
}
pub fn select_to_home(&mut self) {
if self.selection_anchor.is_none() {
self.selection_anchor = Some(self.cursor);
}
self.cursor = 0;
}
pub fn select_to_end(&mut self) {
if self.selection_anchor.is_none() {
self.selection_anchor = Some(self.cursor);
}
self.cursor = self.value.len();
}
pub fn to_props(&self) -> TextInputProps {
TextInputProps {
value: self.value.clone(),
cursor: self.cursor,
selection_anchor: self.selection_anchor,
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_input_props_default() {
let props = TextInputProps::default();
assert!(props.value.is_empty());
assert_eq!(props.cursor, 0);
assert!(props.focused);
assert!(!props.mask);
}
#[test]
fn test_text_input_props_new() {
let props = TextInputProps::new("Hello");
assert_eq!(props.value, "Hello");
assert_eq!(props.cursor, 5); }
#[test]
fn test_text_input_props_builder() {
let props = TextInputProps::new("test")
.placeholder("Enter text")
.cursor(2)
.color(Color::Green)
.mask();
assert_eq!(props.value, "test");
assert_eq!(props.placeholder, Some("Enter text".to_string()));
assert_eq!(props.cursor, 2);
assert_eq!(props.color, Some(Color::Green));
assert!(props.mask);
}
#[test]
fn test_text_input_render_string_empty() {
let props = TextInputProps::default().focused(true);
assert_eq!(props.render_string(), "▏");
}
#[test]
fn test_text_input_render_string_with_placeholder() {
let props = TextInputProps::default()
.placeholder("Type here")
.focused(true);
assert_eq!(props.render_string(), "▏Type here");
}
#[test]
fn test_text_input_render_string_with_value() {
let props = TextInputProps::new("Hello").cursor(2).focused(true);
assert_eq!(props.render_string(), "He▏llo");
}
#[test]
fn test_text_input_render_string_cursor_at_end() {
let props = TextInputProps::new("Hello").focused(true);
assert_eq!(props.render_string(), "Hello▏");
}
#[test]
fn test_text_input_render_string_not_focused() {
let props = TextInputProps::new("Hello").focused(false);
assert_eq!(props.render_string(), "Hello");
}
#[test]
fn test_text_input_render_string_placeholder_not_focused() {
let props = TextInputProps::default()
.placeholder("Type here")
.focused(false);
assert_eq!(props.render_string(), " Type here");
}
#[test]
fn test_text_input_render_string_masked() {
let props = TextInputProps::new("secret").mask().focused(true);
assert_eq!(props.render_string(), "••••••▏");
}
#[test]
fn test_text_input_render_string_masked_custom_char() {
let props = TextInputProps::new("abc")
.mask()
.mask_char('•')
.focused(false);
assert_eq!(props.render_string(), "•••");
}
#[test]
fn test_text_input_state_new() {
let state = TextInputState::new();
assert!(state.is_empty());
assert_eq!(state.cursor, 0);
}
#[test]
fn test_text_input_state_with_value() {
let state = TextInputState::with_value("Hello");
assert_eq!(state.value(), "Hello");
assert_eq!(state.cursor, 5);
}
#[test]
fn test_text_input_state_insert() {
let mut state = TextInputState::new();
state.insert('H');
state.insert('i');
assert_eq!(state.value(), "Hi");
assert_eq!(state.cursor, 2);
}
#[test]
fn test_text_input_state_backspace() {
let mut state = TextInputState::with_value("Hello");
assert!(state.backspace());
assert_eq!(state.value(), "Hell");
assert_eq!(state.cursor, 4);
}
#[test]
fn test_text_input_state_backspace_at_start() {
let mut state = TextInputState::with_value("Hello");
state.cursor = 0;
assert!(!state.backspace());
assert_eq!(state.value(), "Hello");
}
#[test]
fn test_text_input_state_delete() {
let mut state = TextInputState::with_value("Hello");
state.cursor = 0;
assert!(state.delete());
assert_eq!(state.value(), "ello");
}
#[test]
fn test_text_input_state_delete_at_end() {
let mut state = TextInputState::with_value("Hello");
assert!(!state.delete());
assert_eq!(state.value(), "Hello");
}
#[test]
fn test_text_input_state_move_left() {
let mut state = TextInputState::with_value("Hello");
assert!(state.move_left());
assert_eq!(state.cursor, 4);
}
#[test]
fn test_text_input_state_move_right() {
let mut state = TextInputState::with_value("Hello");
state.cursor = 0;
assert!(state.move_right());
assert_eq!(state.cursor, 1);
}
#[test]
fn test_text_input_state_move_home_end() {
let mut state = TextInputState::with_value("Hello");
state.move_home();
assert_eq!(state.cursor, 0);
state.move_end();
assert_eq!(state.cursor, 5);
}
#[test]
fn test_text_input_state_clear() {
let mut state = TextInputState::with_value("Hello");
state.clear();
assert!(state.is_empty());
assert_eq!(state.cursor, 0);
}
#[test]
fn test_text_input_component_render() {
let props = TextInputProps::new("Test").focused(true);
let elem = TextInput::render(&props);
match elem {
Element::Text { content, .. } => {
assert_eq!(content, "Test▏");
}
_ => panic!("Expected Text element"),
}
}
}