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 pub sort_column: Option<usize>,
443 pub sort_ascending: bool,
445 pub filter: String,
447 pub page: usize,
449 pub page_size: usize,
451 view_indices: Vec<usize>,
452}
453
454impl TableState {
455 pub fn new(headers: Vec<impl Into<String>>, rows: Vec<Vec<impl Into<String>>>) -> Self {
457 let headers: Vec<String> = headers.into_iter().map(Into::into).collect();
458 let rows: Vec<Vec<String>> = rows
459 .into_iter()
460 .map(|r| r.into_iter().map(Into::into).collect())
461 .collect();
462 let mut state = Self {
463 headers,
464 rows,
465 selected: 0,
466 column_widths: Vec::new(),
467 dirty: true,
468 sort_column: None,
469 sort_ascending: true,
470 filter: String::new(),
471 page: 0,
472 page_size: 0,
473 view_indices: Vec::new(),
474 };
475 state.rebuild_view();
476 state.recompute_widths();
477 state
478 }
479
480 pub fn set_rows(&mut self, rows: Vec<Vec<impl Into<String>>>) {
485 self.rows = rows
486 .into_iter()
487 .map(|r| r.into_iter().map(Into::into).collect())
488 .collect();
489 self.rebuild_view();
490 }
491
492 pub fn toggle_sort(&mut self, column: usize) {
494 if self.sort_column == Some(column) {
495 self.sort_ascending = !self.sort_ascending;
496 } else {
497 self.sort_column = Some(column);
498 self.sort_ascending = true;
499 }
500 self.rebuild_view();
501 }
502
503 pub fn sort_by(&mut self, column: usize) {
505 self.sort_column = Some(column);
506 self.sort_ascending = true;
507 self.rebuild_view();
508 }
509
510 pub fn set_filter(&mut self, filter: impl Into<String>) {
512 self.filter = filter.into();
513 self.page = 0;
514 self.rebuild_view();
515 }
516
517 pub fn clear_sort(&mut self) {
519 self.sort_column = None;
520 self.sort_ascending = true;
521 self.rebuild_view();
522 }
523
524 pub fn next_page(&mut self) {
526 if self.page_size == 0 {
527 return;
528 }
529 let last_page = self.total_pages().saturating_sub(1);
530 self.page = (self.page + 1).min(last_page);
531 }
532
533 pub fn prev_page(&mut self) {
535 self.page = self.page.saturating_sub(1);
536 }
537
538 pub fn total_pages(&self) -> usize {
540 if self.page_size == 0 {
541 return 1;
542 }
543
544 let len = self.view_indices.len();
545 if len == 0 {
546 1
547 } else {
548 len.div_ceil(self.page_size)
549 }
550 }
551
552 pub fn visible_indices(&self) -> &[usize] {
554 &self.view_indices
555 }
556
557 pub fn selected_row(&self) -> Option<&[String]> {
559 if self.view_indices.is_empty() {
560 return None;
561 }
562 let data_idx = self.view_indices.get(self.selected)?;
563 self.rows.get(*data_idx).map(|r| r.as_slice())
564 }
565
566 fn rebuild_view(&mut self) {
568 let mut indices: Vec<usize> = (0..self.rows.len()).collect();
569
570 let tokens: Vec<String> = self
571 .filter
572 .split_whitespace()
573 .map(|t| t.to_lowercase())
574 .collect();
575 if !tokens.is_empty() {
576 indices.retain(|&idx| {
577 let row = match self.rows.get(idx) {
578 Some(r) => r,
579 None => return false,
580 };
581 tokens.iter().all(|token| {
582 row.iter()
583 .any(|cell| cell.to_lowercase().contains(token.as_str()))
584 })
585 });
586 }
587
588 if let Some(column) = self.sort_column {
589 indices.sort_by(|a, b| {
590 let left = self
591 .rows
592 .get(*a)
593 .and_then(|row| row.get(column))
594 .map(String::as_str)
595 .unwrap_or("");
596 let right = self
597 .rows
598 .get(*b)
599 .and_then(|row| row.get(column))
600 .map(String::as_str)
601 .unwrap_or("");
602
603 match (left.parse::<f64>(), right.parse::<f64>()) {
604 (Ok(l), Ok(r)) => l.partial_cmp(&r).unwrap_or(std::cmp::Ordering::Equal),
605 _ => left.to_lowercase().cmp(&right.to_lowercase()),
606 }
607 });
608
609 if !self.sort_ascending {
610 indices.reverse();
611 }
612 }
613
614 self.view_indices = indices;
615
616 if self.page_size > 0 {
617 self.page = self.page.min(self.total_pages().saturating_sub(1));
618 } else {
619 self.page = 0;
620 }
621
622 self.selected = self.selected.min(self.view_indices.len().saturating_sub(1));
623 self.dirty = true;
624 }
625
626 pub(crate) fn recompute_widths(&mut self) {
627 let col_count = self.headers.len();
628 self.column_widths = vec![0u32; col_count];
629 for (i, header) in self.headers.iter().enumerate() {
630 let mut width = UnicodeWidthStr::width(header.as_str()) as u32;
631 if self.sort_column == Some(i) {
632 width += 2;
633 }
634 self.column_widths[i] = width;
635 }
636 for row in &self.rows {
637 for (i, cell) in row.iter().enumerate() {
638 if i < col_count {
639 let w = UnicodeWidthStr::width(cell.as_str()) as u32;
640 self.column_widths[i] = self.column_widths[i].max(w);
641 }
642 }
643 }
644 self.dirty = false;
645 }
646
647 pub(crate) fn column_widths(&self) -> &[u32] {
648 &self.column_widths
649 }
650
651 pub(crate) fn is_dirty(&self) -> bool {
652 self.dirty
653 }
654}
655
656pub struct ScrollState {
662 pub offset: usize,
664 content_height: u32,
665 viewport_height: u32,
666}
667
668impl ScrollState {
669 pub fn new() -> Self {
671 Self {
672 offset: 0,
673 content_height: 0,
674 viewport_height: 0,
675 }
676 }
677
678 pub fn can_scroll_up(&self) -> bool {
680 self.offset > 0
681 }
682
683 pub fn can_scroll_down(&self) -> bool {
685 (self.offset as u32) + self.viewport_height < self.content_height
686 }
687
688 pub fn content_height(&self) -> u32 {
690 self.content_height
691 }
692
693 pub fn viewport_height(&self) -> u32 {
695 self.viewport_height
696 }
697
698 pub fn progress(&self) -> f32 {
700 let max = self.content_height.saturating_sub(self.viewport_height);
701 if max == 0 {
702 0.0
703 } else {
704 self.offset as f32 / max as f32
705 }
706 }
707
708 pub fn scroll_up(&mut self, amount: usize) {
710 self.offset = self.offset.saturating_sub(amount);
711 }
712
713 pub fn scroll_down(&mut self, amount: usize) {
715 let max_offset = self.content_height.saturating_sub(self.viewport_height) as usize;
716 self.offset = (self.offset + amount).min(max_offset);
717 }
718
719 pub(crate) fn set_bounds(&mut self, content_height: u32, viewport_height: u32) {
720 self.content_height = content_height;
721 self.viewport_height = viewport_height;
722 }
723}
724
725impl Default for ScrollState {
726 fn default() -> Self {
727 Self::new()
728 }
729}
730
731#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
741pub enum ButtonVariant {
742 #[default]
744 Default,
745 Primary,
747 Danger,
749 Outline,
751}
752
753pub struct SelectState {
760 pub items: Vec<String>,
761 pub selected: usize,
762 pub open: bool,
763 pub placeholder: String,
764 cursor: usize,
765}
766
767impl SelectState {
768 pub fn new(items: Vec<impl Into<String>>) -> Self {
769 Self {
770 items: items.into_iter().map(Into::into).collect(),
771 selected: 0,
772 open: false,
773 placeholder: String::new(),
774 cursor: 0,
775 }
776 }
777
778 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
779 self.placeholder = p.into();
780 self
781 }
782
783 pub fn selected_item(&self) -> Option<&str> {
784 self.items.get(self.selected).map(String::as_str)
785 }
786
787 pub(crate) fn cursor(&self) -> usize {
788 self.cursor
789 }
790
791 pub(crate) fn set_cursor(&mut self, c: usize) {
792 self.cursor = c;
793 }
794}
795
796pub struct RadioState {
802 pub items: Vec<String>,
803 pub selected: usize,
804}
805
806impl RadioState {
807 pub fn new(items: Vec<impl Into<String>>) -> Self {
808 Self {
809 items: items.into_iter().map(Into::into).collect(),
810 selected: 0,
811 }
812 }
813
814 pub fn selected_item(&self) -> Option<&str> {
815 self.items.get(self.selected).map(String::as_str)
816 }
817}
818
819pub struct MultiSelectState {
825 pub items: Vec<String>,
826 pub cursor: usize,
827 pub selected: HashSet<usize>,
828}
829
830impl MultiSelectState {
831 pub fn new(items: Vec<impl Into<String>>) -> Self {
832 Self {
833 items: items.into_iter().map(Into::into).collect(),
834 cursor: 0,
835 selected: HashSet::new(),
836 }
837 }
838
839 pub fn selected_items(&self) -> Vec<&str> {
840 let mut indices: Vec<usize> = self.selected.iter().copied().collect();
841 indices.sort();
842 indices
843 .iter()
844 .filter_map(|&i| self.items.get(i).map(String::as_str))
845 .collect()
846 }
847
848 pub fn toggle(&mut self, index: usize) {
849 if self.selected.contains(&index) {
850 self.selected.remove(&index);
851 } else {
852 self.selected.insert(index);
853 }
854 }
855}
856
857pub struct TreeNode {
861 pub label: String,
862 pub children: Vec<TreeNode>,
863 pub expanded: bool,
864}
865
866impl TreeNode {
867 pub fn new(label: impl Into<String>) -> Self {
868 Self {
869 label: label.into(),
870 children: Vec::new(),
871 expanded: false,
872 }
873 }
874
875 pub fn expanded(mut self) -> Self {
876 self.expanded = true;
877 self
878 }
879
880 pub fn children(mut self, children: Vec<TreeNode>) -> Self {
881 self.children = children;
882 self
883 }
884
885 pub fn is_leaf(&self) -> bool {
886 self.children.is_empty()
887 }
888
889 fn flatten(&self, depth: usize, out: &mut Vec<FlatTreeEntry>) {
890 out.push(FlatTreeEntry {
891 depth,
892 label: self.label.clone(),
893 is_leaf: self.is_leaf(),
894 expanded: self.expanded,
895 });
896 if self.expanded {
897 for child in &self.children {
898 child.flatten(depth + 1, out);
899 }
900 }
901 }
902}
903
904pub(crate) struct FlatTreeEntry {
905 pub depth: usize,
906 pub label: String,
907 pub is_leaf: bool,
908 pub expanded: bool,
909}
910
911pub struct TreeState {
913 pub nodes: Vec<TreeNode>,
914 pub selected: usize,
915}
916
917impl TreeState {
918 pub fn new(nodes: Vec<TreeNode>) -> Self {
919 Self { nodes, selected: 0 }
920 }
921
922 pub(crate) fn flatten(&self) -> Vec<FlatTreeEntry> {
923 let mut entries = Vec::new();
924 for node in &self.nodes {
925 node.flatten(0, &mut entries);
926 }
927 entries
928 }
929
930 pub(crate) fn toggle_at(&mut self, flat_index: usize) {
931 let mut counter = 0usize;
932 Self::toggle_recursive(&mut self.nodes, flat_index, &mut counter);
933 }
934
935 fn toggle_recursive(nodes: &mut [TreeNode], target: usize, counter: &mut usize) -> bool {
936 for node in nodes.iter_mut() {
937 if *counter == target {
938 if !node.is_leaf() {
939 node.expanded = !node.expanded;
940 }
941 return true;
942 }
943 *counter += 1;
944 if node.expanded && Self::toggle_recursive(&mut node.children, target, counter) {
945 return true;
946 }
947 }
948 false
949 }
950}
951
952pub struct PaletteCommand {
956 pub label: String,
957 pub description: String,
958 pub shortcut: Option<String>,
959}
960
961impl PaletteCommand {
962 pub fn new(label: impl Into<String>, description: impl Into<String>) -> Self {
963 Self {
964 label: label.into(),
965 description: description.into(),
966 shortcut: None,
967 }
968 }
969
970 pub fn shortcut(mut self, s: impl Into<String>) -> Self {
971 self.shortcut = Some(s.into());
972 self
973 }
974}
975
976pub struct CommandPaletteState {
980 pub commands: Vec<PaletteCommand>,
981 pub input: String,
982 pub cursor: usize,
983 pub open: bool,
984 selected: usize,
985}
986
987impl CommandPaletteState {
988 pub fn new(commands: Vec<PaletteCommand>) -> Self {
989 Self {
990 commands,
991 input: String::new(),
992 cursor: 0,
993 open: false,
994 selected: 0,
995 }
996 }
997
998 pub fn toggle(&mut self) {
999 self.open = !self.open;
1000 if self.open {
1001 self.input.clear();
1002 self.cursor = 0;
1003 self.selected = 0;
1004 }
1005 }
1006
1007 pub(crate) fn filtered_indices(&self) -> Vec<usize> {
1008 let tokens: Vec<String> = self
1009 .input
1010 .split_whitespace()
1011 .map(|t| t.to_lowercase())
1012 .collect();
1013 if tokens.is_empty() {
1014 return (0..self.commands.len()).collect();
1015 }
1016 self.commands
1017 .iter()
1018 .enumerate()
1019 .filter(|(_, cmd)| {
1020 let label = cmd.label.to_lowercase();
1021 let desc = cmd.description.to_lowercase();
1022 tokens
1023 .iter()
1024 .all(|token| label.contains(token.as_str()) || desc.contains(token.as_str()))
1025 })
1026 .map(|(i, _)| i)
1027 .collect()
1028 }
1029
1030 pub(crate) fn selected(&self) -> usize {
1031 self.selected
1032 }
1033
1034 pub(crate) fn set_selected(&mut self, s: usize) {
1035 self.selected = s;
1036 }
1037}
1038
1039pub struct StreamingTextState {
1044 pub content: String,
1046 pub streaming: bool,
1048 pub(crate) cursor_visible: bool,
1050 pub(crate) cursor_tick: u64,
1051}
1052
1053impl StreamingTextState {
1054 pub fn new() -> Self {
1056 Self {
1057 content: String::new(),
1058 streaming: false,
1059 cursor_visible: true,
1060 cursor_tick: 0,
1061 }
1062 }
1063
1064 pub fn push(&mut self, chunk: &str) {
1066 self.content.push_str(chunk);
1067 }
1068
1069 pub fn finish(&mut self) {
1071 self.streaming = false;
1072 }
1073
1074 pub fn start(&mut self) {
1076 self.content.clear();
1077 self.streaming = true;
1078 self.cursor_visible = true;
1079 self.cursor_tick = 0;
1080 }
1081
1082 pub fn clear(&mut self) {
1084 self.content.clear();
1085 self.streaming = false;
1086 self.cursor_visible = true;
1087 self.cursor_tick = 0;
1088 }
1089}
1090
1091impl Default for StreamingTextState {
1092 fn default() -> Self {
1093 Self::new()
1094 }
1095}
1096
1097#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1099pub enum ApprovalAction {
1100 Pending,
1102 Approved,
1104 Rejected,
1106}
1107
1108pub struct ToolApprovalState {
1114 pub tool_name: String,
1116 pub description: String,
1118 pub action: ApprovalAction,
1120}
1121
1122impl ToolApprovalState {
1123 pub fn new(tool_name: impl Into<String>, description: impl Into<String>) -> Self {
1125 Self {
1126 tool_name: tool_name.into(),
1127 description: description.into(),
1128 action: ApprovalAction::Pending,
1129 }
1130 }
1131
1132 pub fn reset(&mut self) {
1134 self.action = ApprovalAction::Pending;
1135 }
1136}
1137
1138#[derive(Debug, Clone)]
1140pub struct ContextItem {
1141 pub label: String,
1143 pub tokens: usize,
1145}
1146
1147impl ContextItem {
1148 pub fn new(label: impl Into<String>, tokens: usize) -> Self {
1150 Self {
1151 label: label.into(),
1152 tokens,
1153 }
1154 }
1155}