Repository: LiveSplit/livesplit-core
Pull request #155 (2018-12-27 23:01:15)

Implement Parsing the original LiveSplit's layout files
This is the full implementation of the work started in https://github.com/LiveSplit/livesplit-core/pull/103


Patch:
diff --git a/capi/bind_gen/src/emscripten.rs b/capi/bind_gen/src/emscripten.rs
index 745c9c3..456d09b 100644
--- a/capi/bind_gen/src/emscripten.rs
+++ b/capi/bind_gen/src/emscripten.rs
@@ -620,6 +620,68 @@ const liveSplitCoreNative = {};"#
             }
         }
 
+        if class_name == "Layout" {
+            if type_script {
+                write!(
+                    writer,
+                    "{}",
+                    r#"
+    static parseOriginalLivesplitArray(data: Int8Array): Layout | null {
+        const buf = emscriptenModule._malloc(data.length);
+        try {
+            emscriptenModule.writeArrayToMemory(data, buf);
+            return Layout.parseOriginalLivesplit(buf, data.length);
+        } finally {
+            emscriptenModule._free(buf);
+        }
+    }
+    static parseOriginalLivesplitString(text: string): Layout | null {
+        const len = (text.length << 2) + 1;
+        const buf = emscriptenModule._malloc(len);
+        try {
+            const actualLen = emscriptenModule.stringToUTF8(text, buf, len);
+            return Layout.parseOriginalLivesplit(buf, actualLen);
+        } finally {
+            emscriptenModule._free(buf);
+        }
+    }"#
+                )?;
+            } else {
+                write!(
+                    writer,
+                    "{}",
+                    r#"
+    /**
+     * @param {Int8Array} data
+     * @return {Layout | null}
+     */
+    static parseOriginalLivesplitArray(data) {
+        const buf = emscriptenModule._malloc(data.length);
+        try {
+            emscriptenModule.writeArrayToMemory(data, buf);
+            return Layout.parseOriginalLivesplit(buf, data.length);
+        } finally {
+            emscriptenModule._free(buf);
+        }
+    }
+    /**
+     * @param {string} text
+     * @return {Layout | null}
+     */
+    static parseOriginalLivesplitString(text) {
+        const len = (text.length << 2) + 1;
+        const buf = emscriptenModule._malloc(len);
+        try {
+            const actualLen = emscriptenModule.stringToUTF8(text, buf, len);
+            return Layout.parseOriginalLivesplit(buf, actualLen);
+        } finally {
+            emscriptenModule._free(buf);
+        }
+    }"#
+                )?;
+            }
+        }
+
         if class_name == "Run" {
             if type_script {
                 write!(
diff --git a/capi/bind_gen/src/node.rs b/capi/bind_gen/src/node.rs
index 5c2158c..ffbef57 100644
--- a/capi/bind_gen/src/node.rs
+++ b/capi/bind_gen/src/node.rs
@@ -672,6 +672,50 @@ const liveSplitCoreNative = ffi.Library('livesplit_core', {"#
     static parseString(text, path, loadFiles) {
         const data = new Buffer(text);
         return Run.parse(data, data.byteLength, path, loadFiles);
+    }"#
+                )?;
+            }
+        } else if class_name == "Layout" {
+            if type_script {
+                write!(
+                    writer,
+                    "{}",
+                    r#"
+    static parseOriginalLivesplitArray(data: Int8Array): Layout | null {
+        let buf = Buffer.from(data.buffer);
+        if (data.byteLength !== data.buffer.byteLength) {
+            buf = buf.slice(data.byteOffset, data.byteOffset + data.byteLength);
+        }
+        return Layout.parseOriginalLivesplit(buf, buf.byteLength);
+    }
+    static parseOriginalLivesplitString(text: string): Layout | null {
+        const data = new Buffer(text);
+        return Layout.parseOriginalLivesplit(data, data.byteLength);
+    }"#
+                )?;
+            } else {
+                write!(
+                    writer,
+                    "{}",
+                    r#"
+    /**
+     * @param {Int8Array} data
+     * @return {Layout | null}
+     */
+    static parseOriginalLivesplitArray(data) {
+        let buf = Buffer.from(data.buffer);
+        if (data.byteLength !== data.buffer.byteLength) {
+            buf = buf.slice(data.byteOffset, data.byteOffset + data.byteLength);
+        }
+        return Layout.parseOriginalLivesplit(buf, buf.byteLength);
+    }
+    /**
+     * @param {string} text
+     * @return {Layout | null}
+     */
+    static parseOriginalLivesplitString(text) {
+        const data = new Buffer(text);
+        return Layout.parseOriginalLivesplit(data, data.byteLength);
     }"#
                 )?;
             }
diff --git a/capi/bind_gen/src/wasm.rs b/capi/bind_gen/src/wasm.rs
index 9622a60..288ccd8 100644
--- a/capi/bind_gen/src/wasm.rs
+++ b/capi/bind_gen/src/wasm.rs
@@ -1053,6 +1053,52 @@ function dealloc(slice) {
         const result = Run.parse(slice.ptr, slice.len, path, loadFiles);
         dealloc(slice);
         return result;
+    }"#
+                )?;
+            }
+        } else if class_name == "Layout" {
+            if type_script {
+                write!(
+                    writer,
+                    "{}",
+                    r#"
+    static parseOriginalLivesplitArray(data: Int8Array): Layout | null {
+        const slice = allocInt8Array(data);
+        const result = Layout.parseOriginalLivesplit(slice.ptr, slice.len);
+        dealloc(slice);
+        return result;
+    }
+    static parseOriginalLivesplitString(text: string): Layout | null {
+        const slice = allocString(text);
+        const result = Layout.parseOriginalLivesplit(slice.ptr, slice.len);
+        dealloc(slice);
+        return result;
+    }"#
+                )?;
+            } else {
+                write!(
+                    writer,
+                    "{}",
+                    r#"
+    /**
+     * @param {Int8Array} data
+     * @return {Layout | null}
+     */
+    static parseOriginalLivesplitArray(data) {
+        const slice = allocInt8Array(data);
+        const result = Layout.parseOriginalLivesplit(slice.ptr, slice.len);
+        dealloc(slice);
+        return result;
+    }
+    /**
+     * @param {string} text
+     * @return {Layout | null}
+     */
+    static parseOriginalLivesplitString(text) {
+        const slice = allocString(text);
+        const result = Layout.parseOriginalLivesplit(slice.ptr, slice.len);
+        dealloc(slice);
+        return result;
     }"#
                 )?;
             }
diff --git a/capi/src/layout.rs b/capi/src/layout.rs
index d28d6bf..16eafdb 100644
--- a/capi/src/layout.rs
+++ b/capi/src/layout.rs
@@ -3,9 +3,10 @@
 
 use super::{output_vec, str, Json};
 use crate::component::OwnedComponent;
-use livesplit_core::layout::LayoutSettings;
+use livesplit_core::layout::{parser, LayoutSettings};
 use livesplit_core::{Layout, Timer};
 use std::io::Cursor;
+use std::slice;
 
 /// type
 pub type OwnedLayout = Box<Layout>;
@@ -51,6 +52,21 @@ pub unsafe extern "C" fn Layout_parse_json(settings: Json) -> NullableOwnedLayou
     }
 }
 
+/// Parses a layout saved by the original LiveSplit. This is lossy, as not
+/// everything can be converted completely. <NULL> is returned if it couldn't be
+/// parsed at all.
+#[no_mangle]
+pub unsafe extern "C" fn Layout_parse_original_livesplit(
+    data: *const u8,
+    length: usize,
+) -> NullableOwnedLayout {
+    if let Ok(parsed) = parser::parse(Cursor::new(slice::from_raw_parts(data, length))) {
+        Some(Box::new(parsed))
+    } else {
+        None
+    }
+}
+
 /// Calculates the layout's state based on the timer provided and encodes it as
 /// JSON. You can use this to visualize all of the components of a layout.
 #[no_mangle]
diff --git a/hotkey/Cargo.toml b/hotkey/Cargo.toml
index 3b055db..bedead9 100644
--- a/hotkey/Cargo.toml
+++ b/hotkey/Cargo.toml
@@ -15,7 +15,7 @@ winapi = { version = "0.3.2", features = [
     "processthreadsapi",
     "winuser"
 ] }
-parking_lot = "0.6.4"
+parking_lot = "0.7.0"
 
 [target.'cfg(target_os = "linux")'.dependencies]
 x11-dl = "2.18.3"
@@ -24,7 +24,7 @@ promising-future = "0.2.4"
 
 [target.'cfg(target_os = "emscripten")'.dependencies]
 stdweb = "0.3.0"
-parking_lot = "0.6.4"
+parking_lot = "0.7.0"
 
 [dependencies]
 quick-error = "1.2.2"
diff --git a/src/analysis/state_helper.rs b/src/analysis/state_helper.rs
index fb1d008..baf77bd 100644
--- a/src/analysis/state_helper.rs
+++ b/src/analysis/state_helper.rs
@@ -227,7 +227,7 @@ pub fn check_live_delta(
     method: TimingMethod,
 ) -> Option<TimeSpan> {
     if timer.current_phase() == TimerPhase::Running || timer.current_phase() == TimerPhase::Paused {
-        let use_best_segment = true; // TODO: Make this a parameter
+        let use_best_segment = true; // FIXME: Make this a parameter
         let current_split = timer
             .current_split()
             .unwrap()
@@ -277,7 +277,7 @@ pub fn split_color(
     comparison: &str,
     method: TimingMethod,
 ) -> SemanticColor {
-    let use_best_segment = true; // TODO: Make this a parameter
+    let use_best_segment = true; // FIXME: Make this a parameter
 
     if show_best_segments && use_best_segment && check_best_segment(timer, segment_index, method) {
         SemanticColor::BestSegment
diff --git a/src/comparison/average_segments.rs b/src/comparison/average_segments.rs
index 2c3c1f1..38d3e0b 100644
--- a/src/comparison/average_segments.rs
+++ b/src/comparison/average_segments.rs
@@ -25,13 +25,11 @@ const WEIGHT: f64 = 0.75;
 fn generate(segments: &mut [Segment], method: TimingMethod) {
     let mut accumulated = Some(TimeSpan::zero());
 
-    // TODO: This may actually be possible to be fixed with a window like
+    // FIXME: This may actually be possible to be fixed with a window like
     // iterator.
     #[allow(clippy::needless_range_loop)]
     for i in 0..segments.len() {
-        // TODO: Borrowcheck. if accumulated.is_some() is only necessary because
-        // we can't assign to the outer variable otherwise.
-        if accumulated.is_some() {
+        if let Some(accumulated_val) = &mut accumulated {
             let (mut total_weights, mut total_time) = (0.0, 0.0);
             let mut current_weight = 1.0;
 
@@ -54,8 +52,7 @@ fn generate(segments: &mut [Segment], method: TimingMethod) {
             if total_weights == 0.0 {
                 accumulated = None;
             } else {
-                accumulated =
-                    Some(accumulated.unwrap() + TimeSpan::from_seconds(total_time / total_weights));
+                *accumulated_val += TimeSpan::from_seconds(total_time / total_weights);
             }
         }
         segments[i].comparison_mut(NAME)[method] = accumulated;
diff --git a/src/comparison/median_segments.rs b/src/comparison/median_segments.rs
index a74aa78..3f49f82 100644
--- a/src/comparison/median_segments.rs
+++ b/src/comparison/median_segments.rs
@@ -25,13 +25,11 @@ const WEIGHT: f64 = 0.75;
 fn generate(segments: &mut [Segment], medians: &mut Vec<(f64, f64)>, method: TimingMethod) {
     let mut accumulated = Some(TimeSpan::zero());
 
-    // TODO: This may actually be possible to be fixed with a window like
+    // FIXME: This may actually be possible to be fixed with a window like
     // iterator.
     #[allow(clippy::needless_range_loop)]
     for i in 0..segments.len() {
-        // TODO: Borrowcheck. if accumulated.is_some() is only necessary because
-        // we can't assign to the outer variable otherwise.
-        if accumulated.is_some() {
+        if let Some(accumulated_val) = &mut accumulated {
             medians.clear();
 
             let mut current_weight = 1.0;
@@ -68,7 +66,7 @@ fn generate(segments: &mut [Segment], medians: &mut Vec<(f64, f64)>, method: Tim
                     Ok(index) => medians[index].1,
                     Err(right_index) => medians[right_index].1,
                 };
-                accumulated = Some(accumulated.unwrap() + TimeSpan::from_seconds(segment_time));
+                *accumulated_val += TimeSpan::from_seconds(segment_time);
             }
         }
         segments[i].comparison_mut(NAME)[method] = accumulated;
diff --git a/src/component/graph.rs b/src/component/graph.rs
index af2e2cb..d3bec91 100644
--- a/src/component/graph.rs
+++ b/src/component/graph.rs
@@ -585,21 +585,21 @@ impl Component {
         draw_info: &DrawInfo,
     ) -> (f32, f32, f32) {
         let mut graph_edge = 0.0;
-        let graph_height = HEIGHT / 2.0; // TODO: Make const
+        const GRAPH_HEIGHT: f32 = HEIGHT / 2.0;
         let middle = if total_delta != TimeSpan::zero() {
             graph_edge = GRAPH_EDGE_VALUE
                 / (-total_delta.total_milliseconds() as f32 + 2.0 * GRAPH_EDGE_VALUE)
-                * (graph_height * 2.0 - GRAPH_EDGE_MIN * 2.0);
+                * (GRAPH_HEIGHT * 2.0 - GRAPH_EDGE_MIN * 2.0);
             graph_edge += GRAPH_EDGE_MIN;
             (-(draw_info.max_delta.total_milliseconds() as f32
                 / total_delta.total_milliseconds() as f32))
-                * (graph_height - graph_edge)
+                * (GRAPH_HEIGHT - graph_edge)
                 * 2.0
                 + graph_edge
         } else {
-            graph_height
+            GRAPH_HEIGHT
         };
-        (graph_edge, graph_height, middle)
+        (graph_edge, GRAPH_HEIGHT, middle)
     }
 
     fn calculate_final_split(&self, timer: &Timer, draw_info: &mut DrawInfo) {
@@ -651,7 +651,7 @@ impl Component {
                 let timing_method = timer.current_timing_method();
                 let mut best_segment =
                     analysis::check_live_delta(timer, true, comparison, timing_method);
-                // TODO: Try if let instead of checking current phase up there,
+                // FIXME: Try if let instead of checking current phase up there,
                 // so we can skip this unwrap
                 let current_split =
                     timer.current_split().unwrap().comparison(comparison)[timing_method];
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index c1b36ec..4cc35df 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -9,6 +9,7 @@ pub mod editor;
 mod general_settings;
 mod layout_settings;
 mod layout_state;
+pub mod parser;
 
 pub use self::component::Component;
 pub use self::component_settings::ComponentSettings;
diff --git a/src/layout/parser.rs b/src/layout/parser.rs
new file mode 100644
index 0000000..ae017f6
--- /dev/null
+++ b/src/layout/parser.rs
@@ -0,0 +1,533 @@
+//! Provides the parser for layout files of the original LiveSplit.
+
+use super::{Component, Layout};
+use crate::{
+    component::separator,
+    settings::{Alignment, Color, Gradient, ListGradient},
+    timing::{
+        formatter::{Accuracy, DigitsFormat},
+        TimingMethod,
+    },
+    xml_util::{
+        end_tag, parse_base, parse_children, text, text_as_escaped_bytes_err, text_err,
+        text_parsed, Error as XmlError, Tag,
+    },
+};
+use quick_xml::Reader;
+use std::io::BufRead;
+
+mod blank_space;
+mod current_comparison;
+mod current_pace;
+mod delta;
+mod detailed_timer;
+mod graph;
+mod possible_time_save;
+mod previous_segment;
+mod splits;
+mod sum_of_best;
+mod text;
+mod timer;
+mod title;
+mod total_playtime;
+
+quick_error! {
+    /// The Error type for parsing layout files of the original LiveSplit.
+    #[derive(Debug)]
+    pub enum Error {
+        /// The underlying XML format couldn't be parsed.
+        Xml(err: XmlError) {
+            from()
+        }
+        /// Failed to decode a string slice as UTF-8.
+        Utf8Str(err: std::str::Utf8Error) {
+            from()
+        }
+        /// Failed to decode a string as UTF-8.
+        Utf8String(err: std::string::FromUtf8Error) {
+            from()
+        }
+        /// Failed to parse an integer.
+        Int(err: std::num::ParseIntError) {
+            from()
+        }
+        /// Failed to parse a boolean.
+        Bool {}
+        /// Failed to parse a gradient type.
+        GradientType {}
+        /// Failed to parse an accuracy.
+        Accuracy {}
+        /// Failed to parse a digits format.
+        DigitsFormat {}
+        /// Failed to parse a timing method.
+        TimingMethod {}
+        /// Failed to parse an alignment.
+        Alignment {}
+        /// Failed to parse a column type.
+        ColumnType {}
+        /// Parsed an empty layout, which is considered an invalid layout.
+        Empty {}
+    }
+}
+
+/// The Result type for parsing layout files of the original LiveSplit.
+pub type Result<T> = std::result::Result<T, Error>;
+
+enum GradientKind {
+    Transparent,
+    Plain,
+    Vertical,
+    Horizontal,
+}
+
+enum ListGradientKind {
+    Same(GradientKind),
+    Alternating,
+}
+
+trait GradientType: Sized {
+    type Built;
+    fn default() -> Self;
+    fn parse(kind: &[u8]) -> Result<Self>;
+    fn build(self, first: Color, second: Color) -> Self::Built;
+}
+
+impl GradientType for GradientKind {
+    type Built = Gradient;
+    fn default() -> Self {
+        GradientKind::Transparent
+    }
+    fn parse(kind: &[u8]) -> Result<Self> {
+        Ok(match kind {
+            b"Plain" => GradientKind::Plain,
+            b"Vertical" => GradientKind::Vertical,
+            b"Horizontal" => GradientKind::Horizontal,
+            _ => return Err(Error::GradientType),
+        })
+    }
+    fn build(self, first: Color, second: Color) -> Self::Built {
+        match self {
+            GradientKind::Transparent => Gradient::Transparent,
+            GradientKind::Plain => {
+                if first.rgba.alpha == 0.0 {
+                    Gradient::Transparent
+                } else {
+                    Gradient::Plain(first)
+                }
+            }
+            GradientKind::Horizontal => Gradient::Horizontal(first, second),
+            GradientKind::Vertical => Gradient::Vertical(first, second),
+        }
+    }
+}
+
+impl GradientType for ListGradientKind {
+    type Built = ListGradient;
+    fn default() -> Self {
+        ListGradientKind::Same(GradientKind::default())
+    }
+    fn parse(kind: &[u8]) -> Result<Self> {
+        Ok(if kind == b"Alternating" {
+            ListGradientKind::Alternating
+        } else {
+            ListGradientKind::Same(GradientKind::parse(kind)?)
+        })
+    }
+    fn build(self, first: Color, second: Color) -> Self::Built {
+        match self {
+            ListGradientKind::Alternating => ListGradient::Alternating(first, second),
+            ListGradientKind::Same(same) => ListGradient::Same(same.build(first, second)),
+        }
+    }
+}
+
+struct GradientBuilder<T: GradientType = GradientKind> {
+    tag_color1: &'static [u8],
+    tag_color2: &'static [u8],
+    tag_kind: &'static [u8],
+    kind: T,
+    first: Color,
+    second: Color,
+}
+
+impl GradientBuilder<GradientKind> {
+    fn new() -> Self {
+        Self::new_gradient_type()
+    }
+}
+
+impl<T: GradientType> GradientBuilder<T> {
+    fn new_gradient_type() -> Self {
+        Self::with_tags(
+            b"BackgroundColor",
+            b"BackgroundColor2",
+            b"BackgroundGradient",
+        )
+    }
+
+    fn with_tags(
+        tag_color1: &'static [u8],
+        tag_color2: &'static [u8],
+        tag_kind: &'static [u8],
+    ) -> Self {
+        Self {
+            tag_color1,
+            tag_color2,
+            tag_kind,
+            kind: T::default(),
+            first: Color::transparent(),
+            second: Color::transparent(),
+        }
+    }
+
+    fn parse_background<'a, R>(
+        &mut self,
+        reader: &mut Reader<R>,
+        tag: Tag<'a>,
+    ) -> Result<Option<Tag<'a>>>
+    where
+        R: BufRead,
+    {
+        if tag.name() == self.tag_color1 {
+            color(reader, tag.into_buf(), |c| self.first = c)?;
+        } else if tag.name() == self.tag_color2 {
+            color(reader, tag.into_buf(), |c| self.second = c)?;
+        } else if tag.name() == self.tag_kind {
+            text_as_escaped_bytes_err::<_, _, _, Error>(reader, tag.into_buf(), |text| {
+                self.kind = T::parse(&text)?;
+                Ok(())
+            })?;
+        } else {
+            return Ok(Some(tag));
+        }
+        Ok(None)
+    }
+
+    fn build(self) -> T::Built {
+        self.kind.build(self.first, self.second)
+    }
+}
+
+fn color<R, F>(reader: &mut Reader<R>, buf: &mut Vec<u8>, f: F) -> Result<()>
+where
+    R: BufRead,
+    F: FnOnce(Color),
+{
+    text_err(reader, buf, |text| {
+        let n = u32::from_str_radix(&text, 16)?;
+        let b = (n & 0xFF) as u8;
+        let g = ((n >> 8) & 0xFF) as u8;
+        let r = ((n >> 16) & 0xFF) as u8;
+        let a = ((n >> 24) & 0xFF) as u8;
+        let mut color = Color::from([r, g, b, a]);
+        let (r, g, b, a) = color.rgba.into_components();
+
+        // Adjust alpha based on the lightness of the color. The formula is
+        // based on two sRGB curves measured for white on top of a black
+        // background and for black on top of a white background. We interpolate
+        // between the two curves based on the lightness of the color. The
+        // problem is that we only have the foreground color, so based on the
+        // actual background color, this may be wrong. Therefore this is only a
+        // heuristic. We often have white on dark grey, instead of white on
+        // black. Because of that, we use 1.75 as the exponent denominator for
+        // the white on black case instead of the usual 2.2 for sRGB.
+        let lightness = (r + g + b) / 3.0;
+        color.rgba.alpha =
+            (1.0 - lightness) * (1.0 - (1.0 - a).powf(1.0 / 2.2)) + lightness * a.powf(1.0 / 1.75);
+
+        f(color);
+        Ok(())
+    })
+}
+
+fn parse_bool<R, F>(reader: &mut Reader<R>, buf: &mut Vec<u8>, f: F) -> Result<()>
+where
+    R: BufRead,
+    F: FnOnce(bool),
+{
+    text_as_escaped_bytes_err(reader, buf, |t| match &*t {
+        b"True" => {
+            f(true);
+            Ok(())
+        }
+        b"False" => {
+            f(false);
+            Ok(())
+        }
+        _ => Err(Error::Bool),
+    })
+}
+
+fn comparison_override<R, F>(reader: &mut Reader<R>, buf: &mut Vec<u8>, f: F) -> Result<()>
+where
+    R: BufRead,
+    F: FnOnce(Option<String>),
+{
+    text(reader, buf, |t| {
+        f(if t == "Current Comparison" {
+            None
+        } else {
+            Some(t.into_owned())
+        })
+    })
+}
+
+fn timing_method_override<R, F>(reader: &mut Reader<R>, buf: &mut Vec<u8>, f: F) -> Result<()>
+where
+    R: BufRead,
+    F: FnOnce(Option<TimingMethod>),
+{
+    text_as_escaped_bytes_err(reader, buf, |t| {
+        f(match &*t {
+            b"Current Timing Method" => None,
+            b"Real Time" => Some(TimingMethod::RealTime),
+            b"Game Time" => Some(TimingMethod::GameTime),
+            _ => return Err(Error::TimingMethod),
+        });
+        Ok(())
+    })
+}
+
+fn accuracy<R, F>(reader: &mut Reader<R>, buf: &mut Vec<u8>, f: F) -> Result<()>
+where
+    R: BufRead,
+    F: FnOnce(Accuracy),
+{
+    text_as_escaped_bytes_err(reader, buf, |t| {
+        f(match &*t {
+            b"Tenths" => Accuracy::Tenths,
+            b"Seconds" => Accuracy::Seconds,
+            b"Hundredths" => Accuracy::Hundredths,
+            _ => return Err(Error::Accuracy),
+        });
+        Ok(())
+    })
+}
+
+fn timer_format<R, F>(reader: &mut Reader<R>, buf: &mut Vec<u8>, f: F) -> Result<()>
+where
+    R: BufRead,
+    F: FnOnce(DigitsFormat, Accuracy),
+{
+    text_as_escaped_bytes_err(reader, buf, |t| {
+        let mut splits = t.splitn(2, |&b| b == b'.');
+        let digits_format = match splits.next().unwrap_or(b"") {
+            b"1" => DigitsFormat::SingleDigitSeconds,
+            b"00:01" => DigitsFormat::DoubleDigitMinutes,
+            b"0:00:01" => DigitsFormat::SingleDigitHours,
+            b"00:00:01" => DigitsFormat::DoubleDigitHours,
+            _ => return Err(Error::DigitsFormat),
+        };
+        let accuracy = match splits.next().unwrap_or(b"") {
+            b"23" => Accuracy::Hundredths,
+            b"2" => Accuracy::Tenths,
+            b"" => Accuracy::Seconds,
+            _ => return Err(Error::Accuracy),
+        };
+        f(digits_format, accuracy);
+        Ok(())
+    })
+}
+
+fn component<R, F>(reader: &mut Reader<R>, buf: &mut Vec<u8>, f: F) -> Result<()>
+where
+    R: BufRead,
+    F: FnOnce(Component),
+{
+    let mut component = None;
+
+    parse_children(reader, buf, |reader, tag| {
+        if tag.name() == b"Path" {
+            text_err(reader, tag.into_buf(), |text| {
+                component = Some(match &*text {
+                    "LiveSplit.BlankSpace.dll" => blank_space::Component::new().into(),
+                    "LiveSplit.CurrentComparison.dll" => {
+                        current_comparison::Component::new().into()
+                    }
+                    "LiveSplit.RunPrediction.dll" => current_pace::Component::new().into(),
+                    "LiveSplit.Delta.dll" => delta::Component::new().into(),
+                    "LiveSplit.DetailedTimer.dll" => {
+                        Box::new(detailed_timer::Component::new()).into()
+                    }
+                    "LiveSplit.Graph.dll" => graph::Component::new().into(),
+                    "LiveSplit.PossibleTimeSave.dll" => possible_time_save::Component::new().into(),
+                    "LiveSplit.PreviousSegment.dll" => previous_segment::Component::new().into(),
+                    "" => separator::Component::new().into(),
+                    "LiveSplit.Splits.dll" | "LiveSplit.Subsplits.dll" => {
+                        splits::Component::new().into()
+                    }
+                    "LiveSplit.SumOfBest.dll" => sum_of_best::Component::new().into(),
+                    "LiveSplit.Text.dll" => text::Component::new().into(),
+                    "LiveSplit.Timer.dll" => timer::Component::new().into(),
+                    "LiveSplit.Title.dll" => title::Component::new().into(),
+                    "LiveSplit.TotalPlaytime.dll" => total_playtime::Component::new().into(),
+                    _ => return Ok(()),
+                });
+                Ok(())
+            })
+        } else if tag.name() == b"Settings" {
+            // Assumption: Settings always has to come after the Path.
+            // Otherwise we need to cache the settings and load them later.
+            if let Some(component) = &mut component {
+                match component {
+                    Component::BlankSpace(c) => blank_space::settings(reader, tag.into_buf(), c),
+                    Component::CurrentComparison(c) => {
+                        current_comparison::settings(reader, tag.into_buf(), c)
+                    }
+                    Component::CurrentPace(c) => current_pace::settings(reader, tag.into_buf(), c),
+                    Component::Delta(c) => delta::settings(reader, tag.into_buf(), c),
+                    Component::DetailedTimer(c) => {
+                        detailed_timer::settings(reader, tag.into_buf(), c)
+                    }
+                    Component::Graph(c) => graph::settings(reader, tag.into_buf(), c),
+                    Component::PossibleTimeSave(c) => {
+                        possible_time_save::settings(reader, tag.into_buf(), c)
+                    }
+                    Component::PreviousSegment(c) => {
+                        previous_segment::settings(reader, tag.into_buf(), c)
+                    }
+                    Component::Separator(_) => end_tag(reader, tag.into_buf()),
+                    Component::Splits(c) => splits::settings(reader, tag.into_buf(), c),
+                    Component::SumOfBest(c) => sum_of_best::settings(reader, tag.into_buf(), c),
+                    Component::Text(c) => text::settings(reader, tag.into_buf(), c),
+                    Component::Timer(c) => timer::settings(reader, tag.into_buf(), c),
+                    Component::Title(c) => title::settings(reader, tag.into_buf(), c),
+                    Component::TotalPlaytime(c) => {
+                        total_playtime::settings(reader, tag.into_buf(), c)
+                    }
+                }
+            } else {
+                end_tag(reader, tag.into_buf())
+            }
+        } else {
+            end_tag(reader, tag.into_buf())
+        }
+    })?;
+
+    if let Some(component) = component {
+        f(component);
+    }
+
+    Ok(())
+}
+
+fn parse_general_settings<R: BufRead>(
+    layout: &mut Layout,
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+) -> Result<()> {
+    let settings = layout.general_settings_mut();
+    let mut background_builder = GradientBuilder::new();
+
+    parse_children(reader, buf, |reader, tag| {
+        if tag.name() == b"TextColor" {
+            color(reader, tag.into_buf(), |color| {
+                settings.text_color = color;
+            })
+        } else if tag.name() == b"BackgroundColor" {
+            color(reader, tag.into_buf(), |color| {
+                background_builder.first = color;
+            })
+        } else if tag.name() == b"BackgroundColor2" {
+            color(reader, tag.into_buf(), |color| {
+                background_builder.second = color;
+            })
+        } else if tag.name() == b"ThinSeparatorsColor" {
+            color(reader, tag.into_buf(), |color| {
+                settings.thin_separators_color = color;
+            })
+        } else if tag.name() == b"SeparatorsColor" {
+            color(reader, tag.into_buf(), |color| {
+                settings.separators_color = color;
+            })
+        } else if tag.name() == b"PersonalBestColor" {
+            color(reader, tag.into_buf(), |color| {
+                settings.personal_best_color = color;
+            })
+        } else if tag.name() == b"AheadGainingTimeColor" {
+            color(reader, tag.into_buf(), |color| {
+                settings.ahead_gaining_time_color = color;
+            })
+        } else if tag.name() == b"AheadLosingTimeColor" {
+            color(reader, tag.into_buf(), |color| {
+                settings.ahead_losing_time_color = color;
+            })
+        } else if tag.name() == b"BehindGainingTimeColor" {
+            color(reader, tag.into_buf(), |color| {
+                settings.behind_gaining_time_color = color;
+            })
+        } else if tag.name() == b"BehindLosingTimeColor" {
+            color(reader, tag.into_buf(), |color| {
+                settings.behind_losing_time_color = color;
+            })
+        } else if tag.name() == b"BestSegmentColor" {
+            color(reader, tag.into_buf(), |color| {
+                settings.best_segment_color = color;
+            })
+        } else if tag.name() == b"NotRunningColor" {
+            color(reader, tag.into_buf(), |color| {
+                settings.not_running_color = color;
+            })
+        } else if tag.name() == b"PausedColor" {
+            color(reader, tag.into_buf(), |color| {
+                settings.paused_color = color;
+            })
+        } else if tag.name() == b"BackgroundType" {
+            text_err(reader, tag.into_buf(), |text| {
+                background_builder.kind = match &*text {
+                    "SolidColor" => GradientKind::Plain,
+                    "VerticalGradient" => GradientKind::Vertical,
+                    "HorizontalGradient" => GradientKind::Horizontal,
+                    "Image" => {
+                        background_builder.first = Color::black();
+                        background_builder.second = Color::black();
+                        GradientKind::Plain
+                    }
+                    _ => return Err(Error::GradientType),
+                };
+                Ok(())
+            })
+        } else {
+            end_tag(reader, tag.into_buf())
+        }
+    })?;
+
+    settings.background = background_builder.build();
+
+    Ok(())
+}
+
+/// Attempts to parse a layout file of the original LiveSplit. They are only
+/// parsed on a best effort basis, so if something isn't supported by
+/// livesplit-core, then it will be parsed without that option.
+pub fn parse<R: BufRead>(source: R) -> Result<Layout> {
+    let reader = &mut Reader::from_reader(source);
+    reader.expand_empty_elements(true);
+    reader.trim_text(true);
+
+    let mut buf = Vec::with_capacity(4096);
+
+    let mut layout = Layout::new();
+
+    parse_base(reader, &mut buf, b"Layout", |reader, tag| {
+        parse_children(reader, tag.into_buf(), |reader, tag| {
+            if tag.name() == b"Settings" {
+                parse_general_settings(&mut layout, reader, tag.into_buf())
+            } else if tag.name() == b"Components" {
+                parse_children(reader, tag.into_buf(), |reader, tag| {
+                    component(reader, tag.into_buf(), |c| {
+                        layout.push(c);
+                    })
+                })
+            } else {
+                end_tag(reader, tag.into_buf())
+            }
+        })
+    })?;
+
+    if layout.components.is_empty() {
+        Err(Error::Empty)
+    } else {
+        Ok(layout)
+    }
+}
diff --git a/src/layout/parser/blank_space.rs b/src/layout/parser/blank_space.rs
new file mode 100644
index 0000000..13f2c79
--- /dev/null
+++ b/src/layout/parser/blank_space.rs
@@ -0,0 +1,36 @@
+use super::{Error, GradientBuilder, Result};
+use crate::xml_util::{end_tag, parse_children, text_parsed};
+use quick_xml::Reader;
+use std::io::BufRead;
+
+pub use crate::component::blank_space::Component;
+
+pub fn settings<R>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    component: &mut Component,
+) -> Result<()>
+where
+    R: BufRead,
+{
+    let settings = component.settings_mut();
+    let mut background_builder = GradientBuilder::new();
+
+    parse_children::<_, _, Error>(reader, buf, |reader, tag| {
+        if let Some(tag) = background_builder.parse_background(reader, tag)? {
+            if tag.name() == b"SpaceHeight" {
+                text_parsed(reader, tag.into_buf(), |h| settings.height = h)
+            } else {
+                // FIXME:
+                // SpaceWidth
+                end_tag(reader, tag.into_buf())
+            }
+        } else {
+            Ok(())
+        }
+    })?;
+
+    settings.background = background_builder.build();
+
+    Ok(())
+}
diff --git a/src/layout/parser/current_comparison.rs b/src/layout/parser/current_comparison.rs
new file mode 100644
index 0000000..1191e52
--- /dev/null
+++ b/src/layout/parser/current_comparison.rs
@@ -0,0 +1,54 @@
+use super::{color, parse_bool, GradientBuilder, Result};
+use crate::xml_util::{end_tag, parse_children};
+use quick_xml::Reader;
+use std::io::BufRead;
+
+pub use crate::component::current_comparison::Component;
+
+pub fn settings<R>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    component: &mut Component,
+) -> Result<()>
+where
+    R: BufRead,
+{
+    let settings = component.settings_mut();
+    let mut background_builder = GradientBuilder::new();
+    let (mut override_label, mut override_value) = (false, false);
+
+    parse_children(reader, buf, |reader, tag| {
+        if let Some(tag) = background_builder.parse_background(reader, tag)? {
+            if tag.name() == b"TextColor" {
+                color(reader, tag.into_buf(), |c| settings.label_color = Some(c))
+            } else if tag.name() == b"OverrideTextColor" {
+                parse_bool(reader, tag.into_buf(), |b| override_label = b)
+            } else if tag.name() == b"TimeColor" {
+                color(reader, tag.into_buf(), |c| settings.value_color = Some(c))
+            } else if tag.name() == b"OverrideTimeColor" {
+                parse_bool(reader, tag.into_buf(), |b| override_value = b)
+            } else if tag.name() == b"Display2Rows" {
+                parse_bool(reader, tag.into_buf(), |b| settings.display_two_rows = b)
+            } else {
+                // FIXME:
+                // Font1
+                // Font2
+                // OverrideFont1
+                // OverrideFont2
+                end_tag(reader, tag.into_buf())
+            }
+        } else {
+            Ok(())
+        }
+    })?;
+
+    if !override_label {
+        settings.label_color = None;
+    }
+    if !override_value {
+        settings.value_color = None;
+    }
+    settings.background = background_builder.build();
+
+    Ok(())
+}
diff --git a/src/layout/parser/current_pace.rs b/src/layout/parser/current_pace.rs
new file mode 100644
index 0000000..289ea5c
--- /dev/null
+++ b/src/layout/parser/current_pace.rs
@@ -0,0 +1,53 @@
+use super::{accuracy, color, comparison_override, parse_bool, GradientBuilder, Result};
+use crate::xml_util::{end_tag, parse_children};
+use quick_xml::Reader;
+use std::io::BufRead;
+
+pub use crate::component::current_pace::Component;
+
+pub fn settings<R>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    component: &mut Component,
+) -> Result<()>
+where
+    R: BufRead,
+{
+    let settings = component.settings_mut();
+    let mut background_builder = GradientBuilder::new();
+    let (mut override_label, mut override_value) = (false, false);
+
+    parse_children(reader, buf, |reader, tag| {
+        if let Some(tag) = background_builder.parse_background(reader, tag)? {
+            if tag.name() == b"TextColor" {
+                color(reader, tag.into_buf(), |c| settings.label_color = Some(c))
+            } else if tag.name() == b"OverrideTextColor" {
+                parse_bool(reader, tag.into_buf(), |b| override_label = b)
+            } else if tag.name() == b"TimeColor" {
+                color(reader, tag.into_buf(), |c| settings.value_color = Some(c))
+            } else if tag.name() == b"OverrideTimeColor" {
+                parse_bool(reader, tag.into_buf(), |b| override_value = b)
+            } else if tag.name() == b"Accuracy" {
+                accuracy(reader, tag.into_buf(), |a| settings.accuracy = a)
+            } else if tag.name() == b"Comparison" {
+                comparison_override(reader, tag.into_buf(), |v| settings.comparison_override = v)
+            } else if tag.name() == b"Display2Rows" {
+                parse_bool(reader, tag.into_buf(), |b| settings.display_two_rows = b)
+            } else {
+                end_tag(reader, tag.into_buf())
+            }
+        } else {
+            Ok(())
+        }
+    })?;
+
+    if !override_label {
+        settings.label_color = None;
+    }
+    if !override_value {
+        settings.value_color = None;
+    }
+    settings.background = background_builder.build();
+
+    Ok(())
+}
diff --git a/src/layout/parser/delta.rs b/src/layout/parser/delta.rs
new file mode 100644
index 0000000..3da98fe
--- /dev/null
+++ b/src/layout/parser/delta.rs
@@ -0,0 +1,48 @@
+use super::{accuracy, color, comparison_override, parse_bool, GradientBuilder, Result};
+use crate::xml_util::{end_tag, parse_children};
+use quick_xml::Reader;
+use std::io::BufRead;
+
+pub use crate::component::delta::Component;
+
+pub fn settings<R>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    component: &mut Component,
+) -> Result<()>
+where
+    R: BufRead,
+{
+    let settings = component.settings_mut();
+    let mut background_builder = GradientBuilder::new();
+    let mut override_label = false;
+
+    parse_children(reader, buf, |reader, tag| {
+        if let Some(tag) = background_builder.parse_background(reader, tag)? {
+            if tag.name() == b"TextColor" {
+                color(reader, tag.into_buf(), |c| settings.label_color = Some(c))
+            } else if tag.name() == b"OverrideTextColor" {
+                parse_bool(reader, tag.into_buf(), |b| override_label = b)
+            } else if tag.name() == b"Accuracy" {
+                accuracy(reader, tag.into_buf(), |a| settings.accuracy = a)
+            } else if tag.name() == b"Comparison" {
+                comparison_override(reader, tag.into_buf(), |v| settings.comparison_override = v)
+            } else if tag.name() == b"Display2Rows" {
+                parse_bool(reader, tag.into_buf(), |b| settings.display_two_rows = b)
+            } else if tag.name() == b"DropDecimals" {
+                parse_bool(reader, tag.into_buf(), |b| settings.drop_decimals = b)
+            } else {
+                end_tag(reader, tag.into_buf())
+            }
+        } else {
+            Ok(())
+        }
+    })?;
+
+    if !override_label {
+        settings.label_color = None;
+    }
+    settings.background = background_builder.build();
+
+    Ok(())
+}
diff --git a/src/layout/parser/detailed_timer.rs b/src/layout/parser/detailed_timer.rs
new file mode 100644
index 0000000..ea25a77
--- /dev/null
+++ b/src/layout/parser/detailed_timer.rs
@@ -0,0 +1,117 @@
+use super::{
+    accuracy, color, comparison_override, end_tag, parse_bool, parse_children, text_parsed,
+    timer_format, timing_method_override, GradientBuilder, Result,
+};
+use crate::timing::formatter::DigitsFormat;
+use quick_xml::Reader;
+use std::io::BufRead;
+
+pub use crate::component::detailed_timer::Component;
+
+pub fn settings<R>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    component: &mut Component,
+) -> Result<()>
+where
+    R: BufRead,
+{
+    let mut settings = component.settings().clone();
+    let mut background_builder = GradientBuilder::new();
+    let mut timer_override_color = false;
+    let (mut total_height, mut segment_timer_ratio) = (65u32, 0.4);
+
+    parse_children(reader, buf, |reader, tag| {
+        if let Some(tag) = background_builder.parse_background(reader, tag)? {
+            if tag.name() == b"Height" {
+                text_parsed(reader, tag.into_buf(), |v| total_height = v)
+            } else if tag.name() == b"SegmentTimerSizeRatio" {
+                text_parsed(reader, tag.into_buf(), |v: u32| {
+                    segment_timer_ratio = v as f32 / 100.0
+                })
+            } else if tag.name() == b"TimerShowGradient" {
+                parse_bool(reader, tag.into_buf(), |b| settings.timer.show_gradient = b)
+            } else if tag.name() == b"OverrideTimerColors" {
+                // Version >= 1.3
+                parse_bool(reader, tag.into_buf(), |b| timer_override_color = b)
+            } else if tag.name() == b"TimerUseSplitColors" {
+                // Version < 1.3
+                parse_bool(reader, tag.into_buf(), |b| timer_override_color = !b)
+            } else if tag.name() == b"SegmentTimerShowGradient" {
+                parse_bool(reader, tag.into_buf(), |b| {
+                    settings.segment_timer.show_gradient = b
+                })
+            } else if tag.name() == b"TimerFormat" {
+                // Version >= 1.5
+                timer_format(reader, tag.into_buf(), |d, a| {
+                    settings.timer.digits_format = d;
+                    settings.timer.accuracy = a;
+                })
+            } else if tag.name() == b"SegmentTimerFormat" {
+                // Version >= 1.5
+                timer_format(reader, tag.into_buf(), |d, a| {
+                    settings.segment_timer.digits_format = d;
+                    settings.segment_timer.accuracy = a;
+                })
+            } else if tag.name() == b"TimerAccuracy" {
+                // Version < 1.5
+                settings.timer.digits_format = DigitsFormat::SingleDigitSeconds;
+                accuracy(reader, tag.into_buf(), |v| settings.timer.accuracy = v)
+            } else if tag.name() == b"SegmentTimerAccuracy" {
+                // Version < 1.5
+                settings.segment_timer.digits_format = DigitsFormat::SingleDigitSeconds;
+                accuracy(reader, tag.into_buf(), |v| {
+                    settings.segment_timer.accuracy = v
+                })
+            } else if tag.name() == b"TimerColor" {
+                color(reader, tag.into_buf(), |v| {
+                    settings.timer.color_override = Some(v)
+                })
+            } else if tag.name() == b"DisplayIcon" {
+                parse_bool(reader, tag.into_buf(), |b| settings.display_icon = b)
+            } else if tag.name() == b"ShowSplitName" {
+                parse_bool(reader, tag.into_buf(), |b| settings.show_segment_name = b)
+            } else if tag.name() == b"Comparison" {
+                comparison_override(reader, tag.into_buf(), |v| settings.comparison1 = v)
+            } else if tag.name() == b"Comparison2" {
+                comparison_override(reader, tag.into_buf(), |v| settings.comparison2 = v)
+            } else if tag.name() == b"HideComparison" {
+                parse_bool(reader, tag.into_buf(), |b| {
+                    settings.hide_second_comparison = b
+                })
+            } else if tag.name() == b"TimingMethod" {
+                timing_method_override(reader, tag.into_buf(), |v| settings.timer.timing_method = v)
+            } else {
+                // FIXME:
+                // Width
+                // SegmentTimesAccuracy
+                // SegmentTimerColor
+                // SegmentLabelsColor
+                // SegmentTimesColor
+                // SegmentLabelsFont
+                // SegmentTimesFont
+                // SplitNameFont
+                // IconSize
+                // SplitNameColor
+                // DecimalsSize
+                // SegmentTimerDecimalsSize
+                end_tag(reader, tag.into_buf())
+            }
+        } else {
+            Ok(())
+        }
+    })?;
+
+    if !timer_override_color {
+        // FIXME: This isn't actually exposed in the Detailed Timer's settings.
+        settings.timer.color_override = None;
+    }
+    settings.background = background_builder.build();
+
+    settings.segment_timer.height = (total_height as f32 * segment_timer_ratio) as u32;
+    settings.timer.height = total_height - settings.segment_timer.height;
+
+    component.set_settings(settings);
+
+    Ok(())
+}
diff --git a/src/layout/parser/graph.rs b/src/layout/parser/graph.rs
new file mode 100644
index 0000000..7dcf133
--- /dev/null
+++ b/src/layout/parser/graph.rs
@@ -0,0 +1,66 @@
+use super::{color, comparison_override, end_tag, parse_bool, parse_children, text_parsed, Result};
+use quick_xml::Reader;
+use std::io::BufRead;
+
+pub use crate::component::graph::Component;
+
+pub fn settings<R>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    component: &mut Component,
+) -> Result<()>
+where
+    R: BufRead,
+{
+    let settings = component.settings_mut();
+
+    parse_children(reader, buf, |reader, tag| {
+        if tag.name() == b"Height" {
+            text_parsed(reader, tag.into_buf(), |v| settings.height = v)
+        } else if tag.name() == b"BehindGraphColor" {
+            color(reader, tag.into_buf(), |c| {
+                settings.behind_background_color = c
+            })
+        } else if tag.name() == b"AheadGraphColor" {
+            color(reader, tag.into_buf(), |c| {
+                settings.ahead_background_color = c
+            })
+        } else if tag.name() == b"GridlinesColor" {
+            color(reader, tag.into_buf(), |c| {
+                println!("{:?} <- {:?}", settings.grid_lines_color, c);
+                settings.grid_lines_color = c
+            })
+        } else if tag.name() == b"PartialFillColorAhead" {
+            // Version >= 1.2
+            color(reader, tag.into_buf(), |c| settings.partial_fill_color = c)
+        } else if tag.name() == b"CompleteFillColorAhead" {
+            // Version >= 1.2
+            color(reader, tag.into_buf(), |c| settings.complete_fill_color = c)
+        } else if tag.name() == b"PartialFillColor" {
+            // Version < 1.2
+            color(reader, tag.into_buf(), |c| settings.partial_fill_color = c)
+        } else if tag.name() == b"CompleteFillColor" {
+            // Version < 1.2
+            color(reader, tag.into_buf(), |c| settings.complete_fill_color = c)
+        } else if tag.name() == b"GraphColor" {
+            color(reader, tag.into_buf(), |c| settings.graph_lines_color = c)
+        } else if tag.name() == b"LiveGraph" {
+            parse_bool(reader, tag.into_buf(), |b| settings.live_graph = b)
+        } else if tag.name() == b"FlipGraph" {
+            parse_bool(reader, tag.into_buf(), |b| settings.flip_graph = b)
+        } else if tag.name() == b"Comparison" {
+            comparison_override(reader, tag.into_buf(), |v| settings.comparison_override = v)
+        } else if tag.name() == b"ShowBestSegments" {
+            parse_bool(reader, tag.into_buf(), |b| settings.show_best_segments = b)
+        } else {
+            // FIXME:
+            // Width
+            // PartialFillColorBehind // Version >= 1.2
+            // CompleteFillColorBehind // Version >= 1.2
+            // ShadowsColor
+            // GraphLinesColor (separators, not our graph_lines_color)
+            // GraphGoldColor
+            end_tag(reader, tag.into_buf())
+        }
+    })
+}
diff --git a/src/layout/parser/possible_time_save.rs b/src/layout/parser/possible_time_save.rs
new file mode 100644
index 0000000..e9f531f
--- /dev/null
+++ b/src/layout/parser/possible_time_save.rs
@@ -0,0 +1,59 @@
+use super::{
+    accuracy, color, comparison_override, end_tag, parse_bool, parse_children, GradientBuilder,
+    Result,
+};
+use quick_xml::Reader;
+use std::io::BufRead;
+
+pub use crate::component::possible_time_save::Component;
+
+pub fn settings<R>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    component: &mut Component,
+) -> Result<()>
+where
+    R: BufRead,
+{
+    let settings = component.settings_mut();
+    let mut background_builder = GradientBuilder::new();
+    let (mut override_label, mut override_value) = (false, false);
+
+    parse_children(reader, buf, |reader, tag| {
+        if let Some(tag) = background_builder.parse_background(reader, tag)? {
+            if tag.name() == b"TextColor" {
+                color(reader, tag.into_buf(), |c| settings.label_color = Some(c))
+            } else if tag.name() == b"OverrideTextColor" {
+                parse_bool(reader, tag.into_buf(), |b| override_label = b)
+            } else if tag.name() == b"TimeColor" {
+                color(reader, tag.into_buf(), |c| settings.value_color = Some(c))
+            } else if tag.name() == b"OverrideTimeColor" {
+                parse_bool(reader, tag.into_buf(), |b| override_value = b)
+            } else if tag.name() == b"Accuracy" {
+                accuracy(reader, tag.into_buf(), |v| settings.accuracy = v)
+            } else if tag.name() == b"Comparison" {
+                comparison_override(reader, tag.into_buf(), |v| settings.comparison_override = v)
+            } else if tag.name() == b"Display2Rows" {
+                parse_bool(reader, tag.into_buf(), |b| settings.display_two_rows = b)
+            } else if tag.name() == b"TotalTimeSave" {
+                parse_bool(reader, tag.into_buf(), |b| {
+                    settings.total_possible_time_save = b
+                })
+            } else {
+                end_tag(reader, tag.into_buf())
+            }
+        } else {
+            Ok(())
+        }
+    })?;
+
+    if !override_label {
+        settings.label_color = None;
+    }
+    if !override_value {
+        settings.value_color = None;
+    }
+    settings.background = background_builder.build();
+
+    Ok(())
+}
diff --git a/src/layout/parser/previous_segment.rs b/src/layout/parser/previous_segment.rs
new file mode 100644
index 0000000..bc2758b
--- /dev/null
+++ b/src/layout/parser/previous_segment.rs
@@ -0,0 +1,56 @@
+use super::{
+    accuracy, color, comparison_override, end_tag, parse_bool, parse_children, GradientBuilder,
+    Result,
+};
+use quick_xml::Reader;
+use std::io::BufRead;
+
+pub use crate::component::previous_segment::Component;
+
+pub fn settings<R>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    component: &mut Component,
+) -> Result<()>
+where
+    R: BufRead,
+{
+    let settings = component.settings_mut();
+    let mut background_builder = GradientBuilder::new();
+    let mut override_label = false;
+
+    parse_children(reader, buf, |reader, tag| {
+        if let Some(tag) = background_builder.parse_background(reader, tag)? {
+            if tag.name() == b"TextColor" {
+                color(reader, tag.into_buf(), |c| settings.label_color = Some(c))
+            } else if tag.name() == b"OverrideTextColor" {
+                parse_bool(reader, tag.into_buf(), |b| override_label = b)
+            } else if tag.name() == b"DeltaAccuracy" {
+                accuracy(reader, tag.into_buf(), |v| settings.accuracy = v)
+            } else if tag.name() == b"DropDecimals" {
+                parse_bool(reader, tag.into_buf(), |b| settings.drop_decimals = b)
+            } else if tag.name() == b"Comparison" {
+                comparison_override(reader, tag.into_buf(), |v| settings.comparison_override = v)
+            } else if tag.name() == b"Display2Rows" {
+                parse_bool(reader, tag.into_buf(), |b| settings.display_two_rows = b)
+            } else if tag.name() == b"ShowPossibleTimeSave" {
+                parse_bool(reader, tag.into_buf(), |b| {
+                    settings.show_possible_time_save = b
+                })
+            } else {
+                // FIXME:
+                // TimeSaveAccuracy
+                end_tag(reader, tag.into_buf())
+            }
+        } else {
+            Ok(())
+        }
+    })?;
+
+    if !override_label {
+        settings.label_color = None;
+    }
+    settings.background = background_builder.build();
+
+    Ok(())
+}
diff --git a/src/layout/parser/splits.rs b/src/layout/parser/splits.rs
new file mode 100644
index 0000000..73e74b4
--- /dev/null
+++ b/src/layout/parser/splits.rs
@@ -0,0 +1,227 @@
+use super::{
+    comparison_override, end_tag, parse_bool, parse_children, text, text_err, text_parsed,
+    timing_method_override, Error, GradientBuilder, GradientKind, ListGradientKind, Result,
+};
+use quick_xml::Reader;
+use std::io::BufRead;
+
+use crate::component::splits;
+pub use crate::component::splits::Component;
+
+pub fn settings<R>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    component: &mut Component,
+) -> Result<()>
+where
+    R: BufRead,
+{
+    let settings = component.settings_mut();
+    let mut split_gradient_builder = GradientBuilder::<GradientKind>::with_tags(
+        b"CurrentSplitTopColor",
+        b"CurrentSplitBottomColor",
+        b"CurrentSplitGradient",
+    );
+    let mut background_builder = GradientBuilder::<ListGradientKind>::new_gradient_type();
+
+    parse_children(reader, buf, |reader, tag| {
+        if let Some(tag) = background_builder.parse_background(reader, tag)? {
+            if let Some(tag) = split_gradient_builder.parse_background(reader, tag)? {
+                if tag.name() == b"VisualSplitCount" {
+                    text_parsed(reader, tag.into_buf(), |v| settings.visual_split_count = v)
+                } else if tag.name() == b"SplitPreviewCount" {
+                    text_parsed(reader, tag.into_buf(), |v| settings.split_preview_count = v)
+                } else if tag.name() == b"ShowThinSeparators" {
+                    parse_bool(reader, tag.into_buf(), |b| {
+                        settings.show_thin_separators = b
+                    })
+                } else if tag.name() == b"AlwaysShowLastSplit" {
+                    parse_bool(reader, tag.into_buf(), |b| {
+                        settings.always_show_last_split = b
+                    })
+                } else if tag.name() == b"SplitPreviewCount" {
+                    text_parsed(reader, tag.into_buf(), |v| settings.split_preview_count = v)
+                } else if tag.name() == b"ShowBlankSplits" {
+                    parse_bool(reader, tag.into_buf(), |b| {
+                        settings.fill_with_blank_space = b
+                    })
+                } else if tag.name() == b"SeparatorLastSplit" {
+                    parse_bool(reader, tag.into_buf(), |b| {
+                        settings.separator_last_split = b
+                    })
+                } else if tag.name() == b"Display2Rows" {
+                    parse_bool(reader, tag.into_buf(), |b| settings.display_two_rows = b)
+                } else if tag.name() == b"ShowColumnLabels" {
+                    parse_bool(reader, tag.into_buf(), |b| settings.show_column_labels = b)
+                } else if tag.name() == b"Columns" {
+                    // Version >= 1.5
+                    settings.columns.clear();
+
+                    parse_children(reader, tag.into_buf(), |reader, tag| {
+                        let mut column = splits::ColumnSettings::default();
+                        parse_children(reader, tag.into_buf(), |reader, tag| {
+                            if tag.name() == b"Name" {
+                                text(reader, tag.into_buf(), |v| column.name = v.into_owned())
+                            } else if tag.name() == b"Comparison" {
+                                comparison_override(reader, tag.into_buf(), |v| {
+                                    column.comparison_override = v
+                                })
+                            } else if tag.name() == b"TimingMethod" {
+                                timing_method_override(reader, tag.into_buf(), |v| {
+                                    column.timing_method = v
+                                })
+                            } else if tag.name() == b"Type" {
+                                text_err(reader, tag.into_buf(), |v| {
+                                    use self::splits::{
+                                        ColumnStartWith, ColumnUpdateTrigger, ColumnUpdateWith,
+                                    };
+                                    let (start_with, update_with, update_trigger) = match &*v {
+                                        "Delta" => (
+                                            ColumnStartWith::Empty,
+                                            ColumnUpdateWith::Delta,
+                                            ColumnUpdateTrigger::Contextual,
+                                        ),
+                                        "SplitTime" => (
+                                            ColumnStartWith::ComparisonTime,
+                                            ColumnUpdateWith::SplitTime,
+                                            ColumnUpdateTrigger::OnEndingSegment,
+                                        ),
+                                        "DeltaorSplitTime" => (
+                                            ColumnStartWith::ComparisonTime,
+                                            ColumnUpdateWith::Delta, // FIXME: With Fallback
+                                            ColumnUpdateTrigger::Contextual,
+                                        ),
+                                        "SegmentDelta" => (
+                                            ColumnStartWith::Empty,
+                                            ColumnUpdateWith::SegmentDelta,
+                                            ColumnUpdateTrigger::Contextual,
+                                        ),
+                                        "SegmentTime" => (
+                                            ColumnStartWith::ComparisonSegmentTime,
+                                            ColumnUpdateWith::SegmentTime,
+                                            ColumnUpdateTrigger::OnEndingSegment,
+                                        ),
+                                        "SegmentDeltaorSegmentTime" => (
+                                            ColumnStartWith::ComparisonSegmentTime,
+                                            ColumnUpdateWith::SegmentDelta, // FIXME: With Fallback
+                                            ColumnUpdateTrigger::Contextual,
+                                        ),
+                                        _ => return Err(Error::ColumnType),
+                                    };
+                                    column.start_with = start_with;
+                                    column.update_with = update_with;
+                                    column.update_trigger = update_trigger;
+                                    Ok(())
+                                })
+                            } else {
+                                end_tag(reader, tag.into_buf())
+                            }
+                        })?;
+                        settings.columns.insert(0, column);
+                        Ok(())
+                    })
+                } else if tag.name() == b"Comparison" {
+                    // Version < 1.5
+                    comparison_override(reader, tag.into_buf(), |v| {
+                        for column in &mut settings.columns {
+                            column.comparison_override = v.clone();
+                        }
+                    })
+                } else if tag.name() == b"ShowSplitTimes" {
+                    // Version < 1.5
+                    use self::splits::{
+                        ColumnSettings, ColumnStartWith, ColumnUpdateTrigger, ColumnUpdateWith,
+                    };
+                    parse_bool(reader, tag.into_buf(), |b| {
+                        if !b {
+                            let comparison_override =
+                                settings.columns.pop().and_then(|c| c.comparison_override);
+                            settings.columns.clear();
+                            settings.columns.push(ColumnSettings {
+                                name: String::from("Time"),
+                                start_with: ColumnStartWith::ComparisonTime,
+                                update_with: ColumnUpdateWith::SplitTime,
+                                update_trigger: ColumnUpdateTrigger::OnEndingSegment,
+                                comparison_override: comparison_override.clone(),
+                                timing_method: None,
+                            });
+                            settings.columns.push(ColumnSettings {
+                                name: String::from("+/−"),
+                                start_with: ColumnStartWith::Empty,
+                                update_with: ColumnUpdateWith::Delta,
+                                update_trigger: ColumnUpdateTrigger::Contextual,
+                                comparison_override: comparison_override,
+                                timing_method: None,
+                            });
+                        }
+                    })
+                } else {
+                    // FIXME:
+                    // DisplayIcons
+                    // SplitWidth
+                    // SplitTimesAccuracy
+                    // AutomaticAbbreviations
+                    // BeforeNamesColor // Version >= 1.3
+                    // CurrentNamesColor // Version >= 1.3
+                    // AfterNamesColor // Version >= 1.3
+                    // OverrideTextColor // Version >= 1.3
+                    // SplitNamesColor // Version >= 1.2 && Version < 1.3
+                    // UseTextColor // Version < 1.3
+                    // BeforeTimesColor
+                    // CurrentTimesColor
+                    // AfterTimesColor
+                    // OverrideTimesColor
+                    // LockLastSplit
+                    // IconSize
+                    // IconShadows
+                    // SplitHeight
+                    // DeltasAccuracy
+                    // DropDecimals
+                    // OverrideDeltasColor
+                    // DeltasColor
+                    // LabelsColor
+
+                    // FIXME: Subsplits
+                    // MinimumMajorSplits
+                    // IndentBlankIcons
+                    // IndentSubsplits
+                    // HideSubsplits
+                    // ShowSubsplits
+                    // CurrentSectionOnly
+                    // OverrideSubsplitColor
+                    // SubsplitTopColor
+                    // SubsplitBottomColor
+                    // SubsplitGradient
+                    // ShowHeader
+                    // IndentSectionSplit
+                    // ShowIconSectionSplit
+                    // ShowSectionIcon
+                    // HeaderTopColor
+                    // HeaderBottomColor
+                    // HeaderGradient
+                    // OverrideHeaderColor
+                    // HeaderTextColor
+                    // HeaderText
+                    // HeaderTimesColor
+                    // HeaderTimes
+                    // HeaderAccuracy
+                    // SectionTimer
+                    // SectionTimerColor
+                    // SectionTimerGradient
+                    // SectionTimerAccuracy
+
+                    end_tag(reader, tag.into_buf())
+                }
+            } else {
+                Ok(())
+            }
+        } else {
+            Ok(())
+        }
+    })?;
+
+    settings.current_split_gradient = split_gradient_builder.build();
+    settings.background = background_builder.build();
+
+    Ok(())
+}
diff --git a/src/layout/parser/sum_of_best.rs b/src/layout/parser/sum_of_best.rs
new file mode 100644
index 0000000..0954253
--- /dev/null
+++ b/src/layout/parser/sum_of_best.rs
@@ -0,0 +1,50 @@
+use super::{accuracy, color, end_tag, parse_bool, parse_children, GradientBuilder, Result};
+use quick_xml::Reader;
+use std::io::BufRead;
+
+pub use crate::component::sum_of_best::Component;
+
+pub fn settings<R>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    component: &mut Component,
+) -> Result<()>
+where
+    R: BufRead,
+{
+    let settings = component.settings_mut();
+    let mut background_builder = GradientBuilder::new();
+    let (mut override_label, mut override_value) = (false, false);
+
+    parse_children(reader, buf, |reader, tag| {
+        if let Some(tag) = background_builder.parse_background(reader, tag)? {
+            if tag.name() == b"TextColor" {
+                color(reader, tag.into_buf(), |c| settings.label_color = Some(c))
+            } else if tag.name() == b"OverrideTextColor" {
+                parse_bool(reader, tag.into_buf(), |b| override_label = b)
+            } else if tag.name() == b"TimeColor" {
+                color(reader, tag.into_buf(), |c| settings.value_color = Some(c))
+            } else if tag.name() == b"OverrideTimeColor" {
+                parse_bool(reader, tag.into_buf(), |b| override_value = b)
+            } else if tag.name() == b"Accuracy" {
+                accuracy(reader, tag.into_buf(), |v| settings.accuracy = v)
+            } else if tag.name() == b"Display2Rows" {
+                parse_bool(reader, tag.into_buf(), |b| settings.display_two_rows = b)
+            } else {
+                end_tag(reader, tag.into_buf())
+            }
+        } else {
+            Ok(())
+        }
+    })?;
+
+    if !override_label {
+        settings.label_color = None;
+    }
+    if !override_value {
+        settings.value_color = None;
+    }
+    settings.background = background_builder.build();
+
+    Ok(())
+}
diff --git a/src/layout/parser/text.rs b/src/layout/parser/text.rs
new file mode 100644
index 0000000..cf19a62
--- /dev/null
+++ b/src/layout/parser/text.rs
@@ -0,0 +1,66 @@
+use super::{color, end_tag, parse_bool, parse_children, text, GradientBuilder, Result};
+use quick_xml::Reader;
+use std::io::BufRead;
+
+pub use crate::component::text::Component;
+use crate::component::text::Text;
+
+pub fn settings<R>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    component: &mut Component,
+) -> Result<()>
+where
+    R: BufRead,
+{
+    let settings = component.settings_mut();
+    let mut background_builder = GradientBuilder::new();
+    let (mut override_label, mut override_value) = (false, false);
+    let (mut left_center, mut right) = (String::new(), String::new());
+
+    parse_children(reader, buf, |reader, tag| {
+        if let Some(tag) = background_builder.parse_background(reader, tag)? {
+            if tag.name() == b"TextColor" {
+                color(reader, tag.into_buf(), |c| {
+                    settings.left_center_color = Some(c)
+                })
+            } else if tag.name() == b"OverrideTextColor" {
+                parse_bool(reader, tag.into_buf(), |b| override_label = b)
+            } else if tag.name() == b"TimeColor" {
+                color(reader, tag.into_buf(), |c| settings.right_color = Some(c))
+            } else if tag.name() == b"OverrideTimeColor" {
+                parse_bool(reader, tag.into_buf(), |b| override_value = b)
+            } else if tag.name() == b"Text1" {
+                text(reader, tag.into_buf(), |v| left_center = v.into_owned())
+            } else if tag.name() == b"Text2" {
+                text(reader, tag.into_buf(), |v| right = v.into_owned())
+            } else if tag.name() == b"Display2Rows" {
+                parse_bool(reader, tag.into_buf(), |b| settings.display_two_rows = b)
+            } else {
+                // FIXME:
+                // Font1
+                // Font2
+                // OverrideFont1
+                // OverrideFont2
+                end_tag(reader, tag.into_buf())
+            }
+        } else {
+            Ok(())
+        }
+    })?;
+
+    if !override_label {
+        settings.left_center_color = None;
+    }
+    if !override_value {
+        settings.right_color = None;
+    }
+    settings.text = match (left_center.is_empty(), right.is_empty()) {
+        (false, false) => Text::Split(left_center, right),
+        (false, true) => Text::Center(left_center),
+        _ => Text::Center(right),
+    };
+    settings.background = background_builder.build();
+
+    Ok(())
+}
diff --git a/src/layout/parser/timer.rs b/src/layout/parser/timer.rs
new file mode 100644
index 0000000..6ae3425
--- /dev/null
+++ b/src/layout/parser/timer.rs
@@ -0,0 +1,67 @@
+use super::{
+    accuracy, color, end_tag, parse_bool, parse_children, text_parsed, timer_format,
+    timing_method_override, GradientBuilder, Result,
+};
+use quick_xml::Reader;
+use std::io::BufRead;
+
+pub use crate::component::timer::Component;
+
+pub fn settings<R>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    component: &mut Component,
+) -> Result<()>
+where
+    R: BufRead,
+{
+    let settings = component.settings_mut();
+    let mut background_builder = GradientBuilder::new();
+    let mut override_color = false;
+
+    parse_children(reader, buf, |reader, tag| {
+        if let Some(tag) = background_builder.parse_background(reader, tag)? {
+            if tag.name() == b"TimerHeight" {
+                text_parsed(reader, tag.into_buf(), |v| settings.height = v)
+            } else if tag.name() == b"TimerFormat" {
+                // Version >= 1.5
+                timer_format(reader, tag.into_buf(), |d, a| {
+                    settings.digits_format = d;
+                    settings.accuracy = a;
+                })
+            } else if tag.name() == b"TimerAccuracy" {
+                // Version >= 1.2 && Version < 1.5
+                accuracy(reader, tag.into_buf(), |v| settings.accuracy = v)
+            } else if tag.name() == b"OverrideSplitColors" {
+                // Version >= 1.3
+                parse_bool(reader, tag.into_buf(), |b| override_color = b)
+            } else if tag.name() == b"UseSplitColors" {
+                // Version < 1.3
+                parse_bool(reader, tag.into_buf(), |b| override_color = !b)
+            } else if tag.name() == b"ShowGradient" {
+                parse_bool(reader, tag.into_buf(), |b| settings.show_gradient = b)
+            } else if tag.name() == b"TimerColor" {
+                color(reader, tag.into_buf(), |c| {
+                    settings.color_override = Some(c)
+                })
+            } else if tag.name() == b"TimingMethod" {
+                timing_method_override(reader, tag.into_buf(), |v| settings.timing_method = v)
+            } else {
+                // FIXME:
+                // TimerWidth
+                // CenterTimer
+                // DecimalsSize
+                end_tag(reader, tag.into_buf())
+            }
+        } else {
+            Ok(())
+        }
+    })?;
+
+    if !override_color {
+        settings.color_override = None;
+    }
+    settings.background = background_builder.build();
+
+    Ok(())
+}
diff --git a/src/layout/parser/title.rs b/src/layout/parser/title.rs
new file mode 100644
index 0000000..788a237
--- /dev/null
+++ b/src/layout/parser/title.rs
@@ -0,0 +1,88 @@
+use super::{
+    color, end_tag, parse_bool, parse_children, text_as_escaped_bytes_err, Alignment, Error,
+    GradientBuilder, Result,
+};
+use quick_xml::Reader;
+use std::io::BufRead;
+
+pub use crate::component::title::Component;
+
+pub fn settings<R>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    component: &mut Component,
+) -> Result<()>
+where
+    R: BufRead,
+{
+    let settings = component.settings_mut();
+    let mut background_builder = GradientBuilder::new();
+    let mut override_title_color = false;
+
+    parse_children(reader, buf, |reader, tag| {
+        if let Some(tag) = background_builder.parse_background(reader, tag)? {
+            if tag.name() == b"ShowGameName" {
+                parse_bool(reader, tag.into_buf(), |b| settings.show_game_name = b)
+            } else if tag.name() == b"ShowCategoryName" {
+                parse_bool(reader, tag.into_buf(), |b| settings.show_category_name = b)
+            } else if tag.name() == b"ShowAttemptCount" {
+                parse_bool(reader, tag.into_buf(), |b| settings.show_attempt_count = b)
+            } else if tag.name() == b"ShowFinishedRunsCount" {
+                parse_bool(reader, tag.into_buf(), |b| {
+                    settings.show_finished_runs_count = b
+                })
+            } else if tag.name() == b"OverrideTitleColor" {
+                parse_bool(reader, tag.into_buf(), |b| override_title_color = b)
+            } else if tag.name() == b"TextAlignment" {
+                // Version >= 1.7.3
+                text_as_escaped_bytes_err(reader, tag.into_buf(), |v| {
+                    settings.text_alignment = match &*v {
+                        b"0" => Alignment::Auto,
+                        b"1" => Alignment::Left,
+                        b"2" => Alignment::Center,
+                        _ => return Err(Error::Alignment),
+                    };
+                    Ok(())
+                })
+            } else if tag.name() == b"CenterTitle" {
+                // Version >= 1.3 && Version < 1.7.3
+                parse_bool(reader, tag.into_buf(), |b| {
+                    settings.text_alignment = if b {
+                        Alignment::Center
+                    } else {
+                        Alignment::Auto
+                    }
+                })
+            } else if tag.name() == b"SingleLine" {
+                parse_bool(reader, tag.into_buf(), |b| {
+                    settings.display_as_single_line = b
+                })
+            } else if tag.name() == b"TitleColor" {
+                color(reader, tag.into_buf(), |c| settings.text_color = Some(c))
+            } else if tag.name() == b"DisplayGameIcon" {
+                parse_bool(reader, tag.into_buf(), |b| settings.display_game_icon = b)
+            } else if tag.name() == b"ShowRegion" {
+                parse_bool(reader, tag.into_buf(), |b| settings.show_region = b)
+            } else if tag.name() == b"ShowPlatform" {
+                parse_bool(reader, tag.into_buf(), |b| settings.show_platform = b)
+            } else if tag.name() == b"ShowVariables" {
+                parse_bool(reader, tag.into_buf(), |b| settings.show_variables = b)
+            } else {
+                // FIXME:
+                // OverrideTitleFont // Version >= 1.3
+                // TitleFont // Version >= 1.2
+                // UseLayoutSettingsFont // Version >= 1.2 && Version < 1.3
+                end_tag(reader, tag.into_buf())
+            }
+        } else {
+            Ok(())
+        }
+    })?;
+
+    if !override_title_color {
+        settings.text_color = None;
+    }
+    settings.background = background_builder.build();
+
+    Ok(())
+}
diff --git a/src/layout/parser/total_playtime.rs b/src/layout/parser/total_playtime.rs
new file mode 100644
index 0000000..ec16378
--- /dev/null
+++ b/src/layout/parser/total_playtime.rs
@@ -0,0 +1,50 @@
+use super::{color, end_tag, parse_bool, parse_children, GradientBuilder, Result};
+use quick_xml::Reader;
+use std::io::BufRead;
+
+pub use crate::component::total_playtime::Component;
+
+pub fn settings<R>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    component: &mut Component,
+) -> Result<()>
+where
+    R: BufRead,
+{
+    let settings = component.settings_mut();
+    let mut background_builder = GradientBuilder::new();
+    let (mut override_label, mut override_value) = (false, false);
+
+    parse_children(reader, buf, |reader, tag| {
+        if let Some(tag) = background_builder.parse_background(reader, tag)? {
+            if tag.name() == b"TextColor" {
+                color(reader, tag.into_buf(), |c| settings.label_color = Some(c))
+            } else if tag.name() == b"OverrideTextColor" {
+                parse_bool(reader, tag.into_buf(), |b| override_label = b)
+            } else if tag.name() == b"TimeColor" {
+                color(reader, tag.into_buf(), |c| settings.value_color = Some(c))
+            } else if tag.name() == b"OverrideTimeColor" {
+                parse_bool(reader, tag.into_buf(), |b| override_value = b)
+            } else if tag.name() == b"Display2Rows" {
+                parse_bool(reader, tag.into_buf(), |b| settings.display_two_rows = b)
+            } else if tag.name() == b"ShowTotalHours" {
+                parse_bool(reader, tag.into_buf(), |b| settings.show_days = !b)
+            } else {
+                end_tag(reader, tag.into_buf())
+            }
+        } else {
+            Ok(())
+        }
+    })?;
+
+    if !override_label {
+        settings.label_color = None;
+    }
+    if !override_value {
+        settings.value_color = None;
+    }
+    settings.background = background_builder.build();
+
+    Ok(())
+}
diff --git a/src/lib.rs b/src/lib.rs
index 4c62b95..67da90a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,7 +3,8 @@
     clippy::correctness,
     clippy::perf,
     clippy::style,
-    clippy::complexity
+    clippy::complexity,
+    rust_2018_idioms
 )]
 // Necessary for some larger quick-error based errors.
 #![recursion_limit = "128"]
@@ -79,6 +80,7 @@ pub mod settings;
 #[cfg(test)]
 mod tests_helper;
 pub mod timing;
+mod xml_util;
 
 pub use {
     crate::{
diff --git a/src/run/parser/face_split.rs b/src/run/parser/face_split.rs
index 4012bf6..caf5e7c 100644
--- a/src/run/parser/face_split.rs
+++ b/src/run/parser/face_split.rs
@@ -69,9 +69,9 @@ pub fn parse<R: BufRead>(source: R, load_icons: bool) -> Result<Run> {
     let mut lines = source.lines();
 
     run.set_category_name(lines.next().ok_or(Error::ExpectedTitle)??);
-    lines.next(); // TODO: Store Goal
+    lines.next(); // FIXME: Store Goal
     run.set_attempt_count(lines.next().ok_or(Error::ExpectedAttemptCount)??.parse()?);
-    lines.next(); // TODO: Store runs completed somehow
+    lines.next(); // FIXME: Store runs completed somehow
 
     for line in lines {
         let line = line?;
diff --git a/src/run/parser/livesplit.rs b/src/run/parser/livesplit.rs
index 6e64a05..6803e43 100644
--- a/src/run/parser/livesplit.rs
+++ b/src/run/parser/livesplit.rs
@@ -1,7 +1,7 @@
 //! Provides the parser for LiveSplit splits files.
 
 use super::super::ComparisonError;
-use super::xml_util::{
+use crate::xml_util::{
     attribute, attribute_err, end_tag, optional_attribute_err, parse_attributes, parse_base,
     parse_children, reencode_children, text, text_as_bytes_err, text_err, text_parsed,
 };
@@ -12,7 +12,59 @@ use std::io::BufRead;
 use std::path::PathBuf;
 use std::str;
 
-pub use super::xml_util::{Error, Result};
+use crate::xml_util::Error as XmlError;
+use chrono::ParseError as ChronoError;
+
+quick_error! {
+    /// The Error type for splits files that couldn't be parsed by the LiveSplit
+    /// Parser.
+    #[derive(Debug)]
+    pub enum Error {
+        /// The underlying XML format couldn't be parsed.
+        Xml(err: XmlError) {
+            from()
+        }
+        /// Failed to decode a string slice as UTF-8.
+        Utf8Str(err: std::str::Utf8Error) {
+            from()
+        }
+        /// Failed to decode a string as UTF-8.
+        Utf8String(err: std::string::FromUtf8Error) {
+            from()
+        }
+        /// Failed to parse an integer.
+        Int(err: std::num::ParseIntError) {
+            from()
+        }
+        /// Failed to parse a floating point number.
+        Float(err: std::num::ParseFloatError) {
+            from()
+        }
+        /// Failed to parse a time.
+        Time(err: crate::timing::ParseError) {
+            from()
+        }
+        /// Failed to parse a date.
+        Date(err: ChronoError) {
+            from()
+        }
+        /// Parsed comparison has an invalid name.
+        InvalidComparisonName(err: ComparisonError) {
+            from()
+        }
+        /// Failed to parse a boolean.
+        Bool {}
+    }
+}
+
+/// The Result type for the LiveSplit Parser.
+pub type Result<T> = std::result::Result<T, Error>;
+
+// FIXME: Generalized Type Ascription (GTA 6)
+#[inline]
+fn type_hint<T>(v: Result<T>) -> Result<T> {
+    v
+}
 
 #[derive(Copy, Clone, PartialOrd, PartialEq, Ord, Eq)]
 struct Version(u32, u32, u32, u32);
@@ -133,10 +185,10 @@ where
     time_span_opt(reader, buf, |t| f(Time::new().with_real_time(t)))
 }
 
-fn parse_bool<S: AsRef<str>>(text: S) -> Result<bool> {
-    match text.as_ref() {
-        "True" => Ok(true),
-        "False" => Ok(false),
+fn parse_bool(value: &[u8]) -> Result<bool> {
+    match value {
+        b"True" => Ok(true),
+        b"False" => Ok(false),
         _ => Err(Error::Bool),
     }
 }
@@ -150,13 +202,13 @@ fn parse_metadata<R: BufRead>(
     if version >= Version(1, 6, 0, 0) {
         parse_children(reader, buf, |reader, tag| {
             if tag.name() == b"Run" {
-                attribute(&tag, b"id", |t| metadata.set_run_id(t))?;
+                type_hint(attribute(&tag, b"id", |t| metadata.set_run_id(t)))?;
                 end_tag(reader, tag.into_buf())
             } else if tag.name() == b"Platform" {
-                attribute_err(&tag, b"usesEmulator", |t| {
-                    metadata.set_emulator_usage(parse_bool(t)?);
+                type_hint(attribute_err(&tag, b"usesEmulator", |t| {
+                    metadata.set_emulator_usage(parse_bool(t.as_bytes())?);
                     Ok(())
-                })?;
+                }))?;
                 text(reader, tag.into_buf(), |t| metadata.set_platform_name(t))
             } else if tag.name() == b"Region" {
                 text(reader, tag.into_buf(), |t| metadata.set_region_name(t))
@@ -164,12 +216,12 @@ fn parse_metadata<R: BufRead>(
                 parse_children(reader, tag.into_buf(), |reader, tag| {
                     let mut name = String::new();
                     let mut value = String::new();
-                    attribute(&tag, b"name", |t| {
+                    type_hint(attribute(&tag, b"name", |t| {
                         name = t.into_owned();
-                    })?;
-                    text(reader, tag.into_buf(), |t| {
+                    }))?;
+                    type_hint(text(reader, tag.into_buf(), |t| {
                         value = t.into_owned();
-                    })?;
+                    }))?;
                     metadata.set_variable(name, value);
                     Ok(())
                 })
@@ -201,9 +253,9 @@ fn parse_segment<R: BufRead>(
                 parse_children(reader, tag.into_buf(), |reader, tag| {
                     if tag.name() == b"SplitTime" {
                         let mut comparison = String::new();
-                        attribute(&tag, b"name", |t| {
+                        type_hint(attribute(&tag, b"name", |t| {
                             comparison = t.into_owned();
-                        })?;
+                        }))?;
                         if version >= Version(1, 4, 1, 0) {
                             time(reader, tag.into_buf(), |t| {
                                 *segment.comparison_mut(&comparison) = t;
@@ -247,10 +299,10 @@ fn parse_segment<R: BufRead>(
         } else if tag.name() == b"SegmentHistory" {
             parse_children(reader, tag.into_buf(), |reader, tag| {
                 let mut index = 0;
-                attribute_err(&tag, b"id", |t| {
+                type_hint(attribute_err(&tag, b"id", |t| {
                     index = t.parse()?;
                     Ok(())
-                })?;
+                }))?;
                 if version >= Version(1, 4, 1, 0) {
                     time(reader, tag.into_buf(), |t| {
                         segment.segment_history_mut().insert(index, t);
@@ -280,10 +332,10 @@ fn parse_run_history<R: BufRead>(
     } else if version >= Version(1, 4, 1, 0) {
         parse_children(reader, buf, |reader, tag| {
             let mut index = 0;
-            attribute_err(&tag, b"id", |t| {
+            type_hint(attribute_err(&tag, b"id", |t| {
                 index = t.parse()?;
                 Ok(())
-            })?;
+            }))?;
             time(reader, tag.into_buf(), |time| {
                 run.add_attempt_with_index(time, index, None, None, None);
             })
@@ -291,10 +343,10 @@ fn parse_run_history<R: BufRead>(
     } else {
         parse_children(reader, buf, |reader, tag| {
             let mut index = 0;
-            attribute_err(&tag, b"id", |t| {
+            type_hint(attribute_err(&tag, b"id", |t| {
                 index = t.parse()?;
                 Ok(())
-            })?;
+            }))?;
             time_old(reader, tag.into_buf(), |time| {
                 run.add_attempt_with_index(time, index, None, None, None);
             })
@@ -316,22 +368,22 @@ fn parse_attempt_history<R: BufRead>(
             let (mut started, mut started_synced) = (None, false);
             let (mut ended, mut ended_synced) = (None, false);
 
-            parse_attributes(&tag, |k, v| {
+            type_hint(parse_attributes(&tag, |k, v| {
                 if k == b"id" {
-                    index = Some(v.get()?.parse()?);
+                    index = Some(v.get::<Error>()?.parse()?);
                 } else if k == b"started" {
-                    started = Some(parse_date_time(v.get()?)?);
+                    started = Some(parse_date_time(v.get::<Error>()?)?);
                 } else if k == b"isStartedSynced" {
-                    started_synced = parse_bool(v.get()?)?;
+                    started_synced = parse_bool(v.get_raw())?;
                 } else if k == b"ended" {
-                    ended = Some(parse_date_time(v.get()?)?);
+                    ended = Some(parse_date_time(v.get::<Error>()?)?);
                 } else if k == b"isEndedSynced" {
-                    ended_synced = parse_bool(v.get()?)?;
+                    ended_synced = parse_bool(v.get_raw())?;
                 }
                 Ok(true)
-            })?;
+            }))?;
 
-            let index = index.ok_or(Error::AttributeNotFound)?;
+            let index = index.ok_or(Error::Xml(XmlError::AttributeNotFound))?;
 
             parse_children(reader, tag.into_buf(), |reader, tag| {
                 if tag.name() == b"RealTime" {
@@ -386,10 +438,10 @@ pub fn parse<R: BufRead>(source: R, path: Option<PathBuf>) -> Result<Run> {
 
     parse_base(reader, &mut buf, b"Run", |reader, tag| {
         let mut version = Version(1, 0, 0, 0);
-        optional_attribute_err(&tag, b"version", |t| {
+        type_hint(optional_attribute_err(&tag, b"version", |t| {
             version = parse_version(t)?;
             Ok(())
-        })?;
+        }))?;
 
         parse_children(reader, tag.into_buf(), |reader, tag| {
             if tag.name() == b"GameIcon" {
@@ -427,7 +479,7 @@ pub fn parse<R: BufRead>(source: R, path: Option<PathBuf>) -> Result<Run> {
                 })
             } else if tag.name() == b"AutoSplitterSettings" {
                 let settings = run.auto_splitter_settings_mut();
-                reencode_children(reader, tag.into_buf(), settings)
+                reencode_children(reader, tag.into_buf(), settings).map_err(Into::into)
             } else {
                 end_tag(reader, tag.into_buf())
             }
@@ -435,7 +487,7 @@ pub fn parse<R: BufRead>(source: R, path: Option<PathBuf>) -> Result<Run> {
     })?;
 
     if required_flags != (1 << 6) - 1 {
-        return Err(Error::ElementNotFound);
+        return Err(Error::Xml(XmlError::ElementNotFound));
     }
 
     run.set_path(path);
diff --git a/src/run/parser/llanfair2.rs b/src/run/parser/llanfair2.rs
index cf25d54..93de6b4 100644
--- a/src/run/parser/llanfair2.rs
+++ b/src/run/parser/llanfair2.rs
@@ -1,6 +1,6 @@
 //! Provides the parser for splits files used by the Llanfair Rewrite.
 
-use super::xml_util::{
+use crate::xml_util::{
     end_tag, parse_base, parse_children, single_child, text, text_err, text_parsed,
 };
 use crate::{RealTime, Run, Segment, Time, TimeSpan};
@@ -10,7 +10,42 @@ use quick_xml::Reader;
 use std::cmp::min;
 use std::io::BufRead;
 
-pub use super::xml_util::{Error, Result};
+use crate::xml_util::Error as XmlError;
+
+quick_error! {
+    /// The Error type for splits files that couldn't be parsed by the Llanfair
+    /// Rewrite Parser.
+    #[derive(Debug)]
+    pub enum Error {
+        /// The underlying XML format couldn't be parsed.
+        Xml(err: XmlError) {
+            from()
+        }
+        /// Failed to decode a string slice as UTF-8.
+        Utf8Str(err: std::str::Utf8Error) {
+            from()
+        }
+        /// Failed to decode a string as UTF-8.
+        Utf8String(err: std::string::FromUtf8Error) {
+            from()
+        }
+        /// Failed to parse an integer.
+        Int(err: std::num::ParseIntError) {
+            from()
+        }
+        /// Failed to parse an image.
+        Image {}
+    }
+}
+
+/// The Result type for the Llanfair Rewrite Parser.
+pub type Result<T> = std::result::Result<T, Error>;
+
+// FIXME: Generalized Type Ascription (GTA 6)
+#[inline]
+fn type_hint<T>(v: Result<T>) -> Result<T> {
+    v
+}
 
 fn time_span<R, F>(reader: &mut Reader<R>, buf: &mut Vec<u8>, mut f: F) -> Result<()>
 where
@@ -47,50 +82,55 @@ where
     let (mut width, mut height) = (None, None);
     image_buf.clear();
 
-    single_child(reader, buf, b"javax.swing.ImageIcon", |reader, tag| {
-        parse_children(reader, tag.into_buf(), |reader, tag| {
-            if tag.name() == b"default" {
-                parse_children(reader, tag.into_buf(), |reader, tag| {
-                    if tag.name() == b"height" {
-                        text_parsed(reader, tag.into_buf(), |t: u32| height = Some(t))
-                    } else if tag.name() == b"width" {
-                        text_parsed(reader, tag.into_buf(), |t: u32| width = Some(t))
-                    } else {
-                        end_tag(reader, tag.into_buf())
+    type_hint(single_child(
+        reader,
+        buf,
+        b"javax.swing.ImageIcon",
+        |reader, tag| {
+            parse_children(reader, tag.into_buf(), |reader, tag| {
+                if tag.name() == b"default" {
+                    parse_children(reader, tag.into_buf(), |reader, tag| {
+                        if tag.name() == b"height" {
+                            text_parsed(reader, tag.into_buf(), |t: u32| height = Some(t))
+                        } else if tag.name() == b"width" {
+                            text_parsed(reader, tag.into_buf(), |t: u32| width = Some(t))
+                        } else {
+                            end_tag(reader, tag.into_buf())
+                        }
+                    })
+                } else if tag.name() == b"int-array" {
+                    image_buf.clear();
+                    if let (Some(width), Some(height)) = (width, height) {
+                        let len = width as usize * height as usize * 4;
+                        image_buf.reserve(min(len, 32 << 20));
                     }
-                })
-            } else if tag.name() == b"int-array" {
-                image_buf.clear();
-                if let (Some(width), Some(height)) = (width, height) {
-                    let len = width as usize * height as usize * 4;
-                    image_buf.reserve(min(len, 32 << 20));
-                }
 
-                let mut tmp = [0; 4];
+                    let mut tmp = [0; 4];
 
-                parse_children(reader, tag.into_buf(), |reader, tag| {
-                    text_parsed(reader, tag.into_buf(), |value: i32| {
-                        BE::write_i32(&mut tmp, value);
-                        image_buf.extend_from_slice(&[tmp[1], tmp[2], tmp[3], tmp[0]]);
+                    parse_children(reader, tag.into_buf(), |reader, tag| {
+                        text_parsed(reader, tag.into_buf(), |value: i32| {
+                            BE::write_i32(&mut tmp, value);
+                            image_buf.extend_from_slice(&[tmp[1], tmp[2], tmp[3], tmp[0]]);
+                        })
                     })
-                })
-            } else {
-                end_tag(reader, tag.into_buf())
-            }
-        })
-    })?;
+                } else {
+                    end_tag(reader, tag.into_buf())
+                }
+            })
+        },
+    ))?;
 
-    let height = height.ok_or(Error::ElementNotFound)?;
-    let width = width.ok_or(Error::ElementNotFound)?;
+    let height = height.ok_or(Error::Image)?;
+    let width = width.ok_or(Error::Image)?;
 
     let image_buf = image_buf.as_slice();
-    let image = ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, image_buf)
-        .ok_or(Error::ElementNotFound)?;
+    let image =
+        ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, image_buf).ok_or(Error::Image)?;
 
     buf.clear();
     png::PNGEncoder::new(&mut *buf)
         .encode(image.as_ref(), width, height, ColorType::RGBA(8))
-        .map_err(|_| Error::ElementNotFound)?;
+        .map_err(|_| Error::Image)?;
 
     f(buf);
 
@@ -134,7 +174,7 @@ pub fn parse<R: BufRead>(source: R) -> Result<Run> {
 
     let mut run = Run::new();
 
-    parse_base(reader, &mut buf, b"run", |reader, tag| {
+    type_hint(parse_base(reader, &mut buf, b"run", |reader, tag| {
         parse_children(reader, tag.into_buf(), |reader, tag| {
             if tag.name() == b"game" {
                 text(reader, tag.into_buf(), |t| run.set_game_name(t))
@@ -166,7 +206,7 @@ pub fn parse<R: BufRead>(source: R) -> Result<Run> {
                 end_tag(reader, tag.into_buf())
             }
         })
-    })?;
+    }))?;
 
     Ok(run)
 }
diff --git a/src/run/parser/llanfair_gered.rs b/src/run/parser/llanfair_gered.rs
index 1350b88..9bf5f8a 100644
--- a/src/run/parser/llanfair_gered.rs
+++ b/src/run/parser/llanfair_gered.rs
@@ -1,6 +1,6 @@
 //! Provides the parser for splits files used by Gered's Llanfair fork.
 
-use super::xml_util::{
+use crate::xml_util::{
     end_tag, optional_attribute_err, parse_base, parse_children, single_child, text,
     text_as_bytes_err, text_err, text_parsed,
 };
@@ -11,7 +11,44 @@ use image::{png, ColorType, ImageBuffer, Rgba};
 use quick_xml::Reader;
 use std::io::{BufRead, Cursor, Seek, SeekFrom};
 
-pub use super::xml_util::{Error, Result};
+use crate::xml_util::Error as XmlError;
+
+quick_error! {
+    /// The Error type for splits files that couldn't be parsed by the Llanfair (Gered)
+    /// Parser.
+    #[derive(Debug)]
+    pub enum Error {
+        /// The underlying XML format couldn't be parsed.
+        Xml(err: XmlError) {
+            from()
+        }
+        /// Failed to decode a string slice as UTF-8.
+        Utf8Str(err: std::str::Utf8Error) {
+            from()
+        }
+        /// Failed to decode a string as UTF-8.
+        Utf8String(err: std::string::FromUtf8Error) {
+            from()
+        }
+        /// Failed to parse an integer.
+        Int(err: std::num::ParseIntError) {
+            from()
+        }
+        /// The length of a buffer was too large.
+        LengthOutOfBounds {}
+        /// Failed to parse an image.
+        Image {}
+    }
+}
+
+/// The Result type for the Llanfair (Gered) Parser.
+pub type Result<T> = std::result::Result<T, Error>;
+
+// FIXME: Generalized Type Ascription (GTA 6)
+#[inline]
+fn type_hint<T>(v: Result<T>) -> Result<T> {
+    v
+}
 
 fn time_span<R, F>(reader: &mut Reader<R>, buf: &mut Vec<u8>, f: F) -> Result<()>
 where
@@ -47,14 +84,16 @@ where
         let tag_buf = tag.into_buf();
         let (width, height, image) = text_as_bytes_err(reader, tag_buf, |t| {
             buf.clear();
-            base64::decode_config_buf(&t, STANDARD, buf).map_err(|_| Error::ElementNotFound)?;
+            base64::decode_config_buf(&t, STANDARD, buf).map_err(|_| Error::Image)?;
 
             let (width, height);
             {
                 let mut cursor = Cursor::new(&buf);
-                cursor.seek(SeekFrom::Current(0xD1))?;
-                height = cursor.read_u32::<BE>()?;
-                width = cursor.read_u32::<BE>()?;
+                cursor
+                    .seek(SeekFrom::Current(0xD1))
+                    .map_err(|_| Error::Image)?;
+                height = cursor.read_u32::<BE>().map_err(|_| Error::Image)?;
+                width = cursor.read_u32::<BE>().map_err(|_| Error::Image)?;
             }
 
             let len = (width as usize)
@@ -63,12 +102,12 @@ where
                 .ok_or(Error::LengthOutOfBounds)?;
 
             if buf.len() < 0xFE + len {
-                return Err(Error::ElementNotFound);
+                return Err(Error::Image);
             }
 
             let buf = &buf[0xFE..][..len];
-            let image = ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, buf)
-                .ok_or(Error::ElementNotFound)?;
+            let image =
+                ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, buf).ok_or(Error::Image)?;
 
             Ok((width, height, image))
         })?;
@@ -76,7 +115,7 @@ where
         tag_buf.clear();
         png::PNGEncoder::new(&mut *tag_buf)
             .encode(image.as_ref(), width, height, ColorType::RGBA(8))
-            .map_err(|_| Error::ElementNotFound)?;
+            .map_err(|_| Error::Image)?;
 
         f(tag_buf);
 
@@ -108,12 +147,12 @@ where
                         })
                     })
                 } else if tag.name() == b"runTime" {
-                    optional_attribute_err(&tag, b"reference", |reference| {
+                    type_hint(optional_attribute_err(&tag, b"reference", |reference| {
                         if reference == "../bestTime" {
                             defer_setting_run_time = true;
                         }
                         Ok(())
-                    })?;
+                    }))?;
                     if !defer_setting_run_time {
                         single_child(reader, tag.into_buf(), b"milliseconds", |reader, tag| {
                             time_span(reader, tag.into_buf(), |t| {
@@ -138,7 +177,7 @@ where
                 *total_time += segment
                     .best_segment_time()
                     .real_time
-                    .ok_or(Error::ElementNotFound)?;
+                    .ok_or(Error::Xml(XmlError::ElementNotFound))?;
                 segment.set_personal_best_split_time(RealTime(Some(*total_time)).into());
             }
 
diff --git a/src/run/parser/mod.rs b/src/run/parser/mod.rs
index 3f4846f..1df292a 100644
--- a/src/run/parser/mod.rs
+++ b/src/run/parser/mod.rs
@@ -50,6 +50,5 @@ pub mod worstrun;
 pub mod wsplit;
 
 mod timer_kind;
-mod xml_util;
 
 pub use self::timer_kind::TimerKind;
diff --git a/src/settings/color.rs b/src/settings/color.rs
index 394e667..e79b0b5 100644
--- a/src/settings/color.rs
+++ b/src/settings/color.rs
@@ -1,4 +1,4 @@
-use palette::{Hsla, LinSrgba};
+use palette::{Hsla, LinSrgba, Pixel};
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
 
 /// Colors can be used to describe what color to use for visualizing
@@ -17,6 +17,16 @@ impl Color {
         (0.0, 0.0, 0.0, 0.0).into()
     }
 
+    /// Creates a new white color.
+    pub fn white() -> Self {
+        (1.0, 1.0, 1.0, 1.0).into()
+    }
+
+    /// Creates a new black color.
+    pub fn black() -> Self {
+        (0.0, 0.0, 0.0, 1.0).into()
+    }
+
     /// Creates a new color by providing the Hue, Saturation, Lightness and
     /// Alpha (HSLA) for it.
     pub fn hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self {
@@ -34,6 +44,14 @@ impl From<[f32; 4]> for Color {
     }
 }
 
+impl From<[u8; 4]> for Color {
+    fn from(rgba: [u8; 4]) -> Self {
+        Self {
+            rgba: LinSrgba::from_raw(&rgba).into_format(),
+        }
+    }
+}
+
 impl From<(f32, f32, f32, f32)> for Color {
     fn from(rgba: (f32, f32, f32, f32)) -> Self {
         Self {
diff --git a/src/timing/timer/mod.rs b/src/timing/timer/mod.rs
index 84c01bd..db6b9fc 100644
--- a/src/timing/timer/mod.rs
+++ b/src/timing/timer/mod.rs
@@ -248,7 +248,7 @@ impl Timer {
             self.deinitialize_game_time();
             self.run.start_next_run();
 
-            // TODO: OnStart
+            // FIXME: OnStart
         }
     }
 
@@ -271,7 +271,7 @@ impl Timer {
             }
             self.run.mark_as_modified();
 
-            // TODO: OnSplit
+            // FIXME: OnSplit
         }
     }
 
@@ -295,7 +295,7 @@ impl Timer {
             self.current_split_index = self.current_split_index.map(|i| i + 1);
             self.run.mark_as_modified();
 
-            // TODO: OnSkipSplit
+            // FIXME: OnSkipSplit
         }
     }
 
@@ -311,7 +311,7 @@ impl Timer {
             self.current_split_mut().unwrap().clear_split_time();
             self.run.mark_as_modified();
 
-            // TODO: OnUndoSplit
+            // FIXME: OnUndoSplit
         }
     }
 
@@ -361,7 +361,7 @@ impl Timer {
             segment.clear_split_time();
         }
 
-        // TODO: OnReset
+        // FIXME: OnReset
 
         self.run.fix_splits();
         self.run.regenerate_comparisons();
@@ -373,7 +373,7 @@ impl Timer {
             self.time_paused_at = self.current_time().real_time.unwrap();
             self.phase = Paused;
 
-            // TODO: OnPause
+            // FIXME: OnPause
         }
     }
 
@@ -383,7 +383,7 @@ impl Timer {
             self.adjusted_start_time = TimeStamp::now() - self.time_paused_at;
             self.phase = Running;
 
-            // TODO: OnResume
+            // FIXME: OnResume
         }
     }
 
@@ -440,7 +440,7 @@ impl Timer {
 
         self.adjusted_start_time = self.start_time_with_offset;
 
-        // TODO: OnUndoAllPauses
+        // FIXME: OnUndoAllPauses
     }
 
     /// Switches the current comparison to the next comparison in the list.
@@ -453,7 +453,7 @@ impl Timer {
         let index = (index + 1) % len;
         self.current_comparison = self.run.comparisons().nth(index).unwrap().to_owned();
 
-        // TODO: OnNextComparison
+        // FIXME: OnNextComparison
     }
 
     /// Switches the current comparison to the previous comparison in the list.
@@ -466,7 +466,7 @@ impl Timer {
         let index = (index + len - 1) % len;
         self.current_comparison = self.run.comparisons().nth(index).unwrap().to_owned();
 
-        // TODO: OnPreviousComparison
+        // FIXME: OnPreviousComparison
     }
 
     /// Returns the total duration of the current attempt. This is not affected
diff --git a/src/run/parser/xml_util.rs b/src/xml_util.rs
similarity index 61%
rename from src/run/parser/xml_util.rs
rename to src/xml_util.rs
index 0b5e29b..394542d 100644
--- a/src/run/parser/xml_util.rs
+++ b/src/xml_util.rs
@@ -1,4 +1,3 @@
-use super::super::ComparisonError;
 use crate::timing;
 use chrono::ParseError as ChronoError;
 use quick_xml::events::{attributes, BytesStart, Event};
@@ -7,7 +6,6 @@ use std::borrow::Cow;
 use std::io::{self, BufRead};
 use std::num::{ParseFloatError, ParseIntError};
 use std::ops::Deref;
-use std::result::Result as StdResult;
 use std::{str, string};
 
 quick_error! {
@@ -34,10 +32,6 @@ quick_error! {
         ElementNotFound {}
         /// The length of a buffer was too large.
         LengthOutOfBounds {}
-        /// Parsed comparison has an invalid name.
-        InvalidComparisonName(err: ComparisonError) {
-            from()
-        }
         /// Failed to decode a string slice as UTF-8.
         Utf8Str(err: str::Utf8Error) {
             from()
@@ -66,7 +60,7 @@ quick_error! {
 }
 
 /// The Result type for Parsers that parse XML-based splits files.
-pub type Result<T> = StdResult<T, Error>;
+// pub type Result<T> = StdResult<T, Error>;
 
 pub struct Tag<'a>(BytesStart<'a>, *mut Vec<u8>);
 
@@ -91,35 +85,46 @@ impl<'a> Tag<'a> {
 pub struct AttributeValue<'a>(&'a attributes::Attribute<'a>);
 
 impl<'a> AttributeValue<'a> {
-    pub fn get(self) -> Result<Cow<'a, str>> {
-        decode_cow_text(self.0.unescaped_value()?)
+    pub fn get<E>(self) -> Result<Cow<'a, str>, E>
+    where
+        E: From<Error> + From<string::FromUtf8Error> + From<str::Utf8Error>,
+    {
+        decode_cow_text(self.0.unescaped_value().map_err(Error::Xml)?)
+    }
+
+    pub fn get_raw(&self) -> &'a [u8] {
+        &self.0.value
     }
 }
 
-fn decode_cow_text(cow: Cow<'_, [u8]>) -> Result<Cow<'_, str>> {
+fn decode_cow_text<E>(cow: Cow<'_, [u8]>) -> Result<Cow<'_, str>, E>
+where
+    E: From<string::FromUtf8Error> + From<str::Utf8Error>,
+{
     Ok(match cow {
         Cow::Borrowed(b) => Cow::Borrowed(str::from_utf8(b)?),
         Cow::Owned(o) => Cow::Owned(String::from_utf8(o)?),
     })
 }
 
-pub fn text_parsed<R, F, T>(reader: &mut Reader<R>, buf: &mut Vec<u8>, f: F) -> Result<()>
+pub fn text_parsed<R, F, T, E>(reader: &mut Reader<R>, buf: &mut Vec<u8>, f: F) -> Result<(), E>
 where
     R: BufRead,
     F: FnOnce(T),
     T: str::FromStr,
-    <T as str::FromStr>::Err: Into<Error>,
+    E: From<Error> + From<string::FromUtf8Error> + From<str::Utf8Error> + From<T::Err>,
 {
     text_err(reader, buf, |t| {
-        f(t.parse().map_err(Into::into)?);
+        f(t.parse()?);
         Ok(())
     })
 }
 
-pub fn text<R, F>(reader: &mut Reader<R>, buf: &mut Vec<u8>, f: F) -> Result<()>
+pub fn text<R, F, E>(reader: &mut Reader<R>, buf: &mut Vec<u8>, f: F) -> Result<(), E>
 where
     R: BufRead,
     F: FnOnce(Cow<'_, str>),
+    E: From<Error> + From<string::FromUtf8Error> + From<str::Utf8Error>,
 {
     text_err(reader, buf, |t| {
         f(t);
@@ -127,47 +132,83 @@ where
     })
 }
 
-pub fn text_err<R, F>(reader: &mut Reader<R>, buf: &mut Vec<u8>, f: F) -> Result<()>
+pub fn text_err<R, F, E>(reader: &mut Reader<R>, buf: &mut Vec<u8>, f: F) -> Result<(), E>
 where
     R: BufRead,
-    F: FnOnce(Cow<'_, str>) -> Result<()>,
+    F: FnOnce(Cow<'_, str>) -> Result<(), E>,
+    E: From<Error> + From<string::FromUtf8Error> + From<str::Utf8Error>,
 {
-    text_as_bytes_err(reader, buf, |b| f(decode_cow_text(b)?))
+    text_as_bytes_err(reader, buf, |b| f(decode_cow_text::<E>(b)?))
 }
 
-pub fn text_as_bytes_err<R, F, T>(reader: &mut Reader<R>, buf: &mut Vec<u8>, f: F) -> Result<T>
+pub fn text_as_escaped_bytes_err<R, F, T, E>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    f: F,
+) -> Result<T, E>
 where
     R: BufRead,
-    F: FnOnce(Cow<'_, [u8]>) -> Result<T>,
+    F: FnOnce(&[u8]) -> Result<T, E>,
+    E: From<Error>,
 {
-    let val;
     loop {
         buf.clear();
-        match reader.read_event(buf)? {
-            Event::Start(_) => return Err(Error::UnexpectedElement),
+        match reader.read_event(buf).map_err(Error::Xml)? {
+            Event::Start(_) => return Err(Error::UnexpectedElement).map_err(Into::into),
+            Event::End(_) => {
+                return f(&[]);
+            }
+            Event::Text(text) | Event::CData(text) => {
+                let val = f(&text)?;
+                end_tag_immediately(reader, buf)?;
+                return Ok(val);
+            }
+            Event::Eof => return Err(Error::UnexpectedEndOfFile).map_err(Into::into),
+            _ => {}
+        }
+    }
+}
+
+pub fn text_as_bytes_err<R, F, T, E>(
+    reader: &mut Reader<R>,
+    buf: &mut Vec<u8>,
+    f: F,
+) -> Result<T, E>
+where
+    R: BufRead,
+    F: FnOnce(Cow<'_, [u8]>) -> Result<T, E>,
+    E: From<Error>,
+{
+    loop {
+        buf.clear();
+        match reader.read_event(buf).map_err(Error::Xml)? {
+            Event::Start(_) => return Err(Error::UnexpectedElement).map_err(Into::into),
             Event::End(_) => {
                 return f(Cow::Borrowed(&[]));
             }
             Event::Text(text) | Event::CData(text) => {
-                let text = text.unescaped()?;
-                val = f(text)?;
-                break;
+                let text = text.unescaped().map_err(Into::into)?;
+                let val = f(text)?;
+                end_tag_immediately(reader, buf)?;
+                return Ok(val);
             }
-            Event::Eof => return Err(Error::UnexpectedEndOfFile),
+            Event::Eof => return Err(Error::UnexpectedEndOfFile).map_err(Into::into),
             _ => {}
         }
     }
-    end_tag_immediately(reader, buf)?;
-    Ok(val)
 }
 
-fn end_tag_immediately<R: BufRead>(reader: &mut Reader<R>, buf: &mut Vec<u8>) -> Result<()> {
+fn end_tag_immediately<R, E>(reader: &mut Reader<R>, buf: &mut Vec<u8>) -> Result<(), E>
+where
+    R: BufRead,
+    E: From<Error>,
+{
     loop {
         buf.clear();
-        match reader.read_event(buf)? {
-            Event::Start(_) => return Err(Error::UnexpectedElement),
+        match reader.read_event(buf).map_err(Error::Xml)? {
+            Event::Start(_) => return Err(Error::UnexpectedElement).map_err(Into::into),
             Event::End(_) => return Ok(()),
-            Event::Eof => return Err(Error::UnexpectedEndOfFile),
+            Event::Eof => return Err(Error::UnexpectedEndOfFile).map_err(Into::into),
             _ => {}
         }
     }
@@ -177,7 +218,7 @@ pub fn reencode_children<R>(
     reader: &mut Reader<R>,
     buf: &mut Vec<u8>,
     target_buf: &mut Vec<u8>,
-) -> Result<()>
+) -> Result<(), Error>
 where
     R: BufRead,
 {
@@ -217,11 +258,15 @@ where
     }
 }
 
-pub fn end_tag<R: BufRead>(reader: &mut Reader<R>, buf: &mut Vec<u8>) -> Result<()> {
+pub fn end_tag<R, E>(reader: &mut Reader<R>, buf: &mut Vec<u8>) -> Result<(), E>
+where
+    R: BufRead,
+    E: From<Error>,
+{
     let mut depth = 0;
     loop {
         buf.clear();
-        match reader.read_event(buf)? {
+        match reader.read_event(buf).map_err(Error::Xml)? {
             Event::Start(_) => {
                 depth += 1;
             }
@@ -231,24 +276,25 @@ pub fn end_tag<R: BufRead>(reader: &mut Reader<R>, buf: &mut Vec<u8>) -> Result<
                 }
                 depth -= 1;
             }
-            Event::Eof => return Err(Error::UnexpectedEndOfFile),
+            Event::Eof => return Err(Error::UnexpectedEndOfFile).map_err(Into::into),
             _ => {}
         }
     }
 }
 
-pub fn single_child<R, F, T>(
+pub fn single_child<R, F, T, E>(
     reader: &mut Reader<R>,
     buf: &mut Vec<u8>,
     tag: &[u8],
     mut f: F,
-) -> Result<T>
+) -> Result<T, E>
 where
     R: BufRead,
-    F: FnMut(&mut Reader<R>, Tag<'_>) -> Result<T>,
+    F: FnMut(&mut Reader<R>, Tag<'_>) -> Result<T, E>,
+    E: From<Error>,
 {
     let mut val = None;
-    parse_children(reader, buf, |reader, t| {
+    parse_children::<_, _, E>(reader, buf, |reader, t| {
         if t.name() == tag && val.is_none() {
             val = Some(f(reader, t)?);
             Ok(())
@@ -256,67 +302,70 @@ where
             end_tag(reader, t.into_buf())
         }
     })?;
-    val.ok_or(Error::ElementNotFound)
+    val.ok_or(Error::ElementNotFound).map_err(Into::into)
 }
 
-pub fn parse_children<R, F>(reader: &mut Reader<R>, buf: &mut Vec<u8>, mut f: F) -> Result<()>
+pub fn parse_children<R, F, E>(reader: &mut Reader<R>, buf: &mut Vec<u8>, mut f: F) -> Result<(), E>
 where
     R: BufRead,
-    F: FnMut(&mut Reader<R>, Tag<'_>) -> Result<()>,
+    F: FnMut(&mut Reader<R>, Tag<'_>) -> Result<(), E>,
+    E: From<Error>,
 {
     unsafe {
         let ptr_buf: *mut Vec<u8> = buf;
         loop {
             buf.clear();
-            match reader.read_event(buf)? {
+            match reader.read_event(buf).map_err(Error::Xml)? {
                 Event::Start(start) => {
                     let tag = Tag::new(start, ptr_buf);
                     f(reader, tag)?;
                 }
                 Event::End(_) => return Ok(()),
-                Event::Eof => return Err(Error::UnexpectedEndOfFile),
+                Event::Eof => return Err(Error::UnexpectedEndOfFile).map_err(Into::into),
                 _ => {}
             }
         }
     }
 }
 
-pub fn parse_base<R, F>(
+pub fn parse_base<R, F, E>(
     reader: &mut Reader<R>,
     buf: &mut Vec<u8>,
     tag: &[u8],
     mut f: F,
-) -> Result<()>
+) -> Result<(), E>
 where
     R: BufRead,
-    F: FnMut(&mut Reader<R>, Tag<'_>) -> Result<()>,
+    F: FnMut(&mut Reader<R>, Tag<'_>) -> Result<(), E>,
+    E: From<Error>,
 {
     unsafe {
         let ptr_buf: *mut Vec<u8> = buf;
         loop {
             buf.clear();
-            match reader.read_event(buf)? {
+            match reader.read_event(buf).map_err(Error::Xml)? {
                 Event::Start(start) => {
                     if start.name() == tag {
                         let tag = Tag::new(start, ptr_buf);
                         return f(reader, tag);
                     } else {
-                        return Err(Error::ElementNotFound);
+                        return Err(Error::ElementNotFound).map_err(Into::into);
                     }
                 }
-                Event::Eof => return Err(Error::UnexpectedEndOfFile),
+                Event::Eof => return Err(Error::UnexpectedEndOfFile).map_err(Into::into),
                 _ => {}
             }
         }
     }
 }
 
-pub fn parse_attributes<'a, F>(tag: &BytesStart<'a>, mut f: F) -> Result<()>
+pub fn parse_attributes<F, E>(tag: &BytesStart<'_>, mut f: F) -> Result<(), E>
 where
-    F: FnMut(&[u8], AttributeValue<'_>) -> Result<bool>,
+    F: FnMut(&[u8], AttributeValue<'_>) -> Result<bool, E>,
+    E: From<Error>,
 {
     for attribute in tag.attributes() {
-        let attribute = attribute?;
+        let attribute = attribute.map_err(Error::Xml)?;
         let key = attribute.key;
         if !f(key, AttributeValue(&attribute))? {
             return Ok(());
@@ -325,9 +374,10 @@ where
     Ok(())
 }
 
-pub fn optional_attribute_err<'a, F>(tag: &BytesStart<'a>, key: &[u8], mut f: F) -> Result<()>
+pub fn optional_attribute_err<F, E>(tag: &BytesStart<'_>, key: &[u8], mut f: F) -> Result<(), E>
 where
-    F: FnMut(Cow<'_, str>) -> Result<()>,
+    F: FnMut(Cow<'_, str>) -> Result<(), E>,
+    E: From<Error>,
 {
     parse_attributes(tag, |k, v| {
         if k == key {
@@ -339,12 +389,13 @@ where
     })
 }
 
-pub fn attribute_err<'a, F>(tag: &BytesStart<'a>, key: &[u8], mut f: F) -> Result<()>
+pub fn attribute_err<F, E>(tag: &BytesStart<'_>, key: &[u8], mut f: F) -> Result<(), E>
 where
-    F: FnMut(Cow<'_, str>) -> Result<()>,
+    F: FnMut(Cow<'_, str>) -> Result<(), E>,
+    E: From<Error>,
 {
     let mut called = false;
-    parse_attributes(tag, |k, v| {
+    parse_attributes::<_, E>(tag, |k, v| {
         if k == key {
             f(v.get()?)?;
             called = true;
@@ -356,13 +407,14 @@ where
     if called {
         Ok(())
     } else {
-        Err(Error::AttributeNotFound)
+        Err(Error::AttributeNotFound).map_err(Into::into)
     }
 }
 
-pub fn attribute<'a, F>(tag: &BytesStart<'a>, key: &[u8], mut f: F) -> Result<()>
+pub fn attribute<F, E>(tag: &BytesStart<'_>, key: &[u8], mut f: F) -> Result<(), E>
 where
     F: FnMut(Cow<'_, str>),
+    E: From<Error>,
 {
     attribute_err(tag, key, |t| {
         f(t);


Test patch:
diff --git a/src/run/editor/tests/mark_as_modified.rs b/src/run/editor/tests/mark_as_modified.rs
index 9630314..326f2b1 100644
--- a/src/run/editor/tests/mark_as_modified.rs
+++ b/src/run/editor/tests/mark_as_modified.rs
@@ -307,4 +307,4 @@ fn not_when_cleaning_sum_of_best_without_applying_a_fix() {
     assert!(!editor.run().has_been_modified());
 }
 
-// TODO: Cleaning Sum of Best
+// FIXME: Cleaning Sum of Best
diff --git a/tests/layout_files/All.lsl b/tests/layout_files/All.lsl
new file mode 100644
index 0000000..d602749
--- /dev/null
+++ b/tests/layout_files/All.lsl
@@ -0,0 +1,359 @@
+﻿<?xml version="1.0" encoding="UTF-8"?>
+<Layout version="1.6.1">
+  <Mode>Vertical</Mode>
+  <X>1318</X>
+  <Y>58</Y>
+  <VerticalWidth>286</VerticalWidth>
+  <VerticalHeight>931</VerticalHeight>
+  <HorizontalWidth>-1</HorizontalWidth>
+  <HorizontalHeight>-1</HorizontalHeight>
+  <Settings>
+    <TextColor>FFFFFFFF</TextColor>
+    <BackgroundColor>FF0F0F0F</BackgroundColor>
+    <BackgroundColor2>00000000</BackgroundColor2>
+    <ThinSeparatorsColor>03FFFFFF</ThinSeparatorsColor>
+    <SeparatorsColor>24FFFFFF</SeparatorsColor>
+    <PersonalBestColor>FF16A6FF</PersonalBestColor>
+    <AheadGainingTimeColor>FF00CC36</AheadGainingTimeColor>
+    <AheadLosingTimeColor>FF52CC73</AheadLosingTimeColor>
+    <BehindGainingTimeColor>FFCC5C52</BehindGainingTimeColor>
+    <BehindLosingTimeColor>FFCC1200</BehindLosingTimeColor>
+    <BestSegmentColor>FFD8AF1F</BestSegmentColor>
+    <UseRainbowColor>False</UseRainbowColor>
+    <NotRunningColor>FFACACAC</NotRunningColor>
+    <PausedColor>FF7A7A7A</PausedColor>
+    <TextOutlineColor>00000000</TextOutlineColor>
+    <ShadowsColor>80000000</ShadowsColor>
+    <TimesFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAQEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAABAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></TimesFont>
+    <TimerFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAdDYWxpYnJpAAAvQgX8////GFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQEAAAAHdmFsdWVfXwAIAgAAAAEAAAAF+////xtTeXN0ZW0uRHJhd2luZy5HcmFwaGljc1VuaXQBAAAAB3ZhbHVlX18ACAIAAAACAAAACw==]]></TimerFont>
+    <TextFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAQEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></TextFont>
+    <AlwaysOnTop>True</AlwaysOnTop>
+    <ShowBestSegments>True</ShowBestSegments>
+    <AntiAliasing>True</AntiAliasing>
+    <DropShadows>True</DropShadows>
+    <BackgroundType>SolidColor</BackgroundType>
+    <BackgroundImage />
+    <ImageOpacity>1</ImageOpacity>
+    <ImageBlur>0</ImageBlur>
+    <Opacity>1</Opacity>
+  </Settings>
+  <Components>
+    <Component>
+      <Path>LiveSplit.BlankSpace.dll</Path>
+      <Settings>
+        <Version>1.7</Version>
+        <SpaceHeight>100</SpaceHeight>
+        <SpaceWidth>100</SpaceWidth>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>00FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.CurrentComparison.dll</Path>
+      <Settings>
+        <Version>1.4</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <TimeColor>FFFFFFFF</TimeColor>
+        <OverrideTimeColor>False</OverrideTimeColor>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>00FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <Font1><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAUEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAgAAAAs=]]></Font1>
+        <Font2><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAUEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAgAAAAs=]]></Font2>
+        <OverrideFont1>False</OverrideFont1>
+        <OverrideFont2>False</OverrideFont2>
+        <Display2Rows>False</Display2Rows>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.RunPrediction.dll</Path>
+      <Settings>
+        <Version>1.4</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <TimeColor>FFFFFFFF</TimeColor>
+        <OverrideTimeColor>False</OverrideTimeColor>
+        <Accuracy>Seconds</Accuracy>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>00FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <Comparison>Current Comparison</Comparison>
+        <Display2Rows>False</Display2Rows>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.Delta.dll</Path>
+      <Settings>
+        <Version>1.4</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <Accuracy>Tenths</Accuracy>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>00FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <Comparison>Current Comparison</Comparison>
+        <Display2Rows>False</Display2Rows>
+        <DropDecimals>True</DropDecimals>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.DetailedTimer.dll</Path>
+      <Settings>
+        <Version>1.5</Version>
+        <Height>75</Height>
+        <Width>200</Width>
+        <SegmentTimerSizeRatio>40</SegmentTimerSizeRatio>
+        <TimerShowGradient>True</TimerShowGradient>
+        <OverrideTimerColors>False</OverrideTimerColors>
+        <SegmentTimerShowGradient>True</SegmentTimerShowGradient>
+        <TimerFormat>1.23</TimerFormat>
+        <SegmentTimerFormat>1.23</SegmentTimerFormat>
+        <SegmentTimesAccuracy>Hundredths</SegmentTimesAccuracy>
+        <TimerColor>FFAAAAAA</TimerColor>
+        <SegmentTimerColor>FFAAAAAA</SegmentTimerColor>
+        <SegmentLabelsColor>FFFFFFFF</SegmentLabelsColor>
+        <SegmentTimesColor>FFFFFFFF</SegmentTimesColor>
+        <SegmentLabelsFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAIEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></SegmentLabelsFont>
+        <SegmentTimesFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAIEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAABAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></SegmentTimesFont>
+        <SplitNameFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAMEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></SplitNameFont>
+        <DisplayIcon>False</DisplayIcon>
+        <IconSize>40</IconSize>
+        <ShowSplitName>False</ShowSplitName>
+        <SplitNameColor>FFFFFFFF</SplitNameColor>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>00FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <Comparison>Current Comparison</Comparison>
+        <Comparison2>Best Segments</Comparison2>
+        <HideComparison>False</HideComparison>
+        <TimingMethod>Current Timing Method</TimingMethod>
+        <DecimalsSize>35</DecimalsSize>
+        <SegmentTimerDecimalsSize>35</SegmentTimerDecimalsSize>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.Graph.dll</Path>
+      <Settings>
+        <Version>1.5</Version>
+        <Height>120</Height>
+        <Width>180</Width>
+        <BehindGraphColor>FF732828</BehindGraphColor>
+        <AheadGraphColor>FF287334</AheadGraphColor>
+        <GridlinesColor>50000000</GridlinesColor>
+        <PartialFillColorBehind>19FFFFFF</PartialFillColorBehind>
+        <CompleteFillColorBehind>32FFFFFF</CompleteFillColorBehind>
+        <PartialFillColorAhead>19FFFFFF</PartialFillColorAhead>
+        <CompleteFillColorAhead>32FFFFFF</CompleteFillColorAhead>
+        <GraphColor>FFFFFFFF</GraphColor>
+        <ShadowsColor>38000000</ShadowsColor>
+        <GraphLinesColor>FFFFFFFF</GraphLinesColor>
+        <LiveGraph>True</LiveGraph>
+        <FlipGraph>False</FlipGraph>
+        <Comparison>Current Comparison</Comparison>
+        <ShowBestSegments>False</ShowBestSegments>
+        <GraphGoldColor>FFD8AF1F</GraphGoldColor>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.PossibleTimeSave.dll</Path>
+      <Settings>
+        <Version>1.5</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <TimeColor>FFFFFFFF</TimeColor>
+        <OverrideTimeColor>False</OverrideTimeColor>
+        <Accuracy>Hundredths</Accuracy>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>00FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <Comparison>Current Comparison</Comparison>
+        <Display2Rows>False</Display2Rows>
+        <TotalTimeSave>False</TotalTimeSave>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.PreviousSegment.dll</Path>
+      <Settings>
+        <Version>1.6</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <BackgroundColor>FF1C1C1C</BackgroundColor>
+        <BackgroundColor2>FF0D0D0D</BackgroundColor2>
+        <BackgroundGradient>Vertical</BackgroundGradient>
+        <DeltaAccuracy>Tenths</DeltaAccuracy>
+        <DropDecimals>True</DropDecimals>
+        <Comparison>Current Comparison</Comparison>
+        <Display2Rows>False</Display2Rows>
+        <ShowPossibleTimeSave>False</ShowPossibleTimeSave>
+        <TimeSaveAccuracy>Tenths</TimeSaveAccuracy>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>
+      </Path>
+      <Settings />
+    </Component>
+    <Component>
+      <Path>LiveSplit.Splits.dll</Path>
+      <Settings>
+        <Version>1.6</Version>
+        <CurrentSplitTopColor>FF3373F4</CurrentSplitTopColor>
+        <CurrentSplitBottomColor>FF153574</CurrentSplitBottomColor>
+        <VisualSplitCount>16</VisualSplitCount>
+        <SplitPreviewCount>1</SplitPreviewCount>
+        <DisplayIcons>True</DisplayIcons>
+        <ShowThinSeparators>True</ShowThinSeparators>
+        <AlwaysShowLastSplit>True</AlwaysShowLastSplit>
+        <SplitWidth>20</SplitWidth>
+        <SplitTimesAccuracy>Seconds</SplitTimesAccuracy>
+        <AutomaticAbbreviations>False</AutomaticAbbreviations>
+        <BeforeNamesColor>FFFFFFFF</BeforeNamesColor>
+        <CurrentNamesColor>FFFFFFFF</CurrentNamesColor>
+        <AfterNamesColor>FFFFFFFF</AfterNamesColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <BeforeTimesColor>FFFFFFFF</BeforeTimesColor>
+        <CurrentTimesColor>FFFFFFFF</CurrentTimesColor>
+        <AfterTimesColor>FFFFFFFF</AfterTimesColor>
+        <OverrideTimesColor>False</OverrideTimesColor>
+        <ShowBlankSplits>True</ShowBlankSplits>
+        <LockLastSplit>True</LockLastSplit>
+        <IconSize>24</IconSize>
+        <IconShadows>True</IconShadows>
+        <SplitHeight>3.6</SplitHeight>
+        <CurrentSplitGradient>Vertical</CurrentSplitGradient>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>01FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Alternating</BackgroundGradient>
+        <SeparatorLastSplit>True</SeparatorLastSplit>
+        <DeltasAccuracy>Tenths</DeltasAccuracy>
+        <DropDecimals>True</DropDecimals>
+        <OverrideDeltasColor>False</OverrideDeltasColor>
+        <DeltasColor>FFFFFFFF</DeltasColor>
+        <Display2Rows>False</Display2Rows>
+        <ShowColumnLabels>False</ShowColumnLabels>
+        <LabelsColor>FFFFFFFF</LabelsColor>
+        <Columns>
+          <Settings>
+            <Version>1.5</Version>
+            <Name>+/-</Name>
+            <Type>Delta</Type>
+            <Comparison>Current Comparison</Comparison>
+            <TimingMethod>Current Timing Method</TimingMethod>
+          </Settings>
+          <Settings>
+            <Version>1.5</Version>
+            <Name>Time</Name>
+            <Type>SplitTime</Type>
+            <Comparison>Current Comparison</Comparison>
+            <TimingMethod>Current Timing Method</TimingMethod>
+          </Settings>
+        </Columns>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.SumOfBest.dll</Path>
+      <Settings>
+        <Version>1.4</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <TimeColor>FFFFFFFF</TimeColor>
+        <OverrideTimeColor>False</OverrideTimeColor>
+        <Accuracy>Seconds</Accuracy>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>00FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <Display2Rows>False</Display2Rows>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.Text.dll</Path>
+      <Settings>
+        <Version>1.4</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <TimeColor>FFFFFFFF</TimeColor>
+        <OverrideTimeColor>False</OverrideTimeColor>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>00FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <Text1>Text</Text1>
+        <Text2>
+        </Text2>
+        <Font1><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAUEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAgAAAAs=]]></Font1>
+        <Font2><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAUEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAgAAAAs=]]></Font2>
+        <OverrideFont1>False</OverrideFont1>
+        <OverrideFont2>False</OverrideFont2>
+        <Display2Rows>False</Display2Rows>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.Timer.dll</Path>
+      <Settings>
+        <Version>1.5</Version>
+        <TimerHeight>69</TimerHeight>
+        <TimerWidth>225</TimerWidth>
+        <TimerFormat>1.23</TimerFormat>
+        <OverrideSplitColors>False</OverrideSplitColors>
+        <ShowGradient>True</ShowGradient>
+        <TimerColor>FFAAAAAA</TimerColor>
+        <BackgroundColor>00000000</BackgroundColor>
+        <BackgroundColor2>FF222222</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <CenterTimer>False</CenterTimer>
+        <TimingMethod>Current Timing Method</TimingMethod>
+        <DecimalsSize>35</DecimalsSize>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.Title.dll</Path>
+      <Settings>
+        <Version>1.7.3</Version>
+        <ShowGameName>True</ShowGameName>
+        <ShowCategoryName>True</ShowCategoryName>
+        <ShowAttemptCount>True</ShowAttemptCount>
+        <ShowFinishedRunsCount>False</ShowFinishedRunsCount>
+        <OverrideTitleFont>False</OverrideTitleFont>
+        <OverrideTitleColor>False</OverrideTitleColor>
+        <TitleFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAUEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAgAAAAs=]]></TitleFont>
+        <SingleLine>False</SingleLine>
+        <TitleColor>FFFFFFFF</TitleColor>
+        <BackgroundColor>FF2A2A2A</BackgroundColor>
+        <BackgroundColor2>FF131313</BackgroundColor2>
+        <BackgroundGradient>Vertical</BackgroundGradient>
+        <DisplayGameIcon>True</DisplayGameIcon>
+        <ShowRegion>False</ShowRegion>
+        <ShowPlatform>False</ShowPlatform>
+        <ShowVariables>True</ShowVariables>
+        <TextAlignment>0</TextAlignment>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.TotalPlaytime.dll</Path>
+      <Settings>
+        <Version>1.6</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <TimeColor>FFFFFFFF</TimeColor>
+        <OverrideTimeColor>False</OverrideTimeColor>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>00FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <Display2Rows>False</Display2Rows>
+        <ShowTotalHours>False</ShowTotalHours>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.ScriptableAutoSplit.dll</Path>
+      <Settings>
+        <Version>1.5</Version>
+        <ScriptPath>C:\Projekte\a-hat-in-time-mock\AHatInTime.asl</ScriptPath>
+        <Start>True</Start>
+        <Reset>True</Reset>
+        <Split>True</Split>
+        <CustomSettings />
+      </Settings>
+    </Component>
+  </Components>
+</Layout>
\ No newline at end of file
diff --git a/tests/layout_files/WSplit.lsl b/tests/layout_files/WSplit.lsl
new file mode 100644
index 0000000..a80a369
--- /dev/null
+++ b/tests/layout_files/WSplit.lsl
@@ -0,0 +1,150 @@
+﻿<?xml version="1.0" encoding="UTF-8"?>
+<Layout version="1.6.1">
+  <Mode>Vertical</Mode>
+  <X>1257</X>
+  <Y>549</Y>
+  <VerticalWidth>229</VerticalWidth>
+  <VerticalHeight>248</VerticalHeight>
+  <HorizontalWidth>-1</HorizontalWidth>
+  <HorizontalHeight>-1</HorizontalHeight>
+  <Settings>
+    <TextColor>FFFFFFFF</TextColor>
+    <BackgroundColor>FF0D0D0D</BackgroundColor>
+    <BackgroundColor2>FF282828</BackgroundColor2>
+    <ThinSeparatorsColor>03FFFFFF</ThinSeparatorsColor>
+    <SeparatorsColor>FF2A302A</SeparatorsColor>
+    <PersonalBestColor>FF16A6FF</PersonalBestColor>
+    <AheadGainingTimeColor>FF00CC36</AheadGainingTimeColor>
+    <AheadLosingTimeColor>FF52CC73</AheadLosingTimeColor>
+    <BehindGainingTimeColor>FFCC5C52</BehindGainingTimeColor>
+    <BehindLosingTimeColor>FFCC1200</BehindLosingTimeColor>
+    <BestSegmentColor>FFD8AF1F</BestSegmentColor>
+    <UseRainbowColor>False</UseRainbowColor>
+    <NotRunningColor>FFACACAC</NotRunningColor>
+    <PausedColor>FF7A7A7A</PausedColor>
+    <TextOutlineColor>00000000</TextOutlineColor>
+    <ShadowsColor>80000000</ShadowsColor>
+    <TimesFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAQEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAABAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></TimesFont>
+    <TimerFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAdDYWxpYnJpAAAvQgX8////GFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQEAAAAHdmFsdWVfXwAIAgAAAAEAAAAF+////xtTeXN0ZW0uRHJhd2luZy5HcmFwaGljc1VuaXQBAAAAB3ZhbHVlX18ACAIAAAACAAAACw==]]></TimerFont>
+    <TextFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAQEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></TextFont>
+    <AlwaysOnTop>True</AlwaysOnTop>
+    <ShowBestSegments>True</ShowBestSegments>
+    <AntiAliasing>False</AntiAliasing>
+    <DropShadows>False</DropShadows>
+    <BackgroundType>HorizontalGradient</BackgroundType>
+    <BackgroundImage />
+    <ImageOpacity>1</ImageOpacity>
+    <ImageBlur>0</ImageBlur>
+    <Opacity>1</Opacity>
+  </Settings>
+  <Components>
+    <Component>
+      <Path>LiveSplit.Title.dll</Path>
+      <Settings>
+        <Version>1.7</Version>
+        <ShowGameName>True</ShowGameName>
+        <ShowCategoryName>True</ShowCategoryName>
+        <ShowAttemptCount>False</ShowAttemptCount>
+        <ShowFinishedRunsCount>False</ShowFinishedRunsCount>
+        <OverrideTitleFont>False</OverrideTitleFont>
+        <OverrideTitleColor>False</OverrideTitleColor>
+        <TitleFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAUEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAgAAAAs=]]></TitleFont>
+        <CenterTitle>False</CenterTitle>
+        <SingleLine>True</SingleLine>
+        <TitleColor>FFFFFFFF</TitleColor>
+        <BackgroundColor>FF3E3E3E</BackgroundColor>
+        <BackgroundColor2>FF343434</BackgroundColor2>
+        <BackgroundGradient>Vertical</BackgroundGradient>
+        <DisplayGameIcon>True</DisplayGameIcon>
+        <ShowRegion>False</ShowRegion>
+        <ShowPlatform>False</ShowPlatform>
+        <ShowVariables>True</ShowVariables>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.Splits.dll</Path>
+      <Settings>
+        <Version>1.6</Version>
+        <CurrentSplitTopColor>FF07233F</CurrentSplitTopColor>
+        <CurrentSplitBottomColor>FF0E487E</CurrentSplitBottomColor>
+        <VisualSplitCount>9</VisualSplitCount>
+        <SplitPreviewCount>1</SplitPreviewCount>
+        <DisplayIcons>True</DisplayIcons>
+        <ShowThinSeparators>False</ShowThinSeparators>
+        <AlwaysShowLastSplit>True</AlwaysShowLastSplit>
+        <SplitWidth>20</SplitWidth>
+        <SplitTimesAccuracy>Seconds</SplitTimesAccuracy>
+        <AutomaticAbbreviations>False</AutomaticAbbreviations>
+        <BeforeNamesColor>FFFFFFFF</BeforeNamesColor>
+        <CurrentNamesColor>FFFFFFFF</CurrentNamesColor>
+        <AfterNamesColor>FFFFFFFF</AfterNamesColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <BeforeTimesColor>FFFFFFFF</BeforeTimesColor>
+        <CurrentTimesColor>FFFFFFFF</CurrentTimesColor>
+        <AfterTimesColor>FFFFFFFF</AfterTimesColor>
+        <OverrideTimesColor>False</OverrideTimesColor>
+        <ShowBlankSplits>False</ShowBlankSplits>
+        <LockLastSplit>True</LockLastSplit>
+        <IconSize>24</IconSize>
+        <IconShadows>True</IconShadows>
+        <SplitHeight>0</SplitHeight>
+        <CurrentSplitGradient>Horizontal</CurrentSplitGradient>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>01FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <SeparatorLastSplit>True</SeparatorLastSplit>
+        <DeltasAccuracy>Tenths</DeltasAccuracy>
+        <DropDecimals>True</DropDecimals>
+        <OverrideDeltasColor>False</OverrideDeltasColor>
+        <DeltasColor>FFFFFFFF</DeltasColor>
+        <Display2Rows>False</Display2Rows>
+        <ShowColumnLabels>False</ShowColumnLabels>
+        <LabelsColor>FFFFFFFF</LabelsColor>
+        <Columns>
+          <Settings>
+            <Version>1.5</Version>
+            <Name>Time</Name>
+            <Type>DeltaorSplitTime</Type>
+            <Comparison>Current Comparison</Comparison>
+            <TimingMethod>Current Timing Method</TimingMethod>
+          </Settings>
+        </Columns>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.Timer.dll</Path>
+      <Settings>
+        <Version>1.5</Version>
+        <TimerHeight>69</TimerHeight>
+        <TimerWidth>225</TimerWidth>
+        <TimerFormat>1.23</TimerFormat>
+        <OverrideSplitColors>True</OverrideSplitColors>
+        <ShowGradient>False</ShowGradient>
+        <TimerColor>FFADFF2F</TimerColor>
+        <BackgroundColor>FF243108</BackgroundColor>
+        <BackgroundColor2>FF486210</BackgroundColor2>
+        <BackgroundGradient>Horizontal</BackgroundGradient>
+        <CenterTimer>False</CenterTimer>
+        <TimingMethod>Current Timing Method</TimingMethod>
+        <DecimalsSize>35</DecimalsSize>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.PreviousSegment.dll</Path>
+      <Settings>
+        <Version>1.6</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <BackgroundColor>FF1C201C</BackgroundColor>
+        <BackgroundColor2>FF394039</BackgroundColor2>
+        <BackgroundGradient>Horizontal</BackgroundGradient>
+        <DeltaAccuracy>Tenths</DeltaAccuracy>
+        <DropDecimals>True</DropDecimals>
+        <Comparison>Current Comparison</Comparison>
+        <Display2Rows>False</Display2Rows>
+        <ShowPossibleTimeSave>False</ShowPossibleTimeSave>
+        <TimeSaveAccuracy>Tenths</TimeSaveAccuracy>
+      </Settings>
+    </Component>
+  </Components>
+</Layout>
\ No newline at end of file
diff --git a/tests/layout_files/dark.lsl b/tests/layout_files/dark.lsl
new file mode 100644
index 0000000..b388406
--- /dev/null
+++ b/tests/layout_files/dark.lsl
@@ -0,0 +1,239 @@
+﻿<?xml version="1.0" encoding="UTF-8"?>
+<Layout version="1.6.1">
+  <Mode>Vertical</Mode>
+  <X>1385</X>
+  <Y>709</Y>
+  <VerticalWidth>286</VerticalWidth>
+  <VerticalHeight>448</VerticalHeight>
+  <HorizontalWidth>-1</HorizontalWidth>
+  <HorizontalHeight>-1</HorizontalHeight>
+  <Settings>
+    <TextColor>FFFFFFFF</TextColor>
+    <BackgroundColor>FF000000</BackgroundColor>
+    <BackgroundColor2>00000000</BackgroundColor2>
+    <ThinSeparatorsColor>03FFFFFF</ThinSeparatorsColor>
+    <SeparatorsColor>24FFFFFF</SeparatorsColor>
+    <PersonalBestColor>FFF15FBD</PersonalBestColor>
+    <AheadGainingTimeColor>FF00CC36</AheadGainingTimeColor>
+    <AheadLosingTimeColor>FF52CC73</AheadLosingTimeColor>
+    <BehindGainingTimeColor>FF55AFFF</BehindGainingTimeColor>
+    <BehindLosingTimeColor>FF2497FF</BehindLosingTimeColor>
+    <BestSegmentColor>FFD8AF1F</BestSegmentColor>
+    <UseRainbowColor>False</UseRainbowColor>
+    <NotRunningColor>FFACACAC</NotRunningColor>
+    <PausedColor>FF7A7A7A</PausedColor>
+    <TextOutlineColor>00000000</TextOutlineColor>
+    <ShadowsColor>80000000</ShadowsColor>
+    <TimesFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAQEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAABAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></TimesFont>
+    <TimerFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAdDYWxpYnJpAAAvQgX8////GFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQEAAAAHdmFsdWVfXwAIAgAAAAEAAAAF+////xtTeXN0ZW0uRHJhd2luZy5HcmFwaGljc1VuaXQBAAAAB3ZhbHVlX18ACAIAAAACAAAACw==]]></TimerFont>
+    <TextFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAQEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></TextFont>
+    <AlwaysOnTop>False</AlwaysOnTop>
+    <ShowBestSegments>True</ShowBestSegments>
+    <AntiAliasing>True</AntiAliasing>
+    <DropShadows>True</DropShadows>
+    <BackgroundType>SolidColor</BackgroundType>
+    <BackgroundImage />
+    <ImageOpacity>1</ImageOpacity>
+    <ImageBlur>0</ImageBlur>
+    <Opacity>1</Opacity>
+    <MousePassThroughWhileRunning>False</MousePassThroughWhileRunning>
+  </Settings>
+  <Components>
+    <Component>
+      <Path>LiveSplit.Title.dll</Path>
+      <Settings>
+        <Version>1.7.3</Version>
+        <ShowGameName>True</ShowGameName>
+        <ShowCategoryName>True</ShowCategoryName>
+        <ShowAttemptCount>True</ShowAttemptCount>
+        <ShowFinishedRunsCount>False</ShowFinishedRunsCount>
+        <OverrideTitleFont>False</OverrideTitleFont>
+        <OverrideTitleColor>False</OverrideTitleColor>
+        <TitleFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAUEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAgAAAAs=]]></TitleFont>
+        <SingleLine>False</SingleLine>
+        <TitleColor>FFFFFFFF</TitleColor>
+        <BackgroundColor>00000000</BackgroundColor>
+        <BackgroundColor2>FF131313</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <DisplayGameIcon>False</DisplayGameIcon>
+        <ShowRegion>False</ShowRegion>
+        <ShowPlatform>False</ShowPlatform>
+        <ShowVariables>True</ShowVariables>
+        <TextAlignment>2</TextAlignment>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>
+      </Path>
+      <Settings />
+    </Component>
+    <Component>
+      <Path>LiveSplit.Splits.dll</Path>
+      <Settings>
+        <Version>1.6</Version>
+        <CurrentSplitTopColor>FF3373F4</CurrentSplitTopColor>
+        <CurrentSplitBottomColor>FF153574</CurrentSplitBottomColor>
+        <VisualSplitCount>12</VisualSplitCount>
+        <SplitPreviewCount>1</SplitPreviewCount>
+        <DisplayIcons>True</DisplayIcons>
+        <ShowThinSeparators>True</ShowThinSeparators>
+        <AlwaysShowLastSplit>True</AlwaysShowLastSplit>
+        <SplitWidth>20</SplitWidth>
+        <SplitTimesAccuracy>Seconds</SplitTimesAccuracy>
+        <AutomaticAbbreviations>False</AutomaticAbbreviations>
+        <BeforeNamesColor>FFFFFFFF</BeforeNamesColor>
+        <CurrentNamesColor>FFFFFFFF</CurrentNamesColor>
+        <AfterNamesColor>FFFFFFFF</AfterNamesColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <BeforeTimesColor>FFFFFFFF</BeforeTimesColor>
+        <CurrentTimesColor>FFFFFFFF</CurrentTimesColor>
+        <AfterTimesColor>FFFFFFFF</AfterTimesColor>
+        <OverrideTimesColor>False</OverrideTimesColor>
+        <ShowBlankSplits>True</ShowBlankSplits>
+        <LockLastSplit>True</LockLastSplit>
+        <IconSize>24</IconSize>
+        <IconShadows>True</IconShadows>
+        <SplitHeight>0</SplitHeight>
+        <CurrentSplitGradient>Vertical</CurrentSplitGradient>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>01FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <SeparatorLastSplit>True</SeparatorLastSplit>
+        <DeltasAccuracy>Tenths</DeltasAccuracy>
+        <DropDecimals>True</DropDecimals>
+        <OverrideDeltasColor>False</OverrideDeltasColor>
+        <DeltasColor>FFFFFFFF</DeltasColor>
+        <Display2Rows>False</Display2Rows>
+        <ShowColumnLabels>False</ShowColumnLabels>
+        <LabelsColor>FFFFFFFF</LabelsColor>
+        <Columns>
+          <Settings>
+            <Version>1.5</Version>
+            <Name>+/−</Name>
+            <Type>Delta</Type>
+            <Comparison>Current Comparison</Comparison>
+            <TimingMethod>Current Timing Method</TimingMethod>
+          </Settings>
+          <Settings>
+            <Version>1.5</Version>
+            <Name>Time</Name>
+            <Type>SplitTime</Type>
+            <Comparison>Current Comparison</Comparison>
+            <TimingMethod>Current Timing Method</TimingMethod>
+          </Settings>
+        </Columns>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>
+      </Path>
+      <Settings />
+    </Component>
+    <Component>
+      <Path>LiveSplit.DetailedTimer.dll</Path>
+      <Settings>
+        <Version>1.5</Version>
+        <Height>73</Height>
+        <Width>200</Width>
+        <SegmentTimerSizeRatio>41</SegmentTimerSizeRatio>
+        <TimerShowGradient>True</TimerShowGradient>
+        <OverrideTimerColors>False</OverrideTimerColors>
+        <SegmentTimerShowGradient>True</SegmentTimerShowGradient>
+        <TimerFormat>1.23</TimerFormat>
+        <SegmentTimerFormat>1.23</SegmentTimerFormat>
+        <SegmentTimesAccuracy>Hundredths</SegmentTimesAccuracy>
+        <TimerColor>FFAAAAAA</TimerColor>
+        <SegmentTimerColor>FF00FFF3</SegmentTimerColor>
+        <SegmentLabelsColor>FFFFFFFF</SegmentLabelsColor>
+        <SegmentTimesColor>FFFFFFFF</SegmentTimesColor>
+        <SegmentLabelsFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAIEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></SegmentLabelsFont>
+        <SegmentTimesFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAIEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAABAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></SegmentTimesFont>
+        <SplitNameFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAMEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></SplitNameFont>
+        <DisplayIcon>False</DisplayIcon>
+        <IconSize>40</IconSize>
+        <ShowSplitName>False</ShowSplitName>
+        <SplitNameColor>FFFFFFFF</SplitNameColor>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>00FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <Comparison>Current Comparison</Comparison>
+        <Comparison2>Best Segments</Comparison2>
+        <HideComparison>False</HideComparison>
+        <TimingMethod>Current Timing Method</TimingMethod>
+        <DecimalsSize>35</DecimalsSize>
+        <SegmentTimerDecimalsSize>35</SegmentTimerDecimalsSize>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.PreviousSegment.dll</Path>
+      <Settings>
+        <Version>1.6</Version>
+        <TextColor>FF5AFFB0</TextColor>
+        <OverrideTextColor>True</OverrideTextColor>
+        <BackgroundColor>00000000</BackgroundColor>
+        <BackgroundColor2>FF0D0D0D</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <DeltaAccuracy>Tenths</DeltaAccuracy>
+        <DropDecimals>True</DropDecimals>
+        <Comparison>Current Comparison</Comparison>
+        <Display2Rows>False</Display2Rows>
+        <ShowPossibleTimeSave>False</ShowPossibleTimeSave>
+        <TimeSaveAccuracy>Tenths</TimeSaveAccuracy>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.RunPrediction.dll</Path>
+      <Settings>
+        <Version>1.4</Version>
+        <TextColor>FFF393FA</TextColor>
+        <OverrideTextColor>True</OverrideTextColor>
+        <TimeColor>FFF393FA</TimeColor>
+        <OverrideTimeColor>True</OverrideTimeColor>
+        <Accuracy>Seconds</Accuracy>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>00FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <Comparison>Best Segments</Comparison>
+        <Display2Rows>False</Display2Rows>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.PossibleTimeSave.dll</Path>
+      <Settings>
+        <Version>1.5</Version>
+        <TextColor>FFEDBB56</TextColor>
+        <OverrideTextColor>True</OverrideTextColor>
+        <TimeColor>FFEDBB56</TimeColor>
+        <OverrideTimeColor>True</OverrideTimeColor>
+        <Accuracy>Hundredths</Accuracy>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>00FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <Comparison>Current Comparison</Comparison>
+        <Display2Rows>False</Display2Rows>
+        <TotalTimeSave>False</TotalTimeSave>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.Text.dll</Path>
+      <Settings>
+        <Version>1.4</Version>
+        <TextColor>FF83C7FF</TextColor>
+        <OverrideTextColor>True</OverrideTextColor>
+        <TimeColor>FFFFFFFF</TimeColor>
+        <OverrideTimeColor>False</OverrideTimeColor>
+        <BackgroundColor>00FFFFFF</BackgroundColor>
+        <BackgroundColor2>00FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <Text1>Twitter: @zekekemarcus</Text1>
+        <Text2>
+        </Text2>
+        <Font1><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAUEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAgAAAAs=]]></Font1>
+        <Font2><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAUEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAgAAAAs=]]></Font2>
+        <OverrideFont1>False</OverrideFont1>
+        <OverrideFont2>False</OverrideFont2>
+        <Display2Rows>False</Display2Rows>
+      </Settings>
+    </Component>
+  </Components>
+</Layout>
\ No newline at end of file
diff --git a/tests/layout_files/subsplits.lsl b/tests/layout_files/subsplits.lsl
new file mode 100644
index 0000000..308548b
--- /dev/null
+++ b/tests/layout_files/subsplits.lsl
@@ -0,0 +1,300 @@
+﻿<?xml version="1.0" encoding="UTF-8"?>
+<Layout version="1.6.1">
+  <Mode>Vertical</Mode>
+  <X>1920</X>
+  <Y>-1</Y>
+  <VerticalWidth>280</VerticalWidth>
+  <VerticalHeight>1080</VerticalHeight>
+  <HorizontalWidth>-1</HorizontalWidth>
+  <HorizontalHeight>-1</HorizontalHeight>
+  <Settings>
+    <TextColor>FFFFFFFF</TextColor>
+    <BackgroundColor>FF0F0F0F</BackgroundColor>
+    <BackgroundColor2>00000000</BackgroundColor2>
+    <ThinSeparatorsColor>03FFFFFF</ThinSeparatorsColor>
+    <SeparatorsColor>642580FF</SeparatorsColor>
+    <PersonalBestColor>FF16A6FF</PersonalBestColor>
+    <AheadGainingTimeColor>FF00CC36</AheadGainingTimeColor>
+    <AheadLosingTimeColor>FF52CC73</AheadLosingTimeColor>
+    <BehindGainingTimeColor>FFCC5C52</BehindGainingTimeColor>
+    <BehindLosingTimeColor>FFCC3232</BehindLosingTimeColor>
+    <BestSegmentColor>FFD8AF1F</BestSegmentColor>
+    <UseRainbowColor>False</UseRainbowColor>
+    <NotRunningColor>FFACACAC</NotRunningColor>
+    <PausedColor>FF7A7A7A</PausedColor>
+    <TextOutlineColor>00000000</TextOutlineColor>
+    <ShadowsColor>80000000</ShadowsColor>
+    <TimesFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAMEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAABAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></TimesFont>
+    <TimerFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAABRNaWNyb3NvZnQgU2FucyBTZXJpZgAAL0IF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAABAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAgAAAAs=]]></TimerFont>
+    <TextFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAMEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAABAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></TextFont>
+    <AlwaysOnTop>True</AlwaysOnTop>
+    <ShowBestSegments>True</ShowBestSegments>
+    <AntiAliasing>False</AntiAliasing>
+    <DropShadows>True</DropShadows>
+    <BackgroundType>SolidColor</BackgroundType>
+    <BackgroundImage />
+    <ImageOpacity>1</ImageOpacity>
+    <ImageBlur>0</ImageBlur>
+    <Opacity>1</Opacity>
+  </Settings>
+  <Components>
+    <Component>
+      <Path>LiveSplit.Title.dll</Path>
+      <Settings>
+        <Version>1.7</Version>
+        <ShowGameName>True</ShowGameName>
+        <ShowCategoryName>True</ShowCategoryName>
+        <ShowAttemptCount>False</ShowAttemptCount>
+        <ShowFinishedRunsCount>False</ShowFinishedRunsCount>
+        <OverrideTitleFont>False</OverrideTitleFont>
+        <OverrideTitleColor>False</OverrideTitleColor>
+        <TitleFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAUEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAgAAAAs=]]></TitleFont>
+        <CenterTitle>False</CenterTitle>
+        <SingleLine>False</SingleLine>
+        <TitleColor>FFFFFFFF</TitleColor>
+        <BackgroundColor>6400FFFF</BackgroundColor>
+        <BackgroundColor2>640000FF</BackgroundColor2>
+        <BackgroundGradient>Vertical</BackgroundGradient>
+        <DisplayGameIcon>True</DisplayGameIcon>
+        <ShowRegion>False</ShowRegion>
+        <ShowPlatform>False</ShowPlatform>
+        <ShowVariables>True</ShowVariables>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.WorldRecord.dll</Path>
+      <Settings>
+        <Version>1.6</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <TimeColor>FFFFFFFF</TimeColor>
+        <OverrideTimeColor>False</OverrideTimeColor>
+        <BackgroundColor>640000FF</BackgroundColor>
+        <BackgroundColor2>00FFFFFF</BackgroundColor2>
+        <BackgroundGradient>Plain</BackgroundGradient>
+        <Display2Rows>False</Display2Rows>
+        <CenteredText>True</CenteredText>
+        <FilterRegion>False</FilterRegion>
+        <FilterPlatform>False</FilterPlatform>
+        <FilterVariables>False</FilterVariables>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.CurrentComparison.dll</Path>
+      <Settings>
+        <Version>1.4</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <TimeColor>FFFFFFFF</TimeColor>
+        <OverrideTimeColor>False</OverrideTimeColor>
+        <BackgroundColor>640000FF</BackgroundColor>
+        <BackgroundColor2>646400FF</BackgroundColor2>
+        <BackgroundGradient>Vertical</BackgroundGradient>
+        <Font1><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAUEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAgAAAAs=]]></Font1>
+        <Font2><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAUEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAgAAAAs=]]></Font2>
+        <OverrideFont1>False</OverrideFont1>
+        <OverrideFont2>False</OverrideFont2>
+        <Display2Rows>False</Display2Rows>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.Subsplits.dll</Path>
+      <Settings>
+        <Version>1.7</Version>
+        <CurrentSplitTopColor>966400FF</CurrentSplitTopColor>
+        <CurrentSplitBottomColor>9600FFFF</CurrentSplitBottomColor>
+        <VisualSplitCount>21</VisualSplitCount>
+        <SplitPreviewCount>7</SplitPreviewCount>
+        <MinimumMajorSplits>0</MinimumMajorSplits>
+        <DisplayIcons>True</DisplayIcons>
+        <ShowThinSeparators>False</ShowThinSeparators>
+        <AlwaysShowLastSplit>True</AlwaysShowLastSplit>
+        <SplitWidth>20</SplitWidth>
+        <SplitTimesAccuracy>Seconds</SplitTimesAccuracy>
+        <BeforeNamesColor>FFFFFFFF</BeforeNamesColor>
+        <CurrentNamesColor>FFFFFFFF</CurrentNamesColor>
+        <AfterNamesColor>FFFFFFFF</AfterNamesColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <BeforeTimesColor>FFFFFFFF</BeforeTimesColor>
+        <CurrentTimesColor>FFFFFFFF</CurrentTimesColor>
+        <AfterTimesColor>FFFFFFFF</AfterTimesColor>
+        <OverrideTimesColor>False</OverrideTimesColor>
+        <LockLastSplit>False</LockLastSplit>
+        <IconSize>22</IconSize>
+        <IconShadows>True</IconShadows>
+        <SplitHeight>5.8</SplitHeight>
+        <CurrentSplitGradient>Horizontal</CurrentSplitGradient>
+        <BackgroundColor>646400FF</BackgroundColor>
+        <BackgroundColor2>6400FFFF</BackgroundColor2>
+        <BackgroundGradient>Horizontal</BackgroundGradient>
+        <SeparatorLastSplit>True</SeparatorLastSplit>
+        <DeltasAccuracy>Tenths</DeltasAccuracy>
+        <DropDecimals>True</DropDecimals>
+        <OverrideDeltasColor>False</OverrideDeltasColor>
+        <DeltasColor>FFFFFFFF</DeltasColor>
+        <HeaderComparison>Current Comparison</HeaderComparison>
+        <HeaderTimingMethod>Current Timing Method</HeaderTimingMethod>
+        <Display2Rows>False</Display2Rows>
+        <IndentBlankIcons>True</IndentBlankIcons>
+        <IndentSubsplits>True</IndentSubsplits>
+        <HideSubsplits>False</HideSubsplits>
+        <ShowSubsplits>False</ShowSubsplits>
+        <CurrentSectionOnly>False</CurrentSectionOnly>
+        <OverrideSubsplitColor>False</OverrideSubsplitColor>
+        <SubsplitGradient>Plain</SubsplitGradient>
+        <ShowHeader>True</ShowHeader>
+        <IndentSectionSplit>True</IndentSectionSplit>
+        <ShowIconSectionSplit>False</ShowIconSectionSplit>
+        <ShowSectionIcon>False</ShowSectionIcon>
+        <HeaderGradient>Horizontal</HeaderGradient>
+        <OverrideHeaderColor>False</OverrideHeaderColor>
+        <HeaderText>True</HeaderText>
+        <HeaderTimes>False</HeaderTimes>
+        <HeaderAccuracy>Seconds</HeaderAccuracy>
+        <SectionTimer>False</SectionTimer>
+        <SectionTimerGradient>True</SectionTimerGradient>
+        <SectionTimerAccuracy>Tenths</SectionTimerAccuracy>
+        <SubsplitTopColor>8D000000</SubsplitTopColor>
+        <SubsplitBottomColor>00FFFFFF</SubsplitBottomColor>
+        <HeaderTopColor>0000FFFF</HeaderTopColor>
+        <HeaderBottomColor>006400FF</HeaderBottomColor>
+        <HeaderTextColor>FFFFFFFF</HeaderTextColor>
+        <HeaderTimesColor>FFFFFFFF</HeaderTimesColor>
+        <SectionTimerColor>FF777777</SectionTimerColor>
+        <ShowColumnLabels>False</ShowColumnLabels>
+        <LabelsColor>FFFFFFFF</LabelsColor>
+        <Columns>
+          <Settings>
+            <Version>1.5</Version>
+            <Name>+/-</Name>
+            <Type>Delta</Type>
+            <Comparison>Current Comparison</Comparison>
+            <TimingMethod>Current Timing Method</TimingMethod>
+          </Settings>
+          <Settings>
+            <Version>1.5</Version>
+            <Name>Time</Name>
+            <Type>SplitTime</Type>
+            <Comparison>Current Comparison</Comparison>
+            <TimingMethod>Current Timing Method</TimingMethod>
+          </Settings>
+        </Columns>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.DetailedTimer.dll</Path>
+      <Settings>
+        <Version>1.5</Version>
+        <Height>75</Height>
+        <Width>200</Width>
+        <SegmentTimerSizeRatio>40</SegmentTimerSizeRatio>
+        <TimerShowGradient>True</TimerShowGradient>
+        <OverrideTimerColors>False</OverrideTimerColors>
+        <SegmentTimerShowGradient>True</SegmentTimerShowGradient>
+        <TimerFormat>1</TimerFormat>
+        <SegmentTimerFormat>1</SegmentTimerFormat>
+        <SegmentTimesAccuracy>Tenths</SegmentTimesAccuracy>
+        <TimerColor>FFAAAAAA</TimerColor>
+        <SegmentTimerColor>FFAAAAAA</SegmentTimerColor>
+        <SegmentLabelsColor>FFFFFFFF</SegmentLabelsColor>
+        <SegmentTimesColor>FFFFFFFF</SegmentTimesColor>
+        <SegmentLabelsFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAIEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></SegmentLabelsFont>
+        <SegmentTimesFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAIEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAABAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></SegmentTimesFont>
+        <SplitNameFont><![CDATA[AAEAAAD/////AQAAAAAAAAAMAgAAAFFTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABNTeXN0ZW0uRHJhd2luZy5Gb250BAAAAAROYW1lBFNpemUFU3R5bGUEVW5pdAEABAQLGFN5c3RlbS5EcmF3aW5nLkZvbnRTdHlsZQIAAAAbU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AgAAAAIAAAAGAwAAAAhTZWdvZSBVSQAAYEEF/P///xhTeXN0ZW0uRHJhd2luZy5Gb250U3R5bGUBAAAAB3ZhbHVlX18ACAIAAAAAAAAABfv///8bU3lzdGVtLkRyYXdpbmcuR3JhcGhpY3NVbml0AQAAAAd2YWx1ZV9fAAgCAAAAAwAAAAs=]]></SplitNameFont>
+        <DisplayIcon>False</DisplayIcon>
+        <IconSize>40</IconSize>
+        <ShowSplitName>False</ShowSplitName>
+        <SplitNameColor>FFFFFFFF</SplitNameColor>
+        <BackgroundColor>966400FF</BackgroundColor>
+        <BackgroundColor2>9600FFFF</BackgroundColor2>
+        <BackgroundGradient>Horizontal</BackgroundGradient>
+        <Comparison>Current Comparison</Comparison>
+        <Comparison2>Best Segments</Comparison2>
+        <HideComparison>False</HideComparison>
+        <TimingMethod>Current Timing Method</TimingMethod>
+        <DecimalsSize>35</DecimalsSize>
+        <SegmentTimerDecimalsSize>35</SegmentTimerDecimalsSize>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.PossibleTimeSave.dll</Path>
+      <Settings>
+        <Version>1.5</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <TimeColor>FFFFFFFF</TimeColor>
+        <OverrideTimeColor>False</OverrideTimeColor>
+        <Accuracy>Tenths</Accuracy>
+        <BackgroundColor>966400FF</BackgroundColor>
+        <BackgroundColor2>9600FFFF</BackgroundColor2>
+        <BackgroundGradient>Horizontal</BackgroundGradient>
+        <Comparison>Current Comparison</Comparison>
+        <Display2Rows>False</Display2Rows>
+        <TotalTimeSave>False</TotalTimeSave>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.PreviousSegment.dll</Path>
+      <Settings>
+        <Version>1.6</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <BackgroundColor>644B00FF</BackgroundColor>
+        <BackgroundColor2>6400FFFF</BackgroundColor2>
+        <BackgroundGradient>Horizontal</BackgroundGradient>
+        <DeltaAccuracy>Tenths</DeltaAccuracy>
+        <DropDecimals>True</DropDecimals>
+        <Comparison>Best Segments</Comparison>
+        <Display2Rows>False</Display2Rows>
+        <ShowPossibleTimeSave>False</ShowPossibleTimeSave>
+        <TimeSaveAccuracy>Tenths</TimeSaveAccuracy>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.Delta.dll</Path>
+      <Settings>
+        <Version>1.4</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <Accuracy>Tenths</Accuracy>
+        <BackgroundColor>644B00FF</BackgroundColor>
+        <BackgroundColor2>6400FFFF</BackgroundColor2>
+        <BackgroundGradient>Horizontal</BackgroundGradient>
+        <Comparison>Best Split Times</Comparison>
+        <Display2Rows>False</Display2Rows>
+        <DropDecimals>True</DropDecimals>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.SumOfBest.dll</Path>
+      <Settings>
+        <Version>1.4</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <TimeColor>FFFFFFFF</TimeColor>
+        <OverrideTimeColor>False</OverrideTimeColor>
+        <Accuracy>Seconds</Accuracy>
+        <BackgroundColor>644B00FF</BackgroundColor>
+        <BackgroundColor2>6400FFFF</BackgroundColor2>
+        <BackgroundGradient>Horizontal</BackgroundGradient>
+        <Display2Rows>False</Display2Rows>
+      </Settings>
+    </Component>
+    <Component>
+      <Path>LiveSplit.RunPrediction.dll</Path>
+      <Settings>
+        <Version>1.4</Version>
+        <TextColor>FFFFFFFF</TextColor>
+        <OverrideTextColor>False</OverrideTextColor>
+        <TimeColor>FFFFFFFF</TimeColor>
+        <OverrideTimeColor>False</OverrideTimeColor>
+        <Accuracy>Seconds</Accuracy>
+        <BackgroundColor>644B00FF</BackgroundColor>
+        <BackgroundColor2>6400FFFF</BackgroundColor2>
+        <BackgroundGradient>Horizontal</BackgroundGradient>
+        <Comparison>Best Segments</Comparison>
+        <Display2Rows>False</Display2Rows>
+      </Settings>
+    </Component>
+  </Components>
+</Layout>
\ No newline at end of file
diff --git a/tests/layout_parsing.rs b/tests/layout_parsing.rs
new file mode 100644
index 0000000..a6b9b66
--- /dev/null
+++ b/tests/layout_parsing.rs
@@ -0,0 +1,44 @@
+mod parse {
+    use livesplit_core::layout::{parser::parse, Layout};
+    use std::{fs::File, io::BufReader};
+
+    fn file(path: &str) -> BufReader<File> {
+        BufReader::new(File::open(path).unwrap())
+    }
+
+    fn livesplit(path: &str) -> Layout {
+        parse(file(path)).unwrap()
+    }
+
+    #[test]
+    fn all() {
+        livesplit("tests/layout_files/All.lsl");
+    }
+
+    #[test]
+    fn dark() {
+        livesplit("tests/layout_files/dark.lsl");
+    }
+
+    #[test]
+    fn subsplits() {
+        livesplit("tests/layout_files/subsplits.lsl");
+    }
+
+    #[test]
+    fn wsplit() {
+        livesplit("tests/layout_files/WSplit.lsl");
+    }
+
+    #[test]
+    fn assert_order_of_default_columns() {
+        use livesplit_core::component::splits;
+
+        // The layout parser assumes that the order is from right to left. If it
+        // changes, the layout parser needs to be adjusted as well.
+        let component = splits::Component::default();
+        let columns = &component.settings().columns;
+        assert_eq!(columns[0].name, "Time");
+        assert_eq!(columns[1].name, "+/−");
+    }
+}