summaryrefslogtreecommitdiff
path: root/src/resources/theme.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/resources/theme.cpp')
-rw-r--r--src/resources/theme.cpp426
1 files changed, 299 insertions, 127 deletions
diff --git a/src/resources/theme.cpp b/src/resources/theme.cpp
index 694f2210..ea2cef45 100644
--- a/src/resources/theme.cpp
+++ b/src/resources/theme.cpp
@@ -25,7 +25,6 @@
#include "configuration.h"
#include "log.h"
-#include "textrenderer.h"
#include "resources/dye.h"
#include "resources/image.h"
@@ -38,7 +37,6 @@
#include <guichan/widget.hpp>
#include <algorithm>
-#include <optional>
/**
* Initializes the directory in which the client looks for GUI themes, which at
@@ -55,18 +53,33 @@ static void initDefaultThemePath()
defaultThemePath = "graphics/gui/";
}
-static std::optional<std::string> findThemePath(const std::string &theme)
+static bool isThemePath(const std::string &theme)
{
- if (theme.empty())
- return {};
+ return FS::exists(defaultThemePath + theme + "/theme.xml");
+}
- std::string themePath = defaultThemePath;
- themePath += theme;
- if (FS::isDirectory(themePath))
- return themePath;
+ThemeInfo::ThemeInfo(const std::string &path)
+ : path(path)
+{
+ auto themeFile = getFullPath() + "/theme.xml";
+ if (!FS::exists(themeFile))
+ return;
- return {};
+ auto doc = std::make_unique<XML::Document>(themeFile);
+ XML::Node rootNode = doc->rootNode();
+ if (!rootNode || rootNode.name() != "theme")
+ return;
+
+ if (rootNode.attribute("name", name) && !name.empty())
+ this->doc = std::move(doc);
+ else
+ Log::error("Theme '%s' has no name!", path.c_str());
+}
+
+std::string ThemeInfo::getFullPath() const
+{
+ return defaultThemePath + path;
}
@@ -120,11 +133,11 @@ void Skin::draw(Graphics *graphics, const WidgetState &state) const
if constexpr (std::is_same_v<T, ImageRect>)
{
- graphics->drawImageRect(state.x + part.offsetX,
+ graphics->drawImageRect(data,
+ state.x + part.offsetX,
state.y + part.offsetY,
state.width,
- state.height,
- data);
+ state.height);
}
else if constexpr (std::is_same_v<T, Image*>)
{
@@ -132,12 +145,21 @@ void Skin::draw(Graphics *graphics, const WidgetState &state) const
}
else if constexpr (std::is_same_v<T, ColoredRectangle>)
{
+ const auto color = graphics->getColor();
+ // TODO: Take GUI alpha into account
graphics->setColor(data.color);
- graphics->fillRectangle(gcn::Rectangle(state.x + part.offsetX,
- state.y + part.offsetY,
- state.width,
- state.height));
- graphics->setColor(gcn::Color(255, 255, 255));
+
+ const gcn::Rectangle rect(state.x + part.offsetX,
+ state.y + part.offsetY,
+ state.width,
+ state.height);
+
+ if (data.filled)
+ graphics->fillRectangle(rect);
+ else
+ graphics->drawRectangle(rect);
+
+ graphics->setColor(color);
}
}, part.data);
}
@@ -195,7 +217,7 @@ void Skin::updateAlpha(float alpha)
for (auto &part : state.parts)
{
if (auto rect = std::get_if<ImageRect>(&part.data))
- rect->setAlpha(alpha);
+ rect->image->setAlpha(alpha);
else if (auto img = std::get_if<Image *>(&part.data))
(*img)->setAlpha(alpha);
}
@@ -203,41 +225,60 @@ void Skin::updateAlpha(float alpha)
}
-Theme::Theme(const std::string &path)
- : Palette(THEME_COLORS_END)
- , mThemePath(path)
- , mProgressColors(THEME_PROG_END)
+Theme::Theme(const ThemeInfo &themeInfo)
+ : mThemePath(themeInfo.getFullPath())
{
listen(Event::ConfigChannel);
- readTheme("theme.xml");
+ readTheme(themeInfo);
+
+ if (mPalettes.empty())
+ {
+ Log::info("Error, theme did not define any palettes: %s",
+ themeInfo.getPath().c_str());
- 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 = '<';
+ // Avoid crashing
+ mPalettes.emplace_back(THEME_COLORS_END);
+ }
}
-Theme::~Theme() = default;
+Theme::~Theme()
+{
+ for (auto &[_, image] : mIcons)
+ delete image;
+}
std::string Theme::prepareThemePath()
{
initDefaultThemePath();
// Try theme from settings
- auto themePath = findThemePath(config.theme);
+ if (isThemePath(config.theme))
+ return config.theme;
// Try theme from branding
- if (!themePath)
- themePath = findThemePath(branding.getStringValue("theme"));
+ if (isThemePath(branding.getStringValue("theme")))
+ return branding.getStringValue("theme");
- return themePath.value_or(defaultThemePath);
+ return std::string();
+}
+
+std::vector<ThemeInfo> Theme::getAvailableThemes()
+{
+ std::vector<ThemeInfo> themes;
+ themes.emplace_back(std::string());
+
+ for (const auto &entry : FS::enumerateFiles(defaultThemePath))
+ {
+ ThemeInfo theme{entry};
+ if (theme.isValid())
+ themes.push_back(std::move(theme));
+ }
+
+ std::sort(themes.begin(), themes.end(), [](const ThemeInfo &a, const ThemeInfo &b) {
+ return a.getName() < b.getName();
+ });
+
+ return themes;
}
std::string Theme::resolvePath(const std::string &path) const
@@ -269,14 +310,9 @@ ResourceRef<Image> Theme::getImageFromTheme(const std::string &path)
return gui->getTheme()->getImage(path);
}
-const gcn::Color &Theme::getThemeColor(int type, int alpha)
-{
- return gui->getTheme()->getColor(type, alpha);
-}
-
-const gcn::Color &Theme::getThemeColor(char c, bool &valid)
+const gcn::Color &Theme::getThemeColor(int type)
{
- return gui->getTheme()->getColor(c, valid);
+ return gui->getTheme()->getColor(type);
}
gcn::Color Theme::getProgressColor(int type, float progress)
@@ -289,14 +325,62 @@ gcn::Color Theme::getProgressColor(int type, float progress)
return gcn::Color(color[0], color[1], color[2]);
}
+const Palette &Theme::getPalette(size_t index) const
+{
+ return mPalettes.at(index < mPalettes.size() ? index : 0);
+}
+
+const gcn::Color &Theme::getColor(int type) const
+{
+ return getPalette(0).getColor(type);
+}
+
+std::optional<int> Theme::getColorIdForChar(char c)
+{
+ switch (c) {
+ case '0': return BLACK;
+ case '1': return RED;
+ case '2': return GREEN;
+ case '3': return BLUE;
+ case '4': return ORANGE;
+ case '5': return YELLOW;
+ case '6': return PINK;
+ case '7': return PURPLE;
+ case '8': return GRAY;
+ case '9': return BROWN;
+
+ case 'H': return HIGHLIGHT;
+ case 'C': return CHAT;
+ case 'G': return GM;
+ case 'g': return GLOBAL;
+ case 'Y': return PLAYER;
+ case 'W': return WHISPER;
+ // case 'w': return WHISPER_TAB_OFFLINE;
+ case 'I': return IS;
+ case 'P': return PARTY;
+ case 'U': return GUILD;
+ case 'S': return SERVER;
+ case 'L': return LOGGER;
+ case '<': return HYPERLINK;
+ // case 's': return SELFNICK;
+ case 'o': return OLDCHAT;
+ case 'a': return AWAYCHAT;
+ }
+
+ return {};
+}
+
void Theme::drawSkin(Graphics *graphics, SkinType type, const WidgetState &state) const
{
getSkin(type).draw(graphics, state);
}
-void Theme::drawProgressBar(Graphics *graphics, const gcn::Rectangle &area,
- const gcn::Color &color, float progress,
- const std::string &text) const
+void Theme::drawProgressBar(Graphics *graphics,
+ const gcn::Rectangle &area,
+ const gcn::Color &color,
+ float progress,
+ const std::string &text,
+ ProgressPalette progressType) const
{
gcn::Font *oldFont = graphics->getFont();
gcn::Color oldColor = graphics->getColor();
@@ -325,17 +409,21 @@ void Theme::drawProgressBar(Graphics *graphics, const gcn::Rectangle &area,
{
if (auto skinState = skin.getState(widgetState.flags))
{
- auto font = skinState->textFormat.bold ? boldFont : gui->getFont();
+ const TextFormat *textFormat = &skinState->textFormat;
+
+ if (progressType < THEME_PROG_END && mProgressTextFormats[progressType])
+ textFormat = &(*mProgressTextFormats[progressType]);
+
+ auto font = textFormat->bold ? boldFont : gui->getFont();
const int textX = area.x + area.width / 2;
const int textY = area.y + (area.height - font->getHeight()) / 2;
- TextRenderer::renderText(graphics,
- text,
- textX,
- textY,
- gcn::Graphics::CENTER,
- font,
- skinState->textFormat);
+ graphics->drawText(text,
+ textX,
+ textY,
+ gcn::Graphics::CENTER,
+ font,
+ *textFormat);
}
}
@@ -350,14 +438,13 @@ const Skin &Theme::getSkin(SkinType skinType) const
return it != mSkins.end() ? it->second : emptySkin;
}
-int Theme::getMinWidth(SkinType skinType) const
+const Image *Theme::getIcon(const std::string &name) const
{
- return getSkin(skinType).getMinWidth();
-}
+ auto it = mIcons.find(name);
+ if (it == mIcons.end())
+ return nullptr;
-int Theme::getMinHeight(SkinType skinType) const
-{
- return getSkin(skinType).getMinHeight();
+ return it->second;
}
void Theme::setMinimumOpacity(float minimumOpacity)
@@ -377,8 +464,8 @@ void Theme::updateAlpha()
mAlpha = alpha;
- for (auto &skin : mSkins)
- skin.second.updateAlpha(mAlpha);
+ for (auto &[_, skin] : mSkins)
+ skin.updateAlpha(mAlpha);
}
void Theme::event(Event::Channel channel, const Event &event)
@@ -395,20 +482,21 @@ static bool check(bool value, const char *msg, ...)
{
if (!value)
{
- va_list args;
- va_start(args, msg);
- logger->log(msg, args);
- va_end(args);
+ va_list ap;
+ va_start(ap, msg);
+ Log::vinfo(msg, ap);
+ va_end(ap);
}
return !value;
}
-bool Theme::readTheme(const std::string &filename)
+bool Theme::readTheme(const ThemeInfo &themeInfo)
{
- logger->log("Loading theme '%s'.", filename.c_str());
+ Log::info("Loading %s theme from '%s'...",
+ themeInfo.getName().c_str(),
+ themeInfo.getPath().c_str());
- XML::Document doc(resolvePath(filename));
- XML::Node rootNode = doc.rootNode();
+ XML::Node rootNode = themeInfo.getDocument().rootNode();
if (!rootNode || rootNode.name() != "theme")
return false;
@@ -417,13 +505,17 @@ bool Theme::readTheme(const std::string &filename)
{
if (childNode.name() == "skin")
readSkinNode(childNode);
- else if (childNode.name() == "color")
- readColorNode(childNode);
+ else if (childNode.name() == "palette")
+ readPaletteNode(childNode);
else if (childNode.name() == "progressbar")
readProgressBarNode(childNode);
+ else if (childNode.name() == "icon")
+ readIconNode(childNode);
+ else
+ Log::info("Theme: Unknown node '%s'!", childNode.name().data());
}
- logger->log("Finished loading theme.");
+ Log::info("Finished loading theme.");
for (auto &[_, skin] : mSkins)
skin.updateAlpha(mAlpha);
@@ -434,8 +526,10 @@ bool Theme::readTheme(const std::string &filename)
static std::optional<SkinType> readSkinType(std::string_view type)
{
if (type == "Window") return SkinType::Window;
+ if (type == "ToolWindow") return SkinType::ToolWindow;
if (type == "Popup") return SkinType::Popup;
if (type == "SpeechBubble") return SkinType::SpeechBubble;
+ if (type == "Desktop") return SkinType::Desktop;
if (type == "Button") return SkinType::Button;
if (type == "ButtonUp") return SkinType::ButtonUp;
if (type == "ButtonDown") return SkinType::ButtonDown;
@@ -459,6 +553,9 @@ static std::optional<SkinType> readSkinType(std::string_view type)
if (type == "SliderHandle") return SkinType::SliderHandle;
if (type == "ResizeGrip") return SkinType::ResizeGrip;
if (type == "ShortcutBox") return SkinType::ShortcutBox;
+ if (type == "EquipmentBox") return SkinType::EquipmentBox;
+ if (type == "ItemSlot") return SkinType::ItemSlot;
+ if (type == "EmoteSlot") return SkinType::EmoteSlot;
return {};
}
@@ -471,18 +568,40 @@ void Theme::readSkinNode(XML::Node node)
auto &skin = mSkins[*skinType];
+ node.attribute("width", skin.width);
+ node.attribute("height", skin.height);
node.attribute("frameSize", skin.frameSize);
node.attribute("padding", skin.padding);
node.attribute("spacing", skin.spacing);
node.attribute("titleBarHeight", skin.titleBarHeight);
node.attribute("titleOffsetX", skin.titleOffsetX);
node.attribute("titleOffsetY", skin.titleOffsetY);
+ node.attribute("palette", skin.palette);
+ node.attribute("showButtons", skin.showButtons);
for (auto childNode : node.children())
if (childNode.name() == "state")
readSkinStateNode(childNode, skin);
}
+static void readSkinStateRectNode(XML::Node node, SkinState &state)
+{
+ auto &part = state.parts.emplace_back();
+ auto &rect = part.data.emplace<ColoredRectangle>();
+
+ node.attribute("color", rect.color);
+ node.attribute("alpha", rect.color.a);
+ node.attribute("fill", rect.filled);
+}
+
+static void readTextNode(XML::Node node, TextFormat &textFormat)
+{
+ node.attribute("bold", textFormat.bold);
+ node.attribute("color", textFormat.color);
+ node.attribute("outlineColor", textFormat.outlineColor);
+ node.attribute("shadowColor", textFormat.shadowColor);
+}
+
void Theme::readSkinStateNode(XML::Node node, Skin &skin) const
{
SkinState state;
@@ -511,21 +630,12 @@ void Theme::readSkinStateNode(XML::Node node, Skin &skin) const
else if (childNode.name() == "rect")
readSkinStateRectNode(childNode, state);
else if (childNode.name() == "text")
- readSkinStateTextNode(childNode, state);
+ readTextNode(childNode, state.textFormat);
}
skin.addState(std::move(state));
}
-void Theme::readSkinStateTextNode(XML::Node node, SkinState &state) const
-{
- auto &textFormat = state.textFormat;
- node.attribute("bold", textFormat.bold);
- node.attribute("color", textFormat.color);
- node.attribute("outlineColor", textFormat.outlineColor);
- node.attribute("shadowColor", textFormat.shadowColor);
-}
-
template<>
inline void fromString(const char *str, FillMode &value)
{
@@ -580,24 +690,13 @@ void Theme::readSkinStateImgNode(XML::Node node, SkinState &state) const
if (left + right + top + bottom > 0)
{
auto &border = part.data.emplace<ImageRect>();
+ border.left = left;
+ border.right = right;
+ border.top = top;
+ border.bottom = bottom;
+ border.image.reset(image->getSubImage(x, y, width, height));
node.attribute("fill", border.fillMode);
-
- 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;
-
- 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
{
@@ -611,8 +710,8 @@ inline void fromString(const char *str, gcn::Color &value)
if (strlen(str) < 7 || str[0] != '#')
{
error:
- logger->log("Error, invalid theme color palette: %s", str);
- value = Palette::BLACK;
+ Log::info("Error, invalid theme color palette: %s", str);
+ value = gcn::Color(0, 0, 0);
return;
}
@@ -637,31 +736,73 @@ inline void fromString(const char *str, gcn::Color &value)
value = gcn::Color(v);
}
-void Theme::readSkinStateRectNode(XML::Node node, SkinState &state) const
+void Theme::readIconNode(XML::Node node)
{
- auto &part = state.parts.emplace_back();
- auto &rect = part.data.emplace<ColoredRectangle>();
+ std::string name;
+ std::string src;
+ node.attribute("name", name);
+ node.attribute("src", src);
- node.attribute("color", rect.color);
- node.attribute("alpha", rect.color.a);
+ if (check(!name.empty(), "Theme: 'icon' element has empty 'name' attribute!"))
+ return;
+ if (check(!src.empty(), "Theme: 'icon' element has empty 'src' attribute!"))
+ return;
+
+ auto image = getImage(src);
+ if (check(image, "Theme: Failed to load image '%s'!", src.c_str()))
+ return;
+
+ int x = 0;
+ int y = 0;
+ int width = image->getWidth();
+ int height = image->getHeight();
+
+ node.attribute("x", x);
+ node.attribute("y", y);
+ node.attribute("width", width);
+ node.attribute("height", height);
+
+ 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;
+
+ mIcons[name] = image->getSubImage(x, y, width, height);
}
-static int readColorType(const std::string &type)
+static int readColorId(const std::string &id)
{
static constexpr const char *colors[Theme::THEME_COLORS_END] = {
"TEXT",
+ "BLACK",
+ "RED",
+ "GREEN",
+ "BLUE",
+ "ORANGE",
+ "YELLOW",
+ "PINK",
+ "PURPLE",
+ "GRAY",
+ "BROWN",
+ "CARET",
"SHADOW",
"OUTLINE",
- "PARTY_CHAT_TAB",
- "PARTY_SOCIAL_TAB",
+ "PARTY_TAB",
+ "WHISPER_TAB",
"BACKGROUND",
"HIGHLIGHT",
+ "HIGHLIGHT_TEXT",
"TAB_FLASH",
"SHOP_WARNING",
"ITEM_EQUIPPED",
"CHAT",
+ "OLDCHAT",
+ "AWAYCHAT",
"BUBBLE_TEXT",
"GM",
+ "GLOBAL",
"PLAYER",
"WHISPER",
"IS",
@@ -687,17 +828,17 @@ static int readColorType(const std::string &type)
"SERVER_VERSION_NOT_SUPPORTED"
};
- if (type.empty())
+ if (id.empty())
return -1;
for (int i = 0; i < Theme::THEME_COLORS_END; i++)
- if (type == colors[i])
+ if (id == colors[i])
return i;
return -1;
}
-static Palette::GradientType readColorGradient(const std::string &grad)
+static Palette::GradientType readGradientType(const std::string &grad)
{
static constexpr const char *grads[] = {
"STATIC",
@@ -716,22 +857,42 @@ static Palette::GradientType readColorGradient(const std::string &grad)
return Palette::STATIC;
}
-void Theme::readColorNode(XML::Node node)
+static void readColorNode(XML::Node node, Palette &palette)
{
- const int type = readColorType(node.getProperty("id", std::string()));
- if (check(type > 0, "Theme: 'color' element has invalid or no 'type' attribute!"))
+ const auto idStr = node.getProperty("id", std::string());
+ const int id = readColorId(idStr);
+ if (check(id >= 0, "Theme: 'color' element has unknown 'id' attribute: '%s'!", idStr.c_str()))
return;
gcn::Color color;
if (check(node.attribute("color", color), "Theme: 'color' element missing 'color' attribute!"))
return;
- const GradientType grad = readColorGradient(node.getProperty("effect", std::string()));
+ std::optional<gcn::Color> outlineColor;
+ node.attribute("outlineColor", outlineColor);
- mColors[type].set(type, color, grad, 10);
+ const auto grad = readGradientType(node.getProperty("effect", std::string()));
+ palette.setColor(id, color, outlineColor, grad, 10);
}
-static int readProgressType(const std::string &type)
+void Theme::readPaletteNode(XML::Node node)
+{
+ int paletteId;
+ if (node.attribute("id", paletteId) && static_cast<size_t>(paletteId) != mPalettes.size())
+ Log::info("Theme: Non-consecutive palette 'id' attribute with value %d!", paletteId);
+
+ Palette &palette = mPalettes.emplace_back(THEME_COLORS_END);
+
+ for (auto childNode : node.children())
+ {
+ if (childNode.name() == "color")
+ readColorNode(childNode, palette);
+ else
+ Log::info("Theme: Unknown node '%s'!", childNode.name().data());
+ }
+}
+
+static int readProgressId(const std::string &id)
{
static constexpr const char *colors[Theme::THEME_PROG_END] = {
"DEFAULT",
@@ -744,11 +905,11 @@ static int readProgressType(const std::string &type)
"JOB"
};
- if (type.empty())
+ if (id.empty())
return -1;
for (int i = 0; i < Theme::THEME_PROG_END; i++)
- if (type == colors[i])
+ if (id == colors[i])
return i;
return -1;
@@ -756,9 +917,20 @@ static int readProgressType(const std::string &type)
void Theme::readProgressBarNode(XML::Node node)
{
- const int type = readProgressType(node.getProperty("id", std::string()));
- if (type < 0) // invalid or no type given
+ const auto idStr = node.getProperty("id", std::string());
+ const int id = readProgressId(idStr);
+ if (check(id >= 0, "Theme: 'progress' element has unknown 'id' attribute: '%s'!", idStr.c_str()))
return;
- mProgressColors[type] = std::make_unique<DyePalette>(node.getProperty("color", std::string()));
+ std::string color;
+ if (node.attribute("color", color))
+ mProgressColors[id] = std::make_unique<DyePalette>(color);
+
+ for (auto childNode : node.children())
+ {
+ if (childNode.name() == "text")
+ readTextNode(childNode, mProgressTextFormats[id].emplace());
+ else
+ Log::info("Theme: Unknown node '%s' in progressbar!", childNode.name().data());
+ }
}