livesplit_core/component/splits/
mod.rs

1//! Provides the Splits Component and relevant types for using it. The Splits
2//! Component is the main component for visualizing all the split times. Each
3//! Each [`Segment`](crate::run::Segment) is shown in a tabular fashion showing
4//! the segment icon, segment name, the delta compared to the chosen comparison,
5//! and the split time. The list provides scrolling functionality, so not every
6//! [`Segment`](crate::run::Segment) needs to be shown all the time.
7
8use crate::{
9    platform::prelude::*,
10    settings::{
11        self, CachedImageId, Color, Field, Gradient, ImageData, ListGradient, SettingsDescription,
12        Value,
13    },
14    timing::{formatter::Accuracy, Snapshot},
15    util::{Clear, ClearVec},
16    GeneralLayoutSettings,
17};
18use core::cmp::{max, min};
19use serde::{Deserialize, Serialize};
20
21#[cfg(test)]
22mod tests;
23
24mod column;
25
26pub use column::{
27    ColumnKind, ColumnSettings, ColumnStartWith, ColumnState, ColumnUpdateTrigger,
28    ColumnUpdateWith, TimeColumn, VariableColumn,
29};
30
31const SETTINGS_BEFORE_COLUMNS: usize = 15;
32const SETTINGS_PER_TIME_COLUMN: usize = 6;
33const SETTINGS_PER_VARIABLE_COLUMN: usize = 2;
34
35/// The Splits Component is the main component for visualizing all the split
36/// times. Each [`Segment`](crate::run::Segment) is shown in a tabular fashion
37/// showing the segment icon, segment name, the delta compared to the chosen
38/// comparison, and the split time.
39/// The list provides scrolling functionality, so not every segment needs
40/// to be shown all the time.
41#[derive(Default, Clone)]
42pub struct Component {
43    icon_ids: Vec<CachedImageId>,
44    settings: Settings,
45    current_split_index: Option<usize>,
46    scroll_offset: isize,
47}
48
49/// The Settings for this component.
50#[derive(Clone, Serialize, Deserialize)]
51#[serde(default)]
52pub struct Settings {
53    /// The background shown behind the splits.
54    pub background: ListGradient,
55    /// The amount of segments to show in the list at any given time. If this is
56    /// set to 0, all the segments are shown. If this is set to a number lower
57    /// than the total amount of segments, only a certain window of all the
58    /// segments is shown. This window can scroll up or down.
59    pub visual_split_count: usize,
60    /// If there's more segments than segments that are shown, the window
61    /// showing the segments automatically scrolls up and down when the current
62    /// segment changes. This count determines the minimum number of future
63    /// segments to be shown in this scrolling window when it automatically
64    /// scrolls.
65    pub split_preview_count: usize,
66    /// Specifies whether thin separators should be shown between the individual
67    /// segments shown by the component.
68    pub show_thin_separators: bool,
69    /// If the last segment is to always be shown, this determines whether to
70    /// show a more pronounced separator in front of the last segment, if it is
71    /// not directly adjacent to the segment shown right before it in the
72    /// scrolling window.
73    pub separator_last_split: bool,
74    /// If not every segment is shown in the scrolling window of segments, then
75    /// this determines whether the final segment is always to be shown, as it
76    /// contains valuable information about the total duration of the chosen
77    /// comparison, which is often the runner's Personal Best.
78    pub always_show_last_split: bool,
79    /// If there's not enough segments to fill the list of splits, this option
80    /// allows filling the remaining splits with blank space in order to
81    /// maintain the visual split count specified. Otherwise the visual split
82    /// count is reduced to the actual amount of segments.
83    pub fill_with_blank_space: bool,
84    /// Specifies whether to display each split as two rows, with the segment
85    /// name being in one row and the times being in the other.
86    pub display_two_rows: bool,
87    /// The gradient to show behind the current segment as an indicator of it
88    /// being the current segment.
89    pub current_split_gradient: Gradient,
90    /// Specifies the display accuracy of split times.
91    pub split_time_accuracy: Accuracy,
92    /// Specifies the display accuracy of segment times.
93    pub segment_time_accuracy: Accuracy,
94    /// Specifies the display accuracy of delta times.
95    pub delta_time_accuracy: Accuracy,
96    /// Whether to drop the fractional part of a delta time once it goes past
97    /// one minute.
98    pub delta_drop_decimals: bool,
99    /// Specifies whether to show the names of the columns above the splits.
100    pub show_column_labels: bool,
101    /// The columns to show on the splits. These can be configured in various
102    /// way to show split times, segment times, deltas and so on. The columns
103    /// are defined from right to left.
104    pub columns: Vec<ColumnSettings>,
105}
106
107/// The state object that describes a single segment's information to visualize.
108#[derive(Debug, Serialize, Deserialize)]
109pub struct SplitState {
110    /// The name of the segment.
111    pub name: String,
112    /// The state of each column from right to left. The amount of columns is
113    /// not guaranteed to be the same across different splits.
114    pub columns: ClearVec<ColumnState>,
115    /// Describes if this segment is the segment the active attempt is currently
116    /// on.
117    pub is_current_split: bool,
118    /// The index of the segment based on all the segments of the run. This may
119    /// differ from the index of this `SplitState` in the `State` object, as
120    /// there can be a scrolling window, showing only a subset of segments. Each
121    /// index is guaranteed to be unique.
122    pub index: usize,
123}
124
125impl Clear for SplitState {
126    fn clear(&mut self) {
127        self.name.clear();
128        self.columns.clear();
129    }
130}
131
132/// Describes the icon to be shown for a certain segment. This is provided
133/// whenever a segment is first shown or whenever its icon changes. If
134/// necessary, you may remount this component to reset the component into a
135/// state where these icons are provided again.
136#[derive(Debug, Serialize, Deserialize)]
137pub struct IconChange {
138    /// The index of the segment of which the icon changed. This is based on the
139    /// index in the run, not on the index of the `SplitState` in the `State`
140    /// object. The corresponding index is the `index` field of the `SplitState`
141    /// object.
142    pub segment_index: usize,
143    /// The segment's icon encoded as the raw file bytes. The buffer itself may
144    /// be empty. This indicates that there is no icon.
145    pub icon: ImageData,
146}
147
148/// The state object describes the information to visualize for this component.
149#[derive(Debug, Default, Serialize, Deserialize)]
150pub struct State {
151    /// The background shown behind the splits.
152    pub background: ListGradient,
153    /// The column labels to visualize about the list of splits. If this is
154    /// `None`, no labels are supposed to be visualized. The list is specified
155    /// from right to left.
156    pub column_labels: Option<ClearVec<String>>,
157    /// The list of all the segments to visualize.
158    pub splits: ClearVec<SplitState>,
159    /// This list describes all the icon changes that happened. Each time a
160    /// segment is first shown or its icon changes, the new icon is provided in
161    /// this list. If necessary, you may remount this component to reset the
162    /// component into a state where these icons are provided again.
163    pub icon_changes: Vec<IconChange>,
164    /// Specifies whether the current run has any icons, even those that are not
165    /// currently visible by the splits component. This allows for properly
166    /// indenting the icon column, even when the icons are scrolled outside the
167    /// splits component.
168    pub has_icons: bool,
169    /// Specifies whether thin separators should be shown between the individual
170    /// segments shown by the component.
171    pub show_thin_separators: bool,
172    /// Describes whether a more pronounced separator should be shown in front
173    /// of the last segment provided.
174    pub show_final_separator: bool,
175    /// Specifies whether to display each split as two rows, with the segment
176    /// name being in one row and the times being in the other.
177    pub display_two_rows: bool,
178    /// The gradient to show behind the current segment as an indicator of it
179    /// being the current segment.
180    pub current_split_gradient: Gradient,
181}
182
183impl Default for Settings {
184    fn default() -> Self {
185        Settings {
186            background: ListGradient::Alternating(
187                Color::transparent(),
188                Color::rgba(1.0, 1.0, 1.0, 0.04),
189            ),
190            visual_split_count: 16,
191            split_preview_count: 1,
192            show_thin_separators: true,
193            separator_last_split: true,
194            always_show_last_split: true,
195            fill_with_blank_space: true,
196            display_two_rows: false,
197            current_split_gradient: Gradient::Vertical(
198                Color::rgba(51.0 / 255.0, 115.0 / 255.0, 244.0 / 255.0, 1.0),
199                Color::rgba(21.0 / 255.0, 53.0 / 255.0, 116.0 / 255.0, 1.0),
200            ),
201            split_time_accuracy: Accuracy::Seconds,
202            segment_time_accuracy: Accuracy::Hundredths,
203            delta_time_accuracy: Accuracy::Tenths,
204            delta_drop_decimals: true,
205            show_column_labels: false,
206            columns: vec![
207                ColumnSettings {
208                    name: String::from("Time"),
209                    kind: ColumnKind::Time(TimeColumn {
210                        start_with: ColumnStartWith::ComparisonTime,
211                        update_with: ColumnUpdateWith::SplitTime,
212                        update_trigger: ColumnUpdateTrigger::OnEndingSegment,
213                        comparison_override: None,
214                        timing_method: None,
215                    }),
216                },
217                ColumnSettings {
218                    name: String::from("+/−"),
219                    kind: ColumnKind::Time(TimeColumn {
220                        start_with: ColumnStartWith::Empty,
221                        update_with: ColumnUpdateWith::Delta,
222                        update_trigger: ColumnUpdateTrigger::Contextual,
223                        comparison_override: None,
224                        timing_method: None,
225                    }),
226                },
227            ],
228        }
229    }
230}
231
232#[cfg(feature = "std")]
233impl State {
234    /// Encodes the state object's information as JSON.
235    pub fn write_json<W>(&self, writer: W) -> serde_json::Result<()>
236    where
237        W: std::io::Write,
238    {
239        serde_json::to_writer(writer, self)
240    }
241}
242
243impl Component {
244    /// Creates a new Splits Component.
245    pub fn new() -> Self {
246        Default::default()
247    }
248
249    /// Creates a new Splits Component with the given settings.
250    pub fn with_settings(settings: Settings) -> Self {
251        Self {
252            settings,
253            ..Default::default()
254        }
255    }
256
257    /// Accesses the settings of the component.
258    pub const fn settings(&self) -> &Settings {
259        &self.settings
260    }
261
262    /// Grants mutable access to the settings of the component.
263    pub fn settings_mut(&mut self) -> &mut Settings {
264        &mut self.settings
265    }
266
267    /// Scrolls up the window of the segments that are shown. Doesn't move the
268    /// scroll window if it reaches the top of the segments.
269    pub fn scroll_up(&mut self) {
270        self.scroll_offset = self.scroll_offset.saturating_sub(1);
271    }
272
273    /// Scrolls down the window of the segments that are shown. Doesn't move the
274    /// scroll window if it reaches the bottom of the segments.
275    pub fn scroll_down(&mut self) {
276        self.scroll_offset = self.scroll_offset.saturating_add(1);
277    }
278
279    /// Remounts the component as if it was freshly initialized. The segment
280    /// icons shown by this component are only provided in the state objects
281    /// whenever the icon changes or whenever the component's state is first
282    /// queried. Remounting returns the segment icons again, whenever its state
283    /// is queried the next time.
284    pub fn remount(&mut self) {
285        self.icon_ids.clear();
286    }
287
288    /// Accesses the name of the component.
289    pub const fn name(&self) -> &'static str {
290        "Splits"
291    }
292
293    /// Updates the component's state based on the timer and layout settings
294    /// provided.
295    pub fn update_state(
296        &mut self,
297        state: &mut State,
298        timer: &Snapshot<'_>,
299        layout_settings: &GeneralLayoutSettings,
300    ) {
301        // Reset Scroll Offset when any movement of the split index is observed.
302        if self.current_split_index != timer.current_split_index() {
303            self.current_split_index = timer.current_split_index();
304            self.scroll_offset = 0;
305        }
306
307        let run = timer.run();
308        self.icon_ids.resize(run.len(), CachedImageId::default());
309
310        let mut visual_split_count = self.settings.visual_split_count;
311        if visual_split_count == 0 {
312            visual_split_count = run.len();
313        }
314
315        let current_split = timer.current_split_index();
316        let method = timer.current_timing_method();
317
318        let locked_last_split = isize::from(self.settings.always_show_last_split);
319        let skip_count = min(
320            current_split.map_or(0, |current_split| {
321                max(
322                    0,
323                    current_split as isize
324                        + self.settings.split_preview_count as isize
325                        + locked_last_split
326                        + 1
327                        - visual_split_count as isize,
328                )
329            }),
330            run.len() as isize - visual_split_count as isize,
331        );
332        self.scroll_offset = min(
333            max(self.scroll_offset, -skip_count),
334            run.len() as isize - skip_count - visual_split_count as isize,
335        );
336        let skip_count = max(0, skip_count + self.scroll_offset) as usize;
337        let take_count = visual_split_count - locked_last_split as usize;
338        let always_show_last_split = self.settings.always_show_last_split;
339
340        let show_final_separator = self.settings.separator_last_split
341            && always_show_last_split
342            && skip_count + take_count + 1 < run.len();
343
344        let Settings {
345            show_thin_separators,
346            fill_with_blank_space,
347            display_two_rows,
348            ref columns,
349            ..
350        } = self.settings;
351
352        state.background = self.settings.background;
353
354        if self.settings.show_column_labels {
355            let column_labels = state.column_labels.get_or_insert_with(Default::default);
356            column_labels.clear();
357            for c in &self.settings.columns {
358                column_labels.push().push_str(&c.name);
359            }
360        } else {
361            state.column_labels = None;
362        }
363
364        let icon_changes = &mut state.icon_changes;
365        icon_changes.clear();
366
367        state.splits.clear();
368        for ((i, segment), icon_id) in run
369            .segments()
370            .iter()
371            .enumerate()
372            .zip(self.icon_ids.iter_mut())
373            .skip(skip_count)
374            .filter(|&((i, _), _)| {
375                i - skip_count < take_count || (always_show_last_split && i + 1 == run.len())
376            })
377        {
378            let state = state.splits.push_with(|| SplitState {
379                name: String::new(),
380                columns: ClearVec::new(),
381                is_current_split: false,
382                index: 0,
383            });
384
385            if let Some(icon_change) = icon_id.update_with(Some(segment.icon())) {
386                icon_changes.push(IconChange {
387                    segment_index: i,
388                    icon: icon_change.into(),
389                });
390            }
391
392            state.name.push_str(segment.name());
393
394            for column in columns {
395                column::update_state(
396                    state.columns.push_with(|| ColumnState {
397                        value: String::new(),
398                        semantic_color: Default::default(),
399                        visual_color: Color::transparent(),
400                        updates_frequently: false,
401                    }),
402                    column,
403                    timer,
404                    &self.settings,
405                    layout_settings,
406                    segment,
407                    i,
408                    current_split,
409                    method,
410                );
411            }
412
413            state.is_current_split = Some(i) == current_split;
414            state.index = i;
415        }
416
417        if fill_with_blank_space && state.splits.len() < visual_split_count {
418            let blank_split_count = visual_split_count - state.splits.len();
419            for i in 0..blank_split_count {
420                let state = state.splits.push_with(|| SplitState {
421                    name: String::new(),
422                    columns: ClearVec::new(),
423                    is_current_split: false,
424                    index: 0,
425                });
426                state.is_current_split = false;
427                state.index = (usize::max_value() ^ 1) - 2 * i;
428            }
429        }
430
431        state.has_icons = run.segments().iter().any(|s| !s.icon().is_empty());
432        state.show_thin_separators = show_thin_separators;
433        state.show_final_separator = show_final_separator;
434        state.display_two_rows = display_two_rows;
435        state.current_split_gradient = self.settings.current_split_gradient;
436    }
437
438    /// Calculates the component's state based on the timer and layout settings
439    /// provided.
440    pub fn state(
441        &mut self,
442        timer: &Snapshot<'_>,
443        layout_settings: &GeneralLayoutSettings,
444    ) -> State {
445        let mut state = Default::default();
446        self.update_state(&mut state, timer, layout_settings);
447        state
448    }
449
450    /// Accesses a generic description of the settings available for this
451    /// component and their current values.
452    pub fn settings_description(&self) -> SettingsDescription {
453        let mut settings = SettingsDescription::with_fields(vec![
454            Field::new("Background".into(), self.settings.background.into()),
455            Field::new(
456                "Total Splits".into(),
457                Value::UInt(self.settings.visual_split_count as _),
458            ),
459            Field::new(
460                "Upcoming Splits".into(),
461                Value::UInt(self.settings.split_preview_count as _),
462            ),
463            Field::new(
464                "Show Thin Separators".into(),
465                self.settings.show_thin_separators.into(),
466            ),
467            Field::new(
468                "Show Separator Before Last Split".into(),
469                self.settings.separator_last_split.into(),
470            ),
471            Field::new(
472                "Always Show Last Split".into(),
473                self.settings.always_show_last_split.into(),
474            ),
475            Field::new(
476                "Fill with Blank Space if Not Enough Splits".into(),
477                self.settings.fill_with_blank_space.into(),
478            ),
479            Field::new(
480                "Display 2 Rows".into(),
481                self.settings.display_two_rows.into(),
482            ),
483            Field::new(
484                "Current Split Gradient".into(),
485                self.settings.current_split_gradient.into(),
486            ),
487            Field::new(
488                "Split Time Accuracy".into(),
489                self.settings.split_time_accuracy.into(),
490            ),
491            Field::new(
492                "Segment Time Accuracy".into(),
493                self.settings.segment_time_accuracy.into(),
494            ),
495            Field::new(
496                "Delta Time Accuracy".into(),
497                self.settings.delta_time_accuracy.into(),
498            ),
499            Field::new(
500                "Drop Delta Decimals When Showing Minutes".into(),
501                self.settings.delta_drop_decimals.into(),
502            ),
503            Field::new(
504                "Show Column Labels".into(),
505                self.settings.show_column_labels.into(),
506            ),
507            Field::new(
508                "Columns".into(),
509                Value::UInt(self.settings.columns.len() as _),
510            ),
511        ]);
512
513        settings.fields.reserve_exact(
514            self.settings
515                .columns
516                .iter()
517                .map(|column| match column.kind {
518                    ColumnKind::Variable(_) => SETTINGS_PER_VARIABLE_COLUMN,
519                    ColumnKind::Time(_) => SETTINGS_PER_TIME_COLUMN,
520                })
521                .sum(),
522        );
523
524        for column in &self.settings.columns {
525            settings
526                .fields
527                .push(Field::new("Column Name".into(), column.name.clone().into()));
528
529            match &column.kind {
530                ColumnKind::Variable(column) => {
531                    settings.fields.push(Field::new(
532                        "Column Type".into(),
533                        settings::ColumnKind::Variable.into(),
534                    ));
535                    settings.fields.push(Field::new(
536                        "Variable Name".into(),
537                        column.variable_name.clone().into(),
538                    ));
539                }
540                ColumnKind::Time(column) => {
541                    settings.fields.push(Field::new(
542                        "Column Type".into(),
543                        settings::ColumnKind::Time.into(),
544                    ));
545                    settings
546                        .fields
547                        .push(Field::new("Start With".into(), column.start_with.into()));
548                    settings
549                        .fields
550                        .push(Field::new("Update With".into(), column.update_with.into()));
551                    settings.fields.push(Field::new(
552                        "Update Trigger".into(),
553                        column.update_trigger.into(),
554                    ));
555                    settings.fields.push(Field::new(
556                        "Comparison".into(),
557                        column.comparison_override.clone().into(),
558                    ));
559                    settings.fields.push(Field::new(
560                        "Timing Method".into(),
561                        column.timing_method.into(),
562                    ));
563                }
564            }
565        }
566
567        settings
568    }
569
570    /// Sets a setting's value by its index to the given value.
571    ///
572    /// # Panics
573    ///
574    /// This panics if the type of the value to be set is not compatible with
575    /// the type of the setting's value. A panic can also occur if the index of
576    /// the setting provided is out of bounds.
577    pub fn set_value(&mut self, index: usize, value: Value) {
578        match index {
579            0 => self.settings.background = value.into(),
580            1 => self.settings.visual_split_count = value.into_uint().unwrap() as _,
581            2 => self.settings.split_preview_count = value.into_uint().unwrap() as _,
582            3 => self.settings.show_thin_separators = value.into(),
583            4 => self.settings.separator_last_split = value.into(),
584            5 => self.settings.always_show_last_split = value.into(),
585            6 => self.settings.fill_with_blank_space = value.into(),
586            7 => self.settings.display_two_rows = value.into(),
587            8 => self.settings.current_split_gradient = value.into(),
588            9 => self.settings.split_time_accuracy = value.into(),
589            10 => self.settings.segment_time_accuracy = value.into(),
590            11 => self.settings.delta_time_accuracy = value.into(),
591            12 => self.settings.delta_drop_decimals = value.into(),
592            13 => self.settings.show_column_labels = value.into(),
593            14 => {
594                let new_len = value.into_uint().unwrap() as usize;
595                self.settings.columns.resize(new_len, Default::default());
596            }
597            index => {
598                let mut index = index - SETTINGS_BEFORE_COLUMNS;
599                for column in &mut self.settings.columns {
600                    if index < 2 {
601                        match index {
602                            0 => column.name = value.into(),
603                            _ => {
604                                column.kind = match settings::ColumnKind::from(value) {
605                                    settings::ColumnKind::Time => {
606                                        ColumnKind::Time(Default::default())
607                                    }
608                                    settings::ColumnKind::Variable => {
609                                        ColumnKind::Variable(Default::default())
610                                    }
611                                }
612                            }
613                        }
614                        return;
615                    }
616                    index -= 2;
617                    match &mut column.kind {
618                        ColumnKind::Variable(column) => {
619                            if index < 1 {
620                                column.variable_name = value.into();
621                                return;
622                            }
623                            index -= 1;
624                        }
625                        ColumnKind::Time(column) => {
626                            if index < 5 {
627                                match index {
628                                    0 => column.start_with = value.into(),
629                                    1 => column.update_with = value.into(),
630                                    2 => column.update_trigger = value.into(),
631                                    3 => column.comparison_override = value.into(),
632                                    _ => column.timing_method = value.into(),
633                                }
634                                return;
635                            }
636                            index -= 5;
637                        }
638                    }
639                }
640                panic!("Unsupported Setting Index")
641            }
642        }
643    }
644}