use super::tables::{header_row, render_scrollable_table, striped_rows};
use crate::widgets::AppWidget;
use crate::{
state::{AppState, Pane},
styles::delta_bar,
};
use radiate_engines::{Chromosome, Objective, Optimize, events::LogLevel};
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Rect};
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Bar, BarChart, BarGroup, Cell, Row, Table, Widget};
const IMPROVEMENT_HEADER: [&str; 5] = ["Idx", "Gen", "Score", "Δ", ""];
const FRONT_EVENT_HEADER: [&str; 6] = [
" Gen", "Size", "+Added", "-Removed", "Compared", "Filtered",
];
const EVENT_HEADER: [&str; 3] = [" Gen", "Level", "Message"];
pub struct ImprovementLogWidget;
pub struct FrontEventLogWidget;
pub struct EventLogWidget;
impl<C: Chromosome> AppWidget<C> for ImprovementLogWidget {
fn render(&self, area: Rect, buf: &mut Buffer, state: &mut AppState<C>) {
let log = &state.evo.improvement_log;
let max_delta = log.iter().map(|e| e.delta).fold(0.0_f32, f32::max);
let best = state.evo.best_score.as_f32();
let is_minimize = matches!(
&state.evo.pareto.objective,
Objective::Single(Optimize::Minimize)
);
state
.tables
.log
.update_rows(log.as_slice(), |entry| entry.generation);
let rows = log
.iter()
.enumerate()
.map(|(i, entry)| {
let bar = delta_bar(entry.delta, max_delta, 15);
let bar_color = delta_color(entry.delta, max_delta);
let score_color = score_quality_color(entry.score, best, is_minimize);
Row::new(vec![
Cell::from(format!("{:?}", i))
.style(Style::default().fg(crate::styles::TEXT_FG_COLOR)),
Cell::from(format!("{:?}", entry.generation))
.style(Style::default().fg(crate::styles::TEXT_FG_COLOR)),
Cell::from(format!("{:.6}", entry.score))
.style(Style::default().fg(score_color)),
Cell::from(Span::styled(
format!("+{:.6}", entry.delta),
Style::default().fg(crate::styles::TREND_UP_COLOR),
)),
Cell::from(bar).style(Style::default().fg(bar_color)),
])
})
.collect::<Vec<_>>();
let focused = state.nav.is_pane_focused(Pane::List);
let table = Table::default()
.block(crate::styles::panel_block(focused))
.header(header_row(&IMPROVEMENT_HEADER))
.rows(striped_rows(rows))
.column_spacing(1)
.style(Color::White)
.row_highlight_style(Style::new().on_black().bold())
.highlight_spacing(ratatui::widgets::HighlightSpacing::Always)
.highlight_symbol(Span::styled("▶ ", Style::default().fg(Color::LightGreen)))
.widths([
Constraint::Length(8),
Constraint::Length(8),
Constraint::Length(14),
Constraint::Length(14),
Constraint::Fill(1),
]);
render_scrollable_table(buf, area, table, &mut state.tables.log);
}
}
impl<C: Chromosome> AppWidget<C> for FrontEventLogWidget {
fn render(&self, area: Rect, buf: &mut Buffer, state: &mut AppState<C>) {
let log = &state.evo.front_event_log;
let max_additions = log.iter().map(|e| e.additions).fold(0, usize::max);
let max_removals = log.iter().map(|e| e.removals).fold(0, usize::max);
let max_comparisons = log.iter().map(|e| e.comparisons).fold(0, usize::max);
state
.tables
.log
.update_rows(log.as_slice(), |entry| entry.generation);
let rows = log
.iter()
.map(|entry| {
let add_ratio = if max_additions > 0 {
entry.additions as f32 / max_additions as f32
} else {
0.0
};
let rem_ratio = if max_removals > 0 {
entry.removals as f32 / max_removals as f32
} else {
0.0
};
let comp_ratio = if max_comparisons > 0 {
entry.comparisons as f32 / max_comparisons as f32
} else {
0.0
};
Row::new(vec![
Cell::from(format!("{:>6}", entry.generation))
.style(Style::default().fg(crate::styles::TEXT_FG_COLOR)),
Cell::from(format!("{}", entry.front_size))
.style(Style::default().fg(crate::styles::TEXT_FG_COLOR)),
Cell::from(Line::from(vec![
Span::styled("+", Style::default().fg(crate::styles::TREND_UP_COLOR)),
Span::styled(
format!("{}", entry.additions),
Style::default()
.fg(crate::styles::sentiment_color(add_ratio, 0.2, 0.6)),
),
])),
Cell::from(Line::from(vec![
Span::styled("-", Style::default().fg(crate::styles::TREND_DOWN_COLOR)),
Span::styled(
format!("{}", entry.removals),
Style::default().fg(if entry.removals == 0 {
Color::DarkGray
} else {
crate::styles::sentiment_color(1.0 - rem_ratio, 0.2, 0.6)
}),
),
])),
Cell::from(Line::from(vec![
Span::styled("", Style::default().fg(crate::styles::TREND_NEUTRAL_COLOR)),
Span::styled(
crate::styles::format_thousands(entry.comparisons),
Style::default()
.fg(crate::styles::sentiment_color(comp_ratio, 0.2, 0.6)),
),
])),
Cell::from(if entry.filtered {
Span::styled(
"Y",
Style::default().fg(crate::styles::TREND_UP_COLOR_LIGHT),
)
} else {
Span::styled(
"N",
Style::default().fg(crate::styles::TREND_DOWN_COLOR_LIGHT),
)
}),
])
})
.collect::<Vec<_>>();
let focused = state.nav.is_pane_focused(Pane::List);
let table = Table::default()
.block(crate::styles::panel_block(focused))
.header(header_row(&FRONT_EVENT_HEADER))
.rows(striped_rows(rows))
.column_spacing(1)
.style(Color::White)
.row_highlight_style(Style::new().on_black().bold())
.highlight_spacing(ratatui::widgets::HighlightSpacing::Always)
.highlight_symbol(Span::styled("▶ ", Style::default().fg(Color::LightGreen)))
.widths([
Constraint::Length(10),
Constraint::Length(10),
Constraint::Fill(1),
Constraint::Fill(1),
Constraint::Fill(1),
Constraint::Fill(1),
]);
render_scrollable_table(buf, area, table, &mut state.tables.log);
}
}
impl<C: Chromosome> AppWidget<C> for EventLogWidget {
fn render(&self, area: Rect, buf: &mut Buffer, state: &mut AppState<C>) {
let log = &state.evo.event_log;
state
.tables
.events
.update_rows(log.as_slice(), |entry| entry.generation);
let rows = log
.iter()
.map(|entry| {
let (label, color) = match entry.level {
LogLevel::Info => ("INFO", crate::styles::LOG_INFO_COLOR),
LogLevel::Warn => ("WARN", crate::styles::LOG_WARN_COLOR),
};
Row::new(vec![
Cell::from(format!("{:>6}", entry.generation))
.style(Style::default().fg(crate::styles::TEXT_FG_COLOR)),
Cell::from(Span::styled(label, Style::default().fg(color).bold())),
Cell::from(entry.message.as_str())
.style(Style::default().fg(crate::styles::TEXT_FG_COLOR)),
])
})
.collect::<Vec<_>>();
let focused = state.nav.is_pane_focused(Pane::List);
let table = Table::default()
.block(crate::styles::panel_block(focused))
.header(header_row(&EVENT_HEADER))
.rows(striped_rows(rows))
.column_spacing(1)
.style(Color::White)
.row_highlight_style(Style::new().on_black().bold())
.highlight_spacing(ratatui::widgets::HighlightSpacing::Always)
.highlight_symbol(Span::styled("▶ ", Style::default().fg(Color::LightGreen)))
.widths([
Constraint::Length(8),
Constraint::Length(8),
Constraint::Fill(1),
]);
render_scrollable_table(buf, area, table, &mut state.tables.events);
}
}
pub struct DeltaBarChartWidget;
impl<C: Chromosome> AppWidget<C> for DeltaBarChartWidget {
fn render(&self, area: Rect, buf: &mut Buffer, state: &mut AppState<C>) {
let log = &state.evo.improvement_log;
let entries = log.as_slice();
let total = entries.len();
let focused = state.nav.is_pane_focused(Pane::Chart);
let block =
crate::styles::panel_block(focused).title(Line::from(" Improvement Δ ").centered());
if total == 0 {
Widget::render(block, area, buf);
return;
}
let max_delta = entries.iter().map(|e| e.delta).fold(0.0_f32, f32::max);
let selected_row = state.tables.log.selected_row;
let selected_chart_pos = total.saturating_sub(1 + selected_row);
let visible = (area.width as usize).saturating_sub(2).max(1);
let window_start = selected_chart_pos
.saturating_sub(visible / 2)
.min(total.saturating_sub(visible.min(total)));
let window_end = (window_start + visible).min(total);
let bars = (window_start..window_end)
.map(|chart_pos| {
let entry = &entries[total - 1 - chart_pos];
let value = if max_delta > 0.0 {
((entry.delta / max_delta) * 1000.0) as u64
} else {
0
};
let color = delta_color(entry.delta, max_delta);
let is_selected = chart_pos == selected_chart_pos;
Bar::default()
.value(value)
.text_value(String::new())
.style(if is_selected {
Style::default().on_black().bold().fg(color)
} else {
Style::default().fg(color)
})
})
.collect::<Vec<_>>();
Widget::render(
BarChart::default()
.block(block)
.data(BarGroup::default().bars(&bars))
.bar_width(1)
.bar_gap(0)
.max(1000),
area,
buf,
);
}
}
fn score_quality_color(score: f32, best: f32, is_minimize: bool) -> Color {
if best == 0.0 {
return Color::White;
}
let ratio = if is_minimize {
best / score.max(f32::EPSILON)
} else {
score / best
}
.clamp(0.0, 1.0);
crate::styles::sentiment_color(ratio, 0.7, 0.95)
}
fn delta_color(delta: f32, max_delta: f32) -> Color {
if max_delta <= 0.0 {
return Color::DarkGray;
}
let ratio = (delta / max_delta).clamp(0.0, 1.0);
if ratio >= 0.6 {
Color::LightGreen
} else if ratio >= 0.25 {
Color::Green
} else {
Color::DarkGray
}
}