1use super::{Component, Layout, LayoutDirection};
4use crate::{
5 component::{separator, timer::DeltaGradient},
6 platform::{math::f32::powf, prelude::*},
7 settings::{
8 Alignment, Color, Font, FontStretch, FontStyle, FontWeight, Gradient, ListGradient,
9 },
10 timing::{
11 formatter::{Accuracy, DigitsFormat},
12 TimingMethod,
13 },
14 util::xml::{
15 helper::{
16 end_tag, parse_base, parse_children, text, text_as_escaped_string_err, text_parsed,
17 Error as XmlError,
18 },
19 Reader,
20 },
21};
22use core::{mem::MaybeUninit, num::ParseIntError, str};
23
24mod blank_space;
25mod current_comparison;
26mod current_pace;
27mod delta;
28mod detailed_timer;
29mod graph;
30mod pb_chance;
31mod possible_time_save;
32mod previous_segment;
33mod splits;
34mod sum_of_best;
35mod text;
36mod timer;
37mod title;
38mod total_playtime;
39
40#[cfg(all(windows, feature = "std"))]
41mod font_resolving;
42
43const PIXEL_SPACE_RATIO: f32 = 24.0 / 30.5;
48
49fn translate_size(v: u32) -> u32 {
50 (v as f32 * PIXEL_SPACE_RATIO + 0.5) as u32
51}
52
53#[derive(Debug, snafu::Snafu)]
55pub enum Error {
56 Xml {
58 source: XmlError,
60 },
61 ParseInt {
63 source: ParseIntError,
65 },
66 ParseBool,
68 ParseLayoutDirection,
70 ParseGradientType,
72 ParseAccuracy,
74 ParseDigitsFormat,
76 ParseTimingMethod,
78 ParseAlignment,
80 ParseColumnType,
82 ParseFont,
84 Empty,
86}
87
88impl From<XmlError> for Error {
89 fn from(source: XmlError) -> Self {
90 Self::Xml { source }
91 }
92}
93
94impl From<ParseIntError> for Error {
95 fn from(source: ParseIntError) -> Self {
96 Self::ParseInt { source }
97 }
98}
99
100pub type Result<T> = core::result::Result<T, Error>;
102
103enum GradientKind {
104 Transparent,
105 Plain,
106 Vertical,
107 Horizontal,
108}
109
110enum DeltaGradientKind {
111 Transparent,
112 Plain,
113 Vertical,
114 Horizontal,
115 PlainWithDeltaColor,
116 VerticalWithDeltaColor,
117 HorizontalWithDeltaColor,
118}
119
120enum ListGradientKind {
121 Same(GradientKind),
122 Alternating,
123}
124
125trait GradientType: Sized {
126 type Built;
127 fn default() -> Self;
128 fn parse(kind: &str) -> Result<Self>;
129 fn build(self, first: Color, second: Color) -> Self::Built;
130}
131
132impl GradientType for DeltaGradientKind {
133 type Built = DeltaGradient;
134
135 fn default() -> Self {
136 DeltaGradientKind::Transparent
137 }
138
139 fn parse(kind: &str) -> Result<Self> {
140 match kind {
141 "Plain" => Ok(DeltaGradientKind::Plain),
142 "PlainWithDeltaColor" => Ok(DeltaGradientKind::PlainWithDeltaColor),
143 "Vertical" => Ok(DeltaGradientKind::Vertical),
144 "VerticalWithDeltaColor" => Ok(DeltaGradientKind::VerticalWithDeltaColor),
145 "Horizontal" => Ok(DeltaGradientKind::Horizontal),
146 "HorizontalWithDeltaColor" => Ok(DeltaGradientKind::HorizontalWithDeltaColor),
147 _ => Err(Error::ParseGradientType),
148 }
149 }
150
151 fn build(self, first: Color, second: Color) -> Self::Built {
152 match self {
153 DeltaGradientKind::Transparent => Gradient::Transparent.into(),
154 DeltaGradientKind::Plain => if first.alpha == 0.0 {
155 Gradient::Transparent
156 } else {
157 Gradient::Plain(first)
158 }
159 .into(),
160 DeltaGradientKind::Vertical => Gradient::Vertical(first, second).into(),
161 DeltaGradientKind::Horizontal => Gradient::Horizontal(first, second).into(),
162 DeltaGradientKind::PlainWithDeltaColor => DeltaGradient::DeltaPlain,
163 DeltaGradientKind::VerticalWithDeltaColor => DeltaGradient::DeltaVertical,
164 DeltaGradientKind::HorizontalWithDeltaColor => DeltaGradient::DeltaHorizontal,
165 }
166 }
167}
168
169impl GradientType for GradientKind {
170 type Built = Gradient;
171 fn default() -> Self {
172 GradientKind::Transparent
173 }
174 fn parse(kind: &str) -> Result<Self> {
175 Ok(match kind {
176 "Plain" => GradientKind::Plain,
177 "Vertical" => GradientKind::Vertical,
178 "Horizontal" => GradientKind::Horizontal,
179 _ => return Err(Error::ParseGradientType),
180 })
181 }
182 fn build(self, first: Color, second: Color) -> Self::Built {
183 match self {
184 GradientKind::Transparent => Gradient::Transparent,
185 GradientKind::Plain => {
186 if first.alpha == 0.0 {
187 Gradient::Transparent
188 } else {
189 Gradient::Plain(first)
190 }
191 }
192 GradientKind::Horizontal => Gradient::Horizontal(first, second),
193 GradientKind::Vertical => Gradient::Vertical(first, second),
194 }
195 }
196}
197
198impl GradientType for ListGradientKind {
199 type Built = ListGradient;
200 fn default() -> Self {
201 ListGradientKind::Same(GradientKind::default())
202 }
203 fn parse(kind: &str) -> Result<Self> {
204 Ok(if kind == "Alternating" {
205 ListGradientKind::Alternating
206 } else {
207 ListGradientKind::Same(GradientKind::parse(kind)?)
208 })
209 }
210 fn build(self, first: Color, second: Color) -> Self::Built {
211 match self {
212 ListGradientKind::Alternating => ListGradient::Alternating(first, second),
213 ListGradientKind::Same(same) => ListGradient::Same(same.build(first, second)),
214 }
215 }
216}
217
218struct GradientBuilder<T: GradientType = GradientKind> {
219 tag_color1: &'static str,
220 tag_color2: &'static str,
221 tag_kind: &'static str,
222 kind: T,
223 first: Color,
224 second: Color,
225}
226
227impl GradientBuilder<GradientKind> {
228 fn new() -> Self {
229 Self::new_gradient_type()
230 }
231}
232
233impl<T: GradientType> GradientBuilder<T> {
234 fn new_gradient_type() -> Self {
235 Self::with_tags("BackgroundColor", "BackgroundColor2", "BackgroundGradient")
236 }
237
238 fn with_tags(
239 tag_color1: &'static str,
240 tag_color2: &'static str,
241 tag_kind: &'static str,
242 ) -> Self {
243 Self {
244 tag_color1,
245 tag_color2,
246 tag_kind,
247 kind: T::default(),
248 first: Color::transparent(),
249 second: Color::transparent(),
250 }
251 }
252
253 fn parse_background(&mut self, reader: &mut Reader<'_>, tag_name: &str) -> Result<bool> {
254 if tag_name == self.tag_color1 {
255 color(reader, |c| self.first = c)?;
256 } else if tag_name == self.tag_color2 {
257 color(reader, |c| self.second = c)?;
258 } else if tag_name == self.tag_kind {
259 text_as_escaped_string_err::<_, _, Error>(reader, |text| {
260 self.kind = T::parse(text)?;
261 Ok(())
262 })?;
263 } else {
264 return Ok(false);
265 }
266 Ok(true)
267 }
268
269 fn build(self) -> T::Built {
270 self.kind.build(self.first, self.second)
271 }
272}
273
274fn color<F>(reader: &mut Reader<'_>, func: F) -> Result<()>
275where
276 F: FnOnce(Color),
277{
278 text_as_escaped_string_err(reader, |text| {
279 let number = u32::from_str_radix(text, 16)?;
280 let [a, r, g, b] = number.to_be_bytes();
281 let mut color = Color::rgba8(r, g, b, a);
282 let [r, g, b, a] = color.to_array();
283
284 let lightness = (r + g + b) * (1.0 / 3.0);
294 color.alpha =
295 (1.0 - lightness) * (1.0 - powf(1.0 - a, 1.0 / 2.2)) + lightness * powf(a, 1.0 / 1.75);
296
297 func(color);
298 Ok(())
299 })
300}
301
302fn font<F>(reader: &mut Reader<'_>, font_buf: &mut Vec<MaybeUninit<u8>>, f: F) -> Result<()>
303where
304 F: FnOnce(Font),
305{
306 text_as_escaped_string_err(reader, |text| {
307 let rem = text.as_bytes().get(304..).ok_or(Error::ParseFont)?;
323
324 font_buf.resize(
325 base64_simd::STANDARD.estimated_decoded_length(rem.len()),
326 MaybeUninit::uninit(),
327 );
328
329 let decoded = base64_simd::STANDARD
330 .decode(rem, base64_simd::Out::from_uninit_slice(font_buf))
331 .map_err(|_| Error::ParseFont)?;
332
333 let mut cursor = decoded.get(1..).ok_or(Error::ParseFont)?.iter();
334
335 let mut len = 0;
337 for _ in 0..5 {
338 let byte = *cursor.next().ok_or(Error::ParseFont)?;
339 len = len << 7 | (byte & 0b0111_1111) as usize;
340 if byte <= 0b0111_1111 {
341 break;
342 }
343 }
344 let rem = cursor.as_slice();
345
346 let font_name = rem.get(..len).ok_or(Error::ParseFont)?;
347 let original_family_name = simdutf8::basic::from_utf8(font_name)
348 .map_err(|_| Error::ParseFont)?
349 .trim();
350 let mut family = original_family_name;
351
352 let mut style = FontStyle::Normal;
353 let mut weight = FontWeight::Normal;
354 let mut stretch = FontStretch::Normal;
355
356 for token in family.split_whitespace().rev() {
377 if token.eq_ignore_ascii_case("italic") {
380 style = FontStyle::Italic;
381 } else if token.eq_ignore_ascii_case("thin") || token.eq_ignore_ascii_case("hairline") {
382 weight = FontWeight::Thin;
383 } else if token.eq_ignore_ascii_case("extralight")
384 || token.eq_ignore_ascii_case("ultralight")
385 {
386 weight = FontWeight::ExtraLight;
387 } else if token.eq_ignore_ascii_case("light") {
388 weight = FontWeight::Light;
389 } else if token.eq_ignore_ascii_case("semilight")
390 || token.eq_ignore_ascii_case("demilight")
391 {
392 weight = FontWeight::SemiLight;
393 } else if token.eq_ignore_ascii_case("medium") {
394 weight = FontWeight::Medium;
395 } else if token.eq_ignore_ascii_case("semibold")
396 || token.eq_ignore_ascii_case("demibold")
397 {
398 weight = FontWeight::SemiBold;
399 } else if token.eq_ignore_ascii_case("bold") {
400 weight = FontWeight::Bold;
401 } else if token.eq_ignore_ascii_case("extrabold")
402 || token.eq_ignore_ascii_case("ultrabold")
403 {
404 weight = FontWeight::ExtraBold;
405 } else if token.eq_ignore_ascii_case("black") || token.eq_ignore_ascii_case("heavy") {
406 weight = FontWeight::Black;
407 } else if token.eq_ignore_ascii_case("extrablack")
408 || token.eq_ignore_ascii_case("ultrablack")
409 {
410 weight = FontWeight::ExtraBlack;
411 } else if token.eq_ignore_ascii_case("ultracondensed") {
412 stretch = FontStretch::UltraCondensed;
413 } else if token.eq_ignore_ascii_case("extracondensed") {
414 stretch = FontStretch::ExtraCondensed;
415 } else if token.eq_ignore_ascii_case("condensed") {
416 stretch = FontStretch::Condensed;
417 } else if token.eq_ignore_ascii_case("semicondensed") {
418 stretch = FontStretch::SemiCondensed;
419 } else if token.eq_ignore_ascii_case("semiexpanded") {
420 stretch = FontStretch::SemiExpanded;
421 } else if token.eq_ignore_ascii_case("expanded") {
422 stretch = FontStretch::Expanded;
423 } else if token.eq_ignore_ascii_case("extraexpanded") {
424 stretch = FontStretch::ExtraExpanded;
425 } else if token.eq_ignore_ascii_case("ultraexpanded") {
426 stretch = FontStretch::UltraExpanded;
427 } else if !token.eq_ignore_ascii_case("regular") {
428 family =
429 &family[..token.as_ptr() as usize - family.as_ptr() as usize + token.len()];
430 break;
431 }
432 }
433
434 let flags = *rem.get(len + 52).ok_or(Error::ParseFont)?;
440 let (bold_flag, italic_flag) = (flags & 1 != 0, flags & 2 != 0);
441
442 #[cfg(all(windows, feature = "std"))]
449 let family = if let Some(info) =
450 font_resolving::FontInfo::from_gdi(original_family_name, bold_flag, italic_flag)
451 {
452 weight = match info.weight {
453 i32::MIN..=149 => FontWeight::Thin,
454 150..=249 => FontWeight::ExtraLight,
455 250..=324 => FontWeight::Light,
456 325..=374 => FontWeight::SemiLight,
457 375..=449 => FontWeight::Normal,
458 450..=549 => FontWeight::Medium,
459 550..=649 => FontWeight::SemiBold,
460 650..=749 => FontWeight::Bold,
461 750..=849 => FontWeight::ExtraBold,
462 850..=924 => FontWeight::Black,
463 925.. => FontWeight::ExtraBlack,
464 };
465 style = if info.italic {
466 FontStyle::Italic
467 } else {
468 FontStyle::Normal
469 };
470 info.family
471 } else {
472 family.to_owned()
473 };
474
475 #[cfg(not(all(windows, feature = "std")))]
476 let family = family.to_owned();
477
478 if bold_flag && weight < FontWeight::Bold {
482 weight = FontWeight::Bold;
483 }
484
485 if italic_flag {
486 style = FontStyle::Italic;
487 }
488
489 f(Font {
490 family,
491 style,
492 weight,
493 stretch,
494 });
495 Ok(())
496 })
497}
498
499fn parse_bool<F>(reader: &mut Reader<'_>, f: F) -> Result<()>
500where
501 F: FnOnce(bool),
502{
503 text_as_escaped_string_err(reader, |t| match t {
504 "True" => {
505 f(true);
506 Ok(())
507 }
508 "False" => {
509 f(false);
510 Ok(())
511 }
512 _ => Err(Error::ParseBool),
513 })
514}
515
516fn comparison_override<F>(reader: &mut Reader<'_>, f: F) -> Result<()>
517where
518 F: FnOnce(Option<String>),
519{
520 text(reader, |t| {
521 f(if t == "Current Comparison" {
522 None
523 } else {
524 Some(t.into_owned())
525 })
526 })
527}
528
529fn timing_method_override<F>(reader: &mut Reader<'_>, f: F) -> Result<()>
530where
531 F: FnOnce(Option<TimingMethod>),
532{
533 text_as_escaped_string_err(reader, |t| {
534 f(match t {
535 "Current Timing Method" => None,
536 "Real Time" => Some(TimingMethod::RealTime),
537 "Game Time" => Some(TimingMethod::GameTime),
538 _ => return Err(Error::ParseTimingMethod),
539 });
540 Ok(())
541 })
542}
543
544fn accuracy<F>(reader: &mut Reader<'_>, f: F) -> Result<()>
545where
546 F: FnOnce(Accuracy),
547{
548 text_as_escaped_string_err(reader, |t| {
549 f(match t {
550 "Tenths" => Accuracy::Tenths,
551 "Seconds" => Accuracy::Seconds,
552 "Hundredths" => Accuracy::Hundredths,
553 _ => return Err(Error::ParseAccuracy),
554 });
555 Ok(())
556 })
557}
558
559fn timer_format<F>(reader: &mut Reader<'_>, f: F) -> Result<()>
560where
561 F: FnOnce(DigitsFormat, Accuracy),
562{
563 text_as_escaped_string_err(reader, |t| {
564 let (digits_format, accuracy) = t.split_once('.').unwrap_or((t, ""));
565 let digits_format = match digits_format {
566 "1" => DigitsFormat::SingleDigitSeconds,
567 "00:01" => DigitsFormat::DoubleDigitMinutes,
568 "0:00:01" => DigitsFormat::SingleDigitHours,
569 "00:00:01" => DigitsFormat::DoubleDigitHours,
570 _ => return Err(Error::ParseDigitsFormat),
571 };
572 let accuracy = match accuracy {
573 "23" => Accuracy::Hundredths,
574 "2" => Accuracy::Tenths,
575 "" => Accuracy::Seconds,
576 _ => return Err(Error::ParseAccuracy),
577 };
578 f(digits_format, accuracy);
579 Ok(())
580 })
581}
582
583fn component<F>(reader: &mut Reader<'_>, f: F) -> Result<()>
584where
585 F: FnOnce(Component),
586{
587 let mut component = None;
588
589 parse_children(reader, |reader, tag, _| {
590 match tag.name() {
591 "Path" => text_as_escaped_string_err(reader, |text| {
592 component = Some(match text {
593 "LiveSplit.BlankSpace.dll" => blank_space::Component::new().into(),
594 "LiveSplit.CurrentComparison.dll" => {
595 current_comparison::Component::new().into()
596 }
597 "LiveSplit.RunPrediction.dll" => current_pace::Component::new().into(),
598 "LiveSplit.Delta.dll" => delta::Component::new().into(),
599 "LiveSplit.DetailedTimer.dll" => {
600 Box::new(detailed_timer::Component::new()).into()
601 }
602 "LiveSplit.Graph.dll" => graph::Component::new().into(),
603 "PBChance.dll" => pb_chance::Component::new().into(),
604 "LiveSplit.PossibleTimeSave.dll" => possible_time_save::Component::new().into(),
605 "LiveSplit.PreviousSegment.dll" => previous_segment::Component::new().into(),
606 "" => separator::Component::new().into(),
607 "LiveSplit.Splits.dll" | "LiveSplit.Subsplits.dll" => {
608 splits::Component::new().into()
609 }
610 "LiveSplit.SumOfBest.dll" => sum_of_best::Component::new().into(),
611 "LiveSplit.Text.dll" => text::Component::new().into(),
612 "LiveSplit.Timer.dll" => timer::Component::new().into(),
613 "LiveSplit.Title.dll" => title::Component::new().into(),
614 "LiveSplit.TotalPlaytime.dll" => total_playtime::Component::new().into(),
615 _ => return Ok(()),
616 });
617 Ok(())
618 }),
619 "Settings" => {
620 if let Some(component) = &mut component {
623 match component {
624 Component::BlankSpace(c) => blank_space::settings(reader, c),
625 Component::CurrentComparison(c) => current_comparison::settings(reader, c),
626 Component::CurrentPace(c) => current_pace::settings(reader, c),
627 Component::Delta(c) => delta::settings(reader, c),
628 Component::DetailedTimer(c) => detailed_timer::settings(reader, c),
629 Component::Graph(c) => graph::settings(reader, c),
630 Component::PbChance(c) => pb_chance::settings(reader, c),
631 Component::PossibleTimeSave(c) => possible_time_save::settings(reader, c),
632 Component::PreviousSegment(c) => previous_segment::settings(reader, c),
633 Component::SegmentTime(_) => end_tag(reader),
634 Component::Separator(_) => end_tag(reader),
635 Component::Splits(c) => splits::settings(reader, c),
636 Component::SumOfBest(c) => sum_of_best::settings(reader, c),
637 Component::Text(c) => text::settings(reader, c),
638 Component::Timer(c) => timer::settings(reader, c),
639 Component::Title(c) => title::settings(reader, c),
640 Component::TotalPlaytime(c) => total_playtime::settings(reader, c),
641 }
642 } else {
643 end_tag(reader)
644 }
645 }
646 _ => end_tag(reader),
647 }
648 })?;
649
650 if let Some(component) = component {
651 f(component);
652 }
653
654 Ok(())
655}
656
657fn parse_general_settings(layout: &mut Layout, reader: &mut Reader<'_>) -> Result<()> {
658 let settings = layout.general_settings_mut();
659 let mut background_builder = GradientBuilder::new();
660
661 let mut font_buf = Vec::new();
662
663 parse_children(reader, |reader, tag, _| match tag.name() {
664 "TextColor" => color(reader, |color| {
665 settings.text_color = color;
666 }),
667 "BackgroundColor" => color(reader, |color| {
668 background_builder.first = color;
669 }),
670 "BackgroundColor2" => color(reader, |color| {
671 background_builder.second = color;
672 }),
673 "ThinSeparatorsColor" => color(reader, |color| {
674 settings.thin_separators_color = color;
675 }),
676 "SeparatorsColor" => color(reader, |color| {
677 settings.separators_color = color;
678 }),
679 "PersonalBestColor" => color(reader, |color| {
680 settings.personal_best_color = color;
681 }),
682 "AheadGainingTimeColor" => color(reader, |color| {
683 settings.ahead_gaining_time_color = color;
684 }),
685 "AheadLosingTimeColor" => color(reader, |color| {
686 settings.ahead_losing_time_color = color;
687 }),
688 "BehindGainingTimeColor" => color(reader, |color| {
689 settings.behind_gaining_time_color = color;
690 }),
691 "BehindLosingTimeColor" => color(reader, |color| {
692 settings.behind_losing_time_color = color;
693 }),
694 "BestSegmentColor" => color(reader, |color| {
695 settings.best_segment_color = color;
696 }),
697 "NotRunningColor" => color(reader, |color| {
698 settings.not_running_color = color;
699 }),
700 "PausedColor" => color(reader, |color| {
701 settings.paused_color = color;
702 }),
703 "TimerFont" => font(reader, &mut font_buf, |font| {
704 if font.family != "Calibri" && font.family != "Century Gothic" {
705 settings.timer_font = Some(font);
706 }
707 }),
708 "TimesFont" => font(reader, &mut font_buf, |font| {
709 if font.family != "Segoe UI" {
710 settings.times_font = Some(font);
711 }
712 }),
713 "TextFont" => font(reader, &mut font_buf, |font| {
714 if font.family != "Segoe UI" {
715 settings.text_font = Some(font);
716 }
717 }),
718 "BackgroundType" => text_as_escaped_string_err(reader, |text| {
719 background_builder.kind = match text {
720 "SolidColor" => GradientKind::Plain,
721 "VerticalGradient" => GradientKind::Vertical,
722 "HorizontalGradient" => GradientKind::Horizontal,
723 "Image" => {
724 background_builder.first = Color::black();
725 background_builder.second = Color::black();
726 GradientKind::Plain
727 }
728 _ => return Err(Error::ParseGradientType),
729 };
730 Ok(())
731 }),
732 _ => end_tag(reader),
733 })?;
734
735 settings.background = background_builder.build();
736
737 Ok(())
738}
739
740pub fn parse(source: &str) -> Result<Layout> {
744 let reader = &mut Reader::new(source);
745
746 let mut layout = Layout::new();
747
748 parse_base(reader, "Layout", |reader, _| {
749 parse_children(reader, |reader, tag, _| match tag.name() {
750 "Mode" => text_as_escaped_string_err(reader, |text| {
751 layout.general_settings_mut().direction = match text {
752 "Vertical" => LayoutDirection::Vertical,
753 "Horizontal" => LayoutDirection::Horizontal,
754 _ => return Err(Error::ParseLayoutDirection),
755 };
756 Ok(())
757 }),
758 "Settings" => parse_general_settings(&mut layout, reader),
759 "Components" => parse_children(reader, |reader, _, _| {
760 component(reader, |c| {
761 layout.push(c);
762 })
763 }),
764 _ => end_tag(reader),
765 })
766 })?;
767
768 if layout.components.is_empty() {
769 Err(Error::Empty)
770 } else {
771 Ok(layout)
772 }
773}