use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
symbols,
text::Span,
widgets::{Block, Borders, Cell, LineGauge, Paragraph, Row, Sparkline, Table},
};
use crate::{
app::{App, AppColors, History, HistoryExt},
metric_key::MetricKey,
metrics::{ClusterMetrics, CpuMetrics, Metrics},
units,
};
const CPU_BLOCK_HEIGHT: u16 = 1;
const SPARKLINE_MAX_OVERSHOOT: f32 = 1.05;
const ACTIVITY_HISTORY_LENGTH: u16 = 8;
const FREQUENCY_LABEL_WIDTH: u16 = 6; const FREQUENCY_VALUE_WIDTH: u16 = 10; const FREQUENCY_HISTORY_LENGTH: u16 = 8;
const FREQUENCY_TABLE_HEIGHT: u16 = 5;
pub(crate) fn draw_cpu_tab(f: &mut Frame, app: &App, area: Rect) {
let metrics = match &app.metrics {
Some(metrics) => metrics,
None => return,
};
let constraints = metrics
.e_clusters
.iter()
.map(|cl| Constraint::Length(2 + CPU_BLOCK_HEIGHT * cl.cpus.len() as u16))
.chain(
metrics
.p_clusters
.iter()
.map(|cl| Constraint::Length(2 + CPU_BLOCK_HEIGHT * cl.cpus.len() as u16)),
)
.chain(
metrics
.s_clusters
.iter()
.map(|cl| Constraint::Length(2 + CPU_BLOCK_HEIGHT * cl.cpus.len() as u16)),
)
.chain(std::iter::once(Constraint::Length(
2 + FREQUENCY_TABLE_HEIGHT,
)))
.chain(std::iter::once(Constraint::Min(0)))
.collect::<Vec<_>>();
let cpu_cluster_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(area);
let mut clu_area_iter = cpu_cluster_chunks.iter();
for cluster in metrics.e_clusters.iter() {
let cluster_area = clu_area_iter
.next()
.expect("layout: expected area for E-cluster");
draw_cpu_cluster(f, cluster, &app.history, &app.colors, *cluster_area);
}
for cluster in metrics.p_clusters.iter() {
let cluster_area = clu_area_iter
.next()
.expect("layout: expected area for P-cluster");
draw_cpu_cluster(f, cluster, &app.history, &app.colors, *cluster_area);
}
for cluster in metrics.s_clusters.iter() {
let cluster_area = clu_area_iter
.next()
.expect("layout: expected area for S-cluster");
draw_cpu_cluster(f, cluster, &app.history, &app.colors, *cluster_area);
}
let freq_table_area = clu_area_iter
.next()
.expect("layout: expected area for frequency table");
draw_freq_table(f, metrics, *freq_table_area);
}
fn draw_cpu_cluster(
f: &mut Frame,
cluster: &ClusterMetrics,
history: &History,
colors: &AppColors,
area: Rect,
) {
let cluster_name = format!(" {}: ", cluster.name);
let block = Block::default().title(cluster_name).borders(Borders::ALL);
f.render_widget(block, area);
let constraints = (0..cluster.cpus.len())
.map(|_| Constraint::Length(CPU_BLOCK_HEIGHT))
.collect::<Vec<_>>();
let cpu_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.margin(1)
.split(area);
let mut cpu_area_iter = cpu_chunks.iter();
for cpu in cluster.cpus.iter() {
let cpu_area = cpu_area_iter
.next()
.expect("layout: expected area for CPU core");
draw_cpu(f, cpu, history, colors, *cpu_area);
}
}
fn draw_cpu(f: &mut Frame, cpu: &CpuMetrics, history: &History, colors: &AppColors, area: Rect) {
let horiz_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(5), Constraint::Min(0)])
.split(area);
let cpu_id_area = horiz_chunks[0];
let other_area = horiz_chunks[1];
let cpu_id_text = format!("{:2} -", cpu.id);
let par = Paragraph::new(Span::styled(
cpu_id_text,
Style::default().fg(colors.accent()),
));
f.render_widget(par, cpu_id_area);
let activity_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.split(other_area);
let activity_area = activity_chunks[0];
let frequency_area = activity_chunks[1];
let activity_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(ACTIVITY_HISTORY_LENGTH + 1),
Constraint::Min(0),
])
.split(activity_area);
let acti_histo_area = activity_chunks[0];
let acti_gauge_area = activity_chunks[1];
let sig = history.get_or_default(&MetricKey::CpuActivePercent(cpu.id));
let activity_history_sparkline = Sparkline::default()
.style(
Style::default()
.fg(colors.history_fg())
.bg(colors.history_bg()),
)
.bar_set(symbols::bar::NINE_LEVELS)
.data(sig.as_slice_last_n(ACTIVITY_HISTORY_LENGTH as usize))
.max((SPARKLINE_MAX_OVERSHOOT * sig.max) as u64);
f.render_widget(activity_history_sparkline, acti_histo_area);
let active_ratio = cpu.active_ratio;
let label = format!("{:.1}%", active_ratio * 100.0);
let gauge = LineGauge::default()
.filled_style(Style::default().fg(colors.gauge_fg()).bg(colors.gauge_bg()))
.filled_symbol(symbols::line::THICK.horizontal)
.unfilled_symbol(symbols::line::THICK.horizontal)
.label(label)
.ratio(active_ratio);
f.render_widget(gauge, acti_gauge_area);
let frequency_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(FREQUENCY_LABEL_WIDTH),
Constraint::Length(FREQUENCY_HISTORY_LENGTH + 1),
Constraint::Length(FREQUENCY_VALUE_WIDTH),
Constraint::Min(0),
])
.split(frequency_area);
let freq_label_area = frequency_chunks[0];
let freq_hist_area = frequency_chunks[1];
let freq_value_area = frequency_chunks[2];
let freq_gauge_area = frequency_chunks[3];
let freq_label_text = "freq:";
let par = Paragraph::new(Span::from(freq_label_text));
f.render_widget(par, freq_label_area);
let sig = history.get_or_default(&MetricKey::CpuFreqPercent(cpu.id));
let freq_history_sparkline = Sparkline::default()
.style(
Style::default()
.fg(colors.history_fg())
.bg(colors.history_bg()),
)
.bar_set(symbols::bar::NINE_LEVELS)
.data(sig.as_slice_last_n(FREQUENCY_HISTORY_LENGTH as usize))
.max((SPARKLINE_MAX_OVERSHOOT * sig.max) as u64);
f.render_widget(freq_history_sparkline, freq_hist_area);
let freq_value_text = units::mhz(cpu.freq_mhz);
let par = Paragraph::new(Span::from(freq_value_text));
f.render_widget(par, freq_value_area);
let gauge = LineGauge::default()
.filled_style(Style::default().fg(colors.gauge_fg()).bg(colors.gauge_bg()))
.filled_symbol(symbols::line::THICK.horizontal)
.unfilled_symbol(symbols::line::THICK.horizontal)
.ratio(cpu.freq_ratio());
f.render_widget(gauge, freq_gauge_area);
}
fn draw_freq_table(f: &mut Frame, metrics: &Metrics, area: Rect) {
let e_cluster_frequencies = metrics
.e_clusters
.first()
.and_then(|c| c.cpus.first())
.map(|c| c.frequencies_mhz())
.unwrap_or_default();
let s_cluster_frequencies = metrics
.s_clusters
.first()
.and_then(|c| c.cpus.first())
.map(|c| c.frequencies_mhz())
.unwrap_or_default();
let p_cluster_frequencies = metrics
.p_clusters
.first()
.and_then(|c| c.cpus.first())
.map(|c| c.frequencies_mhz())
.unwrap_or_default();
let e_clus = e_cluster_frequencies
.iter()
.map(|f| format!("{:4}", *f))
.collect::<Vec<_>>()
.join(" ");
let s_clus = s_cluster_frequencies
.iter()
.map(|f| format!("{:4}", *f))
.collect::<Vec<_>>()
.join(" ");
let p_clus = p_cluster_frequencies
.iter()
.map(|f| format!("{:4}", *f))
.collect::<Vec<_>>()
.join(" ");
let mut row_content: Vec<(&str, String)> = vec![];
if !e_clus.is_empty() {
row_content.push(("E-Cluster:", e_clus));
}
if !p_clus.is_empty() {
row_content.push(("P-Cluster:", p_clus));
}
if !s_clus.is_empty() {
row_content.push(("S-Cluster:", s_clus));
}
row_content.push(("", "".into()));
row_content.push((
"Note:",
"Hardware-wise, CPUs quickly shift between the above frequencies.".into(),
));
let rows = row_content.iter().map(|(left, right)| {
Row::new(vec![
Cell::from(Span::from(*left)),
Cell::from(Span::styled(
right.as_str(),
Style::default().add_modifier(Modifier::BOLD),
)),
])
});
let label_width = 10;
let array_width = area.width - label_width - 2;
let constraints = [
Constraint::Length(label_width),
Constraint::Length(array_width),
];
let table = Table::new(rows, constraints)
.block(Block::default().borders(Borders::ALL).title("Frequencies"));
f.render_widget(table, area);
}