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
147pub struct FormField {
149 pub label: String,
151 pub input: TextInputState,
153 pub error: Option<String>,
155}
156
157impl FormField {
158 pub fn new(label: impl Into<String>) -> Self {
160 Self {
161 label: label.into(),
162 input: TextInputState::new(),
163 error: None,
164 }
165 }
166
167 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
169 self.input.placeholder = p.into();
170 self
171 }
172}
173
174pub struct FormState {
176 pub fields: Vec<FormField>,
178 pub submitted: bool,
180}
181
182impl FormState {
183 pub fn new() -> Self {
185 Self {
186 fields: Vec::new(),
187 submitted: false,
188 }
189 }
190
191 pub fn field(mut self, field: FormField) -> Self {
193 self.fields.push(field);
194 self
195 }
196
197 pub fn validate(&mut self, validators: &[FormValidator]) -> bool {
201 let mut all_valid = true;
202 for (i, field) in self.fields.iter_mut().enumerate() {
203 if let Some(validator) = validators.get(i) {
204 match validator(&field.input.value) {
205 Ok(()) => field.error = None,
206 Err(msg) => {
207 field.error = Some(msg);
208 all_valid = false;
209 }
210 }
211 }
212 }
213 all_valid
214 }
215
216 pub fn value(&self, index: usize) -> &str {
218 self.fields
219 .get(index)
220 .map(|f| f.input.value.as_str())
221 .unwrap_or("")
222 }
223}
224
225impl Default for FormState {
226 fn default() -> Self {
227 Self::new()
228 }
229}
230
231pub struct ToastState {
237 pub messages: Vec<ToastMessage>,
239}
240
241pub struct ToastMessage {
243 pub text: String,
245 pub level: ToastLevel,
247 pub created_tick: u64,
249 pub duration_ticks: u64,
251}
252
253pub enum ToastLevel {
255 Info,
257 Success,
259 Warning,
261 Error,
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum AlertLevel {
267 Info,
268 Success,
269 Warning,
270 Error,
271}
272
273impl ToastState {
274 pub fn new() -> Self {
276 Self {
277 messages: Vec::new(),
278 }
279 }
280
281 pub fn info(&mut self, text: impl Into<String>, tick: u64) {
283 self.push(text, ToastLevel::Info, tick, 30);
284 }
285
286 pub fn success(&mut self, text: impl Into<String>, tick: u64) {
288 self.push(text, ToastLevel::Success, tick, 30);
289 }
290
291 pub fn warning(&mut self, text: impl Into<String>, tick: u64) {
293 self.push(text, ToastLevel::Warning, tick, 50);
294 }
295
296 pub fn error(&mut self, text: impl Into<String>, tick: u64) {
298 self.push(text, ToastLevel::Error, tick, 80);
299 }
300
301 pub fn push(
303 &mut self,
304 text: impl Into<String>,
305 level: ToastLevel,
306 tick: u64,
307 duration_ticks: u64,
308 ) {
309 self.messages.push(ToastMessage {
310 text: text.into(),
311 level,
312 created_tick: tick,
313 duration_ticks,
314 });
315 }
316
317 pub fn cleanup(&mut self, current_tick: u64) {
321 self.messages.retain(|message| {
322 current_tick < message.created_tick.saturating_add(message.duration_ticks)
323 });
324 }
325}
326
327impl Default for ToastState {
328 fn default() -> Self {
329 Self::new()
330 }
331}
332
333pub struct TextareaState {
338 pub lines: Vec<String>,
340 pub cursor_row: usize,
342 pub cursor_col: usize,
344 pub max_length: Option<usize>,
346 pub wrap_width: Option<u32>,
348 pub scroll_offset: usize,
350}
351
352impl TextareaState {
353 pub fn new() -> Self {
355 Self {
356 lines: vec![String::new()],
357 cursor_row: 0,
358 cursor_col: 0,
359 max_length: None,
360 wrap_width: None,
361 scroll_offset: 0,
362 }
363 }
364
365 pub fn value(&self) -> String {
367 self.lines.join("\n")
368 }
369
370 pub fn set_value(&mut self, text: impl Into<String>) {
374 let value = text.into();
375 self.lines = value.split('\n').map(str::to_string).collect();
376 if self.lines.is_empty() {
377 self.lines.push(String::new());
378 }
379 self.cursor_row = 0;
380 self.cursor_col = 0;
381 self.scroll_offset = 0;
382 }
383
384 pub fn max_length(mut self, len: usize) -> Self {
386 self.max_length = Some(len);
387 self
388 }
389
390 pub fn word_wrap(mut self, width: u32) -> Self {
392 self.wrap_width = Some(width);
393 self
394 }
395}
396
397impl Default for TextareaState {
398 fn default() -> Self {
399 Self::new()
400 }
401}
402
403pub struct SpinnerState {
409 chars: Vec<char>,
410}
411
412impl SpinnerState {
413 pub fn dots() -> Self {
417 Self {
418 chars: vec!['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
419 }
420 }
421
422 pub fn line() -> Self {
426 Self {
427 chars: vec!['|', '/', '-', '\\'],
428 }
429 }
430
431 pub fn frame(&self, tick: u64) -> char {
433 if self.chars.is_empty() {
434 return ' ';
435 }
436 self.chars[tick as usize % self.chars.len()]
437 }
438}
439
440impl Default for SpinnerState {
441 fn default() -> Self {
442 Self::dots()
443 }
444}
445
446pub struct ListState {
451 pub items: Vec<String>,
453 pub selected: usize,
455 pub filter: String,
457 view_indices: Vec<usize>,
458}
459
460impl ListState {
461 pub fn new(items: Vec<impl Into<String>>) -> Self {
463 let len = items.len();
464 Self {
465 items: items.into_iter().map(Into::into).collect(),
466 selected: 0,
467 filter: String::new(),
468 view_indices: (0..len).collect(),
469 }
470 }
471
472 pub fn set_items(&mut self, items: Vec<impl Into<String>>) {
477 self.items = items.into_iter().map(Into::into).collect();
478 self.selected = self.selected.min(self.items.len().saturating_sub(1));
479 self.rebuild_view();
480 }
481
482 pub fn set_filter(&mut self, filter: impl Into<String>) {
486 self.filter = filter.into();
487 self.rebuild_view();
488 }
489
490 pub fn visible_indices(&self) -> &[usize] {
492 &self.view_indices
493 }
494
495 pub fn selected_item(&self) -> Option<&str> {
497 let data_idx = *self.view_indices.get(self.selected)?;
498 self.items.get(data_idx).map(String::as_str)
499 }
500
501 fn rebuild_view(&mut self) {
502 let tokens: Vec<String> = self
503 .filter
504 .split_whitespace()
505 .map(|t| t.to_lowercase())
506 .collect();
507 self.view_indices = if tokens.is_empty() {
508 (0..self.items.len()).collect()
509 } else {
510 (0..self.items.len())
511 .filter(|&i| {
512 tokens
513 .iter()
514 .all(|token| self.items[i].to_lowercase().contains(token.as_str()))
515 })
516 .collect()
517 };
518 if !self.view_indices.is_empty() && self.selected >= self.view_indices.len() {
519 self.selected = self.view_indices.len() - 1;
520 }
521 }
522}
523
524#[derive(Debug, Clone)]
525pub struct FilePickerState {
526 pub current_dir: PathBuf,
527 pub entries: Vec<FileEntry>,
528 pub selected: usize,
529 pub selected_file: Option<PathBuf>,
530 pub show_hidden: bool,
531 pub extensions: Vec<String>,
532 pub dirty: bool,
533}
534
535#[derive(Debug, Clone)]
536pub struct FileEntry {
537 pub name: String,
538 pub path: PathBuf,
539 pub is_dir: bool,
540 pub size: u64,
541}
542
543impl FilePickerState {
544 pub fn new(dir: impl Into<PathBuf>) -> Self {
545 Self {
546 current_dir: dir.into(),
547 entries: Vec::new(),
548 selected: 0,
549 selected_file: None,
550 show_hidden: false,
551 extensions: Vec::new(),
552 dirty: true,
553 }
554 }
555
556 pub fn show_hidden(mut self, show: bool) -> Self {
557 self.show_hidden = show;
558 self.dirty = true;
559 self
560 }
561
562 pub fn extensions(mut self, exts: &[&str]) -> Self {
563 self.extensions = exts
564 .iter()
565 .map(|ext| ext.trim().trim_start_matches('.').to_ascii_lowercase())
566 .filter(|ext| !ext.is_empty())
567 .collect();
568 self.dirty = true;
569 self
570 }
571
572 pub fn selected(&self) -> Option<&PathBuf> {
573 self.selected_file.as_ref()
574 }
575
576 pub fn refresh(&mut self) {
577 let mut entries = Vec::new();
578
579 if let Ok(read_dir) = fs::read_dir(&self.current_dir) {
580 for dir_entry in read_dir.flatten() {
581 let name = dir_entry.file_name().to_string_lossy().to_string();
582 if !self.show_hidden && name.starts_with('.') {
583 continue;
584 }
585
586 let Ok(file_type) = dir_entry.file_type() else {
587 continue;
588 };
589 if file_type.is_symlink() {
590 continue;
591 }
592
593 let path = dir_entry.path();
594 let is_dir = file_type.is_dir();
595
596 if !is_dir && !self.extensions.is_empty() {
597 let ext = path
598 .extension()
599 .and_then(|e| e.to_str())
600 .map(|e| e.to_ascii_lowercase());
601 let Some(ext) = ext else {
602 continue;
603 };
604 if !self.extensions.iter().any(|allowed| allowed == &ext) {
605 continue;
606 }
607 }
608
609 let size = if is_dir {
610 0
611 } else {
612 fs::symlink_metadata(&path).map(|m| m.len()).unwrap_or(0)
613 };
614
615 entries.push(FileEntry {
616 name,
617 path,
618 is_dir,
619 size,
620 });
621 }
622 }
623
624 entries.sort_by(|a, b| match (a.is_dir, b.is_dir) {
625 (true, false) => std::cmp::Ordering::Less,
626 (false, true) => std::cmp::Ordering::Greater,
627 _ => a
628 .name
629 .to_ascii_lowercase()
630 .cmp(&b.name.to_ascii_lowercase())
631 .then_with(|| a.name.cmp(&b.name)),
632 });
633
634 self.entries = entries;
635 if self.entries.is_empty() {
636 self.selected = 0;
637 } else {
638 self.selected = self.selected.min(self.entries.len().saturating_sub(1));
639 }
640 self.dirty = false;
641 }
642}
643
644impl Default for FilePickerState {
645 fn default() -> Self {
646 Self::new(".")
647 }
648}
649
650pub struct TabsState {
655 pub labels: Vec<String>,
657 pub selected: usize,
659}
660
661impl TabsState {
662 pub fn new(labels: Vec<impl Into<String>>) -> Self {
664 Self {
665 labels: labels.into_iter().map(Into::into).collect(),
666 selected: 0,
667 }
668 }
669
670 pub fn selected_label(&self) -> Option<&str> {
672 self.labels.get(self.selected).map(String::as_str)
673 }
674}
675
676pub struct TableState {
682 pub headers: Vec<String>,
684 pub rows: Vec<Vec<String>>,
686 pub selected: usize,
688 column_widths: Vec<u32>,
689 dirty: bool,
690 pub sort_column: Option<usize>,
692 pub sort_ascending: bool,
694 pub filter: String,
696 pub page: usize,
698 pub page_size: usize,
700 view_indices: Vec<usize>,
701}
702
703impl TableState {
704 pub fn new(headers: Vec<impl Into<String>>, rows: Vec<Vec<impl Into<String>>>) -> Self {
706 let headers: Vec<String> = headers.into_iter().map(Into::into).collect();
707 let rows: Vec<Vec<String>> = rows
708 .into_iter()
709 .map(|r| r.into_iter().map(Into::into).collect())
710 .collect();
711 let mut state = Self {
712 headers,
713 rows,
714 selected: 0,
715 column_widths: Vec::new(),
716 dirty: true,
717 sort_column: None,
718 sort_ascending: true,
719 filter: String::new(),
720 page: 0,
721 page_size: 0,
722 view_indices: Vec::new(),
723 };
724 state.rebuild_view();
725 state.recompute_widths();
726 state
727 }
728
729 pub fn set_rows(&mut self, rows: Vec<Vec<impl Into<String>>>) {
734 self.rows = rows
735 .into_iter()
736 .map(|r| r.into_iter().map(Into::into).collect())
737 .collect();
738 self.rebuild_view();
739 }
740
741 pub fn toggle_sort(&mut self, column: usize) {
743 if self.sort_column == Some(column) {
744 self.sort_ascending = !self.sort_ascending;
745 } else {
746 self.sort_column = Some(column);
747 self.sort_ascending = true;
748 }
749 self.rebuild_view();
750 }
751
752 pub fn sort_by(&mut self, column: usize) {
754 self.sort_column = Some(column);
755 self.sort_ascending = true;
756 self.rebuild_view();
757 }
758
759 pub fn set_filter(&mut self, filter: impl Into<String>) {
763 self.filter = filter.into();
764 self.page = 0;
765 self.rebuild_view();
766 }
767
768 pub fn clear_sort(&mut self) {
770 self.sort_column = None;
771 self.sort_ascending = true;
772 self.rebuild_view();
773 }
774
775 pub fn next_page(&mut self) {
777 if self.page_size == 0 {
778 return;
779 }
780 let last_page = self.total_pages().saturating_sub(1);
781 self.page = (self.page + 1).min(last_page);
782 }
783
784 pub fn prev_page(&mut self) {
786 self.page = self.page.saturating_sub(1);
787 }
788
789 pub fn total_pages(&self) -> usize {
791 if self.page_size == 0 {
792 return 1;
793 }
794
795 let len = self.view_indices.len();
796 if len == 0 {
797 1
798 } else {
799 len.div_ceil(self.page_size)
800 }
801 }
802
803 pub fn visible_indices(&self) -> &[usize] {
805 &self.view_indices
806 }
807
808 pub fn selected_row(&self) -> Option<&[String]> {
810 if self.view_indices.is_empty() {
811 return None;
812 }
813 let data_idx = self.view_indices.get(self.selected)?;
814 self.rows.get(*data_idx).map(|r| r.as_slice())
815 }
816
817 fn rebuild_view(&mut self) {
819 let mut indices: Vec<usize> = (0..self.rows.len()).collect();
820
821 let tokens: Vec<String> = self
822 .filter
823 .split_whitespace()
824 .map(|t| t.to_lowercase())
825 .collect();
826 if !tokens.is_empty() {
827 indices.retain(|&idx| {
828 let row = match self.rows.get(idx) {
829 Some(r) => r,
830 None => return false,
831 };
832 tokens.iter().all(|token| {
833 row.iter()
834 .any(|cell| cell.to_lowercase().contains(token.as_str()))
835 })
836 });
837 }
838
839 if let Some(column) = self.sort_column {
840 indices.sort_by(|a, b| {
841 let left = self
842 .rows
843 .get(*a)
844 .and_then(|row| row.get(column))
845 .map(String::as_str)
846 .unwrap_or("");
847 let right = self
848 .rows
849 .get(*b)
850 .and_then(|row| row.get(column))
851 .map(String::as_str)
852 .unwrap_or("");
853
854 match (left.parse::<f64>(), right.parse::<f64>()) {
855 (Ok(l), Ok(r)) => l.partial_cmp(&r).unwrap_or(std::cmp::Ordering::Equal),
856 _ => left.to_lowercase().cmp(&right.to_lowercase()),
857 }
858 });
859
860 if !self.sort_ascending {
861 indices.reverse();
862 }
863 }
864
865 self.view_indices = indices;
866
867 if self.page_size > 0 {
868 self.page = self.page.min(self.total_pages().saturating_sub(1));
869 } else {
870 self.page = 0;
871 }
872
873 self.selected = self.selected.min(self.view_indices.len().saturating_sub(1));
874 self.dirty = true;
875 }
876
877 pub(crate) fn recompute_widths(&mut self) {
878 let col_count = self.headers.len();
879 self.column_widths = vec![0u32; col_count];
880 for (i, header) in self.headers.iter().enumerate() {
881 let mut width = UnicodeWidthStr::width(header.as_str()) as u32;
882 if self.sort_column == Some(i) {
883 width += 2;
884 }
885 self.column_widths[i] = width;
886 }
887 for row in &self.rows {
888 for (i, cell) in row.iter().enumerate() {
889 if i < col_count {
890 let w = UnicodeWidthStr::width(cell.as_str()) as u32;
891 self.column_widths[i] = self.column_widths[i].max(w);
892 }
893 }
894 }
895 self.dirty = false;
896 }
897
898 pub(crate) fn column_widths(&self) -> &[u32] {
899 &self.column_widths
900 }
901
902 pub(crate) fn is_dirty(&self) -> bool {
903 self.dirty
904 }
905}
906
907pub struct ScrollState {
913 pub offset: usize,
915 content_height: u32,
916 viewport_height: u32,
917}
918
919impl ScrollState {
920 pub fn new() -> Self {
922 Self {
923 offset: 0,
924 content_height: 0,
925 viewport_height: 0,
926 }
927 }
928
929 pub fn can_scroll_up(&self) -> bool {
931 self.offset > 0
932 }
933
934 pub fn can_scroll_down(&self) -> bool {
936 (self.offset as u32) + self.viewport_height < self.content_height
937 }
938
939 pub fn content_height(&self) -> u32 {
941 self.content_height
942 }
943
944 pub fn viewport_height(&self) -> u32 {
946 self.viewport_height
947 }
948
949 pub fn progress(&self) -> f32 {
951 let max = self.content_height.saturating_sub(self.viewport_height);
952 if max == 0 {
953 0.0
954 } else {
955 self.offset as f32 / max as f32
956 }
957 }
958
959 pub fn scroll_up(&mut self, amount: usize) {
961 self.offset = self.offset.saturating_sub(amount);
962 }
963
964 pub fn scroll_down(&mut self, amount: usize) {
966 let max_offset = self.content_height.saturating_sub(self.viewport_height) as usize;
967 self.offset = (self.offset + amount).min(max_offset);
968 }
969
970 pub(crate) fn set_bounds(&mut self, content_height: u32, viewport_height: u32) {
971 self.content_height = content_height;
972 self.viewport_height = viewport_height;
973 }
974}
975
976impl Default for ScrollState {
977 fn default() -> Self {
978 Self::new()
979 }
980}
981
982#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
992pub enum ButtonVariant {
993 #[default]
995 Default,
996 Primary,
998 Danger,
1000 Outline,
1002}
1003
1004#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1005pub enum Trend {
1006 Up,
1007 Down,
1008}
1009
1010pub struct SelectState {
1017 pub items: Vec<String>,
1018 pub selected: usize,
1019 pub open: bool,
1020 pub placeholder: String,
1021 cursor: usize,
1022}
1023
1024impl SelectState {
1025 pub fn new(items: Vec<impl Into<String>>) -> Self {
1026 Self {
1027 items: items.into_iter().map(Into::into).collect(),
1028 selected: 0,
1029 open: false,
1030 placeholder: String::new(),
1031 cursor: 0,
1032 }
1033 }
1034
1035 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
1036 self.placeholder = p.into();
1037 self
1038 }
1039
1040 pub fn selected_item(&self) -> Option<&str> {
1041 self.items.get(self.selected).map(String::as_str)
1042 }
1043
1044 pub(crate) fn cursor(&self) -> usize {
1045 self.cursor
1046 }
1047
1048 pub(crate) fn set_cursor(&mut self, c: usize) {
1049 self.cursor = c;
1050 }
1051}
1052
1053pub struct RadioState {
1059 pub items: Vec<String>,
1060 pub selected: usize,
1061}
1062
1063impl RadioState {
1064 pub fn new(items: Vec<impl Into<String>>) -> Self {
1065 Self {
1066 items: items.into_iter().map(Into::into).collect(),
1067 selected: 0,
1068 }
1069 }
1070
1071 pub fn selected_item(&self) -> Option<&str> {
1072 self.items.get(self.selected).map(String::as_str)
1073 }
1074}
1075
1076pub struct MultiSelectState {
1082 pub items: Vec<String>,
1083 pub cursor: usize,
1084 pub selected: HashSet<usize>,
1085}
1086
1087impl MultiSelectState {
1088 pub fn new(items: Vec<impl Into<String>>) -> Self {
1089 Self {
1090 items: items.into_iter().map(Into::into).collect(),
1091 cursor: 0,
1092 selected: HashSet::new(),
1093 }
1094 }
1095
1096 pub fn selected_items(&self) -> Vec<&str> {
1097 let mut indices: Vec<usize> = self.selected.iter().copied().collect();
1098 indices.sort();
1099 indices
1100 .iter()
1101 .filter_map(|&i| self.items.get(i).map(String::as_str))
1102 .collect()
1103 }
1104
1105 pub fn toggle(&mut self, index: usize) {
1106 if self.selected.contains(&index) {
1107 self.selected.remove(&index);
1108 } else {
1109 self.selected.insert(index);
1110 }
1111 }
1112}
1113
1114pub struct TreeNode {
1118 pub label: String,
1119 pub children: Vec<TreeNode>,
1120 pub expanded: bool,
1121}
1122
1123impl TreeNode {
1124 pub fn new(label: impl Into<String>) -> Self {
1125 Self {
1126 label: label.into(),
1127 children: Vec::new(),
1128 expanded: false,
1129 }
1130 }
1131
1132 pub fn expanded(mut self) -> Self {
1133 self.expanded = true;
1134 self
1135 }
1136
1137 pub fn children(mut self, children: Vec<TreeNode>) -> Self {
1138 self.children = children;
1139 self
1140 }
1141
1142 pub fn is_leaf(&self) -> bool {
1143 self.children.is_empty()
1144 }
1145
1146 fn flatten(&self, depth: usize, out: &mut Vec<FlatTreeEntry>) {
1147 out.push(FlatTreeEntry {
1148 depth,
1149 label: self.label.clone(),
1150 is_leaf: self.is_leaf(),
1151 expanded: self.expanded,
1152 });
1153 if self.expanded {
1154 for child in &self.children {
1155 child.flatten(depth + 1, out);
1156 }
1157 }
1158 }
1159}
1160
1161pub(crate) struct FlatTreeEntry {
1162 pub depth: usize,
1163 pub label: String,
1164 pub is_leaf: bool,
1165 pub expanded: bool,
1166}
1167
1168pub struct TreeState {
1170 pub nodes: Vec<TreeNode>,
1171 pub selected: usize,
1172}
1173
1174impl TreeState {
1175 pub fn new(nodes: Vec<TreeNode>) -> Self {
1176 Self { nodes, selected: 0 }
1177 }
1178
1179 pub(crate) fn flatten(&self) -> Vec<FlatTreeEntry> {
1180 let mut entries = Vec::new();
1181 for node in &self.nodes {
1182 node.flatten(0, &mut entries);
1183 }
1184 entries
1185 }
1186
1187 pub(crate) fn toggle_at(&mut self, flat_index: usize) {
1188 let mut counter = 0usize;
1189 Self::toggle_recursive(&mut self.nodes, flat_index, &mut counter);
1190 }
1191
1192 fn toggle_recursive(nodes: &mut [TreeNode], target: usize, counter: &mut usize) -> bool {
1193 for node in nodes.iter_mut() {
1194 if *counter == target {
1195 if !node.is_leaf() {
1196 node.expanded = !node.expanded;
1197 }
1198 return true;
1199 }
1200 *counter += 1;
1201 if node.expanded && Self::toggle_recursive(&mut node.children, target, counter) {
1202 return true;
1203 }
1204 }
1205 false
1206 }
1207}
1208
1209pub struct PaletteCommand {
1213 pub label: String,
1214 pub description: String,
1215 pub shortcut: Option<String>,
1216}
1217
1218impl PaletteCommand {
1219 pub fn new(label: impl Into<String>, description: impl Into<String>) -> Self {
1220 Self {
1221 label: label.into(),
1222 description: description.into(),
1223 shortcut: None,
1224 }
1225 }
1226
1227 pub fn shortcut(mut self, s: impl Into<String>) -> Self {
1228 self.shortcut = Some(s.into());
1229 self
1230 }
1231}
1232
1233pub struct CommandPaletteState {
1237 pub commands: Vec<PaletteCommand>,
1238 pub input: String,
1239 pub cursor: usize,
1240 pub open: bool,
1241 selected: usize,
1242}
1243
1244impl CommandPaletteState {
1245 pub fn new(commands: Vec<PaletteCommand>) -> Self {
1246 Self {
1247 commands,
1248 input: String::new(),
1249 cursor: 0,
1250 open: false,
1251 selected: 0,
1252 }
1253 }
1254
1255 pub fn toggle(&mut self) {
1256 self.open = !self.open;
1257 if self.open {
1258 self.input.clear();
1259 self.cursor = 0;
1260 self.selected = 0;
1261 }
1262 }
1263
1264 pub(crate) fn filtered_indices(&self) -> Vec<usize> {
1265 let tokens: Vec<String> = self
1266 .input
1267 .split_whitespace()
1268 .map(|t| t.to_lowercase())
1269 .collect();
1270 if tokens.is_empty() {
1271 return (0..self.commands.len()).collect();
1272 }
1273 self.commands
1274 .iter()
1275 .enumerate()
1276 .filter(|(_, cmd)| {
1277 let label = cmd.label.to_lowercase();
1278 let desc = cmd.description.to_lowercase();
1279 tokens
1280 .iter()
1281 .all(|token| label.contains(token.as_str()) || desc.contains(token.as_str()))
1282 })
1283 .map(|(i, _)| i)
1284 .collect()
1285 }
1286
1287 pub(crate) fn selected(&self) -> usize {
1288 self.selected
1289 }
1290
1291 pub(crate) fn set_selected(&mut self, s: usize) {
1292 self.selected = s;
1293 }
1294}
1295
1296pub struct StreamingTextState {
1301 pub content: String,
1303 pub streaming: bool,
1305 pub(crate) cursor_visible: bool,
1307 pub(crate) cursor_tick: u64,
1308}
1309
1310impl StreamingTextState {
1311 pub fn new() -> Self {
1313 Self {
1314 content: String::new(),
1315 streaming: false,
1316 cursor_visible: true,
1317 cursor_tick: 0,
1318 }
1319 }
1320
1321 pub fn push(&mut self, chunk: &str) {
1323 self.content.push_str(chunk);
1324 }
1325
1326 pub fn finish(&mut self) {
1328 self.streaming = false;
1329 }
1330
1331 pub fn start(&mut self) {
1333 self.content.clear();
1334 self.streaming = true;
1335 self.cursor_visible = true;
1336 self.cursor_tick = 0;
1337 }
1338
1339 pub fn clear(&mut self) {
1341 self.content.clear();
1342 self.streaming = false;
1343 self.cursor_visible = true;
1344 self.cursor_tick = 0;
1345 }
1346}
1347
1348impl Default for StreamingTextState {
1349 fn default() -> Self {
1350 Self::new()
1351 }
1352}
1353
1354pub struct StreamingMarkdownState {
1359 pub content: String,
1361 pub streaming: bool,
1363 pub cursor_visible: bool,
1365 pub cursor_tick: u64,
1366 pub in_code_block: bool,
1367 pub code_block_lang: String,
1368}
1369
1370impl StreamingMarkdownState {
1371 pub fn new() -> Self {
1373 Self {
1374 content: String::new(),
1375 streaming: false,
1376 cursor_visible: true,
1377 cursor_tick: 0,
1378 in_code_block: false,
1379 code_block_lang: String::new(),
1380 }
1381 }
1382
1383 pub fn push(&mut self, chunk: &str) {
1385 self.content.push_str(chunk);
1386 }
1387
1388 pub fn start(&mut self) {
1390 self.content.clear();
1391 self.streaming = true;
1392 self.cursor_visible = true;
1393 self.cursor_tick = 0;
1394 self.in_code_block = false;
1395 self.code_block_lang.clear();
1396 }
1397
1398 pub fn finish(&mut self) {
1400 self.streaming = false;
1401 }
1402
1403 pub fn clear(&mut self) {
1405 self.content.clear();
1406 self.streaming = false;
1407 self.cursor_visible = true;
1408 self.cursor_tick = 0;
1409 self.in_code_block = false;
1410 self.code_block_lang.clear();
1411 }
1412}
1413
1414impl Default for StreamingMarkdownState {
1415 fn default() -> Self {
1416 Self::new()
1417 }
1418}
1419
1420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1422pub enum ApprovalAction {
1423 Pending,
1425 Approved,
1427 Rejected,
1429}
1430
1431pub struct ToolApprovalState {
1437 pub tool_name: String,
1439 pub description: String,
1441 pub action: ApprovalAction,
1443}
1444
1445impl ToolApprovalState {
1446 pub fn new(tool_name: impl Into<String>, description: impl Into<String>) -> Self {
1448 Self {
1449 tool_name: tool_name.into(),
1450 description: description.into(),
1451 action: ApprovalAction::Pending,
1452 }
1453 }
1454
1455 pub fn reset(&mut self) {
1457 self.action = ApprovalAction::Pending;
1458 }
1459}
1460
1461#[derive(Debug, Clone)]
1463pub struct ContextItem {
1464 pub label: String,
1466 pub tokens: usize,
1468}
1469
1470impl ContextItem {
1471 pub fn new(label: impl Into<String>, tokens: usize) -> Self {
1473 Self {
1474 label: label.into(),
1475 tokens,
1476 }
1477 }
1478}