1use crate::pivot_melt_modal::{PivotMeltFocus, PivotMeltModal, PivotMeltTab};
6use ratatui::layout::{Constraint, Direction, Layout, Rect};
7use ratatui::style::{Color, Modifier, Style};
8use ratatui::text::{Line, Span};
9use ratatui::widgets::{
10 Block, BorderType, Borders, Cell, Clear, Paragraph, Row, StatefulWidget, Table, Tabs, Widget,
11};
12
13pub fn render_shell(
17 area: Rect,
18 buf: &mut ratatui::buffer::Buffer,
19 modal: &mut PivotMeltModal,
20 border_color: Color,
21 active_color: Color,
22 text_primary: Color,
23 text_inverse: Color,
24) {
25 Clear.render(area, buf);
26 let block = Block::default()
27 .borders(Borders::ALL)
28 .border_type(BorderType::Rounded)
29 .border_style(Style::default().fg(border_color));
30 let inner = block.inner(area);
31 block.render(area, buf);
32
33 let chunks = Layout::default()
34 .direction(Direction::Vertical)
35 .constraints([
36 Constraint::Length(2),
37 Constraint::Min(10),
38 Constraint::Length(3),
39 ])
40 .split(inner);
41
42 let tab_line_chunks = Layout::default()
44 .direction(Direction::Vertical)
45 .constraints([Constraint::Length(1), Constraint::Length(1)])
46 .split(chunks[0]);
47 let selected = match modal.active_tab {
48 PivotMeltTab::Pivot => 0,
49 PivotMeltTab::Melt => 1,
50 };
51 let tabs = Tabs::new(vec!["Pivot", "Melt"])
52 .style(Style::default().fg(border_color))
53 .highlight_style(
54 Style::default()
55 .fg(active_color)
56 .add_modifier(Modifier::REVERSED),
57 )
58 .select(selected);
59 tabs.render(tab_line_chunks[0], buf);
60 let line_style = if modal.focus == PivotMeltFocus::TabBar {
61 Style::default().fg(active_color)
62 } else {
63 Style::default().fg(border_color)
64 };
65 Block::default()
66 .borders(Borders::BOTTOM)
67 .border_type(BorderType::Rounded)
68 .border_style(line_style)
69 .render(tab_line_chunks[1], buf);
70
71 match modal.active_tab {
73 PivotMeltTab::Pivot => render_pivot_body(
74 chunks[1],
75 buf,
76 modal,
77 border_color,
78 active_color,
79 text_primary,
80 text_inverse,
81 ),
82 PivotMeltTab::Melt => render_melt_body(
83 chunks[1],
84 buf,
85 modal,
86 border_color,
87 active_color,
88 text_primary,
89 text_inverse,
90 ),
91 }
92
93 let footer_chunks = Layout::default()
95 .direction(Direction::Horizontal)
96 .constraints([
97 Constraint::Percentage(33),
98 Constraint::Percentage(33),
99 Constraint::Percentage(34),
100 ])
101 .split(chunks[2]);
102
103 let apply_style = if modal.focus == PivotMeltFocus::Apply {
104 Style::default().fg(active_color)
105 } else {
106 Style::default().fg(border_color)
107 };
108 let cancel_style = if modal.focus == PivotMeltFocus::Cancel {
109 Style::default().fg(active_color)
110 } else {
111 Style::default().fg(border_color)
112 };
113 let clear_style = if modal.focus == PivotMeltFocus::Clear {
114 Style::default().fg(active_color)
115 } else {
116 Style::default().fg(border_color)
117 };
118
119 Paragraph::new("Apply")
120 .block(
121 Block::default()
122 .borders(Borders::ALL)
123 .border_type(BorderType::Rounded)
124 .border_style(apply_style),
125 )
126 .centered()
127 .render(footer_chunks[0], buf);
128 Paragraph::new("Cancel")
129 .block(
130 Block::default()
131 .borders(Borders::ALL)
132 .border_type(BorderType::Rounded)
133 .border_style(cancel_style),
134 )
135 .centered()
136 .render(footer_chunks[1], buf);
137 Paragraph::new("Clear")
138 .block(
139 Block::default()
140 .borders(Borders::ALL)
141 .border_type(BorderType::Rounded)
142 .border_style(clear_style),
143 )
144 .centered()
145 .render(footer_chunks[2], buf);
146}
147
148fn render_pivot_body(
149 area: Rect,
150 buf: &mut ratatui::buffer::Buffer,
151 modal: &mut PivotMeltModal,
152 border_color: Color,
153 active_color: Color,
154 _text_primary: Color,
155 _text_inverse: Color,
156) {
157 let chunks = Layout::default()
158 .direction(Direction::Vertical)
159 .constraints([
160 Constraint::Length(3),
161 Constraint::Min(6),
162 Constraint::Length(5),
163 Constraint::Length(5),
164 Constraint::Length(4),
165 Constraint::Length(4),
166 ])
167 .split(area);
168
169 let filter_style = if modal.focus == PivotMeltFocus::PivotFilter {
171 Style::default().fg(active_color)
172 } else {
173 Style::default().fg(border_color)
174 };
175 let filter_block = Block::default()
176 .borders(Borders::ALL)
177 .border_type(BorderType::Rounded)
178 .title("Filter Index Columns")
179 .border_style(filter_style);
180 let filter_inner = filter_block.inner(chunks[0]);
181 filter_block.render(chunks[0], buf);
182
183 let is_focused = modal.focus == PivotMeltFocus::PivotFilter;
185 modal.pivot_filter_input.set_focused(is_focused);
186 (&modal.pivot_filter_input).render(filter_inner, buf);
187
188 let list_style = if modal.focus == PivotMeltFocus::PivotIndexList {
190 Style::default().fg(active_color)
191 } else {
192 Style::default().fg(border_color)
193 };
194 let list_block = Block::default()
195 .borders(Borders::ALL)
196 .border_type(BorderType::Rounded)
197 .title("Index Columns")
198 .border_style(list_style);
199 let list_inner = list_block.inner(chunks[1]);
200 list_block.render(chunks[1], buf);
201
202 let filtered = modal.pivot_filtered_columns();
203 if !filtered.is_empty() && modal.pivot_index_table.selected().is_none() {
204 modal.pivot_index_table.select(Some(0));
205 }
206 let rows: Vec<Row> = filtered
207 .iter()
208 .map(|c| {
209 let check = if modal.index_columns.contains(c) {
210 "[x]"
211 } else {
212 "[ ]"
213 };
214 Row::new(vec![Cell::from(check), Cell::from(c.as_str())])
215 })
216 .collect();
217 let widths = [Constraint::Length(4), Constraint::Min(10)];
218 let table = Table::new(rows, widths)
219 .column_spacing(1)
220 .header(
221 Row::new(vec!["", "Column"])
222 .style(Style::default().add_modifier(Modifier::BOLD))
223 .bottom_margin(0),
224 )
225 .row_highlight_style(Style::default().add_modifier(Modifier::REVERSED));
226 StatefulWidget::render(table, list_inner, buf, &mut modal.pivot_index_table);
227
228 let pivot_style = if modal.focus == PivotMeltFocus::PivotPivotCol {
230 Style::default().fg(active_color)
231 } else {
232 Style::default().fg(border_color)
233 };
234 let value_style = if modal.focus == PivotMeltFocus::PivotValueCol {
235 Style::default().fg(active_color)
236 } else {
237 Style::default().fg(border_color)
238 };
239 let row_chunks = Layout::default()
240 .direction(Direction::Horizontal)
241 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
242 .split(chunks[2]);
243
244 let pivot_pool = modal.pivot_pool();
245 if !pivot_pool.is_empty() {
246 let n = pivot_pool.len();
247 let idx = modal.pivot_pool_idx.min(n.saturating_sub(1));
248 if modal.pivot_pool_idx != idx {
249 modal.pivot_pool_idx = idx;
250 modal.pivot_column = pivot_pool.get(idx).cloned();
251 }
252 modal.pivot_pool_table.select(Some(idx));
253 }
254 let pivot_rows: Vec<Row> = pivot_pool
255 .iter()
256 .map(|c| Row::new(vec![Cell::from(c.as_str())]))
257 .collect();
258 let pivot_block = Block::default()
259 .borders(Borders::ALL)
260 .border_type(BorderType::Rounded)
261 .title("Pivot Column")
262 .border_style(pivot_style);
263 let pivot_inner = pivot_block.inner(row_chunks[0]);
264 pivot_block.render(row_chunks[0], buf);
265 if pivot_rows.is_empty() {
266 Paragraph::new("(none)").render(pivot_inner, buf);
267 } else {
268 let pt = Table::new(pivot_rows, [Constraint::Min(5)])
269 .row_highlight_style(Style::default().add_modifier(Modifier::REVERSED));
270 StatefulWidget::render(pt, pivot_inner, buf, &mut modal.pivot_pool_table);
271 }
272
273 let value_pool = modal.pivot_value_pool();
274 if !value_pool.is_empty() {
275 let n = value_pool.len();
276 let idx = modal.value_pool_idx.min(n.saturating_sub(1));
277 if modal.value_pool_idx != idx {
278 modal.value_pool_idx = idx;
279 modal.value_column = value_pool.get(idx).cloned();
280 }
281 modal.value_pool_table.select(Some(idx));
282 }
283 let value_rows: Vec<Row> = value_pool
284 .iter()
285 .map(|c| Row::new(vec![Cell::from(c.as_str())]))
286 .collect();
287 let value_block = Block::default()
288 .borders(Borders::ALL)
289 .border_type(BorderType::Rounded)
290 .title("Value Column")
291 .border_style(value_style);
292 let value_inner = value_block.inner(row_chunks[1]);
293 value_block.render(row_chunks[1], buf);
294 if value_rows.is_empty() {
295 Paragraph::new("(none)").render(value_inner, buf);
296 } else {
297 let vt = Table::new(value_rows, [Constraint::Min(5)])
298 .row_highlight_style(Style::default().add_modifier(Modifier::REVERSED));
299 StatefulWidget::render(vt, value_inner, buf, &mut modal.value_pool_table);
300 }
301
302 let agg_style = if modal.focus == PivotMeltFocus::PivotAggregation {
304 Style::default().fg(active_color)
305 } else {
306 Style::default().fg(border_color)
307 };
308 let opts = modal.pivot_aggregation_options();
309 let agg_label = opts
310 .get(modal.aggregation_idx)
311 .map(|a| a.as_str())
312 .unwrap_or("last");
313 let agg_block = Block::default()
314 .borders(Borders::ALL)
315 .border_type(BorderType::Rounded)
316 .title("Aggregation")
317 .border_style(agg_style);
318 let agg_inner = agg_block.inner(chunks[3]);
319 agg_block.render(chunks[3], buf);
320 Paragraph::new(agg_label).render(agg_inner, buf);
321
322 let sort_style = if modal.focus == PivotMeltFocus::PivotSortToggle {
324 Style::default().fg(active_color)
325 } else {
326 Style::default().fg(border_color)
327 };
328 let sort_check = if modal.sort_new_columns { "[x]" } else { "[ ]" };
329 let sort_block = Block::default()
330 .borders(Borders::ALL)
331 .border_type(BorderType::Rounded)
332 .title("Sort New Columns")
333 .border_style(sort_style);
334 let sort_inner = sort_block.inner(chunks[4]);
335 sort_block.render(chunks[4], buf);
336 Paragraph::new(format!("{} Sort", sort_check)).render(sort_inner, buf);
337}
338
339fn render_melt_body(
340 area: Rect,
341 buf: &mut ratatui::buffer::Buffer,
342 modal: &mut PivotMeltModal,
343 border_color: Color,
344 active_color: Color,
345 text_primary: Color,
346 text_inverse: Color,
347) {
348 use crate::pivot_melt_modal::{MeltValueStrategy, PivotMeltFocus};
349
350 let chunks = Layout::default()
351 .direction(Direction::Vertical)
352 .constraints([
353 Constraint::Length(3),
354 Constraint::Min(6),
355 Constraint::Length(4),
356 Constraint::Length(5),
357 Constraint::Length(4),
358 ])
359 .split(area);
360
361 let filter_style = if modal.focus == PivotMeltFocus::MeltFilter {
363 Style::default().fg(active_color)
364 } else {
365 Style::default().fg(border_color)
366 };
367 let filter_block = Block::default()
368 .borders(Borders::ALL)
369 .border_type(BorderType::Rounded)
370 .title("Filter Index Columns")
371 .border_style(filter_style);
372 let filter_inner = filter_block.inner(chunks[0]);
373 filter_block.render(chunks[0], buf);
374
375 let is_focused = modal.focus == PivotMeltFocus::MeltFilter;
377 modal.melt_filter_input.set_focused(is_focused);
378 (&modal.melt_filter_input).render(filter_inner, buf);
379
380 let list_style = if modal.focus == PivotMeltFocus::MeltIndexList {
382 Style::default().fg(active_color)
383 } else {
384 Style::default().fg(border_color)
385 };
386 let list_block = Block::default()
387 .borders(Borders::ALL)
388 .border_type(BorderType::Rounded)
389 .title("Index Columns")
390 .border_style(list_style);
391 let list_inner = list_block.inner(chunks[1]);
392 list_block.render(chunks[1], buf);
393
394 let filtered = modal.melt_filtered_columns();
395 if !filtered.is_empty() && modal.melt_index_table.selected().is_none() {
396 modal.melt_index_table.select(Some(0));
397 }
398 let rows: Vec<Row> = filtered
399 .iter()
400 .map(|c| {
401 let check = if modal.melt_index_columns.contains(c) {
402 "[x]"
403 } else {
404 "[ ]"
405 };
406 Row::new(vec![Cell::from(check), Cell::from(c.as_str())])
407 })
408 .collect();
409 let widths = [Constraint::Length(4), Constraint::Min(10)];
410 let table = Table::new(rows, widths)
411 .column_spacing(1)
412 .header(
413 Row::new(vec!["", "Column"])
414 .style(Style::default().add_modifier(Modifier::BOLD))
415 .bottom_margin(0),
416 )
417 .row_highlight_style(Style::default().add_modifier(Modifier::REVERSED));
418 StatefulWidget::render(table, list_inner, buf, &mut modal.melt_index_table);
419
420 let strat_style = if modal.focus == PivotMeltFocus::MeltStrategy {
422 Style::default().fg(active_color)
423 } else {
424 Style::default().fg(border_color)
425 };
426 let strat_block = Block::default()
427 .borders(Borders::ALL)
428 .border_type(BorderType::Rounded)
429 .title("Strategy")
430 .border_style(strat_style);
431 let strat_inner = strat_block.inner(chunks[2]);
432 strat_block.render(chunks[2], buf);
433 Paragraph::new(modal.melt_value_strategy.as_str()).render(strat_inner, buf);
434
435 let opt_chunks = Layout::default()
437 .direction(Direction::Horizontal)
438 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
439 .split(chunks[3]);
440
441 match modal.melt_value_strategy {
442 MeltValueStrategy::ByPattern => {
443 let pat_style = if modal.focus == PivotMeltFocus::MeltPattern {
444 Style::default().fg(active_color)
445 } else {
446 Style::default().fg(border_color)
447 };
448 let pat_block = Block::default()
449 .borders(Borders::ALL)
450 .border_type(BorderType::Rounded)
451 .title("Pattern (Regex)")
452 .border_style(pat_style);
453 let pat_inner = pat_block.inner(opt_chunks[0]);
454 pat_block.render(opt_chunks[0], buf);
455 let pt = modal.melt_pattern.as_str();
456 let pc = modal.melt_pattern_cursor.min(pt.chars().count());
457 let mut ch = pt.chars();
458 let b: String = ch.by_ref().take(pc).collect();
459 let a = ch
460 .next()
461 .map(|c| c.to_string())
462 .unwrap_or_else(|| " ".to_string());
463 let af: String = ch.collect();
464 let mut pl = Line::default();
465 pl.spans.push(Span::raw(b));
466 if modal.focus == PivotMeltFocus::MeltPattern {
467 pl.spans.push(Span::styled(
468 a,
469 Style::default().bg(text_inverse).fg(text_primary),
470 ));
471 } else {
472 pl.spans.push(Span::raw(a));
473 }
474 if !af.is_empty() {
475 pl.spans.push(Span::raw(af));
476 }
477 Paragraph::new(pl).render(pat_inner, buf);
478 }
479 MeltValueStrategy::ByType => {
480 let ty_style = if modal.focus == PivotMeltFocus::MeltType {
481 Style::default().fg(active_color)
482 } else {
483 Style::default().fg(border_color)
484 };
485 let ty_block = Block::default()
486 .borders(Borders::ALL)
487 .border_type(BorderType::Rounded)
488 .title("Type")
489 .border_style(ty_style);
490 let ty_inner = ty_block.inner(opt_chunks[0]);
491 ty_block.render(opt_chunks[0], buf);
492 Paragraph::new(modal.melt_type_filter.as_str()).render(ty_inner, buf);
493 }
494 MeltValueStrategy::ExplicitList => {
495 let ex_style = if modal.focus == PivotMeltFocus::MeltExplicitList {
496 Style::default().fg(active_color)
497 } else {
498 Style::default().fg(border_color)
499 };
500 let ex_block = Block::default()
501 .borders(Borders::ALL)
502 .border_type(BorderType::Rounded)
503 .title("Value Columns")
504 .border_style(ex_style);
505 let ex_inner = ex_block.inner(chunks[3]);
506 ex_block.render(chunks[3], buf);
507 let pool = modal.melt_explicit_pool();
508 if !pool.is_empty() && modal.melt_explicit_table.selected().is_none() {
509 modal.melt_explicit_table.select(Some(0));
510 }
511 let ex_rows: Vec<Row> = pool
512 .iter()
513 .map(|c| {
514 let check = if modal.melt_explicit_list.contains(c) {
515 "[x]"
516 } else {
517 "[ ]"
518 };
519 Row::new(vec![Cell::from(check), Cell::from(c.as_str())])
520 })
521 .collect();
522 let ew = [Constraint::Length(4), Constraint::Min(10)];
523 let ex_table = Table::new(ex_rows, ew)
524 .column_spacing(1)
525 .header(
526 Row::new(vec!["", "Column"])
527 .style(Style::default().add_modifier(Modifier::BOLD))
528 .bottom_margin(0),
529 )
530 .row_highlight_style(Style::default().add_modifier(Modifier::REVERSED));
531 StatefulWidget::render(ex_table, ex_inner, buf, &mut modal.melt_explicit_table);
532 }
533 MeltValueStrategy::AllExceptIndex => {}
534 }
535
536 let var_style = if modal.focus == PivotMeltFocus::MeltVarName {
538 Style::default().fg(active_color)
539 } else {
540 Style::default().fg(border_color)
541 };
542 let val_style = if modal.focus == PivotMeltFocus::MeltValName {
543 Style::default().fg(active_color)
544 } else {
545 Style::default().fg(border_color)
546 };
547 let vchunks = Layout::default()
548 .direction(Direction::Horizontal)
549 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
550 .split(chunks[4]);
551 let var_block = Block::default()
552 .borders(Borders::ALL)
553 .border_type(BorderType::Rounded)
554 .title("Variable Name")
555 .border_style(var_style);
556 let var_inner = var_block.inner(vchunks[0]);
557 var_block.render(vchunks[0], buf);
558 let vn = modal.melt_variable_name.as_str();
559 let vc = modal.melt_variable_cursor.min(vn.chars().count());
560 let mut ch = vn.chars();
561 let vb: String = ch.by_ref().take(vc).collect();
562 let va = ch
563 .next()
564 .map(|c| c.to_string())
565 .unwrap_or_else(|| " ".to_string());
566 let vaf: String = ch.collect();
567 let mut vl = Line::default();
568 vl.spans.push(Span::raw(vb));
569 if modal.focus == PivotMeltFocus::MeltVarName {
570 vl.spans.push(Span::styled(
571 va,
572 Style::default().bg(text_inverse).fg(text_primary),
573 ));
574 } else {
575 vl.spans.push(Span::raw(va));
576 }
577 if !vaf.is_empty() {
578 vl.spans.push(Span::raw(vaf));
579 }
580 Paragraph::new(vl).render(var_inner, buf);
581
582 let val_block = Block::default()
583 .borders(Borders::ALL)
584 .border_type(BorderType::Rounded)
585 .title("Value Name")
586 .border_style(val_style);
587 let val_inner = val_block.inner(vchunks[1]);
588 val_block.render(vchunks[1], buf);
589 let wn = modal.melt_value_name.as_str();
590 let wc = modal.melt_value_cursor.min(wn.chars().count());
591 let mut ch = wn.chars();
592 let wb: String = ch.by_ref().take(wc).collect();
593 let wa = ch
594 .next()
595 .map(|c| c.to_string())
596 .unwrap_or_else(|| " ".to_string());
597 let waf: String = ch.collect();
598 let mut wl = Line::default();
599 wl.spans.push(Span::raw(wb));
600 if modal.focus == PivotMeltFocus::MeltValName {
601 wl.spans.push(Span::styled(
602 wa,
603 Style::default().bg(text_inverse).fg(text_primary),
604 ));
605 } else {
606 wl.spans.push(Span::raw(wa));
607 }
608 if !waf.is_empty() {
609 wl.spans.push(Span::raw(waf));
610 }
611 Paragraph::new(wl).render(val_inner, buf);
612}