envelope_cli/tui/widgets/
input.rs

1//! Text input widget
2//!
3//! A text input field with cursor support
4
5use ratatui::{
6    buffer::Buffer,
7    layout::Rect,
8    style::{Color, Style},
9    text::{Line, Span},
10    widgets::Widget,
11};
12
13/// A simple text input widget
14#[derive(Debug, Clone)]
15pub struct TextInput {
16    /// Current text content
17    pub content: String,
18    /// Cursor position
19    pub cursor: usize,
20    /// Whether the input is focused
21    pub focused: bool,
22    /// Placeholder text
23    pub placeholder: String,
24    /// Label
25    pub label: String,
26}
27
28impl TextInput {
29    /// Create a new text input
30    pub fn new() -> Self {
31        Self {
32            content: String::new(),
33            cursor: 0,
34            focused: false,
35            placeholder: String::new(),
36            label: String::new(),
37        }
38    }
39
40    /// Set the label
41    pub fn label(mut self, label: impl Into<String>) -> Self {
42        self.label = label.into();
43        self
44    }
45
46    /// Set the placeholder
47    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
48        self.placeholder = placeholder.into();
49        self
50    }
51
52    /// Set focused state
53    pub fn focused(mut self, focused: bool) -> Self {
54        self.focused = focused;
55        self
56    }
57
58    /// Set content
59    pub fn content(mut self, content: impl Into<String>) -> Self {
60        self.content = content.into();
61        self.cursor = self.content.len();
62        self
63    }
64
65    /// Insert a character at the cursor
66    pub fn insert(&mut self, c: char) {
67        self.content.insert(self.cursor, c);
68        self.cursor += 1;
69    }
70
71    /// Delete character before cursor
72    pub fn backspace(&mut self) {
73        if self.cursor > 0 {
74            self.cursor -= 1;
75            self.content.remove(self.cursor);
76        }
77    }
78
79    /// Delete character at cursor
80    pub fn delete(&mut self) {
81        if self.cursor < self.content.len() {
82            self.content.remove(self.cursor);
83        }
84    }
85
86    /// Move cursor left
87    pub fn move_left(&mut self) {
88        if self.cursor > 0 {
89            self.cursor -= 1;
90        }
91    }
92
93    /// Move cursor right
94    pub fn move_right(&mut self) {
95        if self.cursor < self.content.len() {
96            self.cursor += 1;
97        }
98    }
99
100    /// Move cursor to start
101    pub fn move_start(&mut self) {
102        self.cursor = 0;
103    }
104
105    /// Move cursor to end
106    pub fn move_end(&mut self) {
107        self.cursor = self.content.len();
108    }
109
110    /// Clear the content
111    pub fn clear(&mut self) {
112        self.content.clear();
113        self.cursor = 0;
114    }
115
116    /// Get the current content
117    pub fn value(&self) -> &str {
118        &self.content
119    }
120}
121
122impl Default for TextInput {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128impl Widget for TextInput {
129    fn render(self, area: Rect, buf: &mut Buffer) {
130        // Calculate areas
131        let label_width = if self.label.is_empty() {
132            0
133        } else {
134            self.label.len() + 2
135        };
136
137        let input_start = area.x + label_width as u16;
138        let _input_width = area.width.saturating_sub(label_width as u16);
139
140        // Render label if present
141        if !self.label.is_empty() {
142            let label_line = Line::from(vec![
143                Span::styled(&self.label, Style::default().fg(Color::Cyan)),
144                Span::raw(": "),
145            ]);
146            buf.set_line(area.x, area.y, &label_line, label_width as u16);
147        }
148
149        // Determine display text
150        let display_text = if self.content.is_empty() && !self.focused {
151            self.placeholder.clone()
152        } else {
153            self.content.clone()
154        };
155
156        let text_style = if self.content.is_empty() && !self.focused {
157            Style::default().fg(Color::Yellow)
158        } else if self.focused {
159            Style::default().fg(Color::White)
160        } else {
161            Style::default().fg(Color::Yellow)
162        };
163
164        // Render text
165        buf.set_string(input_start, area.y, &display_text, text_style);
166
167        // Render cursor if focused
168        if self.focused {
169            let cursor_x = input_start + self.cursor as u16;
170            if cursor_x < area.x + area.width {
171                let cursor_char = if self.cursor < self.content.len() {
172                    self.content.chars().nth(self.cursor).unwrap_or('_')
173                } else {
174                    '_'
175                };
176                buf.set_string(
177                    cursor_x,
178                    area.y,
179                    cursor_char.to_string(),
180                    Style::default().fg(Color::Black).bg(Color::Cyan),
181                );
182            }
183        }
184    }
185}