use retroglyph_core::{Color, FrameStats, Rect, Style};
use super::{Panel, Sparkline, Text, Widget};
use crate::Surface;
use crate::Theme;
const DEFAULT_SPARKLINE_COLOR: Color = Color::Rgb {
r: 90,
g: 170,
b: 250,
};
#[derive(Clone, Copy, Debug)]
pub struct PerfOverlay<'a, const N: usize = 120> {
stats: &'a FrameStats<N>,
backend: &'a str,
title: &'a str,
metrics: &'a [(&'a str, &'a str)],
border_style: Style,
fill_style: Style,
text_style: Style,
sparkline_style: Style,
}
impl<'a, const N: usize> PerfOverlay<'a, N> {
#[must_use]
pub fn new(stats: &'a FrameStats<N>) -> Self {
Self {
stats,
backend: "",
title: "perf",
metrics: &[],
border_style: Style::new(),
fill_style: Style::new(),
text_style: Style::new(),
sparkline_style: Style::new().fg(DEFAULT_SPARKLINE_COLOR),
}
}
#[must_use]
pub const fn backend(mut self, backend: &'a str) -> Self {
self.backend = backend;
self
}
#[must_use]
pub const fn title(mut self, title: &'a str) -> Self {
self.title = title;
self
}
#[must_use]
pub const fn metrics(mut self, metrics: &'a [(&'a str, &'a str)]) -> Self {
self.metrics = metrics;
self
}
#[must_use]
pub const fn text_style(mut self, style: Style) -> Self {
self.text_style = style;
self
}
#[must_use]
pub const fn sparkline_style(mut self, style: Style) -> Self {
self.sparkline_style = style;
self
}
#[must_use]
pub fn theme(mut self, theme: Theme) -> Self {
self.border_style = Style::new().fg(theme.border).bg(theme.title_bg);
self.fill_style = Style::new().bg(theme.panel_bg);
self.text_style = Style::new().fg(theme.fg).bg(theme.panel_bg);
self.sparkline_style = Style::new().fg(theme.accent);
self
}
}
impl<const N: usize> Widget for PerfOverlay<'_, N> {
fn render(&self, area: Rect, surface: &mut Surface<'_>) {
if area.width() < 4 || area.height() < 3 {
return;
}
Panel::new()
.title(self.title)
.border_style(self.border_style)
.fill_style(self.fill_style)
.render(area, surface);
let inner = Rect::new(
area.left() + 1,
area.top() + 1,
area.width() - 2,
area.height() - 2,
);
let mut y = inner.top();
let row = |y: u16| Rect::new(inner.left(), y, inner.width(), 1);
if y < inner.bottom() {
let readout = if self.backend.is_empty() {
format!(
"{:>3.0}fps {:>4.1}ms min {:>4.1} max {:>4.1}",
self.stats.fps(),
millis(self.stats.current()),
millis(self.stats.min()),
millis(self.stats.max()),
)
} else {
format!(
"{:>3.0}fps {:>4.1}ms min {:>4.1} max {:>4.1} {}",
self.stats.fps(),
millis(self.stats.current()),
millis(self.stats.min()),
millis(self.stats.max()),
self.backend,
)
};
Text::new(&readout)
.style(self.text_style)
.render(row(y), surface);
y += 1;
}
for (label, value) in self.metrics {
if y >= inner.bottom() {
break;
}
let line = format!("{label}: {value}");
Text::new(&line)
.style(self.text_style)
.render(row(y), surface);
y += 1;
}
if y < inner.bottom() {
let mut samples = [0.0f32; N];
let mut len = 0;
for duration in self.stats.samples() {
if len >= N {
break;
}
samples[len] = millis(duration);
len += 1;
}
Sparkline::new(&samples[..len])
.style(self.sparkline_style)
.render(row(y), surface);
}
}
}
fn millis(duration: core::time::Duration) -> f32 {
duration.as_secs_f32() * 1000.0
}
#[cfg(test)]
mod tests {
use core::time::Duration;
use retroglyph_core::{Grid, Pos};
use super::*;
fn settled<const N: usize>(millis: u64, frames: usize) -> FrameStats<N> {
let mut stats = FrameStats::new();
for _ in 0..frames {
stats.record(Duration::from_millis(millis));
}
stats
}
#[test]
fn draws_a_readout_border_and_title() {
let stats = settled::<120>(16, 5);
let area = Rect::new(0, 0, 40, 5);
let mut grid = Grid::new(40, 5);
PerfOverlay::new(&stats).render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), '┌');
let title_row: String = (0..40).map(|x| grid[Pos::new(x, 0)].glyph()).collect();
assert!(title_row.contains("perf"));
let readout_row: String = (0..40).map(|x| grid[Pos::new(x, 1)].glyph()).collect();
assert!(readout_row.contains("fps"));
assert!(readout_row.contains("ms"));
assert!(readout_row.contains("min"));
}
#[test]
fn backend_label_is_appended_when_set() {
let stats = settled::<120>(16, 5);
let area = Rect::new(0, 0, 70, 5);
let mut grid = Grid::new(70, 5);
PerfOverlay::new(&stats)
.backend("software")
.render(area, &mut Surface::new(&mut grid, area, 0));
let readout_row: String = (0..70).map(|x| grid[Pos::new(x, 1)].glyph()).collect();
assert!(readout_row.contains("software"));
}
#[test]
fn backend_label_omitted_when_empty() {
let stats = settled::<120>(16, 5);
let area = Rect::new(0, 0, 40, 5);
let mut grid = Grid::new(40, 5);
PerfOverlay::new(&stats).render(area, &mut Surface::new(&mut grid, area, 0));
let readout_row: String = (0..40).map(|x| grid[Pos::new(x, 1)].glyph()).collect();
assert!(!readout_row.contains("softw"));
}
#[test]
fn extra_metric_rows_are_drawn_below_the_readout() {
let stats = settled::<120>(16, 5);
let area = Rect::new(0, 0, 40, 6);
let mut grid = Grid::new(40, 6);
PerfOverlay::new(&stats)
.metrics(&[("res", "80x24"), ("vsync", "on")])
.render(area, &mut Surface::new(&mut grid, area, 0));
let row2: String = (0..40).map(|x| grid[Pos::new(x, 2)].glyph()).collect();
let row3: String = (0..40).map(|x| grid[Pos::new(x, 3)].glyph()).collect();
assert!(row2.contains("res") && row2.contains("80x24"));
assert!(row3.contains("vsync") && row3.contains("on"));
}
#[test]
fn metrics_beyond_available_height_are_dropped_not_overflowed() {
let stats = settled::<120>(16, 5);
let area = Rect::new(0, 0, 40, 3);
let mut grid = Grid::new(40, 3);
PerfOverlay::new(&stats)
.metrics(&[("res", "80x24")])
.render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 2)].glyph(), '└', "bottom border intact");
}
#[test]
fn too_small_is_a_no_op() {
let stats = settled::<120>(16, 5);
let area = Rect::new(0, 0, 2, 2);
let mut grid = Grid::new(2, 2);
PerfOverlay::new(&stats).render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
}
#[test]
fn theme_maps_named_roles_onto_border_fill_and_text() {
let stats = settled::<120>(16, 5);
let area = Rect::new(0, 0, 40, 5);
let mut grid = Grid::new(40, 5);
PerfOverlay::new(&stats)
.theme(Theme::DARK)
.render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(
grid[Pos::new(0, 0)].style().foreground(),
Theme::DARK.border
);
assert_eq!(grid[Pos::new(1, 1)].style().foreground(), Theme::DARK.fg);
}
}