1use ratatui::Frame;
13use ratatui::layout::Rect;
14use ratatui::style::{Color, Modifier, Style};
15use ratatui::text::Line;
16use ratatui::widgets::{Block, Borders, Clear, Paragraph};
17
18use crate::search::{FilterField, StructuredFilter};
19
20use super::modal::modal_rect;
21
22pub struct FilterModal {
24 filter: StructuredFilter,
26 focus: FilterField,
28}
29
30impl Default for FilterModal {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36impl FilterModal {
37 pub fn new() -> Self {
38 Self {
39 filter: StructuredFilter::default(),
40 focus: FilterField::Assignee,
41 }
42 }
43
44 pub fn with_filter(filter: StructuredFilter) -> Self {
47 Self {
48 filter,
49 focus: FilterField::Assignee,
50 }
51 }
52
53 pub fn active_filter(&self) -> &StructuredFilter {
55 &self.filter
56 }
57
58 pub fn active_filter_mut(&mut self) -> &mut StructuredFilter {
59 &mut self.filter
60 }
61
62 pub fn apply(&mut self) {}
65
66 pub fn clear(&mut self) {
70 self.filter = StructuredFilter::default();
71 self.focus = FilterField::Assignee;
72 }
73
74 pub fn focus_next(&mut self) {
76 self.focus = self.focus.next();
77 }
78
79 pub fn focus_prev(&mut self) {
81 self.focus = self.focus.prev();
82 }
83
84 pub fn focus(&self) -> FilterField {
86 self.focus
87 }
88
89 pub fn render(&self, f: &mut Frame, area: Rect) {
93 let rect = modal_rect(area);
94 let mut rows: Vec<Line> = Vec::new();
95
96 for field in FilterField::ALL {
97 let (label, value) = (field.label(), field.value(&self.filter));
98 let is_focus = field == self.focus;
99 let value_text = value.unwrap_or("(unset)");
100 let style = if is_focus {
101 Style::default()
102 .fg(Color::Yellow)
103 .add_modifier(Modifier::BOLD)
104 } else {
105 Style::default().fg(Color::White)
106 };
107 let label_style = if is_focus {
108 Style::default()
109 .fg(Color::Cyan)
110 .add_modifier(Modifier::BOLD)
111 } else {
112 Style::default().fg(Color::DarkGray)
113 };
114 let prefix = if is_focus { "> " } else { " " };
115 rows.push(Line::from(vec![
116 ratatui::text::Span::styled(format!("{prefix}{label}:"), label_style),
117 ratatui::text::Span::styled(format!(" {value_text}"), style),
118 ]));
119 }
120
121 let block = Block::default()
122 .borders(Borders::ALL)
123 .title(" Filter ")
124 .style(Style::default().fg(Color::Cyan));
125 let para = Paragraph::new(rows).block(block).style(Style::default());
126 f.render_widget(Clear, rect);
127 f.render_widget(para, rect);
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use ratatui::Terminal;
135 use ratatui::backend::TestBackend;
136
137 fn smoke(w: u16, h: u16, title_focus: bool) {
138 let backend = TestBackend::new(w, h);
139 let mut terminal = Terminal::new(backend).unwrap();
140 terminal
141 .draw(|f| {
142 let area = f.area();
143 let mut modal = FilterModal::new();
144 if title_focus {
145 modal.focus_next();
146 }
147 modal.render(f, area);
148 })
149 .unwrap();
150 }
151
152 #[test]
153 fn renders_at_120x40() {
154 smoke(120, 40, false);
155 }
156
157 #[test]
158 fn renders_at_80x24() {
159 smoke(80, 24, false);
160 }
161
162 #[test]
163 fn renders_at_tiny_20x5() {
164 smoke(20, 5, true);
165 }
166
167 #[test]
168 fn focus_cycles_forward() {
169 let mut modal = FilterModal::new();
170 assert_eq!(modal.focus(), FilterField::Assignee);
171 modal.focus_next();
172 assert_eq!(modal.focus(), FilterField::Label);
173 modal.focus_next();
174 modal.focus_next();
175 assert_eq!(modal.focus(), FilterField::Priority);
176 modal.focus_next();
177 assert_eq!(modal.focus(), FilterField::Assignee);
178 }
179
180 #[test]
181 fn focus_cycles_backward() {
182 let mut modal = FilterModal::new();
183 modal.focus_prev();
184 assert_eq!(modal.focus(), FilterField::Priority);
185 modal.focus_prev();
186 assert_eq!(modal.focus(), FilterField::Status);
187 }
188
189 #[test]
190 fn clear_resets_all_criteria_and_focus() {
191 let mut modal = FilterModal::with_filter(StructuredFilter {
192 assignee: Some("alice".to_string()),
193 label: Some("urgent".to_string()),
194 status: Some("todo".to_string()),
195 priority: Some("high".to_string()),
196 });
197 modal.clear();
198 assert_eq!(modal.active_filter(), &StructuredFilter::default());
199 assert_eq!(modal.focus(), FilterField::Assignee);
200 }
201}