mxr_tui/ui/
label_picker.rs1use mxr_core::types::Label;
2use ratatui::prelude::*;
3use ratatui::widgets::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum LabelPickerMode {
7 Apply,
8 Move,
9}
10
11pub struct LabelPicker {
12 pub visible: bool,
13 pub input: String,
14 pub labels: Vec<Label>,
15 pub filtered: Vec<usize>,
16 pub selected: usize,
17 pub mode: LabelPickerMode,
18}
19
20impl Default for LabelPicker {
21 fn default() -> Self {
22 Self {
23 visible: false,
24 input: String::new(),
25 labels: Vec::new(),
26 filtered: Vec::new(),
27 selected: 0,
28 mode: LabelPickerMode::Apply,
29 }
30 }
31}
32
33impl LabelPicker {
34 pub fn open(&mut self, labels: Vec<Label>, mode: LabelPickerMode) {
35 self.visible = true;
36 self.input.clear();
37 self.selected = 0;
38 self.labels = labels;
39 self.mode = mode;
40 self.update_filtered();
41 }
42
43 pub fn close(&mut self) {
44 self.visible = false;
45 self.input.clear();
46 self.labels.clear();
47 self.filtered.clear();
48 }
49
50 pub fn on_char(&mut self, c: char) {
51 self.input.push(c);
52 self.selected = 0;
53 self.update_filtered();
54 }
55
56 pub fn on_backspace(&mut self) {
57 self.input.pop();
58 self.selected = 0;
59 self.update_filtered();
60 }
61
62 pub fn select_next(&mut self) {
63 if !self.filtered.is_empty() {
64 self.selected = (self.selected + 1) % self.filtered.len();
65 }
66 }
67
68 pub fn select_prev(&mut self) {
69 if !self.filtered.is_empty() {
70 self.selected = self
71 .selected
72 .checked_sub(1)
73 .unwrap_or(self.filtered.len() - 1);
74 }
75 }
76
77 pub fn confirm(&mut self) -> Option<String> {
79 if let Some(&idx) = self.filtered.get(self.selected) {
80 let name = self.labels[idx].name.clone();
81 self.close();
82 Some(name)
83 } else {
84 None
85 }
86 }
87
88 fn update_filtered(&mut self) {
89 let query = self.input.to_lowercase();
90 self.filtered = self
91 .labels
92 .iter()
93 .enumerate()
94 .filter(|(_, label)| {
95 if query.is_empty() {
96 return true;
97 }
98 label.name.to_lowercase().contains(&query)
99 })
100 .map(|(i, _)| i)
101 .collect();
102 }
103}
104
105pub fn draw(frame: &mut Frame, area: Rect, picker: &LabelPicker, theme: &crate::theme::Theme) {
106 if !picker.visible {
107 return;
108 }
109
110 let title = match picker.mode {
111 LabelPickerMode::Apply => " Apply Label ",
112 LabelPickerMode::Move => " Move to Label ",
113 };
114
115 let width = (area.width as u32 * 50 / 100).min(60) as u16;
116 let height = (picker.filtered.len() as u16 + 4)
117 .min(area.height * 50 / 100)
118 .max(6);
119 let x = area.x + (area.width.saturating_sub(width)) / 2;
120 let y = area.y + (area.height.saturating_sub(height)) / 2;
121 let popup_area = Rect::new(x, y, width, height);
122
123 frame.render_widget(Clear, popup_area);
124
125 let block = Block::default()
126 .title(title)
127 .borders(Borders::ALL)
128 .border_style(Style::default().fg(theme.success));
129
130 let inner = block.inner(popup_area);
131 frame.render_widget(block, popup_area);
132
133 if inner.height < 2 {
134 return;
135 }
136
137 let input_area = Rect::new(inner.x, inner.y, inner.width, 1);
139 let input_line = Paragraph::new(format!("> {}", picker.input))
140 .style(Style::default().fg(theme.text_primary));
141 frame.render_widget(input_line, input_area);
142
143 let list_area = Rect::new(
145 inner.x,
146 inner.y + 1,
147 inner.width,
148 inner.height.saturating_sub(1),
149 );
150
151 let items: Vec<ListItem> = picker
152 .filtered
153 .iter()
154 .enumerate()
155 .take(list_area.height as usize)
156 .map(|(i, &label_idx)| {
157 let label = &picker.labels[label_idx];
158 let display = humanize_label(&label.name);
159 let style = if i == picker.selected {
160 theme.highlight_style()
161 } else {
162 Style::default()
163 };
164 ListItem::new(format!(" {}", display)).style(style)
165 })
166 .collect();
167
168 let list = List::new(items);
169 frame.render_widget(list, list_area);
170}
171
172fn humanize_label(name: &str) -> &str {
173 match name {
174 "INBOX" => "Inbox",
175 "SENT" => "Sent",
176 "DRAFT" => "Drafts",
177 "TRASH" => "Trash",
178 "SPAM" => "Spam",
179 "STARRED" => "Starred",
180 "IMPORTANT" => "Important",
181 "UNREAD" => "Unread",
182 other => other,
183 }
184}