1use unicode_width::UnicodeWidthStr;
2
3pub struct TextInputState {
19 pub value: String,
21 pub cursor: usize,
23 pub placeholder: String,
25 pub max_length: Option<usize>,
26}
27
28impl TextInputState {
29 pub fn new() -> Self {
31 Self {
32 value: String::new(),
33 cursor: 0,
34 placeholder: String::new(),
35 max_length: None,
36 }
37 }
38
39 pub fn with_placeholder(p: impl Into<String>) -> Self {
41 Self {
42 placeholder: p.into(),
43 ..Self::new()
44 }
45 }
46
47 pub fn max_length(mut self, len: usize) -> Self {
48 self.max_length = Some(len);
49 self
50 }
51}
52
53impl Default for TextInputState {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59pub struct ToastState {
65 pub messages: Vec<ToastMessage>,
67}
68
69pub struct ToastMessage {
71 pub text: String,
73 pub level: ToastLevel,
75 pub created_tick: u64,
77 pub duration_ticks: u64,
79}
80
81pub enum ToastLevel {
83 Info,
85 Success,
87 Warning,
89 Error,
91}
92
93impl ToastState {
94 pub fn new() -> Self {
96 Self {
97 messages: Vec::new(),
98 }
99 }
100
101 pub fn info(&mut self, text: impl Into<String>, tick: u64) {
103 self.push(text, ToastLevel::Info, tick, 30);
104 }
105
106 pub fn success(&mut self, text: impl Into<String>, tick: u64) {
108 self.push(text, ToastLevel::Success, tick, 30);
109 }
110
111 pub fn warning(&mut self, text: impl Into<String>, tick: u64) {
113 self.push(text, ToastLevel::Warning, tick, 50);
114 }
115
116 pub fn error(&mut self, text: impl Into<String>, tick: u64) {
118 self.push(text, ToastLevel::Error, tick, 80);
119 }
120
121 pub fn push(
123 &mut self,
124 text: impl Into<String>,
125 level: ToastLevel,
126 tick: u64,
127 duration_ticks: u64,
128 ) {
129 self.messages.push(ToastMessage {
130 text: text.into(),
131 level,
132 created_tick: tick,
133 duration_ticks,
134 });
135 }
136
137 pub fn cleanup(&mut self, current_tick: u64) {
141 self.messages.retain(|message| {
142 current_tick < message.created_tick.saturating_add(message.duration_ticks)
143 });
144 }
145}
146
147impl Default for ToastState {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153pub struct TextareaState {
158 pub lines: Vec<String>,
160 pub cursor_row: usize,
162 pub cursor_col: usize,
164 pub max_length: Option<usize>,
165}
166
167impl TextareaState {
168 pub fn new() -> Self {
170 Self {
171 lines: vec![String::new()],
172 cursor_row: 0,
173 cursor_col: 0,
174 max_length: None,
175 }
176 }
177
178 pub fn value(&self) -> String {
180 self.lines.join("\n")
181 }
182
183 pub fn set_value(&mut self, text: impl Into<String>) {
187 let value = text.into();
188 self.lines = value.split('\n').map(str::to_string).collect();
189 if self.lines.is_empty() {
190 self.lines.push(String::new());
191 }
192 self.cursor_row = 0;
193 self.cursor_col = 0;
194 }
195
196 pub fn max_length(mut self, len: usize) -> Self {
197 self.max_length = Some(len);
198 self
199 }
200}
201
202impl Default for TextareaState {
203 fn default() -> Self {
204 Self::new()
205 }
206}
207
208pub struct SpinnerState {
214 chars: Vec<char>,
215}
216
217impl SpinnerState {
218 pub fn dots() -> Self {
222 Self {
223 chars: vec!['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
224 }
225 }
226
227 pub fn line() -> Self {
231 Self {
232 chars: vec!['|', '/', '-', '\\'],
233 }
234 }
235
236 pub fn frame(&self, tick: u64) -> char {
238 if self.chars.is_empty() {
239 return ' ';
240 }
241 self.chars[tick as usize % self.chars.len()]
242 }
243}
244
245impl Default for SpinnerState {
246 fn default() -> Self {
247 Self::dots()
248 }
249}
250
251pub struct ListState {
256 pub items: Vec<String>,
258 pub selected: usize,
260}
261
262impl ListState {
263 pub fn new(items: Vec<impl Into<String>>) -> Self {
265 Self {
266 items: items.into_iter().map(Into::into).collect(),
267 selected: 0,
268 }
269 }
270
271 pub fn selected_item(&self) -> Option<&str> {
273 self.items.get(self.selected).map(String::as_str)
274 }
275}
276
277pub struct TabsState {
282 pub labels: Vec<String>,
284 pub selected: usize,
286}
287
288impl TabsState {
289 pub fn new(labels: Vec<impl Into<String>>) -> Self {
291 Self {
292 labels: labels.into_iter().map(Into::into).collect(),
293 selected: 0,
294 }
295 }
296
297 pub fn selected_label(&self) -> Option<&str> {
299 self.labels.get(self.selected).map(String::as_str)
300 }
301}
302
303pub struct TableState {
309 pub headers: Vec<String>,
311 pub rows: Vec<Vec<String>>,
313 pub selected: usize,
315 column_widths: Vec<u32>,
316 dirty: bool,
317}
318
319impl TableState {
320 pub fn new(headers: Vec<impl Into<String>>, rows: Vec<Vec<impl Into<String>>>) -> Self {
322 let headers: Vec<String> = headers.into_iter().map(Into::into).collect();
323 let rows: Vec<Vec<String>> = rows
324 .into_iter()
325 .map(|r| r.into_iter().map(Into::into).collect())
326 .collect();
327 let mut state = Self {
328 headers,
329 rows,
330 selected: 0,
331 column_widths: Vec::new(),
332 dirty: true,
333 };
334 state.recompute_widths();
335 state
336 }
337
338 pub fn set_rows(&mut self, rows: Vec<Vec<impl Into<String>>>) {
343 self.rows = rows
344 .into_iter()
345 .map(|r| r.into_iter().map(Into::into).collect())
346 .collect();
347 self.dirty = true;
348 self.selected = self.selected.min(self.rows.len().saturating_sub(1));
349 }
350
351 pub fn selected_row(&self) -> Option<&[String]> {
353 self.rows.get(self.selected).map(|r| r.as_slice())
354 }
355
356 pub(crate) fn recompute_widths(&mut self) {
357 let col_count = self.headers.len();
358 self.column_widths = vec![0u32; col_count];
359 for (i, header) in self.headers.iter().enumerate() {
360 self.column_widths[i] = UnicodeWidthStr::width(header.as_str()) as u32;
361 }
362 for row in &self.rows {
363 for (i, cell) in row.iter().enumerate() {
364 if i < col_count {
365 let w = UnicodeWidthStr::width(cell.as_str()) as u32;
366 self.column_widths[i] = self.column_widths[i].max(w);
367 }
368 }
369 }
370 self.dirty = false;
371 }
372
373 pub(crate) fn column_widths(&self) -> &[u32] {
374 &self.column_widths
375 }
376
377 pub(crate) fn is_dirty(&self) -> bool {
378 self.dirty
379 }
380}
381
382pub struct ScrollState {
388 pub offset: usize,
390 content_height: u32,
391 viewport_height: u32,
392}
393
394impl ScrollState {
395 pub fn new() -> Self {
397 Self {
398 offset: 0,
399 content_height: 0,
400 viewport_height: 0,
401 }
402 }
403
404 pub fn can_scroll_up(&self) -> bool {
406 self.offset > 0
407 }
408
409 pub fn can_scroll_down(&self) -> bool {
411 (self.offset as u32) + self.viewport_height < self.content_height
412 }
413
414 pub fn scroll_up(&mut self, amount: usize) {
416 self.offset = self.offset.saturating_sub(amount);
417 }
418
419 pub fn scroll_down(&mut self, amount: usize) {
421 let max_offset = self.content_height.saturating_sub(self.viewport_height) as usize;
422 self.offset = (self.offset + amount).min(max_offset);
423 }
424
425 pub(crate) fn set_bounds(&mut self, content_height: u32, viewport_height: u32) {
426 self.content_height = content_height;
427 self.viewport_height = viewport_height;
428 }
429}
430
431impl Default for ScrollState {
432 fn default() -> Self {
433 Self::new()
434 }
435}