livesplit_core/layout/parser/
mod.rs

1//! Provides the parser for layout files of the original LiveSplit.
2
3use 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
43// One single row component is:
44// 1.0 units high in component space.
45// 24 pixels high in LiveSplit One's pixel coordinate space.
46// ~30.5 pixels high in the original LiveSplit's pixel coordinate space.
47const 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/// The Error type for parsing layout files of the original LiveSplit.
54#[derive(Debug, snafu::Snafu)]
55pub enum Error {
56    /// The underlying XML format couldn't be parsed.
57    Xml {
58        /// The underlying error.
59        source: XmlError,
60    },
61    /// Failed to parse an integer.
62    ParseInt {
63        /// The underlying error.
64        source: ParseIntError,
65    },
66    /// Failed to parse a boolean.
67    ParseBool,
68    /// Failed to parse the layout direction.
69    ParseLayoutDirection,
70    /// Failed to parse a gradient type.
71    ParseGradientType,
72    /// Failed to parse an accuracy.
73    ParseAccuracy,
74    /// Failed to parse a digits format.
75    ParseDigitsFormat,
76    /// Failed to parse a timing method.
77    ParseTimingMethod,
78    /// Failed to parse an alignment.
79    ParseAlignment,
80    /// Failed to parse a column type.
81    ParseColumnType,
82    /// Failed to parse a font.
83    ParseFont,
84    /// Parsed an empty layout, which is considered an invalid layout.
85    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
100/// The Result type for parsing layout files of the original LiveSplit.
101pub 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        // Adjust alpha based on the lightness of the color. The formula is
285        // based on two sRGB curves measured for white on top of a black
286        // background and for black on top of a white background. We interpolate
287        // between the two curves based on the lightness of the color. The
288        // problem is that we only have the foreground color, so based on the
289        // actual background color, this may be wrong. Therefore this is only a
290        // heuristic. We often have white on dark grey, instead of white on
291        // black. Because of that, we use 1.75 as the exponent denominator for
292        // the white on black case instead of the usual 2.2 for sRGB.
293        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        // The format for this is documented here:
308        // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/75b9fe09-be15-475f-85b8-ae7b7558cfe5
309        //
310        // The structure follows roughly:
311        //
312        // class System.Drawing.Font {
313        //     String Name;
314        //     float Size;
315        //     System.Drawing.FontStyle Style;
316        //     System.Drawing.GraphicsUnit Unit;
317        // }
318        //
319        // The full definition can be found here:
320        // https://referencesource.microsoft.com/#System.Drawing/commonui/System/Drawing/Advanced/Font.cs,130
321
322        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        // Strings are encoded as varint for the length + the UTF-8 string data.
336        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        // The original LiveSplit is based on Windows Forms, which is just a
357        // .NET wrapper around GDI+. It's a pretty old API from before
358        // DirectWrite existed, and fonts used to be very different back then.
359        // This is why GDI uses a very different identifier for fonts than
360        // modern APIs. Since all the modern APIs take a font family, we somehow
361        // need to convert the font identifier from the original LiveSplit into
362        // a font family. The problem is that we may not necessarily even have
363        // the font, nor be on a platform where we could even query for any
364        // fonts or get enough metadata about them, such as in the browser. So
365        // for those cases, we implement a very simple, though also really lossy
366        // algorithm that simply splits away common tokens at the end that refer
367        // to the subfamily / styling of the font. In most cases this should
368        // yield the font family that we are looking for and the additional
369        // styling information. Another problem with this approach is that GDI
370        // limits its font identifiers to 32 characters, so the tokens that we
371        // may want to split off, may themselves already be cut off, causing us
372        // to not recognize them. An example of this is "Bahnschrift SemiLight
373        // SemiConde" where the end should say "SemiCondensed" but doesn't due
374        // to the character limit.
375
376        for token in family.split_whitespace().rev() {
377            // FontWeight and FontStretch both have the variant "normal"
378            // which is the default and can thus be ignored.
379            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        // Later on we find the style and weight as bitflags of System.Drawing.FontStyle.
435        // 1 -> bold
436        // 2 -> italic
437        // 4 -> underline
438        // 8 -> strikeout
439        let flags = *rem.get(len + 52).ok_or(Error::ParseFont)?;
440        let (bold_flag, italic_flag) = (flags & 1 != 0, flags & 2 != 0);
441
442        // If we are on Windows, we can however directly use GDI to get the
443        // proper family name out of the font. The problem is that GDI does not
444        // give us access to either the path of the font or its data. However we can
445        // receive the byte representation of individual tables we query for, so
446        // we can get the family name from the `name` table.
447
448        #[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        // The font might not exist on the user's system, so we still prefer to
479        // apply these flags.
480
481        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                // Assumption: Settings always has to come after the Path.
621                // Otherwise we need to cache the settings and load them later.
622                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
740/// Attempts to parse a layout file of the original LiveSplit. They are only
741/// parsed on a best effort basis, so if something isn't supported by
742/// livesplit-core, then it will be parsed without that option.
743pub 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}