fresh/view/controls/map_input/
render.rs

1//! Map control rendering functions
2
3use ratatui::layout::Rect;
4use ratatui::style::{Modifier, Style};
5use ratatui::text::{Line, Span};
6use ratatui::widgets::Paragraph;
7use ratatui::Frame;
8
9use super::{FocusState, MapColors, MapEntryLayout, MapLayout, MapState};
10
11/// Render a map control
12pub fn render_map(
13    frame: &mut Frame,
14    area: Rect,
15    state: &MapState,
16    colors: &MapColors,
17    key_width: u16,
18) -> MapLayout {
19    let empty_layout = MapLayout {
20        full_area: area,
21        entry_areas: Vec::new(),
22        add_row_area: None,
23    };
24
25    if area.height == 0 || area.width < 15 {
26        return empty_layout;
27    }
28
29    let label_color = match state.focus {
30        FocusState::Focused => colors.focused,
31        FocusState::Hovered => colors.focused,
32        FocusState::Disabled => colors.disabled,
33        FocusState::Normal => colors.label,
34    };
35
36    // Render label
37    let label_line = Line::from(vec![
38        Span::styled(&state.label, Style::default().fg(label_color)),
39        Span::raw(":"),
40    ]);
41    frame.render_widget(
42        Paragraph::new(label_line),
43        Rect::new(area.x, area.y, area.width, 1),
44    );
45
46    let mut entry_areas = Vec::new();
47    let mut y = area.y + 1;
48    let indent = 2u16;
49    let actual_key_width = key_width.min(area.width.saturating_sub(indent + 8));
50
51    // Render entries
52    for (idx, (key, value)) in state.entries.iter().enumerate() {
53        if y >= area.y + area.height {
54            break;
55        }
56
57        let is_focused = state.focused_entry == Some(idx) && state.focus == FocusState::Focused;
58        let is_expanded = state.is_expanded(idx);
59
60        let arrow = if is_expanded { "▼" } else { ">" };
61
62        // Value preview using display_field if available
63        let value_preview = state.get_display_value(value);
64        // Truncate if too long
65        let max_preview_len = 30;
66        let value_preview = if value_preview.len() > max_preview_len {
67            format!("{}...", &value_preview[..max_preview_len - 3])
68        } else {
69            value_preview
70        };
71
72        let row_area = Rect::new(area.x, y, area.width, 1);
73
74        // Full row background highlight for focused entry
75        if is_focused {
76            let highlight_style = Style::default().bg(colors.focused);
77            let bg_line = Line::from(Span::styled(
78                " ".repeat(area.width as usize),
79                highlight_style,
80            ));
81            frame.render_widget(Paragraph::new(bg_line), row_area);
82        }
83
84        // Row content with appropriate colors
85        let (arrow_color, key_color, value_color) = if is_focused {
86            (colors.label, colors.label, colors.value_preview)
87        } else {
88            (colors.expand_arrow, colors.key, colors.value_preview)
89        };
90
91        let base_style = if is_focused {
92            Style::default().bg(colors.focused)
93        } else {
94            Style::default()
95        };
96
97        let mut spans = vec![
98            Span::styled(" ".repeat(indent as usize), base_style),
99            Span::styled(arrow, base_style.fg(arrow_color)),
100            Span::raw(" "),
101            Span::styled(
102                format!("{:width$}", key, width = actual_key_width as usize),
103                base_style.fg(key_color),
104            ),
105            Span::raw(" "),
106            Span::styled(value_preview, base_style.fg(value_color)),
107        ];
108
109        // Add [Edit] hint for focused entry
110        if is_focused {
111            spans.push(Span::styled(
112                "  [Enter to edit]",
113                base_style
114                    .fg(colors.value_preview)
115                    .add_modifier(Modifier::DIM),
116            ));
117        }
118
119        frame.render_widget(Paragraph::new(Line::from(spans)), row_area);
120
121        entry_areas.push(MapEntryLayout {
122            index: idx,
123            row_area,
124            expand_area: Rect::new(area.x + indent, y, 1, 1),
125            key_area: Rect::new(area.x + indent + 2, y, actual_key_width, 1),
126            remove_area: Rect::new(area.x + indent + 2 + actual_key_width + 22, y, 3, 1),
127        });
128
129        y += 1;
130
131        // If expanded, show nested values (simplified view)
132        if is_expanded && y < area.y + area.height {
133            if let Some(obj) = value.as_object() {
134                for (nested_key, nested_value) in obj.iter().take(5) {
135                    if y >= area.y + area.height {
136                        break;
137                    }
138                    let nested_preview = format_value_preview(nested_value, 15);
139                    let nested_line = Line::from(vec![
140                        Span::raw(" ".repeat((indent + 4) as usize)),
141                        Span::styled(
142                            format!("{}: ", nested_key),
143                            Style::default().fg(colors.label),
144                        ),
145                        Span::styled(nested_preview, Style::default().fg(colors.value_preview)),
146                    ]);
147                    frame.render_widget(
148                        Paragraph::new(nested_line),
149                        Rect::new(area.x, y, area.width, 1),
150                    );
151                    y += 1;
152                }
153                if obj.len() > 5 && y < area.y + area.height {
154                    let more_line = Line::from(Span::styled(
155                        format!(
156                            "{}... and {} more",
157                            " ".repeat((indent + 4) as usize),
158                            obj.len() - 5
159                        ),
160                        Style::default()
161                            .fg(colors.value_preview)
162                            .add_modifier(Modifier::ITALIC),
163                    ));
164                    frame.render_widget(
165                        Paragraph::new(more_line),
166                        Rect::new(area.x, y, area.width, 1),
167                    );
168                    y += 1;
169                }
170            }
171        }
172    }
173
174    // Render "add new" row
175    let add_row_area = if y < area.y + area.height {
176        let is_focused = state.focused_entry.is_none() && state.focus == FocusState::Focused;
177        let (border_color, text_color) = if is_focused {
178            (colors.focused, colors.label)
179        } else if state.focus == FocusState::Disabled {
180            (colors.disabled, colors.disabled)
181        } else {
182            (colors.border, colors.label)
183        };
184
185        let inner_width = actual_key_width.saturating_sub(2) as usize;
186        let visible: String = state.new_key_text.chars().take(inner_width).collect();
187        let padded = format!("{:width$}", visible, width = inner_width);
188
189        let line = Line::from(vec![
190            Span::raw(" ".repeat(indent as usize)),
191            Span::styled("[", Style::default().fg(border_color)),
192            Span::styled(padded, Style::default().fg(text_color)),
193            Span::styled("]", Style::default().fg(border_color)),
194            Span::raw(" "),
195            Span::styled("[+]", Style::default().fg(colors.add_button)),
196            Span::raw(" Add entry..."),
197        ]);
198
199        let row_area = Rect::new(area.x, y, area.width, 1);
200        frame.render_widget(Paragraph::new(line), row_area);
201
202        // Render cursor if focused
203        if is_focused && state.cursor <= inner_width {
204            let cursor_x = area.x + indent + 1 + state.cursor as u16;
205            let cursor_char = state.new_key_text.chars().nth(state.cursor).unwrap_or(' ');
206            let cursor_area = Rect::new(cursor_x, y, 1, 1);
207            let cursor_span = Span::styled(
208                cursor_char.to_string(),
209                Style::default()
210                    .fg(colors.cursor)
211                    .add_modifier(Modifier::REVERSED),
212            );
213            frame.render_widget(Paragraph::new(Line::from(vec![cursor_span])), cursor_area);
214        }
215
216        Some(row_area)
217    } else {
218        None
219    };
220
221    MapLayout {
222        full_area: area,
223        entry_areas,
224        add_row_area,
225    }
226}
227
228/// Format a JSON value as a short preview string
229pub(super) fn format_value_preview(value: &serde_json::Value, max_len: usize) -> String {
230    let s = match value {
231        serde_json::Value::Null => "null".to_string(),
232        serde_json::Value::Bool(b) => b.to_string(),
233        serde_json::Value::Number(n) => n.to_string(),
234        serde_json::Value::String(s) => format!("\"{}\"", s),
235        serde_json::Value::Array(arr) => format!("[{} items]", arr.len()),
236        serde_json::Value::Object(obj) => format!("{{{} fields}}", obj.len()),
237    };
238    if s.len() > max_len {
239        format!("{}...", &s[..max_len - 3])
240    } else {
241        s
242    }
243}