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