diff --git a/capi/src/current_segment_component.rs b/capi/src/current_segment_component.rs new file mode 100644 index 00000000..919e661e --- /dev/null +++ b/capi/src/current_segment_component.rs @@ -0,0 +1,59 @@ +//! The Current Segment Component is a component that shows how much time will +//! be saved or lost during the current segment based on the chosen comparison. +//! It displays the difference between the current segment time +//! and the chosen comparison segment time. + +use super::{output_vec, Json}; +use crate::component::OwnedComponent; +use crate::key_value_component_state::OwnedKeyValueComponentState; +use livesplit_core::component::current_segment::Component as CurrentSegmentComponent; +use livesplit_core::{GeneralLayoutSettings, Timer}; + +/// type +pub type OwnedCurrentSegmentComponent = Box; + +/// Creates a new Current Segment Component. +#[unsafe(no_mangle)] +pub extern "C" fn CurrentSegmentComponent_new() -> OwnedCurrentSegmentComponent { + Box::new(CurrentSegmentComponent::new()) +} + +/// drop +#[unsafe(no_mangle)] +pub extern "C" fn CurrentSegmentComponent_drop(this: OwnedCurrentSegmentComponent) { + drop(this); +} + +/// Converts the component into a generic component suitable for using with a +/// layout. +#[unsafe(no_mangle)] +pub extern "C" fn CurrentSegmentComponent_into_generic( + this: OwnedCurrentSegmentComponent, +) -> OwnedComponent { + Box::new((*this).into()) +} + +/// Encodes the component's state information as JSON. +#[unsafe(no_mangle)] +pub extern "C" fn CurrentSegmentComponent_state_as_json( + this: &CurrentSegmentComponent, + timer: &Timer, + layout_settings: &GeneralLayoutSettings, +) -> Json { + output_vec(|o| { + this.state(&timer.snapshot(), layout_settings) + .write_json(o) + .unwrap(); + }) +} + +/// Calculates the component's state based on the timer and the layout +/// settings provided. +#[unsafe(no_mangle)] +pub extern "C" fn CurrentSegmentComponent_state( + this: &CurrentSegmentComponent, + timer: &Timer, + layout_settings: &GeneralLayoutSettings, +) -> OwnedKeyValueComponentState { + Box::new(this.state(&timer.snapshot(), layout_settings)) +} diff --git a/capi/src/lib.rs b/capi/src/lib.rs index 2b3bb4d6..eb5f1ba3 100644 --- a/capi/src/lib.rs +++ b/capi/src/lib.rs @@ -30,6 +30,7 @@ pub mod command_sink; pub mod component; pub mod current_comparison_component; pub mod current_pace_component; +pub mod current_segment_component; pub mod delta_component; pub mod detailed_timer_component; pub mod detailed_timer_component_state; diff --git a/src/component/current_segment.rs b/src/component/current_segment.rs new file mode 100644 index 00000000..aa9cd680 --- /dev/null +++ b/src/component/current_segment.rs @@ -0,0 +1,261 @@ +//! Provides the Current Segment Component and relevant types for using it. The +//! Current Segment Component is a component that shows how much time will be saved +//! or lost during the current [`Segment`](crate::run::Segment) based on the +//! chosen comparison. It displays the difference between the current segment time +//! and the chosen comparison segment time. Additionally, the potential time save for the current +//! [`Segment`](crate::run::Segment) can be displayed. + +use super::key_value; +use crate::{ + analysis, comparison, platform::prelude::*, settings::{Color, Field, Gradient, SemanticColor, SettingsDescription, Value}, timing::{ + formatter::{Accuracy, Delta, SegmentTime, TimeFormatter}, Snapshot + }, GeneralLayoutSettings +}; +use alloc::borrow::Cow; +use core::fmt::Write as FmtWrite; +use serde_derive::{Deserialize, Serialize}; + +/// Provides the Current Segment Component and relevant types for using it. The +/// Current Segment Component is a component that shows how much time will be saved +/// or lost during the current [`Segment`](crate::run::Segment) based on the +/// chosen comparison. It displays the difference between the current segment time +/// and the chosen comparison segment time. Additionally, the potential time save for the current +/// [`Segment`](crate::run::Segment) can be displayed. +#[derive(Default, Clone)] +pub struct Component { + settings: Settings, +} + +/// The Settings for this component. +#[derive(Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct Settings { + /// The background shown behind the component. + pub background: Gradient, + /// The comparison chosen. Uses the Timer's current comparison if set to + /// `None`. + pub comparison_override: Option, + /// Specifies whether to display the name of the component and its value in + /// two separate rows. + pub display_two_rows: bool, + /// The color of the label. If `None` is specified, the color is taken from + /// the layout. + pub label_color: Option, + /// Specifies if the decimals should not be shown anymore when the + /// visualized delta is above one minute. + pub drop_decimals: bool, + /// The accuracy of the time shown. + pub accuracy: Accuracy, + /// Determines if the time save that could've been saved is shown in + /// addition to the previous segment. + pub show_possible_time_save: bool, +} + +impl Default for Settings { + fn default() -> Self { + Self { + background: key_value::DEFAULT_GRADIENT, + comparison_override: None, + display_two_rows: false, + label_color: None, + drop_decimals: true, + accuracy: Accuracy::Tenths, + show_possible_time_save: false, + } + } +} + +impl Component { + /// Creates a new Current Segment Component. + pub fn new() -> Self { + Default::default() + } + + /// Creates a new Current Segment Component with the given settings. + pub const fn with_settings(settings: Settings) -> Self { + Self { settings } + } + + /// Accesses the settings of the component. + pub const fn settings(&self) -> &Settings { + &self.settings + } + + /// Grants mutable access to the settings of the component. + pub const fn settings_mut(&mut self) -> &mut Settings { + &mut self.settings + } + + /// Accesses the name of the component. + 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> { + let text = "Current Segment"; + let mut text = Cow::from(text); + if let Some(comparison) = comparison { + write!(text.to_mut(), " ({})", comparison::shorten(comparison)).unwrap(); + } + text + } + + /// Updates the component's state based on the timer and layout settings + /// provided. + pub fn update_state( + &self, + state: &mut key_value::State, + timer: &Snapshot<'_>, + layout_settings: &GeneralLayoutSettings, + ) { + let resolved_comparison = comparison::resolve(&self.settings.comparison_override, timer); + let comparison = comparison::or_current(resolved_comparison, timer); + let method = timer.current_timing_method(); + let phase = timer.current_phase(); + let mut time_change = None; + let mut possible_save = None; + let mut semantic_color = SemanticColor::Default; + if phase.is_running() || phase.is_paused() { + if let Some(split_index) = timer.current_split_index() { + time_change = analysis::live_segment_delta( + timer, + split_index, + comparison, + method + ); + if self.settings.show_possible_time_save { + possible_save = analysis::possible_time_save::calculate( + timer, + split_index, + comparison, + false, + ) + .0 + }; + semantic_color = analysis::split_color( + timer, + time_change, + split_index, + false, + false, + comparison, + method, + ); + }; + }; + + let value_color = Some(semantic_color.visualize(layout_settings)); + + let text = self.text(resolved_comparison); + + state.background = self.settings.background; + state.key_color = self.settings.label_color; + state.value_color = value_color; + state.semantic_color = semantic_color; + + state.key.clear(); + state.key.push_str(&text); // FIXME: Uncow + + state.value.clear(); + let _ = write!( + state.value, + "{}", + Delta::custom(self.settings.drop_decimals, self.settings.accuracy).format(time_change), + ); + + if self.settings.show_possible_time_save { + let _ = write!( + state.value, + " / {}", + SegmentTime::with_accuracy(self.settings.accuracy).format(possible_save), + ); + } + + state.key_abbreviations.clear(); + state.key_abbreviations.push("Current Segment".into()); + state.key_abbreviations.push("Curr. Segment".into()); + state.key_abbreviations.push("Curr. Seg.".into()); + + state.display_two_rows = self.settings.display_two_rows; + state.updates_frequently = phase.updates_frequently(method); + } + + /// Calculates the component's state based on the timer and the layout + /// settings provided. + pub fn state( + &self, + timer: &Snapshot<'_>, + layout_settings: &GeneralLayoutSettings, + ) -> key_value::State { + let mut state = Default::default(); + self.update_state(&mut state, timer, layout_settings); + state + } + + /// Accesses a generic description of the settings available for this + /// component and their current values. + pub fn settings_description(&self) -> SettingsDescription { + SettingsDescription::with_fields(vec![ + Field::new( + "Background".into(), + "The background shown behind the component.".into(), + self.settings.background.into(), + ), + Field::new( + "Comparison".into(), + "The comparison used for calculating how much time was saved or lost. If not specified, the current comparison is used.".into(), + self.settings.comparison_override.clone().into(), + ), + Field::new( + "Display 2 Rows".into(), + "Specifies whether to display the name of the component and how much time was saved or lost in two separate rows.".into(), + self.settings.display_two_rows.into(), + ), + Field::new( + "Label Color".into(), + "The color of the component's name. If not specified, the color is taken from the layout.".into(), + self.settings.label_color.into(), + ), + Field::new( + "Drop Decimals".into(), + "Specifies whether to drop the decimals from the time when the time shown is over a minute.".into(), + self.settings.drop_decimals.into(), + ), + Field::new( + "Accuracy".into(), + "The accuracy of the time shown.".into(), + self.settings.accuracy.into(), + ), + Field::new( + "Show Possible Time Save".into(), + "Specifies whether to show how much time could be saved for the currrent segment in addition to the current delta.".into(), + self.settings.show_possible_time_save.into(), + ), + ]) + } + + /// Sets a setting's value by its index to the given value. + /// + /// # Panics + /// + /// This panics if the type of the value to be set is not compatible with + /// the type of the setting's value. A panic can also occur if the index of + /// the setting provided is out of bounds. + pub fn set_value(&mut self, index: usize, value: Value) { + match index { + 0 => self.settings.background = value.into(), + 1 => self.settings.comparison_override = value.into(), + 2 => self.settings.display_two_rows = value.into(), + 3 => self.settings.label_color = value.into(), + 4 => self.settings.drop_decimals = value.into(), + 5 => self.settings.accuracy = value.into(), + 6 => self.settings.show_possible_time_save = value.into(), + _ => panic!("Unsupported Setting Index"), + } + } +} diff --git a/src/component/mod.rs b/src/component/mod.rs index a9b41f0d..7940385c 100644 --- a/src/component/mod.rs +++ b/src/component/mod.rs @@ -6,6 +6,7 @@ pub mod blank_space; pub mod current_comparison; pub mod current_pace; +pub mod current_segment; pub mod delta; pub mod detailed_timer; pub mod graph; @@ -26,6 +27,7 @@ pub mod key_value; pub use blank_space::Component as BlankSpace; pub use current_comparison::Component as CurrentComparison; pub use current_pace::Component as CurrentPace; +pub use current_segment::Component as CurrentSegment; pub use delta::Component as Delta; pub use detailed_timer::Component as DetailedTimer; pub use graph::Component as Graph; diff --git a/src/layout/component.rs b/src/layout/component.rs index ad860b7d..907c8d73 100644 --- a/src/layout/component.rs +++ b/src/layout/component.rs @@ -1,7 +1,7 @@ use super::{ComponentSettings, ComponentState, GeneralSettings}; use crate::{ component::{ - blank_space, current_comparison, current_pace, delta, detailed_timer, graph, pb_chance, + blank_space, current_comparison, current_pace, current_segment, delta, detailed_timer, graph, pb_chance, possible_time_save, previous_segment, segment_time, separator, splits, sum_of_best, text, timer, title, total_playtime, }, @@ -21,6 +21,8 @@ pub enum Component { CurrentComparison(current_comparison::Component), /// The Current Pace Component. CurrentPace(current_pace::Component), + /// The Current Segment Componenet + CurrentSegment(current_segment::Component), /// The Delta Component. Delta(delta::Component), /// The Detailed Timer Component. @@ -69,6 +71,12 @@ impl From for Component { } } +impl From for Component { + fn from(component: current_segment::Component) -> Self { + Self::CurrentSegment(component) + } +} + impl From for Component { fn from(component: delta::Component) -> Self { Self::Delta(component) @@ -178,6 +186,9 @@ impl Component { (ComponentState::KeyValue(state), Component::CurrentPace(component)) => { component.update_state(state, timer) } + (ComponentState::KeyValue(state), Component::CurrentSegment(component)) => { + component.update_state(state, timer, layout_settings) + } (ComponentState::KeyValue(state), Component::Delta(component)) => { component.update_state(state, timer, layout_settings) } @@ -247,6 +258,9 @@ impl Component { Component::Delta(component) => { ComponentState::KeyValue(component.state(timer, layout_settings)) } + Component::CurrentSegment(component) => { + ComponentState::KeyValue(component.state(timer, layout_settings)) + } Component::DetailedTimer(component) => ComponentState::DetailedTimer(Box::new( component.state(image_cache, timer, layout_settings), )), @@ -292,6 +306,9 @@ impl Component { Component::CurrentPace(component) => { ComponentSettings::CurrentPace(component.settings().clone()) } + Component::CurrentSegment(component) => { + ComponentSettings::CurrentSegment(component.settings().clone()) + } Component::Delta(component) => ComponentSettings::Delta(component.settings().clone()), Component::DetailedTimer(component) => { ComponentSettings::DetailedTimer(Box::new(component.settings().clone())) @@ -329,6 +346,7 @@ impl Component { Component::BlankSpace(component) => component.name().into(), Component::CurrentComparison(component) => component.name().into(), Component::CurrentPace(component) => component.name(), + Component::CurrentSegment(component) => component.name(), Component::Delta(component) => component.name(), Component::DetailedTimer(component) => component.name().into(), Component::Graph(component) => component.name(), @@ -371,6 +389,7 @@ impl Component { Component::BlankSpace(component) => component.settings_description(), Component::CurrentComparison(component) => component.settings_description(), Component::CurrentPace(component) => component.settings_description(), + Component::CurrentSegment(component) => component.settings_description(), Component::Delta(component) => component.settings_description(), Component::DetailedTimer(component) => component.settings_description(), Component::Graph(component) => component.settings_description(), @@ -401,6 +420,7 @@ impl Component { Component::BlankSpace(component) => component.set_value(index, value), Component::CurrentComparison(component) => component.set_value(index, value), Component::CurrentPace(component) => component.set_value(index, value), + Component::CurrentSegment(component) => component.set_value(index, value), Component::Delta(component) => component.set_value(index, value), Component::DetailedTimer(component) => component.set_value(index, value), Component::Graph(component) => component.set_value(index, value), diff --git a/src/layout/component_settings.rs b/src/layout/component_settings.rs index 228031b8..07e0f564 100644 --- a/src/layout/component_settings.rs +++ b/src/layout/component_settings.rs @@ -1,7 +1,7 @@ use super::Component; use crate::{ component::{ - blank_space, current_comparison, current_pace, delta, detailed_timer, graph, pb_chance, + blank_space, current_comparison, current_pace, current_segment, delta, detailed_timer, graph, pb_chance, possible_time_save, previous_segment, segment_time, separator, splits, sum_of_best, text, timer, title, total_playtime, }, @@ -18,6 +18,8 @@ pub enum ComponentSettings { CurrentComparison(current_comparison::Settings), /// The Settings for the Current Pace Component. CurrentPace(current_pace::Settings), + /// The Settings for the Current Segment Component. + CurrentSegment(current_segment::Settings), /// The Settings for the Delta Component. Delta(delta::Settings), /// The Settings for the Detailed Timer Component. @@ -60,6 +62,9 @@ impl From for Component { ComponentSettings::CurrentPace(settings) => { Component::CurrentPace(current_pace::Component::with_settings(settings)) } + ComponentSettings::CurrentSegment(settings) => { + Component::CurrentSegment(current_segment::Component::with_settings(settings)) + } ComponentSettings::Delta(settings) => { Component::Delta(delta::Component::with_settings(settings)) } diff --git a/src/layout/parser/current_segment.rs b/src/layout/parser/current_segment.rs new file mode 100644 index 00000000..311771d5 --- /dev/null +++ b/src/layout/parser/current_segment.rs @@ -0,0 +1,43 @@ +use super::{ + accuracy, color, comparison_override, end_tag, parse_bool, parse_children, GradientBuilder, + Result, +}; +use crate::util::xml::Reader; + +pub use crate::component::current_segment::Component; + +pub fn settings(reader: &mut Reader<'_>, component: &mut Component) -> Result<()> { + let settings = component.settings_mut(); + let mut background_builder = GradientBuilder::new(); + let mut override_label = false; + + parse_children(reader, |reader, tag, _| { + if !background_builder.parse_background(reader, tag.name())? { + match tag.name() { + "TextColor" => color(reader, |c| settings.label_color = Some(c)), + "OverrideTextColor" => parse_bool(reader, |b| override_label = b), + "DeltaAccuracy" => accuracy(reader, |v| settings.accuracy = v), + "DropDecimals" => parse_bool(reader, |b| settings.drop_decimals = b), + "Comparison" => comparison_override(reader, |v| settings.comparison_override = v), + "Display2Rows" => parse_bool(reader, |b| settings.display_two_rows = b), + "ShowPossibleTimeSave" => { + parse_bool(reader, |b| settings.show_possible_time_save = b) + } + _ => { + // FIXME: + // TimeSaveAccuracy + end_tag(reader) + } + } + } else { + Ok(()) + } + })?; + + if !override_label { + settings.label_color = None; + } + settings.background = background_builder.build(); + + Ok(()) +} diff --git a/src/layout/parser/mod.rs b/src/layout/parser/mod.rs index 705f20a0..6af6f4a2 100644 --- a/src/layout/parser/mod.rs +++ b/src/layout/parser/mod.rs @@ -25,6 +25,7 @@ use core::{mem::MaybeUninit, num::ParseIntError, str}; mod blank_space; mod current_comparison; mod current_pace; +mod current_segment; mod delta; mod detailed_timer; mod graph; @@ -648,6 +649,7 @@ where "PBChance.dll" => pb_chance::Component::new().into(), "LiveSplit.PossibleTimeSave.dll" => possible_time_save::Component::new().into(), "LiveSplit.PreviousSegment.dll" => previous_segment::Component::new().into(), + "LiveSplit.CurrentSegment.dll" => current_segment::Component::new().into(), "" => separator::Component::new().into(), "LiveSplit.Splits.dll" | "LiveSplit.Subsplits.dll" => { splits::Component::new().into() @@ -669,6 +671,7 @@ where Component::BlankSpace(c) => blank_space::settings(reader, c), Component::CurrentComparison(c) => current_comparison::settings(reader, c), Component::CurrentPace(c) => current_pace::settings(reader, c), + Component::CurrentSegment(c) => current_segment::settings(reader, c), Component::Delta(c) => delta::settings(reader, c), Component::DetailedTimer(c) => detailed_timer::settings(reader, c), Component::Graph(c) => graph::settings(reader, c),