livesplit_core/component/splits/
column.rs

1use crate::{
2    analysis::{self, possible_time_save, split_color},
3    comparison,
4    component::splits::Settings as SplitsSettings,
5    platform::prelude::*,
6    settings::{Color, SemanticColor},
7    timing::{
8        formatter::{Delta, Regular, SegmentTime, TimeFormatter},
9        Snapshot,
10    },
11    util::Clear,
12    GeneralLayoutSettings, Segment, TimeSpan, TimingMethod,
13};
14use core::fmt::Write;
15use serde::{Deserialize, Serialize};
16
17/// The settings of an individual column showing timing information on each
18/// split.
19#[derive(Clone, Serialize, Deserialize)]
20#[serde(default)]
21pub struct ColumnSettings {
22    /// The name of the column.
23    pub name: String,
24    /// The kind of the column.
25    #[serde(flatten)]
26    pub kind: ColumnKind,
27}
28
29/// The kind of a column. It can either be a column that shows a variable or a
30/// time.
31#[derive(Clone, Serialize, Deserialize)]
32#[serde(untagged)]
33pub enum ColumnKind {
34    /// A column that shows a variable.
35    Variable(VariableColumn),
36    /// A column that shows a time.
37    Time(TimeColumn),
38}
39
40/// A column that shows a time.
41#[derive(Clone, Serialize, Deserialize)]
42#[serde(default)]
43pub struct TimeColumn {
44    /// Specifies the value a segment starts out with before it gets replaced
45    /// with the current attempt's information when splitting.
46    pub start_with: ColumnStartWith,
47    /// Once a certain condition is met, which is usually being on the split or
48    /// already having completed the split, the time gets updated with the value
49    /// specified here.
50    pub update_with: ColumnUpdateWith,
51    /// Specifies when a column's value gets updated.
52    pub update_trigger: ColumnUpdateTrigger,
53    /// The comparison chosen. Uses the Timer's current comparison if set to
54    /// `None`.
55    pub comparison_override: Option<String>,
56    /// Specifies the Timing Method to use. If set to `None` the Timing Method
57    /// of the Timer is used for showing the time. Otherwise the Timing Method
58    /// provided is used.
59    pub timing_method: Option<TimingMethod>,
60}
61
62/// A column that shows a variable.
63#[derive(Default, Clone, Serialize, Deserialize)]
64pub struct VariableColumn {
65    /// The name of the variable to visualize.
66    pub variable_name: String,
67}
68
69/// Specifies the value a segment starts out with before it gets replaced
70/// with the current attempt's information when splitting.
71#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
72pub enum ColumnStartWith {
73    /// The column starts out with an empty value.
74    Empty,
75    /// The column starts out with the times stored in the comparison that is
76    /// being compared against.
77    ComparisonTime,
78    /// The column starts out with the segment times stored in the comparison
79    /// that is being compared against.
80    ComparisonSegmentTime,
81    /// The column starts out with the time that can be saved on each individual
82    /// segment stored in the comparison that is being compared against.
83    PossibleTimeSave,
84}
85
86/// Once a certain condition is met, which is usually being on the split or
87/// already having completed the split, the time gets updated with the value
88/// specified here.
89#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
90pub enum ColumnUpdateWith {
91    /// The value doesn't get updated and stays on the value it started out
92    /// with.
93    DontUpdate,
94    /// The value gets replaced by the current attempt's split time.
95    SplitTime,
96    /// The value gets replaced by the delta of the current attempt's and the
97    /// comparison's split time.
98    Delta,
99    /// The value gets replaced by the delta of the current attempt's and the
100    /// comparison's split time. If there is no delta, the value gets replaced
101    /// by the current attempt's split time instead.
102    DeltaWithFallback,
103    /// The value gets replaced by the current attempt's segment time.
104    SegmentTime,
105    /// The value gets replaced by the current attempt's time saved or lost,
106    /// which is how much faster or slower the current attempt's segment time is
107    /// compared to the comparison's segment time. This matches the Previous
108    /// Segment component.
109    SegmentDelta,
110    /// The value gets replaced by the current attempt's time saved or lost,
111    /// which is how much faster or slower the current attempt's segment time is
112    /// compared to the comparison's segment time. This matches the Previous
113    /// Segment component. If there is no time saved or lost, then value gets
114    /// replaced by the current attempt's segment time instead.
115    SegmentDeltaWithFallback,
116}
117
118/// Specifies when a column's value gets updated.
119#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
120pub enum ColumnUpdateTrigger {
121    /// The value gets updated as soon as the segment is started. The value
122    /// constantly updates until the segment ends.
123    OnStartingSegment,
124    /// The value doesn't immediately get updated when the segment is started.
125    /// Instead the value constantly gets updated once the segment time is
126    /// longer than the best segment time. The final update to the value happens
127    /// when the segment ends.
128    Contextual,
129    /// The value of a segment gets updated once the segment ends.
130    OnEndingSegment,
131}
132
133impl Default for ColumnSettings {
134    fn default() -> Self {
135        ColumnSettings {
136            name: String::from("Column"),
137            kind: ColumnKind::Time(TimeColumn::default()),
138        }
139    }
140}
141
142impl Default for TimeColumn {
143    fn default() -> Self {
144        TimeColumn {
145            start_with: ColumnStartWith::Empty,
146            update_with: ColumnUpdateWith::DontUpdate,
147            update_trigger: ColumnUpdateTrigger::Contextual,
148            comparison_override: None,
149            timing_method: None,
150        }
151    }
152}
153
154/// Describes the state of a single segment's column to visualize.
155#[derive(Debug, Serialize, Deserialize)]
156pub struct ColumnState {
157    /// The value shown in the column.
158    pub value: String,
159    /// The semantic coloring information the value carries.
160    pub semantic_color: SemanticColor,
161    /// The visual color of the value.
162    pub visual_color: Color,
163    /// This value indicates whether the column is currently frequently being
164    /// updated. This can be used for rendering optimizations.
165    pub updates_frequently: bool,
166}
167
168impl Clear for ColumnState {
169    fn clear(&mut self) {
170        self.value.clear();
171    }
172}
173
174enum ColumnFormatter {
175    Time,
176    Delta,
177    SegmentTime,
178}
179
180pub fn update_state(
181    state: &mut ColumnState,
182    column_settings: &ColumnSettings,
183    timer: &Snapshot<'_>,
184    splits_settings: &SplitsSettings,
185    layout_settings: &GeneralLayoutSettings,
186    segment: &Segment,
187    segment_index: usize,
188    current_split: Option<usize>,
189    method: TimingMethod,
190) {
191    match &column_settings.kind {
192        ColumnKind::Variable(column) => {
193            state.value.clear();
194            if let Some(value) = segment.variables().get(column.variable_name.as_str()) {
195                state.value.push_str(value);
196            } else if Some(segment_index) == current_split {
197                if let Some(value) = timer
198                    .run()
199                    .metadata()
200                    .custom_variable_value(column.variable_name.as_str())
201                {
202                    // FIXME: We show the live value of the variable, which means it
203                    // might update frequently. So we possibly should mark it as
204                    // such. However it's currently impossible to tell if it
205                    // actually does update frequently. On top of that, the text
206                    // component would need to support this as well, as it also
207                    // shows the live value of the variable.
208                    state.value.push_str(value);
209                }
210            }
211            state.semantic_color = SemanticColor::Default;
212            state.visual_color = layout_settings.text_color;
213            state.updates_frequently = false;
214        }
215        ColumnKind::Time(column) => {
216            update_time_column(
217                state,
218                column,
219                timer,
220                splits_settings,
221                layout_settings,
222                segment,
223                segment_index,
224                current_split,
225                method,
226            );
227        }
228    }
229}
230
231fn update_time_column(
232    state: &mut ColumnState,
233    column_settings: &TimeColumn,
234    timer: &Snapshot<'_>,
235    splits_settings: &SplitsSettings,
236    layout_settings: &GeneralLayoutSettings,
237    segment: &Segment,
238    segment_index: usize,
239    current_split: Option<usize>,
240    method: TimingMethod,
241) {
242    let method = column_settings.timing_method.unwrap_or(method);
243    let resolved_comparison = comparison::resolve(&column_settings.comparison_override, timer);
244    let comparison = comparison::or_current(resolved_comparison, timer);
245    let update_value = time_column_update_value(
246        column_settings,
247        timer,
248        segment,
249        segment_index,
250        current_split,
251        method,
252        comparison,
253    );
254    let updated = update_value.is_some();
255    let ((column_value, semantic_color, formatter), is_live) = update_value.unwrap_or_else(|| {
256        (
257            match column_settings.start_with {
258                ColumnStartWith::Empty => (None, SemanticColor::Default, ColumnFormatter::Time),
259                ColumnStartWith::ComparisonTime => (
260                    segment.comparison(comparison)[method],
261                    SemanticColor::Default,
262                    ColumnFormatter::Time,
263                ),
264                ColumnStartWith::ComparisonSegmentTime => (
265                    analysis::comparison_combined_segment_time(
266                        timer.run(),
267                        segment_index,
268                        comparison,
269                        method,
270                    ),
271                    SemanticColor::Default,
272                    ColumnFormatter::SegmentTime,
273                ),
274                ColumnStartWith::PossibleTimeSave => (
275                    possible_time_save::calculate(timer, segment_index, comparison, false).0,
276                    SemanticColor::Default,
277                    ColumnFormatter::SegmentTime,
278                ),
279            },
280            false,
281        )
282    });
283    let is_empty = column_settings.start_with == ColumnStartWith::Empty && !updated;
284    state.updates_frequently = is_live && column_value.is_some();
285    state.value.clear();
286    if !is_empty {
287        let _ = match formatter {
288            ColumnFormatter::Time => write!(
289                state.value,
290                "{}",
291                Regular::with_accuracy(splits_settings.split_time_accuracy).format(column_value)
292            ),
293            ColumnFormatter::Delta => write!(
294                state.value,
295                "{}",
296                Delta::custom(
297                    splits_settings.delta_drop_decimals,
298                    splits_settings.delta_time_accuracy,
299                )
300                .format(column_value)
301            ),
302            ColumnFormatter::SegmentTime => {
303                write!(
304                    state.value,
305                    "{}",
306                    SegmentTime::with_accuracy(splits_settings.segment_time_accuracy)
307                        .format(column_value)
308                )
309            }
310        };
311    }
312    state.semantic_color = semantic_color;
313    state.visual_color = semantic_color.visualize(layout_settings);
314}
315
316fn time_column_update_value(
317    column: &TimeColumn,
318    timer: &Snapshot<'_>,
319    segment: &Segment,
320    segment_index: usize,
321    current_split: Option<usize>,
322    method: TimingMethod,
323    comparison: &str,
324) -> Option<((Option<TimeSpan>, SemanticColor, ColumnFormatter), bool)> {
325    use self::{ColumnUpdateTrigger::*, ColumnUpdateWith::*};
326
327    if current_split < Some(segment_index) {
328        // Didn't reach the segment yet.
329        return None;
330    }
331
332    let is_current_split = current_split == Some(segment_index);
333
334    if is_current_split {
335        if column.update_trigger == OnEndingSegment {
336            // The trigger wants the value to be updated when splitting, not before.
337            return None;
338        }
339
340        if column.update_trigger == Contextual
341            && analysis::check_live_delta(
342                timer,
343                !column.update_with.is_segment_based(),
344                comparison,
345                method,
346            )
347            .is_none()
348        {
349            // It's contextual and the live delta shouldn't be shown yet.
350            return None;
351        }
352    }
353
354    let is_live = is_current_split;
355
356    let value = match (column.update_with, is_live) {
357        (DontUpdate, _) => return None,
358
359        (SplitTime, false) => (
360            segment.split_time()[method],
361            SemanticColor::Default,
362            ColumnFormatter::Time,
363        ),
364        (SplitTime, true) => (
365            timer.current_time()[method],
366            SemanticColor::Default,
367            ColumnFormatter::Time,
368        ),
369
370        (Delta | DeltaWithFallback, false) => {
371            let split_time = segment.split_time()[method];
372            let delta = catch! {
373                split_time? -
374                segment.comparison(comparison)[method]?
375            };
376            let (value, formatter) = if delta.is_none() && column.update_with.has_fallback() {
377                (split_time, ColumnFormatter::Time)
378            } else {
379                (delta, ColumnFormatter::Delta)
380            };
381            (
382                value,
383                split_color(timer, delta, segment_index, true, true, comparison, method),
384                formatter,
385            )
386        }
387        (Delta | DeltaWithFallback, true) => (
388            catch! {
389                timer.current_time()[method]? -
390                segment.comparison(comparison)[method]?
391            },
392            SemanticColor::Default,
393            ColumnFormatter::Delta,
394        ),
395
396        (SegmentTime, false) => (
397            analysis::previous_segment_time(timer, segment_index, method),
398            SemanticColor::Default,
399            ColumnFormatter::SegmentTime,
400        ),
401        (SegmentTime, true) => (
402            analysis::live_segment_time(timer, segment_index, method),
403            SemanticColor::Default,
404            ColumnFormatter::SegmentTime,
405        ),
406
407        (SegmentDelta | SegmentDeltaWithFallback, false) => {
408            let delta = analysis::previous_segment_delta(timer, segment_index, comparison, method);
409            let (value, formatter) = if delta.is_none() && column.update_with.has_fallback() {
410                (
411                    analysis::previous_segment_time(timer, segment_index, method),
412                    ColumnFormatter::SegmentTime,
413                )
414            } else {
415                (delta, ColumnFormatter::Delta)
416            };
417            (
418                value,
419                split_color(timer, delta, segment_index, false, true, comparison, method),
420                formatter,
421            )
422        }
423        (SegmentDelta | SegmentDeltaWithFallback, true) => (
424            analysis::live_segment_delta(timer, segment_index, comparison, method),
425            SemanticColor::Default,
426            ColumnFormatter::Delta,
427        ),
428    };
429
430    Some((value, is_live))
431}
432
433impl ColumnUpdateWith {
434    const fn is_segment_based(self) -> bool {
435        use ColumnUpdateWith::*;
436        matches!(self, SegmentDelta | SegmentTime | SegmentDeltaWithFallback)
437    }
438
439    const fn has_fallback(self) -> bool {
440        use ColumnUpdateWith::*;
441        matches!(self, DeltaWithFallback | SegmentDeltaWithFallback)
442    }
443}