From b3e7ac879c27b73c121b77894be9ae2e1288f217 Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Thu, 24 Feb 2022 02:33:31 +0100 Subject: [PATCH] Implement Columns that show Variables This implements columns for the splits component that can visualize the custom variables of the run at each split. This is mostly only useful in combination with an auto splitter that provides the variables. --- src/component/splits/column.rs | 99 +++++++++++++++-- src/component/splits/mod.rs | 156 ++++++++++++++++++--------- src/component/splits/tests/column.rs | 31 ++++-- src/component/splits/tests/mod.rs | 14 ++- src/layout/parser/splits.rs | 59 ++++++---- src/run/segment.rs | 26 +++++ src/settings/mod.rs | 2 +- src/settings/value.rs | 31 ++++++ src/timing/timer/mod.rs | 24 +++-- src/util/clear_vec.rs | 2 +- 10 files changed, 342 insertions(+), 102 deletions(-) diff --git a/src/component/splits/column.rs b/src/component/splits/column.rs index 67aa75a9..0b77914d 100644 --- a/src/component/splits/column.rs +++ b/src/component/splits/column.rs @@ -21,6 +21,26 @@ use serde::{Deserialize, Serialize}; pub struct ColumnSettings { /// The name of the column. pub name: String, + /// The kind of the column. + #[serde(flatten)] + pub kind: ColumnKind, +} + +/// The kind of a column. It can either be a column that shows a variable or a +/// time. +#[derive(Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ColumnKind { + /// A column that shows a variable. + Variable(VariableColumn), + /// A column that shows a time. + Time(TimeColumn), +} + +/// A column that shows a time. +#[derive(Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct TimeColumn { /// Specifies the value a segment starts out with before it gets replaced /// with the current attempt's information when splitting. pub start_with: ColumnStartWith, @@ -39,6 +59,13 @@ pub struct ColumnSettings { pub timing_method: Option, } +/// A column that shows a variable. +#[derive(Default, Clone, Serialize, Deserialize)] +pub struct VariableColumn { + /// The name of the variable to visualize. + pub variable_name: String, +} + /// Specifies the value a segment starts out with before it gets replaced /// with the current attempt's information when splitting. #[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -107,6 +134,14 @@ impl Default for ColumnSettings { fn default() -> Self { ColumnSettings { name: String::from("Column"), + kind: ColumnKind::Time(TimeColumn::default()), + } + } +} + +impl Default for TimeColumn { + fn default() -> Self { + TimeColumn { start_with: ColumnStartWith::Empty, update_with: ColumnUpdateWith::DontUpdate, update_trigger: ColumnUpdateTrigger::Contextual, @@ -152,12 +187,62 @@ pub fn update_state( segment_index: usize, current_split: Option, method: TimingMethod, +) { + match &column_settings.kind { + ColumnKind::Variable(column) => { + state.value.clear(); + if let Some(value) = segment.variables().get(column.variable_name.as_str()) { + state.value.push_str(value); + } else if Some(segment_index) == current_split { + if let Some(value) = timer + .run() + .metadata() + .custom_variable_value(column.variable_name.as_str()) + { + // FIXME: We show the live value of the variable, which means it + // might update frequently. So we possibly should mark it as + // such. However it's currently impossible to tell if it + // actually does update frequently. On top of that, the text + // component would need to support this as well, as it also + // shows the live value of the variable. + state.value.push_str(value); + } + } + state.semantic_color = SemanticColor::Default; + state.visual_color = layout_settings.text_color; + state.updates_frequently = false; + } + ColumnKind::Time(column) => { + update_time_column( + state, + column, + timer, + splits_settings, + layout_settings, + segment, + segment_index, + current_split, + method, + ); + } + } +} + +fn update_time_column( + state: &mut ColumnState, + column_settings: &TimeColumn, + timer: &Snapshot<'_>, + splits_settings: &SplitsSettings, + layout_settings: &GeneralLayoutSettings, + segment: &Segment, + segment_index: usize, + current_split: Option, + method: TimingMethod, ) { let method = column_settings.timing_method.unwrap_or(method); let resolved_comparison = comparison::resolve(&column_settings.comparison_override, timer); let comparison = comparison::or_current(resolved_comparison, timer); - - let update_value = column_update_value( + let update_value = time_column_update_value( column_settings, timer, segment, @@ -166,9 +251,7 @@ pub fn update_state( method, comparison, ); - let updated = update_value.is_some(); - let ((column_value, semantic_color, formatter), is_live) = update_value.unwrap_or_else(|| { ( match column_settings.start_with { @@ -197,11 +280,8 @@ pub fn update_state( false, ) }); - let is_empty = column_settings.start_with == ColumnStartWith::Empty && !updated; - state.updates_frequently = is_live && column_value.is_some(); - state.value.clear(); if !is_empty { let _ = match formatter { @@ -229,13 +309,12 @@ pub fn update_state( } }; } - state.semantic_color = semantic_color; state.visual_color = semantic_color.visualize(layout_settings); } -fn column_update_value( - column: &ColumnSettings, +fn time_column_update_value( + column: &TimeColumn, timer: &Snapshot<'_>, segment: &Segment, segment_index: usize, diff --git a/src/component/splits/mod.rs b/src/component/splits/mod.rs index 6cb58037..bc3b8462 100644 --- a/src/component/splits/mod.rs +++ b/src/component/splits/mod.rs @@ -8,7 +8,8 @@ use crate::{ platform::prelude::*, settings::{ - CachedImageId, Color, Field, Gradient, ImageData, ListGradient, SettingsDescription, Value, + self, CachedImageId, Color, Field, Gradient, ImageData, ListGradient, SettingsDescription, + Value, }, timing::{formatter::Accuracy, Snapshot}, util::{Clear, ClearVec}, @@ -23,11 +24,13 @@ mod tests; mod column; pub use column::{ - ColumnSettings, ColumnStartWith, ColumnState, ColumnUpdateTrigger, ColumnUpdateWith, + ColumnKind, ColumnSettings, ColumnStartWith, ColumnState, ColumnUpdateTrigger, + ColumnUpdateWith, TimeColumn, VariableColumn, }; const SETTINGS_BEFORE_COLUMNS: usize = 15; -const SETTINGS_PER_COLUMN: usize = 6; +const SETTINGS_PER_TIME_COLUMN: usize = 6; +const SETTINGS_PER_VARIABLE_COLUMN: usize = 2; /// The Splits Component is the main component for visualizing all the split /// times. Each segment is shown in a tabular fashion showing the segment icon, @@ -202,19 +205,23 @@ impl Default for Settings { columns: vec![ ColumnSettings { name: String::from("Time"), - start_with: ColumnStartWith::ComparisonTime, - update_with: ColumnUpdateWith::SplitTime, - update_trigger: ColumnUpdateTrigger::OnEndingSegment, - comparison_override: None, - timing_method: None, + kind: ColumnKind::Time(TimeColumn { + start_with: ColumnStartWith::ComparisonTime, + update_with: ColumnUpdateWith::SplitTime, + update_trigger: ColumnUpdateTrigger::OnEndingSegment, + comparison_override: None, + timing_method: None, + }), }, ColumnSettings { name: String::from("+/−"), - start_with: ColumnStartWith::Empty, - update_with: ColumnUpdateWith::Delta, - update_trigger: ColumnUpdateTrigger::Contextual, - comparison_override: None, - timing_method: None, + kind: ColumnKind::Time(TimeColumn { + start_with: ColumnStartWith::Empty, + update_with: ColumnUpdateWith::Delta, + update_trigger: ColumnUpdateTrigger::Contextual, + comparison_override: None, + timing_method: None, + }), }, ], } @@ -506,32 +513,58 @@ impl Component { ), ]); - settings - .fields - .reserve_exact(SETTINGS_PER_COLUMN * self.settings.columns.len()); + settings.fields.reserve_exact( + self.settings + .columns + .iter() + .map(|column| match column.kind { + ColumnKind::Variable(_) => SETTINGS_PER_VARIABLE_COLUMN, + ColumnKind::Time(_) => SETTINGS_PER_TIME_COLUMN, + }) + .sum(), + ); for column in &self.settings.columns { settings .fields .push(Field::new("Column Name".into(), column.name.clone().into())); - settings - .fields - .push(Field::new("Start With".into(), column.start_with.into())); - settings - .fields - .push(Field::new("Update With".into(), column.update_with.into())); - settings.fields.push(Field::new( - "Update Trigger".into(), - column.update_trigger.into(), - )); - settings.fields.push(Field::new( - "Comparison".into(), - column.comparison_override.clone().into(), - )); - settings.fields.push(Field::new( - "Timing Method".into(), - column.timing_method.into(), - )); + + match &column.kind { + ColumnKind::Variable(column) => { + settings.fields.push(Field::new( + "Column Type".into(), + settings::ColumnKind::Variable.into(), + )); + settings.fields.push(Field::new( + "Variable Name".into(), + column.variable_name.clone().into(), + )); + } + ColumnKind::Time(column) => { + settings.fields.push(Field::new( + "Column Type".into(), + settings::ColumnKind::Time.into(), + )); + settings + .fields + .push(Field::new("Start With".into(), column.start_with.into())); + settings + .fields + .push(Field::new("Update With".into(), column.update_with.into())); + settings.fields.push(Field::new( + "Update Trigger".into(), + column.update_trigger.into(), + )); + settings.fields.push(Field::new( + "Comparison".into(), + column.comparison_override.clone().into(), + )); + settings.fields.push(Field::new( + "Timing Method".into(), + column.timing_method.into(), + )); + } + } } settings @@ -565,22 +598,49 @@ impl Component { self.settings.columns.resize(new_len, Default::default()); } index => { - let index = index - SETTINGS_BEFORE_COLUMNS; - let column_index = index / SETTINGS_PER_COLUMN; - let setting_index = index % SETTINGS_PER_COLUMN; - if let Some(column) = self.settings.columns.get_mut(column_index) { - match setting_index { - 0 => column.name = value.into(), - 1 => column.start_with = value.into(), - 2 => column.update_with = value.into(), - 3 => column.update_trigger = value.into(), - 4 => column.comparison_override = value.into(), - 5 => column.timing_method = value.into(), - _ => unreachable!(), + let mut index = index - SETTINGS_BEFORE_COLUMNS; + for column in &mut self.settings.columns { + if index < 2 { + match index { + 0 => column.name = value.into(), + _ => { + column.kind = match settings::ColumnKind::from(value) { + settings::ColumnKind::Time => { + ColumnKind::Time(Default::default()) + } + settings::ColumnKind::Variable => { + ColumnKind::Variable(Default::default()) + } + } + } + } + return; + } + index -= 2; + match &mut column.kind { + ColumnKind::Variable(column) => { + if index < 1 { + column.variable_name = value.into(); + return; + } + index -= 1; + } + ColumnKind::Time(column) => { + if index < 5 { + match index { + 0 => column.start_with = value.into(), + 1 => column.update_with = value.into(), + 2 => column.update_trigger = value.into(), + 3 => column.comparison_override = value.into(), + _ => column.timing_method = value.into(), + } + return; + } + index -= 5; + } } - } else { - panic!("Unsupported Setting Index") } + panic!("Unsupported Setting Index") } } } diff --git a/src/component/splits/tests/column.rs b/src/component/splits/tests/column.rs index e92aafd5..e734e747 100644 --- a/src/component/splits/tests/column.rs +++ b/src/component/splits/tests/column.rs @@ -3,6 +3,7 @@ use super::{ State, }; use crate::{ + component::splits::{ColumnKind, TimeColumn}, settings::SemanticColor::{ self, AheadGainingTime as AheadGaining, BehindLosingTime as BehindLosing, BestSegment as Best, Default as Text, @@ -490,8 +491,11 @@ fn check_columns( let layout_settings = Default::default(); let mut component = Component::with_settings(Settings { columns: vec![ColumnSettings { - start_with, - update_with, + kind: ColumnKind::Time(TimeColumn { + start_with, + update_with, + ..Default::default() + }), ..Default::default() }], fill_with_blank_space: false, @@ -1079,9 +1083,12 @@ fn check_columns_update_trigger( let layout_settings = Default::default(); let mut component = Component::with_settings(Settings { columns: vec![ColumnSettings { - start_with: ColumnStartWith::Empty, - update_with, - update_trigger, + kind: ColumnKind::Time(TimeColumn { + start_with: ColumnStartWith::Empty, + update_with, + update_trigger, + ..Default::default() + }), ..Default::default() }], fill_with_blank_space: false, @@ -1157,8 +1164,11 @@ fn column_delta_best_segment_colors() { let layout_settings = Default::default(); let mut component = Component::with_settings(Settings { columns: vec![ColumnSettings { - start_with: ColumnStartWith::Empty, - update_with: ColumnUpdateWith::Delta, + kind: ColumnKind::Time(TimeColumn { + start_with: ColumnStartWith::Empty, + update_with: ColumnUpdateWith::Delta, + ..Default::default() + }), ..Default::default() }], fill_with_blank_space: false, @@ -1249,8 +1259,11 @@ fn delta_or_split_time() { let layout_settings = Default::default(); let mut component = Component::with_settings(Settings { columns: vec![ColumnSettings { - start_with: ColumnStartWith::ComparisonTime, - update_with: ColumnUpdateWith::DeltaWithFallback, + kind: ColumnKind::Time(TimeColumn { + start_with: ColumnStartWith::ComparisonTime, + update_with: ColumnUpdateWith::DeltaWithFallback, + ..Default::default() + }), ..Default::default() }], fill_with_blank_space: false, diff --git a/src/component/splits/tests/mod.rs b/src/component/splits/tests/mod.rs index 9a8e5c5f..d20404ca 100644 --- a/src/component/splits/tests/mod.rs +++ b/src/component/splits/tests/mod.rs @@ -2,7 +2,10 @@ use super::{ ColumnSettings, ColumnStartWith, ColumnUpdateTrigger, ColumnUpdateWith, Component, Settings, State, }; -use crate::{Run, Segment, TimeSpan, Timer, TimingMethod}; +use crate::{ + component::splits::{ColumnKind, TimeColumn}, + Run, Segment, TimeSpan, Timer, TimingMethod, +}; pub mod column; @@ -85,9 +88,12 @@ fn negative_segment_times() { let layout_settings = Default::default(); let mut component = Component::with_settings(Settings { columns: vec![ColumnSettings { - start_with: ColumnStartWith::Empty, - update_with: ColumnUpdateWith::SegmentTime, - update_trigger: ColumnUpdateTrigger::OnStartingSegment, + kind: ColumnKind::Time(TimeColumn { + start_with: ColumnStartWith::Empty, + update_with: ColumnUpdateWith::SegmentTime, + update_trigger: ColumnUpdateTrigger::OnStartingSegment, + ..Default::default() + }), ..Default::default() }], ..Default::default() diff --git a/src/layout/parser/splits.rs b/src/layout/parser/splits.rs index 567037ed..b759773e 100644 --- a/src/layout/parser/splits.rs +++ b/src/layout/parser/splits.rs @@ -3,7 +3,10 @@ use super::{ timing_method_override, Error, GradientBuilder, GradientKind, ListGradientKind, Result, }; use crate::{ - component::splits, + component::splits::{ + self, ColumnKind, ColumnSettings, ColumnStartWith, ColumnUpdateTrigger, ColumnUpdateWith, + TimeColumn, + }, platform::prelude::*, util::xml::{helper::text_as_escaped_string_err, Reader}, }; @@ -44,9 +47,10 @@ pub fn settings(reader: &mut Reader<'_>, component: &mut Component) -> Result<() settings.columns.clear(); parse_children(reader, |reader, _, _| { - let mut column = splits::ColumnSettings::default(); + let mut column_name = String::new(); + let mut column = TimeColumn::default(); parse_children(reader, |reader, tag, _| match tag.name() { - "Name" => text(reader, |v| column.name = v.into_owned()), + "Name" => text(reader, |v| column_name = v.into_owned()), "Comparison" => { comparison_override(reader, |v| column.comparison_override = v) } @@ -54,10 +58,6 @@ pub fn settings(reader: &mut Reader<'_>, component: &mut Component) -> Result<() timing_method_override(reader, |v| column.timing_method = v) } "Type" => text_as_escaped_string_err(reader, |v| { - use self::splits::{ - ColumnStartWith, ColumnUpdateTrigger, ColumnUpdateWith, - }; - ( column.start_with, column.update_with, @@ -100,7 +100,13 @@ pub fn settings(reader: &mut Reader<'_>, component: &mut Component) -> Result<() }), _ => end_tag(reader), })?; - settings.columns.insert(0, column); + settings.columns.insert( + 0, + splits::ColumnSettings { + name: column_name, + kind: ColumnKind::Time(column), + }, + ); Ok(()) }) } @@ -108,35 +114,42 @@ pub fn settings(reader: &mut Reader<'_>, component: &mut Component) -> Result<() // Version < 1.5 comparison_override(reader, |v| { for column in &mut settings.columns { - column.comparison_override = v.clone(); + if let ColumnKind::Time(column) = &mut column.kind { + column.comparison_override = v.clone(); + } } }) } "ShowSplitTimes" => { // Version < 1.5 - use self::splits::{ - ColumnSettings, ColumnStartWith, ColumnUpdateTrigger, ColumnUpdateWith, - }; parse_bool(reader, |b| { if !b { let comparison_override = - settings.columns.pop().and_then(|c| c.comparison_override); + settings.columns.pop().and_then(|c| match c.kind { + ColumnKind::Variable(_) => None, + ColumnKind::Time(c) => c.comparison_override, + }); + settings.columns.clear(); settings.columns.push(ColumnSettings { name: String::from("Time"), - start_with: ColumnStartWith::ComparisonTime, - update_with: ColumnUpdateWith::SplitTime, - update_trigger: ColumnUpdateTrigger::OnEndingSegment, - comparison_override: comparison_override.clone(), - timing_method: None, + kind: ColumnKind::Time(TimeColumn { + start_with: ColumnStartWith::ComparisonTime, + update_with: ColumnUpdateWith::SplitTime, + update_trigger: ColumnUpdateTrigger::OnEndingSegment, + comparison_override: comparison_override.clone(), + timing_method: None, + }), }); settings.columns.push(ColumnSettings { name: String::from("+/−"), - start_with: ColumnStartWith::Empty, - update_with: ColumnUpdateWith::Delta, - update_trigger: ColumnUpdateTrigger::Contextual, - comparison_override, - timing_method: None, + kind: ColumnKind::Time(TimeColumn { + start_with: ColumnStartWith::Empty, + update_with: ColumnUpdateWith::Delta, + update_trigger: ColumnUpdateTrigger::Contextual, + comparison_override, + timing_method: None, + }), }); } }) diff --git a/src/run/segment.rs b/src/run/segment.rs index 2f78924a..dcda3229 100644 --- a/src/run/segment.rs +++ b/src/run/segment.rs @@ -1,3 +1,5 @@ +use hashbrown::HashMap; + use super::Comparisons; use crate::{ comparison::personal_best, platform::prelude::*, settings::Image, util::PopulateString, @@ -26,6 +28,7 @@ pub struct Segment { split_time: Time, segment_history: SegmentHistory, comparisons: Comparisons, + variables: HashMap, } impl Segment { @@ -176,4 +179,27 @@ impl Segment { pub fn segment_history_mut(&mut self) -> &mut SegmentHistory { &mut self.segment_history } + + /// Accesses the segment's variables for the current attempt. + pub const fn variables(&self) -> &HashMap { + &self.variables + } + + /// Grants mutable access to the segment's variables for the current + /// attempt. + pub fn variables_mut(&mut self) -> &mut HashMap { + &mut self.variables + } + + /// Clears the variables of the current attempt. + pub fn clear_variables(&mut self) { + self.variables.clear(); + } + + /// Clears all the information the segment stores when it has been splitted, + /// such as the split's time and variables. + pub fn clear_split_info(&mut self) { + self.clear_variables(); + self.clear_split_time(); + } } diff --git a/src/settings/mod.rs b/src/settings/mod.rs index a43b9387..db978b78 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -20,5 +20,5 @@ pub use self::{ image::{CachedImageId, Image, ImageData}, semantic_color::SemanticColor, settings_description::SettingsDescription, - value::{Error as ValueError, Result as ValueResult, Value}, + value::{Error as ValueError, Result as ValueResult, Value, ColumnKind}, }; diff --git a/src/settings/value.rs b/src/settings/value.rs index 55cbb6f2..5bb036f6 100644 --- a/src/settings/value.rs +++ b/src/settings/value.rs @@ -13,6 +13,15 @@ use crate::{ use core::result::Result as StdResult; use serde::{Deserialize, Serialize}; +/// Describes the kind of a column. +#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ColumnKind { + /// The column shows a time. + Time, + /// The column shows a variable. + Variable, +} + /// Describes a setting's value. Such a value can be of a variety of different /// types. #[derive(Clone, PartialEq, Serialize, Deserialize)] @@ -45,6 +54,8 @@ pub enum Value { ListGradient(ListGradient), /// An alignment for the Title Component's title. Alignment(Alignment), + /// A column kind. + ColumnKind(ColumnKind), /// A value describing what a column of the Splits Component starts out /// with. ColumnStartWith(ColumnStartWith), @@ -185,6 +196,12 @@ impl From for Value { } } +impl From for Value { + fn from(x: ColumnKind) -> Self { + Value::ColumnKind(x) + } +} + /// The Error type for values that couldn't be converted. #[derive(Debug, snafu::Snafu)] pub enum Error { @@ -365,6 +382,14 @@ impl Value { _ => Err(Error::WrongType), } } + + /// Tries to convert the value into a column kind. + pub fn into_column_kind(self) -> Result { + match self { + Value::ColumnKind(v) => Ok(v), + _ => Err(Error::WrongType), + } + } } impl From for bool { @@ -486,3 +511,9 @@ impl From for DeltaGradient { value.into_delta_gradient().unwrap() } } + +impl From for ColumnKind { + fn from(value: Value) -> Self { + value.into_column_kind().unwrap() + } +} diff --git a/src/timing/timer/mod.rs b/src/timing/timer/mod.rs index b9a6d61b..20cb8b2e 100644 --- a/src/timing/timer/mod.rs +++ b/src/timing/timer/mod.rs @@ -319,9 +319,18 @@ impl Timer { .real_time .map_or(false, |t| t >= TimeSpan::zero()) { - self.current_split_mut() - .unwrap() - .set_split_time(current_time); + // FIXME: We shouldn't need to collect here. + let variables = self + .run + .metadata() + .custom_variables() + .map(|(k, v)| (k.to_owned(), v.value.clone())) + .collect(); + let segment = self.current_split_mut().unwrap(); + + segment.set_split_time(current_time); + *segment.variables_mut() = variables; + *self.current_split_index.as_mut().unwrap() += 1; if Some(self.run.len()) == self.current_split_index { self.phase = Ended; @@ -349,7 +358,8 @@ impl Timer { if (self.phase == Running || self.phase == Paused) && self.current_split_index < self.run.len().checked_sub(1) { - self.current_split_mut().unwrap().clear_split_time(); + self.current_split_mut().unwrap().clear_split_info(); + self.current_split_index = self.current_split_index.map(|i| i + 1); self.run.mark_as_modified(); @@ -366,7 +376,9 @@ impl Timer { self.phase = Running; } self.current_split_index = self.current_split_index.map(|i| i - 1); - self.current_split_mut().unwrap().clear_split_time(); + + self.current_split_mut().unwrap().clear_split_info(); + self.run.mark_as_modified(); // FIXME: OnUndoSplit @@ -416,7 +428,7 @@ impl Timer { // Reset Splits for segment in self.run.segments_mut() { - segment.clear_split_time(); + segment.clear_split_info(); } // FIXME: OnReset diff --git a/src/util/clear_vec.rs b/src/util/clear_vec.rs index 7fb72fce..3410bcf3 100644 --- a/src/util/clear_vec.rs +++ b/src/util/clear_vec.rs @@ -174,7 +174,7 @@ impl Serialize for ClearVec { where S: serde::Serializer, { - (&**self).serialize(serializer) + <[T]>::serialize(self, serializer) } }