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,
342 pub cursor: Color,
343 pub disabled: Color,
344 pub expand_arrow: Color,
345}
346
347impl Default for MapColors {
348 fn default() -> Self {
349 Self {
350 label: Color::White,
351 key: Color::Cyan,
352 value_preview: Color::Gray,
353 border: Color::Gray,
354 remove_button: Color::Red,
355 add_button: Color::Green,
356 focused: Color::Yellow,
357 cursor: Color::Yellow,
358 disabled: Color::DarkGray,
359 expand_arrow: Color::White,
360 }
361 }
362}
363
364impl MapColors {
365 pub fn from_theme(theme: &crate::view::theme::Theme) -> Self {
366 Self {
367 label: theme.editor_fg,
368 key: theme.menu_highlight_fg,
369 value_preview: theme.line_number_fg,
370 border: theme.line_number_fg,
371 remove_button: theme.diagnostic_error_fg,
372 add_button: theme.diagnostic_info_fg,
373 focused: theme.selection_bg,
374 cursor: theme.cursor,
375 disabled: theme.line_number_fg,
376 expand_arrow: theme.editor_fg,
377 }
378 }
379}
380
381#[derive(Debug, Clone, Default)]
383pub struct MapLayout {
384 pub full_area: Rect,
385 pub entry_areas: Vec<MapEntryLayout>,
386 pub add_row_area: Option<Rect>,
387}
388
389#[derive(Debug, Clone)]
391pub struct MapEntryLayout {
392 pub index: usize,
393 pub row_area: Rect,
394 pub expand_area: Rect,
395 pub key_area: Rect,
396 pub remove_area: Rect,
397}
398
399impl MapLayout {
400 pub fn hit_test(&self, x: u16, y: u16) -> Option<MapHit> {
402 for entry in &self.entry_areas {
404 if y == entry.row_area.y {
405 if x >= entry.remove_area.x && x < entry.remove_area.x + entry.remove_area.width {
406 return Some(MapHit::RemoveButton(entry.index));
407 }
408 if x >= entry.expand_area.x && x < entry.expand_area.x + entry.expand_area.width {
409 return Some(MapHit::ExpandArrow(entry.index));
410 }
411 if x >= entry.key_area.x && x < entry.key_area.x + entry.key_area.width {
412 return Some(MapHit::EntryKey(entry.index));
413 }
414 }
415 }
416
417 if let Some(ref add_row) = self.add_row_area {
419 if y == add_row.y && x >= add_row.x && x < add_row.x + add_row.width {
420 return Some(MapHit::AddRow);
421 }
422 }
423
424 None
425 }
426}
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq)]
430pub enum MapHit {
431 ExpandArrow(usize),
433 EntryKey(usize),
435 RemoveButton(usize),
437 AddRow,
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn test_map_state_new() {
447 let state = MapState::new("Test");
448 assert_eq!(state.label, "Test");
449 assert!(state.entries.is_empty());
450 assert!(state.focused_entry.is_none());
451 }
452
453 #[test]
454 fn test_map_state_add_entry() {
455 let mut state = MapState::new("Test");
456 state.add_entry("key1".to_string(), serde_json::json!({"foo": "bar"}));
457 assert_eq!(state.entries.len(), 1);
458 assert_eq!(state.entries[0].0, "key1");
459 }
460
461 #[test]
462 fn test_map_state_remove_entry() {
463 let mut state = MapState::new("Test");
464 state.add_entry("a".to_string(), serde_json::json!({}));
465 state.add_entry("b".to_string(), serde_json::json!({}));
466 state.remove_entry(0);
467 assert_eq!(state.entries.len(), 1);
468 assert_eq!(state.entries[0].0, "b");
469 }
470
471 #[test]
472 fn test_map_state_navigation() {
473 let mut state = MapState::new("Test").with_focus(FocusState::Focused);
474 state.add_entry("a".to_string(), serde_json::json!({}));
475 state.add_entry("b".to_string(), serde_json::json!({}));
476
477 assert!(state.focused_entry.is_none());
479
480 state.focus_prev();
482 assert_eq!(state.focused_entry, Some(1));
483
484 state.focus_prev();
486 assert_eq!(state.focused_entry, Some(0));
487
488 state.focus_next();
490 assert_eq!(state.focused_entry, Some(1));
491
492 state.focus_next();
494 assert!(state.focused_entry.is_none());
495 }
496
497 #[test]
498 fn test_map_state_expand() {
499 let mut state = MapState::new("Test");
500 state.add_entry("key1".to_string(), serde_json::json!({}));
501
502 assert!(!state.is_expanded(0));
503 state.toggle_expand(0);
504 assert!(state.is_expanded(0));
505 state.toggle_expand(0);
506 assert!(!state.is_expanded(0));
507 }
508
509 #[test]
510 fn test_map_hit_test() {
511 let layout = MapLayout {
512 full_area: Rect::new(0, 0, 50, 5),
513 entry_areas: vec![MapEntryLayout {
514 index: 0,
515 row_area: Rect::new(0, 1, 50, 1),
516 expand_area: Rect::new(2, 1, 1, 1),
517 key_area: Rect::new(4, 1, 10, 1),
518 remove_area: Rect::new(40, 1, 3, 1),
519 }],
520 add_row_area: Some(Rect::new(0, 2, 50, 1)),
521 };
522
523 assert_eq!(layout.hit_test(2, 1), Some(MapHit::ExpandArrow(0)));
524 assert_eq!(layout.hit_test(5, 1), Some(MapHit::EntryKey(0)));
525 assert_eq!(layout.hit_test(40, 1), Some(MapHit::RemoveButton(0)));
526 assert_eq!(layout.hit_test(5, 2), Some(MapHit::AddRow));
527 assert_eq!(layout.hit_test(13, 2), Some(MapHit::AddRow));
528 assert_eq!(layout.hit_test(0, 0), None);
529 }
530}