1use std::collections::HashSet;
8use unicode_width::UnicodeWidthStr;
9
10type FormValidator = fn(&str) -> Result<(), String>;
11
12pub struct TextInputState {
28 pub value: String,
30 pub cursor: usize,
32 pub placeholder: String,
34 pub max_length: Option<usize>,
36 pub validation_error: Option<String>,
38 pub masked: bool,
40}
41
42impl TextInputState {
43 pub fn new() -> Self {
45 Self {
46 value: String::new(),
47 cursor: 0,
48 placeholder: String::new(),
49 max_length: None,
50 validation_error: None,
51 masked: false,
52 }
53 }
54
55 pub fn with_placeholder(p: impl Into<String>) -> Self {
57 Self {
58 placeholder: p.into(),
59 ..Self::new()
60 }
61 }
62
63 pub fn max_length(mut self, len: usize) -> Self {
65 self.max_length = Some(len);
66 self
67 }
68
69 pub fn validate(&mut self, validator: impl Fn(&str) -> Result<(), String>) {
74 self.validation_error = validator(&self.value).err();
75 }
76}
77
78impl Default for TextInputState {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84pub struct FormField {
86 pub label: String,
88 pub input: TextInputState,
90 pub error: Option<String>,
92}
93
94impl FormField {
95 pub fn new(label: impl Into<String>) -> Self {
97 Self {
98 label: label.into(),
99 input: TextInputState::new(),
100 error: None,
101 }
102 }
103
104 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
106 self.input.placeholder = p.into();
107 self
108 }
109}
110
111pub struct FormState {
113 pub fields: Vec<FormField>,
115 pub submitted: bool,
117}
118
119impl FormState {
120 pub fn new() -> Self {
122 Self {
123 fields: Vec::new(),
124 submitted: false,
125 }
126 }
127
128 pub fn field(mut self, field: FormField) -> Self {
130 self.fields.push(field);
131 self
132 }
133
134 pub fn validate(&mut self, validators: &[FormValidator]) -> bool {
138 let mut all_valid = true;
139 for (i, field) in self.fields.iter_mut().enumerate() {
140 if let Some(validator) = validators.get(i) {
141 match validator(&field.input.value) {
142 Ok(()) => field.error = None,
143 Err(msg) => {
144 field.error = Some(msg);
145 all_valid = false;
146 }
147 }
148 }
149 }
150 all_valid
151 }
152
153 pub fn value(&self, index: usize) -> &str {
155 self.fields
156 .get(index)
157 .map(|f| f.input.value.as_str())
158 .unwrap_or("")
159 }
160}
161
162impl Default for FormState {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168pub struct ToastState {
174 pub messages: Vec<ToastMessage>,
176}
177
178pub struct ToastMessage {
180 pub text: String,
182 pub level: ToastLevel,
184 pub created_tick: u64,
186 pub duration_ticks: u64,
188}
189
190pub enum ToastLevel {
192 Info,
194 Success,
196 Warning,
198 Error,
200}
201
202impl ToastState {
203 pub fn new() -> Self {
205 Self {
206 messages: Vec::new(),
207 }
208 }
209
210 pub fn info(&mut self, text: impl Into<String>, tick: u64) {
212 self.push(text, ToastLevel::Info, tick, 30);
213 }
214
215 pub fn success(&mut self, text: impl Into<String>, tick: u64) {
217 self.push(text, ToastLevel::Success, tick, 30);
218 }
219
220 pub fn warning(&mut self, text: impl Into<String>, tick: u64) {
222 self.push(text, ToastLevel::Warning, tick, 50);
223 }
224
225 pub fn error(&mut self, text: impl Into<String>, tick: u64) {
227 self.push(text, ToastLevel::Error, tick, 80);
228 }
229
230 pub fn push(
232 &mut self,
233 text: impl Into<String>,
234 level: ToastLevel,
235 tick: u64,
236 duration_ticks: u64,
237 ) {
238 self.messages.push(ToastMessage {
239 text: text.into(),
240 level,
241 created_tick: tick,
242 duration_ticks,
243 });
244 }
245
246 pub fn cleanup(&mut self, current_tick: u64) {
250 self.messages.retain(|message| {
251 current_tick < message.created_tick.saturating_add(message.duration_ticks)
252 });
253 }
254}
255
256impl Default for ToastState {
257 fn default() -> Self {
258 Self::new()
259 }
260}
261
262pub struct TextareaState {
267 pub lines: Vec<String>,
269 pub cursor_row: usize,
271 pub cursor_col: usize,
273 pub max_length: Option<usize>,
275 pub wrap_width: Option<u32>,
277 pub scroll_offset: usize,
279}
280
281impl TextareaState {
282 pub fn new() -> Self {
284 Self {
285 lines: vec![String::new()],
286 cursor_row: 0,
287 cursor_col: 0,
288 max_length: None,
289 wrap_width: None,
290 scroll_offset: 0,
291 }
292 }
293
294 pub fn value(&self) -> String {
296 self.lines.join("\n")
297 }
298
299 pub fn set_value(&mut self, text: impl Into<String>) {
303 let value = text.into();
304 self.lines = value.split('\n').map(str::to_string).collect();
305 if self.lines.is_empty() {
306 self.lines.push(String::new());
307 }
308 self.cursor_row = 0;
309 self.cursor_col = 0;
310 self.scroll_offset = 0;
311 }
312
313 pub fn max_length(mut self, len: usize) -> Self {
315 self.max_length = Some(len);
316 self
317 }
318
319 pub fn word_wrap(mut self, width: u32) -> Self {
321 self.wrap_width = Some(width);
322 self
323 }
324}
325
326impl Default for TextareaState {
327 fn default() -> Self {
328 Self::new()
329 }
330}
331
332pub struct SpinnerState {
338 chars: Vec<char>,
339}
340
341impl SpinnerState {
342 pub fn dots() -> Self {
346 Self {
347 chars: vec!['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
348 }
349 }
350
351 pub fn line() -> Self {
355 Self {
356 chars: vec!['|', '/', '-', '\\'],
357 }
358 }
359
360 pub fn frame(&self, tick: u64) -> char {
362 if self.chars.is_empty() {
363 return ' ';
364 }
365 self.chars[tick as usize % self.chars.len()]
366 }
367}
368
369impl Default for SpinnerState {
370 fn default() -> Self {
371 Self::dots()
372 }
373}
374
375pub struct ListState {
380 pub items: Vec<String>,
382 pub selected: usize,
384}
385
386impl ListState {
387 pub fn new(items: Vec<impl Into<String>>) -> Self {
389 Self {
390 items: items.into_iter().map(Into::into).collect(),
391 selected: 0,
392 }
393 }
394
395 pub fn selected_item(&self) -> Option<&str> {
397 self.items.get(self.selected).map(String::as_str)
398 }
399}
400
401pub struct TabsState {
406 pub labels: Vec<String>,
408 pub selected: usize,
410}
411
412impl TabsState {
413 pub fn new(labels: Vec<impl Into<String>>) -> Self {
415 Self {
416 labels: labels.into_iter().map(Into::into).collect(),
417 selected: 0,
418 }
419 }
420
421 pub fn selected_label(&self) -> Option<&str> {
423 self.labels.get(self.selected).map(String::as_str)
424 }
425}
426
427pub struct TableState {
433 pub headers: Vec<String>,
435 pub rows: Vec<Vec<String>>,
437 pub selected: usize,
439 column_widths: Vec<u32>,
440 dirty: bool,
441}
442
443impl TableState {
444 pub fn new(headers: Vec<impl Into<String>>, rows: Vec<Vec<impl Into<String>>>) -> Self {
446 let headers: Vec<String> = headers.into_iter().map(Into::into).collect();
447 let rows: Vec<Vec<String>> = rows
448 .into_iter()
449 .map(|r| r.into_iter().map(Into::into).collect())
450 .collect();
451 let mut state = Self {
452 headers,
453 rows,
454 selected: 0,
455 column_widths: Vec::new(),
456 dirty: true,
457 };
458 state.recompute_widths();
459 state
460 }
461
462 pub fn set_rows(&mut self, rows: Vec<Vec<impl Into<String>>>) {
467 self.rows = rows
468 .into_iter()
469 .map(|r| r.into_iter().map(Into::into).collect())
470 .collect();
471 self.dirty = true;
472 self.selected = self.selected.min(self.rows.len().saturating_sub(1));
473 }
474
475 pub fn selected_row(&self) -> Option<&[String]> {
477 self.rows.get(self.selected).map(|r| r.as_slice())
478 }
479
480 pub(crate) fn recompute_widths(&mut self) {
481 let col_count = self.headers.len();
482 self.column_widths = vec![0u32; col_count];
483 for (i, header) in self.headers.iter().enumerate() {
484 self.column_widths[i] = UnicodeWidthStr::width(header.as_str()) as u32;
485 }
486 for row in &self.rows {
487 for (i, cell) in row.iter().enumerate() {
488 if i < col_count {
489 let w = UnicodeWidthStr::width(cell.as_str()) as u32;
490 self.column_widths[i] = self.column_widths[i].max(w);
491 }
492 }
493 }
494 self.dirty = false;
495 }
496
497 pub(crate) fn column_widths(&self) -> &[u32] {
498 &self.column_widths
499 }
500
501 pub(crate) fn is_dirty(&self) -> bool {
502 self.dirty
503 }
504}
505
506pub struct ScrollState {
512 pub offset: usize,
514 content_height: u32,
515 viewport_height: u32,
516}
517
518impl ScrollState {
519 pub fn new() -> Self {
521 Self {
522 offset: 0,
523 content_height: 0,
524 viewport_height: 0,
525 }
526 }
527
528 pub fn can_scroll_up(&self) -> bool {
530 self.offset > 0
531 }
532
533 pub fn can_scroll_down(&self) -> bool {
535 (self.offset as u32) + self.viewport_height < self.content_height
536 }
537
538 pub fn scroll_up(&mut self, amount: usize) {
540 self.offset = self.offset.saturating_sub(amount);
541 }
542
543 pub fn scroll_down(&mut self, amount: usize) {
545 let max_offset = self.content_height.saturating_sub(self.viewport_height) as usize;
546 self.offset = (self.offset + amount).min(max_offset);
547 }
548
549 pub(crate) fn set_bounds(&mut self, content_height: u32, viewport_height: u32) {
550 self.content_height = content_height;
551 self.viewport_height = viewport_height;
552 }
553}
554
555impl Default for ScrollState {
556 fn default() -> Self {
557 Self::new()
558 }
559}
560
561#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
571pub enum ButtonVariant {
572 #[default]
574 Default,
575 Primary,
577 Danger,
579 Outline,
581}
582
583pub struct SelectState {
590 pub items: Vec<String>,
591 pub selected: usize,
592 pub open: bool,
593 pub placeholder: String,
594 cursor: usize,
595}
596
597impl SelectState {
598 pub fn new(items: Vec<impl Into<String>>) -> Self {
599 Self {
600 items: items.into_iter().map(Into::into).collect(),
601 selected: 0,
602 open: false,
603 placeholder: String::new(),
604 cursor: 0,
605 }
606 }
607
608 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
609 self.placeholder = p.into();
610 self
611 }
612
613 pub fn selected_item(&self) -> Option<&str> {
614 self.items.get(self.selected).map(String::as_str)
615 }
616
617 pub(crate) fn cursor(&self) -> usize {
618 self.cursor
619 }
620
621 pub(crate) fn set_cursor(&mut self, c: usize) {
622 self.cursor = c;
623 }
624}
625
626pub struct RadioState {
632 pub items: Vec<String>,
633 pub selected: usize,
634}
635
636impl RadioState {
637 pub fn new(items: Vec<impl Into<String>>) -> Self {
638 Self {
639 items: items.into_iter().map(Into::into).collect(),
640 selected: 0,
641 }
642 }
643
644 pub fn selected_item(&self) -> Option<&str> {
645 self.items.get(self.selected).map(String::as_str)
646 }
647}
648
649pub struct MultiSelectState {
655 pub items: Vec<String>,
656 pub cursor: usize,
657 pub selected: HashSet<usize>,
658}
659
660impl MultiSelectState {
661 pub fn new(items: Vec<impl Into<String>>) -> Self {
662 Self {
663 items: items.into_iter().map(Into::into).collect(),
664 cursor: 0,
665 selected: HashSet::new(),
666 }
667 }
668
669 pub fn selected_items(&self) -> Vec<&str> {
670 let mut indices: Vec<usize> = self.selected.iter().copied().collect();
671 indices.sort();
672 indices
673 .iter()
674 .filter_map(|&i| self.items.get(i).map(String::as_str))
675 .collect()
676 }
677
678 pub fn toggle(&mut self, index: usize) {
679 if self.selected.contains(&index) {
680 self.selected.remove(&index);
681 } else {
682 self.selected.insert(index);
683 }
684 }
685}
686
687pub struct TreeNode {
691 pub label: String,
692 pub children: Vec<TreeNode>,
693 pub expanded: bool,
694}
695
696impl TreeNode {
697 pub fn new(label: impl Into<String>) -> Self {
698 Self {
699 label: label.into(),
700 children: Vec::new(),
701 expanded: false,
702 }
703 }
704
705 pub fn expanded(mut self) -> Self {
706 self.expanded = true;
707 self
708 }
709
710 pub fn children(mut self, children: Vec<TreeNode>) -> Self {
711 self.children = children;
712 self
713 }
714
715 pub fn is_leaf(&self) -> bool {
716 self.children.is_empty()
717 }
718
719 fn flatten(&self, depth: usize, out: &mut Vec<FlatTreeEntry>) {
720 out.push(FlatTreeEntry {
721 depth,
722 label: self.label.clone(),
723 is_leaf: self.is_leaf(),
724 expanded: self.expanded,
725 });
726 if self.expanded {
727 for child in &self.children {
728 child.flatten(depth + 1, out);
729 }
730 }
731 }
732}
733
734pub(crate) struct FlatTreeEntry {
735 pub depth: usize,
736 pub label: String,
737 pub is_leaf: bool,
738 pub expanded: bool,
739}
740
741pub struct TreeState {
743 pub nodes: Vec<TreeNode>,
744 pub selected: usize,
745}
746
747impl TreeState {
748 pub fn new(nodes: Vec<TreeNode>) -> Self {
749 Self { nodes, selected: 0 }
750 }
751
752 pub(crate) fn flatten(&self) -> Vec<FlatTreeEntry> {
753 let mut entries = Vec::new();
754 for node in &self.nodes {
755 node.flatten(0, &mut entries);
756 }
757 entries
758 }
759
760 pub(crate) fn toggle_at(&mut self, flat_index: usize) {
761 let mut counter = 0usize;
762 Self::toggle_recursive(&mut self.nodes, flat_index, &mut counter);
763 }
764
765 fn toggle_recursive(nodes: &mut [TreeNode], target: usize, counter: &mut usize) -> bool {
766 for node in nodes.iter_mut() {
767 if *counter == target {
768 if !node.is_leaf() {
769 node.expanded = !node.expanded;
770 }
771 return true;
772 }
773 *counter += 1;
774 if node.expanded && Self::toggle_recursive(&mut node.children, target, counter) {
775 return true;
776 }
777 }
778 false
779 }
780}
781
782pub struct PaletteCommand {
786 pub label: String,
787 pub description: String,
788 pub shortcut: Option<String>,
789}
790
791impl PaletteCommand {
792 pub fn new(label: impl Into<String>, description: impl Into<String>) -> Self {
793 Self {
794 label: label.into(),
795 description: description.into(),
796 shortcut: None,
797 }
798 }
799
800 pub fn shortcut(mut self, s: impl Into<String>) -> Self {
801 self.shortcut = Some(s.into());
802 self
803 }
804}
805
806pub struct CommandPaletteState {
810 pub commands: Vec<PaletteCommand>,
811 pub input: String,
812 pub cursor: usize,
813 pub open: bool,
814 selected: usize,
815}
816
817impl CommandPaletteState {
818 pub fn new(commands: Vec<PaletteCommand>) -> Self {
819 Self {
820 commands,
821 input: String::new(),
822 cursor: 0,
823 open: false,
824 selected: 0,
825 }
826 }
827
828 pub fn toggle(&mut self) {
829 self.open = !self.open;
830 if self.open {
831 self.input.clear();
832 self.cursor = 0;
833 self.selected = 0;
834 }
835 }
836
837 pub(crate) fn filtered_indices(&self) -> Vec<usize> {
838 if self.input.is_empty() {
839 return (0..self.commands.len()).collect();
840 }
841 let query = self.input.to_lowercase();
842 self.commands
843 .iter()
844 .enumerate()
845 .filter(|(_, cmd)| {
846 cmd.label.to_lowercase().contains(&query)
847 || cmd.description.to_lowercase().contains(&query)
848 })
849 .map(|(i, _)| i)
850 .collect()
851 }
852
853 pub(crate) fn selected(&self) -> usize {
854 self.selected
855 }
856
857 pub(crate) fn set_selected(&mut self, s: usize) {
858 self.selected = s;
859 }
860}