rusticity_term/ui/
filter.rs

1use crate::keymap::Mode;
2use crate::ui::{get_cursor, SEARCH_ICON};
3use ratatui::{prelude::*, widgets::*};
4
5pub struct FilterControl {
6    pub text: String,
7    pub is_focused: bool,
8}
9
10pub struct FilterConfig<'a> {
11    pub filter_text: &'a str,
12    pub placeholder: &'a str,
13    pub mode: Mode,
14    pub is_input_focused: bool,
15    pub controls: Vec<FilterControl>,
16    pub area: Rect,
17}
18
19/// Renders a filter bar with flush-right controls (like top bar datetime/profile)
20pub fn render_filter_bar(frame: &mut Frame, config: FilterConfig) {
21    let cursor = get_cursor(config.mode == Mode::FilterInput && config.is_input_focused);
22
23    // Calculate actual controls width based on mode
24    let controls_width = if config.mode == Mode::FilterInput {
25        // In FilterInput mode: " ⋮ " + control1 + " ⋮ " + control2 + ...
26        let separators_width = 3 + (config.controls.len().saturating_sub(1) * 3); // " ⋮ " = 3 chars
27        let controls_text_width: usize = config.controls.iter().map(|c| c.text.len()).sum();
28        (separators_width + controls_text_width) as u16
29    } else {
30        // In Normal mode: " ⋮ " + joined controls
31        let controls_text: String = config
32            .controls
33            .iter()
34            .map(|c| c.text.as_str())
35            .collect::<Vec<_>>()
36            .join(" ⋮ ");
37        (3 + controls_text.len()) as u16
38    };
39
40    // Split the filter area into left (input) and right (controls) like status bar
41    let inner_area = Rect {
42        x: config.area.x + 2,
43        y: config.area.y + 1,
44        width: config.area.width.saturating_sub(4),
45        height: 1,
46    };
47
48    let chunks = Layout::default()
49        .direction(Direction::Horizontal)
50        .constraints([Constraint::Min(0), Constraint::Length(controls_width)])
51        .split(inner_area);
52
53    // Render border
54    frame.render_widget(
55        Block::default()
56            .title(SEARCH_ICON)
57            .borders(Borders::ALL)
58            .border_style(if config.mode == Mode::FilterInput {
59                Style::default().fg(Color::Yellow)
60            } else {
61                Style::default()
62            }),
63        config.area,
64    );
65
66    // Left side: filter input
67    let mut input_spans = vec![];
68    if config.filter_text.is_empty() {
69        if config.mode == Mode::FilterInput {
70            input_spans.push(Span::raw(""));
71        } else {
72            input_spans.push(Span::styled(
73                config.placeholder,
74                Style::default().fg(Color::DarkGray),
75            ));
76        }
77    } else {
78        input_spans.push(Span::raw(config.filter_text));
79    }
80
81    if config.mode == Mode::FilterInput && config.is_input_focused {
82        input_spans.push(Span::styled(cursor, Style::default().fg(Color::Yellow)));
83    }
84
85    frame.render_widget(
86        Paragraph::new(Line::from(input_spans))
87            .alignment(Alignment::Left)
88            .style(Style::default()),
89        chunks[0],
90    );
91
92    // Right side: controls (flush right)
93    let mut control_spans = vec![Span::raw(" ⋮ ")];
94    if config.mode == Mode::FilterInput {
95        let highlight = Style::default().fg(Color::Yellow);
96        for (i, control) in config.controls.iter().enumerate() {
97            if i > 0 {
98                control_spans.push(Span::raw(" ⋮ "));
99            }
100            control_spans.push(Span::styled(
101                &control.text,
102                if control.is_focused {
103                    highlight
104                } else {
105                    Style::default()
106                },
107            ));
108        }
109    } else {
110        let controls_text: String = config
111            .controls
112            .iter()
113            .map(|c| c.text.as_str())
114            .collect::<Vec<_>>()
115            .join(" ⋮ ");
116        control_spans.push(Span::styled(controls_text, Style::default()));
117    }
118
119    frame.render_widget(
120        Paragraph::new(Line::from(control_spans))
121            .alignment(Alignment::Right)
122            .style(Style::default()),
123        chunks[1],
124    );
125}
126
127/// Helper to create a simple pagination-only filter
128pub struct SimpleFilterConfig<'a> {
129    pub filter_text: &'a str,
130    pub placeholder: &'a str,
131    pub pagination: &'a str,
132    pub mode: Mode,
133    pub is_input_focused: bool,
134    pub is_pagination_focused: bool,
135}
136
137pub fn render_simple_filter(frame: &mut Frame, area: Rect, config: SimpleFilterConfig) {
138    render_filter_bar(
139        frame,
140        FilterConfig {
141            filter_text: config.filter_text,
142            placeholder: config.placeholder,
143            mode: config.mode,
144            is_input_focused: config.is_input_focused,
145            controls: vec![FilterControl {
146                text: config.pagination.to_string(),
147                is_focused: config.is_pagination_focused,
148            }],
149            area,
150        },
151    );
152}