livesplit_core/run/editor/
state.rs

1use super::{Editor, SegmentRow, TimingMethod};
2use crate::{
3    comparison::personal_best,
4    platform::prelude::*,
5    run::RunMetadata,
6    settings::{CachedImageId, ImageData},
7    timing::formatter::{none_wrapper::EmptyWrapper, Accuracy, SegmentTime, TimeFormatter},
8};
9use serde::{Deserialize, Serialize};
10
11/// Represents the current state of the Run Editor in order to visualize it
12/// properly.
13#[derive(Debug, Serialize, Deserialize)]
14pub struct State {
15    /// The game's icon encoded as the raw file bytes. This value is only
16    /// specified whenever the icon changes. The buffer itself may be empty.
17    /// This indicates that there is no icon.
18    pub icon_change: Option<ImageData>,
19    /// The name of the game the Run is for.
20    pub game: String,
21    /// The name of the category the Run is for.
22    pub category: String,
23    /// The timer offset specifies the time that the timer starts at when
24    /// starting a new attempt.
25    pub offset: String,
26    /// The number of times this Run has been attempted by the runner. This
27    /// is mostly just a visual number and has no effect on any history.
28    pub attempts: u32,
29    /// The timing method that is currently selected to be visualized and
30    /// edited.
31    pub timing_method: TimingMethod,
32    /// The state of all the segments.
33    pub segments: Vec<Segment>,
34    /// The names of all the custom comparisons that exist for this Run.
35    pub comparison_names: Vec<String>,
36    /// Describes which actions are currently available.
37    pub buttons: Buttons,
38    /// Additional metadata of this Run, like the platform and region of the
39    /// game.
40    pub metadata: RunMetadata,
41}
42
43/// Describes which actions are currently available. Depending on how many
44/// segments exist and which ones are selected, only some actions can be
45/// executed successfully.
46#[derive(Debug, Serialize, Deserialize)]
47pub struct Buttons {
48    /// Describes whether the currently selected segments can be removed. If all
49    /// segments are selected, they can't be removed.
50    pub can_remove: bool,
51    /// Describes whether the currently selected segments can be moved up. If
52    /// any one of the selected segments is the first segment, then they can't
53    /// be moved.
54    pub can_move_up: bool,
55    /// Describes whether the currently selected segments can be moved down. If
56    /// any one of the selected segments is the last segment, then they can't be
57    /// moved.
58    pub can_move_down: bool,
59}
60
61/// Describes the current state of a segment.
62#[derive(Debug, Serialize, Deserialize)]
63pub struct Segment {
64    /// The segment's icon encoded as the raw file bytes. This value is only
65    /// specified whenever the icon changes. The buffer itself may be empty.
66    /// This indicates that there is no icon.
67    pub icon_change: Option<ImageData>,
68    /// The name of the segment.
69    pub name: String,
70    /// The segment's split time for the active timing method.
71    pub split_time: String,
72    /// The segment time for the active timing method.
73    pub segment_time: String,
74    /// The best segment time for the active timing method.
75    pub best_segment_time: String,
76    /// All of the times of the custom comparison for the active timing method.
77    /// The order of these matches up with the order of the custom comparisons
78    /// provided by the Run Editor's State object.
79    pub comparison_times: Vec<String>,
80    /// Describes the segment's selection state.
81    pub selected: SelectionState,
82}
83
84/// Describes a segment's selection state.
85#[derive(Debug, Serialize, Deserialize)]
86pub enum SelectionState {
87    /// The segment is not selected.
88    NotSelected,
89    /// The segment is selected.
90    Selected,
91    /// The segment is selected and active. There is only one active segment and
92    /// it is the one that is being actively edited.
93    Active,
94}
95
96impl SelectionState {
97    /// Returns `true` if the segment is selected.
98    pub const fn is_selected_or_active(&self) -> bool {
99        matches!(self, Self::Selected | Self::Active)
100    }
101}
102
103#[cfg(feature = "std")]
104impl State {
105    /// Encodes the state object's information as JSON.
106    pub fn write_json<W>(&self, writer: W) -> serde_json::Result<()>
107    where
108        W: std::io::Write,
109    {
110        serde_json::to_writer(writer, self)
111    }
112}
113
114impl Editor {
115    /// Calculates the Run Editor's state in order to visualize it.
116    pub fn state(&mut self) -> State {
117        let formatter = EmptyWrapper::new(SegmentTime::with_accuracy(Accuracy::Hundredths));
118
119        let icon_change = self
120            .game_icon_id
121            .update_with(self.run.game_icon().into())
122            .map(Into::into);
123        let game = self.game_name().to_string();
124        let category = self.category_name().to_string();
125        let offset = formatter.format(self.offset()).to_string();
126        let attempts = self.attempt_count();
127        let timing_method = self.selected_timing_method();
128        let comparison_names = self
129            .custom_comparisons()
130            .iter()
131            .filter(|&n| n != personal_best::NAME)
132            .cloned()
133            .collect::<Vec<_>>();
134
135        let buttons = Buttons {
136            can_remove: self.can_remove_segments(),
137            can_move_up: self.can_move_segments_up(),
138            can_move_down: self.can_move_segments_down(),
139        };
140        let mut segments = Vec::with_capacity(self.run.len());
141
142        self.segment_icon_ids
143            .resize(self.run.len(), CachedImageId::default());
144
145        for segment_index in 0..self.run.len() {
146            let (name, split_time, segment_time, best_segment_time, comparison_times);
147            {
148                let row = SegmentRow::new(segment_index, self);
149                name = row.name().to_string();
150                split_time = formatter.format(row.split_time()).to_string();
151                segment_time = formatter.format(row.segment_time()).to_string();
152                best_segment_time = formatter.format(row.best_segment_time()).to_string();
153                comparison_times = comparison_names
154                    .iter()
155                    .map(|c| formatter.format(row.comparison_time(c)).to_string())
156                    .collect();
157            }
158
159            let icon_change = self.segment_icon_ids[segment_index]
160                .update_with(self.run.segment(segment_index).icon().into())
161                .map(Into::into);
162
163            let selected = if self.active_segment_index() == segment_index {
164                SelectionState::Active
165            } else if self.selected_segments.contains(&segment_index) {
166                SelectionState::Selected
167            } else {
168                SelectionState::NotSelected
169            };
170
171            segments.push(Segment {
172                icon_change,
173                name,
174                split_time,
175                segment_time,
176                best_segment_time,
177                comparison_times,
178                selected,
179            });
180        }
181
182        State {
183            icon_change,
184            game,
185            category,
186            offset,
187            attempts,
188            timing_method,
189            segments,
190            comparison_names,
191            buttons,
192            metadata: self.run.metadata().clone(),
193        }
194    }
195}