use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span, Text},
widgets::{
Block, BorderType, Borders, Cell, Gauge, List, ListItem, Paragraph, Row, Table, Tabs,
},
Frame,
};
use crate::tui::app::{ActiveTab, App};
pub fn draw(f: &mut Frame, app: &mut App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(
[
Constraint::Length(6), Constraint::Length(3), Constraint::Min(0), Constraint::Length(1), ]
.as_ref(),
)
.split(f.area());
let header_text = vec![
Line::from(Span::styled(
" _______ __ ___ ___ ___ ___ ___ ___ ",
Style::default().fg(Color::Magenta),
)),
Line::from(Span::styled(
" /_ __/ |/ / / _ \\/ _ \\/ _ \\/ _ \\/ _ \\/ _ \\",
Style::default().fg(Color::Cyan),
)),
Line::from(Span::styled(
" / / | / / // / // / // / // / // / // /",
Style::default().fg(Color::LightGreen),
)),
Line::from(Span::styled(
" /_/ |__| /____/____/____/____/____/____/ ",
Style::default().fg(Color::Magenta),
)),
Line::from(Span::styled(
" TRANSACTION DECODER ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)),
];
let header = Paragraph::new(header_text)
.alignment(ratatui::layout::Alignment::Center)
.block(Block::default().borders(Borders::NONE));
f.render_widget(header, chunks[0]);
let titles = vec!["Overview", "Input", "Logs"]
.iter()
.map(|t| {
let (first, rest) = t.split_at(1);
Line::from(vec![
Span::styled(first, Style::default().fg(Color::Yellow)),
Span::styled(rest, Style::default().fg(Color::Green)),
])
})
.collect::<Vec<Line>>();
let tabs = Tabs::new(titles)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title("Tabs"),
)
.select(app.active_tab as usize)
.style(Style::default().fg(Color::DarkGray))
.highlight_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
);
f.render_widget(tabs, chunks[1]);
match app.active_tab {
ActiveTab::Overview => draw_overview(f, app, chunks[2]),
ActiveTab::Input => draw_input(f, app, chunks[2]),
ActiveTab::Logs => draw_logs(f, app, chunks[2]),
}
let footer = Paragraph::new(
"Controls: [Tab/h/l] Switch Tabs | [j/k/Up/Down] Scroll (input & params may be below) | [q/Esc] Quit",
)
.style(Style::default().fg(Color::DarkGray));
f.render_widget(footer, chunks[3]);
}
fn draw_overview(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3), Constraint::Min(0), ]
.as_ref(),
)
.split(area);
let gas_ratio = if app.gas_limit > 0 {
app.gas_used as f64 / app.gas_limit as f64
} else {
0.0
};
let label = format!(
"Gas Used: {} / {} ({:.2}%)",
app.gas_used,
app.gas_limit,
gas_ratio * 100.0
);
let gauge = Gauge::default()
.block(
Block::default()
.title("Gas Usage")
.borders(Borders::ALL)
.border_type(BorderType::Rounded),
)
.gauge_style(Style::default().fg(Color::LightGreen).bg(Color::DarkGray))
.ratio(gas_ratio)
.label(label)
.use_unicode(true);
f.render_widget(gauge, chunks[0]);
let value_width = ((area.width as usize).saturating_mul(70) / 100)
.saturating_sub(4)
.max(1);
let max_row_height = usize::from(chunks[1].height.saturating_sub(2).max(1));
let rows = build_rows(&app.overview_details, value_width, max_row_height);
let table = Table::new(
rows,
[Constraint::Percentage(30), Constraint::Percentage(70)],
)
.block(
Block::default()
.title("Transaction Details (scroll to view input/params)")
.borders(Borders::ALL)
.border_type(BorderType::Rounded),
)
.widths(&[Constraint::Percentage(30), Constraint::Percentage(70)])
.column_spacing(1)
.row_highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::DarkGray),
);
f.render_stateful_widget(table, chunks[1], &mut app.overview_scroll_state);
}
fn draw_input(f: &mut Frame, app: &mut App, area: Rect) {
if app.input_details.is_empty() {
let p = Paragraph::new("No input or params found")
.block(
Block::default()
.title("Input & Params")
.borders(Borders::ALL)
.border_type(BorderType::Rounded),
)
.style(Style::default().fg(Color::Yellow));
f.render_widget(p, area);
return;
}
let value_width = ((area.width as usize).saturating_mul(70) / 100)
.saturating_sub(4)
.max(1);
let max_row_height = usize::from(area.height.saturating_sub(2).max(1));
let rows = build_rows(&app.input_details, value_width, max_row_height);
let table = Table::new(
rows,
[Constraint::Percentage(30), Constraint::Percentage(70)],
)
.block(
Block::default()
.title("Input & Params")
.borders(Borders::ALL)
.border_type(BorderType::Rounded),
)
.widths(&[Constraint::Percentage(30), Constraint::Percentage(70)])
.column_spacing(1)
.row_highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::DarkGray),
);
f.render_stateful_widget(table, area, &mut app.input_scroll_state);
}
fn build_rows(
data: &[(String, String)],
value_width: usize,
max_row_height: usize,
) -> Vec<Row<'_>> {
data.iter()
.map(|(k, v)| {
let options = textwrap::Options::new(value_width)
.break_words(true)
.word_splitter(textwrap::WordSplitter::NoHyphenation);
let wrapped_lines: Vec<String> = textwrap::fill(v, options)
.lines()
.map(|line| line.to_string())
.collect();
let display_lines = if wrapped_lines.len() > max_row_height {
let mut lines = wrapped_lines
.into_iter()
.take(max_row_height)
.collect::<Vec<_>>();
if let Some(last) = lines.last_mut() {
last.push_str(" ...");
}
lines
} else {
wrapped_lines
};
let height = display_lines.len().max(1) as u16;
let val_text =
Text::from(display_lines.join("\n")).style(Style::default().fg(Color::White));
Row::new(vec![
Cell::from(Span::styled(k, Style::default().fg(Color::Blue))),
Cell::from(val_text),
])
.height(height)
})
.collect()
}
fn draw_logs(f: &mut Frame, app: &mut App, area: Rect) {
if app.logs.is_empty() {
let p = Paragraph::new("No logs found")
.block(Block::default().title("Logs").borders(Borders::ALL))
.style(Style::default().fg(Color::Yellow));
f.render_widget(p, area);
return;
}
let available_width = area.width as usize - 4;
let items: Vec<ListItem> = app
.logs
.iter()
.map(|(title, details)| {
let mut lines = vec![Line::from(Span::styled(
title,
Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Magenta),
))];
for (k, v) in details {
let prefix = format!("{}: ", k);
let prefix_len = prefix.len();
let wrap_width = if available_width > prefix_len {
available_width - prefix_len
} else {
available_width
};
let options = textwrap::Options::new(wrap_width)
.break_words(true)
.word_splitter(textwrap::WordSplitter::NoHyphenation);
let wrapped_val = textwrap::fill(v, options);
let mut val_lines = wrapped_val.lines();
if let Some(first_line) = val_lines.next() {
lines.push(Line::from(vec![
Span::styled(prefix.clone(), Style::default().fg(Color::Blue)),
Span::styled(first_line.to_string(), Style::default().fg(Color::White)),
]));
} else {
lines.push(Line::from(vec![Span::styled(
prefix.clone(),
Style::default().fg(Color::Blue),
)]));
}
for line in val_lines {
lines.push(Line::from(vec![
Span::raw(" ".repeat(prefix_len)),
Span::styled(line.to_string(), Style::default().fg(Color::White)),
]));
}
}
lines.push(Line::from("")); ListItem::new(lines)
})
.collect();
let list = List::new(items)
.block(
Block::default()
.title("Logs")
.borders(Borders::ALL)
.border_type(BorderType::Rounded),
)
.highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::DarkGray),
);
f.render_stateful_widget(list, area, &mut app.logs_scroll_state);
}