livesplit_core/component/detailed_timer/
mod.rs

1//! Provides the Detailed Timer Component and relevant types for using it. The
2//! Detailed Timer Component is a component that shows two timers, one for the
3//! total time of the current attempt and one showing the time of just the
4//! current segment. Other information, like segment times of up to two
5//! comparisons, the segment icon, and the segment's name, can also be shown.
6
7use super::timer;
8use crate::{
9    analysis::comparison_single_segment_time,
10    comparison::{self, best_segments, none},
11    platform::prelude::*,
12    settings::{CachedImageId, Color, Field, Gradient, ImageData, SettingsDescription, Value},
13    timing::{
14        formatter::{Accuracy, DigitsFormat, SegmentTime, TimeFormatter},
15        Snapshot,
16    },
17    GeneralLayoutSettings, Segment, TimeSpan, TimerPhase,
18};
19use core::fmt::Write;
20use serde::{Deserialize, Serialize};
21
22#[cfg(test)]
23mod tests;
24
25/// The Detailed Timer Component is a component that shows two timers, one for
26/// the total time of the current attempt and one showing the time of just the
27/// current segment. Other information, like segment times of up to two
28/// comparisons, the segment icon, and the segment's name, can also be shown.
29#[derive(Default, Clone)]
30pub struct Component {
31    icon_id: CachedImageId,
32    timer: timer::Component,
33    segment_timer: timer::Component,
34    settings: Settings,
35}
36
37/// The Settings for this component.
38#[derive(Clone, Serialize, Deserialize)]
39#[serde(default)]
40pub struct Settings {
41    /// The background shown behind the component.
42    pub background: timer::DeltaGradient,
43    /// The first comparison to show the segment time of. If it's not specified,
44    /// the current comparison is used.
45    pub comparison1: Option<String>,
46    /// The first comparison to show the segment time of. If it's not specified,
47    /// the current comparison is used, unless the first comparison is also
48    /// `None`. This is not shown if the second comparison is hidden.
49    pub comparison2: Option<String>,
50    /// Specifies whether to only show a single comparison.
51    pub hide_second_comparison: bool,
52    /// The settings of the attempt timer.
53    pub timer: timer::Settings,
54    /// The settings of the segment timer.
55    pub segment_timer: timer::Settings,
56    /// Specifies whether the segment icon should be shown.
57    pub display_icon: bool,
58    /// Specifies whether the segment name should be shown.
59    pub show_segment_name: bool,
60}
61
62/// The state object describes the information to visualize for this component.
63#[derive(Default, Serialize, Deserialize)]
64pub struct State {
65    /// The background shown behind the component.
66    pub background: Gradient,
67    /// The state of the attempt timer.
68    pub timer: timer::State,
69    /// The state of the segment timer.
70    pub segment_timer: timer::State,
71    /// The first comparison to visualize.
72    pub comparison1: Option<ComparisonState>,
73    /// The second comparison to visualize.
74    pub comparison2: Option<ComparisonState>,
75    /// The name of the segment. This may be `None` if it's not supposed to be
76    /// visualized.
77    pub segment_name: Option<String>,
78    /// The segment's icon encoded as the raw file bytes. This value is only
79    /// specified whenever the icon changes. If you explicitly want to query
80    /// this value, remount the component. The buffer itself may be empty. This
81    /// indicates that there is no icon.
82    pub icon_change: Option<ImageData>,
83}
84
85/// The state object describing a comparison to visualize.
86#[derive(Serialize, Deserialize)]
87pub struct ComparisonState {
88    /// The name of the comparison.
89    pub name: String,
90    /// The time to show for the comparison.
91    pub time: String,
92}
93
94fn update_comparison(
95    state: &mut Option<ComparisonState>,
96    new_state: Option<(&str, Option<TimeSpan>)>,
97) {
98    if let Some((name, time)) = new_state {
99        let state = state.get_or_insert_with(|| ComparisonState {
100            name: String::new(),
101            time: String::new(),
102        });
103
104        state.name.clear();
105        state.name.push_str(name);
106
107        state.time.clear();
108        let _ = write!(state.time, "{}", SegmentTime::new().format(time));
109    } else {
110        *state = None;
111    }
112}
113
114impl Default for Settings {
115    fn default() -> Self {
116        Settings {
117            background: timer::DeltaGradient::from(Gradient::Transparent),
118            comparison1: None,
119            comparison2: Some(String::from(best_segments::NAME)),
120            hide_second_comparison: false,
121            timer: timer::Settings {
122                height: 40,
123                ..Default::default()
124            },
125            segment_timer: timer::Settings {
126                height: 25,
127                is_segment_timer: true,
128                color_override: Some(Color::rgba(
129                    170.0 / 255.0,
130                    170.0 / 255.0,
131                    170.0 / 255.0,
132                    1.0,
133                )),
134                ..Default::default()
135            },
136            display_icon: false,
137            show_segment_name: false,
138        }
139    }
140}
141
142#[cfg(feature = "std")]
143impl State {
144    /// Encodes the state object's information as JSON.
145    pub fn write_json<W>(&self, writer: W) -> serde_json::Result<()>
146    where
147        W: std::io::Write,
148    {
149        serde_json::to_writer(writer, self)
150    }
151}
152
153impl Component {
154    /// Creates a new Detailed Timer Component.
155    pub fn new() -> Self {
156        Self::with_settings(Default::default())
157    }
158
159    /// Creates a new Detailed Timer Component with the given settings.
160    pub fn with_settings(settings: Settings) -> Self {
161        let timer = timer::Component::with_settings(settings.timer.clone());
162        let segment_timer = timer::Component::with_settings(settings.segment_timer.clone());
163        Self {
164            timer,
165            segment_timer,
166            settings,
167            ..Default::default()
168        }
169    }
170
171    /// Accesses the settings of the component.
172    pub const fn settings(&self) -> &Settings {
173        &self.settings
174    }
175
176    /// Sets the settings of the component.
177    pub fn set_settings(&mut self, settings: Settings) {
178        self.settings = settings;
179        *self.timer.settings_mut() = self.settings.timer.clone();
180        *self.segment_timer.settings_mut() = self.settings.segment_timer.clone();
181    }
182
183    /// Accesses the name of the component.
184    pub const fn name(&self) -> &'static str {
185        "Detailed Timer"
186    }
187
188    /// Updates the component's state based on the timer and layout settings
189    /// provided.
190    pub fn update_state(
191        &mut self,
192        state: &mut State,
193        timer: &Snapshot<'_>,
194        layout_settings: &GeneralLayoutSettings,
195    ) {
196        let current_phase = timer.current_phase();
197        let timing_method = self
198            .settings
199            .timer
200            .timing_method
201            .unwrap_or_else(|| timer.current_timing_method());
202
203        let run = timer.run();
204
205        let (current_split, last_split_index) = if current_phase == TimerPhase::Ended {
206            (run.segments().last(), run.len() - 1)
207        } else {
208            (
209                timer.current_split(),
210                timer.current_split_index().unwrap_or(0),
211            )
212        };
213
214        let (comparison1, comparison2) = if current_phase != TimerPhase::NotRunning {
215            let mut comparison1 = self
216                .settings
217                .comparison1
218                .as_deref()
219                .unwrap_or_else(|| timer.current_comparison());
220
221            let comparison2 = self
222                .settings
223                .comparison2
224                .as_deref()
225                .unwrap_or_else(|| timer.current_comparison());
226
227            let mut hide_comparison = self.settings.hide_second_comparison;
228
229            if hide_comparison
230                || !run.comparisons().any(|c| c == comparison2)
231                || comparison2 == none::NAME
232            {
233                hide_comparison = true;
234                if !run.comparisons().any(|c| c == comparison1) || comparison1 == none::NAME {
235                    comparison1 = timer.current_comparison();
236                }
237            } else if !run.comparisons().any(|c| c == comparison1) || comparison1 == none::NAME {
238                hide_comparison = true;
239                comparison1 = comparison2;
240            } else if comparison1 == comparison2 {
241                hide_comparison = true;
242            }
243
244            let comparison1 = Some((
245                comparison::shorten(comparison1),
246                comparison_single_segment_time(run, last_split_index, comparison1, timing_method),
247            ));
248
249            let comparison2 = if !hide_comparison {
250                Some((
251                    comparison::shorten(comparison2),
252                    comparison_single_segment_time(
253                        run,
254                        last_split_index,
255                        comparison2,
256                        timing_method,
257                    ),
258                ))
259            } else {
260                None
261            };
262
263            (comparison1, comparison2)
264        } else {
265            Default::default()
266        };
267
268        let display_icon = self.settings.display_icon;
269        let icon_change = self
270            .icon_id
271            .update_with(current_split.filter(|_| display_icon).map(Segment::icon))
272            .map(Into::into);
273
274        self.timer
275            .update_state(&mut state.timer, timer, layout_settings);
276
277        self.segment_timer
278            .update_state(&mut state.segment_timer, timer, layout_settings);
279
280        state.background = self
281            .settings
282            .background
283            .gradient(state.timer.semantic_color.visualize(layout_settings));
284
285        update_comparison(&mut state.comparison1, comparison1);
286        update_comparison(&mut state.comparison2, comparison2);
287
288        match current_split.filter(|_| self.settings.show_segment_name) {
289            Some(segment) => {
290                let segment_name = state.segment_name.get_or_insert_with(String::new);
291                segment_name.clear();
292                segment_name.push_str(segment.name());
293            }
294            None => state.segment_name = None,
295        }
296
297        state.icon_change = icon_change;
298    }
299
300    /// Calculates the component's state based on the timer and layout settings
301    /// provided.
302    pub fn state(
303        &mut self,
304        timer: &Snapshot<'_>,
305        layout_settings: &GeneralLayoutSettings,
306    ) -> State {
307        let mut state = Default::default();
308        self.update_state(&mut state, timer, layout_settings);
309        state
310    }
311
312    /// Remounts the component as if it was freshly initialized. The segment
313    /// icons shown by this component are only provided in the state objects
314    /// whenever the icon changes or whenever the component's state is first
315    /// queried. Remounting returns the segment icon again, whenever its state
316    /// is queried the next time.
317    pub fn remount(&mut self) {
318        self.icon_id.reset();
319    }
320
321    /// Accesses a generic description of the settings available for this
322    /// component and their current values.
323    pub fn settings_description(&self) -> SettingsDescription {
324        SettingsDescription::with_fields(vec![
325            Field::new("Background".into(), self.settings.background.into()),
326            Field::new(
327                "Timing Method".into(),
328                self.settings.timer.timing_method.into(),
329            ),
330            Field::new(
331                "Comparison 1".into(),
332                self.settings.comparison1.clone().into(),
333            ),
334            Field::new(
335                "Comparison 2".into(),
336                self.settings.comparison2.clone().into(),
337            ),
338            Field::new(
339                "Hide Second Comparison".into(),
340                self.settings.hide_second_comparison.into(),
341            ),
342            Field::new(
343                "Timer Height".into(),
344                u64::from(self.settings.timer.height).into(),
345            ),
346            Field::new(
347                "Segment Timer Height".into(),
348                u64::from(self.settings.segment_timer.height).into(),
349            ),
350            Field::new(
351                "Timer Digits Format".into(),
352                self.settings.timer.digits_format.into(),
353            ),
354            Field::new("Timer Accuracy".into(), self.settings.timer.accuracy.into()),
355            Field::new(
356                "Segment Timer Digits Format".into(),
357                self.settings.segment_timer.digits_format.into(),
358            ),
359            Field::new(
360                "Segment Timer Accuracy".into(),
361                self.settings.segment_timer.accuracy.into(),
362            ),
363            Field::new(
364                "Show Segment Name".into(),
365                self.settings.show_segment_name.into(),
366            ),
367            Field::new("Display Icon".into(), self.settings.display_icon.into()),
368        ])
369    }
370
371    /// Sets a setting's value by its index to the given value.
372    ///
373    /// # Panics
374    ///
375    /// This panics if the type of the value to be set is not compatible with
376    /// the type of the setting's value. A panic can also occur if the index of
377    /// the setting provided is out of bounds.
378    pub fn set_value(&mut self, index: usize, value: Value) {
379        match index {
380            0 => self.settings.background = value.into(),
381            1 => {
382                let value = value.into();
383                self.settings.timer.timing_method = value;
384                self.settings.segment_timer.timing_method = value;
385            }
386            2 => self.settings.comparison1 = value.into(),
387            3 => self.settings.comparison2 = value.into(),
388            4 => self.settings.hide_second_comparison = value.into(),
389            5 => {
390                let value = value.into_uint().unwrap() as _;
391                self.settings.timer.height = value;
392                self.timer.settings_mut().height = value;
393            }
394            6 => {
395                let value = value.into_uint().unwrap() as _;
396                self.settings.segment_timer.height = value;
397                self.segment_timer.settings_mut().height = value;
398            }
399            7 => {
400                let value: DigitsFormat = value.into();
401                self.settings.timer.digits_format = value;
402                self.timer.settings_mut().digits_format = value;
403            }
404            8 => {
405                let value: Accuracy = value.into();
406                self.settings.timer.accuracy = value;
407                self.timer.settings_mut().accuracy = value;
408            }
409            9 => {
410                let value: DigitsFormat = value.into();
411                self.settings.segment_timer.digits_format = value;
412                self.segment_timer.settings_mut().digits_format = value;
413            }
414            10 => {
415                let value: Accuracy = value.into();
416                self.settings.segment_timer.accuracy = value;
417                self.segment_timer.settings_mut().accuracy = value;
418            }
419            11 => self.settings.show_segment_name = value.into(),
420            12 => self.settings.display_icon = value.into(),
421            _ => panic!("Unsupported Setting Index"),
422        }
423    }
424}