use crate::{
analysis, comparison,
platform::prelude::*,
settings::{Color, Field, SettingsDescription, Value},
timing::Snapshot,
GeneralLayoutSettings, TimeSpan, Timer, TimerPhase,
};
use alloc::borrow::Cow;
use serde::{Deserialize, Serialize};
const WIDTH: f32 = 1.0;
const HEIGHT: f32 = 1.0;
const DEFAULT_X_AXIS: f32 = HEIGHT / 2.0;
#[derive(Default, Clone)]
pub struct Component {
settings: Settings,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Settings {
pub comparison_override: Option<String>,
pub show_best_segments: bool,
pub live_graph: bool,
pub flip_graph: bool,
pub behind_background_color: Color,
pub ahead_background_color: Color,
pub grid_lines_color: Color,
pub graph_lines_color: Color,
pub partial_fill_color: Color,
pub complete_fill_color: Color,
pub height: u32,
}
#[derive(Default, Serialize, Deserialize)]
pub struct State {
pub points: Vec<Point>,
pub horizontal_grid_lines: Vec<f32>,
pub vertical_grid_lines: Vec<f32>,
pub middle: f32,
pub is_live_delta_active: bool,
pub is_flipped: bool,
pub top_background_color: Color,
pub bottom_background_color: Color,
pub grid_lines_color: Color,
pub graph_lines_color: Color,
pub partial_fill_color: Color,
pub complete_fill_color: Color,
pub best_segment_color: Color,
pub height: u32,
}
#[derive(Serialize, Deserialize)]
pub struct Point {
pub x: f32,
pub y: f32,
pub is_best_segment: bool,
}
impl Default for Settings {
fn default() -> Self {
Self {
comparison_override: None,
show_best_segments: false,
live_graph: true,
flip_graph: false,
behind_background_color: Color::rgba(115.0 / 255.0, 40.0 / 255.0, 40.0 / 255.0, 1.0),
ahead_background_color: Color::rgba(40.0 / 255.0, 115.0 / 255.0, 52.0 / 255.0, 1.0),
grid_lines_color: Color::rgba(0.0, 0.0, 0.0, 0.15),
graph_lines_color: Color::rgba(1.0, 1.0, 1.0, 1.0),
partial_fill_color: Color::rgba(1.0, 1.0, 1.0, 0.25),
complete_fill_color: Color::rgba(1.0, 1.0, 1.0, 0.4),
height: 80,
}
}
}
#[cfg(feature = "std")]
impl State {
pub fn write_json<W>(&self, writer: W) -> serde_json::Result<()>
where
W: std::io::Write,
{
serde_json::to_writer(writer, self)
}
}
#[derive(Default)]
struct DrawInfo {
points: Vec<Point>,
min_delta: f32,
max_delta: f32,
scale_factor_x: Option<f32>,
scale_factor_y: Option<f32>,
padding_y: f32,
split_index: usize,
flip_graph: bool,
is_live_delta_active: bool,
}
#[derive(Default)]
struct GridLines {
horizontal: Option<(f32, f32)>,
vertical: Option<f32>,
}
impl Component {
pub fn new() -> Self {
Self::default()
}
pub const fn with_settings(settings: Settings) -> Self {
Self { settings }
}
pub const fn settings(&self) -> &Settings {
&self.settings
}
pub fn settings_mut(&mut self) -> &mut Settings {
&mut self.settings
}
pub fn name(&self) -> Cow<'static, str> {
self.text(
self.settings
.comparison_override
.as_ref()
.map(String::as_ref),
)
}
fn text(&self, comparison: Option<&str>) -> Cow<'static, str> {
if let Some(comparison) = comparison {
format!("Graph ({})", comparison::shorten(comparison)).into()
} else {
"Graph".into()
}
}
pub fn update_state(
&self,
state: &mut State,
timer: &Snapshot<'_>,
layout_settings: &GeneralLayoutSettings,
) {
let mut draw_info = DrawInfo {
flip_graph: self.settings.flip_graph,
..DrawInfo::default()
};
let x_axis = self
.calculate_graph(timer, &mut draw_info)
.unwrap_or(DEFAULT_X_AXIS);
if draw_info.points.is_empty() {
draw_info.points.push(Point {
x: 0.0,
y: DEFAULT_X_AXIS,
is_best_segment: false,
});
}
let grid_lines = calculate_grid_lines(&draw_info, x_axis);
update_grid_line_vecs(state, grid_lines);
self.copy_settings_to_state(state);
state.best_segment_color = layout_settings.best_segment_color;
state.middle = x_axis;
state.is_live_delta_active = draw_info.is_live_delta_active;
state.points = draw_info.points;
}
pub fn state(&self, timer: &Snapshot<'_>, layout_settings: &GeneralLayoutSettings) -> State {
let mut state = State::default();
self.update_state(&mut state, timer, layout_settings);
state
}
pub fn settings_description(&self) -> SettingsDescription {
SettingsDescription::with_fields(vec![
Field::new(
"Comparison".into(),
self.settings.comparison_override.clone().into(),
),
Field::new("Height".into(), u64::from(self.settings.height).into()),
Field::new(
"Show Best Segments".into(),
self.settings.show_best_segments.into(),
),
Field::new("Live Graph".into(), self.settings.live_graph.into()),
Field::new("Flip Graph".into(), self.settings.flip_graph.into()),
Field::new(
"Behind Background Color".into(),
self.settings.behind_background_color.into(),
),
Field::new(
"Ahead Background Color".into(),
self.settings.ahead_background_color.into(),
),
Field::new(
"Grid Lines Color".into(),
self.settings.grid_lines_color.into(),
),
Field::new(
"Graph Lines Color".into(),
self.settings.graph_lines_color.into(),
),
Field::new(
"Partial Fill Color".into(),
self.settings.partial_fill_color.into(),
),
Field::new(
"Complete Fill Color".into(),
self.settings.complete_fill_color.into(),
),
])
}
pub fn set_value(&mut self, index: usize, value: Value) {
match index {
0 => self.settings.comparison_override = value.into(),
1 => self.settings.height = value.into_uint().unwrap() as _,
2 => self.settings.show_best_segments = value.into(),
3 => self.settings.live_graph = value.into(),
4 => self.settings.flip_graph = value.into(),
5 => self.settings.behind_background_color = value.into(),
6 => self.settings.ahead_background_color = value.into(),
7 => self.settings.grid_lines_color = value.into(),
8 => self.settings.graph_lines_color = value.into(),
9 => self.settings.partial_fill_color = value.into(),
10 => self.settings.complete_fill_color = value.into(),
_ => panic!("Unsupported Setting Index"),
}
}
fn calculate_graph(&self, timer: &Snapshot<'_>, draw_info: &mut DrawInfo) -> Option<f32> {
let settings = &self.settings;
draw_info.split_index = timer.current_split_index()?;
let comparison = comparison::resolve(&self.settings.comparison_override, timer);
let comparison = comparison::or_current(comparison, timer);
calculate_horizontal_scaling(timer, draw_info, settings.live_graph);
draw_info.scale_factor_x?;
draw_info.points = Vec::with_capacity(draw_info.split_index + 1);
draw_info.points.push(Point {
x: 0.0,
y: 0.0, is_best_segment: false,
});
calculate_split_points(timer, draw_info, comparison, settings.show_best_segments);
if settings.live_graph {
calculate_live_delta_point(timer, draw_info, comparison);
}
calculate_vertical_scaling(draw_info);
let x_axis = calculate_x_axis(draw_info);
transform_y_coordinates(draw_info);
Some(x_axis)
}
fn copy_settings_to_state(&self, state: &mut State) {
let settings = &self.settings;
(state.top_background_color, state.bottom_background_color) = if settings.flip_graph {
(
settings.ahead_background_color,
settings.behind_background_color,
)
} else {
(
settings.behind_background_color,
settings.ahead_background_color,
)
};
state.is_flipped = settings.flip_graph;
state.grid_lines_color = settings.grid_lines_color;
state.graph_lines_color = settings.graph_lines_color;
state.partial_fill_color = settings.partial_fill_color;
state.complete_fill_color = settings.complete_fill_color;
state.height = settings.height;
}
}
fn calculate_horizontal_scaling(timer: &Snapshot<'_>, draw_info: &mut DrawInfo, live_graph: bool) {
let timing_method = timer.current_timing_method();
let mut final_split = 0.0;
if live_graph {
let current_time = timer.current_time();
final_split = current_time[timing_method]
.or(current_time.real_time)
.unwrap_or_else(TimeSpan::zero)
.total_seconds() as f32;
} else {
for segment in timer.run().segments()[..draw_info.split_index].iter().rev() {
if let Some(time) = segment.split_time()[timing_method] {
final_split = time.total_seconds() as f32;
break;
}
}
}
if final_split > 0.0 {
draw_info.scale_factor_x = Some(WIDTH / final_split);
}
}
fn calculate_split_points(
timer: &Timer,
draw_info: &mut DrawInfo,
comparison: &str,
show_best_segments: bool,
) {
let timing_method = timer.current_timing_method();
for (i, segment) in timer.run().segments()[..draw_info.split_index]
.iter()
.enumerate()
{
catch! {
let split_time = segment.split_time()[timing_method]?;
let comparison_time = segment.comparison(comparison)[timing_method]?;
let delta = (split_time - comparison_time).total_seconds() as f32;
if delta > draw_info.max_delta {
draw_info.max_delta = delta;
} else if delta < draw_info.min_delta {
draw_info.min_delta = delta;
}
let x = split_time.total_seconds() as f32 * draw_info.scale_factor_x.unwrap_or(0.0);
let is_best_segment =
show_best_segments && analysis::check_best_segment(timer, i, timing_method);
draw_info.points.push(Point {
x,
y: delta, is_best_segment,
});
};
}
}
fn calculate_live_delta_point(timer: &Snapshot<'_>, draw_info: &mut DrawInfo, comparison: &str) {
if timer.current_phase() == TimerPhase::Ended {
return;
}
let timing_method = timer.current_timing_method();
let mut live_delta = analysis::check_live_delta(timer, true, comparison, timing_method);
let current_time = timer.current_time()[timing_method];
let current_split_comparison = timer
.run()
.segment(draw_info.split_index)
.comparison(comparison)[timing_method];
if let (Some(current_time), Some(current_split_comparison), None) =
(current_time, current_split_comparison, live_delta)
{
let delta = current_time - current_split_comparison;
if delta.total_seconds() as f32 > draw_info.min_delta {
live_delta = Some(delta);
}
}
if let Some(live_delta) = live_delta {
let delta = live_delta.total_seconds() as f32;
if delta > draw_info.max_delta {
draw_info.max_delta = delta;
} else if delta < draw_info.min_delta {
draw_info.min_delta = delta;
}
draw_info.points.push(Point {
x: WIDTH,
y: delta, is_best_segment: false,
});
draw_info.is_live_delta_active = true;
}
}
fn calculate_vertical_scaling(draw_info: &mut DrawInfo) {
const MIN_PADDING: f32 = HEIGHT / 24.0;
const MAX_CONTENT_HEIGHT: f32 = HEIGHT - MIN_PADDING * 2.0;
const SMOOTHNESS: f32 = 0.2;
let total_delta = draw_info.max_delta - draw_info.min_delta;
if total_delta > 0.0 {
draw_info.padding_y =
MAX_CONTENT_HEIGHT * SMOOTHNESS / (total_delta + SMOOTHNESS * 2.0) + MIN_PADDING;
let content_height = HEIGHT - draw_info.padding_y * 2.0;
draw_info.scale_factor_y = Some(content_height / total_delta);
}
}
fn calculate_x_axis(draw_info: &DrawInfo) -> f32 {
if let Some(scale_factor_y) = draw_info.scale_factor_y {
let x_axis = draw_info.max_delta * scale_factor_y + draw_info.padding_y;
if draw_info.flip_graph {
HEIGHT - x_axis
} else {
x_axis
}
} else {
DEFAULT_X_AXIS
}
}
fn calculate_grid_lines(draw_info: &DrawInfo, x_axis: f32) -> GridLines {
const REDUCE_LINES_THRESHOLD_HORIZONTAL: f32 = HEIGHT / 6.0;
const REDUCE_LINES_THRESHOLD_VERTICAL: f32 = HEIGHT / 9.0;
const LINE_DISTANCE_FACTOR: f32 = 6.0;
let mut ret = GridLines::default();
if let Some(scale_factor_y) = draw_info.scale_factor_y {
let mut distance = scale_factor_y;
while distance < REDUCE_LINES_THRESHOLD_HORIZONTAL {
distance *= LINE_DISTANCE_FACTOR;
}
let offset = x_axis % distance;
ret.horizontal = Some((offset, distance));
} else {
ret.horizontal = Some((DEFAULT_X_AXIS, f32::INFINITY));
}
if let Some(scale_factor_x) = draw_info.scale_factor_x {
let mut distance = scale_factor_x;
while distance < REDUCE_LINES_THRESHOLD_VERTICAL {
distance *= LINE_DISTANCE_FACTOR;
}
ret.vertical = Some(distance);
}
ret
}
fn update_grid_line_vecs(state: &mut State, grid_lines: GridLines) {
state.horizontal_grid_lines.clear();
if let Some((offset, distance)) = grid_lines.horizontal {
let mut y = offset;
while y < HEIGHT {
state.horizontal_grid_lines.push(y);
y += distance;
}
}
state.vertical_grid_lines.clear();
if let Some(distance) = grid_lines.vertical {
let mut x = distance;
while x < WIDTH {
state.vertical_grid_lines.push(x);
x += distance;
}
}
}
fn transform_y_coordinates(draw_info: &mut DrawInfo) {
if let Some(scale_factor_y) = draw_info.scale_factor_y {
for point in &mut draw_info.points {
let delta = point.y;
point.y = (draw_info.max_delta - delta) * scale_factor_y + draw_info.padding_y;
if draw_info.flip_graph {
point.y = HEIGHT - point.y;
}
}
} else {
for point in &mut draw_info.points {
point.y = DEFAULT_X_AXIS;
}
}
}