kimun_notes/components/preferences/
mod.rs1pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SectionClick {
19 Row(usize),
22 Control { row: usize, key: KeyCode },
25}
26
27#[derive(Default)]
35pub struct ClickMap {
36 targets: Vec<(Rect, SectionClick)>,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum SectionMouse {
42 Select(usize),
44 Key(usize, KeyEvent),
46 Wheel(KeyEvent),
48 None,
49}
50
51impl SectionMouse {
52 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 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 pub fn clear(&mut self) {
86 self.targets.clear();
87 }
88
89 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 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 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 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
139pub 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}