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