1use std::collections::HashSet;
8use unicode_width::UnicodeWidthStr;
9
10type FormValidator = fn(&str) -> Result<(), String>;
11
12pub struct TextInputState {
28 pub value: String,
30 pub cursor: usize,
32 pub placeholder: String,
34 pub max_length: Option<usize>,
36 pub validation_error: Option<String>,
38 pub masked: bool,
40}
41
42impl TextInputState {
43 pub fn new() -> Self {
45 Self {
46 value: String::new(),
47 cursor: 0,
48 placeholder: String::new(),
49 max_length: None,
50 validation_error: None,
51 masked: false,
52 }
53 }
54
55 pub fn with_placeholder(p: impl Into<String>) -> Self {
57 Self {
58 placeholder: p.into(),
59 ..Self::new()
60 }
61 }
62
63 pub fn max_length(mut self, len: usize) -> Self {
65 self.max_length = Some(len);
66 self
67 }
68
69 pub fn validate(&mut self, validator: impl Fn(&str) -> Result<(), String>) {
74 self.validation_error = validator(&self.value).err();
75 }
76}
77
78impl Default for TextInputState {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84pub struct FormField {
86 pub label: String,
88 pub input: TextInputState,
90 pub error: Option<String>,
92}
93
94impl FormField {
95 pub fn new(label: impl Into<String>) -> Self {
97 Self {
98 label: label.into(),
99 input: TextInputState::new(),
100 error: None,
101 }
102 }
103
104 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
106 self.input.placeholder = p.into();
107 self
108 }
109}
110
111pub struct FormState {
113 pub fields: Vec<FormField>,
115 pub submitted: bool,
117}
118
119impl FormState {
120 pub fn new() -> Self {
122 Self {
123 fields: Vec::new(),
124 submitted: false,
125 }
126 }
127
128 pub fn field(mut self, field: FormField) -> Self {
130 self.fields.push(field);
131 self
132 }
133
134 pub fn validate(&mut self, validators: &[FormValidator]) -> bool {
138 let mut all_valid = true;
139 for (i, field) in self.fields.iter_mut().enumerate() {
140 if let Some(validator) = validators.get(i) {
141 match validator(&field.input.value) {
142 Ok(()) => field.error = None,
143 Err(msg) => {
144 field.error = Some(msg);
145 all_valid = false;
146 }
147 }
148 }
149 }
150 all_valid
151 }
152
153 pub fn value(&self, index: usize) -> &str {
155 self.fields
156 .get(index)
157 .map(|f| f.input.value.as_str())
158 .unwrap_or("")
159 }
160}
161
162impl Default for FormState {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168pub struct ToastState {
174 pub messages: Vec<ToastMessage>,
176}
177
178pub struct ToastMessage {
180 pub text: String,
182 pub level: ToastLevel,
184 pub created_tick: u64,
186 pub duration_ticks: u64,
188}
189
190pub enum ToastLevel {
192 Info,
194 Success,
196 Warning,
198 Error,
200}
201
202impl ToastState {
203 pub fn new() -> Self {
205 Self {
206 messages: Vec::new(),
207 }
208 }
209
210 pub fn info(&mut self, text: impl Into<String>, tick: u64) {
212 self.push(text, ToastLevel::Info, tick, 30);
213 }
214
215 pub fn success(&mut self, text: impl Into<String>, tick: u64) {
217 self.push(text, ToastLevel::Success, tick, 30);
218 }
219
220 pub fn warning(&mut self, text: impl Into<String>, tick: u64) {
222 self.push(text, ToastLevel::Warning, tick, 50);
223 }
224
225 pub fn error(&mut self, text: impl Into<String>, tick: u64) {
227 self.push(text, ToastLevel::Error, tick, 80);
228 }
229
230 pub fn push(
232 &mut self,
233 text: impl Into<String>,
234 level: ToastLevel,
235 tick: u64,
236 duration_ticks: u64,
237 ) {
238 self.messages.push(ToastMessage {
239 text: text.into(),
240 level,
241 created_tick: tick,
242 duration_ticks,
243 });
244 }
245
246 pub fn cleanup(&mut self, current_tick: u64) {
250 self.messages.retain(|message| {
251 current_tick < message.created_tick.saturating_add(message.duration_ticks)
252 });
253 }
254}
255
256impl Default for ToastState {
257 fn default() -> Self {
258 Self::new()
259 }
260}
261
262pub struct TextareaState {
267 pub lines: Vec<String>,
269 pub cursor_row: usize,
271 pub cursor_col: usize,
273 pub max_length: Option<usize>,
275 pub wrap_width: Option<u32>,
277 pub scroll_offset: usize,
279}
280
281impl TextareaState {
282 pub fn new() -> Self {
284 Self {
285 lines: vec![String::new()],
286 cursor_row: 0,
287 cursor_col: 0,
288 max_length: None,
289 wrap_width: None,
290 scroll_offset: 0,
291 }
292 }
293
294 pub fn value(&self) -> String {
296 self.lines.join("\n")
297 }
298
299 pub fn set_value(&mut self, text: impl Into<String>) {
303 let value = text.into();
304 self.lines = value.split('\n').map(str::to_string).collect();
305 if self.lines.is_empty() {
306 self.lines.push(String::new());
307 }
308 self.cursor_row = 0;
309 self.cursor_col = 0;
310 self.scroll_offset = 0;
311 }
312
313 pub fn max_length(mut self, len: usize) -> Self {
315 self.max_length = Some(len);
316 self
317 }
318
319 pub fn word_wrap(mut self, width: u32) -> Self {
321 self.wrap_width = Some(width);
322 self
323 }
324}
325
326impl Default for TextareaState {
327 fn default() -> Self {
328 Self::new()
329 }
330}
331
332pub struct SpinnerState {
338 chars: Vec<char>,
339}
340
341impl SpinnerState {
342 pub fn dots() -> Self {
346 Self {
347 chars: vec!['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
348 }
349 }
350
351 pub fn line() -> Self {
355 Self {
356 chars: vec!['|', '/', '-', '\\'],
357 }
358 }
359
360 pub fn frame(&self, tick: u64) -> char {
362 if self.chars.is_empty() {
363 return ' ';
364 }
365 self.chars[tick as usize % self.chars.len()]
366 }
367}
368
369impl Default for SpinnerState {
370 fn default() -> Self {
371 Self::dots()
372 }
373}
374
375pub struct ListState {
380 pub items: Vec<String>,
382 pub selected: usize,
384 pub filter: String,
386 view_indices: Vec<usize>,
387}
388
389impl ListState {
390 pub fn new(items: Vec<impl Into<String>>) -> Self {
392 let len = items.len();
393 Self {
394 items: items.into_iter().map(Into::into).collect(),
395 selected: 0,
396 filter: String::new(),
397 view_indices: (0..len).collect(),
398 }
399 }
400
401 pub fn set_filter(&mut self, filter: impl Into<String>) {
405 self.filter = filter.into();
406 self.rebuild_view();
407 }
408
409 pub fn visible_indices(&self) -> &[usize] {
411 &self.view_indices
412 }
413
414 pub fn selected_item(&self) -> Option<&str> {
416 let data_idx = *self.view_indices.get(self.selected)?;
417 self.items.get(data_idx).map(String::as_str)
418 }
419
420 fn rebuild_view(&mut self) {
421 let tokens: Vec<String> = self
422 .filter
423 .split_whitespace()
424 .map(|t| t.to_lowercase())
425 .collect();
426 self.view_indices = if tokens.is_empty() {
427 (0..self.items.len()).collect()
428 } else {
429 (0..self.items.len())
430 .filter(|&i| {
431 tokens
432 .iter()
433 .all(|token| self.items[i].to_lowercase().contains(token.as_str()))
434 })
435 .collect()
436 };
437 if !self.view_indices.is_empty() && self.selected >= self.view_indices.len() {
438 self.selected = self.view_indices.len() - 1;
439 }
440 }
441}
442
443pub struct TabsState {
448 pub labels: Vec<String>,
450 pub selected: usize,
452}
453
454impl TabsState {
455 pub fn new(labels: Vec<impl Into<String>>) -> Self {
457 Self {
458 labels: labels.into_iter().map(Into::into).collect(),
459 selected: 0,
460 }
461 }
462
463 pub fn selected_label(&self) -> Option<&str> {
465 self.labels.get(self.selected).map(String::as_str)
466 }
467}
468
469pub struct TableState {
475 pub headers: Vec<String>,
477 pub rows: Vec<Vec<String>>,
479 pub selected: usize,
481 column_widths: Vec<u32>,
482 dirty: bool,
483 pub sort_column: Option<usize>,
485 pub sort_ascending: bool,
487 pub filter: String,
489 pub page: usize,
491 pub page_size: usize,
493 view_indices: Vec<usize>,
494}
495
496impl TableState {
497 pub fn new(headers: Vec<impl Into<String>>, rows: Vec<Vec<impl Into<String>>>) -> Self {
499 let headers: Vec<String> = headers.into_iter().map(Into::into).collect();
500 let rows: Vec<Vec<String>> = rows
501 .into_iter()
502 .map(|r| r.into_iter().map(Into::into).collect())
503 .collect();
504 let mut state = Self {
505 headers,
506 rows,
507 selected: 0,
508 column_widths: Vec::new(),
509 dirty: true,
510 sort_column: None,
511 sort_ascending: true,
512 filter: String::new(),
513 page: 0,
514 page_size: 0,
515 view_indices: Vec::new(),
516 };
517 state.rebuild_view();
518 state.recompute_widths();
519 state
520 }
521
522 pub fn set_rows(&mut self, rows: Vec<Vec<impl Into<String>>>) {
527 self.rows = rows
528 .into_iter()
529 .map(|r| r.into_iter().map(Into::into).collect())
530 .collect();
531 self.rebuild_view();
532 }
533
534 pub fn toggle_sort(&mut self, column: usize) {
536 if self.sort_column == Some(column) {
537 self.sort_ascending = !self.sort_ascending;
538 } else {
539 self.sort_column = Some(column);
540 self.sort_ascending = true;
541 }
542 self.rebuild_view();
543 }
544
545 pub fn sort_by(&mut self, column: usize) {
547 self.sort_column = Some(column);
548 self.sort_ascending = true;
549 self.rebuild_view();
550 }
551
552 pub fn set_filter(&mut self, filter: impl Into<String>) {
556 self.filter = filter.into();
557 self.page = 0;
558 self.rebuild_view();
559 }
560
561 pub fn clear_sort(&mut self) {
563 self.sort_column = None;
564 self.sort_ascending = true;
565 self.rebuild_view();
566 }
567
568 pub fn next_page(&mut self) {
570 if self.page_size == 0 {
571 return;
572 }
573 let last_page = self.total_pages().saturating_sub(1);
574 self.page = (self.page + 1).min(last_page);
575 }
576
577 pub fn prev_page(&mut self) {
579 self.page = self.page.saturating_sub(1);
580 }
581
582 pub fn total_pages(&self) -> usize {
584 if self.page_size == 0 {
585 return 1;
586 }
587
588 let len = self.view_indices.len();
589 if len == 0 {
590 1
591 } else {
592 len.div_ceil(self.page_size)
593 }
594 }
595
596 pub fn visible_indices(&self) -> &[usize] {
598 &self.view_indices
599 }
600
601 pub fn selected_row(&self) -> Option<&[String]> {
603 if self.view_indices.is_empty() {
604 return None;
605 }
606 let data_idx = self.view_indices.get(self.selected)?;
607 self.rows.get(*data_idx).map(|r| r.as_slice())
608 }
609
610 fn rebuild_view(&mut self) {
612 let mut indices: Vec<usize> = (0..self.rows.len()).collect();
613
614 let tokens: Vec<String> = self
615 .filter
616 .split_whitespace()
617 .map(|t| t.to_lowercase())
618 .collect();
619 if !tokens.is_empty() {
620 indices.retain(|&idx| {
621 let row = match self.rows.get(idx) {
622 Some(r) => r,
623 None => return false,
624 };
625 tokens.iter().all(|token| {
626 row.iter()
627 .any(|cell| cell.to_lowercase().contains(token.as_str()))
628 })
629 });
630 }
631
632 if let Some(column) = self.sort_column {
633 indices.sort_by(|a, b| {
634 let left = self
635 .rows
636 .get(*a)
637 .and_then(|row| row.get(column))
638 .map(String::as_str)
639 .unwrap_or("");
640 let right = self
641 .rows
642 .get(*b)
643 .and_then(|row| row.get(column))
644 .map(String::as_str)
645 .unwrap_or("");
646
647 match (left.parse::<f64>(), right.parse::<f64>()) {
648 (Ok(l), Ok(r)) => l.partial_cmp(&r).unwrap_or(std::cmp::Ordering::Equal),
649 _ => left.to_lowercase().cmp(&right.to_lowercase()),
650 }
651 });
652
653 if !self.sort_ascending {
654 indices.reverse();
655 }
656 }
657
658 self.view_indices = indices;
659
660 if self.page_size > 0 {
661 self.page = self.page.min(self.total_pages().saturating_sub(1));
662 } else {
663 self.page = 0;
664 }
665
666 self.selected = self.selected.min(self.view_indices.len().saturating_sub(1));
667 self.dirty = true;
668 }
669
670 pub(crate) fn recompute_widths(&mut self) {
671 let col_count = self.headers.len();
672 self.column_widths = vec![0u32; col_count];
673 for (i, header) in self.headers.iter().enumerate() {
674 let mut width = UnicodeWidthStr::width(header.as_str()) as u32;
675 if self.sort_column == Some(i) {
676 width += 2;
677 }
678 self.column_widths[i] = width;
679 }
680 for row in &self.rows {
681 for (i, cell) in row.iter().enumerate() {
682 if i < col_count {
683 let w = UnicodeWidthStr::width(cell.as_str()) as u32;
684 self.column_widths[i] = self.column_widths[i].max(w);
685 }
686 }
687 }
688 self.dirty = false;
689 }
690
691 pub(crate) fn column_widths(&self) -> &[u32] {
692 &self.column_widths
693 }
694
695 pub(crate) fn is_dirty(&self) -> bool {
696 self.dirty
697 }
698}
699
700pub struct ScrollState {
706 pub offset: usize,
708 content_height: u32,
709 viewport_height: u32,
710}
711
712impl ScrollState {
713 pub fn new() -> Self {
715 Self {
716 offset: 0,
717 content_height: 0,
718 viewport_height: 0,
719 }
720 }
721
722 pub fn can_scroll_up(&self) -> bool {
724 self.offset > 0
725 }
726
727 pub fn can_scroll_down(&self) -> bool {
729 (self.offset as u32) + self.viewport_height < self.content_height
730 }
731
732 pub fn content_height(&self) -> u32 {
734 self.content_height
735 }
736
737 pub fn viewport_height(&self) -> u32 {
739 self.viewport_height
740 }
741
742 pub fn progress(&self) -> f32 {
744 let max = self.content_height.saturating_sub(self.viewport_height);
745 if max == 0 {
746 0.0
747 } else {
748 self.offset as f32 / max as f32
749 }
750 }
751
752 pub fn scroll_up(&mut self, amount: usize) {
754 self.offset = self.offset.saturating_sub(amount);
755 }
756
757 pub fn scroll_down(&mut self, amount: usize) {
759 let max_offset = self.content_height.saturating_sub(self.viewport_height) as usize;
760 self.offset = (self.offset + amount).min(max_offset);
761 }
762
763 pub(crate) fn set_bounds(&mut self, content_height: u32, viewport_height: u32) {
764 self.content_height = content_height;
765 self.viewport_height = viewport_height;
766 }
767}
768
769impl Default for ScrollState {
770 fn default() -> Self {
771 Self::new()
772 }
773}
774
775#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
785pub enum ButtonVariant {
786 #[default]
788 Default,
789 Primary,
791 Danger,
793 Outline,
795}
796
797pub struct SelectState {
804 pub items: Vec<String>,
805 pub selected: usize,
806 pub open: bool,
807 pub placeholder: String,
808 cursor: usize,
809}
810
811impl SelectState {
812 pub fn new(items: Vec<impl Into<String>>) -> Self {
813 Self {
814 items: items.into_iter().map(Into::into).collect(),
815 selected: 0,
816 open: false,
817 placeholder: String::new(),
818 cursor: 0,
819 }
820 }
821
822 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
823 self.placeholder = p.into();
824 self
825 }
826
827 pub fn selected_item(&self) -> Option<&str> {
828 self.items.get(self.selected).map(String::as_str)
829 }
830
831 pub(crate) fn cursor(&self) -> usize {
832 self.cursor
833 }
834
835 pub(crate) fn set_cursor(&mut self, c: usize) {
836 self.cursor = c;
837 }
838}
839
840pub struct RadioState {
846 pub items: Vec<String>,
847 pub selected: usize,
848}
849
850impl RadioState {
851 pub fn new(items: Vec<impl Into<String>>) -> Self {
852 Self {
853 items: items.into_iter().map(Into::into).collect(),
854 selected: 0,
855 }
856 }
857
858 pub fn selected_item(&self) -> Option<&str> {
859 self.items.get(self.selected).map(String::as_str)
860 }
861}
862
863pub struct MultiSelectState {
869 pub items: Vec<String>,
870 pub cursor: usize,
871 pub selected: HashSet<usize>,
872}
873
874impl MultiSelectState {
875 pub fn new(items: Vec<impl Into<String>>) -> Self {
876 Self {
877 items: items.into_iter().map(Into::into).collect(),
878 cursor: 0,
879 selected: HashSet::new(),
880 }
881 }
882
883 pub fn selected_items(&self) -> Vec<&str> {
884 let mut indices: Vec<usize> = self.selected.iter().copied().collect();
885 indices.sort();
886 indices
887 .iter()
888 .filter_map(|&i| self.items.get(i).map(String::as_str))
889 .collect()
890 }
891
892 pub fn toggle(&mut self, index: usize) {
893 if self.selected.contains(&index) {
894 self.selected.remove(&index);
895 } else {
896 self.selected.insert(index);
897 }
898 }
899}
900
901pub struct TreeNode {
905 pub label: String,
906 pub children: Vec<TreeNode>,
907 pub expanded: bool,
908}
909
910impl TreeNode {
911 pub fn new(label: impl Into<String>) -> Self {
912 Self {
913 label: label.into(),
914 children: Vec::new(),
915 expanded: false,
916 }
917 }
918
919 pub fn expanded(mut self) -> Self {
920 self.expanded = true;
921 self
922 }
923
924 pub fn children(mut self, children: Vec<TreeNode>) -> Self {
925 self.children = children;
926 self
927 }
928
929 pub fn is_leaf(&self) -> bool {
930 self.children.is_empty()
931 }
932
933 fn flatten(&self, depth: usize, out: &mut Vec<FlatTreeEntry>) {
934 out.push(FlatTreeEntry {
935 depth,
936 label: self.label.clone(),
937 is_leaf: self.is_leaf(),
938 expanded: self.expanded,
939 });
940 if self.expanded {
941 for child in &self.children {
942 child.flatten(depth + 1, out);
943 }
944 }
945 }
946}
947
948pub(crate) struct FlatTreeEntry {
949 pub depth: usize,
950 pub label: String,
951 pub is_leaf: bool,
952 pub expanded: bool,
953}
954
955pub struct TreeState {
957 pub nodes: Vec<TreeNode>,
958 pub selected: usize,
959}
960
961impl TreeState {
962 pub fn new(nodes: Vec<TreeNode>) -> Self {
963 Self { nodes, selected: 0 }
964 }
965
966 pub(crate) fn flatten(&self) -> Vec<FlatTreeEntry> {
967 let mut entries = Vec::new();
968 for node in &self.nodes {
969 node.flatten(0, &mut entries);
970 }
971 entries
972 }
973
974 pub(crate) fn toggle_at(&mut self, flat_index: usize) {
975 let mut counter = 0usize;
976 Self::toggle_recursive(&mut self.nodes, flat_index, &mut counter);
977 }
978
979 fn toggle_recursive(nodes: &mut [TreeNode], target: usize, counter: &mut usize) -> bool {
980 for node in nodes.iter_mut() {
981 if *counter == target {
982 if !node.is_leaf() {
983 node.expanded = !node.expanded;
984 }
985 return true;
986 }
987 *counter += 1;
988 if node.expanded && Self::toggle_recursive(&mut node.children, target, counter) {
989 return true;
990 }
991 }
992 false
993 }
994}
995
996pub struct PaletteCommand {
1000 pub label: String,
1001 pub description: String,
1002 pub shortcut: Option<String>,
1003}
1004
1005impl PaletteCommand {
1006 pub fn new(label: impl Into<String>, description: impl Into<String>) -> Self {
1007 Self {
1008 label: label.into(),
1009 description: description.into(),
1010 shortcut: None,
1011 }
1012 }
1013
1014 pub fn shortcut(mut self, s: impl Into<String>) -> Self {
1015 self.shortcut = Some(s.into());
1016 self
1017 }
1018}
1019
1020pub struct CommandPaletteState {
1024 pub commands: Vec<PaletteCommand>,
1025 pub input: String,
1026 pub cursor: usize,
1027 pub open: bool,
1028 selected: usize,
1029}
1030
1031impl CommandPaletteState {
1032 pub fn new(commands: Vec<PaletteCommand>) -> Self {
1033 Self {
1034 commands,
1035 input: String::new(),
1036 cursor: 0,
1037 open: false,
1038 selected: 0,
1039 }
1040 }
1041
1042 pub fn toggle(&mut self) {
1043 self.open = !self.open;
1044 if self.open {
1045 self.input.clear();
1046 self.cursor = 0;
1047 self.selected = 0;
1048 }
1049 }
1050
1051 pub(crate) fn filtered_indices(&self) -> Vec<usize> {
1052 let tokens: Vec<String> = self
1053 .input
1054 .split_whitespace()
1055 .map(|t| t.to_lowercase())
1056 .collect();
1057 if tokens.is_empty() {
1058 return (0..self.commands.len()).collect();
1059 }
1060 self.commands
1061 .iter()
1062 .enumerate()
1063 .filter(|(_, cmd)| {
1064 let label = cmd.label.to_lowercase();
1065 let desc = cmd.description.to_lowercase();
1066 tokens
1067 .iter()
1068 .all(|token| label.contains(token.as_str()) || desc.contains(token.as_str()))
1069 })
1070 .map(|(i, _)| i)
1071 .collect()
1072 }
1073
1074 pub(crate) fn selected(&self) -> usize {
1075 self.selected
1076 }
1077
1078 pub(crate) fn set_selected(&mut self, s: usize) {
1079 self.selected = s;
1080 }
1081}
1082
1083pub struct StreamingTextState {
1088 pub content: String,
1090 pub streaming: bool,
1092 pub(crate) cursor_visible: bool,
1094 pub(crate) cursor_tick: u64,
1095}
1096
1097impl StreamingTextState {
1098 pub fn new() -> Self {
1100 Self {
1101 content: String::new(),
1102 streaming: false,
1103 cursor_visible: true,
1104 cursor_tick: 0,
1105 }
1106 }
1107
1108 pub fn push(&mut self, chunk: &str) {
1110 self.content.push_str(chunk);
1111 }
1112
1113 pub fn finish(&mut self) {
1115 self.streaming = false;
1116 }
1117
1118 pub fn start(&mut self) {
1120 self.content.clear();
1121 self.streaming = true;
1122 self.cursor_visible = true;
1123 self.cursor_tick = 0;
1124 }
1125
1126 pub fn clear(&mut self) {
1128 self.content.clear();
1129 self.streaming = false;
1130 self.cursor_visible = true;
1131 self.cursor_tick = 0;
1132 }
1133}
1134
1135impl Default for StreamingTextState {
1136 fn default() -> Self {
1137 Self::new()
1138 }
1139}
1140
1141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1143pub enum ApprovalAction {
1144 Pending,
1146 Approved,
1148 Rejected,
1150}
1151
1152pub struct ToolApprovalState {
1158 pub tool_name: String,
1160 pub description: String,
1162 pub action: ApprovalAction,
1164}
1165
1166impl ToolApprovalState {
1167 pub fn new(tool_name: impl Into<String>, description: impl Into<String>) -> Self {
1169 Self {
1170 tool_name: tool_name.into(),
1171 description: description.into(),
1172 action: ApprovalAction::Pending,
1173 }
1174 }
1175
1176 pub fn reset(&mut self) {
1178 self.action = ApprovalAction::Pending;
1179 }
1180}
1181
1182#[derive(Debug, Clone)]
1184pub struct ContextItem {
1185 pub label: String,
1187 pub tokens: usize,
1189}
1190
1191impl ContextItem {
1192 pub fn new(label: impl Into<String>, tokens: usize) -> Self {
1194 Self {
1195 label: label.into(),
1196 tokens,
1197 }
1198 }
1199}