Fix text alignment handling of spaces around wrapping.
Follows f8d2927748
.
There is an issue with text-wrapping and word separators (aka spaces).
Because 0 A.D. collates the space after a word to the same TextCall, we
occasionally need to ignore it when considering line-wrapping, because
we don't want empty spaces on the right-side of right-aligned text.
However, the logic to handle this is currently broken and inconsistent.
The method introduced here uses the SFeedback structure to properly
report it and generalises the checks.
Note that multiples spaces are not collapsed in 0 A.D., and for
consistency the word-separator-collapsing behaviour is ignored.
Comments by: phosit, vlasdislavbelov
Fixes #6551
Differential Revision: https://code.wildfiregames.com/D4662
This was SVN commit r26915.
This commit is contained in:
parent
b5abab5c79
commit
f4128208de
@ -1,7 +1,9 @@
|
||||
100
|
||||
101
|
||||
256 256
|
||||
a
|
||||
606
|
||||
20
|
||||
15
|
||||
12
|
||||
32 0 256 0 0 0 0 5
|
||||
33 250 154 3 11 1 11 4
|
||||
34 121 18 5 4 0 11 5
|
||||
|
@ -79,7 +79,14 @@ CGUIText::CGUIText(const CGUI& pGUI, const CGUIString& string, const CStrW& font
|
||||
int posLastImage = -1; // Position in the string where last img (either left or right) were encountered.
|
||||
// in order to avoid duplicate processing.
|
||||
|
||||
// Go through string word by word
|
||||
// The calculated width of each word includes the space between the current
|
||||
// word and the next. When we're wrapping, we need subtract the width of the
|
||||
// space after the last word on the line before the wrap.
|
||||
CFontMetrics currentFont(font);
|
||||
float spaceWidth = currentFont.GetCharacterWidth(L' ');
|
||||
|
||||
// Go through string word by word.
|
||||
// a word is defined as [start, end[ in string.m_Words so we skip the last item.
|
||||
for (int i = 0; i < static_cast<int>(string.m_Words.size()) - 1; ++i)
|
||||
{
|
||||
// Pre-process each line one time, so we know which floating images
|
||||
@ -102,8 +109,10 @@ CGUIText::CGUIText(const CGUI& pGUI, const CGUIString& string, const CStrW& font
|
||||
|
||||
prelimLineHeight = std::max(prelimLineHeight, feedback.m_Size.Height);
|
||||
|
||||
float spaceCorrection = feedback.m_EndsWithSpace ? spaceWidth : 0.f;
|
||||
|
||||
// If width is 0, then there's no word-wrapping, disable NewLine.
|
||||
if ((width != 0 && from != i && (lineWidth + 2 * bufferZone > width || feedback.m_NewLine)) || i == static_cast<int>(string.m_Words.size()) - 2)
|
||||
if ((width != 0 && from != i && (lineWidth - spaceCorrection + 2 * bufferZone > width || feedback.m_NewLine)) || i == static_cast<int>(string.m_Words.size()) - 2)
|
||||
{
|
||||
if (ProcessLine(pGUI, string, font, pObject, images, align, prelimLineHeight, width, bufferZone, firstLine, y, i, from))
|
||||
return;
|
||||
@ -173,6 +182,14 @@ void CGUIText::ComputeLineSize(
|
||||
const int tempFrom,
|
||||
CSize2D& lineSize) const
|
||||
{
|
||||
// The calculated width of each word includes the space between the current
|
||||
// word and the next. When we're wrapping, we need subtract the width of the
|
||||
// space after the last word on the line before the wrap.
|
||||
CFontMetrics currentFont(font);
|
||||
float spaceWidth = currentFont.GetCharacterWidth(L' ');
|
||||
|
||||
float spaceCorrection = 0.f;
|
||||
|
||||
float x = widthRangeFrom;
|
||||
for (int j = tempFrom; j <= i; ++j)
|
||||
{
|
||||
@ -187,15 +204,12 @@ void CGUIText::ComputeLineSize(
|
||||
// Append X value.
|
||||
x += feedback2.m_Size.Width;
|
||||
|
||||
if (width != 0 && x > widthRangeTo && j != tempFrom && !feedback2.m_NewLine)
|
||||
{
|
||||
// The calculated width of each word includes the space between the current
|
||||
// word and the next. When we're wrapping, we need subtract the width of the
|
||||
// space after the last word on the line before the wrap.
|
||||
CFontMetrics currentFont(font);
|
||||
lineSize.Width -= currentFont.GetCharacterWidth(*L" ");
|
||||
if (width != 0 && x - spaceCorrection > widthRangeTo && j != tempFrom && !feedback2.m_NewLine)
|
||||
break;
|
||||
}
|
||||
|
||||
// Update after the line-break detection, because otherwise spaceCorrection above
|
||||
// will refer to the wrapped word and not the last-word-before-the-line-break.
|
||||
spaceCorrection = feedback2.m_EndsWithSpace ? spaceWidth : 0.f;
|
||||
|
||||
// Let lineSize.cy be the maximum m_Height we encounter.
|
||||
lineSize.Height = std::max(lineSize.Height, feedback2.m_Size.Height);
|
||||
@ -209,6 +223,8 @@ void CGUIText::ComputeLineSize(
|
||||
|
||||
lineSize.Width += feedback2.m_Size.Width;
|
||||
}
|
||||
// Remove the space if necessary.
|
||||
lineSize.Width -= spaceCorrection;
|
||||
}
|
||||
|
||||
bool CGUIText::ProcessLine(
|
||||
@ -364,14 +380,16 @@ bool CGUIText::AssembleCalls(
|
||||
tc.m_pSpriteCall->m_Area += tc.m_Pos - CSize2D(0, tc.m_pSpriteCall->m_Area.GetHeight());
|
||||
}
|
||||
|
||||
// Append X value.
|
||||
x += feedback2.m_Size.Width;
|
||||
|
||||
// The first word overrides the width limit, what we
|
||||
// do, in those cases, are just drawing that word even
|
||||
// though it'll extend the object.
|
||||
if (width != 0) // only if word-wrapping is applicable
|
||||
{
|
||||
// Check if we need to wrap, using the same algorithm as ComputeLineSize
|
||||
// This means we must ignore the 'space before the next word' for the purposes of wrapping.
|
||||
CFontMetrics currentFont(font);
|
||||
float spaceWidth = currentFont.GetCharacterWidth(L' ');
|
||||
float spaceCorrection = feedback2.m_EndsWithSpace ? spaceWidth : 0.f;
|
||||
|
||||
if (feedback2.m_NewLine)
|
||||
{
|
||||
from = j + 1;
|
||||
@ -392,12 +410,17 @@ bool CGUIText::AssembleCalls(
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (x > widthRangeTo && j == tempFrom)
|
||||
else if (x - spaceCorrection > widthRangeTo && j == tempFrom)
|
||||
{
|
||||
// The first word overrides the width limit, what we do,
|
||||
// in those cases, is just drawing that word even
|
||||
// though it'll extend the object.
|
||||
// Ergo: do not break, since we want it to be added to m_TextCalls.
|
||||
from = j+1;
|
||||
// do not break, since we want it to be added to m_TextCalls
|
||||
// To avoid doing redundant computations, set up j to exit the loop right away.
|
||||
j = i + 1;
|
||||
}
|
||||
else if (x > widthRangeTo)
|
||||
else if (x - spaceCorrection > widthRangeTo)
|
||||
{
|
||||
from = j;
|
||||
break;
|
||||
|
@ -229,6 +229,17 @@ void CGUIString::GenerateTextCall(const CGUI& pGUI, SFeedback& Feedback, CStrInt
|
||||
|
||||
if (!TextCall.m_String.empty() && TextCall.m_String[0] == '\n')
|
||||
Feedback.m_NewLine = true;
|
||||
// Multiple empty spaces are treated as individual words (one per space),
|
||||
// and for coherence we'll do the 'ignore space after the word' thing
|
||||
// only if the word actually has some other text in it, so process this only if size >= 2
|
||||
else if (TextCall.m_String.size() >= 2)
|
||||
{
|
||||
const wchar_t lastChar = TextCall.m_String.back();
|
||||
// If the last word ends with a 'space', we'll ignore it when aligning, so mark it.
|
||||
Feedback.m_EndsWithSpace = lastChar == ' ' || lastChar == 0x3000;
|
||||
}
|
||||
else
|
||||
Feedback.m_EndsWithSpace = false;
|
||||
|
||||
// Add text-chunk
|
||||
Feedback.m_TextCalls.emplace_back(std::move(TextCall));
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2021 Wildfire Games.
|
||||
/* Copyright (C) 2022 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -156,6 +156,11 @@ public:
|
||||
* If the word inputted was a new line.
|
||||
*/
|
||||
bool m_NewLine;
|
||||
|
||||
/**
|
||||
* If the word inputted ends with a space that can be collapsed when aligning.
|
||||
*/
|
||||
bool m_EndsWithSpace;
|
||||
};
|
||||
|
||||
/**
|
||||
|
254
source/gui/tests/test_CGUIText.h
Normal file
254
source/gui/tests/test_CGUIText.h
Normal file
@ -0,0 +1,254 @@
|
||||
/* Copyright (C) 2022 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 "gui/CGUI.h"
|
||||
#include "gui/CGUIText.h"
|
||||
#include "gui/SettingTypes/CGUIString.h"
|
||||
#include "ps/CLogger.h"
|
||||
#include "ps/ConfigDB.h"
|
||||
#include "ps/ProfileViewer.h"
|
||||
#include "ps/VideoMode.h"
|
||||
#include "renderer/Renderer.h"
|
||||
|
||||
class TestCGUIText : public CxxTest::TestSuite
|
||||
{
|
||||
CProfileViewer* m_Viewer = nullptr;
|
||||
CRenderer* m_Renderer = nullptr;
|
||||
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));
|
||||
|
||||
CXeromyces::Startup();
|
||||
|
||||
// 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");
|
||||
g_VideoMode.InitNonSDL();
|
||||
g_VideoMode.CreateBackendDevice(false);
|
||||
m_Viewer = new CProfileViewer;
|
||||
m_Renderer = new CRenderer;
|
||||
}
|
||||
|
||||
void tearDown()
|
||||
{
|
||||
delete m_Renderer;
|
||||
delete m_Viewer;
|
||||
g_VideoMode.Shutdown();
|
||||
CConfigDB::Shutdown();
|
||||
CXeromyces::Terminate();
|
||||
g_VFS.reset();
|
||||
DeleteDirectory(DataDir()/"_testcache");
|
||||
}
|
||||
|
||||
void test_empty()
|
||||
{
|
||||
CGUI gui(g_ScriptContext);
|
||||
CGUIText empty;
|
||||
}
|
||||
|
||||
void test_wrapping()
|
||||
{
|
||||
CGUI gui(g_ScriptContext);
|
||||
|
||||
static CStrW font = L"console";
|
||||
// Make sure this matches the value of the file.
|
||||
// TODO: load dynamically.
|
||||
static const float lineHeight = 12.f;
|
||||
static const float lineSpacing = 15.f;
|
||||
|
||||
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 * 4);
|
||||
|
||||
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 * 4);
|
||||
|
||||
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_overflow()
|
||||
{
|
||||
CGUI gui(g_ScriptContext);
|
||||
|
||||
static CStrW font = L"console";
|
||||
// Make sure this matches the value of the file.
|
||||
// TODO: load dynamically.
|
||||
static const float lineHeight = 12.f;
|
||||
static const float lineSpacing = 15.f;
|
||||
|
||||
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()
|
||||
{
|
||||
TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "mod" / "", VFS_MOUNT_MUST_EXIST));
|
||||
|
||||
CGUI gui(g_ScriptContext);
|
||||
|
||||
static CStrW font = L"sans-bold-13";
|
||||
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, L"sans-bold-13", 53, 8.f, EAlign::LEFT, nullptr);
|
||||
|
||||
TS_ASSERT_EQUALS(text.GetTextCalls().size(), 2);
|
||||
TS_ASSERT_EQUALS(text.GetSize().Height, 14 + 9 + 8 * 2);
|
||||
}
|
||||
|
||||
void test_multiple_blank_spaces()
|
||||
{
|
||||
CGUI gui(g_ScriptContext);
|
||||
|
||||
static CStrW font = L"console";
|
||||
// Make sure this matches the value of the file.
|
||||
// TODO: load dynamically.
|
||||
static const float lineHeight = 12.f;
|
||||
static const float lineSpacing = 15.f;
|
||||
|
||||
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);
|
||||
TS_ASSERT_EQUALS(text.GetSize().Width, 89.f);
|
||||
renderedWidth = text.GetSize().Width;
|
||||
|
||||
align = EAlign::RIGHT;
|
||||
text = CGUIText(gui, string, font, width, padding, align, nullptr);
|
||||
TS_ASSERT_EQUALS(text.GetSize().Width, renderedWidth);
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user