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>,
45 pub suggestion_index: usize,
47 pub show_suggestions: bool,
49 validators: Vec<TextInputValidator>,
51 validation_errors: Vec<String>,
53}
54
55impl std::fmt::Debug for TextInputState {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 f.debug_struct("TextInputState")
58 .field("value", &self.value)
59 .field("cursor", &self.cursor)
60 .field("placeholder", &self.placeholder)
61 .field("max_length", &self.max_length)
62 .field("validation_error", &self.validation_error)
63 .field("masked", &self.masked)
64 .field("suggestions", &self.suggestions)
65 .field("suggestion_index", &self.suggestion_index)
66 .field("show_suggestions", &self.show_suggestions)
67 .field("validators_len", &self.validators.len())
68 .field("validation_errors", &self.validation_errors)
69 .finish()
70 }
71}
72
73impl Clone for TextInputState {
74 fn clone(&self) -> Self {
75 Self {
76 value: self.value.clone(),
77 cursor: self.cursor,
78 placeholder: self.placeholder.clone(),
79 max_length: self.max_length,
80 validation_error: self.validation_error.clone(),
81 masked: self.masked,
82 suggestions: self.suggestions.clone(),
83 suggestion_index: self.suggestion_index,
84 show_suggestions: self.show_suggestions,
85 validators: Vec::new(),
86 validation_errors: self.validation_errors.clone(),
87 }
88 }
89}
90
91impl TextInputState {
92 pub fn new() -> Self {
94 Self {
95 value: String::new(),
96 cursor: 0,
97 placeholder: String::new(),
98 max_length: None,
99 validation_error: None,
100 masked: false,
101 suggestions: Vec::new(),
102 suggestion_index: 0,
103 show_suggestions: false,
104 validators: Vec::new(),
105 validation_errors: Vec::new(),
106 }
107 }
108
109 pub fn with_placeholder(p: impl Into<String>) -> Self {
111 Self {
112 placeholder: p.into(),
113 ..Self::new()
114 }
115 }
116
117 pub fn max_length(mut self, len: usize) -> Self {
119 self.max_length = Some(len);
120 self
121 }
122
123 pub fn validate(&mut self, validator: impl Fn(&str) -> Result<(), String>) {
131 self.validation_error = validator(&self.value).err();
132 }
133
134 pub fn add_validator(&mut self, f: impl Fn(&str) -> Result<(), String> + 'static) {
139 self.validators.push(Box::new(f));
140 }
141
142 pub fn run_validators(&mut self) {
147 self.validation_errors.clear();
148 for validator in &self.validators {
149 if let Err(err) = validator(&self.value) {
150 self.validation_errors.push(err);
151 }
152 }
153 self.validation_error = self.validation_errors.first().cloned();
154 }
155
156 pub fn errors(&self) -> &[String] {
158 &self.validation_errors
159 }
160
161 pub fn set_suggestions(&mut self, suggestions: Vec<String>) {
163 self.suggestions = suggestions;
164 self.suggestion_index = 0;
165 self.show_suggestions = !self.suggestions.is_empty();
166 }
167
168 pub fn matched_suggestions(&self) -> Vec<&str> {
170 if self.value.is_empty() {
171 return Vec::new();
172 }
173 let lower = self.value.to_lowercase();
174 self.suggestions
175 .iter()
176 .filter(|s| s.to_lowercase().starts_with(&lower))
177 .map(|s| s.as_str())
178 .collect()
179 }
180}
181
182impl Default for TextInputState {
183 fn default() -> Self {
184 Self::new()
185 }
186}
187
188#[derive(Debug, Default)]
190pub struct FormField {
191 pub label: String,
193 pub input: TextInputState,
195 pub error: Option<String>,
197}
198
199impl FormField {
200 pub fn new(label: impl Into<String>) -> Self {
202 Self {
203 label: label.into(),
204 input: TextInputState::new(),
205 error: None,
206 }
207 }
208
209 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
211 self.input.placeholder = p.into();
212 self
213 }
214}
215
216#[derive(Debug)]
218pub struct FormState {
219 pub fields: Vec<FormField>,
221 pub submitted: bool,
223}
224
225impl FormState {
226 pub fn new() -> Self {
228 Self {
229 fields: Vec::new(),
230 submitted: false,
231 }
232 }
233
234 pub fn field(mut self, field: FormField) -> Self {
236 self.fields.push(field);
237 self
238 }
239
240 pub fn validate(&mut self, validators: &[FormValidator]) -> bool {
244 let mut all_valid = true;
245 for (i, field) in self.fields.iter_mut().enumerate() {
246 if let Some(validator) = validators.get(i) {
247 match validator(&field.input.value) {
248 Ok(()) => field.error = None,
249 Err(msg) => {
250 field.error = Some(msg);
251 all_valid = false;
252 }
253 }
254 }
255 }
256 all_valid
257 }
258
259 pub fn value(&self, index: usize) -> &str {
261 self.fields
262 .get(index)
263 .map(|f| f.input.value.as_str())
264 .unwrap_or("")
265 }
266}
267
268impl Default for FormState {
269 fn default() -> Self {
270 Self::new()
271 }
272}
273
274#[derive(Debug, Clone)]
280pub struct ToastState {
281 pub messages: Vec<ToastMessage>,
283}
284
285#[derive(Debug, Clone)]
287pub struct ToastMessage {
288 pub text: String,
290 pub level: ToastLevel,
292 pub created_tick: u64,
294 pub duration_ticks: u64,
296}
297
298impl Default for ToastMessage {
299 fn default() -> Self {
300 Self {
301 text: String::new(),
302 level: ToastLevel::Info,
303 created_tick: 0,
304 duration_ticks: 30,
305 }
306 }
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub enum ToastLevel {
312 Info,
314 Success,
316 Warning,
318 Error,
320}
321
322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub enum AlertLevel {
324 Info,
326 Success,
328 Warning,
330 Error,
332}
333
334impl ToastState {
335 pub fn new() -> Self {
337 Self {
338 messages: Vec::new(),
339 }
340 }
341
342 pub fn info(&mut self, text: impl Into<String>, tick: u64) {
344 self.push(text, ToastLevel::Info, tick, 30);
345 }
346
347 pub fn success(&mut self, text: impl Into<String>, tick: u64) {
349 self.push(text, ToastLevel::Success, tick, 30);
350 }
351
352 pub fn warning(&mut self, text: impl Into<String>, tick: u64) {
354 self.push(text, ToastLevel::Warning, tick, 50);
355 }
356
357 pub fn error(&mut self, text: impl Into<String>, tick: u64) {
359 self.push(text, ToastLevel::Error, tick, 80);
360 }
361
362 pub fn push(
364 &mut self,
365 text: impl Into<String>,
366 level: ToastLevel,
367 tick: u64,
368 duration_ticks: u64,
369 ) {
370 self.messages.push(ToastMessage {
371 text: text.into(),
372 level,
373 created_tick: tick,
374 duration_ticks,
375 });
376 }
377
378 pub fn cleanup(&mut self, current_tick: u64) {
382 self.messages.retain(|message| {
383 current_tick < message.created_tick.saturating_add(message.duration_ticks)
384 });
385 }
386}
387
388impl Default for ToastState {
389 fn default() -> Self {
390 Self::new()
391 }
392}
393
394#[derive(Debug, Clone)]
399pub struct TextareaState {
400 pub lines: Vec<String>,
402 pub cursor_row: usize,
404 pub cursor_col: usize,
406 pub max_length: Option<usize>,
408 pub wrap_width: Option<u32>,
410 pub scroll_offset: usize,
412}
413
414impl TextareaState {
415 pub fn new() -> Self {
417 Self {
418 lines: vec![String::new()],
419 cursor_row: 0,
420 cursor_col: 0,
421 max_length: None,
422 wrap_width: None,
423 scroll_offset: 0,
424 }
425 }
426
427 pub fn value(&self) -> String {
429 self.lines.join("\n")
430 }
431
432 pub fn set_value(&mut self, text: impl Into<String>) {
436 let value = text.into();
437 self.lines = value.split('\n').map(str::to_string).collect();
438 if self.lines.is_empty() {
439 self.lines.push(String::new());
440 }
441 self.cursor_row = 0;
442 self.cursor_col = 0;
443 self.scroll_offset = 0;
444 }
445
446 pub fn max_length(mut self, len: usize) -> Self {
448 self.max_length = Some(len);
449 self
450 }
451
452 pub fn word_wrap(mut self, width: u32) -> Self {
454 self.wrap_width = Some(width);
455 self
456 }
457}
458
459impl Default for TextareaState {
460 fn default() -> Self {
461 Self::new()
462 }
463}
464
465#[derive(Debug, Clone)]
471pub struct SpinnerState {
472 chars: Vec<char>,
473}
474
475impl SpinnerState {
476 pub fn dots() -> Self {
480 Self {
481 chars: vec!['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
482 }
483 }
484
485 pub fn line() -> Self {
489 Self {
490 chars: vec!['|', '/', '-', '\\'],
491 }
492 }
493
494 pub fn frame(&self, tick: u64) -> char {
496 if self.chars.is_empty() {
497 return ' ';
498 }
499 self.chars[tick as usize % self.chars.len()]
500 }
501}
502
503impl Default for SpinnerState {
504 fn default() -> Self {
505 Self::dots()
506 }
507}
508
509#[derive(Debug, Clone, Default)]
514pub struct ListState {
515 pub items: Vec<String>,
517 pub selected: usize,
519 pub filter: String,
521 view_indices: Vec<usize>,
522}
523
524impl ListState {
525 pub fn new(items: Vec<impl Into<String>>) -> Self {
527 let len = items.len();
528 Self {
529 items: items.into_iter().map(Into::into).collect(),
530 selected: 0,
531 filter: String::new(),
532 view_indices: (0..len).collect(),
533 }
534 }
535
536 pub fn set_items(&mut self, items: Vec<impl Into<String>>) {
541 self.items = items.into_iter().map(Into::into).collect();
542 self.selected = self.selected.min(self.items.len().saturating_sub(1));
543 self.rebuild_view();
544 }
545
546 pub fn set_filter(&mut self, filter: impl Into<String>) {
550 self.filter = filter.into();
551 self.rebuild_view();
552 }
553
554 pub fn visible_indices(&self) -> &[usize] {
556 &self.view_indices
557 }
558
559 pub fn selected_item(&self) -> Option<&str> {
561 let data_idx = *self.view_indices.get(self.selected)?;
562 self.items.get(data_idx).map(String::as_str)
563 }
564
565 fn rebuild_view(&mut self) {
566 let tokens: Vec<String> = self
567 .filter
568 .split_whitespace()
569 .map(|t| t.to_lowercase())
570 .collect();
571 self.view_indices = if tokens.is_empty() {
572 (0..self.items.len()).collect()
573 } else {
574 (0..self.items.len())
575 .filter(|&i| {
576 tokens
577 .iter()
578 .all(|token| self.items[i].to_lowercase().contains(token.as_str()))
579 })
580 .collect()
581 };
582 if !self.view_indices.is_empty() && self.selected >= self.view_indices.len() {
583 self.selected = self.view_indices.len() - 1;
584 }
585 }
586}
587
588#[derive(Debug, Clone)]
592pub struct FilePickerState {
593 pub current_dir: PathBuf,
595 pub entries: Vec<FileEntry>,
597 pub selected: usize,
599 pub selected_file: Option<PathBuf>,
601 pub show_hidden: bool,
603 pub extensions: Vec<String>,
605 pub dirty: bool,
607}
608
609#[derive(Debug, Clone, Default)]
611pub struct FileEntry {
612 pub name: String,
614 pub path: PathBuf,
616 pub is_dir: bool,
618 pub size: u64,
620}
621
622impl FilePickerState {
623 pub fn new(dir: impl Into<PathBuf>) -> Self {
625 Self {
626 current_dir: dir.into(),
627 entries: Vec::new(),
628 selected: 0,
629 selected_file: None,
630 show_hidden: false,
631 extensions: Vec::new(),
632 dirty: true,
633 }
634 }
635
636 pub fn show_hidden(mut self, show: bool) -> Self {
638 self.show_hidden = show;
639 self.dirty = true;
640 self
641 }
642
643 pub fn extensions(mut self, exts: &[&str]) -> Self {
645 self.extensions = exts
646 .iter()
647 .map(|ext| ext.trim().trim_start_matches('.').to_ascii_lowercase())
648 .filter(|ext| !ext.is_empty())
649 .collect();
650 self.dirty = true;
651 self
652 }
653
654 pub fn selected(&self) -> Option<&PathBuf> {
656 self.selected_file.as_ref()
657 }
658
659 pub fn refresh(&mut self) {
661 let mut entries = Vec::new();
662
663 if let Ok(read_dir) = fs::read_dir(&self.current_dir) {
664 for dir_entry in read_dir.flatten() {
665 let name = dir_entry.file_name().to_string_lossy().to_string();
666 if !self.show_hidden && name.starts_with('.') {
667 continue;
668 }
669
670 let Ok(file_type) = dir_entry.file_type() else {
671 continue;
672 };
673 if file_type.is_symlink() {
674 continue;
675 }
676
677 let path = dir_entry.path();
678 let is_dir = file_type.is_dir();
679
680 if !is_dir && !self.extensions.is_empty() {
681 let ext = path
682 .extension()
683 .and_then(|e| e.to_str())
684 .map(|e| e.to_ascii_lowercase());
685 let Some(ext) = ext else {
686 continue;
687 };
688 if !self.extensions.iter().any(|allowed| allowed == &ext) {
689 continue;
690 }
691 }
692
693 let size = if is_dir {
694 0
695 } else {
696 fs::symlink_metadata(&path).map(|m| m.len()).unwrap_or(0)
697 };
698
699 entries.push(FileEntry {
700 name,
701 path,
702 is_dir,
703 size,
704 });
705 }
706 }
707
708 entries.sort_by(|a, b| match (a.is_dir, b.is_dir) {
709 (true, false) => std::cmp::Ordering::Less,
710 (false, true) => std::cmp::Ordering::Greater,
711 _ => a
712 .name
713 .to_ascii_lowercase()
714 .cmp(&b.name.to_ascii_lowercase())
715 .then_with(|| a.name.cmp(&b.name)),
716 });
717
718 self.entries = entries;
719 if self.entries.is_empty() {
720 self.selected = 0;
721 } else {
722 self.selected = self.selected.min(self.entries.len().saturating_sub(1));
723 }
724 self.dirty = false;
725 }
726}
727
728impl Default for FilePickerState {
729 fn default() -> Self {
730 Self::new(".")
731 }
732}
733
734#[derive(Debug, Clone, Default)]
739pub struct TabsState {
740 pub labels: Vec<String>,
742 pub selected: usize,
744}
745
746impl TabsState {
747 pub fn new(labels: Vec<impl Into<String>>) -> Self {
749 Self {
750 labels: labels.into_iter().map(Into::into).collect(),
751 selected: 0,
752 }
753 }
754
755 pub fn selected_label(&self) -> Option<&str> {
757 self.labels.get(self.selected).map(String::as_str)
758 }
759}
760
761#[derive(Debug, Clone)]
767pub struct TableState {
768 pub headers: Vec<String>,
770 pub rows: Vec<Vec<String>>,
772 pub selected: usize,
774 column_widths: Vec<u32>,
775 dirty: bool,
776 pub sort_column: Option<usize>,
778 pub sort_ascending: bool,
780 pub filter: String,
782 pub page: usize,
784 pub page_size: usize,
786 view_indices: Vec<usize>,
787}
788
789impl Default for TableState {
790 fn default() -> Self {
791 Self {
792 headers: Vec::new(),
793 rows: Vec::new(),
794 selected: 0,
795 column_widths: Vec::new(),
796 dirty: true,
797 sort_column: None,
798 sort_ascending: true,
799 filter: String::new(),
800 page: 0,
801 page_size: 0,
802 view_indices: Vec::new(),
803 }
804 }
805}
806
807impl TableState {
808 pub fn new(headers: Vec<impl Into<String>>, rows: Vec<Vec<impl Into<String>>>) -> Self {
810 let headers: Vec<String> = headers.into_iter().map(Into::into).collect();
811 let rows: Vec<Vec<String>> = rows
812 .into_iter()
813 .map(|r| r.into_iter().map(Into::into).collect())
814 .collect();
815 let mut state = Self {
816 headers,
817 rows,
818 selected: 0,
819 column_widths: Vec::new(),
820 dirty: true,
821 sort_column: None,
822 sort_ascending: true,
823 filter: String::new(),
824 page: 0,
825 page_size: 0,
826 view_indices: Vec::new(),
827 };
828 state.rebuild_view();
829 state.recompute_widths();
830 state
831 }
832
833 pub fn set_rows(&mut self, rows: Vec<Vec<impl Into<String>>>) {
838 self.rows = rows
839 .into_iter()
840 .map(|r| r.into_iter().map(Into::into).collect())
841 .collect();
842 self.rebuild_view();
843 }
844
845 pub fn toggle_sort(&mut self, column: usize) {
847 if self.sort_column == Some(column) {
848 self.sort_ascending = !self.sort_ascending;
849 } else {
850 self.sort_column = Some(column);
851 self.sort_ascending = true;
852 }
853 self.rebuild_view();
854 }
855
856 pub fn sort_by(&mut self, column: usize) {
858 self.sort_column = Some(column);
859 self.sort_ascending = true;
860 self.rebuild_view();
861 }
862
863 pub fn set_filter(&mut self, filter: impl Into<String>) {
867 self.filter = filter.into();
868 self.page = 0;
869 self.rebuild_view();
870 }
871
872 pub fn clear_sort(&mut self) {
874 self.sort_column = None;
875 self.sort_ascending = true;
876 self.rebuild_view();
877 }
878
879 pub fn next_page(&mut self) {
881 if self.page_size == 0 {
882 return;
883 }
884 let last_page = self.total_pages().saturating_sub(1);
885 self.page = (self.page + 1).min(last_page);
886 }
887
888 pub fn prev_page(&mut self) {
890 self.page = self.page.saturating_sub(1);
891 }
892
893 pub fn total_pages(&self) -> usize {
895 if self.page_size == 0 {
896 return 1;
897 }
898
899 let len = self.view_indices.len();
900 if len == 0 {
901 1
902 } else {
903 len.div_ceil(self.page_size)
904 }
905 }
906
907 pub fn visible_indices(&self) -> &[usize] {
909 &self.view_indices
910 }
911
912 pub fn selected_row(&self) -> Option<&[String]> {
914 if self.view_indices.is_empty() {
915 return None;
916 }
917 let data_idx = self.view_indices.get(self.selected)?;
918 self.rows.get(*data_idx).map(|r| r.as_slice())
919 }
920
921 fn rebuild_view(&mut self) {
923 let mut indices: Vec<usize> = (0..self.rows.len()).collect();
924
925 let tokens: Vec<String> = self
926 .filter
927 .split_whitespace()
928 .map(|t| t.to_lowercase())
929 .collect();
930 if !tokens.is_empty() {
931 indices.retain(|&idx| {
932 let row = match self.rows.get(idx) {
933 Some(r) => r,
934 None => return false,
935 };
936 tokens.iter().all(|token| {
937 row.iter()
938 .any(|cell| cell.to_lowercase().contains(token.as_str()))
939 })
940 });
941 }
942
943 if let Some(column) = self.sort_column {
944 indices.sort_by(|a, b| {
945 let left = self
946 .rows
947 .get(*a)
948 .and_then(|row| row.get(column))
949 .map(String::as_str)
950 .unwrap_or("");
951 let right = self
952 .rows
953 .get(*b)
954 .and_then(|row| row.get(column))
955 .map(String::as_str)
956 .unwrap_or("");
957
958 match (left.parse::<f64>(), right.parse::<f64>()) {
959 (Ok(l), Ok(r)) => l.partial_cmp(&r).unwrap_or(std::cmp::Ordering::Equal),
960 _ => left.to_lowercase().cmp(&right.to_lowercase()),
961 }
962 });
963
964 if !self.sort_ascending {
965 indices.reverse();
966 }
967 }
968
969 self.view_indices = indices;
970
971 if self.page_size > 0 {
972 self.page = self.page.min(self.total_pages().saturating_sub(1));
973 } else {
974 self.page = 0;
975 }
976
977 self.selected = self.selected.min(self.view_indices.len().saturating_sub(1));
978 self.dirty = true;
979 }
980
981 pub(crate) fn recompute_widths(&mut self) {
982 let col_count = self.headers.len();
983 self.column_widths = vec![0u32; col_count];
984 for (i, header) in self.headers.iter().enumerate() {
985 let mut width = UnicodeWidthStr::width(header.as_str()) as u32;
986 if self.sort_column == Some(i) {
987 width += 2;
988 }
989 self.column_widths[i] = width;
990 }
991 for row in &self.rows {
992 for (i, cell) in row.iter().enumerate() {
993 if i < col_count {
994 let w = UnicodeWidthStr::width(cell.as_str()) as u32;
995 self.column_widths[i] = self.column_widths[i].max(w);
996 }
997 }
998 }
999 self.dirty = false;
1000 }
1001
1002 pub(crate) fn column_widths(&self) -> &[u32] {
1003 &self.column_widths
1004 }
1005
1006 pub(crate) fn is_dirty(&self) -> bool {
1007 self.dirty
1008 }
1009}
1010
1011#[derive(Debug, Clone)]
1017pub struct ScrollState {
1018 pub offset: usize,
1020 content_height: u32,
1021 viewport_height: u32,
1022}
1023
1024impl ScrollState {
1025 pub fn new() -> Self {
1027 Self {
1028 offset: 0,
1029 content_height: 0,
1030 viewport_height: 0,
1031 }
1032 }
1033
1034 pub fn can_scroll_up(&self) -> bool {
1036 self.offset > 0
1037 }
1038
1039 pub fn can_scroll_down(&self) -> bool {
1041 (self.offset as u32) + self.viewport_height < self.content_height
1042 }
1043
1044 pub fn content_height(&self) -> u32 {
1046 self.content_height
1047 }
1048
1049 pub fn viewport_height(&self) -> u32 {
1051 self.viewport_height
1052 }
1053
1054 pub fn progress(&self) -> f32 {
1056 let max = self.content_height.saturating_sub(self.viewport_height);
1057 if max == 0 {
1058 0.0
1059 } else {
1060 self.offset as f32 / max as f32
1061 }
1062 }
1063
1064 pub fn scroll_up(&mut self, amount: usize) {
1066 self.offset = self.offset.saturating_sub(amount);
1067 }
1068
1069 pub fn scroll_down(&mut self, amount: usize) {
1071 let max_offset = self.content_height.saturating_sub(self.viewport_height) as usize;
1072 self.offset = (self.offset + amount).min(max_offset);
1073 }
1074
1075 pub(crate) fn set_bounds(&mut self, content_height: u32, viewport_height: u32) {
1076 self.content_height = content_height;
1077 self.viewport_height = viewport_height;
1078 }
1079}
1080
1081impl Default for ScrollState {
1082 fn default() -> Self {
1083 Self::new()
1084 }
1085}
1086
1087#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1097pub enum ButtonVariant {
1098 #[default]
1100 Default,
1101 Primary,
1103 Danger,
1105 Outline,
1107}
1108
1109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1110pub enum Trend {
1111 Up,
1113 Down,
1115}
1116
1117#[derive(Debug, Clone, Default)]
1124pub struct SelectState {
1125 pub items: Vec<String>,
1127 pub selected: usize,
1129 pub open: bool,
1131 pub placeholder: String,
1133 cursor: usize,
1134}
1135
1136impl SelectState {
1137 pub fn new(items: Vec<impl Into<String>>) -> Self {
1139 Self {
1140 items: items.into_iter().map(Into::into).collect(),
1141 selected: 0,
1142 open: false,
1143 placeholder: String::new(),
1144 cursor: 0,
1145 }
1146 }
1147
1148 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
1150 self.placeholder = p.into();
1151 self
1152 }
1153
1154 pub fn selected_item(&self) -> Option<&str> {
1156 self.items.get(self.selected).map(String::as_str)
1157 }
1158
1159 pub(crate) fn cursor(&self) -> usize {
1160 self.cursor
1161 }
1162
1163 pub(crate) fn set_cursor(&mut self, c: usize) {
1164 self.cursor = c;
1165 }
1166}
1167
1168#[derive(Debug, Clone, Default)]
1174pub struct RadioState {
1175 pub items: Vec<String>,
1177 pub selected: usize,
1179}
1180
1181impl RadioState {
1182 pub fn new(items: Vec<impl Into<String>>) -> Self {
1184 Self {
1185 items: items.into_iter().map(Into::into).collect(),
1186 selected: 0,
1187 }
1188 }
1189
1190 pub fn selected_item(&self) -> Option<&str> {
1192 self.items.get(self.selected).map(String::as_str)
1193 }
1194}
1195
1196#[derive(Debug, Clone)]
1202pub struct MultiSelectState {
1203 pub items: Vec<String>,
1205 pub cursor: usize,
1207 pub selected: HashSet<usize>,
1209}
1210
1211impl MultiSelectState {
1212 pub fn new(items: Vec<impl Into<String>>) -> Self {
1214 Self {
1215 items: items.into_iter().map(Into::into).collect(),
1216 cursor: 0,
1217 selected: HashSet::new(),
1218 }
1219 }
1220
1221 pub fn selected_items(&self) -> Vec<&str> {
1223 let mut indices: Vec<usize> = self.selected.iter().copied().collect();
1224 indices.sort();
1225 indices
1226 .iter()
1227 .filter_map(|&i| self.items.get(i).map(String::as_str))
1228 .collect()
1229 }
1230
1231 pub fn toggle(&mut self, index: usize) {
1233 if self.selected.contains(&index) {
1234 self.selected.remove(&index);
1235 } else {
1236 self.selected.insert(index);
1237 }
1238 }
1239}
1240
1241#[derive(Debug, Clone)]
1245pub struct TreeNode {
1246 pub label: String,
1248 pub children: Vec<TreeNode>,
1250 pub expanded: bool,
1252}
1253
1254impl TreeNode {
1255 pub fn new(label: impl Into<String>) -> Self {
1257 Self {
1258 label: label.into(),
1259 children: Vec::new(),
1260 expanded: false,
1261 }
1262 }
1263
1264 pub fn expanded(mut self) -> Self {
1266 self.expanded = true;
1267 self
1268 }
1269
1270 pub fn children(mut self, children: Vec<TreeNode>) -> Self {
1272 self.children = children;
1273 self
1274 }
1275
1276 pub fn is_leaf(&self) -> bool {
1278 self.children.is_empty()
1279 }
1280
1281 fn flatten(&self, depth: usize, out: &mut Vec<FlatTreeEntry>) {
1282 out.push(FlatTreeEntry {
1283 depth,
1284 label: self.label.clone(),
1285 is_leaf: self.is_leaf(),
1286 expanded: self.expanded,
1287 });
1288 if self.expanded {
1289 for child in &self.children {
1290 child.flatten(depth + 1, out);
1291 }
1292 }
1293 }
1294}
1295
1296pub(crate) struct FlatTreeEntry {
1297 pub depth: usize,
1298 pub label: String,
1299 pub is_leaf: bool,
1300 pub expanded: bool,
1301}
1302
1303#[derive(Debug, Clone)]
1305pub struct TreeState {
1306 pub nodes: Vec<TreeNode>,
1308 pub selected: usize,
1310}
1311
1312impl TreeState {
1313 pub fn new(nodes: Vec<TreeNode>) -> Self {
1315 Self { nodes, selected: 0 }
1316 }
1317
1318 pub(crate) fn flatten(&self) -> Vec<FlatTreeEntry> {
1319 let mut entries = Vec::new();
1320 for node in &self.nodes {
1321 node.flatten(0, &mut entries);
1322 }
1323 entries
1324 }
1325
1326 pub(crate) fn toggle_at(&mut self, flat_index: usize) {
1327 let mut counter = 0usize;
1328 Self::toggle_recursive(&mut self.nodes, flat_index, &mut counter);
1329 }
1330
1331 fn toggle_recursive(nodes: &mut [TreeNode], target: usize, counter: &mut usize) -> bool {
1332 for node in nodes.iter_mut() {
1333 if *counter == target {
1334 if !node.is_leaf() {
1335 node.expanded = !node.expanded;
1336 }
1337 return true;
1338 }
1339 *counter += 1;
1340 if node.expanded && Self::toggle_recursive(&mut node.children, target, counter) {
1341 return true;
1342 }
1343 }
1344 false
1345 }
1346}
1347
1348#[derive(Debug, Clone)]
1352pub struct PaletteCommand {
1353 pub label: String,
1355 pub description: String,
1357 pub shortcut: Option<String>,
1359}
1360
1361impl PaletteCommand {
1362 pub fn new(label: impl Into<String>, description: impl Into<String>) -> Self {
1364 Self {
1365 label: label.into(),
1366 description: description.into(),
1367 shortcut: None,
1368 }
1369 }
1370
1371 pub fn shortcut(mut self, s: impl Into<String>) -> Self {
1373 self.shortcut = Some(s.into());
1374 self
1375 }
1376}
1377
1378#[derive(Debug, Clone)]
1382pub struct CommandPaletteState {
1383 pub commands: Vec<PaletteCommand>,
1385 pub input: String,
1387 pub cursor: usize,
1389 pub open: bool,
1391 pub last_selected: Option<usize>,
1394 selected: usize,
1395}
1396
1397impl CommandPaletteState {
1398 pub fn new(commands: Vec<PaletteCommand>) -> Self {
1400 Self {
1401 commands,
1402 input: String::new(),
1403 cursor: 0,
1404 open: false,
1405 last_selected: None,
1406 selected: 0,
1407 }
1408 }
1409
1410 pub fn toggle(&mut self) {
1412 self.open = !self.open;
1413 if self.open {
1414 self.input.clear();
1415 self.cursor = 0;
1416 self.selected = 0;
1417 }
1418 }
1419
1420 pub(crate) fn filtered_indices(&self) -> Vec<usize> {
1421 let tokens: Vec<String> = self
1422 .input
1423 .split_whitespace()
1424 .map(|t| t.to_lowercase())
1425 .collect();
1426 if tokens.is_empty() {
1427 return (0..self.commands.len()).collect();
1428 }
1429 self.commands
1430 .iter()
1431 .enumerate()
1432 .filter(|(_, cmd)| {
1433 let label = cmd.label.to_lowercase();
1434 let desc = cmd.description.to_lowercase();
1435 tokens
1436 .iter()
1437 .all(|token| label.contains(token.as_str()) || desc.contains(token.as_str()))
1438 })
1439 .map(|(i, _)| i)
1440 .collect()
1441 }
1442
1443 pub(crate) fn selected(&self) -> usize {
1444 self.selected
1445 }
1446
1447 pub(crate) fn set_selected(&mut self, s: usize) {
1448 self.selected = s;
1449 }
1450}
1451
1452#[derive(Debug, Clone)]
1457pub struct StreamingTextState {
1458 pub content: String,
1460 pub streaming: bool,
1462 pub(crate) cursor_visible: bool,
1464 pub(crate) cursor_tick: u64,
1465}
1466
1467impl StreamingTextState {
1468 pub fn new() -> Self {
1470 Self {
1471 content: String::new(),
1472 streaming: false,
1473 cursor_visible: true,
1474 cursor_tick: 0,
1475 }
1476 }
1477
1478 pub fn push(&mut self, chunk: &str) {
1480 self.content.push_str(chunk);
1481 }
1482
1483 pub fn finish(&mut self) {
1485 self.streaming = false;
1486 }
1487
1488 pub fn start(&mut self) {
1490 self.content.clear();
1491 self.streaming = true;
1492 self.cursor_visible = true;
1493 self.cursor_tick = 0;
1494 }
1495
1496 pub fn clear(&mut self) {
1498 self.content.clear();
1499 self.streaming = false;
1500 self.cursor_visible = true;
1501 self.cursor_tick = 0;
1502 }
1503}
1504
1505impl Default for StreamingTextState {
1506 fn default() -> Self {
1507 Self::new()
1508 }
1509}
1510
1511#[derive(Debug, Clone)]
1516pub struct StreamingMarkdownState {
1517 pub content: String,
1519 pub streaming: bool,
1521 pub cursor_visible: bool,
1523 pub cursor_tick: u64,
1525 pub in_code_block: bool,
1527 pub code_block_lang: String,
1529}
1530
1531impl StreamingMarkdownState {
1532 pub fn new() -> Self {
1534 Self {
1535 content: String::new(),
1536 streaming: false,
1537 cursor_visible: true,
1538 cursor_tick: 0,
1539 in_code_block: false,
1540 code_block_lang: String::new(),
1541 }
1542 }
1543
1544 pub fn push(&mut self, chunk: &str) {
1546 self.content.push_str(chunk);
1547 }
1548
1549 pub fn start(&mut self) {
1551 self.content.clear();
1552 self.streaming = true;
1553 self.cursor_visible = true;
1554 self.cursor_tick = 0;
1555 self.in_code_block = false;
1556 self.code_block_lang.clear();
1557 }
1558
1559 pub fn finish(&mut self) {
1561 self.streaming = false;
1562 }
1563
1564 pub fn clear(&mut self) {
1566 self.content.clear();
1567 self.streaming = false;
1568 self.cursor_visible = true;
1569 self.cursor_tick = 0;
1570 self.in_code_block = false;
1571 self.code_block_lang.clear();
1572 }
1573}
1574
1575impl Default for StreamingMarkdownState {
1576 fn default() -> Self {
1577 Self::new()
1578 }
1579}
1580
1581#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1583pub enum ApprovalAction {
1584 Pending,
1586 Approved,
1588 Rejected,
1590}
1591
1592#[derive(Debug, Clone)]
1598pub struct ToolApprovalState {
1599 pub tool_name: String,
1601 pub description: String,
1603 pub action: ApprovalAction,
1605}
1606
1607impl ToolApprovalState {
1608 pub fn new(tool_name: impl Into<String>, description: impl Into<String>) -> Self {
1610 Self {
1611 tool_name: tool_name.into(),
1612 description: description.into(),
1613 action: ApprovalAction::Pending,
1614 }
1615 }
1616
1617 pub fn reset(&mut self) {
1619 self.action = ApprovalAction::Pending;
1620 }
1621}
1622
1623#[derive(Debug, Clone)]
1625pub struct ContextItem {
1626 pub label: String,
1628 pub tokens: usize,
1630}
1631
1632impl ContextItem {
1633 pub fn new(label: impl Into<String>, tokens: usize) -> Self {
1635 Self {
1636 label: label.into(),
1637 tokens,
1638 }
1639 }
1640}
1641
1642#[cfg(test)]
1643mod tests {
1644 use super::*;
1645
1646 #[test]
1647 fn form_field_default_values() {
1648 let field = FormField::default();
1649 assert_eq!(field.label, "");
1650 assert_eq!(field.input.value, "");
1651 assert_eq!(field.input.cursor, 0);
1652 assert_eq!(field.error, None);
1653 }
1654
1655 #[test]
1656 fn toast_message_default_values() {
1657 let msg = ToastMessage::default();
1658 assert_eq!(msg.text, "");
1659 assert!(matches!(msg.level, ToastLevel::Info));
1660 assert_eq!(msg.created_tick, 0);
1661 assert_eq!(msg.duration_ticks, 30);
1662 }
1663
1664 #[test]
1665 fn list_state_default_values() {
1666 let state = ListState::default();
1667 assert!(state.items.is_empty());
1668 assert_eq!(state.selected, 0);
1669 assert_eq!(state.filter, "");
1670 assert_eq!(state.visible_indices(), &[]);
1671 assert_eq!(state.selected_item(), None);
1672 }
1673
1674 #[test]
1675 fn file_entry_default_values() {
1676 let entry = FileEntry::default();
1677 assert_eq!(entry.name, "");
1678 assert_eq!(entry.path, PathBuf::new());
1679 assert!(!entry.is_dir);
1680 assert_eq!(entry.size, 0);
1681 }
1682
1683 #[test]
1684 fn tabs_state_default_values() {
1685 let state = TabsState::default();
1686 assert!(state.labels.is_empty());
1687 assert_eq!(state.selected, 0);
1688 assert_eq!(state.selected_label(), None);
1689 }
1690
1691 #[test]
1692 fn table_state_default_values() {
1693 let state = TableState::default();
1694 assert!(state.headers.is_empty());
1695 assert!(state.rows.is_empty());
1696 assert_eq!(state.selected, 0);
1697 assert_eq!(state.sort_column, None);
1698 assert!(state.sort_ascending);
1699 assert_eq!(state.filter, "");
1700 assert_eq!(state.page, 0);
1701 assert_eq!(state.page_size, 0);
1702 assert_eq!(state.visible_indices(), &[]);
1703 }
1704
1705 #[test]
1706 fn select_state_default_values() {
1707 let state = SelectState::default();
1708 assert!(state.items.is_empty());
1709 assert_eq!(state.selected, 0);
1710 assert!(!state.open);
1711 assert_eq!(state.placeholder, "");
1712 assert_eq!(state.selected_item(), None);
1713 assert_eq!(state.cursor(), 0);
1714 }
1715
1716 #[test]
1717 fn radio_state_default_values() {
1718 let state = RadioState::default();
1719 assert!(state.items.is_empty());
1720 assert_eq!(state.selected, 0);
1721 assert_eq!(state.selected_item(), None);
1722 }
1723
1724 #[test]
1725 fn text_input_state_default_uses_new() {
1726 let state = TextInputState::default();
1727 assert_eq!(state.value, "");
1728 assert_eq!(state.cursor, 0);
1729 assert_eq!(state.placeholder, "");
1730 assert_eq!(state.max_length, None);
1731 assert_eq!(state.validation_error, None);
1732 assert!(!state.masked);
1733 }
1734
1735 #[test]
1736 fn tabs_state_new_sets_labels() {
1737 let state = TabsState::new(vec!["a", "b"]);
1738 assert_eq!(state.labels, vec!["a".to_string(), "b".to_string()]);
1739 assert_eq!(state.selected, 0);
1740 assert_eq!(state.selected_label(), Some("a"));
1741 }
1742
1743 #[test]
1744 fn list_state_new_selected_item_points_to_first_item() {
1745 let state = ListState::new(vec!["alpha", "beta"]);
1746 assert_eq!(state.items, vec!["alpha".to_string(), "beta".to_string()]);
1747 assert_eq!(state.selected, 0);
1748 assert_eq!(state.visible_indices(), &[0, 1]);
1749 assert_eq!(state.selected_item(), Some("alpha"));
1750 }
1751
1752 #[test]
1753 fn select_state_placeholder_builder_sets_value() {
1754 let state = SelectState::new(vec!["one", "two"]).placeholder("Pick one");
1755 assert_eq!(state.items, vec!["one".to_string(), "two".to_string()]);
1756 assert_eq!(state.placeholder, "Pick one");
1757 assert_eq!(state.selected_item(), Some("one"));
1758 }
1759
1760 #[test]
1761 fn radio_state_new_sets_items_and_selection() {
1762 let state = RadioState::new(vec!["red", "green"]);
1763 assert_eq!(state.items, vec!["red".to_string(), "green".to_string()]);
1764 assert_eq!(state.selected, 0);
1765 assert_eq!(state.selected_item(), Some("red"));
1766 }
1767
1768 #[test]
1769 fn table_state_new_sets_sort_ascending_true() {
1770 let state = TableState::new(vec!["Name"], vec![vec!["Alice"], vec!["Bob"]]);
1771 assert_eq!(state.headers, vec!["Name".to_string()]);
1772 assert_eq!(state.rows.len(), 2);
1773 assert!(state.sort_ascending);
1774 assert_eq!(state.sort_column, None);
1775 assert_eq!(state.visible_indices(), &[0, 1]);
1776 }
1777}