use crate::utils::text_input::TextInput;
use crate::utils::{
disabled_border_style, disabled_text_style, focused_border_style, input_placeholder_style,
input_text_style, unfocused_border_style,
};
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Paragraph};
pub struct TextInputWidget<'a> {
input: &'a TextInput,
title: Option<&'a str>,
placeholder: Option<&'a str>,
focused: bool,
disabled: bool,
title_alignment: Alignment,
masked: bool,
block: Option<Block<'a>>,
}
impl<'a> TextInputWidget<'a> {
#[must_use]
pub fn new(input: &'a TextInput) -> Self {
Self {
input,
title: None,
placeholder: None,
focused: false,
disabled: false,
title_alignment: Alignment::Left,
masked: false,
block: None,
}
}
#[must_use]
pub fn title(mut self, title: &'a str) -> Self {
self.title = Some(title);
self
}
#[must_use]
pub fn placeholder(mut self, placeholder: &'a str) -> Self {
self.placeholder = Some(placeholder);
self
}
#[must_use]
pub fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
#[must_use]
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
#[must_use]
pub fn title_alignment(mut self, alignment: Alignment) -> Self {
self.title_alignment = alignment;
self
}
#[must_use]
pub fn masked(mut self, masked: bool) -> Self {
self.masked = masked;
self
}
#[must_use]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
fn display_text(&self) -> String {
let text = self.input.text();
if text.is_empty() {
self.placeholder.unwrap_or("").to_string()
} else if self.masked {
"•".repeat(text.chars().count())
} else {
text.to_string()
}
}
fn text_style(&self) -> Style {
if self.disabled {
disabled_text_style()
} else if self.input.is_empty() {
input_placeholder_style()
} else {
input_text_style()
}
}
fn border_style(&self) -> Style {
if self.disabled {
disabled_border_style()
} else if self.focused {
focused_border_style()
} else {
unfocused_border_style()
}
}
fn create_block(&self) -> Block<'a> {
if let Some(block) = &self.block {
block.clone().border_style(self.border_style())
} else {
let mut block = Block::default()
.borders(Borders::ALL)
.border_type(crate::styles::theme().border_type(self.focused))
.border_style(self.border_style())
.style(crate::styles::theme().background_style());
if let Some(title) = self.title {
block = block
.title(format!(" {title} "))
.title_alignment(self.title_alignment);
}
block
}
}
}
impl Widget for TextInputWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let block = self.create_block();
let inner = block.inner(area);
let paragraph = Paragraph::new(self.display_text())
.block(block)
.style(self.text_style());
paragraph.render(area, buf);
if self.focused && !self.disabled {
let cursor_pos = self.input.cursor();
let clamped_cursor = cursor_pos.min(self.input.text().chars().count());
let x = inner.x + clamped_cursor.min(inner.width as usize) as u16;
let y = inner.y;
if let Some(cell) = buf.cell_mut((x, y)) {
cell.set_skip(false);
}
}
}
}
pub trait TextInputWidgetExt {
fn render_text_input_widget(&mut self, widget: TextInputWidget, area: Rect);
}
impl TextInputWidgetExt for Frame<'_> {
fn render_text_input_widget(&mut self, widget: TextInputWidget, area: Rect) {
let focused = widget.focused;
let disabled = widget.disabled;
let cursor_pos = widget.input.cursor();
let text = widget.input.text();
let block = widget.create_block();
let inner = block.inner(area);
self.render_widget(widget, area);
if focused && !disabled {
let clamped_cursor = cursor_pos.min(text.chars().count());
let x = inner.x + clamped_cursor.min(inner.width as usize) as u16;
let y = inner.y;
self.set_cursor_position((x, y));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_widget_creation() {
let input = TextInput::new();
let widget = TextInputWidget::new(&input);
assert!(!widget.focused);
assert!(!widget.disabled);
assert!(!widget.masked);
}
#[test]
fn test_widget_builder() {
let input = TextInput::with_text("test");
let widget = TextInputWidget::new(&input)
.title("Test Input")
.placeholder("Enter text")
.focused(true)
.disabled(false)
.masked(true);
assert!(widget.focused);
assert!(!widget.disabled);
assert!(widget.masked);
assert_eq!(widget.title, Some("Test Input"));
assert_eq!(widget.placeholder, Some("Enter text"));
}
#[test]
fn test_display_text_empty_with_placeholder() {
let input = TextInput::new();
let widget = TextInputWidget::new(&input).placeholder("Enter text...");
assert_eq!(widget.display_text(), "Enter text...");
}
#[test]
fn test_display_text_normal() {
let input = TextInput::with_text("hello");
let widget = TextInputWidget::new(&input);
assert_eq!(widget.display_text(), "hello");
}
#[test]
fn test_display_text_masked() {
let input = TextInput::with_text("password123");
let widget = TextInputWidget::new(&input).masked(true);
assert_eq!(widget.display_text(), "•••••••••••");
assert_eq!(widget.display_text().chars().count(), 11);
}
#[test]
fn test_text_style_disabled() {
let input = TextInput::with_text("test");
let widget = TextInputWidget::new(&input).disabled(true);
let _ = widget.text_style();
}
#[test]
fn test_border_style_focused() {
let input = TextInput::new();
let widget = TextInputWidget::new(&input).focused(true);
let _ = widget.border_style();
}
}