Skip to main content

kimun_notes/components/preferences/
sorting_section.rs

1use ratatui::Frame;
2use ratatui::layout::{Constraint, Direction, Layout, Rect};
3use ratatui::style::Style;
4use ratatui::widgets::{Block, Borders, List, ListItem, ListState};
5
6use crate::components::Component;
7use crate::components::event_state::EventState;
8use crate::components::events::{AppTx, InputEvent};
9use crate::settings::themes::Theme;
10use crate::settings::{SortFieldSetting, SortOrderSetting};
11
12use super::{ClickMap, text_rect};
13use ratatui::text::{Line, Span};
14use unicode_width::UnicodeWidthStr;
15
16pub struct SortingSection {
17    pub default_sort_field: SortFieldSetting,
18    pub default_sort_order: SortOrderSetting,
19    pub journal_sort_field: SortFieldSetting,
20    pub journal_sort_order: SortOrderSetting,
21    list_state: ListState,
22    clicks: ClickMap,
23}
24
25impl SortingSection {
26    pub fn new(
27        default_sort_field: SortFieldSetting,
28        default_sort_order: SortOrderSetting,
29        journal_sort_field: SortFieldSetting,
30        journal_sort_order: SortOrderSetting,
31    ) -> Self {
32        let mut list_state = ListState::default();
33        list_state.select(Some(0));
34        Self {
35            default_sort_field,
36            default_sort_order,
37            journal_sort_field,
38            journal_sort_order,
39            list_state,
40            clicks: ClickMap::default(),
41        }
42    }
43
44    /// Resolve a click: a row selects it, its `[value]` (or a re-click on
45    /// the selected row) cycles it. Returns the key the click stands for.
46    pub fn handle_mouse(
47        &mut self,
48        m: &ratatui::crossterm::event::MouseEvent,
49    ) -> Option<ratatui::crossterm::event::KeyEvent> {
50        use ratatui::crossterm::event::KeyCode;
51        self.clicks
52            .resolve(m, self.list_state.selected(), Some(KeyCode::Enter))
53            .into_key(|row| self.list_state.select(Some(row)))
54    }
55
56    /// `  Sort field:  [Name]` with the `[value]` as a click target for
57    /// `row`, on `line` of `inner` (the drawn block's inner area).
58    fn value_row(
59        &mut self,
60        inner: Rect,
61        line: usize,
62        row: usize,
63        label: &'static str,
64        value: &str,
65        theme: &Theme,
66    ) -> ListItem<'static> {
67        let r = if (line as u16) < inner.height {
68            Rect::new(inner.x, inner.y + line as u16, inner.width, 1)
69        } else {
70            Rect::default()
71        };
72        let chip = format!("[{value}]");
73        self.clicks.row(r, row);
74        self.clicks.control(
75            text_rect(r, label.width() as u16, chip.width() as u16),
76            row,
77            ratatui::crossterm::event::KeyCode::Enter,
78        );
79        ListItem::new(Line::from(vec![
80            Span::raw(label),
81            Span::styled(chip, theme.action()),
82        ]))
83        .style(Style::default().fg(theme.fg.to_ratatui()))
84    }
85
86    const ROW_COUNT: usize = 4;
87
88    fn cycle_field(f: SortFieldSetting) -> SortFieldSetting {
89        match f {
90            SortFieldSetting::Name => SortFieldSetting::Title,
91            SortFieldSetting::Title => SortFieldSetting::Name,
92        }
93    }
94
95    fn cycle_order(o: SortOrderSetting) -> SortOrderSetting {
96        match o {
97            SortOrderSetting::Ascending => SortOrderSetting::Descending,
98            SortOrderSetting::Descending => SortOrderSetting::Ascending,
99        }
100    }
101
102    fn field_label(f: SortFieldSetting) -> &'static str {
103        match f {
104            SortFieldSetting::Name => "Name",
105            SortFieldSetting::Title => "Title",
106        }
107    }
108
109    fn order_label(o: SortOrderSetting) -> &'static str {
110        match o {
111            SortOrderSetting::Ascending => "Ascending",
112            SortOrderSetting::Descending => "Descending",
113        }
114    }
115}
116
117impl Component for SortingSection {
118    fn handle_input(&mut self, event: &InputEvent, _tx: &AppTx) -> EventState {
119        let InputEvent::Key(key) = event else {
120            return EventState::NotConsumed;
121        };
122        let selected = self.list_state.selected().unwrap_or(0);
123        match key.code {
124            ratatui::crossterm::event::KeyCode::Up
125            | ratatui::crossterm::event::KeyCode::Char('k') => {
126                self.list_state
127                    .select(Some((selected + Self::ROW_COUNT - 1) % Self::ROW_COUNT));
128                EventState::Consumed
129            }
130            ratatui::crossterm::event::KeyCode::Down
131            | ratatui::crossterm::event::KeyCode::Char('j') => {
132                self.list_state
133                    .select(Some((selected + 1) % Self::ROW_COUNT));
134                EventState::Consumed
135            }
136            ratatui::crossterm::event::KeyCode::Enter
137            | ratatui::crossterm::event::KeyCode::Char(' ') => {
138                match selected {
139                    0 => self.default_sort_field = Self::cycle_field(self.default_sort_field),
140                    1 => self.default_sort_order = Self::cycle_order(self.default_sort_order),
141                    2 => self.journal_sort_field = Self::cycle_field(self.journal_sort_field),
142                    3 => self.journal_sort_order = Self::cycle_order(self.journal_sort_order),
143                    _ => {}
144                }
145                EventState::Consumed
146            }
147            _ => EventState::NotConsumed,
148        }
149    }
150
151    fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
152        let border_style = theme.border_style(focused);
153
154        // Outer container
155        let outer = Block::default()
156            .title("Sorting")
157            .borders(Borders::ALL)
158            .border_style(border_style)
159            .style(theme.base_style());
160        let inner = outer.inner(rect);
161        f.render_widget(outer, rect);
162
163        // Stack the two sub-groups vertically inside the outer block.
164        // Each sub-block needs 2 content rows + 2 border rows = 4 rows.
165        let halves = Layout::default()
166            .direction(Direction::Vertical)
167            .constraints([Constraint::Length(4), Constraint::Length(4)])
168            .split(inner);
169
170        let selected = self.list_state.selected().unwrap_or(0);
171        let highlight = Style::default()
172            .fg(theme.selection_fg.to_ratatui())
173            .bg(theme.selection_bg.to_ratatui());
174
175        // ── Default sub-block (rows 0–1) ───────────────────────────────────
176        self.clicks.clear();
177        let default_block = Block::default()
178            .title("Default")
179            .borders(Borders::ALL)
180            .border_style(theme.border_style(focused && selected < 2))
181            .style(theme.base_style());
182        let default_inner = default_block.inner(halves[0]);
183        let default_items = vec![
184            self.value_row(
185                default_inner,
186                0,
187                0,
188                "  Sort field:  ",
189                Self::field_label(self.default_sort_field),
190                theme,
191            ),
192            self.value_row(
193                default_inner,
194                1,
195                1,
196                "  Sort order:  ",
197                Self::order_label(self.default_sort_order),
198                theme,
199            ),
200        ];
201        let mut default_state = ListState::default();
202        default_state.select(if selected < 2 { Some(selected) } else { None });
203        f.render_stateful_widget(
204            List::new(default_items)
205                .block(default_block)
206                .highlight_style(highlight),
207            halves[0],
208            &mut default_state,
209        );
210
211        // ── Journal sub-block (rows 2–3) ────────────────────────────────────
212        let journal_block = Block::default()
213            .title("Journal")
214            .borders(Borders::ALL)
215            .border_style(theme.border_style(focused && selected >= 2))
216            .style(theme.base_style());
217        let journal_inner = journal_block.inner(halves[1]);
218        let journal_items = vec![
219            self.value_row(
220                journal_inner,
221                0,
222                2,
223                "  Sort field:  ",
224                Self::field_label(self.journal_sort_field),
225                theme,
226            ),
227            self.value_row(
228                journal_inner,
229                1,
230                3,
231                "  Sort order:  ",
232                Self::order_label(self.journal_sort_order),
233                theme,
234            ),
235        ];
236        let mut journal_state = ListState::default();
237        journal_state.select(if selected >= 2 {
238            Some(selected - 2)
239        } else {
240            None
241        });
242        f.render_stateful_widget(
243            List::new(journal_items)
244                .block(journal_block)
245                .highlight_style(highlight),
246            halves[1],
247            &mut journal_state,
248        );
249    }
250}