diff options
author | Thorbjørn Lindeijer <bjorn@lindeijer.nl> | 2025-03-31 15:34:22 +0200 |
---|---|---|
committer | Thorbjørn Lindeijer <bjorn@lindeijer.nl> | 2025-03-31 16:56:38 +0200 |
commit | 49beab3c3415d40d9a1d4326474d16547c4ae9ac (patch) | |
tree | 7065a4dcca9089e711eeba2ef4206cbcd955f80c /src | |
parent | 83339662e47270f2b38e7430775090f409348ae2 (diff) | |
download | mana-49beab3c3415d40d9a1d4326474d16547c4ae9ac.tar.gz mana-49beab3c3415d40d9a1d4326474d16547c4ae9ac.tar.bz2 mana-49beab3c3415d40d9a1d4326474d16547c4ae9ac.tar.xz mana-49beab3c3415d40d9a1d4326474d16547c4ae9ac.zip |
GUI: Support customizing widget text format through theme
The following widgets now support setting the font, text color, outline
color and shadow color through the theme:
* Button
* Tab
* Window (title)
* ProgressBar
The text format can be specified differently per skin state.
Diffstat (limited to 'src')
-rw-r--r-- | src/gui/widgets/button.cpp | 48 | ||||
-rw-r--r-- | src/gui/widgets/label.cpp | 35 | ||||
-rw-r--r-- | src/gui/widgets/label.h | 27 | ||||
-rw-r--r-- | src/gui/widgets/tab.cpp | 33 | ||||
-rw-r--r-- | src/gui/widgets/tab.h | 2 | ||||
-rw-r--r-- | src/gui/widgets/window.cpp | 32 | ||||
-rw-r--r-- | src/resources/theme.cpp | 126 | ||||
-rw-r--r-- | src/resources/theme.h | 16 | ||||
-rw-r--r-- | src/textrenderer.h | 96 |
9 files changed, 291 insertions, 124 deletions
diff --git a/src/gui/widgets/button.cpp b/src/gui/widgets/button.cpp index 31fab2c4..31c3a677 100644 --- a/src/gui/widgets/button.cpp +++ b/src/gui/widgets/button.cpp @@ -28,6 +28,7 @@ #include "resources/image.h" #include "resources/theme.h" +#include "textrenderer.h" #include <guichan/exception.hpp> #include <guichan/font.hpp> @@ -125,33 +126,32 @@ void Button::init() void Button::draw(gcn::Graphics *graphics) { - WidgetState state(this); + WidgetState widgetState(this); if (mHasMouse) - state.flags |= STATE_HOVERED; + widgetState.flags |= STATE_HOVERED; if (isPressed()) - state.flags |= STATE_SELECTED; + widgetState.flags |= STATE_SELECTED; - gui->getTheme()->drawSkin(static_cast<Graphics *>(graphics), SkinType::Button, state); + auto &skin = gui->getTheme()->getSkin(SkinType::Button); + skin.draw(static_cast<Graphics *>(graphics), widgetState); + + auto skinState = skin.getState(widgetState.flags); + auto font = (skinState && skinState->textFormat.bold) ? boldFont : getFont(); int mode; - if (state.flags & STATE_DISABLED) + if (widgetState.flags & STATE_DISABLED) mode = BUTTON_DISABLED; - else if (state.flags & STATE_SELECTED) + else if (widgetState.flags & STATE_SELECTED) mode = BUTTON_PRESSED; - else if (state.flags & (STATE_HOVERED | STATE_FOCUSED)) + else if (widgetState.flags & (STATE_HOVERED | STATE_FOCUSED)) mode = BUTTON_HIGHLIGHTED; else mode = BUTTON_STANDARD; - if (mode == BUTTON_DISABLED) - graphics->setColor(Theme::getThemeColor(Theme::BUTTON_DISABLED)); - else - graphics->setColor(Theme::getThemeColor(Theme::BUTTON)); - Image *icon = mButtonIcon.empty() ? nullptr : mButtonIcon[mode].get(); int textX = 0; - int textY = getHeight() / 2 - getFont()->getHeight() / 2; + int textY = getHeight() / 2 - font->getHeight() / 2; int btnIconX = 0; int btnIconY = getHeight() / 2 - (icon ? icon->getHeight() / 2 : 0); int btnIconWidth = icon ? icon->getWidth() : 0; @@ -172,9 +172,8 @@ void Button::draw(gcn::Graphics *graphics) case gcn::Graphics::CENTER: if (btnIconWidth) { - btnIconX = getWidth() / 2 - (getFont()->getWidth(mCaption) - + icon->getWidth() + 2) / 2; - textX = getWidth() / 2 + icon->getWidth() / 2 + 2; + btnIconX = (getWidth() - font->getWidth(mCaption) - icon->getWidth() - 2) / 2; + textX = (getWidth() + icon->getWidth()) / 2 + 2; } else { @@ -183,15 +182,13 @@ void Button::draw(gcn::Graphics *graphics) break; case gcn::Graphics::RIGHT: if (btnIconWidth) - btnIconX = getWidth() - 4 - getFont()->getWidth(mCaption) - 2; + btnIconX = getWidth() - 4 - font->getWidth(mCaption) - 2; textX = getWidth() - 4; break; default: throw GCN_EXCEPTION("Button::draw(). Unknown alignment."); } - graphics->setFont(getFont()); - if (isPressed()) { textX++; textY++; @@ -200,7 +197,18 @@ void Button::draw(gcn::Graphics *graphics) if (btnIconWidth) static_cast<Graphics *>(graphics)->drawImage(icon, btnIconX, btnIconY); - graphics->drawText(getCaption(), textX, textY, getAlignment()); + + if (auto skinState = skin.getState(widgetState.flags)) + { + auto &textFormat = skinState->textFormat; + TextRenderer::renderText(static_cast<Graphics *>(graphics), + getCaption(), + textX, + textY, + getAlignment(), + font, + textFormat); + } } void Button::adjustSize() diff --git a/src/gui/widgets/label.cpp b/src/gui/widgets/label.cpp index af5220ef..53a82e14 100644 --- a/src/gui/widgets/label.cpp +++ b/src/gui/widgets/label.cpp @@ -21,8 +21,13 @@ #include "gui/widgets/label.h" +#include "textrenderer.h" + #include "resources/theme.h" +#include <guichan/exception.hpp> +#include <guichan/font.hpp> + Label::Label() { setForegroundColor(Theme::getThemeColor(Theme::TEXT)); @@ -36,5 +41,33 @@ Label::Label(const std::string &caption) : void Label::draw(gcn::Graphics *graphics) { - gcn::Label::draw(static_cast<gcn::Graphics*>(graphics)); + int textX; + int textY = getHeight() / 2 - getFont()->getHeight() / 2; + + switch (getAlignment()) + { + case Graphics::LEFT: + textX = 0; + break; + case Graphics::CENTER: + textX = getWidth() / 2; + break; + case Graphics::RIGHT: + textX = getWidth(); + break; + default: + throw GCN_EXCEPTION("Unknown alignment."); + } + + TextRenderer::renderText(static_cast<Graphics *>(graphics), + getCaption(), + textX, + textY, + getAlignment(), + getForegroundColor(), + getFont(), + mOutlineColor.has_value(), + mShadowColor.has_value(), + mOutlineColor, + mShadowColor); } diff --git a/src/gui/widgets/label.h b/src/gui/widgets/label.h index a383517f..85bcbe23 100644 --- a/src/gui/widgets/label.h +++ b/src/gui/widgets/label.h @@ -22,10 +22,11 @@ #pragma once #include <guichan/widgets/label.hpp> +#include <optional> /** * Label widget. Same as the Guichan label but modified to use the palette - * system. + * system and support outlines and shadows. * * \ingroup GUI */ @@ -41,7 +42,31 @@ class Label : public gcn::Label Label(const std::string &caption); /** + * Sets the color of the outline. + */ + void setOutlineColor(std::optional<gcn::Color> color); + + /** + * Sets the color of the shadow. + */ + void setShadowColor(std::optional<gcn::Color> color); + + /** * Draws the label. */ void draw(gcn::Graphics *graphics) override; + + private: + std::optional<gcn::Color> mOutlineColor; + std::optional<gcn::Color> mShadowColor; }; + +inline void Label::setOutlineColor(std::optional<gcn::Color> color) +{ + mOutlineColor = color; +} + +inline void Label::setShadowColor(std::optional<gcn::Color> color) +{ + mShadowColor = color; +} diff --git a/src/gui/widgets/tab.cpp b/src/gui/widgets/tab.cpp index 3bfa405f..b2779c4f 100644 --- a/src/gui/widgets/tab.cpp +++ b/src/gui/widgets/tab.cpp @@ -24,17 +24,22 @@ #include "graphics.h" #include "gui/gui.h" +#include "gui/widgets/label.h" #include "gui/widgets/tabbedarea.h" #include "resources/theme.h" #include <guichan/widgets/label.hpp> -Tab::Tab() : - mTabColor(&Theme::getThemeColor(Theme::TAB)) +Tab::Tab() { setFocusable(false); + // Replace the label with customized version + delete mLabel; + mLabel = new Label(); + add(mLabel); + auto &skin = gui->getTheme()->getSkin(SkinType::Tab); setFrameSize(skin.frameSize); mPadding = skin.padding; @@ -62,10 +67,26 @@ void Tab::draw(gcn::Graphics *graphics) if (mTabbedArea && mTabbedArea->isTabSelected(this)) mFlash = false; - if (mFlash) - mLabel->setForegroundColor(Theme::getThemeColor(Theme::TAB_FLASH)); - else - mLabel->setForegroundColor(*mTabColor); + uint8_t flags = 0; + if (mHasMouse) + flags |= STATE_HOVERED; + if (mTabbedArea && mTabbedArea->isTabSelected(this)) + flags |= STATE_SELECTED; + + auto &skin = gui->getTheme()->getSkin(SkinType::Tab); + if (auto state = skin.getState(flags)) + { + gcn::Color foregroundColor = state->textFormat.color; + if (mFlash) + foregroundColor = Theme::getThemeColor(Theme::TAB_FLASH); + else if (mTabColor) + foregroundColor = *mTabColor; + + auto label = static_cast<Label*>(mLabel); + label->setForegroundColor(foregroundColor); + label->setOutlineColor(state->textFormat.outlineColor); + label->setShadowColor(state->textFormat.shadowColor); + } // draw label drawChildren(graphics); diff --git a/src/gui/widgets/tab.h b/src/gui/widgets/tab.h index d67321c6..534abaff 100644 --- a/src/gui/widgets/tab.h +++ b/src/gui/widgets/tab.h @@ -66,7 +66,7 @@ class Tab : public gcn::Tab virtual void setCurrent() {} private: - const gcn::Color *mTabColor; + const gcn::Color *mTabColor = nullptr; bool mFlash = false; int mPadding = 8; }; diff --git a/src/gui/widgets/window.cpp b/src/gui/widgets/window.cpp index 15267019..4a0b8286 100644 --- a/src/gui/widgets/window.cpp +++ b/src/gui/widgets/window.cpp @@ -23,6 +23,7 @@ #include "configuration.h" #include "log.h" +#include "textrenderer.h" #include "gui/gui.h" #include "gui/viewport.h" @@ -124,21 +125,26 @@ void Window::drawFrame(gcn::Graphics *graphics) auto g = static_cast<Graphics*>(graphics); auto theme = gui->getTheme(); - WidgetState state(this); - state.width += getFrameSize() * 2; - state.height += getFrameSize() * 2; + WidgetState widgetState(this); + widgetState.width += getFrameSize() * 2; + widgetState.height += getFrameSize() * 2; auto &skin = theme->getSkin(SkinType::Window); - skin.draw(g, state); + skin.draw(g, widgetState); if (mShowTitle) { - g->setColor(Theme::getThemeColor(Theme::TEXT)); - g->setFont(getFont()); - g->drawText(getCaption(), - getFrameSize() + skin.titleOffsetX, - getFrameSize() + skin.titleOffsetY, - gcn::Graphics::LEFT); + if (auto skinState = skin.getState(widgetState.flags)) + { + auto &textFormat = skinState->textFormat; + TextRenderer::renderText(g, + getCaption(), + getFrameSize() + skin.titleOffsetX, + getFrameSize() + skin.titleOffsetY, + gcn::Graphics::LEFT, + textFormat.bold ? boldFont : getFont(), + textFormat); + } } } @@ -686,6 +692,9 @@ int Window::getResizeHandles(gcn::MouseEvent &event) gcn::Rectangle Window::getCloseButtonRect() const { + if (!mCloseButton) + return {}; + auto theme = gui->getTheme(); auto &closeButtonSkin = theme->getSkin(SkinType::ButtonClose); @@ -702,6 +711,9 @@ gcn::Rectangle Window::getCloseButtonRect() const gcn::Rectangle Window::getStickyButtonRect() const { + if (!mStickyButton) + return {}; + auto theme = gui->getTheme(); auto &closeButtonSkin = theme->getSkin(SkinType::ButtonClose); diff --git a/src/resources/theme.cpp b/src/resources/theme.cpp index 01c6b6f1..9dd602c7 100644 --- a/src/resources/theme.cpp +++ b/src/resources/theme.cpp @@ -108,44 +108,50 @@ void Skin::addState(SkinState state) void Skin::draw(Graphics *graphics, const WidgetState &state) const { - for (const auto &skinState : mStates) - { - if (skinState.stateFlags != (skinState.setFlags & state.flags)) - continue; + // Only draw the first matching state + auto skinState = getState(state.flags); + if (!skinState) + return; - for (const auto &part : skinState.parts) - { - 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); - } - else if constexpr (std::is_same_v<T, ColoredRectangle>) - { - 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)); - } - }, part.data); - } + for (const auto &part : skinState->parts) + { + std::visit([&](const auto &data) { + using T = std::decay_t<decltype(data)>; - break; // Only draw the first matching state + 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); + } + else if constexpr (std::is_same_v<T, ColoredRectangle>) + { + 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)); + } + }, part.data); } } +const SkinState *Skin::getState(uint8_t flags) const +{ + for (const auto &skinState : mStates) + if (skinState.stateFlags == (skinState.setFlags & flags)) + return &skinState; + + return nullptr; +} + int Skin::getMinWidth() const { int minWidth = 0; @@ -295,13 +301,14 @@ void Theme::drawProgressBar(Graphics *graphics, const gcn::Rectangle &area, gcn::Font *oldFont = graphics->getFont(); gcn::Color oldColor = graphics->getColor(); - WidgetState state; - state.x = area.x; - state.y = area.y; - state.width = area.width; - state.height = area.height; + WidgetState widgetState; + widgetState.x = area.x; + widgetState.y = area.y; + widgetState.width = area.width; + widgetState.height = area.height; - drawSkin(graphics, SkinType::ProgressBar, state); + auto &skin = getSkin(SkinType::ProgressBar); + skin.draw(graphics, widgetState); // The bar if (progress > 0) @@ -316,18 +323,20 @@ void Theme::drawProgressBar(Graphics *graphics, const gcn::Rectangle &area, // The label if (!text.empty()) { - const int textX = area.x + area.width / 2; - const int textY = area.y + (area.height - boldFont->getHeight()) / 2; - - TextRenderer::renderText(graphics, - text, - textX, - textY, - gcn::Graphics::CENTER, - Theme::getThemeColor(Theme::PROGRESS_BAR), - gui->getFont(), - true, - false); + if (auto skinState = skin.getState(widgetState.flags)) + { + auto font = skinState->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->setFont(oldFont); @@ -500,11 +509,22 @@ void Theme::readSkinStateNode(XML::Node node, Skin &skin) const readSkinStateImgNode(childNode, state); else if (childNode.name() == "rect") readSkinStateRectNode(childNode, state); + else if (childNode.name() == "text") + readSkinStateTextNode(childNode, state); } 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) { @@ -631,10 +651,6 @@ static int readColorType(const std::string &type) "TEXT", "SHADOW", "OUTLINE", - "PROGRESS_BAR", - "BUTTON", - "BUTTON_DISABLED", - "TAB", "PARTY_CHAT_TAB", "PARTY_SOCIAL_TAB", "BACKGROUND", diff --git a/src/resources/theme.h b/src/resources/theme.h index 55092127..4f3535b4 100644 --- a/src/resources/theme.h +++ b/src/resources/theme.h @@ -95,10 +95,19 @@ struct SkinPart std::variant<ImageRect, Image *, ColoredRectangle> data; }; +struct TextFormat +{ + bool bold = false; + gcn::Color color; + std::optional<gcn::Color> outlineColor; + std::optional<gcn::Color> shadowColor; +}; + struct SkinState { uint8_t stateFlags = 0; uint8_t setFlags = 0; + TextFormat textFormat; std::vector<SkinPart> parts; }; @@ -125,6 +134,8 @@ class Skin void draw(Graphics *graphics, const WidgetState &state) const; + const SkinState *getState(uint8_t flags) const; + /** * Returns the minimum width which can be used with this skin. */ @@ -169,10 +180,6 @@ class Theme : public Palette, public EventListener TEXT, SHADOW, OUTLINE, - PROGRESS_BAR, - BUTTON, - BUTTON_DISABLED, - TAB, PARTY_CHAT_TAB, PARTY_SOCIAL_TAB, BACKGROUND, @@ -276,6 +283,7 @@ class Theme : public Palette, public EventListener bool readTheme(const std::string &filename); void readSkinNode(XML::Node node); void readSkinStateNode(XML::Node node, Skin &skin) const; + void readSkinStateTextNode(XML::Node node, SkinState &state) const; void readSkinStateImgNode(XML::Node node, SkinState &state) const; void readSkinStateRectNode(XML::Node node, SkinState &state) const; void readColorNode(XML::Node node); diff --git a/src/textrenderer.h b/src/textrenderer.h index a646b2f5..c9cd9310 100644 --- a/src/textrenderer.h +++ b/src/textrenderer.h @@ -21,13 +21,13 @@ #pragma once -#include "graphics.h" - #include "resources/theme.h" +#include <guichan/exception.hpp> +#include <guichan/font.hpp> + /** - * Class for text rendering. Used by the TextParticle, the Text and FlashText - * objects and the Preview in the color dialog. + * Class for text rendering which can apply an outline and shadow. */ class TextRenderer { @@ -38,46 +38,90 @@ public: static void renderText(gcn::Graphics *graphics, const std::string &text, int x, int y, - gcn::Graphics::Alignment align, + gcn::Graphics::Alignment alignment, const gcn::Color &color, gcn::Font *font, bool outline = false, - bool shadow = false) + bool shadow = false, + const std::optional<gcn::Color> &outlineColor = {}, + const std::optional<gcn::Color> &shadowColor = {}) { - graphics->setFont(font); + switch (alignment) + { + case gcn::Graphics::LEFT: + break; + case gcn::Graphics::CENTER: + x -= font->getWidth(text) / 2; + break; + case gcn::Graphics::RIGHT: + x -= font->getWidth(text); + break; + default: + throw GCN_EXCEPTION("Unknown alignment."); + } // Text shadow if (shadow) { - graphics->setColor(Theme::getThemeColor(Theme::SHADOW, - color.a / 2)); + if (shadowColor) + graphics->setColor(*shadowColor); + else + graphics->setColor(Theme::getThemeColor(Theme::SHADOW, color.a / 2)); + if (outline) - { - graphics->drawText(text, x + 2, y + 2, align); - } + font->drawString(graphics, text, x + 2, y + 2); else - { - graphics->drawText(text, x + 1, y + 1, align); - } + font->drawString(graphics, text, x + 1, y + 1); } - if (outline) { -/* graphics->setColor(guiPalette->getColor(Palette::OUTLINE, + if (outline) + { + /* + graphics->setColor(guiPalette->getColor(Palette::OUTLINE, alpha/4)); // TODO: Reanable when we can draw it nicely in software mode - graphics->drawText(text, x + 2, y + 2, align); - graphics->drawText(text, x + 1, y + 2, align); - graphics->drawText(text, x + 2, y + 1, align);*/ + font->drawString(graphics, text, x + 2, y + 2); + font->drawString(graphics, text, x + 1, y + 2); + font->drawString(graphics, text, x + 2, y + 1); + */ // Text outline - graphics->setColor(Theme::getThemeColor(Theme::OUTLINE, color.a)); - graphics->drawText(text, x + 1, y, align); - graphics->drawText(text, x - 1, y, align); - graphics->drawText(text, x, y + 1, align); - graphics->drawText(text, x, y - 1, align); + if (outlineColor) + graphics->setColor(*outlineColor); + else + graphics->setColor(Theme::getThemeColor(Theme::OUTLINE, color.a)); + + font->drawString(graphics, text, x + 1, y); + font->drawString(graphics, text, x - 1, y); + font->drawString(graphics, text, x, y + 1); + font->drawString(graphics, text, x, y - 1); } graphics->setColor(color); - graphics->drawText(text, x, y, align); + font->drawString(graphics, text, x, y); + } + + /** + * Renders a specified text. + */ + static void renderText(gcn::Graphics *graphics, + const std::string &text, + int x, + int y, + gcn::Graphics::Alignment align, + gcn::Font *font, + const TextFormat &format) + { + renderText(graphics, + text, + x, + y, + align, + format.color, + font, + format.outlineColor.has_value(), + format.shadowColor.has_value(), + format.outlineColor, + format.shadowColor); } }; |