mirror of
https://gitea.wildfiregames.com/0ad/0ad.git
synced 2026-07-20 07:07:48 +00:00
bdd94bd264
This commit enhances text rendering by leveraging FreeType and a dynamic font atlas. Previously, GUI scaling relied on bitmap fonts, which led to blurry and distorted text when scaling up. Now, we scale the font size directly using FreeType, resulting in much sharper and more readable text at any GUI scale. Key improvements: - Replaced bitmap font scaling with true font size scaling via FreeType. - Reduced glyph cache dependency on fixed positions, relying on shader scaling instead. - Switched to float-based glyph positioning for more accurate rendering and scaling. These changes ensure clean and consistent text appearance across different GUI scales.
382 lines
14 KiB
C++
382 lines
14 KiB
C++
/* Copyright (C) 2025 Wildfire Games.
|
||
* This file is part of 0 A.D.
|
||
*
|
||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU General Public License as published by
|
||
* the Free Software Foundation, either version 2 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* 0 A.D. is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU General Public License
|
||
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
#include "lib/self_test.h"
|
||
|
||
#include "graphics/FontManager.h"
|
||
#include "graphics/FontMetrics.h"
|
||
#include "gui/CGUI.h"
|
||
#include "gui/CGUIText.h"
|
||
#include "gui/SettingTypes/CGUIString.h"
|
||
#include "i18n/L10n.h"
|
||
#include "ps/CLogger.h"
|
||
#include "ps/ConfigDB.h"
|
||
#include "ps/Filesystem.h"
|
||
#include "ps/ProfileViewer.h"
|
||
#include "ps/VideoMode.h"
|
||
#include "renderer/Renderer.h"
|
||
#include "scriptinterface/ScriptInterface.h"
|
||
|
||
#include <algorithm>
|
||
#include <array>
|
||
#include <optional>
|
||
|
||
class TestCGUIText : public CxxTest::TestSuite
|
||
{
|
||
std::optional<CXeromycesEngine> m_XeromycesEngine;
|
||
CProfileViewer* m_Viewer = nullptr;
|
||
CRenderer* m_Renderer = nullptr;
|
||
std::unique_ptr<L10n> m_L10n;
|
||
|
||
public:
|
||
void setUp()
|
||
{
|
||
g_VFS = CreateVfs();
|
||
TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "_test.minimal" / "", VFS_MOUNT_MUST_EXIST));
|
||
TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY));
|
||
|
||
m_XeromycesEngine.emplace();
|
||
|
||
// The renderer spews messages.
|
||
TestLogger logger;
|
||
|
||
// We need to initialise the renderer to initialise the font manager.
|
||
// TODO: decouple this.
|
||
CConfigDB::Initialise();
|
||
CConfigDB::Instance()->SetValueString(CFG_SYSTEM, "rendererbackend", "dummy");
|
||
|
||
CConfigDB::Instance()->SetValueString(CFG_SYSTEM, "fonts.default", "LinBiolinum_Rah.ttf");
|
||
CConfigDB::Instance()->SetValueString(CFG_SYSTEM, "fonts.sans.regular", "LinBiolinum_Rah.ttf");
|
||
CConfigDB::Instance()->SetValueString(CFG_SYSTEM, "fonts.sans.bold", "LinBiolinum_RBah.ttf");
|
||
CConfigDB::Instance()->SetValueString(CFG_SYSTEM, "fonts.sans.italic", "LinBiolinum_RIah.ttf");
|
||
CConfigDB::Instance()->SetValueString(CFG_SYSTEM, "fonts.mono.regular", "DejaVuSansMono.ttf");
|
||
|
||
g_VideoMode.InitNonSDL();
|
||
g_VideoMode.CreateBackendDevice(false);
|
||
m_Viewer = new CProfileViewer;
|
||
m_Renderer = new CRenderer(g_VideoMode.GetBackendDevice());
|
||
m_L10n = std::make_unique<L10n>();
|
||
}
|
||
|
||
void tearDown()
|
||
{
|
||
m_L10n.reset();
|
||
delete m_Renderer;
|
||
delete m_Viewer;
|
||
g_VideoMode.Shutdown();
|
||
CConfigDB::Shutdown();
|
||
m_XeromycesEngine.reset();
|
||
g_VFS.reset();
|
||
DeleteDirectory(DataDir() / "_testcache");
|
||
}
|
||
|
||
void test_empty()
|
||
{
|
||
CGUI gui{*g_ScriptContext};
|
||
CGUIText empty;
|
||
}
|
||
|
||
void test_wrapping()
|
||
{
|
||
CGUI gui{*g_ScriptContext};
|
||
|
||
const CStrW font{L"mono-10"};
|
||
const CFontMetrics fontMetrics{CStrIntern(font.ToUTF8())};
|
||
|
||
const float lineHeight{fontMetrics.GetHeight()};
|
||
const float lineSpacing{fontMetrics.GetLineSpacing()};
|
||
|
||
CGUIString string;
|
||
CGUIText text;
|
||
float width = 0.f;
|
||
float renderedWidth = 0.f;
|
||
float padding = 0.f;
|
||
EAlign align = EAlign::LEFT;
|
||
|
||
// Thing to note: the space before the newline should collapse in right-alignment.
|
||
string.SetValue(L"Some long text that will wrap-around. \n New line.");
|
||
text = CGUIText(gui, string, font, width, padding, align, nullptr);
|
||
|
||
// Width 0 means no wrapping, so we should be getting one render call & one line.
|
||
// TODO: is it wanted that \n doesn't wrap in that case?
|
||
// We have 11 calls: the 9 words (wrap-around is split in two), the space after the newline, and the newline itself.
|
||
TS_ASSERT_EQUALS(text.GetTextCalls().size(), 11);
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight);
|
||
|
||
width = 100.f;
|
||
padding = 2.0f;
|
||
align = EAlign::LEFT;
|
||
text = CGUIText(gui, string, font, width, padding, align, nullptr);
|
||
renderedWidth = text.GetSize().Width;
|
||
// We have 10 calls: the 9 words (wrap-around is split in two), the space after the newline.
|
||
TS_ASSERT_EQUALS(text.GetTextCalls().size(), 10);
|
||
TS_ASSERT_LESS_THAN(text.GetSize().Width, width);
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing * 3);
|
||
|
||
align = EAlign::RIGHT;
|
||
text = CGUIText(gui, string, font, width, padding, align, nullptr);
|
||
TS_ASSERT_EQUALS(text.GetTextCalls().size(), 10);
|
||
TS_ASSERT_EQUALS(text.GetSize().Width, renderedWidth); // Should be the same width as the left-case.
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing * 3);
|
||
|
||
width = 400.f;
|
||
padding = 3.0f;
|
||
align = EAlign::LEFT;
|
||
text = CGUIText(gui, string, font, width, padding, align, nullptr);
|
||
TS_ASSERT_EQUALS(text.GetTextCalls().size(), 10);
|
||
TS_ASSERT_LESS_THAN(text.GetSize().Width, width);
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing);
|
||
|
||
width = 400.f;
|
||
padding = 5.0f;
|
||
align = EAlign::CENTER;
|
||
text = CGUIText(gui, string, font, width, padding, align, nullptr);
|
||
renderedWidth = text.GetSize().Width;
|
||
TS_ASSERT_LESS_THAN(text.GetSize().Width, width);
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing);
|
||
|
||
align = EAlign::RIGHT;
|
||
text = CGUIText(gui, string, font, width, padding, align, nullptr);
|
||
TS_ASSERT_EQUALS(text.GetSize().Width, renderedWidth); // Should be the same width as the center-case.
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing);
|
||
|
||
align = EAlign::LEFT;
|
||
text = CGUIText(gui, string, font, width, padding, align, nullptr);
|
||
TS_ASSERT_EQUALS(text.GetSize().Width, renderedWidth); // Should be the same width as the center-case.
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing);
|
||
|
||
width = 400.f;
|
||
padding = 100.0f;
|
||
align = EAlign::LEFT;
|
||
text = CGUIText(gui, string, font, width, padding, align, nullptr);
|
||
renderedWidth = text.GetSize().Width;
|
||
TS_ASSERT_LESS_THAN(text.GetSize().Width, width);
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing * 2);
|
||
|
||
align = EAlign::RIGHT;
|
||
text = CGUIText(gui, string, font, width, padding, align, nullptr);
|
||
TS_ASSERT_EQUALS(text.GetSize().Width, renderedWidth); // Should be the same width as the left-case.
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, padding * 2 + lineHeight + lineSpacing * 2);
|
||
}
|
||
|
||
void test_layout_wrapping_center()
|
||
{
|
||
// The short word should be layouted the same as when there is no
|
||
// second word, because it's the only one word in the first line until
|
||
// the width is enough to fit both. So we need to check that for
|
||
// increasing width X positions of both words are also increasing until
|
||
// they fit into a single line.
|
||
//
|
||
// Width is too small for both:
|
||
// +--------------------+
|
||
// | Shortword |
|
||
// | (Veryverylongword) |
|
||
// +--------------------+
|
||
//
|
||
// Right before the big enough width:
|
||
// +---------------------------+
|
||
// | Shortword |
|
||
// | (Veryverylongword) |
|
||
// +---------------------------+
|
||
//
|
||
// Width is enough to fit both:
|
||
// +------------------------------+
|
||
// | Shortword (Veryverylongword) |
|
||
// +------------------------------+
|
||
//
|
||
|
||
CGUI gui{*g_ScriptContext};
|
||
const CStrW firstWord = L"Shortword";
|
||
const CStrW secondWord = L"(Veryverylongword)";
|
||
const CStrW text = firstWord + L" " + secondWord;
|
||
CGUIString string;
|
||
string.SetValue(text);
|
||
const CStrW font{L"mono-10"};
|
||
CFontMetrics fontMetrics{CStrIntern(font.ToUTF8())};
|
||
|
||
float firstWordWidth{0};
|
||
float firstWordHeight{0};
|
||
fontMetrics.CalculateStringSize(firstWord.c_str(), firstWordWidth, firstWordHeight);
|
||
TS_ASSERT(firstWordWidth > 0);
|
||
float secondWordWidth{0};
|
||
float secondWordHeight{0};
|
||
fontMetrics.CalculateStringSize(secondWord.c_str(), secondWordWidth, secondWordHeight);
|
||
TS_ASSERT(secondWordWidth > 0);
|
||
TS_ASSERT(firstWordWidth < secondWordWidth);
|
||
const float spaceWidth = fontMetrics.GetCharacterWidth(L' ');
|
||
|
||
const float lineHeight = fontMetrics.GetHeight();
|
||
const float lineSpacing = fontMetrics.GetLineSpacing();
|
||
|
||
auto layoutText = [&gui, &string, &font, lineHeight, firstWordWidth](const float width)
|
||
{
|
||
const float padding = 0.0f;
|
||
const CGUIText text(gui, string, font, width, padding, EAlign::CENTER, nullptr);
|
||
|
||
std::array<CVector2D, 2> positions;
|
||
TS_ASSERT_EQUALS(text.GetTextCalls().size(), positions.size());
|
||
|
||
// If the second word is on the next line then the first word should be
|
||
// centered alone.
|
||
if (text.GetTextCalls()[0].m_Pos.Y + lineHeight <= text.GetTextCalls()[1].m_Pos.Y)
|
||
{
|
||
const float expectedX = (width - firstWordWidth) / 2.0f;
|
||
TS_ASSERT_EQUALS(text.GetTextCalls()[0].m_Pos.X, expectedX);
|
||
}
|
||
|
||
std::transform(
|
||
text.GetTextCalls().begin(), text.GetTextCalls().end(), positions.begin(),
|
||
[](const CGUIText::STextCall& textCall) { return textCall.m_Pos; });
|
||
return positions;
|
||
};
|
||
|
||
const float testPadding = 2;
|
||
const float beginWidth = firstWordWidth - testPadding;
|
||
const float endWidth = firstWordWidth + spaceWidth + secondWordWidth + testPadding;
|
||
std::array<CVector2D, 2> previousPositions = layoutText(beginWidth);
|
||
TS_ASSERT_EQUALS(std::get<0>(previousPositions).Y, lineHeight);
|
||
TS_ASSERT_EQUALS(std::get<1>(previousPositions).Y, lineHeight + lineSpacing);
|
||
bool firstFit = false;
|
||
for (float width = beginWidth; width <= endWidth; width += 1.0f)
|
||
{
|
||
std::array<CVector2D, 2> positions = layoutText(width);
|
||
TS_ASSERT_EQUALS(std::get<0>(positions).Y, lineHeight);
|
||
if (std::get<0>(positions).X >= std::get<0>(previousPositions).X)
|
||
{
|
||
if (firstFit)
|
||
{
|
||
TS_ASSERT_EQUALS(std::get<0>(positions).Y, std::get<1>(positions).Y);
|
||
}
|
||
else
|
||
{
|
||
TS_ASSERT_EQUALS(std::get<1>(positions).Y, lineHeight + lineSpacing);
|
||
}
|
||
TS_ASSERT(std::get<1>(positions).X >= std::get<1>(previousPositions).X);
|
||
}
|
||
else
|
||
{
|
||
TS_ASSERT(!firstFit);
|
||
firstFit = true;
|
||
TS_ASSERT_EQUALS(std::get<0>(positions).Y, std::get<1>(positions).Y);
|
||
TS_ASSERT(std::get<0>(positions).X < std::get<1>(positions).X);
|
||
}
|
||
previousPositions = positions;
|
||
}
|
||
TS_ASSERT(firstFit);
|
||
}
|
||
|
||
void test_overflow()
|
||
{
|
||
CGUI gui{*g_ScriptContext};
|
||
|
||
const CStrW font{L"mono-10"};
|
||
const CFontMetrics fontMetrics{CStrIntern(font.ToUTF8())};
|
||
const float lineHeight{fontMetrics.GetHeight()};
|
||
const float lineSpacing{fontMetrics.GetLineSpacing()};
|
||
|
||
float renderedWidth = 0.f;
|
||
const float width = 200.f;
|
||
const float padding = 20.f;
|
||
|
||
CGUIString string;
|
||
CGUIText text;
|
||
string.SetValue(L"wordthatisverylonganddefinitelywontfitinaline and other words");
|
||
text = CGUIText(gui, string, font, width, padding, EAlign::LEFT, nullptr);
|
||
renderedWidth = text.GetSize().Width;
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing * 1 + padding * 2);
|
||
text = CGUIText(gui, string, font, width, padding, EAlign::CENTER, nullptr);
|
||
TS_ASSERT_EQUALS(renderedWidth, text.GetSize().Width);
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing * 1 + padding * 2);
|
||
text = CGUIText(gui, string, font, width, padding, EAlign::RIGHT, nullptr);
|
||
TS_ASSERT_EQUALS(renderedWidth, text.GetSize().Width);
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing * 1 + padding * 2);
|
||
|
||
string.SetValue(L"other words and wordthatisverylonganddefinitelywontfitinaline");
|
||
text = CGUIText(gui, string, font, width, padding, EAlign::LEFT, nullptr);
|
||
renderedWidth = text.GetSize().Width;
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing * 1 + padding * 2);
|
||
text = CGUIText(gui, string, font, width, padding, EAlign::CENTER, nullptr);
|
||
TS_ASSERT_EQUALS(renderedWidth, text.GetSize().Width);
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing * 1 + padding * 2);
|
||
text = CGUIText(gui, string, font, width, padding, EAlign::RIGHT, nullptr);
|
||
TS_ASSERT_EQUALS(renderedWidth, text.GetSize().Width);
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing * 1 + padding * 2);
|
||
|
||
string.SetValue(L"wordthatisverylonganddefinitelywontfitinaline");
|
||
text = CGUIText(gui, string, font, width, padding, EAlign::LEFT, nullptr);
|
||
renderedWidth = text.GetSize().Width;
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + padding * 2);
|
||
text = CGUIText(gui, string, font, width, padding, EAlign::CENTER, nullptr);
|
||
TS_ASSERT_EQUALS(renderedWidth, text.GetSize().Width);
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + padding * 2);
|
||
text = CGUIText(gui, string, font, width, padding, EAlign::RIGHT, nullptr);
|
||
TS_ASSERT_EQUALS(renderedWidth, text.GetSize().Width);
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + padding * 2);
|
||
}
|
||
|
||
void test_regression_rP26522()
|
||
{
|
||
CGUI gui{*g_ScriptContext};
|
||
|
||
const CStrW font{L"sans-bold-13"};
|
||
CFontMetrics fontMetrics{CStrIntern(font.ToUTF8())};
|
||
const float lineHeight = fontMetrics.GetHeight();
|
||
const float lineSpacing = fontMetrics.GetLineSpacing();
|
||
|
||
CGUIString string;
|
||
CGUIText text;
|
||
|
||
// rP26522 introduced a bug that triggered in rare cases with word-wrapping.
|
||
string.SetValue(L"90–120 min");
|
||
text = CGUIText(gui, string, font, 53, 8.f, EAlign::LEFT, nullptr);
|
||
|
||
TS_ASSERT_EQUALS(text.GetTextCalls().size(), 2);
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing + 8 * 2);
|
||
}
|
||
|
||
void test_multiple_blank_spaces()
|
||
{
|
||
CGUI gui{*g_ScriptContext};
|
||
|
||
const CStrW font{L"mono-10"};
|
||
const CFontMetrics fontMetrics{CStrIntern(font.ToUTF8())};
|
||
const float lineHeight{fontMetrics.GetHeight()};
|
||
const float lineSpacing{fontMetrics.GetLineSpacing()};
|
||
|
||
CGUIString string;
|
||
CGUIText text;
|
||
float width = 100.f;
|
||
float renderedWidth = 0.f;
|
||
float padding = 0.f;
|
||
EAlign align = EAlign::LEFT;
|
||
|
||
string.SetValue(L" word another \n spaces \n \n word ");
|
||
text = CGUIText(gui, string, font, width, padding, align, nullptr);
|
||
|
||
// Blank spaces are treated as a word.
|
||
TS_ASSERT_EQUALS(text.GetTextCalls().size(), 26);
|
||
TS_ASSERT_EQUALS(text.GetSize().Height, lineHeight + lineSpacing * 4);
|
||
// The longest line in text is " spaces " which amounts to 13 characters with a character width of 6.
|
||
TS_ASSERT_EQUALS(text.GetSize().Width, 78.f);
|
||
renderedWidth = text.GetSize().Width;
|
||
|
||
align = EAlign::RIGHT;
|
||
text = CGUIText(gui, string, font, width, padding, align, nullptr);
|
||
TS_ASSERT_EQUALS(text.GetSize().Width, renderedWidth);
|
||
}
|
||
};
|