fresh/view/controls/keybinding_list/
mod.rs1mod input;
10mod render;
11
12use super::FocusState;
13use ratatui::layout::Rect;
14use ratatui::style::Color;
15use serde_json::Value;
16
17pub use input::KeybindingListEvent;
18pub use render::{format_key_combo, render_keybinding_list};
19
20#[derive(Debug, Clone)]
22pub struct KeybindingListState {
23 pub bindings: Vec<Value>,
25 pub focused_index: Option<usize>,
27 pub label: String,
29 pub focus: FocusState,
31 pub item_schema: Option<Box<crate::view::settings::schema::SettingSchema>>,
33 pub display_field: Option<String>,
35}
36
37impl KeybindingListState {
38 pub fn new(label: impl Into<String>) -> Self {
40 Self {
41 bindings: Vec::new(),
42 focused_index: None,
43 label: label.into(),
44 focus: FocusState::Normal,
45 item_schema: None,
46 display_field: None,
47 }
48 }
49
50 pub fn with_bindings(mut self, value: &Value) -> Self {
52 if let Some(arr) = value.as_array() {
53 self.bindings = arr.clone();
54 }
55 self
56 }
57
58 pub fn with_focus(mut self, focus: FocusState) -> Self {
60 self.focus = focus;
61 self
62 }
63
64 pub fn with_item_schema(
66 mut self,
67 schema: crate::view::settings::schema::SettingSchema,
68 ) -> Self {
69 self.item_schema = Some(Box::new(schema));
70 self
71 }
72
73 pub fn with_display_field(mut self, field: String) -> Self {
75 self.display_field = Some(field);
76 self
77 }
78
79 pub fn is_enabled(&self) -> bool {
81 self.focus != FocusState::Disabled
82 }
83
84 pub fn to_value(&self) -> Value {
86 Value::Array(self.bindings.clone())
87 }
88
89 pub fn focused_binding(&self) -> Option<&Value> {
91 self.focused_index.and_then(|idx| self.bindings.get(idx))
92 }
93
94 pub fn focus_next(&mut self) {
96 match self.focused_index {
97 None => {
98 if !self.bindings.is_empty() {
100 self.focused_index = Some(0);
101 }
102 }
103 Some(idx) if idx + 1 < self.bindings.len() => {
104 self.focused_index = Some(idx + 1);
105 }
106 Some(_) => {
107 self.focused_index = None;
109 }
110 }
111 }
112
113 pub fn focus_prev(&mut self) {
115 match self.focused_index {
116 None => {
117 if !self.bindings.is_empty() {
119 self.focused_index = Some(self.bindings.len() - 1);
120 }
121 }
122 Some(0) => {
123 }
125 Some(idx) => {
126 self.focused_index = Some(idx - 1);
127 }
128 }
129 }
130
131 pub fn remove_focused(&mut self) {
133 if let Some(idx) = self.focused_index {
134 if idx < self.bindings.len() {
135 self.bindings.remove(idx);
136 if self.bindings.is_empty() {
138 self.focused_index = None;
139 } else if idx >= self.bindings.len() {
140 self.focused_index = Some(self.bindings.len() - 1);
141 }
142 }
143 }
144 }
145
146 pub fn remove_binding(&mut self, index: usize) {
148 if index < self.bindings.len() {
149 self.bindings.remove(index);
150 if let Some(focused) = self.focused_index {
152 if focused >= self.bindings.len() {
153 self.focused_index = if self.bindings.is_empty() {
154 None
155 } else {
156 Some(self.bindings.len() - 1)
157 };
158 }
159 }
160 }
161 }
162
163 pub fn add_binding(&mut self, binding: Value) {
165 self.bindings.push(binding);
166 }
167
168 pub fn update_binding(&mut self, index: usize, binding: Value) {
170 if index < self.bindings.len() {
171 self.bindings[index] = binding;
172 }
173 }
174
175 pub fn focus_entry(&mut self, index: usize) {
177 if index < self.bindings.len() {
178 self.focused_index = Some(index);
179 }
180 }
181
182 pub fn focus_add_row(&mut self) {
184 self.focused_index = None;
185 }
186}
187
188#[derive(Debug, Clone, Copy)]
190pub struct KeybindingListColors {
191 pub label_fg: Color,
192 pub key_fg: Color,
193 pub action_fg: Color,
194 pub row_bg: Color,
200 pub focused_bg: Color,
202 pub focused_fg: Color,
204 pub delete_fg: Color,
205 pub add_fg: Color,
206}
207
208impl Default for KeybindingListColors {
209 fn default() -> Self {
210 Self {
211 label_fg: Color::White,
212 key_fg: Color::Yellow,
213 action_fg: Color::Cyan,
214 row_bg: Color::Reset,
215 focused_bg: Color::DarkGray,
216 focused_fg: Color::White,
217 delete_fg: Color::Red,
218 add_fg: Color::Green,
219 }
220 }
221}
222
223#[derive(Debug, Clone, Default)]
225pub struct KeybindingListLayout {
226 pub entry_rects: Vec<(usize, Rect)>,
228 pub delete_rects: Vec<Rect>,
229 pub add_rect: Option<Rect>,
230}
231
232impl KeybindingListLayout {
233 pub fn hit_test(&self, x: u16, y: u16) -> Option<KeybindingListHit> {
235 for (idx, rect) in self.delete_rects.iter().enumerate() {
237 if y == rect.y && x >= rect.x && x < rect.x + rect.width {
238 return Some(KeybindingListHit::DeleteButton(idx));
239 }
240 }
241
242 for &(data_idx, rect) in self.entry_rects.iter() {
244 if y == rect.y && x >= rect.x && x < rect.x + rect.width {
245 return Some(KeybindingListHit::Entry(data_idx));
246 }
247 }
248
249 if let Some(ref add_rect) = self.add_rect {
251 if y == add_rect.y && x >= add_rect.x && x < add_rect.x + add_rect.width {
252 return Some(KeybindingListHit::AddRow);
253 }
254 }
255
256 None
257 }
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
262pub enum KeybindingListHit {
263 Entry(usize),
265 DeleteButton(usize),
267 AddRow,
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_keybinding_list_state_new() {
277 let state = KeybindingListState::new("Keybindings");
278 assert_eq!(state.label, "Keybindings");
279 assert!(state.bindings.is_empty());
280 assert!(state.focused_index.is_none());
281 }
282
283 #[test]
284 fn test_keybinding_list_navigation() {
285 let mut state = KeybindingListState::new("Test");
286 state.add_binding(serde_json::json!({"key": "a", "action": "test1"}));
287 state.add_binding(serde_json::json!({"key": "b", "action": "test2"}));
288
289 assert!(state.focused_index.is_none());
291
292 state.focus_next();
294 assert_eq!(state.focused_index, Some(0));
295
296 state.focus_next();
297 assert_eq!(state.focused_index, Some(1));
298
299 state.focus_next();
301 assert!(state.focused_index.is_none());
302
303 state.focus_prev();
305 assert_eq!(state.focused_index, Some(1));
306
307 state.focus_prev();
308 assert_eq!(state.focused_index, Some(0));
309
310 state.focus_prev();
312 assert_eq!(state.focused_index, Some(0));
313 }
314
315 #[test]
316 fn test_keybinding_list_remove() {
317 let mut state = KeybindingListState::new("Test");
318 state.add_binding(serde_json::json!({"key": "a", "action": "test1"}));
319 state.add_binding(serde_json::json!({"key": "b", "action": "test2"}));
320 state.focus_entry(0);
321
322 state.remove_focused();
323 assert_eq!(state.bindings.len(), 1);
324 assert_eq!(state.focused_index, Some(0));
325 }
326
327 #[test]
328 fn test_keybinding_list_hit_test() {
329 let layout = KeybindingListLayout {
330 entry_rects: vec![(0, Rect::new(2, 1, 40, 1)), (1, Rect::new(2, 2, 40, 1))],
331 delete_rects: vec![Rect::new(38, 1, 3, 1), Rect::new(38, 2, 3, 1)],
332 add_rect: Some(Rect::new(2, 3, 40, 1)),
333 };
334
335 assert_eq!(
336 layout.hit_test(38, 1),
337 Some(KeybindingListHit::DeleteButton(0))
338 );
339 assert_eq!(layout.hit_test(10, 1), Some(KeybindingListHit::Entry(0)));
340 assert_eq!(layout.hit_test(10, 2), Some(KeybindingListHit::Entry(1)));
341 assert_eq!(layout.hit_test(10, 3), Some(KeybindingListHit::AddRow));
342 assert_eq!(layout.hit_test(0, 0), None);
343 }
344}