use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::Arc;
use crate::color::Color;
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult};
use crate::geometry::{Rect, Size};
use crate::text::Font;
use crate::widget::Widget;
use crate::widgets::Label;
mod run_mode;
pub use run_mode::{shared_run_mode, RunMode, RunModeDesc, RunModeRow};
pub struct FrameHistory {
times: Vec<f32>,
head: usize,
len: usize,
revision: u64,
}
impl FrameHistory {
pub const CAP: usize = 60;
pub fn new() -> Self {
Self {
times: vec![0.0; Self::CAP],
head: 0,
len: 0,
revision: 0,
}
}
pub fn push(&mut self, frame_ms: f32) {
self.times[self.head] = frame_ms;
self.head = (self.head + 1) % Self::CAP;
if self.len < Self::CAP {
self.len += 1;
}
self.revision = self.revision.wrapping_add(1);
}
pub fn revision(&self) -> u64 {
self.revision
}
pub fn mean_ms(&self) -> f32 {
if self.len == 0 {
return 0.0;
}
self.times[..self.len].iter().sum::<f32>() / self.len as f32
}
pub fn fps(&self) -> f32 {
let m = self.mean_ms();
if m < 0.001 {
0.0
} else {
1000.0 / m
}
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn samples(&self) -> impl Iterator<Item = f32> + '_ {
let cap = Self::CAP;
(0..self.len).map(move |i| {
let idx = (self.head + cap - self.len + i) % cap;
self.times[idx]
})
}
}
impl Default for FrameHistory {
fn default() -> Self {
Self::new()
}
}
pub type SharedFrameHistory = Rc<RefCell<FrameHistory>>;
pub fn shared_frame_history() -> SharedFrameHistory {
Rc::new(RefCell::new(FrameHistory::new()))
}
pub struct PerformanceView {
bounds: Rect,
children: Vec<Box<dyn Widget>>,
history: SharedFrameHistory,
sparkline_height: f64,
label_height: f64,
padding: f64,
show_background: bool,
live_redraw: bool,
redraw_on_history_change: bool,
last_painted_revision: Cell<u64>,
font: Arc<Font>,
selector: Option<SelectorLayout>,
run_mode: Option<Rc<Cell<RunMode>>>,
}
struct SelectorLayout {
mode_label_idx: usize,
mode_row_idx: usize,
desc_idx: usize,
mode_label_height: f64,
row_height: f64,
desc_height: f64,
inner_gap: f64,
separator_pad: f64,
}
impl PerformanceView {
pub fn new(font: Arc<Font>, history: SharedFrameHistory) -> Self {
let mut label =
Label::new("Mean CPU usage: 0.00 ms / frame", Arc::clone(&font)).with_font_size(11.0);
label.buffered = false;
Self {
bounds: Rect::default(),
children: vec![Box::new(label)],
history,
sparkline_height: 56.0,
label_height: 18.0,
padding: 12.0,
show_background: false,
live_redraw: false,
redraw_on_history_change: false,
last_painted_revision: Cell::new(0),
font,
selector: None,
run_mode: None,
}
}
pub fn with_run_mode_selector(mut self, run_mode: Rc<Cell<RunMode>>) -> Self {
let mode_label = Label::new("Mode", Arc::clone(&self.font)).with_font_size(11.0);
let mode_row = RunModeRow::new(Arc::clone(&self.font), Rc::clone(&run_mode));
let desc = RunModeDesc::new(
Arc::clone(&self.font),
Rc::clone(&run_mode),
Rc::clone(&self.history),
);
let mean_idx = 0;
let _ = mean_idx; let mode_label_idx = self.children.len();
self.children.push(Box::new(mode_label));
let mode_row_idx = self.children.len();
self.children.push(Box::new(mode_row));
let desc_idx = self.children.len();
self.children.push(Box::new(desc));
self.selector = Some(SelectorLayout {
mode_label_idx,
mode_row_idx,
desc_idx,
mode_label_height: 16.0,
row_height: RunModeRow::ROW_HEIGHT,
desc_height: 18.0,
inner_gap: 4.0,
separator_pad: 6.0,
});
self.run_mode = Some(run_mode);
self
}
pub fn run_mode(&self) -> Option<Rc<Cell<RunMode>>> {
self.run_mode.clone()
}
pub fn with_sparkline_height(mut self, h: f64) -> Self {
self.sparkline_height = h.max(8.0);
self
}
pub fn with_padding(mut self, p: f64) -> Self {
self.padding = p.max(0.0);
self
}
pub fn with_background(mut self, on: bool) -> Self {
self.show_background = on;
self
}
pub fn with_live_redraw(mut self, on: bool) -> Self {
self.live_redraw = on;
self
}
pub fn with_history_redraw(mut self, on: bool) -> Self {
self.redraw_on_history_change = on;
self
}
fn total_height(&self) -> f64 {
let base = self.label_height + self.sparkline_height + self.padding * 3.0;
match &self.selector {
Some(s) => {
base + s.mode_label_height
+ s.row_height
+ s.desc_height
+ s.inner_gap * 3.0
+ s.separator_pad * 2.0
}
None => base,
}
}
}
impl Widget for PerformanceView {
fn type_name(&self) -> &'static str {
"PerformanceView"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn layout(&mut self, available: Size) -> Size {
let w = available.width.max(1.0);
let h = self.total_height();
self.bounds = Rect::new(0.0, 0.0, w, h);
let inner_w = (w - self.padding * 2.0).max(1.0);
let mut cursor_top = h - self.padding;
if let Some(s) = &self.selector {
let row_top = cursor_top;
let row_bottom = row_top - s.mode_label_height;
let label_size =
self.children[s.mode_label_idx].layout(Size::new(inner_w, s.mode_label_height));
let label_y = row_bottom + (s.mode_label_height - label_size.height) * 0.5;
self.children[s.mode_label_idx].set_bounds(Rect::new(
self.padding,
label_y,
label_size.width,
label_size.height,
));
cursor_top = row_bottom - s.inner_gap;
let row_bottom = cursor_top - s.row_height;
self.children[s.mode_row_idx].layout(Size::new(inner_w, s.row_height));
self.children[s.mode_row_idx].set_bounds(Rect::new(
self.padding,
row_bottom,
inner_w,
s.row_height,
));
cursor_top = row_bottom - s.inner_gap;
let desc_size = self.children[s.desc_idx].layout(Size::new(inner_w, s.desc_height));
let desc_h = desc_size.height.max(s.desc_height);
let desc_bottom = cursor_top - desc_h;
self.children[s.desc_idx].set_bounds(Rect::new(
self.padding,
desc_bottom,
inner_w,
desc_h,
));
cursor_top = desc_bottom - s.separator_pad * 2.0 - s.inner_gap;
}
let mean_row_top = cursor_top;
let mean_row_bottom = mean_row_top - self.label_height;
let mean_size = self.children[0].layout(Size::new(inner_w, self.label_height));
let mean_y = mean_row_bottom + (self.label_height - mean_size.height) * 0.5;
self.children[0].set_bounds(Rect::new(
self.padding,
mean_y,
mean_size.width,
mean_size.height,
));
Size::new(w, h)
}
fn paint(&mut self, ctx: &mut dyn DrawCtx) {
let v = ctx.visuals();
let w = self.bounds.width;
let h = self.bounds.height;
if self.show_background {
ctx.set_fill_color(v.panel_fill);
ctx.begin_path();
ctx.rect(0.0, 0.0, w, h);
ctx.fill();
}
let (mean, revision) = {
let hist = self.history.borrow();
(hist.mean_ms(), hist.revision())
};
let text = format!("Mean CPU usage: {mean:.2} ms / frame");
self.children[0].set_label_text(&text);
self.children[0].set_label_color(v.text_dim);
if let Some(s) = &self.selector {
let desc_bottom = self.children[s.desc_idx].bounds().y;
let sep_y = desc_bottom - s.separator_pad;
if sep_y > self.padding {
ctx.set_stroke_color(v.separator);
ctx.set_line_width(1.0);
ctx.begin_path();
ctx.move_to(self.padding, sep_y);
ctx.line_to(w - self.padding, sep_y);
ctx.stroke();
}
}
if let Some(s) = &self.selector {
self.children[s.mode_label_idx].set_label_color(v.text_dim);
}
let sx = self.padding;
let sy = self.padding;
let sw = (w - self.padding * 2.0).max(1.0);
let sh = self.sparkline_height;
paint_sparkline(ctx, &self.history, sx, sy, sw, sh);
self.last_painted_revision.set(revision);
}
fn on_event(&mut self, _event: &Event) -> EventResult {
EventResult::Ignored
}
fn needs_draw(&self) -> bool {
if let Some(rm) = &self.run_mode {
if rm.get() == RunMode::Reactive {
return false;
}
}
self.live_redraw
|| (self.redraw_on_history_change
&& self.history.borrow().revision() != self.last_painted_revision.get())
}
}
pub fn paint_sparkline(
ctx: &mut dyn DrawCtx,
history: &SharedFrameHistory,
x: f64,
y: f64,
w: f64,
h: f64,
) {
let v = ctx.visuals();
let hist = history.borrow();
ctx.set_fill_color(v.track_bg);
ctx.begin_path();
ctx.rounded_rect(x, y, w, h, 4.0);
ctx.fill();
if hist.len() < 2 {
return;
}
let samples: Vec<f32> = hist.samples().collect();
let max_ms = samples.iter().cloned().fold(0.1_f32, f32::max).max(16.7);
ctx.set_stroke_color(v.accent);
ctx.set_line_width(1.5);
ctx.begin_path();
let n = samples.len();
for (i, &ms) in samples.iter().enumerate() {
let px = x + i as f64 / (n - 1) as f64 * w;
let py = y + (1.0 - ms as f64 / max_ms as f64) * (h - 4.0) + 2.0;
if i == 0 {
ctx.move_to(px, py);
} else {
ctx.line_to(px, py);
}
}
ctx.stroke();
let ref_y = y + (1.0 - 16.7 / max_ms as f64) * (h - 4.0) + 2.0;
if ref_y >= y + 2.0 && ref_y <= y + h - 2.0 {
ctx.set_stroke_color(Color::rgba(1.0, 0.6, 0.0, 0.7));
ctx.set_line_width(1.0);
ctx.begin_path();
ctx.move_to(x, ref_y);
ctx.line_to(x + w, ref_y);
ctx.stroke();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_history_mean_with_no_samples_is_zero() {
let h = FrameHistory::new();
assert_eq!(h.mean_ms(), 0.0);
assert_eq!(h.fps(), 0.0);
assert!(h.is_empty());
}
#[test]
fn frame_history_mean_averages_recent_samples() {
let mut h = FrameHistory::new();
h.push(10.0);
h.push(20.0);
h.push(30.0);
assert!((h.mean_ms() - 20.0).abs() < 0.001);
assert_eq!(h.len(), 3);
}
#[test]
fn frame_history_revision_increments_on_push() {
let mut h = FrameHistory::new();
assert_eq!(h.revision(), 0);
h.push(10.0);
assert_eq!(h.revision(), 1);
h.push(20.0);
assert_eq!(h.revision(), 2);
}
#[test]
fn frame_history_wraps_at_capacity() {
let mut h = FrameHistory::new();
for i in 0..(FrameHistory::CAP * 2) {
h.push(i as f32);
}
assert_eq!(h.len(), FrameHistory::CAP);
let cap = FrameHistory::CAP as f32;
let expected = (cap + (2.0 * cap - 1.0)) / 2.0;
assert!((h.mean_ms() - expected).abs() < 0.01);
}
#[test]
fn frame_history_samples_yield_oldest_first() {
let mut h = FrameHistory::new();
h.push(1.0);
h.push(2.0);
h.push(3.0);
let collected: Vec<f32> = h.samples().collect();
assert_eq!(collected, vec![1.0, 2.0, 3.0]);
}
}