livesplit_core/component/title/
mod.rs1use crate::{
7 platform::prelude::*,
8 settings::{
9 Alignment, CachedImageId, Color, Field, Gradient, Image, ImageData, SettingsDescription,
10 Value,
11 },
12 Timer, TimerPhase,
13};
14use core::fmt::Write;
15use livesplit_title_abbreviations::{abbreviate as abbreviate_title, abbreviate_category};
16use serde::{Deserialize, Serialize};
17use smallstr::SmallString;
18
19#[cfg(test)]
20mod tests;
21
22#[derive(Default, Clone)]
26pub struct Component {
27 icon_id: CachedImageId,
28 settings: Settings,
29}
30
31#[derive(Clone, Serialize, Deserialize)]
33#[serde(default)]
34pub struct Settings {
35 pub background: Gradient,
37 pub text_color: Option<Color>,
40 pub show_game_name: bool,
43 pub show_category_name: bool,
46 pub show_finished_runs_count: bool,
49 pub show_attempt_count: bool,
51 pub text_alignment: Alignment,
53 pub display_as_single_line: bool,
57 pub display_game_icon: bool,
60 pub show_region: bool,
64 pub show_platform: bool,
68 pub show_variables: bool,
71}
72
73#[derive(Default, Serialize, Deserialize)]
75pub struct State {
76 pub background: Gradient,
78 pub text_color: Option<Color>,
81 pub icon_change: Option<ImageData>,
86 pub line1: Vec<Box<str>>,
91 pub line2: Vec<Box<str>>,
97 pub is_centered: bool,
100 pub finished_runs: Option<u32>,
103 pub attempts: Option<u32>,
106}
107
108impl Default for Settings {
109 fn default() -> Self {
110 Self {
111 background: Gradient::Vertical(
112 Color::hsla(0.0, 0.0, 1.0, 0.13),
113 Color::hsla(0.0, 0.0, 1.0, 0.0),
114 ),
115 text_color: None,
116 show_game_name: true,
117 show_category_name: true,
118 show_finished_runs_count: false,
119 show_attempt_count: true,
120 text_alignment: Alignment::Auto,
121 display_as_single_line: false,
122 display_game_icon: true,
123 show_region: false,
124 show_platform: false,
125 show_variables: true,
126 }
127 }
128}
129
130#[cfg(feature = "std")]
131impl State {
132 pub fn write_json<W>(&self, writer: W) -> serde_json::Result<()>
134 where
135 W: std::io::Write,
136 {
137 serde_json::to_writer(writer, self)
138 }
139}
140
141impl Component {
142 pub fn new() -> Self {
144 Default::default()
145 }
146
147 pub fn with_settings(settings: Settings) -> Self {
149 Self {
150 settings,
151 ..Default::default()
152 }
153 }
154
155 pub const fn settings(&self) -> &Settings {
157 &self.settings
158 }
159
160 pub fn settings_mut(&mut self) -> &mut Settings {
162 &mut self.settings
163 }
164
165 pub const fn name(&self) -> &'static str {
167 "Title"
168 }
169
170 pub fn update_state(&mut self, state: &mut State, timer: &Timer) {
172 let run = timer.run();
173
174 let finished_runs = if self.settings.show_finished_runs_count {
175 let mut count = timer
176 .run()
177 .attempt_history()
178 .iter()
179 .filter(|a| a.time().real_time.is_some())
180 .count() as u32;
181
182 if timer.current_phase() == TimerPhase::Ended {
183 count += 1;
184 }
185
186 Some(count)
187 } else {
188 None
189 };
190
191 let attempts = if self.settings.show_attempt_count {
192 Some(run.attempt_count())
193 } else {
194 None
195 };
196
197 let game_icon = Some(run.game_icon()).filter(|_| self.settings.display_game_icon);
198 let icon_change = self.icon_id.update_with(game_icon).map(Into::into);
199
200 let is_centered = match self.settings.text_alignment {
201 Alignment::Center => true,
202 Alignment::Left => false,
203 Alignment::Auto => game_icon.map_or(true, Image::is_empty),
204 };
205
206 let game_name = if self.settings.show_game_name {
207 run.game_name()
208 } else {
209 ""
210 };
211
212 let mut full_category_name = SmallString::<[u8; 1024]>::new();
213 let _ = write!(
214 full_category_name,
215 "{}",
216 run.extended_category_name(
217 self.settings.show_region,
218 self.settings.show_platform,
219 self.settings.show_variables,
220 ),
221 );
222
223 let full_category_name = if self.settings.show_category_name {
224 &full_category_name
225 } else {
226 ""
227 };
228
229 match (!game_name.is_empty(), !full_category_name.is_empty()) {
230 (true, true) => {
231 if self.settings.display_as_single_line {
232 let unchanged = catch! {
233 let mut rem = &**state.line1.last()?;
234
235 let Some(rest) = rem.strip_prefix(game_name) else {
236 return None;
237 };
238 rem = rest;
239
240 if !game_name.is_empty() && !full_category_name.is_empty() {
241 let Some(rest) = rem.strip_prefix(" - ") else {
242 return None;
243 };
244 rem = rest;
245 }
246
247 if rem != full_category_name {
248 return None;
249 }
250 };
251 if unchanged.is_none() {
252 let abbrevs = &mut state.line1;
253 abbrevs.clear();
254
255 let mut abbrev = String::new();
256 let game_abbrevs = abbreviate_title(game_name);
257 let category_abbrevs = abbreviate_category(full_category_name);
258
259 if !full_category_name.is_empty() {
260 for game_abbrev in game_abbrevs.iter() {
261 abbrev.clear();
262 abbrev.push_str(game_abbrev);
263 if !game_abbrev.is_empty() {
264 abbrev.push_str(" - ");
265 }
266 abbrev.push_str(full_category_name);
267 abbrevs.push(abbrev.as_str().into());
268 }
269 }
270 let swap_index = abbrevs.len().checked_sub(1);
274
275 if let Some(shortest_game_name) =
276 game_abbrevs.iter().min_by_key(|a| a.len())
277 {
278 abbrev.clear();
279 abbrev.push_str(shortest_game_name);
280 let game_len = abbrev.len();
281 for category_abbrev in category_abbrevs.iter() {
282 if !shortest_game_name.is_empty() && !category_abbrev.is_empty() {
283 abbrev.push_str(" - ");
284 }
285 abbrev.push_str(category_abbrev);
286 abbrevs.push(abbrev.as_str().into());
287 abbrev.drain(game_len..);
288 }
289 }
290
291 if let Some(swap_index) = swap_index {
294 let last_element_idx = abbrevs.len() - 1;
295 abbrevs.swap(swap_index, last_element_idx);
296 }
297 }
298 state.line2.clear();
299 } else {
300 if state.line1.last().map_or(true, |g| game_name != &**g) {
301 state.line1.clear();
302 state.line1.extend(abbreviate_title(game_name));
303 }
304 if state
305 .line2
306 .last()
307 .map_or(true, |c| full_category_name != &**c)
308 {
309 state.line2.clear();
310 state.line2.extend(abbreviate_category(full_category_name));
311 }
312 }
313 }
314 (true, false) => {
315 if state.line1.last().map_or(true, |g| game_name != &**g) {
316 state.line1.clear();
317 state.line1.extend(abbreviate_title(game_name));
318 }
319 state.line2.clear();
320 }
321 (false, true) => {
322 if state
323 .line1
324 .last()
325 .map_or(true, |c| full_category_name != &**c)
326 {
327 state.line1.clear();
328 state.line1.extend(abbreviate_category(full_category_name));
329 }
330 state.line2.clear();
331 }
332 (false, false) => {
333 state.line1.clear();
334 state.line1.push("Untitled".into());
335 state.line2.clear();
336 }
337 }
338
339 state.background = self.settings.background;
340 state.text_color = self.settings.text_color;
341 state.icon_change = icon_change;
342 state.finished_runs = finished_runs;
343 state.attempts = attempts;
344 state.is_centered = is_centered;
345 }
346
347 pub fn state(&mut self, timer: &Timer) -> State {
349 let mut state = Default::default();
350 self.update_state(&mut state, timer);
351 state
352 }
353
354 pub fn remount(&mut self) {
360 self.icon_id.reset();
361 }
362
363 pub fn settings_description(&self) -> SettingsDescription {
366 SettingsDescription::with_fields(vec![
367 Field::new("Background".into(), self.settings.background.into()),
368 Field::new("Text Color".into(), self.settings.text_color.into()),
369 Field::new("Show Game Name".into(), self.settings.show_game_name.into()),
370 Field::new(
371 "Show Category Name".into(),
372 self.settings.show_category_name.into(),
373 ),
374 Field::new(
375 "Show Finished Runs Count".into(),
376 self.settings.show_finished_runs_count.into(),
377 ),
378 Field::new(
379 "Show Attempt Count".into(),
380 self.settings.show_attempt_count.into(),
381 ),
382 Field::new("Text Alignment".into(), self.settings.text_alignment.into()),
383 Field::new(
384 "Display Text as Single Line".into(),
385 self.settings.display_as_single_line.into(),
386 ),
387 Field::new(
388 "Display Game Icon".into(),
389 self.settings.display_game_icon.into(),
390 ),
391 Field::new("Show Region".into(), self.settings.show_region.into()),
392 Field::new("Show Platform".into(), self.settings.show_platform.into()),
393 Field::new("Show Variables".into(), self.settings.show_variables.into()),
394 ])
395 }
396
397 pub fn set_value(&mut self, index: usize, value: Value) {
405 match index {
406 0 => self.settings.background = value.into(),
407 1 => self.settings.text_color = value.into(),
408 2 => self.settings.show_game_name = value.into(),
409 3 => self.settings.show_category_name = value.into(),
410 4 => self.settings.show_finished_runs_count = value.into(),
411 5 => self.settings.show_attempt_count = value.into(),
412 6 => self.settings.text_alignment = value.into(),
413 7 => self.settings.display_as_single_line = value.into(),
414 8 => self.settings.display_game_icon = value.into(),
415 9 => self.settings.show_region = value.into(),
416 10 => self.settings.show_platform = value.into(),
417 11 => self.settings.show_variables = value.into(),
418 _ => panic!("Unsupported Setting Index"),
419 }
420 }
421}