Skip to main content

kimun_notes/components/preferences/
mod.rs

1pub mod appearance_section;
2pub mod display_section;
3pub mod editor_section;
4pub mod indexing_section;
5pub mod server_section;
6pub mod sorting_section;
7pub mod theme_picker;
8pub mod vault_section;
9pub mod workspaces_section;
10
11use ratatui::crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind};
12use ratatui::layout::{Position, Rect};
13
14use crate::components::clickable::is_left_press;
15
16/// What a click in a preferences section asks for.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SectionClick {
19    /// A row: select it. Clicking the row that is already selected runs
20    /// the row's own key instead (the "click it again" list convention).
21    Row(usize),
22    /// A control on a row (`[x]`, `◀`, `▶`, `[Name]`, a button): select the
23    /// row and run `key` at once.
24    Control { row: usize, key: KeyCode },
25}
26
27/// The mouse model shared by every preferences section: rows select,
28/// controls act, the wheel moves the selection. A click is always turned
29/// into the key it stands for, so the screen runs it through the same path
30/// a key takes — settings write-back included.
31///
32/// Rows are list rows and get no hover; controls are click targets and
33/// register for it (`components::clickable`).
34#[derive(Default)]
35pub struct ClickMap {
36    targets: Vec<(Rect, SectionClick)>,
37}
38
39/// What the section should do with a mouse event.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum SectionMouse {
42    /// Select this row (no key).
43    Select(usize),
44    /// Select this row, then run this key.
45    Key(usize, KeyEvent),
46    /// The wheel: run this key (Up/Down) against the current selection.
47    Wheel(KeyEvent),
48    None,
49}
50
51impl SectionMouse {
52    /// Apply the outcome: `select` any row it names, and return the key the
53    /// click stands for (for the screen's key path).
54    pub fn into_key(self, mut select: impl FnMut(usize)) -> Option<KeyEvent> {
55        match self {
56            SectionMouse::Select(row) => {
57                select(row);
58                None
59            }
60            SectionMouse::Key(row, key) => {
61                select(row);
62                Some(key)
63            }
64            SectionMouse::Wheel(key) => Some(key),
65            SectionMouse::None => None,
66        }
67    }
68}
69
70impl ClickMap {
71    /// Record the visible rows of a scrolled list drawn in `inner`: item
72    /// `offset` on the first line, up to `len` items or the bottom edge.
73    /// Call after rendering, once the list has settled its offset.
74    pub fn list_rows(&mut self, inner: Rect, offset: usize, len: usize) {
75        for (line, row) in (offset..len).enumerate() {
76            let y = inner.y as usize + line;
77            if y >= inner.bottom() as usize {
78                break;
79            }
80            self.row(Rect::new(inner.x, y as u16, inner.width, 1), row);
81        }
82    }
83
84    /// Forget the last render's targets — call at the top of `render`.
85    pub fn clear(&mut self) {
86        self.targets.clear();
87    }
88
89    /// Record a selectable row.
90    pub fn row(&mut self, rect: Rect, row: usize) {
91        if !rect.is_empty() {
92            self.targets.push((rect, SectionClick::Row(row)));
93        }
94    }
95
96    /// Record a control that runs `key` for `row`; it gets hover.
97    pub fn control(&mut self, rect: Rect, row: usize, key: KeyCode) {
98        if !rect.is_empty() {
99            crate::components::clickable::register(rect);
100            self.targets
101                .push((rect, SectionClick::Control { row, key }));
102        }
103    }
104
105    /// Resolve a mouse event. `selected` is the section's current row and
106    /// `activate` the key a click on the already-selected row runs (`None`
107    /// when re-clicking does nothing, e.g. a theme list).
108    pub fn resolve(
109        &self,
110        m: &MouseEvent,
111        selected: Option<usize>,
112        activate: Option<KeyCode>,
113    ) -> SectionMouse {
114        match m.kind {
115            MouseEventKind::ScrollUp => return SectionMouse::Wheel(KeyEvent::from(KeyCode::Up)),
116            MouseEventKind::ScrollDown => {
117                return SectionMouse::Wheel(KeyEvent::from(KeyCode::Down));
118            }
119            _ => {}
120        }
121        if !is_left_press(m) {
122            return SectionMouse::None;
123        }
124        let pos = Position::new(m.column, m.row);
125        // Controls sit on top of their row: the last match wins.
126        match self.targets.iter().rev().find(|(r, _)| r.contains(pos)) {
127            Some((_, SectionClick::Control { row, key })) => {
128                SectionMouse::Key(*row, KeyEvent::from(*key))
129            }
130            Some((_, SectionClick::Row(row))) => match activate {
131                Some(key) if selected == Some(*row) => SectionMouse::Key(*row, KeyEvent::from(key)),
132                _ => SectionMouse::Select(*row),
133            },
134            None => SectionMouse::None,
135        }
136    }
137}
138
139/// A one-line rect for the text at `col` cells into `row`, `width` wide,
140/// clipped to `row`.
141pub fn text_rect(row: Rect, col: u16, width: u16) -> Rect {
142    let x = row.x.saturating_add(col);
143    if x >= row.right() {
144        return Rect::default();
145    }
146    Rect::new(x, row.y, width.min(row.right() - x), 1)
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use ratatui::crossterm::event::{KeyModifiers, MouseButton};
153
154    fn press(col: u16, row: u16) -> MouseEvent {
155        MouseEvent {
156            kind: MouseEventKind::Down(MouseButton::Left),
157            column: col,
158            row,
159            modifiers: KeyModifiers::NONE,
160        }
161    }
162
163    #[test]
164    fn rows_select_controls_act_and_reclick_activates() {
165        let mut map = ClickMap::default();
166        map.row(Rect::new(0, 0, 20, 1), 0);
167        map.row(Rect::new(0, 1, 20, 1), 1);
168        map.control(Rect::new(10, 1, 3, 1), 1, KeyCode::Right);
169        let enter = Some(KeyCode::Enter);
170        assert_eq!(
171            map.resolve(&press(2, 1), Some(0), enter),
172            SectionMouse::Select(1)
173        );
174        assert_eq!(
175            map.resolve(&press(2, 1), Some(1), enter),
176            SectionMouse::Key(1, KeyEvent::from(KeyCode::Enter)),
177            "re-clicking the selected row runs its key"
178        );
179        assert_eq!(
180            map.resolve(&press(11, 1), Some(0), enter),
181            SectionMouse::Key(1, KeyEvent::from(KeyCode::Right)),
182            "a control wins over its row"
183        );
184        assert_eq!(
185            map.resolve(&press(2, 1), Some(1), None),
186            SectionMouse::Select(1)
187        );
188        assert_eq!(
189            map.resolve(&press(2, 5), Some(1), enter),
190            SectionMouse::None
191        );
192    }
193
194    #[test]
195    fn text_rect_clips_to_the_row() {
196        let row = Rect::new(5, 2, 10, 1);
197        assert_eq!(text_rect(row, 2, 4), Rect::new(7, 2, 4, 1));
198        assert_eq!(text_rect(row, 8, 4), Rect::new(13, 2, 2, 1));
199        assert_eq!(text_rect(row, 12, 4), Rect::default());
200    }
201}