1use crate::{
14 analysis, comparison,
15 platform::prelude::*,
16 settings::{Color, Field, SettingsDescription, Value},
17 timing::Snapshot,
18 GeneralLayoutSettings, TimeSpan, Timer, TimerPhase,
19};
20use alloc::borrow::Cow;
21use serde::{Deserialize, Serialize};
22
23const WIDTH: f32 = 1.0;
24const HEIGHT: f32 = 1.0;
25const DEFAULT_X_AXIS: f32 = HEIGHT / 2.0;
26
27#[derive(Default, Clone)]
31pub struct Component {
32 settings: Settings,
33}
34
35#[derive(Clone, Serialize, Deserialize)]
37#[serde(default)]
38pub struct Settings {
39 pub comparison_override: Option<String>,
42 pub show_best_segments: bool,
45 pub live_graph: bool,
49 pub flip_graph: bool,
53 pub behind_background_color: Color,
56 pub ahead_background_color: Color,
59 pub grid_lines_color: Color,
61 pub graph_lines_color: Color,
63 pub partial_fill_color: Color,
68 pub complete_fill_color: Color,
71 pub height: u32,
73}
74
75#[derive(Default, Serialize, Deserialize)]
78pub struct State {
79 pub points: Vec<Point>,
84 pub horizontal_grid_lines: Vec<f32>,
86 pub vertical_grid_lines: Vec<f32>,
88 pub middle: f32,
90 pub is_live_delta_active: bool,
94 pub is_flipped: bool,
98 pub top_background_color: Color,
101 pub bottom_background_color: Color,
104 pub grid_lines_color: Color,
106 pub graph_lines_color: Color,
108 pub partial_fill_color: Color,
113 pub complete_fill_color: Color,
116 pub best_segment_color: Color,
119 pub height: u32,
121}
122
123#[derive(Serialize, Deserialize)]
125pub struct Point {
126 pub x: f32,
128 pub y: f32,
132 pub is_best_segment: bool,
135}
136
137impl Default for Settings {
138 fn default() -> Self {
139 Self {
140 comparison_override: None,
141 show_best_segments: false,
142 live_graph: true,
143 flip_graph: false,
144 behind_background_color: Color::rgba(115.0 / 255.0, 40.0 / 255.0, 40.0 / 255.0, 1.0),
145 ahead_background_color: Color::rgba(40.0 / 255.0, 115.0 / 255.0, 52.0 / 255.0, 1.0),
146 grid_lines_color: Color::rgba(0.0, 0.0, 0.0, 0.15),
147 graph_lines_color: Color::rgba(1.0, 1.0, 1.0, 1.0),
148 partial_fill_color: Color::rgba(1.0, 1.0, 1.0, 0.25),
149 complete_fill_color: Color::rgba(1.0, 1.0, 1.0, 0.4),
150 height: 80,
151 }
152 }
153}
154
155#[cfg(feature = "std")]
156impl State {
157 pub fn write_json<W>(&self, writer: W) -> serde_json::Result<()>
159 where
160 W: std::io::Write,
161 {
162 serde_json::to_writer(writer, self)
163 }
164}
165
166#[derive(Default)]
168struct DrawInfo {
169 points: Vec<Point>,
170 min_delta: f32,
172 max_delta: f32,
174 scale_factor_x: Option<f32>,
175 scale_factor_y: Option<f32>,
176 padding_y: f32,
177 split_index: usize,
178 flip_graph: bool,
179 is_live_delta_active: bool,
180}
181
182#[derive(Default)]
183struct GridLines {
184 horizontal: Option<(f32, f32)>,
186 vertical: Option<f32>,
187}
188
189impl Component {
190 pub fn new() -> Self {
192 Self::default()
193 }
194
195 pub const fn with_settings(settings: Settings) -> Self {
197 Self { settings }
198 }
199
200 pub const fn settings(&self) -> &Settings {
202 &self.settings
203 }
204
205 pub fn settings_mut(&mut self) -> &mut Settings {
207 &mut self.settings
208 }
209
210 pub fn name(&self) -> Cow<'static, str> {
212 self.text(
213 self.settings
214 .comparison_override
215 .as_ref()
216 .map(String::as_ref),
217 )
218 }
219
220 fn text(&self, comparison: Option<&str>) -> Cow<'static, str> {
221 if let Some(comparison) = comparison {
222 format!("Graph ({})", comparison::shorten(comparison)).into()
223 } else {
224 "Graph".into()
225 }
226 }
227
228 pub fn update_state(
231 &self,
232 state: &mut State,
233 timer: &Snapshot<'_>,
234 layout_settings: &GeneralLayoutSettings,
235 ) {
236 let mut draw_info = DrawInfo {
237 flip_graph: self.settings.flip_graph,
238 ..DrawInfo::default()
239 };
240
241 let x_axis = self
242 .calculate_graph(timer, &mut draw_info)
243 .unwrap_or(DEFAULT_X_AXIS);
244
245 if draw_info.points.is_empty() {
246 draw_info.points.push(Point {
247 x: 0.0,
248 y: DEFAULT_X_AXIS,
249 is_best_segment: false,
250 });
251 }
252
253 let grid_lines = calculate_grid_lines(&draw_info, x_axis);
254 update_grid_line_vecs(state, grid_lines);
255 self.copy_settings_to_state(state);
256 state.best_segment_color = layout_settings.best_segment_color;
257 state.middle = x_axis;
258 state.is_live_delta_active = draw_info.is_live_delta_active;
259 state.points = draw_info.points;
260 }
261
262 pub fn state(&self, timer: &Snapshot<'_>, layout_settings: &GeneralLayoutSettings) -> State {
265 let mut state = State::default();
266 self.update_state(&mut state, timer, layout_settings);
267 state
268 }
269
270 pub fn settings_description(&self) -> SettingsDescription {
273 SettingsDescription::with_fields(vec![
274 Field::new(
275 "Comparison".into(),
276 self.settings.comparison_override.clone().into(),
277 ),
278 Field::new("Height".into(), u64::from(self.settings.height).into()),
279 Field::new(
280 "Show Best Segments".into(),
281 self.settings.show_best_segments.into(),
282 ),
283 Field::new("Live Graph".into(), self.settings.live_graph.into()),
284 Field::new("Flip Graph".into(), self.settings.flip_graph.into()),
285 Field::new(
286 "Behind Background Color".into(),
287 self.settings.behind_background_color.into(),
288 ),
289 Field::new(
290 "Ahead Background Color".into(),
291 self.settings.ahead_background_color.into(),
292 ),
293 Field::new(
294 "Grid Lines Color".into(),
295 self.settings.grid_lines_color.into(),
296 ),
297 Field::new(
298 "Graph Lines Color".into(),
299 self.settings.graph_lines_color.into(),
300 ),
301 Field::new(
302 "Partial Fill Color".into(),
303 self.settings.partial_fill_color.into(),
304 ),
305 Field::new(
306 "Complete Fill Color".into(),
307 self.settings.complete_fill_color.into(),
308 ),
309 ])
310 }
311
312 pub fn set_value(&mut self, index: usize, value: Value) {
320 match index {
321 0 => self.settings.comparison_override = value.into(),
322 1 => self.settings.height = value.into_uint().unwrap() as _,
323 2 => self.settings.show_best_segments = value.into(),
324 3 => self.settings.live_graph = value.into(),
325 4 => self.settings.flip_graph = value.into(),
326 5 => self.settings.behind_background_color = value.into(),
327 6 => self.settings.ahead_background_color = value.into(),
328 7 => self.settings.grid_lines_color = value.into(),
329 8 => self.settings.graph_lines_color = value.into(),
330 9 => self.settings.partial_fill_color = value.into(),
331 10 => self.settings.complete_fill_color = value.into(),
332 _ => panic!("Unsupported Setting Index"),
333 }
334 }
335
336 fn calculate_graph(&self, timer: &Snapshot<'_>, draw_info: &mut DrawInfo) -> Option<f32> {
337 let settings = &self.settings;
338 draw_info.split_index = timer.current_split_index()?;
339 let comparison = comparison::resolve(&self.settings.comparison_override, timer);
340 let comparison = comparison::or_current(comparison, timer);
341
342 calculate_horizontal_scaling(timer, draw_info, settings.live_graph);
343 draw_info.scale_factor_x?;
344
345 draw_info.points = Vec::with_capacity(draw_info.split_index + 1);
346 draw_info.points.push(Point {
347 x: 0.0,
348 y: 0.0, is_best_segment: false,
350 });
351
352 calculate_split_points(timer, draw_info, comparison, settings.show_best_segments);
353 if settings.live_graph {
354 calculate_live_delta_point(timer, draw_info, comparison);
355 }
356
357 calculate_vertical_scaling(draw_info);
358 let x_axis = calculate_x_axis(draw_info);
359
360 transform_y_coordinates(draw_info);
361
362 Some(x_axis)
363 }
364
365 fn copy_settings_to_state(&self, state: &mut State) {
366 let settings = &self.settings;
367 (state.top_background_color, state.bottom_background_color) = if settings.flip_graph {
368 (
369 settings.ahead_background_color,
370 settings.behind_background_color,
371 )
372 } else {
373 (
374 settings.behind_background_color,
375 settings.ahead_background_color,
376 )
377 };
378
379 state.is_flipped = settings.flip_graph;
380 state.grid_lines_color = settings.grid_lines_color;
381 state.graph_lines_color = settings.graph_lines_color;
382 state.partial_fill_color = settings.partial_fill_color;
383 state.complete_fill_color = settings.complete_fill_color;
384 state.height = settings.height;
385 }
386}
387
388fn calculate_horizontal_scaling(timer: &Snapshot<'_>, draw_info: &mut DrawInfo, live_graph: bool) {
389 let timing_method = timer.current_timing_method();
390
391 let mut final_split = 0.0;
394 if live_graph {
395 let current_time = timer.current_time();
396 final_split = current_time[timing_method]
397 .or(current_time.real_time)
398 .unwrap_or_else(TimeSpan::zero)
399 .total_seconds() as f32;
400 } else {
401 for segment in timer.run().segments()[..draw_info.split_index].iter().rev() {
403 if let Some(time) = segment.split_time()[timing_method] {
404 final_split = time.total_seconds() as f32;
405 break;
406 }
407 }
408 }
409
410 if final_split > 0.0 {
411 draw_info.scale_factor_x = Some(WIDTH / final_split);
412 }
413
414 }
416
417fn calculate_split_points(
421 timer: &Timer,
422 draw_info: &mut DrawInfo,
423 comparison: &str,
424 show_best_segments: bool,
425) {
426 let timing_method = timer.current_timing_method();
427
428 for (i, segment) in timer.run().segments()[..draw_info.split_index]
429 .iter()
430 .enumerate()
431 {
432 catch! {
433 let split_time = segment.split_time()[timing_method]?;
434 let comparison_time = segment.comparison(comparison)[timing_method]?;
435 let delta = (split_time - comparison_time).total_seconds() as f32;
436
437 if delta > draw_info.max_delta {
438 draw_info.max_delta = delta;
439 } else if delta < draw_info.min_delta {
440 draw_info.min_delta = delta;
441 }
442
443 let x = split_time.total_seconds() as f32 * draw_info.scale_factor_x.unwrap_or(0.0);
444
445 let is_best_segment =
446 show_best_segments && analysis::check_best_segment(timer, i, timing_method);
447
448 draw_info.points.push(Point {
449 x,
450 y: delta, is_best_segment,
452 });
453 };
454 }
455}
456
457fn calculate_live_delta_point(timer: &Snapshot<'_>, draw_info: &mut DrawInfo, comparison: &str) {
458 if timer.current_phase() == TimerPhase::Ended {
459 return;
460 }
461
462 let timing_method = timer.current_timing_method();
463 let mut live_delta = analysis::check_live_delta(timer, true, comparison, timing_method);
464 let current_time = timer.current_time()[timing_method];
465 let current_split_comparison = timer
466 .run()
467 .segment(draw_info.split_index)
468 .comparison(comparison)[timing_method];
469
470 if let (Some(current_time), Some(current_split_comparison), None) =
471 (current_time, current_split_comparison, live_delta)
472 {
473 let delta = current_time - current_split_comparison;
475 if delta.total_seconds() as f32 > draw_info.min_delta {
476 live_delta = Some(delta);
477 }
478 }
479
480 if let Some(live_delta) = live_delta {
481 let delta = live_delta.total_seconds() as f32;
482 if delta > draw_info.max_delta {
483 draw_info.max_delta = delta;
484 } else if delta < draw_info.min_delta {
485 draw_info.min_delta = delta;
486 }
487
488 draw_info.points.push(Point {
489 x: WIDTH,
490 y: delta, is_best_segment: false,
492 });
493 draw_info.is_live_delta_active = true;
494 }
495}
496
497fn calculate_vertical_scaling(draw_info: &mut DrawInfo) {
503 const MIN_PADDING: f32 = HEIGHT / 24.0;
504 const MAX_CONTENT_HEIGHT: f32 = HEIGHT - MIN_PADDING * 2.0;
505 const SMOOTHNESS: f32 = 0.2;
508
509 let total_delta = draw_info.max_delta - draw_info.min_delta;
510 if total_delta > 0.0 {
511 draw_info.padding_y =
513 MAX_CONTENT_HEIGHT * SMOOTHNESS / (total_delta + SMOOTHNESS * 2.0) + MIN_PADDING;
514
515 let content_height = HEIGHT - draw_info.padding_y * 2.0;
516 draw_info.scale_factor_y = Some(content_height / total_delta);
517 }
518
519 }
522
523fn calculate_x_axis(draw_info: &DrawInfo) -> f32 {
524 if let Some(scale_factor_y) = draw_info.scale_factor_y {
525 let x_axis = draw_info.max_delta * scale_factor_y + draw_info.padding_y;
526 if draw_info.flip_graph {
527 HEIGHT - x_axis
528 } else {
529 x_axis
530 }
531 } else {
532 DEFAULT_X_AXIS
533 }
534}
535
536fn calculate_grid_lines(draw_info: &DrawInfo, x_axis: f32) -> GridLines {
537 const REDUCE_LINES_THRESHOLD_HORIZONTAL: f32 = HEIGHT / 6.0;
542 const REDUCE_LINES_THRESHOLD_VERTICAL: f32 = HEIGHT / 9.0;
543 const LINE_DISTANCE_FACTOR: f32 = 6.0;
545
546 let mut ret = GridLines::default();
547 if let Some(scale_factor_y) = draw_info.scale_factor_y {
548 let mut distance = scale_factor_y;
549 while distance < REDUCE_LINES_THRESHOLD_HORIZONTAL {
550 distance *= LINE_DISTANCE_FACTOR;
551 }
552
553 let offset = x_axis % distance;
555
556 ret.horizontal = Some((offset, distance));
557 } else {
558 ret.horizontal = Some((DEFAULT_X_AXIS, f32::INFINITY));
560 }
561
562 if let Some(scale_factor_x) = draw_info.scale_factor_x {
563 let mut distance = scale_factor_x;
564 while distance < REDUCE_LINES_THRESHOLD_VERTICAL {
565 distance *= LINE_DISTANCE_FACTOR;
566 }
567
568 ret.vertical = Some(distance);
569 }
570
571 ret
572}
573
574fn update_grid_line_vecs(state: &mut State, grid_lines: GridLines) {
576 state.horizontal_grid_lines.clear();
577 if let Some((offset, distance)) = grid_lines.horizontal {
578 let mut y = offset;
579 while y < HEIGHT {
580 state.horizontal_grid_lines.push(y);
581 y += distance;
582 }
583 }
584
585 state.vertical_grid_lines.clear();
586 if let Some(distance) = grid_lines.vertical {
587 let mut x = distance;
588 while x < WIDTH {
589 state.vertical_grid_lines.push(x);
590 x += distance;
591 }
592 }
593}
594
595fn transform_y_coordinates(draw_info: &mut DrawInfo) {
600 if let Some(scale_factor_y) = draw_info.scale_factor_y {
601 for point in &mut draw_info.points {
602 let delta = point.y;
603 point.y = (draw_info.max_delta - delta) * scale_factor_y + draw_info.padding_y;
604 if draw_info.flip_graph {
605 point.y = HEIGHT - point.y;
606 }
607 }
608 } else {
609 for point in &mut draw_info.points {
610 point.y = DEFAULT_X_AXIS;
611 }
612 }
613}