Skip to main content

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_type(BorderType::Rounded)
59            .border_style(if config.mode == Mode::FilterInput {
60                Style::default().fg(Color::Yellow)
61            } else {
62                Style::default()
63            }),
64        config.area,
65    );
66
67    // Left side: filter input
68    let mut input_spans = vec![];
69    if config.filter_text.is_empty() {
70        if config.mode == Mode::FilterInput {
71            input_spans.push(Span::raw(""));
72        } else {
73            input_spans.push(Span::styled(
74                config.placeholder,
75                Style::default().fg(Color::DarkGray),
76            ));
77        }
78    } else {
79        input_spans.push(Span::raw(config.filter_text));
80    }
81
82    if config.mode == Mode::FilterInput && config.is_input_focused {
83        input_spans.push(Span::styled(cursor, Style::default().fg(Color::Yellow)));
84    }
85
86    frame.render_widget(
87        Paragraph::new(Line::from(input_spans))
88            .alignment(Alignment::Left)
89            .style(Style::default()),
90        chunks[0],
91    );
92
93    // Right side: controls (flush right)
94    let mut control_spans = vec![Span::raw(" ⋮ ")];
95    if config.mode == Mode::FilterInput {
96        let highlight = Style::default().fg(Color::Yellow);
97        for (i, control) in config.controls.iter().enumerate() {
98            if i > 0 {
99                control_spans.push(Span::raw(" ⋮ "));
100            }
101            control_spans.push(Span::styled(
102                &control.text,
103                if control.is_focused {
104                    highlight
105                } else {
106                    Style::default()
107                },
108            ));
109        }
110    } else {
111        let controls_text: String = config
112            .controls
113            .iter()
114            .map(|c| c.text.as_str())
115            .collect::<Vec<_>>()
116            .join(" ⋮ ");
117        control_spans.push(Span::styled(controls_text, Style::default()));
118    }
119
120    frame.render_widget(
121        Paragraph::new(Line::from(control_spans))
122            .alignment(Alignment::Right)
123            .style(Style::default()),
124        chunks[1],
125    );
126}
127
128/// Helper to create a simple pagination-only filter
129pub struct SimpleFilterConfig<'a> {
130    pub filter_text: &'a str,
131    pub placeholder: &'a str,
132    pub pagination: &'a str,
133    pub mode: Mode,
134    pub is_input_focused: bool,
135    pub is_pagination_focused: bool,
136}
137
138pub fn render_simple_filter(frame: &mut Frame, area: Rect, config: SimpleFilterConfig) {
139    render_filter_bar(
140        frame,
141        FilterConfig {
142            filter_text: config.filter_text,
143            placeholder: config.placeholder,
144            mode: config.mode,
145            is_input_focused: config.is_input_focused,
146            controls: vec![FilterControl {
147                text: config.pagination.to_string(),
148                is_focused: config.is_pagination_focused,
149            }],
150            area,
151        },
152    );
153}