Skip to main content

mxr_tui/ui/
label_picker.rs

1use 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    /// Returns the selected label's name, or None.
78    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    // Input line
138    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    // Label list
144    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}