From 5274cc92c1055a3209dfae7e5346bfe52c35e4a8 Mon Sep 17 00:00:00 2001
From: Thorbjørn Lindeijer <bjorn@lindeijer.nl>
Date: Mon, 17 Mar 2025 10:39:38 +0100
Subject: Define the GUI theme in XML

Now all images used by the various UI widgets are defined in a
`theme.xml`, removing hardcoded requirements on the size of images,
borders and sub-images and their locations. The `colors.xml` file was
merged into this new file as well.

The `<img>` element defines either a plain image, or a 9-scale that is
automatically rendered at the size of the widget when any of the `left`,
`right`, `top` or `bottom` attributes are given.

The `x`, `y`, `width` and `height` attributes determine the
sub-rectangle within the image referenced by `src`. `x` and `y` default
to 0 and `width` and `height` default to the imge size.

The `<state>` element defines in which state its images are used by
setting its `selected`, `disabled`, `hovered` or `focused` attributes to
either `true` or `false`. Only the first matching state is rendered.

The `Text` and `SpeechBubble` classes now use the same skin to draw the
bubble, as well as using a newly introduced `BUBBLE_TEXT` color from the
theme palette.
---
 data/graphics/gui/colors.xml       |  50 --
 data/graphics/gui/speechbubble.xml |  18 -
 data/graphics/gui/theme.xml        | 234 ++++++++++
 data/graphics/gui/window.xml       |  18 -
 src/being.cpp                      |   4 +-
 src/graphics.cpp                   |  11 +
 src/graphics.h                     |   2 +
 src/gui/minimap.cpp                |   3 +-
 src/gui/speechbubble.cpp           |  13 +-
 src/gui/widgets/button.cpp         |  20 +-
 src/gui/widgets/checkbox.cpp       |  11 +-
 src/gui/widgets/dropdown.cpp       |  32 +-
 src/gui/widgets/popup.cpp          |  25 +-
 src/gui/widgets/popup.h            |  23 +-
 src/gui/widgets/radiobutton.cpp    |  11 +-
 src/gui/widgets/resizegrip.cpp     |  13 +-
 src/gui/widgets/scrollarea.cpp     |  76 +--
 src/gui/widgets/scrollarea.h       |   8 +
 src/gui/widgets/slider.cpp         |  16 +-
 src/gui/widgets/tab.cpp            |  16 +-
 src/gui/widgets/textfield.cpp      |   8 +-
 src/gui/widgets/window.cpp         |  81 ++--
 src/gui/widgets/window.h           |   5 +-
 src/resources/theme.cpp            | 918 ++++++++++++++-----------------------
 src/resources/theme.h              | 176 ++++---
 src/text.cpp                       |  63 +--
 src/text.h                         |   8 +-
 27 files changed, 883 insertions(+), 980 deletions(-)
 delete mode 100644 data/graphics/gui/colors.xml
 delete mode 100644 data/graphics/gui/speechbubble.xml
 create mode 100644 data/graphics/gui/theme.xml
 delete mode 100644 data/graphics/gui/window.xml

diff --git a/data/graphics/gui/colors.xml b/data/graphics/gui/colors.xml
deleted file mode 100644
index 4a35d081..00000000
--- a/data/graphics/gui/colors.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-<colors>
-	<color id="TEXT" color="#000000" />
-	<color id="SHADOW" color="#000000" />
-	<color id="OUTLINE" color="#000000" />
-	<color id="PROGRESS_BAR" color="#ffffff" />
-	<color id="BUTTON" color="#000000" />
-	<color id="BUTTON_DISABLED" color="#333333" />
-	<color id="TAB" color="#000000" />
-	<color id="PARTY_CHAT_TAB" color="#f48055" />
-	<color id="PARTY_SOCIAL_TAB" color="#ff00d8" />
-	<color id="BACKGROUND" color="#ffffff" />
-	<color id="HIGHLIGHT" color="#c0c0c0" />
-	<color id="TAB_FLASH" color="#ff0000" effect="pulse" />
-	<color id="SHOP_WARNING" color="#910000" />
-	<color id="ITEM_EQUIPPED" color="#000091" />
-	<color id="CHAT" color="#000000" />
-	<color id="GM" color="#ff0000" />
-	<color id="PLAYER" color="#1fa052" />
-	<color id="WHISPER" color="#0000ff" />
-	<color id="IS" color="#a08527" />
-	<color id="PARTY" color="#ff00d8" />
-	<color id="GUILD" color="#ff00d8" />
-	<color id="SERVER" color="#8415e2" />
-	<color id="LOGGER" color="#919191" />
-	<color id="HYPERLINK" color="#e50d0d" />
-	<color id="UNKNOWN_ITEM" color="#000000" />
-	<color id="GENERIC" color="#21a5b1" />
-	<color id="HEAD" color="#527fa4" />
-	<color id="USABLE" color="#268d24" />
-	<color id="TORSO" color="#d12aa4" />
-	<color id="ONEHAND" color="#f42a2a" />
-	<color id="LEGS" color="#699900" />
-	<color id="FEET" color="#aa1d48" />
-	<color id="TWOHAND" color="#f46d0e" />
-	<color id="SHIELD" color="#9c2424" />
-	<color id="RING" color="#0000ff" />
-	<color id="NECKLACE" color="#ff00ff" />
-	<color id="ARMS" color="#9c24e8" />
-	<color id="AMMO" color="#8b6311" />
-	<color id="SERVER_VERSION_NOT_SUPPORTED" color="#DC0000" />
-
-	<progressbar id="DEFAULT" color="#969696" />
-	<progressbar id="HP" color="#ff0000,e28000,c38948,0f6a20" />
-	<progressbar id="MP" color="#1a66e6" />
-	<progressbar id="NO_MP" color="#646464" />
-	<progressbar id="EXP" color="#8fc0d3" />
-	<progressbar id="INVY_SLOTS" color="#e1c819" />
-	<progressbar id="WEIGHT" color="#0000ff,ffff00,ff0000" />
-	<progressbar id="JOB" color="#e187cb" />
-</colors>
diff --git a/data/graphics/gui/speechbubble.xml b/data/graphics/gui/speechbubble.xml
deleted file mode 100644
index 84b6557b..00000000
--- a/data/graphics/gui/speechbubble.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<skinset name="SpeechBubble" image="bubble.png">
-    <widget type="Window">
-        <!-- Top Row -->
-        <part type="top-left-corner" xpos="0" ypos="0" width="5" height="5" />
-	<part type="top-edge" xpos="5" ypos="0" width="5" height="5" />
-	<part type="top-right-corner" xpos="10" ypos="0" width="5" height="5" />
-
-	<!-- Middle Row -->
-	<part type="left-edge" xpos="0" ypos="5" width="5" height="5" />
-	<part type="bg-quad" xpos="5" ypos="5" width="5" height="5" />
-	<part type="right-edge" xpos="10" ypos="5" width="5" height="5" />
-
-	<!-- Bottom Row -->
-	<part type="bottom-left-corner" xpos="0" ypos="10" width="5" height="5" />
-	<part type="bottom-edge" xpos="5" ypos="10" width="5" height="5" />
-	<part type="bottom-right-corner" xpos="10" ypos="10" width="5" height="5" />
-	</widget>
-</skinset>
diff --git a/data/graphics/gui/theme.xml b/data/graphics/gui/theme.xml
new file mode 100644
index 00000000..652a9422
--- /dev/null
+++ b/data/graphics/gui/theme.xml
@@ -0,0 +1,234 @@
+<theme name="Mana">
+  <color id="TEXT" color="#000000" />
+  <color id="SHADOW" color="#000000" />
+  <color id="OUTLINE" color="#000000" />
+  <color id="PROGRESS_BAR" color="#ffffff" />
+  <color id="BUTTON" color="#000000" />
+  <color id="BUTTON_DISABLED" color="#333333" />
+  <color id="TAB" color="#000000" />
+  <color id="PARTY_CHAT_TAB" color="#f48055" />
+  <color id="PARTY_SOCIAL_TAB" color="#ff00d8" />
+  <color id="BACKGROUND" color="#ffffff" />
+  <color id="HIGHLIGHT" color="#c0c0c0" />
+  <color id="TAB_FLASH" color="#ff0000" effect="pulse" />
+  <color id="SHOP_WARNING" color="#910000" />
+  <color id="ITEM_EQUIPPED" color="#000091" />
+  <color id="CHAT" color="#000000" />
+  <color id="BUBBLE_TEXT" color="#ffffff" />
+  <color id="GM" color="#ff0000" />
+  <color id="PLAYER" color="#1fa052" />
+  <color id="WHISPER" color="#0000ff" />
+  <color id="IS" color="#a08527" />
+  <color id="PARTY" color="#ff00d8" />
+  <color id="GUILD" color="#ff00d8" />
+  <color id="SERVER" color="#8415e2" />
+  <color id="LOGGER" color="#919191" />
+  <color id="HYPERLINK" color="#e50d0d" />
+  <color id="UNKNOWN_ITEM" color="#000000" />
+  <color id="GENERIC" color="#21a5b1" />
+  <color id="HEAD" color="#527fa4" />
+  <color id="USABLE" color="#268d24" />
+  <color id="TORSO" color="#d12aa4" />
+  <color id="ONEHAND" color="#f42a2a" />
+  <color id="LEGS" color="#699900" />
+  <color id="FEET" color="#aa1d48" />
+  <color id="TWOHAND" color="#f46d0e" />
+  <color id="SHIELD" color="#9c2424" />
+  <color id="RING" color="#0000ff" />
+  <color id="NECKLACE" color="#ff00ff" />
+  <color id="ARMS" color="#9c24e8" />
+  <color id="AMMO" color="#8b6311" />
+  <color id="SERVER_VERSION_NOT_SUPPORTED" color="#DC0000" />
+
+  <progressbar id="DEFAULT" color="#969696" />
+  <progressbar id="HP" color="#ff0000,e28000,c38948,0f6a20" />
+  <progressbar id="MP" color="#1a66e6" />
+  <progressbar id="NO_MP" color="#646464" />
+  <progressbar id="EXP" color="#8fc0d3" />
+  <progressbar id="INVY_SLOTS" color="#e1c819" />
+  <progressbar id="WEIGHT" color="#0000ff,ffff00,ff0000" />
+  <progressbar id="JOB" color="#e187cb" />
+
+  <skin type="Window">
+    <state>
+      <img src="window.png" left="4" right="4" top="4" bottom="4" />
+    </state>
+  </skin>
+  <skin type="ResizeGrip">
+    <state>
+      <img src="resize.png" />
+    </state>
+  </skin>
+  <skin type="Popup">
+    <state>
+      <img src="window.png" left="4" right="4" top="4" bottom="4" />
+    </state>
+  </skin>
+  <skin type="SpeechBubble">
+    <state>
+      <img src="bubble.png|W:#000000" left="5" right="5" top="5" bottom="5" height="15" />
+    </state>
+  </skin>
+  <skin type="Button">
+    <state disabled="true">
+      <img src="button_disabled.png" left="10" right="10" top="5" bottom="5" />
+    </state>
+    <state selected="true">
+      <img src="buttonpress.png" left="10" right="10" top="5" bottom="5" />
+    </state>
+    <state hovered="true">
+      <img src="buttonhi.png" left="10" right="10" top="5" bottom="5" />
+    </state>
+    <state>
+      <img src="button.png" left="10" right="10" top="5" bottom="5" />
+    </state>
+  </skin>
+  <skin type="Tab">
+    <state selected="true">
+      <img src="tabselected.png" left="10" right="10" top="5" bottom="9" />
+    </state>
+    <state hovered="true">
+      <img src="tab_hilight.png" left="10" right="10" top="14" bottom="2" />
+    </state>
+    <state>
+      <img src="tab.png" left="10" right="10" top="14" bottom="2" />
+    </state>
+  </skin>
+  <skin type="CheckBox">
+    <state disabled="true" selected="true">
+      <img src="checkbox.png" x="27" y="0" width="9" height="10" offsetX="2" offsetY="2" />
+    </state>
+    <state disabled="true">
+      <img src="checkbox.png" x="18" y="0" width="9" height="10" offsetX="2" offsetY="2" />
+    </state>
+    <state selected="true" hovered="true">
+      <img src="checkbox.png" x="45" y="0" width="9" height="10" offsetX="2" offsetY="2" />
+    </state>
+    <state hovered="true">
+      <img src="checkbox.png" x="36" y="0" width="9" height="10" offsetX="2" offsetY="2" />
+    </state>
+    <state selected="true">
+      <img src="checkbox.png" x="9" y="0" width="9" height="10" offsetX="2" offsetY="2" />
+    </state>
+    <state>
+      <img src="checkbox.png" x="0" y="0" width="9" height="10" offsetX="2" offsetY="2" />
+    </state>
+  </skin>
+  <skin type="RadioButton">
+    <state hovered="true" selected="true">
+      <img src="radioin_highlight.png" offsetX="2" offsetY="2" />
+    </state>
+    <state hovered="true">
+      <img src="radioout_highlight.png" offsetX="2" offsetY="2" />
+    </state>
+    <state selected="true">
+      <img src="radioin.png" offsetX="2" offsetY="2" />
+    </state>
+    <state>
+      <img src="radioout.png" offsetX="2" offsetY="2" />
+    </state>
+  </skin>
+  <skin type="TextField">
+    <state>
+      <img src="deepbox.png" left="4" right="4" top="4" bottom="4" />
+    </state>
+  </skin>
+  <skin type="ScrollArea">
+    <state>
+      <img src="deepbox.png" left="4" right="4" top="4" bottom="4" />
+    </state>
+  </skin>
+  <skin type="DropDownFrame">
+    <state>
+      <img src="deepbox.png" left="4" right="4" top="4" bottom="4" />
+    </state>
+  </skin>
+  <skin type="DropDownButton">
+    <state selected="true" hovered="true">
+      <img src="vscroll_up_pressed.png" offsetX="-2" offsetY="2" />
+    </state>
+    <state selected="true">
+      <img src="vscroll_up_default.png" offsetX="-2" offsetY="2" />
+    </state>
+    <state hovered="true">
+      <img src="vscroll_down_pressed.png" offsetX="-2" offsetY="2" />
+    </state>
+    <state>
+      <img src="vscroll_down_default.png" offsetX="-2" offsetY="2" />
+    </state>
+  </skin>
+  <skin type="ScrollBar">
+    <state hovered="true">
+      <img src="vscroll_highlight.png" left="4" right="4" top="4" bottom="4" />
+    </state>
+    <state>
+      <img src="vscroll_grey.png" left="4" right="4" top="4" bottom="4" />
+    </state>
+  </skin>
+  <skin type="ProgressBar">
+    <state>
+      <img src="vscroll_grey.png" left="4" right="4" top="4" bottom="4" />
+    </state>
+  </skin>
+  <skin type="Slider">
+    <state hovered="true">
+      <img src="slider_hilight.png" height="6" left="4" right="4" top="6" offsetY="4" />
+    </state>
+    <state>
+      <img src="slider.png" height="6" left="4" right="4" top="6" offsetY="4" />
+    </state>
+  </skin>
+  <skin type="SliderHandle">
+    <state hovered="true">
+      <img src="slider_hilight.png" x="6" y="8" width="9" height="10" offsetY="2" />
+    </state>
+    <state>
+      <img src="slider.png" x="6" y="8" width="9" height="10" offsetY="2" />
+    </state>
+  </skin>
+  <skin type="ButtonUp">
+    <state selected="true">
+      <img src="vscroll_up_pressed.png" />
+    </state>
+    <state>
+      <img src="vscroll_up_default.png" />
+    </state>
+  </skin>
+  <skin type="ButtonDown">
+    <state selected="true">
+      <img src="vscroll_down_pressed.png" />
+    </state>
+    <state>
+      <img src="vscroll_down_default.png" />
+    </state>
+  </skin>
+  <skin type="ButtonLeft">
+    <state selected="true">
+      <img src="hscroll_left_pressed.png" />
+    </state>
+    <state>
+      <img src="hscroll_left_default.png" />
+    </state>
+  </skin>
+  <skin type="ButtonRight">
+    <state selected="true">
+      <img src="hscroll_right_pressed.png" />
+    </state>
+    <state>
+      <img src="hscroll_right_default.png" />
+    </state>
+  </skin>
+  <skin type="ButtonClose">
+    <state>
+      <img src="close_button.png" />
+    </state>
+  </skin>
+  <skin type="ButtonSticky">
+    <state selected="true">
+      <img src="sticky_button.png" x="15" width="15" height="15" />
+    </state>
+    <state>
+      <img src="sticky_button.png" width="15" height="15" />
+    </state>
+  </skin>
+</theme>
diff --git a/data/graphics/gui/window.xml b/data/graphics/gui/window.xml
deleted file mode 100644
index f27dbc7f..00000000
--- a/data/graphics/gui/window.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<skinset name="Default" image="window.png">
-	<widget type="Window">
-		<!-- Top Row -->
-		<part type="top-left-corner" xpos="0" ypos="0" width="4" height="4" />
-		<part type="top-edge" xpos="4" ypos="0" width="32" height="4" />
-		<part type="top-right-corner" xpos="36" ypos="0" width="4" height="4" />
-
-		<!-- Middle Row -->
-		<part type="left-edge" xpos="0" ypos="4" width="4" height="216" />
-		<part type="bg-quad" xpos="4" ypos="4" width="32" height="216" />
-		<part type="right-edge" xpos="36" ypos="4" width="4" height="216" />
-
-		<!-- Bottom Row -->
-		<part type="bottom-left-corner" xpos="0" ypos="220" width="4" height="4" />
-		<part type="bottom-edge" xpos="4" ypos="220" width="32" height="4" />
-		<part type="bottom-right-corner" xpos="36" ypos="220" width="4" height="4" />
-	</widget>
-</skinset>
diff --git a/src/being.cpp b/src/being.cpp
index 6cb68994..bbecf712 100644
--- a/src/being.cpp
+++ b/src/being.cpp
@@ -312,7 +312,7 @@ void Being::setSpeech(const std::string &text, int time)
         mText = new Text(mSpeech,
                          getPixelX(), getSpeechTextYPosition(),
                          gcn::Graphics::CENTER,
-                         &userPalette->getColor(UserPalette::PARTICLE),
+                         &Theme::getThemeColor(Theme::BUBBLE_TEXT),
                          true);
     }
 }
@@ -999,7 +999,7 @@ void Being::drawSpeech(int offsetX, int offsetY)
             mText = new Text(mSpeech,
                              getPixelX(), getPixelY() - getHeight(),
                              gcn::Graphics::CENTER,
-                             &userPalette->getColor(UserPalette::PARTICLE),
+                             &Theme::getThemeColor(Theme::BUBBLE_TEXT),
                              true);
         }
     }
diff --git a/src/graphics.cpp b/src/graphics.cpp
index 2b262625..e9ccfc49 100644
--- a/src/graphics.cpp
+++ b/src/graphics.cpp
@@ -48,6 +48,17 @@ void ImageRect::setAlpha(float alpha)
         img->setAlpha(alpha);
 }
 
+int ImageRect::minWidth() const
+{
+    return grid[ImageRect::UPPER_LEFT]->getWidth() + grid[ImageRect::UPPER_RIGHT]->getWidth();
+}
+
+int ImageRect::minHeight() const
+{
+    return grid[ImageRect::UPPER_LEFT]->getHeight() + grid[ImageRect::LOWER_LEFT]->getHeight();
+}
+
+
 void Graphics::updateSize(int width, int height, float /*scale*/)
 {
     mWidth = width;
diff --git a/src/graphics.h b/src/graphics.h
index 4556e499..085ad5d4 100644
--- a/src/graphics.h
+++ b/src/graphics.h
@@ -72,6 +72,8 @@ public:
     Image *grid[9];
 
     void setAlpha(float alpha);
+    int minWidth() const;
+    int minHeight() const;
 };
 
 /**
diff --git a/src/gui/minimap.cpp b/src/gui/minimap.cpp
index 14e2a257..fc7fddd0 100644
--- a/src/gui/minimap.cpp
+++ b/src/gui/minimap.cpp
@@ -59,8 +59,7 @@ Minimap::Minimap():
     setVisible(config.showMinimap, isSticky());
 }
 
-Minimap::~Minimap()
-{}
+Minimap::~Minimap() = default;
 
 void Minimap::setMap(Map *map)
 {
diff --git a/src/gui/speechbubble.cpp b/src/gui/speechbubble.cpp
index 58747d57..72ec8bd2 100644
--- a/src/gui/speechbubble.cpp
+++ b/src/gui/speechbubble.cpp
@@ -27,14 +27,11 @@
 #include "gui/widgets/label.h"
 #include "gui/widgets/textbox.h"
 
-#include "resources/theme.h"
-
 #include <guichan/font.hpp>
-
 #include <guichan/widgets/label.hpp>
 
-SpeechBubble::SpeechBubble():
-    Popup("Speech", "speechbubble.xml")
+SpeechBubble::SpeechBubble()
+    : Popup("Speech", SkinType::SpeechBubble)
 {
     setMinWidth(0);
     setMinHeight(0);
@@ -45,7 +42,7 @@ SpeechBubble::SpeechBubble():
     mSpeechBox = new TextBox;
     mSpeechBox->setEditable(false);
     mSpeechBox->setOpaque(false);
-    mSpeechBox->setTextColor(&Theme::getThemeColor(Theme::CHAT));
+    mSpeechBox->setTextColor(&Theme::getThemeColor(Theme::BUBBLE_TEXT));
 
     add(mCaption);
     add(mSpeechBox);
@@ -60,11 +57,9 @@ void SpeechBubble::setCaption(const std::string &name, const gcn::Color *color)
 
 void SpeechBubble::setText(const std::string &text, bool showName)
 {
-    if (text == mText && (mCaption->getWidth() <= mSpeechBox->getMinWidth()))
+    if (text == mText && mCaption->getWidth() <= mSpeechBox->getMinWidth())
         return;
 
-    mSpeechBox->setTextColor(&Theme::getThemeColor(Theme::TEXT));
-
     int width = mCaption->getWidth();
     mSpeechBox->setTextWrapped(text, 130 > width ? 130 : width);
     const int speechWidth = mSpeechBox->getMinWidth();
diff --git a/src/gui/widgets/button.cpp b/src/gui/widgets/button.cpp
index eb2722e5..215cb5a0 100644
--- a/src/gui/widgets/button.cpp
+++ b/src/gui/widgets/button.cpp
@@ -123,23 +123,21 @@ void Button::init()
 
 void Button::draw(gcn::Graphics *graphics)
 {
-    Theme::WidgetState state;
-    state.width = getWidth();
-    state.height = getHeight();
-    state.enabled = isEnabled();
-    state.hovered = mHasMouse;
-    state.selected = isPressed();
-    state.focused = isFocused();
+    WidgetState state(this);
+    if (mHasMouse)
+        state.flags |= STATE_HOVERED;
+    if (isPressed())
+        state.flags |= STATE_SELECTED;
 
-    gui->getTheme()->drawButton(static_cast<Graphics*>(graphics), state);
+    gui->getTheme()->drawSkin(static_cast<Graphics *>(graphics), SkinType::Button, state);
 
     int mode;
 
-    if (!state.enabled)
+    if (state.flags & STATE_DISABLED)
         mode = BUTTON_DISABLED;
-    else if (state.selected)
+    else if (state.flags & STATE_SELECTED)
         mode = BUTTON_PRESSED;
-    else if (state.hovered || state.focused)
+    else if (state.flags & (STATE_HOVERED | STATE_FOCUSED))
         mode = BUTTON_HIGHLIGHTED;
     else
         mode = BUTTON_STANDARD;
diff --git a/src/gui/widgets/checkbox.cpp b/src/gui/widgets/checkbox.cpp
index e2d13ab2..bea5f1d9 100644
--- a/src/gui/widgets/checkbox.cpp
+++ b/src/gui/widgets/checkbox.cpp
@@ -43,12 +43,13 @@ void CheckBox::draw(gcn::Graphics* graphics)
 
 void CheckBox::drawBox(gcn::Graphics* graphics)
 {
-    Theme::WidgetState state;
-    state.enabled = isEnabled();
-    state.hovered = mHasMouse;
-    state.selected = isSelected();
+    WidgetState state(this);
+    if (mHasMouse)
+        state.flags |= STATE_HOVERED;
+    if (isSelected())
+        state.flags |= STATE_SELECTED;
 
-    gui->getTheme()->drawCheckBox(graphics, state);
+    gui->getTheme()->drawSkin(static_cast<Graphics *>(graphics), SkinType::CheckBox, state);
 }
 
 void CheckBox::mouseEntered(gcn::MouseEvent& event)
diff --git a/src/gui/widgets/dropdown.cpp b/src/gui/widgets/dropdown.cpp
index 7202827b..1d54efd7 100644
--- a/src/gui/widgets/dropdown.cpp
+++ b/src/gui/widgets/dropdown.cpp
@@ -87,23 +87,31 @@ void DropDown::drawFrame(gcn::Graphics *graphics)
 {
     const int bs = getFrameSize();
 
-    Theme::WidgetState state;
-    state.width = getWidth() + bs * 2;
-    state.height = getHeight() + bs * 2;
+    WidgetState state(this);
+    state.width += bs * 2;
+    state.height += bs * 2;
 
-    gui->getTheme()->drawDropDownFrame(static_cast<Graphics*>(graphics), state);
+    gui->getTheme()->drawSkin(static_cast<Graphics *>(graphics), SkinType::DropDownFrame, state);
 }
 
 void DropDown::drawButton(gcn::Graphics *graphics)
 {
-    Theme::WidgetState state;
-    state.width = getWidth();
-    state.height = mDroppedDown ? mFoldedUpHeight : getHeight();
-    state.enabled = isEnabled();
-    state.selected = mDroppedDown;
-    state.hovered = mPushed;
-
-    gui->getTheme()->drawDropDownButton(static_cast<Graphics*>(graphics), state);
+    WidgetState state(this);
+    if (mDroppedDown)
+    {
+        state.height = mFoldedUpHeight;
+        state.flags |= STATE_SELECTED;
+    }
+    if (mPushed)
+        state.flags |= STATE_HOVERED;
+
+    const auto theme = gui->getTheme();
+    const int buttonWidth = theme->getMinWidth(SkinType::DropDownButton);
+
+    // FIXME: Needs support for setting alignment in the theme.
+    state.x = state.width - buttonWidth;
+
+    theme->drawSkin(static_cast<Graphics *>(graphics), SkinType::DropDownButton, state);
 }
 
 // -- KeyListener notifications
diff --git a/src/gui/widgets/popup.cpp b/src/gui/widgets/popup.cpp
index b12acf31..8bbab948 100644
--- a/src/gui/widgets/popup.cpp
+++ b/src/gui/widgets/popup.cpp
@@ -29,14 +29,13 @@
 #include "gui/viewport.h"
 #include "gui/widgets/windowcontainer.h"
 
-#include "resources/theme.h"
-
 #include <guichan/exception.hpp>
 
-Popup::Popup(const std::string &name, const std::string &skin):
-    mPopupName(name),
-    mMaxWidth(graphics->getWidth()),
-    mMaxHeight(graphics->getHeight())
+Popup::Popup(const std::string &name, SkinType skinType)
+    : mPopupName(name)
+    , mMaxWidth(graphics->getWidth())
+    , mMaxHeight(graphics->getHeight())
+    , mSkinType(skinType)
 {
     logger->log("Popup::Popup(\"%s\")", name.c_str());
 
@@ -45,9 +44,6 @@ Popup::Popup(const std::string &name, const std::string &skin):
 
     setPadding(6);
 
-    // Loads the skin
-    mSkin = gui->getTheme()->load(skin);
-
     // Add this window to the window container
     windowContainer->add(this);
 
@@ -58,8 +54,6 @@ Popup::Popup(const std::string &name, const std::string &skin):
 Popup::~Popup()
 {
     logger->log("Popup::~Popup(\"%s\")", mPopupName.c_str());
-
-    mSkin->instances--;
 }
 
 void Popup::setWindowContainer(WindowContainer *wc)
@@ -69,10 +63,7 @@ void Popup::setWindowContainer(WindowContainer *wc)
 
 void Popup::draw(gcn::Graphics *graphics)
 {
-    auto *g = static_cast<Graphics*>(graphics);
-
-    g->drawImageRect(0, 0, getWidth(), getHeight(), mSkin->getBorder());
-
+    gui->getTheme()->drawSkin(static_cast<Graphics *>(graphics), mSkinType, WidgetState(this));
     drawChildren(graphics);
 }
 
@@ -116,12 +107,12 @@ void Popup::setLocationRelativeTo(gcn::Widget *widget)
 
 void Popup::setMinWidth(int width)
 {
-    mMinWidth = std::max(width, mSkin->getMinWidth());
+    mMinWidth = std::max(gui->getTheme()->getMinWidth(mSkinType), width);
 }
 
 void Popup::setMinHeight(int height)
 {
-    mMinHeight = std::max(height, mSkin->getMinHeight());
+    mMinHeight = std::max(gui->getTheme()->getMinHeight(mSkinType), height);
 }
 
 void Popup::setMaxWidth(int width)
diff --git a/src/gui/widgets/popup.h b/src/gui/widgets/popup.h
index 41914984..6a206672 100644
--- a/src/gui/widgets/popup.h
+++ b/src/gui/widgets/popup.h
@@ -25,6 +25,7 @@
 #include "guichanfwd.h"
 
 #include "gui/widgets/container.h"
+#include "resources/theme.h"
 
 #include <guichan/mouselistener.hpp>
 
@@ -52,10 +53,10 @@ class Popup : public Container, public gcn::MouseListener
          *
          * @param name    A human readable name for the popup. Only useful for
          *                debugging purposes.
-         * @param skin    The location where the Popup's skin XML can be found.
+         * @param skinType The skin type used when drawing the popup.
          */
-        Popup(const std::string &name = std::string(),
-              const std::string &skin = "window.xml");
+        explicit Popup(const std::string &name = std::string(),
+                       SkinType skinType = SkinType::Popup);
 
         /**
          * Destructor. Deletes all the added widgets.
@@ -151,12 +152,12 @@ class Popup : public Container, public gcn::MouseListener
         void position(int x, int y);
 
     private:
-        std::string mPopupName;       /**< Name of the popup */
-        int mMinWidth = 100;          /**< Minimum popup width */
-        int mMinHeight = 40;          /**< Minimum popup height */
-        int mMaxWidth;                /**< Maximum popup width */
-        int mMaxHeight;               /**< Maximum popup height */
-        int mPadding;                 /**< Holds the padding of the popup. */
-
-        Skin *mSkin;                  /**< Skin in use by this popup */
+        std::string mPopupName;     /**< Name of the popup */
+        int mMinWidth = 100;        /**< Minimum popup width */
+        int mMinHeight = 40;        /**< Minimum popup height */
+        int mMaxWidth;              /**< Maximum popup width */
+        int mMaxHeight;             /**< Maximum popup height */
+        int mPadding;               /**< Holds the padding of the popup. */
+
+        SkinType mSkinType;         /**< The skin type used when drawing the popup widget. */
 };
diff --git a/src/gui/widgets/radiobutton.cpp b/src/gui/widgets/radiobutton.cpp
index ee2f4983..93ceee07 100644
--- a/src/gui/widgets/radiobutton.cpp
+++ b/src/gui/widgets/radiobutton.cpp
@@ -33,12 +33,13 @@ RadioButton::RadioButton(const std::string &caption,
 
 void RadioButton::drawBox(gcn::Graphics* graphics)
 {
-    Theme::WidgetState state;
-    state.enabled = isEnabled();
-    state.hovered = mHasMouse;
-    state.selected = isSelected();
+    WidgetState state(this);
+    if (mHasMouse)
+        state.flags |= STATE_HOVERED;
+    if (isSelected())
+        state.flags |= STATE_SELECTED;
 
-    gui->getTheme()->drawRadioButton(graphics, state);
+    gui->getTheme()->drawSkin(static_cast<Graphics *>(graphics), SkinType::RadioButton, state);
 }
 
 void RadioButton::draw(gcn::Graphics* graphics)
diff --git a/src/gui/widgets/resizegrip.cpp b/src/gui/widgets/resizegrip.cpp
index bda0b386..69803d07 100644
--- a/src/gui/widgets/resizegrip.cpp
+++ b/src/gui/widgets/resizegrip.cpp
@@ -24,20 +24,21 @@
 #include "graphics.h"
 
 #include "gui/gui.h"
-#include "resources/image.h"
 #include "resources/theme.h"
 
 #include <guichan/graphics.hpp>
 
 ResizeGrip::ResizeGrip()
 {
-    const auto gripImage = gui->getTheme()->getResizeGripImage();
-    setSize(gripImage->getWidth() + 2,
-            gripImage->getHeight() + 2);
+    const auto theme = gui->getTheme();
+    const auto minWidth = theme->getMinWidth(SkinType::ResizeGrip);
+    const auto minHeight = theme->getMinHeight(SkinType::ResizeGrip);
+    setSize(minWidth + 2, minHeight + 2);
 }
 
 void ResizeGrip::draw(gcn::Graphics *graphics)
 {
-    const auto gripImage = gui->getTheme()->getResizeGripImage();
-    static_cast<Graphics*>(graphics)->drawImage(gripImage, 0, 0);
+    gui->getTheme()->drawSkin(static_cast<Graphics *>(graphics),
+                              SkinType::ResizeGrip,
+                              WidgetState(this));
 }
diff --git a/src/gui/widgets/scrollarea.cpp b/src/gui/widgets/scrollarea.cpp
index 6a2cb7e4..62c37c64 100644
--- a/src/gui/widgets/scrollarea.cpp
+++ b/src/gui/widgets/scrollarea.cpp
@@ -24,7 +24,6 @@
 #include "graphics.h"
 
 #include "gui/gui.h"
-#include "resources/theme.h"
 
 ScrollArea::ScrollArea()
 {
@@ -107,11 +106,11 @@ void ScrollArea::drawFrame(gcn::Graphics *graphics)
 
     const int bs = getFrameSize();
 
-    Theme::WidgetState state;
-    state.width = getWidth() + bs * 2;
-    state.height = getHeight() + bs * 2;
+    WidgetState state(this);
+    state.width += bs * 2;
+    state.height += + bs * 2;
 
-    gui->getTheme()->drawScrollAreaFrame(static_cast<Graphics *>(graphics), state);
+    gui->getTheme()->drawSkin(static_cast<Graphics *>(graphics), SkinType::ScrollArea, state);
 }
 
 void ScrollArea::setOpaque(bool opaque)
@@ -127,38 +126,22 @@ void ScrollArea::drawBackground(gcn::Graphics *graphics)
 
 void ScrollArea::drawUpButton(gcn::Graphics *graphics)
 {
-    auto theme = gui->getTheme();
-    theme->drawScrollAreaButton(static_cast<Graphics *>(graphics),
-                                Theme::ARROW_UP,
-                                mUpButtonPressed,
-                                getUpButtonDimension());
+    drawButton(graphics, SkinType::ButtonUp, mUpButtonPressed, getUpButtonDimension());
 }
 
 void ScrollArea::drawDownButton(gcn::Graphics *graphics)
 {
-    auto theme = gui->getTheme();
-    theme->drawScrollAreaButton(static_cast<Graphics *>(graphics),
-                                Theme::ARROW_DOWN,
-                                mDownButtonPressed,
-                                getDownButtonDimension());
+    drawButton(graphics, SkinType::ButtonDown, mDownButtonPressed, getDownButtonDimension());
 }
 
 void ScrollArea::drawLeftButton(gcn::Graphics *graphics)
 {
-    auto theme = gui->getTheme();
-    theme->drawScrollAreaButton(static_cast<Graphics *>(graphics),
-                                Theme::ARROW_LEFT,
-                                mLeftButtonPressed,
-                                getLeftButtonDimension());
+    drawButton(graphics, SkinType::ButtonLeft, mLeftButtonPressed, getLeftButtonDimension());
 }
 
 void ScrollArea::drawRightButton(gcn::Graphics *graphics)
 {
-    auto theme = gui->getTheme();
-    theme->drawScrollAreaButton(static_cast<Graphics *>(graphics),
-                                Theme::ARROW_RIGHT,
-                                mRightButtonPressed,
-                                getRightButtonDimension());
+    drawButton(graphics, SkinType::ButtonRight, mRightButtonPressed, getRightButtonDimension());
 }
 
 void ScrollArea::drawVBar(gcn::Graphics *graphics)
@@ -177,18 +160,45 @@ void ScrollArea::drawHBar(gcn::Graphics *graphics)
 
 void ScrollArea::drawVMarker(gcn::Graphics *graphics)
 {
-    auto theme = gui->getTheme();
-    theme->drawScrollAreaMarker(static_cast<Graphics *>(graphics),
-                                mHasMouse && (mX > (getWidth() - getScrollbarWidth())),
-                                getVerticalMarkerDimension());
+    drawMarker(static_cast<Graphics *>(graphics),
+               mHasMouse && (mX > (getWidth() - getScrollbarWidth())),
+               getVerticalMarkerDimension());
 }
 
 void ScrollArea::drawHMarker(gcn::Graphics *graphics)
 {
-    auto theme = gui->getTheme();
-    theme->drawScrollAreaMarker(static_cast<Graphics *>(graphics),
-                                mHasMouse && (mY > (getHeight() - getScrollbarWidth())),
-                                getHorizontalMarkerDimension());
+    drawMarker(static_cast<Graphics *>(graphics),
+               mHasMouse && (mY > (getHeight() - getScrollbarWidth())),
+               getHorizontalMarkerDimension());
+}
+
+void ScrollArea::drawButton(gcn::Graphics *graphics,
+                            SkinType skinType,
+                            bool pressed,
+                            const gcn::Rectangle &dim)
+{
+    WidgetState state;
+    state.x = dim.x;
+    state.y = dim.y;
+    state.width = dim.width;
+    state.height = dim.height;
+    if (pressed)
+        state.flags |= STATE_SELECTED;
+
+    gui->getTheme()->drawSkin(static_cast<Graphics *>(graphics), skinType, state);
+}
+
+void ScrollArea::drawMarker(gcn::Graphics *graphics, bool hovered, const gcn::Rectangle &dim)
+{
+    WidgetState state;
+    state.x = dim.x;
+    state.y = dim.y;
+    state.width = dim.width;
+    state.height = dim.height;
+    if (hovered)
+        state.flags |= STATE_HOVERED;
+
+    gui->getTheme()->drawSkin(static_cast<Graphics *>(graphics), SkinType::ScrollBar, state);
 }
 
 void ScrollArea::mouseMoved(gcn::MouseEvent& event)
diff --git a/src/gui/widgets/scrollarea.h b/src/gui/widgets/scrollarea.h
index ab0eb026..17f69548 100644
--- a/src/gui/widgets/scrollarea.h
+++ b/src/gui/widgets/scrollarea.h
@@ -21,6 +21,8 @@
 
 #pragma once
 
+#include "resources/theme.h"
+
 #include <guichan/widgetlistener.hpp>
 #include <guichan/widgets/scrollarea.hpp>
 
@@ -108,6 +110,12 @@ class ScrollArea : public gcn::ScrollArea, public gcn::WidgetListener
         void drawVMarker(gcn::Graphics *graphics) override;
         void drawHMarker(gcn::Graphics *graphics) override;
 
+        static void drawButton(gcn::Graphics *graphics,
+                               SkinType skinType,
+                               bool pressed,
+                               const gcn::Rectangle &dim);
+        static void drawMarker(gcn::Graphics *graphics, bool hovered, const gcn::Rectangle &dim);
+
         int mX = 0;
         int mY = 0;
         bool mHasMouse = false;
diff --git a/src/gui/widgets/slider.cpp b/src/gui/widgets/slider.cpp
index 20a843f3..7ad5aa60 100644
--- a/src/gui/widgets/slider.cpp
+++ b/src/gui/widgets/slider.cpp
@@ -41,19 +41,21 @@ Slider::Slider(double scaleStart, double scaleEnd):
 void Slider::init()
 {
     setFrameSize(0);
-    setMarkerLength(gui->getTheme()->getSliderMarkerLength());
+    setMarkerLength(gui->getTheme()->getMinWidth(SkinType::SliderHandle));
 }
 
 void Slider::draw(gcn::Graphics *graphics)
 {
-    Theme::WidgetState state;
-    state.width = getWidth();
-    state.height = getHeight();
-    state.enabled = isEnabled();
-    state.hovered = mHasMouse;
+    WidgetState state(this);
+    if (mHasMouse)
+        state.flags |= STATE_HOVERED;
 
     auto theme = gui->getTheme();
-    theme->drawSlider(static_cast<Graphics*>(graphics), state, getMarkerPosition());
+    theme->drawSkin(static_cast<Graphics*>(graphics), SkinType::Slider, state);
+
+    WidgetState handleState(state);
+    handleState.x += getMarkerPosition();
+    theme->drawSkin(static_cast<Graphics*>(graphics), SkinType::SliderHandle, handleState);
 }
 
 void Slider::drawMarker(gcn::Graphics *graphics)
diff --git a/src/gui/widgets/tab.cpp b/src/gui/widgets/tab.cpp
index 1a857e55..5779b561 100644
--- a/src/gui/widgets/tab.cpp
+++ b/src/gui/widgets/tab.cpp
@@ -44,18 +44,16 @@ void Tab::init()
 
 void Tab::draw(gcn::Graphics *graphics)
 {
-    Theme::WidgetState state;
-    state.width = getWidth();
-    state.height = getHeight();
-    state.enabled = isEnabled();
-    state.hovered = mHasMouse;
-    state.selected = mTabbedArea && mTabbedArea->isTabSelected(this);
-    state.focused = isFocused();
+    WidgetState state(this);
+    if (mHasMouse)
+        state.flags |= STATE_HOVERED;
+    if (mTabbedArea && mTabbedArea->isTabSelected(this))
+        state.flags |= STATE_SELECTED;
 
-    gui->getTheme()->drawTab(static_cast<Graphics*>(graphics), state);
+    gui->getTheme()->drawSkin(static_cast<Graphics *>(graphics), SkinType::Tab, state);
 
     // if tab is selected, it doesnt need to highlight activity
-    if (state.selected)
+    if (state.flags & STATE_SELECTED)
         mFlash = false;
 
     if (mFlash)
diff --git a/src/gui/widgets/textfield.cpp b/src/gui/widgets/textfield.cpp
index a1a20e90..46fa18ae 100644
--- a/src/gui/widgets/textfield.cpp
+++ b/src/gui/widgets/textfield.cpp
@@ -60,11 +60,11 @@ void TextField::drawFrame(gcn::Graphics *graphics)
 {
     const int bs = getFrameSize();
 
-    Theme::WidgetState state;
-    state.width = getWidth() + bs * 2;
-    state.height = getHeight() + bs * 2;
+    WidgetState state(this);
+    state.width += bs * 2;
+    state.height += bs * 2;
 
-    gui->getTheme()->drawTextFieldFrame(static_cast<Graphics*>(graphics), state);
+    gui->getTheme()->drawSkin(static_cast<Graphics *>(graphics), SkinType::TextField, state);
 }
 
 void TextField::setNumeric(bool numeric)
diff --git a/src/gui/widgets/window.cpp b/src/gui/widgets/window.cpp
index 52ee917e..d47e1a29 100644
--- a/src/gui/widgets/window.cpp
+++ b/src/gui/widgets/window.cpp
@@ -31,7 +31,6 @@
 #include "gui/widgets/resizegrip.h"
 #include "gui/widgets/windowcontainer.h"
 
-#include "resources/image.h"
 #include "resources/theme.h"
 
 #include <guichan/exception.hpp>
@@ -43,13 +42,12 @@
 int Window::instances = 0;
 int Window::mouseResize = 0;
 
-Window::Window(const std::string &caption, bool modal, Window *parent,
-               const std::string &skin):
-    gcn::Window(caption),
-    mParent(parent),
-    mModal(modal),
-    mMaxWinWidth(graphics->getWidth()),
-    mMaxWinHeight(graphics->getHeight())
+Window::Window(const std::string &caption, bool modal, Window *parent)
+    : gcn::Window(caption)
+    , mParent(parent)
+    , mModal(modal)
+    , mMaxWinWidth(graphics->getWidth())
+    , mMaxWinHeight(graphics->getHeight())
 {
     logger->log("Window::Window(\"%s\")", caption.c_str());
 
@@ -62,9 +60,6 @@ Window::Window(const std::string &caption, bool modal, Window *parent,
     setPadding(3);
     setTitleBarHeight(20);
 
-    // Loads the skin
-    mSkin = gui->getTheme()->load(skin);
-
     // Add this window to the window container
     windowContainer->add(this);
 
@@ -94,8 +89,6 @@ Window::~Window()
     removeWidgetListener(this);
 
     instances--;
-
-    mSkin->instances--;
 }
 
 void Window::setWindowContainer(WindowContainer *wc)
@@ -105,9 +98,11 @@ void Window::setWindowContainer(WindowContainer *wc)
 
 void Window::draw(gcn::Graphics *graphics)
 {
-    auto *g = static_cast<Graphics*>(graphics);
+    auto g = static_cast<Graphics*>(graphics);
+    auto theme = gui->getTheme();
 
-    g->drawImageRect(0, 0, getWidth(), getHeight(), mSkin->getBorder());
+    WidgetState state(this);
+    theme->drawSkin(g, SkinType::Window, state);
 
     // Draw title
     if (mShowTitle)
@@ -117,23 +112,26 @@ void Window::draw(gcn::Graphics *graphics)
         g->drawText(getCaption(), 7, 5, gcn::Graphics::LEFT);
     }
 
+    const int closeButtonWidth = theme->getMinWidth(SkinType::ButtonClose);
+    const int stickyButtonWidth = theme->getMinWidth(SkinType::ButtonSticky);
+
     // Draw Close Button
     if (mCloseButton)
     {
-        g->drawImage(mSkin->getCloseImage(),
-            getWidth() - mSkin->getCloseImage()->getWidth() - getPadding(),
-            getPadding());
+        state.x = state.width - closeButtonWidth - getPadding();
+        state.y = getPadding();
+        theme->drawSkin(g, SkinType::ButtonClose, state);
     }
 
     // Draw Sticky Button
     if (mStickyButton)
     {
-        Image *button = mSkin->getStickyImage(mSticky);
-        int x = getWidth() - button->getWidth() - getPadding();
+        state.flags = mSticky ? STATE_SELECTED : 0;
+        state.x = state.width - stickyButtonWidth - getPadding();
+        state.y = getPadding();
         if (mCloseButton)
-            x -= mSkin->getCloseImage()->getWidth();
-
-        g->drawImage(button, x, getPadding());
+            state.x -= closeButtonWidth;
+        theme->drawSkin(g, SkinType::ButtonSticky, state);
     }
 
     drawChildren(graphics);
@@ -218,13 +216,12 @@ void Window::setLocationRelativeTo(ImageRect::ImagePosition position,
 
 void Window::setMinWidth(int width)
 {
-    mMinWinWidth = width > mSkin->getMinWidth() ? width : mSkin->getMinWidth();
+    mMinWinWidth = std::max(gui->getTheme()->getMinWidth(SkinType::Window), width);
 }
 
 void Window::setMinHeight(int height)
 {
-    mMinWinHeight = height > mSkin->getMinHeight() ?
-                        height : mSkin->getMinHeight();
+    mMinWinHeight = std::max(gui->getTheme()->getMinHeight(SkinType::Window), height);
 }
 
 void Window::setMaxWidth(int width)
@@ -338,38 +335,42 @@ void Window::mousePressed(gcn::MouseEvent &event)
 
     if (event.getButton() == gcn::MouseEvent::LEFT)
     {
+        auto theme = gui->getTheme();
+
         const int x = event.getX();
         const int y = event.getY();
 
+        const int closeButtonWidth = theme->getMinWidth(SkinType::ButtonClose);
+        const int closeButtonHeight = theme->getMinHeight(SkinType::ButtonClose);
+        const int stickyButtonWidth = theme->getMinWidth(SkinType::ButtonSticky);
+        const int stickyButtonHeight = theme->getMinHeight(SkinType::ButtonSticky);
+
         // Handle close button
         if (mCloseButton)
         {
-            gcn::Rectangle closeButtonRect(
-                getWidth() - mSkin->getCloseImage()->getWidth() - getPadding(),
-                getPadding(),
-                mSkin->getCloseImage()->getWidth(),
-                mSkin->getCloseImage()->getHeight());
+            gcn::Rectangle closeButtonRect(getWidth() - closeButtonWidth - getPadding(),
+                                           getPadding(),
+                                           closeButtonWidth,
+                                           closeButtonHeight);
 
             if (closeButtonRect.isPointInRect(x, y))
-            {
                 close();
-            }
         }
 
         // Handle sticky button
         if (mStickyButton)
         {
-            Image *button = mSkin->getStickyImage(mSticky);
-            int rx = getWidth() - button->getWidth() - getPadding();
+            int stickyButtonX = getWidth() - stickyButtonWidth - getPadding();
             if (mCloseButton)
-                rx -= mSkin->getCloseImage()->getWidth();
-            gcn::Rectangle stickyButtonRect(rx, getPadding(),
-                                    button->getWidth(), button->getHeight());
+                stickyButtonX -= closeButtonWidth;
+
+            gcn::Rectangle stickyButtonRect(stickyButtonX,
+                                            getPadding(),
+                                            stickyButtonWidth,
+                                            stickyButtonHeight);
 
             if (stickyButtonRect.isPointInRect(x, y))
-            {
                 setSticky(!isSticky());
-            }
         }
 
         // Handle window resizing
diff --git a/src/gui/widgets/window.h b/src/gui/widgets/window.h
index 686e5f43..916d767b 100644
--- a/src/gui/widgets/window.h
+++ b/src/gui/widgets/window.h
@@ -53,10 +53,9 @@ class Window : public gcn::Window, gcn::WidgetListener
          * @param parent  The parent window. This is the window standing above
          *                this one in the window hiearchy. When reordering,
          *                a window will never go below its parent window.
-         * @param skin    The location where the window's skin XML can be found.
          */
         Window(const std::string &caption = "Window", bool modal = false,
-               Window *parent = nullptr, const std::string &skin = "window.xml");
+               Window *parent = nullptr);
 
         /**
          * Destructor. Deletes all the added widgets.
@@ -398,8 +397,6 @@ class Window : public gcn::Window, gcn::WidgetListener
 
         static int instances;         /**< Number of Window instances */
 
-        Skin *mSkin;                  /**< Skin in use by this window */
-
         /**
          * The width of the resize border. Is independent of the actual window
          * border width, and determines mostly the size of the corner area
diff --git a/src/resources/theme.cpp b/src/resources/theme.cpp
index 41d09ab2..a3cbbabd 100644
--- a/src/resources/theme.cpp
+++ b/src/resources/theme.cpp
@@ -33,10 +33,9 @@
 #include "resources/resourcemanager.h"
 
 #include "utils/filesystem.h"
-#include "utils/stringutils.h"
-#include "utils/xml.h"
 
 #include <guichan/font.hpp>
+#include <guichan/widget.hpp>
 
 #include <algorithm>
 #include <optional>
@@ -71,425 +70,207 @@ static std::optional<std::string> findThemePath(const std::string &theme)
 }
 
 
-Skin::Skin(ImageRect skin, Image *close, Image *stickyUp, Image *stickyDown):
-    mBorder(std::move(skin)),
-    mCloseImage(close),
-    mStickyImageUp(stickyUp),
-    mStickyImageDown(stickyDown)
-{}
-
-Skin::~Skin() = default;
-
-void Skin::updateAlpha(float alpha)
+WidgetState::WidgetState(const gcn::Widget *widget)
+    : width(widget->getWidth())
+    , height(widget->getHeight())
 {
-    mBorder.setAlpha(alpha);
+    // x and y are not set based on the widget because the rendering usually
+    // happens in local coordinates.
 
-    mCloseImage->setAlpha(alpha);
-    mStickyImageUp->setAlpha(alpha);
-    mStickyImageDown->setAlpha(alpha);
+    if (!widget->isEnabled())
+        flags |= STATE_DISABLED;
+    if (widget->isFocused())
+        flags |= STATE_FOCUSED;
 }
 
-int Skin::getMinWidth() const
+
+Skin::~Skin()
 {
-    return mBorder.grid[ImageRect::UPPER_LEFT]->getWidth() +
-           mBorder.grid[ImageRect::UPPER_RIGHT]->getWidth();
+    // Raw Image* need explicit deletion
+    for (auto &state : mStates)
+        for (auto &part : state.parts)
+            if (auto image = std::get_if<Image *>(&part.data))
+                delete *image;
 }
 
-int Skin::getMinHeight() const
+void Skin::addState(SkinState state)
 {
-    return mBorder.grid[ImageRect::UPPER_LEFT]->getHeight() +
-           mBorder.grid[ImageRect::LOWER_LEFT]->getHeight();
+    mStates.emplace_back(std::move(state));
 }
 
-
-enum {
-    BUTTON_MODE_STANDARD,       // 0
-    BUTTON_MODE_HIGHLIGHTED,    // 1
-    BUTTON_MODE_PRESSED,        // 2
-    BUTTON_MODE_DISABLED,       // 3
-    BUTTON_MODE_COUNT           // 4 - Must be last.
-};
-
-enum {
-    TAB_STANDARD,    // 0
-    TAB_HIGHLIGHTED, // 1
-    TAB_SELECTED,    // 2
-    TAB_UNUSED,      // 3
-    TAB_COUNT        // 4 - Must be last.
-};
-
-Theme::Theme(const std::string &path)
-    : Palette(THEME_COLORS_END)
-    , mThemePath(path)
-    , mProgressColors(THEME_PROG_END)
+void Skin::draw(Graphics *graphics, const WidgetState &state) const
 {
-    listen(Event::ConfigChannel);
-    loadColors();
-
-    mColors[HIGHLIGHT].ch = 'H';
-    mColors[CHAT].ch = 'C';
-    mColors[GM].ch = 'G';
-    mColors[PLAYER].ch = 'Y';
-    mColors[WHISPER].ch = 'W';
-    mColors[IS].ch = 'I';
-    mColors[PARTY].ch = 'P';
-    mColors[GUILD].ch = 'U';
-    mColors[SERVER].ch = 'S';
-    mColors[LOGGER].ch = 'L';
-    mColors[HYPERLINK].ch = '<';
-
-    // Button skin
-    struct ButtonData
+    for (const auto &skinState : mStates)
     {
-        char const *file;
-        int gridX;
-        int gridY;
-    };
-
-    constexpr ButtonData data[BUTTON_MODE_COUNT] = {
-        { "button.png", 0, 0 },
-        { "buttonhi.png", 9, 4 },
-        { "buttonpress.png", 16, 19 },
-        { "button_disabled.png", 25, 23 }
-    };
+        if (skinState.stateFlags != (skinState.setFlags & state.flags))
+            continue;
 
-    for (int mode = 0; mode < BUTTON_MODE_COUNT; ++mode)
-    {
-        auto modeImage = getImage(data[mode].file);
-        int a = 0;
-        for (int y = 0; y < 3; y++)
+        for (const auto &part : skinState.parts)
         {
-            for (int x = 0; x < 3; x++)
-            {
-                mButton[mode].grid[a] = modeImage->getSubImage(
-                    data[x].gridX, data[y].gridY,
-                    data[x + 1].gridX - data[x].gridX + 1,
-                    data[y + 1].gridY - data[y].gridY + 1);
-                a++;
-            }
+            std::visit([&](const auto &data) {
+                using T = std::decay_t<decltype(data)>;
+
+                if constexpr (std::is_same_v<T, ImageRect>)
+                {
+                    graphics->drawImageRect(state.x + part.offsetX,
+                                            state.y + part.offsetY,
+                                            state.width,
+                                            state.height,
+                                            data);
+                }
+                else if constexpr (std::is_same_v<T, Image*>)
+                {
+                    graphics->drawImage(data, state.x + part.offsetX, state.y + part.offsetY);
+                }
+            }, part.data);
         }
-    }
 
-    // Tab skin
-    struct TabData
-    {
-        char const *file;
-        int gridX[4];
-        int gridY[4];
-    };
+        break;  // Only draw the first matching state
+    }
+}
 
-    constexpr TabData tabData[TAB_COUNT] = {
-        { "tab.png", {0, 9, 16, 25}, {0, 13, 19, 20} },
-        { "tab_hilight.png", {0, 9, 16, 25}, {0, 13, 19, 20} },
-        { "tabselected.png", {0, 9, 16, 25}, {0, 4, 12, 20} },
-        { "tab.png", {0, 9, 16, 25}, {0, 13, 19, 20} }
-    };
+int Skin::getMinWidth() const
+{
+    int minWidth = 0;
 
-    for (int mode = 0; mode < TAB_COUNT; mode++)
+    for (const auto &state : mStates)
     {
-        auto tabImage = getImage(tabData[mode].file);
-        int a = 0;
-        for (int y = 0; y < 3; y++)
+        for (const auto &part : state.parts)
         {
-            for (int x = 0; x < 3; x++)
-            {
-                mTabImg[mode].grid[a] = tabImage->getSubImage(
-                    tabData[mode].gridX[x], tabData[mode].gridY[y],
-                    tabData[mode].gridX[x + 1] - tabData[mode].gridX[x] + 1,
-                    tabData[mode].gridY[y + 1] - tabData[mode].gridY[y] + 1);
-                a++;
-            }
+            if (auto imageRect = std::get_if<ImageRect>(&part.data))
+                minWidth = std::max(minWidth, imageRect->minWidth());
+            else if (auto img = std::get_if<Image *>(&part.data))
+                minWidth = std::max(minWidth, (*img)->getWidth());
         }
-        mTabImg[mode].setAlpha(mAlpha);
     }
 
-    // TextField images
-    auto deepBox = getImage("deepbox.png");
-    constexpr int gridx[4] = {0, 3, 28, 31};
-    constexpr int gridy[4] = {0, 3, 28, 31};
-    int a = 0;
+    return minWidth;
+}
+
+int Skin::getMinHeight() const
+{
+    int minHeight = 0;
 
-    for (int y = 0; y < 3; y++)
+    for (const auto &state : mStates)
     {
-        for (int x = 0; x < 3; x++)
+        for (const auto &part : state.parts)
         {
-            mDeepBoxImageRect.grid[a] = deepBox->getSubImage(gridx[x],
-                                                             gridy[y],
-                                                             gridx[x + 1] - gridx[x] + 1,
-                                                             gridy[y + 1] - gridy[y] + 1);
-            a++;
+            if (auto imageRect = std::get_if<ImageRect>(&part.data))
+                minHeight = std::max(minHeight, imageRect->minHeight());
+            else if (auto img = std::get_if<Image *>(&part.data))
+                minHeight = std::max(minHeight, (*img)->getHeight());
         }
     }
 
-    // CheckBox images
-    auto checkBox = getImage("checkbox.png");
-    mCheckBoxNormal.reset(checkBox->getSubImage(0, 0, 9, 10));
-    mCheckBoxChecked.reset(checkBox->getSubImage(9, 0, 9, 10));
-    mCheckBoxDisabled.reset(checkBox->getSubImage(18, 0, 9, 10));
-    mCheckBoxDisabledChecked.reset(checkBox->getSubImage(27, 0, 9, 10));
-    mCheckBoxNormalHi.reset(checkBox->getSubImage(36, 0, 9, 10));
-    mCheckBoxCheckedHi.reset(checkBox->getSubImage(45, 0, 9, 10));
-
-    // RadioButton images
-    mRadioNormal = getImage("radioout.png");
-    mRadioChecked = getImage("radioin.png");
-    mRadioDisabled = getImage("radioout.png");
-    mRadioDisabledChecked = getImage("radioin.png");
-    mRadioNormalHi = getImage("radioout_highlight.png");
-    mRadioCheckedHi = getImage("radioin_highlight.png");
-
-    // Slider images
-    int x, y, w, h, o1, o2;
-    auto slider = getImage("slider.png");
-    auto sliderHi = getImage("slider_hilight.png");
-
-    x = 0; y = 0;
-    w = 15; h = 6;
-    o1 = 4; o2 = 11;
-    hStart.reset(slider->getSubImage(x, y, o1 - x, h));
-    hMid.reset(slider->getSubImage(o1, y, o2 - o1, h));
-    hEnd.reset(slider->getSubImage(o2, y, w - o2 + x, h));
-    hStartHi.reset(sliderHi->getSubImage(x, y, o1 - x, h));
-    hMidHi.reset(sliderHi->getSubImage(o1, y, o2 - o1, h));
-    hEndHi.reset(sliderHi->getSubImage(o2, y, w - o2 + x, h));
-
-    x = 6; y = 8;
-    w = 9; h = 10;
-    hGrip.reset(slider->getSubImage(x, y, w, h));
-    hGripHi.reset(sliderHi->getSubImage(x, y, w, h));
-
-    x = 0; y = 6;
-    w = 6; h = 21;
-    o1 = 10; o2 = 18;
-    vStart.reset(slider->getSubImage(x, y, w, o1 - y));
-    vMid.reset(slider->getSubImage(x, o1, w, o2 - o1));
-    vEnd.reset(slider->getSubImage(x, o2, w, h - o2 + y));
-    vStartHi.reset(sliderHi->getSubImage(x, y, w, o1 - y));
-    vMidHi.reset(sliderHi->getSubImage(x, o1, w, o2 - o1));
-    vEndHi.reset(sliderHi->getSubImage(x, o2, w, h - o2 + y));
-
-    x = 6; y = 8;
-    w = 9; h = 10;
-    vGrip.reset(slider->getSubImage(x, y, w, h));
-    vGripHi.reset(sliderHi->getSubImage(x, y, w, h));
-
-    // ProgressBar and ScrollArea images
-    auto vscroll = getImage("vscroll_grey.png");
-    auto vscrollHi = getImage("vscroll_highlight.png");
-
-    constexpr int vsgridx[4] = {0, 4, 7, 11};
-    constexpr int vsgridy[4] = {0, 4, 15, 19};
-    a = 0;
-
-    for (int y = 0; y < 3; y++)
+    return minHeight;
+}
+
+void Skin::updateAlpha(float alpha)
+{
+    for (auto &state : mStates)
     {
-        for (int x = 0; x < 3; x++)
+        for (auto &part : state.parts)
         {
-            mScrollBarMarker.grid[a] = vscroll->getSubImage(
-                vsgridx[x], vsgridy[y],
-                vsgridx[x + 1] - vsgridx[x],
-                vsgridy[y + 1] - vsgridy[y]);
-            mScrollBarMarkerHi.grid[a] = vscrollHi->getSubImage(
-                vsgridx[x], vsgridy[y],
-                vsgridx[x + 1] - vsgridx[x],
-                vsgridy[y + 1] - vsgridy[y]);
-            a++;
+            if (auto rect = std::get_if<ImageRect>(&part.data))
+                rect->setAlpha(alpha);
+            else if (auto img = std::get_if<Image *>(&part.data))
+                (*img)->setAlpha(alpha);
         }
     }
-
-    mScrollBarMarker.setAlpha(config.guiAlpha);
-    mScrollBarMarkerHi.setAlpha(config.guiAlpha);
-
-    // DropDown and ScrollArea buttons
-    mArrowButtons[ARROW_UP][0] = getImage("vscroll_up_default.png");
-    mArrowButtons[ARROW_DOWN][0] = getImage("vscroll_down_default.png");
-    mArrowButtons[ARROW_LEFT][0] = getImage("hscroll_left_default.png");
-    mArrowButtons[ARROW_RIGHT][0] = getImage("hscroll_right_default.png");
-    mArrowButtons[ARROW_UP][1] = getImage("vscroll_up_pressed.png");
-    mArrowButtons[ARROW_DOWN][1] = getImage("vscroll_down_pressed.png");
-    mArrowButtons[ARROW_LEFT][1] = getImage("hscroll_left_pressed.png");
-    mArrowButtons[ARROW_RIGHT][1] = getImage("hscroll_right_pressed.png");
-
-    mResizeGripImage = getImage("resize.png");
 }
 
-Theme::~Theme() = default;
 
-const gcn::Color &Theme::getThemeColor(int type, int alpha)
+Theme::Theme(const std::string &path)
+    : Palette(THEME_COLORS_END)
+    , mThemePath(path)
+    , mProgressColors(THEME_PROG_END)
 {
-    return gui->getTheme()->getColor(type, alpha);
-}
+    listen(Event::ConfigChannel);
+    readTheme("theme.xml");
 
-const gcn::Color &Theme::getThemeColor(char c, bool &valid)
-{
-    return gui->getTheme()->getColor(c, valid);
+    mColors[HIGHLIGHT].ch = 'H';
+    mColors[CHAT].ch = 'C';
+    mColors[GM].ch = 'G';
+    mColors[PLAYER].ch = 'Y';
+    mColors[WHISPER].ch = 'W';
+    mColors[IS].ch = 'I';
+    mColors[PARTY].ch = 'P';
+    mColors[GUILD].ch = 'U';
+    mColors[SERVER].ch = 'S';
+    mColors[LOGGER].ch = 'L';
+    mColors[HYPERLINK].ch = '<';
 }
 
-gcn::Color Theme::getProgressColor(int type, float progress)
-{
-    const auto &dye = gui->getTheme()->mProgressColors[type];
-
-    int color[3] = {0, 0, 0};
-    dye->getColor(progress, color);
-
-    return gcn::Color(color[0], color[1], color[2]);
-}
+Theme::~Theme() = default;
 
-Skin *Theme::load(const std::string &filename)
+std::string Theme::prepareThemePath()
 {
-    // Check if this skin was already loaded
-    auto skinIterator = mSkins.find(filename);
-    if (skinIterator != mSkins.end())
-    {
-        auto &skin = skinIterator->second;
-        skin->instances++;
-        return skin.get();
-    }
+    initDefaultThemePath();
 
-    auto skin = readSkin(filename);
-    if (!skin)
-    {
-        logger->error(strprintf("Error: Loading default skin '%s' failed. "
-                                "Make sure the skin file is valid.",
-                                mThemePath.c_str()));
-    }
+    // Try theme from settings
+    auto themePath = findThemePath(config.theme);
+
+    // Try theme from branding
+    if (!themePath)
+        themePath = findThemePath(branding.getStringValue("theme"));
 
-    // Add the skin to the loaded skins
-    return (mSkins[filename] = std::move(skin)).get();
+    return themePath.value_or(defaultThemePath);
 }
 
-void Theme::drawButton(Graphics *graphics, const WidgetState &state) const
+std::string Theme::resolvePath(const std::string &path) const
 {
-    int mode;
-
-    if (!state.enabled)
-        mode = BUTTON_MODE_DISABLED;
-    else if (state.selected)
-        mode = BUTTON_MODE_PRESSED;
-    else if (state.hovered || state.focused)
-        mode = BUTTON_MODE_HIGHLIGHTED;
+    // Need to strip off any dye info for the existence tests
+    int pos = path.find('|');
+    std::string file;
+    if (pos > 0)
+        file = path.substr(0, pos);
     else
-        mode = BUTTON_MODE_STANDARD;
+        file = path;
 
-    graphics->drawImageRect(0, 0, state.width, state.height, mButton[mode]);
+    // Try the theme
+    file = mThemePath + "/" + file;
+    if (FS::exists(file))
+        return mThemePath + "/" + path;
+
+    // Backup
+    return defaultThemePath + "/" + path;
 }
 
-void Theme::drawTextFieldFrame(Graphics *graphics, const WidgetState &state) const
+ResourceRef<Image> Theme::getImage(const std::string &path) const
 {
-    graphics->drawImageRect(0, 0, state.width, state.height, mDeepBoxImageRect);
+    return ResourceManager::getInstance()->getImage(resolvePath(path));
 }
 
-void Theme::drawTab(Graphics *graphics, const WidgetState &state) const
+ResourceRef<Image> Theme::getImageFromTheme(const std::string &path)
 {
-    int mode = TAB_STANDARD;
-
-    if (state.selected)
-        mode = TAB_SELECTED;
-    else if (state.hovered)
-        mode = TAB_HIGHLIGHTED;
-
-    graphics->drawImageRect(0, 0, state.width, state.height, mTabImg[mode]);
+    return gui->getTheme()->getImage(path);
 }
 
-void Theme::drawCheckBox(gcn::Graphics *graphics, const WidgetState &state) const
+const gcn::Color &Theme::getThemeColor(int type, int alpha)
 {
-    Image *box;
-
-    if (state.enabled)
-    {
-        if (state.selected)
-        {
-            if (state.hovered)
-                box = mCheckBoxCheckedHi.get();
-            else
-                box = mCheckBoxChecked.get();
-        }
-        else
-        {
-            if (state.hovered)
-                box = mCheckBoxNormalHi.get();
-            else
-                box = mCheckBoxNormal.get();
-        }
-    }
-    else
-    {
-        if (state.selected)
-            box = mCheckBoxDisabledChecked.get();
-        else
-            box = mCheckBoxDisabled.get();
-    }
-
-    static_cast<Graphics*>(graphics)->drawImage(box, 2, 2);
+    return gui->getTheme()->getColor(type, alpha);
 }
 
-void Theme::drawRadioButton(gcn::Graphics *graphics, const WidgetState &state) const
+const gcn::Color &Theme::getThemeColor(char c, bool &valid)
 {
-    Image *box = nullptr;
-
-    if (state.enabled)
-    {
-        if (state.selected)
-        {
-            if (state.hovered)
-                box = mRadioCheckedHi;
-            else
-                box = mRadioChecked;
-        }
-        else
-        {
-            if (state.hovered)
-                box = mRadioNormalHi;
-            else
-                box = mRadioNormal;
-        }
-    }
-    else
-    {
-        if (state.selected)
-            box = mRadioDisabledChecked;
-        else
-            box = mRadioDisabled;
-    }
-
-    static_cast<Graphics*>(graphics)->drawImage(box, 2, 2);
+    return gui->getTheme()->getColor(c, valid);
 }
 
-void Theme::drawSlider(Graphics *graphics, const WidgetState &state, int markerPosition) const
+gcn::Color Theme::getProgressColor(int type, float progress)
 {
-    auto start = state.hovered ? hStartHi.get() : hStart.get();
-    auto mid = state.hovered ? hMidHi.get() : hMid.get();
-    auto end = state.hovered ? hEndHi.get() : hEnd.get();
-    auto grip = state.hovered ? hGripHi.get() : hGrip.get();
-
-    int w = state.width;
-    int h = state.height;
-    int x = 0;
-    int y = (h - start->getHeight()) / 2;
-
-    graphics->drawImage(start, x, y);
-
-    w -= start->getWidth() + end->getWidth();
-    x += start->getWidth();
-
-    graphics->drawImagePattern(mid, x, y, w, mid->getHeight());
-
-    x += w;
-    graphics->drawImage(end, x, y);
+    const auto &dye = gui->getTheme()->mProgressColors[type];
 
-    graphics->drawImage(grip, markerPosition, (state.height - grip->getHeight()) / 2);
-}
+    int color[3] = {0, 0, 0};
+    dye->getColor(progress, color);
 
-void Theme::drawDropDownFrame(Graphics *graphics, const WidgetState &state) const
-{
-    graphics->drawImageRect(0, 0, state.width, state.height, mDeepBoxImageRect);
+    return gcn::Color(color[0], color[1], color[2]);
 }
 
-void Theme::drawDropDownButton(Graphics *graphics, const WidgetState &state) const
+void Theme::drawSkin(Graphics *graphics, SkinType type, const WidgetState &state) const
 {
-    const auto buttonDir = state.selected ? ARROW_UP : ARROW_DOWN;
-    const Image *img = mArrowButtons[buttonDir][state.hovered];
-    graphics->drawImage(img, state.width - img->getHeight() - 2, 2);
+    auto it = mSkins.find(type);
+    if (it != mSkins.end())
+        it->second.draw(graphics, state);
 }
 
 void Theme::drawProgressBar(Graphics *graphics, const gcn::Rectangle &area,
@@ -499,7 +280,13 @@ void Theme::drawProgressBar(Graphics *graphics, const gcn::Rectangle &area,
     gcn::Font *oldFont = graphics->getFont();
     gcn::Color oldColor = graphics->getColor();
 
-    graphics->drawImageRect(area, mScrollBarMarker);
+    WidgetState state;
+    state.x = area.x;
+    state.y = area.y;
+    state.width = area.width;
+    state.height = area.height;
+
+    drawSkin(graphics, SkinType::ProgressBar, state);
 
     // The bar
     if (progress > 0)
@@ -532,29 +319,20 @@ void Theme::drawProgressBar(Graphics *graphics, const gcn::Rectangle &area,
     graphics->setColor(oldColor);
 }
 
-void Theme::drawScrollAreaFrame(Graphics *graphics, const WidgetState &state) const
+int Theme::getMinWidth(SkinType skinType) const
 {
-    graphics->drawImageRect(0, 0, state.width, state.height, mDeepBoxImageRect);
+    auto it = mSkins.find(skinType);
+    if (it != mSkins.end())
+        return it->second.getMinWidth();
+    return 0;
 }
 
-void Theme::drawScrollAreaButton(Graphics *graphics,
-                                 ArrowButtonDirection dir,
-                                 bool pressed,
-                                 const gcn::Rectangle &dim) const
+int Theme::getMinHeight(SkinType skinType) const
 {
-    const int state = pressed ? 1 : 0;
-    graphics->drawImage(mArrowButtons[dir][state], dim.x, dim.y);
-}
-
-void Theme::drawScrollAreaMarker(Graphics *graphics, bool hovered, const gcn::Rectangle &dim) const
-{
-    const auto &imageRect = hovered ? mScrollBarMarkerHi : mScrollBarMarker;
-    graphics->drawImageRect(dim.x, dim.y, dim.width, dim.height, imageRect);
-}
-
-int Theme::getSliderMarkerLength() const
-{
-    return hGrip->getWidth();
+    auto it = mSkins.find(skinType);
+    if (it != mSkins.end())
+        return it->second.getMinHeight();
+    return 0;
 }
 
 void Theme::setMinimumOpacity(float minimumOpacity)
@@ -575,61 +353,7 @@ void Theme::updateAlpha()
     mAlpha = alpha;
 
     for (auto &skin : mSkins)
-        skin.second->updateAlpha(mAlpha);
-
-    for (auto &mode : mButton)
-        mode.setAlpha(mAlpha);
-
-    for (auto &t : mTabImg)
-        t.setAlpha(mAlpha);
-
-    mDeepBoxImageRect.setAlpha(mAlpha);
-
-    mCheckBoxNormal->setAlpha(mAlpha);
-    mCheckBoxChecked->setAlpha(mAlpha);
-    mCheckBoxDisabled->setAlpha(mAlpha);
-    mCheckBoxDisabledChecked->setAlpha(mAlpha);
-    mCheckBoxNormalHi->setAlpha(mAlpha);
-    mCheckBoxCheckedHi->setAlpha(mAlpha);
-
-    mRadioNormal->setAlpha(mAlpha);
-    mRadioChecked->setAlpha(mAlpha);
-    mRadioDisabled->setAlpha(mAlpha);
-    mRadioDisabledChecked->setAlpha(mAlpha);
-    mRadioNormalHi->setAlpha(mAlpha);
-    mRadioCheckedHi->setAlpha(mAlpha);
-
-    hStart->setAlpha(mAlpha);
-    hMid->setAlpha(mAlpha);
-    hEnd->setAlpha(mAlpha);
-    hGrip->setAlpha(mAlpha);
-    hStartHi->setAlpha(mAlpha);
-    hMidHi->setAlpha(mAlpha);
-    hEndHi->setAlpha(mAlpha);
-    hGripHi->setAlpha(mAlpha);
-
-    vStart->setAlpha(mAlpha);
-    vMid->setAlpha(mAlpha);
-    vEnd->setAlpha(mAlpha);
-    vGrip->setAlpha(mAlpha);
-    vStartHi->setAlpha(mAlpha);
-    vMidHi->setAlpha(mAlpha);
-    vEndHi->setAlpha(mAlpha);
-    vGripHi->setAlpha(mAlpha);
-
-    mScrollBarMarker.setAlpha(mAlpha);
-    mScrollBarMarkerHi.setAlpha(mAlpha);
-
-    mArrowButtons[ARROW_UP][0]->setAlpha(mAlpha);
-    mArrowButtons[ARROW_DOWN][0]->setAlpha(mAlpha);
-    mArrowButtons[ARROW_LEFT][0]->setAlpha(mAlpha);
-    mArrowButtons[ARROW_RIGHT][0]->setAlpha(mAlpha);
-    mArrowButtons[ARROW_UP][1]->setAlpha(mAlpha);
-    mArrowButtons[ARROW_DOWN][1]->setAlpha(mAlpha);
-    mArrowButtons[ARROW_LEFT][1]->setAlpha(mAlpha);
-    mArrowButtons[ARROW_RIGHT][1]->setAlpha(mAlpha);
-
-    mResizeGripImage->setAlpha(mAlpha);
+        skin.second.updateAlpha(mAlpha);
 }
 
 void Theme::event(Event::Channel channel, const Event &event)
@@ -642,149 +366,204 @@ void Theme::event(Event::Channel channel, const Event &event)
     }
 }
 
-std::unique_ptr<Skin> Theme::readSkin(const std::string &filename) const
+static bool check(bool value, const char *msg, ...)
 {
-    if (filename.empty())
-        return nullptr;
+    if (!value)
+    {
+        va_list args;
+        va_start(args, msg);
+        logger->log(msg, args);
+        va_end(args);
+    }
+    return !value;
+}
 
-    logger->log("Loading skin '%s'.", filename.c_str());
+bool Theme::readTheme(const std::string &filename)
+{
+    logger->log("Loading theme '%s'.", filename.c_str());
 
     XML::Document doc(resolvePath(filename));
     XML::Node rootNode = doc.rootNode();
 
-    if (!rootNode || rootNode.name() != "skinset")
-        return nullptr;
+    if (!rootNode || rootNode.name() != "theme")
+        return false;
 
-    const std::string skinSetImage = rootNode.getProperty("image", "");
-
-    if (skinSetImage.empty())
+    for (auto childNode : rootNode.children())
     {
-        logger->log("Theme::readSkin(): Skinset does not define an image!");
-        return nullptr;
+        if (childNode.name() == "skin")
+            readSkinNode(childNode);
+        else if (childNode.name() == "color")
+            readColorNode(childNode);
+        else if (childNode.name() == "progressbar")
+            readProgressBarNode(childNode);
     }
 
-    logger->log("Theme::load(): <skinset> defines '%s' as a skin image.",
-                skinSetImage.c_str());
+    logger->log("Finished loading theme.");
+
+    for (auto &[_, skin] : mSkins)
+        skin.updateAlpha(mAlpha);
+
+    return true;
+}
+
+static std::optional<SkinType> readSkinType(std::string_view type)
+{
+    if (type == "Window")
+        return SkinType::Window;
+    if (type == "Popup")
+        return SkinType::Popup;
+    if (type == "SpeechBubble")
+        return SkinType::SpeechBubble;
+    if (type == "Button")
+        return SkinType::Button;
+    if (type == "ButtonUp")
+        return SkinType::ButtonUp;
+    if (type == "ButtonDown")
+        return SkinType::ButtonDown;
+    if (type == "ButtonLeft")
+        return SkinType::ButtonLeft;
+    if (type == "ButtonRight")
+        return SkinType::ButtonRight;
+    if (type == "ButtonClose")
+        return SkinType::ButtonClose;
+    if (type == "ButtonSticky")
+        return SkinType::ButtonSticky;
+    if (type == "CheckBox")
+        return SkinType::CheckBox;
+    if (type == "RadioButton")
+        return SkinType::RadioButton;
+    if (type == "TextField")
+        return SkinType::TextField;
+    if (type == "Tab")
+        return SkinType::Tab;
+    if (type == "ScrollArea")
+        return SkinType::ScrollArea;
+    if (type == "ScrollBar")
+        return SkinType::ScrollBar;
+    if (type == "DropDownFrame")
+        return SkinType::DropDownFrame;
+    if (type == "DropDownButton")
+        return SkinType::DropDownButton;
+    if (type == "ProgressBar")
+        return SkinType::ProgressBar;
+    if (type == "Slider")
+        return SkinType::Slider;
+    if (type == "SliderHandle")
+        return SkinType::SliderHandle;
+    if (type == "ResizeGrip")
+        return SkinType::ResizeGrip;
+
+    return {};
+}
+
+void Theme::readSkinNode(XML::Node node)
+{
+    const auto skinTypeStr = node.getProperty("type", std::string());
+    const auto skinType = readSkinType(skinTypeStr);
+    if (check(skinType.has_value(), "Theme: Unknown skin type '%s'", skinTypeStr.c_str()))
+        return;
 
-    auto dBorders = getImage(skinSetImage);
-    ImageRect border;
+    auto &skin = mSkins[*skinType];
 
-    // iterate <widget>'s
-    for (auto widgetNode : rootNode.children())
+    for (auto childNode : node.children())
+        if (childNode.name() == "state")
+            readSkinStateNode(childNode, skin);
+}
+
+void Theme::readSkinStateNode(XML::Node node, Skin &skin) const
+{
+    SkinState state;
+
+    auto readFlag = [&] (const char *name, int flag)
     {
-        if (widgetNode.name() != "widget")
-            continue;
+        std::optional<bool> value;
+        node.attribute(name, value);
 
-        const std::string widgetType =
-                widgetNode.getProperty("type", "unknown");
-        if (widgetType == "Window")
-        {
-            // Iterate through <part>'s
-            // LEEOR / TODO:
-            // We need to make provisions to load in a CloseButton image. For
-            // now it can just be hard-coded.
-            for (auto partNode : widgetNode.children())
-            {
-                if (partNode.name() != "part")
-                    continue;
-
-                const std::string partType =
-                        partNode.getProperty("type", "unknown");
-                // TOP ROW
-                const int xPos = partNode.getProperty("xpos", 0);
-                const int yPos = partNode.getProperty("ypos", 0);
-                const int width = partNode.getProperty("width", 1);
-                const int height = partNode.getProperty("height", 1);
-
-                if (partType == "top-left-corner")
-                    border.grid[0] = dBorders->getSubImage(xPos, yPos, width, height);
-                else if (partType == "top-edge")
-                    border.grid[1] = dBorders->getSubImage(xPos, yPos, width, height);
-                else if (partType == "top-right-corner")
-                    border.grid[2] = dBorders->getSubImage(xPos, yPos, width, height);
-
-                // MIDDLE ROW
-                else if (partType == "left-edge")
-                    border.grid[3] = dBorders->getSubImage(xPos, yPos, width, height);
-                else if (partType == "bg-quad")
-                    border.grid[4] = dBorders->getSubImage(xPos, yPos, width, height);
-                else if (partType == "right-edge")
-                    border.grid[5] = dBorders->getSubImage(xPos, yPos, width, height);
-
-                // BOTTOM ROW
-                else if (partType == "bottom-left-corner")
-                    border.grid[6] = dBorders->getSubImage(xPos, yPos, width, height);
-                else if (partType == "bottom-edge")
-                    border.grid[7] = dBorders->getSubImage(xPos, yPos, width, height);
-                else if (partType == "bottom-right-corner")
-                    border.grid[8] = dBorders->getSubImage(xPos, yPos, width, height);
-
-                else
-                    logger->log("Theme::readSkin(): Unknown part type '%s'",
-                                partType.c_str());
-            }
-        }
-        else
+        if (value.has_value())
         {
-            logger->log("Theme::readSkin(): Unknown widget type '%s'",
-                        widgetType.c_str());
+            state.setFlags |= flag;
+            state.stateFlags |= *value ? flag : 0;
         }
-    }
+    };
 
-    logger->log("Finished loading skin.");
+    readFlag("selected", STATE_SELECTED);
+    readFlag("disabled", STATE_DISABLED);
+    readFlag("hovered", STATE_HOVERED);
+    readFlag("focused", STATE_FOCUSED);
 
-    // Hard-coded for now until we update the above code to look for window buttons
-    auto closeImage = getImage("close_button.png");
-    auto sticky = getImage("sticky_button.png");
-    Image *stickyImageUp = sticky->getSubImage(0, 0, 15, 15);
-    Image *stickyImageDown = sticky->getSubImage(15, 0, 15, 15);
+    for (auto childNode : node.children())
+        if (childNode.name() == "img")
+            readSkinStateImgNode(childNode, state);
 
-    auto skin = std::make_unique<Skin>(std::move(border), closeImage, stickyImageUp, stickyImageDown);
-    skin->updateAlpha(mAlpha);
-    return skin;
+    skin.addState(std::move(state));
 }
 
-std::string Theme::prepareThemePath()
+void Theme::readSkinStateImgNode(XML::Node node, SkinState &state) const
 {
-    initDefaultThemePath();
-
-    // Try theme from settings
-    auto themePath = findThemePath(config.theme);
+    const std::string src = node.getProperty("src", std::string());
+    if (check(!src.empty(), "Theme: 'img' element has empty 'src' attribute!"))
+        return;
 
-    // Try theme from branding
-    if (!themePath)
-        themePath = findThemePath(branding.getStringValue("theme"));
+    auto image = getImage(src);
+    if (check(image, "Theme: Failed to load image '%s'!", src.c_str()))
+        return;
 
-    return themePath.value_or(defaultThemePath);
-}
+    int left = 0;
+    int right = 0;
+    int top = 0;
+    int bottom = 0;
+    int x = 0;
+    int y = 0;
+    int width = image->getWidth();
+    int height = image->getHeight();
+
+    node.attribute("left", left);
+    node.attribute("right", right);
+    node.attribute("top", top);
+    node.attribute("bottom", bottom);
+    node.attribute("x", x);
+    node.attribute("y", y);
+    node.attribute("width", width);
+    node.attribute("height", height);
+
+    if (check(left >= 0 || right >= 0 || top >= 0 || bottom >= 0, "Theme: Invalid border value!"))
+        return;
+    if (check(x >= 0 || y >= 0, "Theme: Invalid position value!"))
+        return;
+    if (check(width >= 0 || height >= 0, "Theme: Invalid size value!"))
+        return;
+    if (check(x + width <= image->getWidth() || y + height <= image->getHeight(), "Theme: Image size out of bounds!"))
+        return;
 
-std::string Theme::resolvePath(const std::string &path) const
-{
-    // Need to strip off any dye info for the existence tests
-    int pos = path.find('|');
-    std::string file;
-    if (pos > 0)
-        file = path.substr(0, pos);
-    else
-        file = path;
+    auto &part = state.parts.emplace_back();
 
-    // Try the theme
-    file = mThemePath + "/" + file;
-    if (FS::exists(file))
-        return mThemePath + "/" + path;
+    node.attribute("offsetX", part.offsetX);
+    node.attribute("offsetY", part.offsetY);
 
-    // Backup
-    return defaultThemePath + "/" + path;
-}
+    if (left + right + top + bottom > 0)
+    {
+        auto &border = part.data.emplace<ImageRect>();
 
-ResourceRef<Image> Theme::getImage(const std::string &path) const
-{
-    return ResourceManager::getInstance()->getImage(resolvePath(path));
-}
+        const int gridx[4] = {x, x + left, x + width - right, x + width};
+        const int gridy[4] = {y, y + top, y + height - bottom, y + height};
+        unsigned a = 0;
 
-ResourceRef<Image> Theme::getImageFromTheme(const std::string &path)
-{
-    return gui->getTheme()->getImage(path);
+        for (unsigned y = 0; y < 3; y++)
+        {
+            for (unsigned x = 0; x < 3; x++)
+            {
+                border.grid[a] = image->getSubImage(gridx[x],
+                                                    gridy[y],
+                                                    gridx[x + 1] - gridx[x],
+                                                    gridy[y + 1] - gridy[y]);
+                a++;
+            }
+        }
+    }
+    else
+    {
+        part.data = image->getSubImage(x, y, width, height);
+    }
 }
 
 static int readColorType(const std::string &type)
@@ -805,6 +584,7 @@ static int readColorType(const std::string &type)
         "SHOP_WARNING",
         "ITEM_EQUIPPED",
         "CHAT",
+        "BUBBLE_TEXT",
         "GM",
         "PLAYER",
         "WHISPER",
@@ -892,6 +672,22 @@ static Palette::GradientType readColorGradient(const std::string &grad)
     return Palette::STATIC;
 }
 
+void Theme::readColorNode(XML::Node node)
+{
+    const int type = readColorType(node.getProperty("id", std::string()));
+    if (type < 0) // invalid or no type given
+        return;
+
+    const std::string temp = node.getProperty("color", std::string());
+    if (temp.empty()) // no color set, so move on
+        return;
+
+    const gcn::Color color = readColor(temp);
+    const GradientType grad = readColorGradient(node.getProperty("effect", std::string()));
+
+    mColors[type].set(type, color, grad, 10);
+}
+
 static int readProgressType(const std::string &type)
 {
     static const char *colors[] = {
@@ -915,43 +711,11 @@ static int readProgressType(const std::string &type)
     return -1;
 }
 
-void Theme::loadColors()
+void Theme::readProgressBarNode(XML::Node node)
 {
-    std::string file = resolvePath("colors.xml");
-
-    XML::Document doc(file);
-    XML::Node root = doc.rootNode();
-
-    if (!root || root.name() != "colors")
-    {
-        logger->log("Error loading colors file: %s", file.c_str());
+    const int type = readProgressType(node.getProperty("id", std::string()));
+    if (type < 0) // invalid or no type given
         return;
-    }
-
-    for (auto node : root.children())
-    {
-        if (node.name() == "color")
-        {
-            const int type = readColorType(node.getProperty("id", std::string()));
-            if (type < 0) // invalid or no type given
-                continue;
-
-            const std::string temp = node.getProperty("color", std::string());
-            if (temp.empty()) // no color set, so move on
-                continue;
-
-            const gcn::Color color = readColor(temp);
-            const GradientType grad = readColorGradient(node.getProperty("effect", std::string()));
-
-            mColors[type].set(type, color, grad, 10);
-        }
-        else if (node.name() == "progressbar")
-        {
-            const int type = readProgressType(node.getProperty("id", std::string()));
-            if (type < 0) // invalid or no type given
-                continue;
 
-            mProgressColors[type] = std::make_unique<DyePalette>(node.getProperty("color", std::string()));
-        }
-    }
+    mProgressColors[type] = std::make_unique<DyePalette>(node.getProperty("color", std::string()));
 }
diff --git a/src/resources/theme.h b/src/resources/theme.h
index 8aba8769..20b94823 100644
--- a/src/resources/theme.h
+++ b/src/resources/theme.h
@@ -28,37 +28,92 @@
 
 #include "gui/palette.h"
 #include "resources/image.h"
+#include "utils/xml.h"
 
 #include <map>
 #include <memory>
 #include <string>
+#include <variant>
+
+namespace gcn {
+class Widget;
+}
 
 class DyePalette;
 class Image;
 class ImageSet;
 class ProgressBar;
 
+enum class SkinType
+{
+    Window,
+    Popup,
+    SpeechBubble,
+    Button,
+    ButtonUp,
+    ButtonDown,
+    ButtonLeft,
+    ButtonRight,
+    ButtonClose,
+    ButtonSticky,
+    CheckBox,
+    RadioButton,
+    TextField,
+    Tab,
+    ScrollArea,
+    ScrollBar,
+    DropDownFrame,
+    DropDownButton,
+    ProgressBar,
+    Slider,
+    SliderHandle,
+    ResizeGrip,
+};
+
+enum StateFlags : uint8_t
+{
+    STATE_NORMAL    = 0x01,
+    STATE_HOVERED   = 0x02,
+    STATE_SELECTED  = 0x04,
+    STATE_DISABLED  = 0x08,
+    STATE_FOCUSED   = 0x10,
+};
+
+struct SkinPart
+{
+    int offsetX = 0;
+    int offsetY = 0;
+    std::variant<ImageRect, Image *> data;
+};
+
+struct SkinState
+{
+    uint8_t stateFlags  = 0;
+    uint8_t setFlags = 0;
+    std::vector<SkinPart> parts;
+};
+
+struct WidgetState
+{
+    WidgetState() = default;
+    explicit WidgetState(const gcn::Widget *widget);
+
+    int x = 0;
+    int y = 0;
+    int width = 0;
+    int height = 0;
+    uint8_t flags = 0;
+};
+
 class Skin
 {
     public:
-        Skin(ImageRect skin, Image *close, Image *stickyUp, Image *stickyDown);
+        Skin() = default;
         ~Skin();
 
-        /**
-         * Returns the background skin.
-         */
-        const ImageRect &getBorder() const { return mBorder; }
-
-        /**
-         * Returns the image used by a close button for this skin.
-         */
-        Image *getCloseImage() const { return mCloseImage; }
+        void addState(SkinState state);
 
-        /**
-         * Returns the image used by a sticky button for this skin.
-         */
-        Image *getStickyImage(bool state) const
-        { return state ? mStickyImageDown.get() : mStickyImageUp.get(); }
+        void draw(Graphics *graphics, const WidgetState &state) const;
 
         /**
          * Returns the minimum width which can be used with this skin.
@@ -75,13 +130,8 @@ class Skin
          */
         void updateAlpha(float alpha);
 
-        int instances = 0;
-
     private:
-        ImageRect mBorder;              /**< The window border and background */
-        ResourceRef<Image> mCloseImage; /**< Close Button Image */
-        std::unique_ptr<Image> mStickyImageUp;          /**< Sticky Button Image */
-        std::unique_ptr<Image> mStickyImageDown;        /**< Sticky Button Image */
+        std::vector<SkinState> mStates;
 };
 
 class Theme : public Palette, public EventListener
@@ -99,13 +149,6 @@ class Theme : public Palette, public EventListener
         std::string resolvePath(const std::string &path) const;
         static ResourceRef<Image> getImageFromTheme(const std::string &path);
 
-        enum ArrowButtonDirection {
-            ARROW_UP,
-            ARROW_DOWN,
-            ARROW_LEFT,
-            ARROW_RIGHT
-        };
-
         enum ThemePalette {
             TEXT,
             SHADOW,
@@ -122,6 +165,7 @@ class Theme : public Palette, public EventListener
             SHOP_WARNING,
             ITEM_EQUIPPED,
             CHAT,
+            BUBBLE_TEXT,
             GM,
             PLAYER,
             WHISPER,
@@ -175,43 +219,15 @@ class Theme : public Palette, public EventListener
 
         static gcn::Color getProgressColor(int type, float progress);
 
-        /**
-         * Loads a skin.
-         */
-        Skin *load(const std::string &filename);
-
-        struct WidgetState
-        {
-            int width = 0;
-            int height = 0;
-            bool enabled = true;
-            bool hovered = false;
-            bool selected = false;
-            bool focused = false;
-        };
-
-        void drawButton(Graphics *graphics, const WidgetState &state) const;
-        void drawTextFieldFrame(Graphics *graphics, const WidgetState &state) const;
-        void drawTab(Graphics *graphics, const WidgetState &state) const;
-        void drawCheckBox(gcn::Graphics *graphics, const WidgetState &state) const;
-        void drawRadioButton(gcn::Graphics *graphics, const WidgetState &state) const;
-        void drawSlider(Graphics *graphics, const WidgetState &state, int markerPosition) const;
-        void drawDropDownFrame(Graphics *graphics, const WidgetState &state) const;
-        void drawDropDownButton(Graphics *graphics, const WidgetState &state) const;
+        void drawSkin(Graphics *graphics, SkinType type, const WidgetState &state) const;
         void drawProgressBar(Graphics *graphics,
                              const gcn::Rectangle &area,
                              const gcn::Color &color,
                              float progress,
                              const std::string &text = std::string()) const;
-        void drawScrollAreaFrame(Graphics *graphics, const WidgetState &state) const;
-        void drawScrollAreaButton(Graphics *graphics,
-                                  ArrowButtonDirection dir,
-                                  bool pressed,
-                                  const gcn::Rectangle &dim) const;
-        void drawScrollAreaMarker(Graphics *graphics, bool hovered, const gcn::Rectangle &dim) const;
 
-        int getSliderMarkerLength() const;
-        const Image *getResizeGripImage() const { return mResizeGripImage; }
+        int getMinWidth(SkinType skinType) const;
+        int getMinHeight(SkinType skinType) const;
 
         /**
          * Get the current GUI alpha value.
@@ -237,16 +253,17 @@ class Theme : public Palette, public EventListener
          */
         void updateAlpha();
 
-        std::unique_ptr<Skin> readSkin(const std::string &filename) const;
-
         ResourceRef<Image> getImage(const std::string &path) const;
 
-        // Map containing all window skins
-        std::map<std::string, std::unique_ptr<Skin>> mSkins;
+        bool readTheme(const std::string &filename);
+        void readSkinNode(XML::Node node);
+        void readSkinStateNode(XML::Node node, Skin &skin) const;
+        void readSkinStateImgNode(XML::Node node, SkinState &state) const;
+        void readColorNode(XML::Node node);
+        void readProgressBarNode(XML::Node node);
 
         std::string mThemePath;
-
-        void loadColors();
+        std::map<SkinType, Skin> mSkins;
 
         /**
          * Tells if the current skins opacity
@@ -256,33 +273,4 @@ class Theme : public Palette, public EventListener
         float mAlpha = 1.0;
 
         std::vector<std::unique_ptr<DyePalette>> mProgressColors;
-
-        ImageRect mButton[4];   /**< Button state graphics */
-        ImageRect mTabImg[4];   /**< Tab state graphics */
-        ImageRect mDeepBoxImageRect;
-
-        std::unique_ptr<Image> mCheckBoxNormal;
-        std::unique_ptr<Image> mCheckBoxChecked;
-        std::unique_ptr<Image> mCheckBoxDisabled;
-        std::unique_ptr<Image> mCheckBoxDisabledChecked;
-        std::unique_ptr<Image> mCheckBoxNormalHi;
-        std::unique_ptr<Image> mCheckBoxCheckedHi;
-
-        ResourceRef<Image> mRadioNormal;
-        ResourceRef<Image> mRadioChecked;
-        ResourceRef<Image> mRadioDisabled;
-        ResourceRef<Image> mRadioDisabledChecked;
-        ResourceRef<Image> mRadioNormalHi;
-        ResourceRef<Image> mRadioCheckedHi;
-
-        std::unique_ptr<Image> hStart, hMid, hEnd, hGrip;
-        std::unique_ptr<Image> vStart, vMid, vEnd, vGrip;
-        std::unique_ptr<Image> hStartHi, hMidHi, hEndHi, hGripHi;
-        std::unique_ptr<Image> vStartHi, vMidHi, vEndHi, vGripHi;
-
-        ImageRect mScrollBarMarker;
-        ImageRect mScrollBarMarkerHi;
-        ResourceRef<Image> mArrowButtons[4][2];
-
-        ResourceRef<Image> mResizeGripImage;
 };
diff --git a/src/text.cpp b/src/text.cpp
index f45cb706..4698aa87 100644
--- a/src/text.cpp
+++ b/src/text.cpp
@@ -22,53 +22,34 @@
 
 #include "text.h"
 
-#include "configuration.h"
 #include "textmanager.h"
 #include "textrenderer.h"
 
 #include "gui/gui.h"
 
-#include "resources/image.h"
 #include "resources/theme.h"
 
 #include <guichan/font.hpp>
 
 int Text::mInstances = 0;
-ImageRect Text::mBubble;
-Image *Text::mBubbleArrow;
 
-Text::Text(const std::string &text, int x, int y,
+Text::Text(const std::string &text,
+           int x,
+           int y,
            gcn::Graphics::Alignment alignment,
-           const gcn::Color* color, bool isSpeech,
-           gcn::Font *font) :
-    mText(text),
-    mColor(color),
-    mIsSpeech(isSpeech)
+           const gcn::Color *color,
+           bool isSpeech,
+           gcn::Font *font)
+    : mText(text)
+    , mColor(color)
+    , mFont(font ? font : gui->getFont())
+    , mIsSpeech(isSpeech)
 {
-    if (!font)
-        mFont = gui->getFont();
-    else
-        mFont = font;
-
     if (textManager == nullptr)
-    {
         textManager = new TextManager;
-        auto sbImage = Theme::getImageFromTheme("bubble.png|W:#"
-            + config.speechBubblecolor);
-        mBubble.grid[0] = sbImage->getSubImage(0, 0, 5, 5);
-        mBubble.grid[1] = sbImage->getSubImage(5, 0, 5, 5);
-        mBubble.grid[2] = sbImage->getSubImage(10, 0, 5, 5);
-        mBubble.grid[3] = sbImage->getSubImage(0, 5, 5, 5);
-        mBubble.grid[4] = sbImage->getSubImage(5, 5, 5, 5);
-        mBubble.grid[5] = sbImage->getSubImage(10, 5, 5, 5);
-        mBubble.grid[6] = sbImage->getSubImage(0, 10, 5, 5);
-        mBubble.grid[7] = sbImage->getSubImage(5, 10, 5, 5);
-        mBubble.grid[8] = sbImage->getSubImage(10, 10, 5, 5);
-        mBubbleArrow = sbImage->getSubImage(0, 15, 15, 10);
-        mBubble.setAlpha(config.speechBubbleAlpha);
-        mBubbleArrow->setAlpha(config.speechBubbleAlpha);
-    }
+
     ++mInstances;
+
     mHeight = mFont->getHeight();
     mWidth = mFont->getWidth(text);
 
@@ -84,8 +65,10 @@ Text::Text(const std::string &text, int x, int y,
             mXOffset = mWidth;
             break;
     }
+
     mX = x - mXOffset;
     mY = y;
+
     textManager->addText(this);
 }
 
@@ -96,12 +79,6 @@ Text::~Text()
     {
         delete textManager;
         textManager = nullptr;
-        for (auto &img : mBubble.grid)
-        {
-            delete img;
-            img = nullptr;
-        }
-        delete mBubbleArrow;
     }
 }
 
@@ -119,9 +96,15 @@ void Text::draw(gcn::Graphics *graphics, int xOff, int yOff)
 {
     if (mIsSpeech)
     {
-        static_cast<Graphics*>(graphics)->drawImageRect(
-                mX - xOff - 5, mY - yOff - 5, mWidth + 10, mHeight + 10,
-                mBubble);
+        WidgetState state;
+        state.x = mX - xOff - 5;
+        state.y = mY - yOff - 5;
+        state.width = mWidth + 10;
+        state.height = mHeight + 10;
+
+        auto theme = gui->getTheme();
+        theme->drawSkin(static_cast<Graphics *>(graphics), SkinType::SpeechBubble, state);
+
         /*
         if (mWidth >= 15)
         {
diff --git a/src/text.h b/src/text.h
index c4cb3954..b71dde37 100644
--- a/src/text.h
+++ b/src/text.h
@@ -22,11 +22,11 @@
 
 #pragma once
 
-#include "graphics.h"
-
 #include "utils/time.h"
 
 #include <guichan/color.hpp>
+#include <guichan/font.hpp>
+#include <guichan/graphics.hpp>
 
 class TextManager;
 
@@ -74,10 +74,6 @@ class Text
         const gcn::Color *mColor;     /**< The color of the text. */
         gcn::Font *mFont;      /**< The font of the text */
         bool mIsSpeech;        /**< Is this text a speech bubble? */
-
-    protected:
-        static ImageRect mBubble;   /**< Speech bubble graphic */
-        static Image *mBubbleArrow; /**< Speech bubble arrow graphic */
 };
 
 class FlashText : public Text
-- 
cgit v1.2.3-70-g09d2