fresh/view/controls/map_input/
render.rs1use 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
11pub 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_fg,
31 FocusState::Hovered => colors.focused_fg,
32 FocusState::Disabled => colors.disabled,
33 FocusState::Normal => colors.label,
34 };
35
36 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 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 let value_preview = state.get_display_value(value);
64 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 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 let (arrow_color, key_color, value_color) = if is_focused {
86 (colors.focused_fg, colors.focused_fg, colors.focused_fg)
88 } else {
89 (colors.expand_arrow, colors.key, colors.value_preview)
90 };
91
92 let base_style = if is_focused {
93 Style::default().bg(colors.focused)
94 } else {
95 Style::default()
96 };
97
98 let mut spans = vec![
99 Span::styled(" ".repeat(indent as usize), base_style),
100 Span::styled(arrow, base_style.fg(arrow_color)),
101 Span::raw(" "),
102 Span::styled(
103 format!("{:width$}", key, width = actual_key_width as usize),
104 base_style.fg(key_color),
105 ),
106 Span::raw(" "),
107 Span::styled(value_preview, base_style.fg(value_color)),
108 ];
109
110 if is_focused {
112 spans.push(Span::styled(
113 " [Enter to edit]",
114 base_style.fg(colors.focused_fg).add_modifier(Modifier::DIM),
115 ));
116 }
117
118 frame.render_widget(Paragraph::new(Line::from(spans)), row_area);
119
120 entry_areas.push(MapEntryLayout {
121 index: idx,
122 row_area,
123 expand_area: Rect::new(area.x + indent, y, 1, 1),
124 key_area: Rect::new(area.x + indent + 2, y, actual_key_width, 1),
125 remove_area: Rect::new(area.x + indent + 2 + actual_key_width + 22, y, 3, 1),
126 });
127
128 y += 1;
129
130 if is_expanded && y < area.y + area.height {
132 if let Some(obj) = value.as_object() {
133 for (nested_key, nested_value) in obj.iter().take(5) {
134 if y >= area.y + area.height {
135 break;
136 }
137 let nested_preview = format_value_preview(nested_value, 15);
138 let nested_line = Line::from(vec![
139 Span::raw(" ".repeat((indent + 4) as usize)),
140 Span::styled(
141 format!("{}: ", nested_key),
142 Style::default().fg(colors.label),
143 ),
144 Span::styled(nested_preview, Style::default().fg(colors.value_preview)),
145 ]);
146 frame.render_widget(
147 Paragraph::new(nested_line),
148 Rect::new(area.x, y, area.width, 1),
149 );
150 y += 1;
151 }
152 if obj.len() > 5 && y < area.y + area.height {
153 let more_line = Line::from(Span::styled(
154 format!(
155 "{}... and {} more",
156 " ".repeat((indent + 4) as usize),
157 obj.len() - 5
158 ),
159 Style::default()
160 .fg(colors.value_preview)
161 .add_modifier(Modifier::ITALIC),
162 ));
163 frame.render_widget(
164 Paragraph::new(more_line),
165 Rect::new(area.x, y, area.width, 1),
166 );
167 y += 1;
168 }
169 }
170 }
171 }
172
173 let add_row_area = if y < area.y + area.height {
175 let is_focused = state.focused_entry.is_none() && state.focus == FocusState::Focused;
176 let (border_color, text_color) = if is_focused {
177 (colors.focused, colors.label)
178 } else if state.focus == FocusState::Disabled {
179 (colors.disabled, colors.disabled)
180 } else {
181 (colors.border, colors.label)
182 };
183
184 let inner_width = actual_key_width.saturating_sub(2) as usize;
185 let visible: String = state.new_key_text.chars().take(inner_width).collect();
186 let padded = format!("{:width$}", visible, width = inner_width);
187
188 let line = Line::from(vec![
189 Span::raw(" ".repeat(indent as usize)),
190 Span::styled("[", Style::default().fg(border_color)),
191 Span::styled(padded, Style::default().fg(text_color)),
192 Span::styled("]", Style::default().fg(border_color)),
193 Span::raw(" "),
194 Span::styled("[+]", Style::default().fg(colors.add_button)),
195 Span::raw(" Add entry..."),
196 ]);
197
198 let row_area = Rect::new(area.x, y, area.width, 1);
199 frame.render_widget(Paragraph::new(line), row_area);
200
201 if is_focused && state.cursor <= inner_width {
203 let cursor_x = area.x + indent + 1 + state.cursor as u16;
204 let cursor_char = state.new_key_text.chars().nth(state.cursor).unwrap_or(' ');
205 let cursor_area = Rect::new(cursor_x, y, 1, 1);
206 let cursor_span = Span::styled(
207 cursor_char.to_string(),
208 Style::default()
209 .fg(colors.cursor)
210 .add_modifier(Modifier::REVERSED),
211 );
212 frame.render_widget(Paragraph::new(Line::from(vec![cursor_span])), cursor_area);
213 }
214
215 Some(row_area)
216 } else {
217 None
218 };
219
220 MapLayout {
221 full_area: area,
222 entry_areas,
223 add_row_area,
224 }
225}
226
227pub(super) fn format_value_preview(value: &serde_json::Value, max_len: usize) -> String {
229 let s = match value {
230 serde_json::Value::Null => "null".to_string(),
231 serde_json::Value::Bool(b) => b.to_string(),
232 serde_json::Value::Number(n) => n.to_string(),
233 serde_json::Value::String(s) => format!("\"{}\"", s),
234 serde_json::Value::Array(arr) => format!("[{} items]", arr.len()),
235 serde_json::Value::Object(obj) => format!("{{{} fields}}", obj.len()),
236 };
237 if s.len() > max_len {
238 format!("{}...", &s[..max_len - 3])
239 } else {
240 s
241 }
242}