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 add_fg: Color,
205}
206
207impl Default for KeybindingListColors {
208 fn default() -> Self {
209 Self {
210 label_fg: Color::White,
211 key_fg: Color::Yellow,
212 action_fg: Color::Cyan,
213 row_bg: Color::Reset,
214 focused_bg: Color::DarkGray,
215 focused_fg: Color::White,
216 add_fg: Color::Green,
217 }
218 }
219}
220
221#[derive(Debug, Clone, Default)]
223pub struct KeybindingListLayout {
224 pub entry_rects: Vec<(usize, Rect)>,
226 pub add_rect: Option<Rect>,
227}
228
229impl KeybindingListLayout {
230 pub fn hit_test(&self, x: u16, y: u16) -> Option<KeybindingListHit> {
232 for &(data_idx, rect) in self.entry_rects.iter() {
234 if y == rect.y && x >= rect.x && x < rect.x + rect.width {
235 return Some(KeybindingListHit::Entry(data_idx));
236 }
237 }
238
239 if let Some(ref add_rect) = self.add_rect {
241 if y == add_rect.y && x >= add_rect.x && x < add_rect.x + add_rect.width {
242 return Some(KeybindingListHit::AddRow);
243 }
244 }
245
246 None
247 }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub enum KeybindingListHit {
253 Entry(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![(0, Rect::new(2, 1, 40, 1)), (1, Rect::new(2, 2, 40, 1))],
319 add_rect: Some(Rect::new(2, 3, 40, 1)),
320 };
321
322 assert_eq!(layout.hit_test(10, 1), Some(KeybindingListHit::Entry(0)));
323 assert_eq!(layout.hit_test(10, 2), Some(KeybindingListHit::Entry(1)));
324 assert_eq!(layout.hit_test(10, 3), Some(KeybindingListHit::AddRow));
325 assert_eq!(layout.hit_test(0, 0), None);
326 }
327}