1use std::collections::HashSet;
8use std::fs;
9use std::path::PathBuf;
10use unicode_width::UnicodeWidthStr;
11
12type FormValidator = fn(&str) -> Result<(), String>;
13type TextInputValidator = Box<dyn Fn(&str) -> Result<(), String>>;
14
15pub struct TextInputState {
31 pub value: String,
33 pub cursor: usize,
35 pub placeholder: String,
37 pub max_length: Option<usize>,
39 pub validation_error: Option<String>,
41 pub masked: bool,
43 pub suggestions: Vec<String>,
44 pub suggestion_index: usize,
45 pub show_suggestions: bool,
46 validators: Vec<TextInputValidator>,
48 validation_errors: Vec<String>,
50}
51
52impl TextInputState {
53 pub fn new() -> Self {
55 Self {
56 value: String::new(),
57 cursor: 0,
58 placeholder: String::new(),
59 max_length: None,
60 validation_error: None,
61 masked: false,
62 suggestions: Vec::new(),
63 suggestion_index: 0,
64 show_suggestions: false,
65 validators: Vec::new(),
66 validation_errors: Vec::new(),
67 }
68 }
69
70 pub fn with_placeholder(p: impl Into<String>) -> Self {
72 Self {
73 placeholder: p.into(),
74 ..Self::new()
75 }
76 }
77
78 pub fn max_length(mut self, len: usize) -> Self {
80 self.max_length = Some(len);
81 self
82 }
83
84 pub fn validate(&mut self, validator: impl Fn(&str) -> Result<(), String>) {
92 self.validation_error = validator(&self.value).err();
93 }
94
95 pub fn add_validator(&mut self, f: impl Fn(&str) -> Result<(), String> + 'static) {
100 self.validators.push(Box::new(f));
101 }
102
103 pub fn run_validators(&mut self) {
108 self.validation_errors.clear();
109 for validator in &self.validators {
110 if let Err(err) = validator(&self.value) {
111 self.validation_errors.push(err);
112 }
113 }
114 self.validation_error = self.validation_errors.first().cloned();
115 }
116
117 pub fn errors(&self) -> &[String] {
119 &self.validation_errors
120 }
121
122 pub fn set_suggestions(&mut self, suggestions: Vec<String>) {
123 self.suggestions = suggestions;
124 self.suggestion_index = 0;
125 self.show_suggestions = !self.suggestions.is_empty();
126 }
127
128 pub fn matched_suggestions(&self) -> Vec<&str> {
129 if self.value.is_empty() {
130 return Vec::new();
131 }
132 let lower = self.value.to_lowercase();
133 self.suggestions
134 .iter()
135 .filter(|s| s.to_lowercase().starts_with(&lower))
136 .map(|s| s.as_str())
137 .collect()
138 }
139}
140
141impl Default for TextInputState {
142 fn default() -> Self {
143 Self::new()
144 }
145}
146
147#[derive(Default)]
149pub struct FormField {
150 pub label: String,
152 pub input: TextInputState,
154 pub error: Option<String>,
156}
157
158impl FormField {
159 pub fn new(label: impl Into<String>) -> Self {
161 Self {
162 label: label.into(),
163 input: TextInputState::new(),
164 error: None,
165 }
166 }
167
168 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
170 self.input.placeholder = p.into();
171 self
172 }
173}
174
175pub struct FormState {
177 pub fields: Vec<FormField>,
179 pub submitted: bool,
181}
182
183impl FormState {
184 pub fn new() -> Self {
186 Self {
187 fields: Vec::new(),
188 submitted: false,
189 }
190 }
191
192 pub fn field(mut self, field: FormField) -> Self {
194 self.fields.push(field);
195 self
196 }
197
198 pub fn validate(&mut self, validators: &[FormValidator]) -> bool {
202 let mut all_valid = true;
203 for (i, field) in self.fields.iter_mut().enumerate() {
204 if let Some(validator) = validators.get(i) {
205 match validator(&field.input.value) {
206 Ok(()) => field.error = None,
207 Err(msg) => {
208 field.error = Some(msg);
209 all_valid = false;
210 }
211 }
212 }
213 }
214 all_valid
215 }
216
217 pub fn value(&self, index: usize) -> &str {
219 self.fields
220 .get(index)
221 .map(|f| f.input.value.as_str())
222 .unwrap_or("")
223 }
224}
225
226impl Default for FormState {
227 fn default() -> Self {
228 Self::new()
229 }
230}
231
232pub struct ToastState {
238 pub messages: Vec<ToastMessage>,
240}
241
242pub struct ToastMessage {
244 pub text: String,
246 pub level: ToastLevel,
248 pub created_tick: u64,
250 pub duration_ticks: u64,
252}
253
254impl Default for ToastMessage {
255 fn default() -> Self {
256 Self {
257 text: String::new(),
258 level: ToastLevel::Info,
259 created_tick: 0,
260 duration_ticks: 30,
261 }
262 }
263}
264
265pub enum ToastLevel {
267 Info,
269 Success,
271 Warning,
273 Error,
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278pub enum AlertLevel {
279 Info,
280 Success,
281 Warning,
282 Error,
283}
284
285impl ToastState {
286 pub fn new() -> Self {
288 Self {
289 messages: Vec::new(),
290 }
291 }
292
293 pub fn info(&mut self, text: impl Into<String>, tick: u64) {
295 self.push(text, ToastLevel::Info, tick, 30);
296 }
297
298 pub fn success(&mut self, text: impl Into<String>, tick: u64) {
300 self.push(text, ToastLevel::Success, tick, 30);
301 }
302
303 pub fn warning(&mut self, text: impl Into<String>, tick: u64) {
305 self.push(text, ToastLevel::Warning, tick, 50);
306 }
307
308 pub fn error(&mut self, text: impl Into<String>, tick: u64) {
310 self.push(text, ToastLevel::Error, tick, 80);
311 }
312
313 pub fn push(
315 &mut self,
316 text: impl Into<String>,
317 level: ToastLevel,
318 tick: u64,
319 duration_ticks: u64,
320 ) {
321 self.messages.push(ToastMessage {
322 text: text.into(),
323 level,
324 created_tick: tick,
325 duration_ticks,
326 });
327 }
328
329 pub fn cleanup(&mut self, current_tick: u64) {
333 self.messages.retain(|message| {
334 current_tick < message.created_tick.saturating_add(message.duration_ticks)
335 });
336 }
337}
338
339impl Default for ToastState {
340 fn default() -> Self {
341 Self::new()
342 }
343}
344
345pub struct TextareaState {
350 pub lines: Vec<String>,
352 pub cursor_row: usize,
354 pub cursor_col: usize,
356 pub max_length: Option<usize>,
358 pub wrap_width: Option<u32>,
360 pub scroll_offset: usize,
362}
363
364impl TextareaState {
365 pub fn new() -> Self {
367 Self {
368 lines: vec![String::new()],
369 cursor_row: 0,
370 cursor_col: 0,
371 max_length: None,
372 wrap_width: None,
373 scroll_offset: 0,
374 }
375 }
376
377 pub fn value(&self) -> String {
379 self.lines.join("\n")
380 }
381
382 pub fn set_value(&mut self, text: impl Into<String>) {
386 let value = text.into();
387 self.lines = value.split('\n').map(str::to_string).collect();
388 if self.lines.is_empty() {
389 self.lines.push(String::new());
390 }
391 self.cursor_row = 0;
392 self.cursor_col = 0;
393 self.scroll_offset = 0;
394 }
395
396 pub fn max_length(mut self, len: usize) -> Self {
398 self.max_length = Some(len);
399 self
400 }
401
402 pub fn word_wrap(mut self, width: u32) -> Self {
404 self.wrap_width = Some(width);
405 self
406 }
407}
408
409impl Default for TextareaState {
410 fn default() -> Self {
411 Self::new()
412 }
413}
414
415pub struct SpinnerState {
421 chars: Vec<char>,
422}
423
424impl SpinnerState {
425 pub fn dots() -> Self {
429 Self {
430 chars: vec!['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
431 }
432 }
433
434 pub fn line() -> Self {
438 Self {
439 chars: vec!['|', '/', '-', '\\'],
440 }
441 }
442
443 pub fn frame(&self, tick: u64) -> char {
445 if self.chars.is_empty() {
446 return ' ';
447 }
448 self.chars[tick as usize % self.chars.len()]
449 }
450}
451
452impl Default for SpinnerState {
453 fn default() -> Self {
454 Self::dots()
455 }
456}
457
458#[derive(Default)]
463pub struct ListState {
464 pub items: Vec<String>,
466 pub selected: usize,
468 pub filter: String,
470 view_indices: Vec<usize>,
471}
472
473impl ListState {
474 pub fn new(items: Vec<impl Into<String>>) -> Self {
476 let len = items.len();
477 Self {
478 items: items.into_iter().map(Into::into).collect(),
479 selected: 0,
480 filter: String::new(),
481 view_indices: (0..len).collect(),
482 }
483 }
484
485 pub fn set_items(&mut self, items: Vec<impl Into<String>>) {
490 self.items = items.into_iter().map(Into::into).collect();
491 self.selected = self.selected.min(self.items.len().saturating_sub(1));
492 self.rebuild_view();
493 }
494
495 pub fn set_filter(&mut self, filter: impl Into<String>) {
499 self.filter = filter.into();
500 self.rebuild_view();
501 }
502
503 pub fn visible_indices(&self) -> &[usize] {
505 &self.view_indices
506 }
507
508 pub fn selected_item(&self) -> Option<&str> {
510 let data_idx = *self.view_indices.get(self.selected)?;
511 self.items.get(data_idx).map(String::as_str)
512 }
513
514 fn rebuild_view(&mut self) {
515 let tokens: Vec<String> = self
516 .filter
517 .split_whitespace()
518 .map(|t| t.to_lowercase())
519 .collect();
520 self.view_indices = if tokens.is_empty() {
521 (0..self.items.len()).collect()
522 } else {
523 (0..self.items.len())
524 .filter(|&i| {
525 tokens
526 .iter()
527 .all(|token| self.items[i].to_lowercase().contains(token.as_str()))
528 })
529 .collect()
530 };
531 if !self.view_indices.is_empty() && self.selected >= self.view_indices.len() {
532 self.selected = self.view_indices.len() - 1;
533 }
534 }
535}
536
537#[derive(Debug, Clone)]
538pub struct FilePickerState {
539 pub current_dir: PathBuf,
540 pub entries: Vec<FileEntry>,
541 pub selected: usize,
542 pub selected_file: Option<PathBuf>,
543 pub show_hidden: bool,
544 pub extensions: Vec<String>,
545 pub dirty: bool,
546}
547
548#[derive(Debug, Clone, Default)]
549pub struct FileEntry {
550 pub name: String,
551 pub path: PathBuf,
552 pub is_dir: bool,
553 pub size: u64,
554}
555
556impl FilePickerState {
557 pub fn new(dir: impl Into<PathBuf>) -> Self {
558 Self {
559 current_dir: dir.into(),
560 entries: Vec::new(),
561 selected: 0,
562 selected_file: None,
563 show_hidden: false,
564 extensions: Vec::new(),
565 dirty: true,
566 }
567 }
568
569 pub fn show_hidden(mut self, show: bool) -> Self {
570 self.show_hidden = show;
571 self.dirty = true;
572 self
573 }
574
575 pub fn extensions(mut self, exts: &[&str]) -> Self {
576 self.extensions = exts
577 .iter()
578 .map(|ext| ext.trim().trim_start_matches('.').to_ascii_lowercase())
579 .filter(|ext| !ext.is_empty())
580 .collect();
581 self.dirty = true;
582 self
583 }
584
585 pub fn selected(&self) -> Option<&PathBuf> {
586 self.selected_file.as_ref()
587 }
588
589 pub fn refresh(&mut self) {
590 let mut entries = Vec::new();
591
592 if let Ok(read_dir) = fs::read_dir(&self.current_dir) {
593 for dir_entry in read_dir.flatten() {
594 let name = dir_entry.file_name().to_string_lossy().to_string();
595 if !self.show_hidden && name.starts_with('.') {
596 continue;
597 }
598
599 let Ok(file_type) = dir_entry.file_type() else {
600 continue;
601 };
602 if file_type.is_symlink() {
603 continue;
604 }
605
606 let path = dir_entry.path();
607 let is_dir = file_type.is_dir();
608
609 if !is_dir && !self.extensions.is_empty() {
610 let ext = path
611 .extension()
612 .and_then(|e| e.to_str())
613 .map(|e| e.to_ascii_lowercase());
614 let Some(ext) = ext else {
615 continue;
616 };
617 if !self.extensions.iter().any(|allowed| allowed == &ext) {
618 continue;
619 }
620 }
621
622 let size = if is_dir {
623 0
624 } else {
625 fs::symlink_metadata(&path).map(|m| m.len()).unwrap_or(0)
626 };
627
628 entries.push(FileEntry {
629 name,
630 path,
631 is_dir,
632 size,
633 });
634 }
635 }
636
637 entries.sort_by(|a, b| match (a.is_dir, b.is_dir) {
638 (true, false) => std::cmp::Ordering::Less,
639 (false, true) => std::cmp::Ordering::Greater,
640 _ => a
641 .name
642 .to_ascii_lowercase()
643 .cmp(&b.name.to_ascii_lowercase())
644 .then_with(|| a.name.cmp(&b.name)),
645 });
646
647 self.entries = entries;
648 if self.entries.is_empty() {
649 self.selected = 0;
650 } else {
651 self.selected = self.selected.min(self.entries.len().saturating_sub(1));
652 }
653 self.dirty = false;
654 }
655}
656
657impl Default for FilePickerState {
658 fn default() -> Self {
659 Self::new(".")
660 }
661}
662
663#[derive(Default)]
668pub struct TabsState {
669 pub labels: Vec<String>,
671 pub selected: usize,
673}
674
675impl TabsState {
676 pub fn new(labels: Vec<impl Into<String>>) -> Self {
678 Self {
679 labels: labels.into_iter().map(Into::into).collect(),
680 selected: 0,
681 }
682 }
683
684 pub fn selected_label(&self) -> Option<&str> {
686 self.labels.get(self.selected).map(String::as_str)
687 }
688}
689
690pub struct TableState {
696 pub headers: Vec<String>,
698 pub rows: Vec<Vec<String>>,
700 pub selected: usize,
702 column_widths: Vec<u32>,
703 dirty: bool,
704 pub sort_column: Option<usize>,
706 pub sort_ascending: bool,
708 pub filter: String,
710 pub page: usize,
712 pub page_size: usize,
714 view_indices: Vec<usize>,
715}
716
717impl Default for TableState {
718 fn default() -> Self {
719 Self {
720 headers: Vec::new(),
721 rows: Vec::new(),
722 selected: 0,
723 column_widths: Vec::new(),
724 dirty: true,
725 sort_column: None,
726 sort_ascending: true,
727 filter: String::new(),
728 page: 0,
729 page_size: 0,
730 view_indices: Vec::new(),
731 }
732 }
733}
734
735impl TableState {
736 pub fn new(headers: Vec<impl Into<String>>, rows: Vec<Vec<impl Into<String>>>) -> Self {
738 let headers: Vec<String> = headers.into_iter().map(Into::into).collect();
739 let rows: Vec<Vec<String>> = rows
740 .into_iter()
741 .map(|r| r.into_iter().map(Into::into).collect())
742 .collect();
743 let mut state = Self {
744 headers,
745 rows,
746 selected: 0,
747 column_widths: Vec::new(),
748 dirty: true,
749 sort_column: None,
750 sort_ascending: true,
751 filter: String::new(),
752 page: 0,
753 page_size: 0,
754 view_indices: Vec::new(),
755 };
756 state.rebuild_view();
757 state.recompute_widths();
758 state
759 }
760
761 pub fn set_rows(&mut self, rows: Vec<Vec<impl Into<String>>>) {
766 self.rows = rows
767 .into_iter()
768 .map(|r| r.into_iter().map(Into::into).collect())
769 .collect();
770 self.rebuild_view();
771 }
772
773 pub fn toggle_sort(&mut self, column: usize) {
775 if self.sort_column == Some(column) {
776 self.sort_ascending = !self.sort_ascending;
777 } else {
778 self.sort_column = Some(column);
779 self.sort_ascending = true;
780 }
781 self.rebuild_view();
782 }
783
784 pub fn sort_by(&mut self, column: usize) {
786 self.sort_column = Some(column);
787 self.sort_ascending = true;
788 self.rebuild_view();
789 }
790
791 pub fn set_filter(&mut self, filter: impl Into<String>) {
795 self.filter = filter.into();
796 self.page = 0;
797 self.rebuild_view();
798 }
799
800 pub fn clear_sort(&mut self) {
802 self.sort_column = None;
803 self.sort_ascending = true;
804 self.rebuild_view();
805 }
806
807 pub fn next_page(&mut self) {
809 if self.page_size == 0 {
810 return;
811 }
812 let last_page = self.total_pages().saturating_sub(1);
813 self.page = (self.page + 1).min(last_page);
814 }
815
816 pub fn prev_page(&mut self) {
818 self.page = self.page.saturating_sub(1);
819 }
820
821 pub fn total_pages(&self) -> usize {
823 if self.page_size == 0 {
824 return 1;
825 }
826
827 let len = self.view_indices.len();
828 if len == 0 {
829 1
830 } else {
831 len.div_ceil(self.page_size)
832 }
833 }
834
835 pub fn visible_indices(&self) -> &[usize] {
837 &self.view_indices
838 }
839
840 pub fn selected_row(&self) -> Option<&[String]> {
842 if self.view_indices.is_empty() {
843 return None;
844 }
845 let data_idx = self.view_indices.get(self.selected)?;
846 self.rows.get(*data_idx).map(|r| r.as_slice())
847 }
848
849 fn rebuild_view(&mut self) {
851 let mut indices: Vec<usize> = (0..self.rows.len()).collect();
852
853 let tokens: Vec<String> = self
854 .filter
855 .split_whitespace()
856 .map(|t| t.to_lowercase())
857 .collect();
858 if !tokens.is_empty() {
859 indices.retain(|&idx| {
860 let row = match self.rows.get(idx) {
861 Some(r) => r,
862 None => return false,
863 };
864 tokens.iter().all(|token| {
865 row.iter()
866 .any(|cell| cell.to_lowercase().contains(token.as_str()))
867 })
868 });
869 }
870
871 if let Some(column) = self.sort_column {
872 indices.sort_by(|a, b| {
873 let left = self
874 .rows
875 .get(*a)
876 .and_then(|row| row.get(column))
877 .map(String::as_str)
878 .unwrap_or("");
879 let right = self
880 .rows
881 .get(*b)
882 .and_then(|row| row.get(column))
883 .map(String::as_str)
884 .unwrap_or("");
885
886 match (left.parse::<f64>(), right.parse::<f64>()) {
887 (Ok(l), Ok(r)) => l.partial_cmp(&r).unwrap_or(std::cmp::Ordering::Equal),
888 _ => left.to_lowercase().cmp(&right.to_lowercase()),
889 }
890 });
891
892 if !self.sort_ascending {
893 indices.reverse();
894 }
895 }
896
897 self.view_indices = indices;
898
899 if self.page_size > 0 {
900 self.page = self.page.min(self.total_pages().saturating_sub(1));
901 } else {
902 self.page = 0;
903 }
904
905 self.selected = self.selected.min(self.view_indices.len().saturating_sub(1));
906 self.dirty = true;
907 }
908
909 pub(crate) fn recompute_widths(&mut self) {
910 let col_count = self.headers.len();
911 self.column_widths = vec![0u32; col_count];
912 for (i, header) in self.headers.iter().enumerate() {
913 let mut width = UnicodeWidthStr::width(header.as_str()) as u32;
914 if self.sort_column == Some(i) {
915 width += 2;
916 }
917 self.column_widths[i] = width;
918 }
919 for row in &self.rows {
920 for (i, cell) in row.iter().enumerate() {
921 if i < col_count {
922 let w = UnicodeWidthStr::width(cell.as_str()) as u32;
923 self.column_widths[i] = self.column_widths[i].max(w);
924 }
925 }
926 }
927 self.dirty = false;
928 }
929
930 pub(crate) fn column_widths(&self) -> &[u32] {
931 &self.column_widths
932 }
933
934 pub(crate) fn is_dirty(&self) -> bool {
935 self.dirty
936 }
937}
938
939pub struct ScrollState {
945 pub offset: usize,
947 content_height: u32,
948 viewport_height: u32,
949}
950
951impl ScrollState {
952 pub fn new() -> Self {
954 Self {
955 offset: 0,
956 content_height: 0,
957 viewport_height: 0,
958 }
959 }
960
961 pub fn can_scroll_up(&self) -> bool {
963 self.offset > 0
964 }
965
966 pub fn can_scroll_down(&self) -> bool {
968 (self.offset as u32) + self.viewport_height < self.content_height
969 }
970
971 pub fn content_height(&self) -> u32 {
973 self.content_height
974 }
975
976 pub fn viewport_height(&self) -> u32 {
978 self.viewport_height
979 }
980
981 pub fn progress(&self) -> f32 {
983 let max = self.content_height.saturating_sub(self.viewport_height);
984 if max == 0 {
985 0.0
986 } else {
987 self.offset as f32 / max as f32
988 }
989 }
990
991 pub fn scroll_up(&mut self, amount: usize) {
993 self.offset = self.offset.saturating_sub(amount);
994 }
995
996 pub fn scroll_down(&mut self, amount: usize) {
998 let max_offset = self.content_height.saturating_sub(self.viewport_height) as usize;
999 self.offset = (self.offset + amount).min(max_offset);
1000 }
1001
1002 pub(crate) fn set_bounds(&mut self, content_height: u32, viewport_height: u32) {
1003 self.content_height = content_height;
1004 self.viewport_height = viewport_height;
1005 }
1006}
1007
1008impl Default for ScrollState {
1009 fn default() -> Self {
1010 Self::new()
1011 }
1012}
1013
1014#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1024pub enum ButtonVariant {
1025 #[default]
1027 Default,
1028 Primary,
1030 Danger,
1032 Outline,
1034}
1035
1036#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1037pub enum Trend {
1038 Up,
1039 Down,
1040}
1041
1042#[derive(Default)]
1049pub struct SelectState {
1050 pub items: Vec<String>,
1051 pub selected: usize,
1052 pub open: bool,
1053 pub placeholder: String,
1054 cursor: usize,
1055}
1056
1057impl SelectState {
1058 pub fn new(items: Vec<impl Into<String>>) -> Self {
1059 Self {
1060 items: items.into_iter().map(Into::into).collect(),
1061 selected: 0,
1062 open: false,
1063 placeholder: String::new(),
1064 cursor: 0,
1065 }
1066 }
1067
1068 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
1069 self.placeholder = p.into();
1070 self
1071 }
1072
1073 pub fn selected_item(&self) -> Option<&str> {
1074 self.items.get(self.selected).map(String::as_str)
1075 }
1076
1077 pub(crate) fn cursor(&self) -> usize {
1078 self.cursor
1079 }
1080
1081 pub(crate) fn set_cursor(&mut self, c: usize) {
1082 self.cursor = c;
1083 }
1084}
1085
1086#[derive(Default)]
1092pub struct RadioState {
1093 pub items: Vec<String>,
1094 pub selected: usize,
1095}
1096
1097impl RadioState {
1098 pub fn new(items: Vec<impl Into<String>>) -> Self {
1099 Self {
1100 items: items.into_iter().map(Into::into).collect(),
1101 selected: 0,
1102 }
1103 }
1104
1105 pub fn selected_item(&self) -> Option<&str> {
1106 self.items.get(self.selected).map(String::as_str)
1107 }
1108}
1109
1110pub struct MultiSelectState {
1116 pub items: Vec<String>,
1117 pub cursor: usize,
1118 pub selected: HashSet<usize>,
1119}
1120
1121impl MultiSelectState {
1122 pub fn new(items: Vec<impl Into<String>>) -> Self {
1123 Self {
1124 items: items.into_iter().map(Into::into).collect(),
1125 cursor: 0,
1126 selected: HashSet::new(),
1127 }
1128 }
1129
1130 pub fn selected_items(&self) -> Vec<&str> {
1131 let mut indices: Vec<usize> = self.selected.iter().copied().collect();
1132 indices.sort();
1133 indices
1134 .iter()
1135 .filter_map(|&i| self.items.get(i).map(String::as_str))
1136 .collect()
1137 }
1138
1139 pub fn toggle(&mut self, index: usize) {
1140 if self.selected.contains(&index) {
1141 self.selected.remove(&index);
1142 } else {
1143 self.selected.insert(index);
1144 }
1145 }
1146}
1147
1148pub struct TreeNode {
1152 pub label: String,
1153 pub children: Vec<TreeNode>,
1154 pub expanded: bool,
1155}
1156
1157impl TreeNode {
1158 pub fn new(label: impl Into<String>) -> Self {
1159 Self {
1160 label: label.into(),
1161 children: Vec::new(),
1162 expanded: false,
1163 }
1164 }
1165
1166 pub fn expanded(mut self) -> Self {
1167 self.expanded = true;
1168 self
1169 }
1170
1171 pub fn children(mut self, children: Vec<TreeNode>) -> Self {
1172 self.children = children;
1173 self
1174 }
1175
1176 pub fn is_leaf(&self) -> bool {
1177 self.children.is_empty()
1178 }
1179
1180 fn flatten(&self, depth: usize, out: &mut Vec<FlatTreeEntry>) {
1181 out.push(FlatTreeEntry {
1182 depth,
1183 label: self.label.clone(),
1184 is_leaf: self.is_leaf(),
1185 expanded: self.expanded,
1186 });
1187 if self.expanded {
1188 for child in &self.children {
1189 child.flatten(depth + 1, out);
1190 }
1191 }
1192 }
1193}
1194
1195pub(crate) struct FlatTreeEntry {
1196 pub depth: usize,
1197 pub label: String,
1198 pub is_leaf: bool,
1199 pub expanded: bool,
1200}
1201
1202pub struct TreeState {
1204 pub nodes: Vec<TreeNode>,
1205 pub selected: usize,
1206}
1207
1208impl TreeState {
1209 pub fn new(nodes: Vec<TreeNode>) -> Self {
1210 Self { nodes, selected: 0 }
1211 }
1212
1213 pub(crate) fn flatten(&self) -> Vec<FlatTreeEntry> {
1214 let mut entries = Vec::new();
1215 for node in &self.nodes {
1216 node.flatten(0, &mut entries);
1217 }
1218 entries
1219 }
1220
1221 pub(crate) fn toggle_at(&mut self, flat_index: usize) {
1222 let mut counter = 0usize;
1223 Self::toggle_recursive(&mut self.nodes, flat_index, &mut counter);
1224 }
1225
1226 fn toggle_recursive(nodes: &mut [TreeNode], target: usize, counter: &mut usize) -> bool {
1227 for node in nodes.iter_mut() {
1228 if *counter == target {
1229 if !node.is_leaf() {
1230 node.expanded = !node.expanded;
1231 }
1232 return true;
1233 }
1234 *counter += 1;
1235 if node.expanded && Self::toggle_recursive(&mut node.children, target, counter) {
1236 return true;
1237 }
1238 }
1239 false
1240 }
1241}
1242
1243pub struct PaletteCommand {
1247 pub label: String,
1248 pub description: String,
1249 pub shortcut: Option<String>,
1250}
1251
1252impl PaletteCommand {
1253 pub fn new(label: impl Into<String>, description: impl Into<String>) -> Self {
1254 Self {
1255 label: label.into(),
1256 description: description.into(),
1257 shortcut: None,
1258 }
1259 }
1260
1261 pub fn shortcut(mut self, s: impl Into<String>) -> Self {
1262 self.shortcut = Some(s.into());
1263 self
1264 }
1265}
1266
1267pub struct CommandPaletteState {
1271 pub commands: Vec<PaletteCommand>,
1272 pub input: String,
1273 pub cursor: usize,
1274 pub open: bool,
1275 selected: usize,
1276}
1277
1278impl CommandPaletteState {
1279 pub fn new(commands: Vec<PaletteCommand>) -> Self {
1280 Self {
1281 commands,
1282 input: String::new(),
1283 cursor: 0,
1284 open: false,
1285 selected: 0,
1286 }
1287 }
1288
1289 pub fn toggle(&mut self) {
1290 self.open = !self.open;
1291 if self.open {
1292 self.input.clear();
1293 self.cursor = 0;
1294 self.selected = 0;
1295 }
1296 }
1297
1298 pub(crate) fn filtered_indices(&self) -> Vec<usize> {
1299 let tokens: Vec<String> = self
1300 .input
1301 .split_whitespace()
1302 .map(|t| t.to_lowercase())
1303 .collect();
1304 if tokens.is_empty() {
1305 return (0..self.commands.len()).collect();
1306 }
1307 self.commands
1308 .iter()
1309 .enumerate()
1310 .filter(|(_, cmd)| {
1311 let label = cmd.label.to_lowercase();
1312 let desc = cmd.description.to_lowercase();
1313 tokens
1314 .iter()
1315 .all(|token| label.contains(token.as_str()) || desc.contains(token.as_str()))
1316 })
1317 .map(|(i, _)| i)
1318 .collect()
1319 }
1320
1321 pub(crate) fn selected(&self) -> usize {
1322 self.selected
1323 }
1324
1325 pub(crate) fn set_selected(&mut self, s: usize) {
1326 self.selected = s;
1327 }
1328}
1329
1330pub struct StreamingTextState {
1335 pub content: String,
1337 pub streaming: bool,
1339 pub(crate) cursor_visible: bool,
1341 pub(crate) cursor_tick: u64,
1342}
1343
1344impl StreamingTextState {
1345 pub fn new() -> Self {
1347 Self {
1348 content: String::new(),
1349 streaming: false,
1350 cursor_visible: true,
1351 cursor_tick: 0,
1352 }
1353 }
1354
1355 pub fn push(&mut self, chunk: &str) {
1357 self.content.push_str(chunk);
1358 }
1359
1360 pub fn finish(&mut self) {
1362 self.streaming = false;
1363 }
1364
1365 pub fn start(&mut self) {
1367 self.content.clear();
1368 self.streaming = true;
1369 self.cursor_visible = true;
1370 self.cursor_tick = 0;
1371 }
1372
1373 pub fn clear(&mut self) {
1375 self.content.clear();
1376 self.streaming = false;
1377 self.cursor_visible = true;
1378 self.cursor_tick = 0;
1379 }
1380}
1381
1382impl Default for StreamingTextState {
1383 fn default() -> Self {
1384 Self::new()
1385 }
1386}
1387
1388pub struct StreamingMarkdownState {
1393 pub content: String,
1395 pub streaming: bool,
1397 pub cursor_visible: bool,
1399 pub cursor_tick: u64,
1400 pub in_code_block: bool,
1401 pub code_block_lang: String,
1402}
1403
1404impl StreamingMarkdownState {
1405 pub fn new() -> Self {
1407 Self {
1408 content: String::new(),
1409 streaming: false,
1410 cursor_visible: true,
1411 cursor_tick: 0,
1412 in_code_block: false,
1413 code_block_lang: String::new(),
1414 }
1415 }
1416
1417 pub fn push(&mut self, chunk: &str) {
1419 self.content.push_str(chunk);
1420 }
1421
1422 pub fn start(&mut self) {
1424 self.content.clear();
1425 self.streaming = true;
1426 self.cursor_visible = true;
1427 self.cursor_tick = 0;
1428 self.in_code_block = false;
1429 self.code_block_lang.clear();
1430 }
1431
1432 pub fn finish(&mut self) {
1434 self.streaming = false;
1435 }
1436
1437 pub fn clear(&mut self) {
1439 self.content.clear();
1440 self.streaming = false;
1441 self.cursor_visible = true;
1442 self.cursor_tick = 0;
1443 self.in_code_block = false;
1444 self.code_block_lang.clear();
1445 }
1446}
1447
1448impl Default for StreamingMarkdownState {
1449 fn default() -> Self {
1450 Self::new()
1451 }
1452}
1453
1454#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1456pub enum ApprovalAction {
1457 Pending,
1459 Approved,
1461 Rejected,
1463}
1464
1465pub struct ToolApprovalState {
1471 pub tool_name: String,
1473 pub description: String,
1475 pub action: ApprovalAction,
1477}
1478
1479impl ToolApprovalState {
1480 pub fn new(tool_name: impl Into<String>, description: impl Into<String>) -> Self {
1482 Self {
1483 tool_name: tool_name.into(),
1484 description: description.into(),
1485 action: ApprovalAction::Pending,
1486 }
1487 }
1488
1489 pub fn reset(&mut self) {
1491 self.action = ApprovalAction::Pending;
1492 }
1493}
1494
1495#[derive(Debug, Clone)]
1497pub struct ContextItem {
1498 pub label: String,
1500 pub tokens: usize,
1502}
1503
1504impl ContextItem {
1505 pub fn new(label: impl Into<String>, tokens: usize) -> Self {
1507 Self {
1508 label: label.into(),
1509 tokens,
1510 }
1511 }
1512}
1513
1514#[cfg(test)]
1515mod tests {
1516 use super::*;
1517
1518 #[test]
1519 fn form_field_default_values() {
1520 let field = FormField::default();
1521 assert_eq!(field.label, "");
1522 assert_eq!(field.input.value, "");
1523 assert_eq!(field.input.cursor, 0);
1524 assert_eq!(field.error, None);
1525 }
1526
1527 #[test]
1528 fn toast_message_default_values() {
1529 let msg = ToastMessage::default();
1530 assert_eq!(msg.text, "");
1531 assert!(matches!(msg.level, ToastLevel::Info));
1532 assert_eq!(msg.created_tick, 0);
1533 assert_eq!(msg.duration_ticks, 30);
1534 }
1535
1536 #[test]
1537 fn list_state_default_values() {
1538 let state = ListState::default();
1539 assert!(state.items.is_empty());
1540 assert_eq!(state.selected, 0);
1541 assert_eq!(state.filter, "");
1542 assert_eq!(state.visible_indices(), &[]);
1543 assert_eq!(state.selected_item(), None);
1544 }
1545
1546 #[test]
1547 fn file_entry_default_values() {
1548 let entry = FileEntry::default();
1549 assert_eq!(entry.name, "");
1550 assert_eq!(entry.path, PathBuf::new());
1551 assert!(!entry.is_dir);
1552 assert_eq!(entry.size, 0);
1553 }
1554
1555 #[test]
1556 fn tabs_state_default_values() {
1557 let state = TabsState::default();
1558 assert!(state.labels.is_empty());
1559 assert_eq!(state.selected, 0);
1560 assert_eq!(state.selected_label(), None);
1561 }
1562
1563 #[test]
1564 fn table_state_default_values() {
1565 let state = TableState::default();
1566 assert!(state.headers.is_empty());
1567 assert!(state.rows.is_empty());
1568 assert_eq!(state.selected, 0);
1569 assert_eq!(state.sort_column, None);
1570 assert!(state.sort_ascending);
1571 assert_eq!(state.filter, "");
1572 assert_eq!(state.page, 0);
1573 assert_eq!(state.page_size, 0);
1574 assert_eq!(state.visible_indices(), &[]);
1575 }
1576
1577 #[test]
1578 fn select_state_default_values() {
1579 let state = SelectState::default();
1580 assert!(state.items.is_empty());
1581 assert_eq!(state.selected, 0);
1582 assert!(!state.open);
1583 assert_eq!(state.placeholder, "");
1584 assert_eq!(state.selected_item(), None);
1585 assert_eq!(state.cursor(), 0);
1586 }
1587
1588 #[test]
1589 fn radio_state_default_values() {
1590 let state = RadioState::default();
1591 assert!(state.items.is_empty());
1592 assert_eq!(state.selected, 0);
1593 assert_eq!(state.selected_item(), None);
1594 }
1595
1596 #[test]
1597 fn text_input_state_default_uses_new() {
1598 let state = TextInputState::default();
1599 assert_eq!(state.value, "");
1600 assert_eq!(state.cursor, 0);
1601 assert_eq!(state.placeholder, "");
1602 assert_eq!(state.max_length, None);
1603 assert_eq!(state.validation_error, None);
1604 assert!(!state.masked);
1605 }
1606
1607 #[test]
1608 fn tabs_state_new_sets_labels() {
1609 let state = TabsState::new(vec!["a", "b"]);
1610 assert_eq!(state.labels, vec!["a".to_string(), "b".to_string()]);
1611 assert_eq!(state.selected, 0);
1612 assert_eq!(state.selected_label(), Some("a"));
1613 }
1614
1615 #[test]
1616 fn list_state_new_selected_item_points_to_first_item() {
1617 let state = ListState::new(vec!["alpha", "beta"]);
1618 assert_eq!(state.items, vec!["alpha".to_string(), "beta".to_string()]);
1619 assert_eq!(state.selected, 0);
1620 assert_eq!(state.visible_indices(), &[0, 1]);
1621 assert_eq!(state.selected_item(), Some("alpha"));
1622 }
1623
1624 #[test]
1625 fn select_state_placeholder_builder_sets_value() {
1626 let state = SelectState::new(vec!["one", "two"]).placeholder("Pick one");
1627 assert_eq!(state.items, vec!["one".to_string(), "two".to_string()]);
1628 assert_eq!(state.placeholder, "Pick one");
1629 assert_eq!(state.selected_item(), Some("one"));
1630 }
1631
1632 #[test]
1633 fn radio_state_new_sets_items_and_selection() {
1634 let state = RadioState::new(vec!["red", "green"]);
1635 assert_eq!(state.items, vec!["red".to_string(), "green".to_string()]);
1636 assert_eq!(state.selected, 0);
1637 assert_eq!(state.selected_item(), Some("red"));
1638 }
1639
1640 #[test]
1641 fn table_state_new_sets_sort_ascending_true() {
1642 let state = TableState::new(vec!["Name"], vec![vec!["Alice"], vec!["Bob"]]);
1643 assert_eq!(state.headers, vec!["Name".to_string()]);
1644 assert_eq!(state.rows.len(), 2);
1645 assert!(state.sort_ascending);
1646 assert_eq!(state.sort_column, None);
1647 assert_eq!(state.visible_indices(), &[0, 1]);
1648 }
1649}