use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Line;
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
use crate::search::{FilterField, StructuredFilter};
use super::modal::modal_rect;
pub struct FilterModal {
filter: StructuredFilter,
focus: FilterField,
}
impl Default for FilterModal {
fn default() -> Self {
Self::new()
}
}
impl FilterModal {
pub fn new() -> Self {
Self {
filter: StructuredFilter::default(),
focus: FilterField::Assignee,
}
}
pub fn with_filter(filter: StructuredFilter) -> Self {
Self {
filter,
focus: FilterField::Assignee,
}
}
pub fn active_filter(&self) -> &StructuredFilter {
&self.filter
}
pub fn active_filter_mut(&mut self) -> &mut StructuredFilter {
&mut self.filter
}
pub fn apply(&mut self) {}
pub fn clear(&mut self) {
self.filter = StructuredFilter::default();
self.focus = FilterField::Assignee;
}
pub fn focus_next(&mut self) {
self.focus = self.focus.next();
}
pub fn focus_prev(&mut self) {
self.focus = self.focus.prev();
}
pub fn focus(&self) -> FilterField {
self.focus
}
pub fn render(&self, f: &mut Frame, area: Rect) {
let rect = modal_rect(area);
let mut rows: Vec<Line> = Vec::new();
for field in FilterField::ALL {
let (label, value) = (field.label(), field.value(&self.filter));
let is_focus = field == self.focus;
let value_text = value.unwrap_or("(unset)");
let style = if is_focus {
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::White)
};
let label_style = if is_focus {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::DarkGray)
};
let prefix = if is_focus { "> " } else { " " };
rows.push(Line::from(vec![
ratatui::text::Span::styled(format!("{prefix}{label}:"), label_style),
ratatui::text::Span::styled(format!(" {value_text}"), style),
]));
}
let block = Block::default()
.borders(Borders::ALL)
.title(" Filter ")
.style(Style::default().fg(Color::Cyan));
let para = Paragraph::new(rows).block(block).style(Style::default());
f.render_widget(Clear, rect);
f.render_widget(para, rect);
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
fn smoke(w: u16, h: u16, title_focus: bool) {
let backend = TestBackend::new(w, h);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let area = f.area();
let mut modal = FilterModal::new();
if title_focus {
modal.focus_next();
}
modal.render(f, area);
})
.unwrap();
}
#[test]
fn renders_at_120x40() {
smoke(120, 40, false);
}
#[test]
fn renders_at_80x24() {
smoke(80, 24, false);
}
#[test]
fn renders_at_tiny_20x5() {
smoke(20, 5, true);
}
#[test]
fn focus_cycles_forward() {
let mut modal = FilterModal::new();
assert_eq!(modal.focus(), FilterField::Assignee);
modal.focus_next();
assert_eq!(modal.focus(), FilterField::Label);
modal.focus_next();
modal.focus_next();
assert_eq!(modal.focus(), FilterField::Priority);
modal.focus_next();
assert_eq!(modal.focus(), FilterField::Assignee);
}
#[test]
fn focus_cycles_backward() {
let mut modal = FilterModal::new();
modal.focus_prev();
assert_eq!(modal.focus(), FilterField::Priority);
modal.focus_prev();
assert_eq!(modal.focus(), FilterField::Status);
}
#[test]
fn clear_resets_all_criteria_and_focus() {
let mut modal = FilterModal::with_filter(StructuredFilter {
assignee: Some("alice".to_string()),
label: Some("urgent".to_string()),
status: Some("todo".to_string()),
priority: Some("high".to_string()),
});
modal.clear();
assert_eq!(modal.active_filter(), &StructuredFilter::default());
assert_eq!(modal.focus(), FilterField::Assignee);
}
}