1mod input;
21mod render;
22
23use ratatui::layout::Rect;
24use ratatui::style::Color;
25
26pub use input::MapEvent;
27pub use render::render_map;
28
29use super::FocusState;
30
31#[derive(Debug, Clone)]
33pub struct MapState {
34 pub entries: Vec<(String, serde_json::Value)>,
36 pub focused_entry: Option<usize>,
38 pub new_key_text: String,
40 pub cursor: usize,
42 pub label: String,
44 pub focus: FocusState,
46 pub expanded: Vec<usize>,
48 pub value_schema: Option<Box<crate::view::settings::schema::SettingSchema>>,
50 pub display_field: Option<String>,
52 pub no_add: bool,
54}
55
56impl MapState {
57 pub fn new(label: impl Into<String>) -> Self {
59 Self {
60 entries: Vec::new(),
61 focused_entry: None,
62 new_key_text: String::new(),
63 cursor: 0,
64 label: label.into(),
65 focus: FocusState::Normal,
66 expanded: Vec::new(),
67 value_schema: None,
68 display_field: None,
69 no_add: false,
70 }
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 with_no_add(mut self, no_add: bool) -> Self {
81 self.no_add = no_add;
82 self
83 }
84
85 pub fn get_display_value(&self, value: &serde_json::Value) -> String {
87 if let Some(ref field) = self.display_field {
88 if let Some(v) = value.pointer(field) {
89 return match v {
90 serde_json::Value::String(s) => s.clone(),
91 serde_json::Value::Bool(b) => b.to_string(),
92 serde_json::Value::Number(n) => n.to_string(),
93 serde_json::Value::Null => "null".to_string(),
94 serde_json::Value::Array(arr) => format!("[{} items]", arr.len()),
95 serde_json::Value::Object(obj) => format!("{{{} fields}}", obj.len()),
96 };
97 }
98 }
99 match value {
101 serde_json::Value::Object(obj) => format!("{{{} fields}}", obj.len()),
102 serde_json::Value::Array(arr) => format!("[{} items]", arr.len()),
103 other => other.to_string(),
104 }
105 }
106
107 pub fn with_entries(mut self, value: &serde_json::Value) -> Self {
109 if let Some(obj) = value.as_object() {
110 self.entries = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
111 self.entries.sort_by(|a, b| a.0.cmp(&b.0));
113 if !self.entries.is_empty() {
115 self.focused_entry = Some(0);
116 }
117 }
118 self
119 }
120
121 pub fn with_value_schema(
123 mut self,
124 schema: crate::view::settings::schema::SettingSchema,
125 ) -> Self {
126 self.value_schema = Some(Box::new(schema));
127 self
128 }
129
130 pub fn with_focus(mut self, focus: FocusState) -> Self {
132 self.focus = focus;
133 self
134 }
135
136 pub fn is_enabled(&self) -> bool {
138 self.focus != FocusState::Disabled
139 }
140
141 pub fn add_entry(&mut self, key: String, value: serde_json::Value) {
143 if self.focus == FocusState::Disabled || key.is_empty() {
144 return;
145 }
146 if self.entries.iter().any(|(k, _)| k == &key) {
148 return;
149 }
150 self.entries.push((key, value));
151 self.entries.sort_by(|a, b| a.0.cmp(&b.0));
152 }
153
154 pub fn add_entry_from_input(&mut self) {
156 if self.new_key_text.is_empty() {
157 return;
158 }
159 let key = std::mem::take(&mut self.new_key_text);
160 self.cursor = 0;
161 self.add_entry(key, serde_json::json!({}));
163 }
164
165 pub fn remove_entry(&mut self, index: usize) {
167 if self.focus == FocusState::Disabled || index >= self.entries.len() {
168 return;
169 }
170 self.entries.remove(index);
171 if let Some(focused) = self.focused_entry {
173 if focused >= self.entries.len() {
174 self.focused_entry = if self.entries.is_empty() {
175 None
176 } else {
177 Some(self.entries.len() - 1)
178 };
179 }
180 }
181 self.expanded.retain(|&idx| idx != index);
183 self.expanded = self
185 .expanded
186 .iter()
187 .map(|&idx| if idx > index { idx - 1 } else { idx })
188 .collect();
189 }
190
191 pub fn focus_entry(&mut self, index: usize) {
193 if index < self.entries.len() {
194 self.focused_entry = Some(index);
195 }
196 }
197
198 pub fn focus_new_entry(&mut self) {
200 self.focused_entry = None;
201 self.cursor = self.new_key_text.len();
202 }
203
204 pub fn toggle_expand(&mut self, index: usize) {
206 if index >= self.entries.len() {
207 return;
208 }
209 if let Some(pos) = self.expanded.iter().position(|&i| i == index) {
210 self.expanded.remove(pos);
211 } else {
212 self.expanded.push(index);
213 }
214 }
215
216 pub fn is_expanded(&self, index: usize) -> bool {
218 self.expanded.contains(&index)
219 }
220
221 pub fn insert(&mut self, c: char) {
223 if self.focus == FocusState::Disabled || self.focused_entry.is_some() {
224 return;
225 }
226 self.new_key_text.insert(self.cursor, c);
227 self.cursor += 1;
228 }
229
230 pub fn backspace(&mut self) {
232 if self.focus == FocusState::Disabled || self.focused_entry.is_some() || self.cursor == 0 {
233 return;
234 }
235 self.cursor -= 1;
236 self.new_key_text.remove(self.cursor);
237 }
238
239 pub fn move_left(&mut self) {
241 if self.cursor > 0 {
242 self.cursor -= 1;
243 }
244 }
245
246 pub fn move_right(&mut self) {
248 if self.cursor < self.new_key_text.len() {
249 self.cursor += 1;
250 }
251 }
252
253 pub fn focus_prev(&mut self) -> bool {
255 match self.focused_entry {
256 Some(0) => false, Some(idx) => {
258 self.focused_entry = Some(idx - 1);
259 true
260 }
261 None if !self.entries.is_empty() => {
262 self.focused_entry = Some(self.entries.len() - 1);
263 true
264 }
265 None => false, }
267 }
268
269 pub fn focus_next(&mut self) -> bool {
271 match self.focused_entry {
272 Some(idx) if idx + 1 < self.entries.len() => {
273 self.focused_entry = Some(idx + 1);
274 true
275 }
276 Some(_) if self.no_add => {
277 false
279 }
280 Some(_) => {
281 self.focused_entry = None;
283 self.cursor = self.new_key_text.len();
284 true
285 }
286 None => false, }
288 }
289
290 pub fn init_focus(&mut self, from_above: bool) {
293 if from_above && !self.entries.is_empty() {
294 self.focused_entry = Some(0);
295 } else if !from_above && !self.entries.is_empty() && self.no_add {
296 self.focused_entry = Some(self.entries.len() - 1);
298 } else if !self.no_add {
299 self.focused_entry = None;
301 self.cursor = self.new_key_text.len();
302 } else if !self.entries.is_empty() {
303 self.focused_entry = Some(0);
305 } else {
306 self.focused_entry = None;
308 }
309 }
310
311 pub fn len(&self) -> usize {
313 self.entries.len()
314 }
315
316 pub fn is_empty(&self) -> bool {
318 self.entries.is_empty()
319 }
320
321 pub fn to_value(&self) -> serde_json::Value {
323 let map: serde_json::Map<String, serde_json::Value> = self
324 .entries
325 .iter()
326 .map(|(k, v)| (k.clone(), v.clone()))
327 .collect();
328 serde_json::Value::Object(map)
329 }
330}
331
332#[derive(Debug, Clone, Copy)]
334pub struct MapColors {
335 pub label: Color,
336 pub key: Color,
337 pub value_preview: Color,
338 pub border: Color,
339 pub remove_button: Color,
340 pub add_button: Color,
341 pub focused: Color,
343 pub focused_fg: Color,
345 pub cursor: Color,
346 pub disabled: Color,
347 pub expand_arrow: Color,
348}
349
350impl Default for MapColors {
351 fn default() -> Self {
352 Self {
353 label: Color::White,
354 key: Color::Cyan,
355 value_preview: Color::Gray,
356 border: Color::Gray,
357 remove_button: Color::Red,
358 add_button: Color::Green,
359 focused: Color::Yellow,
360 focused_fg: Color::Black,
361 cursor: Color::Yellow,
362 disabled: Color::DarkGray,
363 expand_arrow: Color::White,
364 }
365 }
366}
367
368impl MapColors {
369 pub fn from_theme(theme: &crate::view::theme::Theme) -> Self {
370 Self {
371 label: theme.editor_fg,
372 key: theme.help_key_fg,
374 value_preview: theme.line_number_fg,
375 border: theme.line_number_fg,
376 remove_button: theme.diagnostic_error_fg,
377 add_button: theme.diagnostic_info_fg,
378 focused: theme.settings_selected_bg,
380 focused_fg: theme.settings_selected_fg,
381 cursor: theme.cursor,
382 disabled: theme.line_number_fg,
383 expand_arrow: theme.editor_fg,
384 }
385 }
386}
387
388#[derive(Debug, Clone, Default)]
390pub struct MapLayout {
391 pub full_area: Rect,
392 pub entry_areas: Vec<MapEntryLayout>,
393 pub add_row_area: Option<Rect>,
394}
395
396#[derive(Debug, Clone)]
398pub struct MapEntryLayout {
399 pub index: usize,
400 pub row_area: Rect,
401 pub expand_area: Rect,
402 pub key_area: Rect,
403 pub remove_area: Rect,
404}
405
406impl MapLayout {
407 pub fn hit_test(&self, x: u16, y: u16) -> Option<MapHit> {
409 for entry in &self.entry_areas {
411 if y == entry.row_area.y {
412 if x >= entry.remove_area.x && x < entry.remove_area.x + entry.remove_area.width {
413 return Some(MapHit::RemoveButton(entry.index));
414 }
415 if x >= entry.expand_area.x && x < entry.expand_area.x + entry.expand_area.width {
416 return Some(MapHit::ExpandArrow(entry.index));
417 }
418 if x >= entry.key_area.x && x < entry.key_area.x + entry.key_area.width {
419 return Some(MapHit::EntryKey(entry.index));
420 }
421 }
422 }
423
424 if let Some(ref add_row) = self.add_row_area {
426 if y == add_row.y && x >= add_row.x && x < add_row.x + add_row.width {
427 return Some(MapHit::AddRow);
428 }
429 }
430
431 None
432 }
433}
434
435#[derive(Debug, Clone, Copy, PartialEq, Eq)]
437pub enum MapHit {
438 ExpandArrow(usize),
440 EntryKey(usize),
442 RemoveButton(usize),
444 AddRow,
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn test_map_state_new() {
454 let state = MapState::new("Test");
455 assert_eq!(state.label, "Test");
456 assert!(state.entries.is_empty());
457 assert!(state.focused_entry.is_none());
458 }
459
460 #[test]
461 fn test_map_state_add_entry() {
462 let mut state = MapState::new("Test");
463 state.add_entry("key1".to_string(), serde_json::json!({"foo": "bar"}));
464 assert_eq!(state.entries.len(), 1);
465 assert_eq!(state.entries[0].0, "key1");
466 }
467
468 #[test]
469 fn test_map_state_remove_entry() {
470 let mut state = MapState::new("Test");
471 state.add_entry("a".to_string(), serde_json::json!({}));
472 state.add_entry("b".to_string(), serde_json::json!({}));
473 state.remove_entry(0);
474 assert_eq!(state.entries.len(), 1);
475 assert_eq!(state.entries[0].0, "b");
476 }
477
478 #[test]
479 fn test_map_state_navigation() {
480 let mut state = MapState::new("Test").with_focus(FocusState::Focused);
481 state.add_entry("a".to_string(), serde_json::json!({}));
482 state.add_entry("b".to_string(), serde_json::json!({}));
483
484 assert!(state.focused_entry.is_none());
486
487 state.focus_prev();
489 assert_eq!(state.focused_entry, Some(1));
490
491 state.focus_prev();
493 assert_eq!(state.focused_entry, Some(0));
494
495 state.focus_next();
497 assert_eq!(state.focused_entry, Some(1));
498
499 state.focus_next();
501 assert!(state.focused_entry.is_none());
502 }
503
504 #[test]
505 fn test_map_state_expand() {
506 let mut state = MapState::new("Test");
507 state.add_entry("key1".to_string(), serde_json::json!({}));
508
509 assert!(!state.is_expanded(0));
510 state.toggle_expand(0);
511 assert!(state.is_expanded(0));
512 state.toggle_expand(0);
513 assert!(!state.is_expanded(0));
514 }
515
516 #[test]
517 fn test_map_hit_test() {
518 let layout = MapLayout {
519 full_area: Rect::new(0, 0, 50, 5),
520 entry_areas: vec![MapEntryLayout {
521 index: 0,
522 row_area: Rect::new(0, 1, 50, 1),
523 expand_area: Rect::new(2, 1, 1, 1),
524 key_area: Rect::new(4, 1, 10, 1),
525 remove_area: Rect::new(40, 1, 3, 1),
526 }],
527 add_row_area: Some(Rect::new(0, 2, 50, 1)),
528 };
529
530 assert_eq!(layout.hit_test(2, 1), Some(MapHit::ExpandArrow(0)));
531 assert_eq!(layout.hit_test(5, 1), Some(MapHit::EntryKey(0)));
532 assert_eq!(layout.hit_test(40, 1), Some(MapHit::RemoveButton(0)));
533 assert_eq!(layout.hit_test(5, 2), Some(MapHit::AddRow));
534 assert_eq!(layout.hit_test(13, 2), Some(MapHit::AddRow));
535 assert_eq!(layout.hit_test(0, 0), None);
536 }
537}