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 focused_bg: Color,
196 pub focused_fg: Color,
198 pub delete_fg: Color,
199 pub add_fg: Color,
200}
201
202impl Default for KeybindingListColors {
203 fn default() -> Self {
204 Self {
205 label_fg: Color::White,
206 key_fg: Color::Yellow,
207 action_fg: Color::Cyan,
208 focused_bg: Color::DarkGray,
209 focused_fg: Color::White,
210 delete_fg: Color::Red,
211 add_fg: Color::Green,
212 }
213 }
214}
215
216#[derive(Debug, Clone, Default)]
218pub struct KeybindingListLayout {
219 pub entry_rects: Vec<Rect>,
220 pub delete_rects: Vec<Rect>,
221 pub add_rect: Option<Rect>,
222}
223
224impl KeybindingListLayout {
225 pub fn hit_test(&self, x: u16, y: u16) -> Option<KeybindingListHit> {
227 for (idx, rect) in self.delete_rects.iter().enumerate() {
229 if y == rect.y && x >= rect.x && x < rect.x + rect.width {
230 return Some(KeybindingListHit::DeleteButton(idx));
231 }
232 }
233
234 for (idx, rect) in self.entry_rects.iter().enumerate() {
236 if y == rect.y && x >= rect.x && x < rect.x + rect.width {
237 return Some(KeybindingListHit::Entry(idx));
238 }
239 }
240
241 if let Some(ref add_rect) = self.add_rect {
243 if y == add_rect.y && x >= add_rect.x && x < add_rect.x + add_rect.width {
244 return Some(KeybindingListHit::AddRow);
245 }
246 }
247
248 None
249 }
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum KeybindingListHit {
255 Entry(usize),
257 DeleteButton(usize),
259 AddRow,
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_keybinding_list_state_new() {
269 let state = KeybindingListState::new("Keybindings");
270 assert_eq!(state.label, "Keybindings");
271 assert!(state.bindings.is_empty());
272 assert!(state.focused_index.is_none());
273 }
274
275 #[test]
276 fn test_keybinding_list_navigation() {
277 let mut state = KeybindingListState::new("Test");
278 state.add_binding(serde_json::json!({"key": "a", "action": "test1"}));
279 state.add_binding(serde_json::json!({"key": "b", "action": "test2"}));
280
281 assert!(state.focused_index.is_none());
283
284 state.focus_next();
286 assert_eq!(state.focused_index, Some(0));
287
288 state.focus_next();
289 assert_eq!(state.focused_index, Some(1));
290
291 state.focus_next();
293 assert!(state.focused_index.is_none());
294
295 state.focus_prev();
297 assert_eq!(state.focused_index, Some(1));
298
299 state.focus_prev();
300 assert_eq!(state.focused_index, Some(0));
301
302 state.focus_prev();
304 assert_eq!(state.focused_index, Some(0));
305 }
306
307 #[test]
308 fn test_keybinding_list_remove() {
309 let mut state = KeybindingListState::new("Test");
310 state.add_binding(serde_json::json!({"key": "a", "action": "test1"}));
311 state.add_binding(serde_json::json!({"key": "b", "action": "test2"}));
312 state.focus_entry(0);
313
314 state.remove_focused();
315 assert_eq!(state.bindings.len(), 1);
316 assert_eq!(state.focused_index, Some(0));
317 }
318
319 #[test]
320 fn test_keybinding_list_hit_test() {
321 let layout = KeybindingListLayout {
322 entry_rects: vec![Rect::new(2, 1, 40, 1), Rect::new(2, 2, 40, 1)],
323 delete_rects: vec![Rect::new(38, 1, 3, 1), Rect::new(38, 2, 3, 1)],
324 add_rect: Some(Rect::new(2, 3, 40, 1)),
325 };
326
327 assert_eq!(
328 layout.hit_test(38, 1),
329 Some(KeybindingListHit::DeleteButton(0))
330 );
331 assert_eq!(layout.hit_test(10, 1), Some(KeybindingListHit::Entry(0)));
332 assert_eq!(layout.hit_test(10, 2), Some(KeybindingListHit::Entry(1)));
333 assert_eq!(layout.hit_test(10, 3), Some(KeybindingListHit::AddRow));
334 assert_eq!(layout.hit_test(0, 0), None);
335 }
336}