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) } }