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 let target = if let serde_json::Value::Array(arr) = value {
90 arr.first()
91 } else {
92 Some(value)
93 };
94 if let Some(target) = target {
95 if let Some(v) = target.pointer(field) {
96 return match v {
97 serde_json::Value::String(s) => s.clone(),
98 serde_json::Value::Bool(b) => b.to_string(),
99 serde_json::Value::Number(n) => n.to_string(),
100 serde_json::Value::Null => "null".to_string(),
101 serde_json::Value::Array(arr) => format!("[{} items]", arr.len()),
102 serde_json::Value::Object(obj) => format!("{{{} fields}}", obj.len()),
103 };
104 }
105 }
106 }
107 match value {
109 serde_json::Value::Object(obj) => {
110 let n = obj.len();
111 if n == 1 {
112 "1 field".to_string()
113 } else {
114 format!("{n} fields")
115 }
116 }
117 serde_json::Value::Array(arr) => {
118 let n = arr.len();
119 if n == 1 {
120 "1 item".to_string()
121 } else {
122 format!("{n} items")
123 }
124 }
125 other => other.to_string(),
126 }
127 }
128
129 pub fn with_entries(mut self, value: &serde_json::Value) -> Self {
131 if let Some(obj) = value.as_object() {
132 self.entries = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
133 self.entries.sort_by(|a, b| a.0.cmp(&b.0));
135 if !self.entries.is_empty() {
137 self.focused_entry = Some(0);
138 }
139 }
140 self
141 }
142
143 pub fn with_value_schema(
145 mut self,
146 schema: crate::view::settings::schema::SettingSchema,
147 ) -> Self {
148 self.value_schema = Some(Box::new(schema));
149 self
150 }
151
152 pub fn with_focus(mut self, focus: FocusState) -> Self {
154 self.focus = focus;
155 self
156 }
157
158 pub fn is_enabled(&self) -> bool {
160 self.focus != FocusState::Disabled
161 }
162
163 pub fn add_entry(&mut self, key: String, value: serde_json::Value) {
165 if self.focus == FocusState::Disabled || key.is_empty() {
166 return;
167 }
168 if self.entries.iter().any(|(k, _)| k == &key) {
170 return;
171 }
172 self.entries.push((key, value));
173 self.entries.sort_by(|a, b| a.0.cmp(&b.0));
174 }
175
176 pub fn add_entry_from_input(&mut self) {
178 if self.new_key_text.is_empty() {
179 return;
180 }
181 let key = std::mem::take(&mut self.new_key_text);
182 self.cursor = 0;
183 self.add_entry(key, serde_json::json!({}));
185 }
186
187 pub fn remove_entry(&mut self, index: usize) {
189 if self.focus == FocusState::Disabled || index >= self.entries.len() {
190 return;
191 }
192 self.entries.remove(index);
193 if let Some(focused) = self.focused_entry {
195 if focused >= self.entries.len() {
196 self.focused_entry = if self.entries.is_empty() {
197 None
198 } else {
199 Some(self.entries.len() - 1)
200 };
201 }
202 }
203 self.expanded.retain(|&idx| idx != index);
205 self.expanded = self
207 .expanded
208 .iter()
209 .map(|&idx| if idx > index { idx - 1 } else { idx })
210 .collect();
211 }
212
213 pub fn focus_entry(&mut self, index: usize) {
215 if index < self.entries.len() {
216 self.focused_entry = Some(index);
217 }
218 }
219
220 pub fn focus_new_entry(&mut self) {
222 self.focused_entry = None;
223 self.cursor = self.new_key_text.len();
224 }
225
226 pub fn toggle_expand(&mut self, index: usize) {
228 if index >= self.entries.len() {
229 return;
230 }
231 if let Some(pos) = self.expanded.iter().position(|&i| i == index) {
232 self.expanded.remove(pos);
233 } else {
234 self.expanded.push(index);
235 }
236 }
237
238 pub fn is_expanded(&self, index: usize) -> bool {
240 self.expanded.contains(&index)
241 }
242
243 pub fn insert(&mut self, c: char) {
245 if self.focus == FocusState::Disabled || self.focused_entry.is_some() {
246 return;
247 }
248 self.new_key_text.insert(self.cursor, c);
249 self.cursor += 1;
250 }
251
252 pub fn backspace(&mut self) {
254 if self.focus == FocusState::Disabled || self.focused_entry.is_some() || self.cursor == 0 {
255 return;
256 }
257 self.cursor -= 1;
258 self.new_key_text.remove(self.cursor);
259 }
260
261 pub fn move_left(&mut self) {
263 if self.cursor > 0 {
264 self.cursor -= 1;
265 }
266 }
267
268 pub fn move_right(&mut self) {
270 if self.cursor < self.new_key_text.len() {
271 self.cursor += 1;
272 }
273 }
274
275 pub fn focus_prev(&mut self) -> bool {
277 match self.focused_entry {
278 Some(0) => false, Some(idx) => {
280 self.focused_entry = Some(idx - 1);
281 true
282 }
283 None if !self.entries.is_empty() => {
284 self.focused_entry = Some(self.entries.len() - 1);
285 true
286 }
287 None => false, }
289 }
290
291 pub fn focus_next(&mut self) -> bool {
293 match self.focused_entry {
294 Some(idx) if idx + 1 < self.entries.len() => {
295 self.focused_entry = Some(idx + 1);
296 true
297 }
298 Some(_) if self.no_add => {
299 false
301 }
302 Some(_) => {
303 self.focused_entry = None;
305 self.cursor = self.new_key_text.len();
306 true
307 }
308 None => false, }
310 }
311
312 pub fn init_focus(&mut self, from_above: bool) {
315 if from_above && !self.entries.is_empty() {
316 self.focused_entry = Some(0);
317 } else if !from_above && !self.entries.is_empty() && self.no_add {
318 self.focused_entry = Some(self.entries.len() - 1);
320 } else if !self.no_add {
321 self.focused_entry = None;
323 self.cursor = self.new_key_text.len();
324 } else if !self.entries.is_empty() {
325 self.focused_entry = Some(0);
327 } else {
328 self.focused_entry = None;
330 }
331 }
332
333 pub fn len(&self) -> usize {
335 self.entries.len()
336 }
337
338 pub fn is_empty(&self) -> bool {
340 self.entries.is_empty()
341 }
342
343 pub fn to_value(&self) -> serde_json::Value {
345 let map: serde_json::Map<String, serde_json::Value> = self
346 .entries
347 .iter()
348 .map(|(k, v)| (k.clone(), v.clone()))
349 .collect();
350 serde_json::Value::Object(map)
351 }
352}
353
354#[derive(Debug, Clone, Copy)]
356pub struct MapColors {
357 pub label: Color,
358 pub key: Color,
359 pub value_preview: Color,
360 pub border: Color,
361 pub remove_button: Color,
362 pub add_button: Color,
363 pub focused: Color,
365 pub focused_fg: Color,
367 pub cursor: Color,
368 pub disabled: Color,
369 pub expand_arrow: Color,
370}
371
372impl Default for MapColors {
373 fn default() -> Self {
374 Self {
375 label: Color::White,
376 key: Color::Cyan,
377 value_preview: Color::Gray,
378 border: Color::Gray,
379 remove_button: Color::Red,
380 add_button: Color::Green,
381 focused: Color::Yellow,
382 focused_fg: Color::Black,
383 cursor: Color::Yellow,
384 disabled: Color::DarkGray,
385 expand_arrow: Color::White,
386 }
387 }
388}
389
390impl MapColors {
391 pub fn from_theme(theme: &crate::view::theme::Theme) -> Self {
392 Self {
393 label: theme.editor_fg,
394 key: theme.help_key_fg,
396 value_preview: theme.line_number_fg,
397 border: theme.line_number_fg,
398 remove_button: theme.diagnostic_error_fg,
399 add_button: theme.diagnostic_info_fg,
400 focused: theme.settings_selected_bg,
402 focused_fg: theme.settings_selected_fg,
403 cursor: theme.cursor,
404 disabled: theme.line_number_fg,
405 expand_arrow: theme.editor_fg,
406 }
407 }
408}
409
410#[derive(Debug, Clone, Default)]
412pub struct MapLayout {
413 pub full_area: Rect,
414 pub entry_areas: Vec<MapEntryLayout>,
415 pub add_row_area: Option<Rect>,
416}
417
418#[derive(Debug, Clone)]
420pub struct MapEntryLayout {
421 pub index: usize,
422 pub row_area: Rect,
423 pub expand_area: Rect,
424 pub key_area: Rect,
425 pub remove_area: Rect,
426}
427
428impl MapLayout {
429 pub fn hit_test(&self, x: u16, y: u16) -> Option<MapHit> {
431 for entry in &self.entry_areas {
433 if y == entry.row_area.y {
434 if x >= entry.remove_area.x && x < entry.remove_area.x + entry.remove_area.width {
435 return Some(MapHit::RemoveButton(entry.index));
436 }
437 if x >= entry.expand_area.x && x < entry.expand_area.x + entry.expand_area.width {
438 return Some(MapHit::ExpandArrow(entry.index));
439 }
440 if x >= entry.key_area.x && x < entry.key_area.x + entry.key_area.width {
441 return Some(MapHit::EntryKey(entry.index));
442 }
443 }
444 }
445
446 if let Some(ref add_row) = self.add_row_area {
448 if y == add_row.y && x >= add_row.x && x < add_row.x + add_row.width {
449 return Some(MapHit::AddRow);
450 }
451 }
452
453 None
454 }
455}
456
457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
459pub enum MapHit {
460 ExpandArrow(usize),
462 EntryKey(usize),
464 RemoveButton(usize),
466 AddRow,
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 fn test_map_state_new() {
476 let state = MapState::new("Test");
477 assert_eq!(state.label, "Test");
478 assert!(state.entries.is_empty());
479 assert!(state.focused_entry.is_none());
480 }
481
482 #[test]
483 fn test_map_state_add_entry() {
484 let mut state = MapState::new("Test");
485 state.add_entry("key1".to_string(), serde_json::json!({"foo": "bar"}));
486 assert_eq!(state.entries.len(), 1);
487 assert_eq!(state.entries[0].0, "key1");
488 }
489
490 #[test]
491 fn test_map_state_remove_entry() {
492 let mut state = MapState::new("Test");
493 state.add_entry("a".to_string(), serde_json::json!({}));
494 state.add_entry("b".to_string(), serde_json::json!({}));
495 state.remove_entry(0);
496 assert_eq!(state.entries.len(), 1);
497 assert_eq!(state.entries[0].0, "b");
498 }
499
500 #[test]
501 fn test_map_state_navigation() {
502 let mut state = MapState::new("Test").with_focus(FocusState::Focused);
503 state.add_entry("a".to_string(), serde_json::json!({}));
504 state.add_entry("b".to_string(), serde_json::json!({}));
505
506 assert!(state.focused_entry.is_none());
508
509 state.focus_prev();
511 assert_eq!(state.focused_entry, Some(1));
512
513 state.focus_prev();
515 assert_eq!(state.focused_entry, Some(0));
516
517 state.focus_next();
519 assert_eq!(state.focused_entry, Some(1));
520
521 state.focus_next();
523 assert!(state.focused_entry.is_none());
524 }
525
526 #[test]
527 fn test_map_state_expand() {
528 let mut state = MapState::new("Test");
529 state.add_entry("key1".to_string(), serde_json::json!({}));
530
531 assert!(!state.is_expanded(0));
532 state.toggle_expand(0);
533 assert!(state.is_expanded(0));
534 state.toggle_expand(0);
535 assert!(!state.is_expanded(0));
536 }
537
538 #[test]
539 fn test_map_hit_test() {
540 let layout = MapLayout {
541 full_area: Rect::new(0, 0, 50, 5),
542 entry_areas: vec![MapEntryLayout {
543 index: 0,
544 row_area: Rect::new(0, 1, 50, 1),
545 expand_area: Rect::new(2, 1, 1, 1),
546 key_area: Rect::new(4, 1, 10, 1),
547 remove_area: Rect::new(40, 1, 3, 1),
548 }],
549 add_row_area: Some(Rect::new(0, 2, 50, 1)),
550 };
551
552 assert_eq!(layout.hit_test(2, 1), Some(MapHit::ExpandArrow(0)));
553 assert_eq!(layout.hit_test(5, 1), Some(MapHit::EntryKey(0)));
554 assert_eq!(layout.hit_test(40, 1), Some(MapHit::RemoveButton(0)));
555 assert_eq!(layout.hit_test(5, 2), Some(MapHit::AddRow));
556 assert_eq!(layout.hit_test(13, 2), Some(MapHit::AddRow));
557 assert_eq!(layout.hit_test(0, 0), None);
558 }
559}