Index: ps/trunk/source/gui/CGUIText.cpp
===================================================================
--- ps/trunk/source/gui/CGUIText.cpp (revision 26521)
+++ ps/trunk/source/gui/CGUIText.cpp (revision 26522)
@@ -1,478 +1,465 @@
/* 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 .
*/
#include "precompiled.h"
#include "CGUIText.h"
#include "graphics/Canvas2D.h"
#include "graphics/FontMetrics.h"
#include "graphics/TextRenderer.h"
#include "gui/CGUI.h"
#include "gui/ObjectBases/IGUIObject.h"
#include "gui/SettingTypes/CGUIString.h"
#include "ps/CStrInternStatic.h"
#include "ps/VideoMode.h"
#include "renderer/backend/gl/DeviceCommandContext.h"
#include "renderer/Renderer.h"
#include
extern int g_xres, g_yres;
// TODO Gee: CRect => CPoint ?
void SGenerateTextImage::SetupSpriteCall(
const bool left, CGUIText::SSpriteCall& spriteCall, const float width, const float y,
const CSize2D& size, const CStr& textureName, const float bufferZone)
{
// TODO Gee: Temp hardcoded values
spriteCall.m_Area.top = y + bufferZone;
spriteCall.m_Area.bottom = y + bufferZone + size.Height;
if (left)
{
spriteCall.m_Area.left = bufferZone;
spriteCall.m_Area.right = size.Width + bufferZone;
}
else
{
spriteCall.m_Area.left = width - bufferZone - size.Width;
spriteCall.m_Area.right = width - bufferZone;
}
spriteCall.m_Sprite = textureName;
m_YFrom = spriteCall.m_Area.top - bufferZone;
m_YTo = spriteCall.m_Area.bottom + bufferZone;
m_Indentation = size.Width + bufferZone * 2;
}
CGUIText::CGUIText(const CGUI& pGUI, const CGUIString& string, const CStrW& fontW, const float width, const float bufferZone, const EAlign align, const IGUIObject* pObject)
{
if (string.m_Words.empty())
return;
CStrIntern font(fontW.ToUTF8());
- float x = bufferZone, y = bufferZone; // drawing pointer
+ float y = bufferZone; // drawing pointer
int from = 0;
bool firstLine = true; // Necessary because text in the first line is shorter
// (it doesn't count the line spacing)
// Images on the left or the right side.
SGenerateTextImages images;
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
for (int i = 0; i < static_cast(string.m_Words.size()) - 1; ++i)
{
// Pre-process each line one time, so we know which floating images
// will be added for that line.
// Generated stuff is stored in feedback.
CGUIString::SFeedback feedback;
// Preliminary line height, used for word-wrapping with floating images.
float prelimLineHeight = 0.f;
// Width and height of all text calls generated.
string.GenerateTextCall(pGUI, feedback, font, string.m_Words[i], string.m_Words[i+1], firstLine);
SetupSpriteCalls(pGUI, feedback.m_Images, y, width, bufferZone, i, posLastImage, images);
posLastImage = std::max(posLastImage, i);
- x += feedback.m_Size.Width;
prelimLineHeight = std::max(prelimLineHeight, feedback.m_Size.Height);
// If width is 0, then there's no word-wrapping, disable NewLine.
- if (((width != 0 && (x > width - bufferZone || feedback.m_NewLine)) || i == static_cast(string.m_Words.size()) - 2) &&
- ProcessLine(pGUI, string, font, pObject, images, align, prelimLineHeight, width, bufferZone, firstLine, x, y, i, from))
+ if (((width != 0 && (feedback.m_Size.Width + 2 * bufferZone > width || feedback.m_NewLine)) || i == static_cast(string.m_Words.size()) - 2) &&
+ ProcessLine(pGUI, string, font, pObject, images, align, prelimLineHeight, width, bufferZone, firstLine, y, i, from))
return;
}
}
// Loop through our images queues, to see if images have been added.
void CGUIText::SetupSpriteCalls(
const CGUI& pGUI,
const std::array, 2>& feedbackImages,
const float y,
const float width,
const float bufferZone,
const int i,
const int posLastImage,
SGenerateTextImages& images)
{
// Check if this has already been processed.
// Also, floating images are only applicable if Word-Wrapping is on
if (width == 0 || i <= posLastImage)
return;
// Loop left/right
for (int j = 0; j < 2; ++j)
for (const CStr& imgname : feedbackImages[j])
{
SSpriteCall spriteCall;
SGenerateTextImage image;
// Y is if no other floating images is above, y. Else it is placed
// after the last image, like a stack downwards.
float _y;
if (!images[j].empty())
_y = std::max(y, images[j].back().m_YTo);
else
_y = y;
const SGUIIcon& icon = pGUI.GetIcon(imgname);
image.SetupSpriteCall(j == CGUIString::SFeedback::Left, spriteCall, width, _y, icon.m_Size, icon.m_SpriteName, bufferZone);
// Check if image is the lowest thing.
m_Size.Height = std::max(m_Size.Height, image.m_YTo);
images[j].emplace_back(image);
m_SpriteCalls.emplace_back(std::move(spriteCall));
}
}
// Now we'll do another loop to figure out the height and width of
// the line (the height of the largest character and the width is
// the sum of all of the individual widths). This
// couldn't be determined in the first loop (main loop)
// because it didn't regard images, so we don't know
// if all characters processed, will actually be involved
// in that line.
void CGUIText::ComputeLineSize(
const CGUI& pGUI,
const CGUIString& string,
const CStrIntern& font,
const bool firstLine,
const float width,
+ const float widthRangeFrom,
const float widthRangeTo,
const int i,
const int tempFrom,
- float& x,
CSize2D& lineSize) const
{
+ float x = widthRangeFrom;
for (int j = tempFrom; j <= i; ++j)
{
// We don't want to use feedback now, so we'll have to use another one.
CGUIString::SFeedback feedback2;
// Don't attach object, it'll suppress the errors
// we want them to be reported in the final GenerateTextCall()
// so that we don't get duplicates.
string.GenerateTextCall(pGUI, feedback2, font, string.m_Words[j], string.m_Words[j+1], firstLine);
// 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" ");
break;
}
// Let lineSize.cy be the maximum m_Height we encounter.
lineSize.Height = std::max(lineSize.Height, feedback2.m_Size.Height);
// If the current word is an explicit new line ("\n"),
// break now before adding the width of this character.
// ("\n" doesn't have a glyph, thus is given the same width as
// the "missing glyph" character by CFont::GetCharacterWidth().)
if (width != 0 && feedback2.m_NewLine)
break;
lineSize.Width += feedback2.m_Size.Width;
}
}
bool CGUIText::ProcessLine(
const CGUI& pGUI,
const CGUIString& string,
const CStrIntern& font,
const IGUIObject* pObject,
const SGenerateTextImages& images,
const EAlign align,
const float prelimLineHeight,
const float width,
const float bufferZone,
bool& firstLine,
- float& x,
float& y,
int& i,
int& from)
{
// Change 'from' to 'i', but first keep a copy of its value.
int tempFrom = from;
from = i;
float widthRangeFrom = bufferZone;
float widthRangeTo = width - bufferZone;
ComputeLineRange(images, y, width, prelimLineHeight, widthRangeFrom, widthRangeTo);
- // Reset X for the next loop
- x = widthRangeFrom;
-
CSize2D lineSize;
- ComputeLineSize(pGUI, string, font, firstLine, width, widthRangeTo, i, tempFrom, x, lineSize);
-
- // Reset x once more
- x = widthRangeFrom;
+ ComputeLineSize(pGUI, string, font, firstLine, width, widthRangeFrom, widthRangeTo, i, tempFrom, lineSize);
// Move down, because font drawing starts from the baseline
y += lineSize.Height;
- const float dx = GetLineOffset(align, widthRangeFrom, widthRangeTo, lineSize);
-
// Do the real processing now
- const bool done = AssembleCalls(pGUI, string, font, pObject, firstLine, width, widthRangeTo, dx, y, tempFrom, i, x, from);
-
- // Reset X
- x = bufferZone;
+ const bool done = AssembleCalls(pGUI, string, font, pObject, firstLine, width, widthRangeTo, GetLineOffset(align, widthRangeFrom, widthRangeTo, lineSize), y, tempFrom, i, from);
// Update dimensions
m_Size.Width = std::max(m_Size.Width, lineSize.Width + bufferZone * 2);
m_Size.Height = std::max(m_Size.Height, y + bufferZone);
firstLine = false;
// Now if we entered as from = i, then we want
// i being one minus that, so that it will become
// the same i in the next loop. The difference is that
// we're on a new line now.
i = from - 1;
return done;
}
// Decide width of the line. We need to iterate our floating images.
// this won't be exact because we're assuming the lineSize.cy
// will be as our preliminary calculation said. But that may change,
// although we'd have to add a couple of more loops to try straightening
// this problem out, and it is very unlikely to happen noticeably if one
// structures his text in a stylistically pure fashion. Even if not, it
// is still quite unlikely it will happen.
// Loop through left and right side, from and to.
void CGUIText::ComputeLineRange(
const SGenerateTextImages& images,
const float y,
const float width,
const float prelimLineHeight,
float& widthRangeFrom,
float& widthRangeTo) const
{
// Floating images are only applicable if word-wrapping is enabled.
if (width == 0)
return;
for (int j = 0; j < 2; ++j)
for (const SGenerateTextImage& img : images[j])
{
// We're working with two intervals here, the image's and the line height's.
// let's find the union of these two.
float unionFrom, unionTo;
unionFrom = std::max(y, img.m_YFrom);
unionTo = std::min(y + prelimLineHeight, img.m_YTo);
// The union is not empty
if (unionTo > unionFrom)
{
if (j == 0)
widthRangeFrom = std::max(widthRangeFrom, img.m_Indentation);
else
widthRangeTo = std::min(widthRangeTo, width - img.m_Indentation);
}
}
}
// compute offset based on what kind of alignment
float CGUIText::GetLineOffset(
const EAlign align,
const float widthRangeFrom,
const float widthRangeTo,
const CSize2D& lineSize) const
{
switch (align)
{
case EAlign::LEFT:
- // don't add an offset
- return 0.f;
+ return widthRangeFrom;
case EAlign::CENTER:
- return ((widthRangeTo - widthRangeFrom) - lineSize.Width) / 2;
+ return (widthRangeTo + widthRangeFrom - lineSize.Width) / 2;
case EAlign::RIGHT:
return widthRangeTo - lineSize.Width;
default:
debug_warn(L"Broken EAlign in CGUIText()");
return 0.f;
}
}
bool CGUIText::AssembleCalls(
const CGUI& pGUI,
const CGUIString& string,
const CStrIntern& font,
const IGUIObject* pObject,
const bool firstLine,
const float width,
const float widthRangeTo,
const float dx,
const float y,
const int tempFrom,
const int i,
- float& x,
int& from)
{
bool done = false;
+ float x = 0.f;
for (int j = tempFrom; j <= i; ++j)
{
// We don't want to use feedback now, so we'll have to use another one.
CGUIString::SFeedback feedback2;
// Defaults
string.GenerateTextCall(pGUI, feedback2, font, string.m_Words[j], string.m_Words[j+1], firstLine, pObject);
// Iterate all and set X/Y values
// Since X values are not set, we need to make an internal
// iteration with an increment that will append the internal
// x, that is what xPointer is for.
float xPointer = 0.f;
for (STextCall& tc : feedback2.m_TextCalls)
{
tc.m_Pos = CVector2D(dx + x + xPointer, y);
xPointer += tc.m_Size.Width;
if (tc.m_pSpriteCall)
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
{
if (feedback2.m_NewLine)
{
from = j + 1;
// Sprite call can exist within only a newline segment,
// therefore we need this.
if (!feedback2.m_SpriteCalls.empty())
{
auto newEnd = std::remove_if(feedback2.m_TextCalls.begin(), feedback2.m_TextCalls.end(), [](const STextCall& call) { return !call.m_pSpriteCall; });
m_TextCalls.insert(
m_TextCalls.end(),
std::make_move_iterator(feedback2.m_TextCalls.begin()),
std::make_move_iterator(newEnd));
m_SpriteCalls.insert(
m_SpriteCalls.end(),
std::make_move_iterator(feedback2.m_SpriteCalls.begin()),
std::make_move_iterator(feedback2.m_SpriteCalls.end()));
}
break;
}
else if (x > widthRangeTo && j == tempFrom)
{
from = j+1;
// do not break, since we want it to be added to m_TextCalls
}
else if (x > widthRangeTo)
{
from = j;
break;
}
}
// Add the whole feedback2.m_TextCalls to our m_TextCalls.
m_TextCalls.insert(
m_TextCalls.end(),
std::make_move_iterator(feedback2.m_TextCalls.begin()),
std::make_move_iterator(feedback2.m_TextCalls.end()));
m_SpriteCalls.insert(
m_SpriteCalls.end(),
std::make_move_iterator(feedback2.m_SpriteCalls.begin()),
std::make_move_iterator(feedback2.m_SpriteCalls.end()));
if (j == static_cast(string.m_Words.size()) - 2)
done = true;
}
return done;
}
void CGUIText::Draw(CGUI& pGUI, CCanvas2D& canvas, const CGUIColor& DefaultColor, const CVector2D& pos, CRect clipping) const
{
Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext =
g_Renderer.GetDeviceCommandContext();
bool isClipped = clipping != CRect();
if (isClipped)
{
// Make clipping rect as small as possible to prevent rounding errors
clipping.top = std::ceil(clipping.top);
clipping.bottom = std::floor(clipping.bottom);
clipping.left = std::ceil(clipping.left);
clipping.right = std::floor(clipping.right);
const float scale = g_VideoMode.GetScale();
Renderer::Backend::GL::CDeviceCommandContext::Rect scissorRect;
scissorRect.x = std::ceil(clipping.left * scale);
scissorRect.y = std::ceil(g_yres - clipping.bottom * scale);
scissorRect.width = std::floor(clipping.GetWidth() * scale);
scissorRect.height = std::floor(clipping.GetHeight() * scale);
// TODO: move scissors to CCanvas2D.
deviceCommandContext->SetScissors(1, &scissorRect);
}
CTextRenderer textRenderer;
textRenderer.SetClippingRect(clipping);
textRenderer.Translate(0.0f, 0.0f);
for (const STextCall& tc : m_TextCalls)
{
// If this is just a placeholder for a sprite call, continue
if (tc.m_pSpriteCall)
continue;
textRenderer.SetCurrentColor(tc.m_UseCustomColor ? tc.m_Color : DefaultColor);
textRenderer.SetCurrentFont(tc.m_Font);
textRenderer.Put(floorf(pos.X + tc.m_Pos.X), floorf(pos.Y + tc.m_Pos.Y), &tc.m_String);
}
canvas.DrawText(textRenderer);
for (const SSpriteCall& sc : m_SpriteCalls)
pGUI.DrawSprite(sc.m_Sprite, canvas, sc.m_Area + pos);
if (isClipped)
deviceCommandContext->SetScissors(0, nullptr);
}
Index: ps/trunk/source/gui/CGUIText.h
===================================================================
--- ps/trunk/source/gui/CGUIText.h (revision 26521)
+++ ps/trunk/source/gui/CGUIText.h (revision 26522)
@@ -1,282 +1,280 @@
/* 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 .
*/
#ifndef INCLUDED_GUITEXT
#define INCLUDED_GUITEXT
#include "gui/CGUISprite.h"
#include "gui/SettingTypes/CGUIColor.h"
#include "gui/SettingTypes/EAlign.h"
#include "maths/Rect.h"
#include "maths/Size2D.h"
#include "maths/Vector2D.h"
#include "ps/CStrIntern.h"
#include
#include
#include
class CCanvas2D;
class CGUI;
class CGUIString;
class IGUIObject;
struct SGenerateTextImage;
using SGenerateTextImages = std::array, 2>;
/**
* An CGUIText object is a parsed string, divided into
* text-rendering components. Each component, being a
* call to the Renderer. For instance, if you by tags
* change the color, then the GUI will have to make
* individual calls saying it want that color on the
* text.
*
* For instance:
* "Hello [b]there[/b] bunny!"
*
* That without word-wrapping would mean 3 components.
* i.e. 3 calls to CRenderer. One drawing "Hello",
* one drawing "there" in bold, and one drawing "bunny!".
*/
class CGUIText
{
public:
/**
* A sprite call to the CRenderer
*/
struct SSpriteCall
{
// The CGUISpriteInstance makes this uncopyable to avoid invalidating its draw cache
NONCOPYABLE(SSpriteCall);
MOVABLE(SSpriteCall);
SSpriteCall() {}
/**
* Size and position of sprite
*/
CRect m_Area;
/**
* Sprite from global GUI sprite database.
*/
CGUISpriteInstance m_Sprite;
};
/**
* A text call to the CRenderer
*/
struct STextCall
{
NONCOPYABLE(STextCall);
MOVABLE(STextCall);
STextCall() :
m_UseCustomColor(false),
m_Bold(false), m_Italic(false), m_Underlined(false),
m_pSpriteCall(nullptr) {}
/**
* Position
*/
CVector2D m_Pos;
/**
* Size
*/
CSize2D m_Size;
/**
* The string that is suppose to be rendered.
*/
CStrW m_String;
/**
* Use custom color? If true then m_Color is used,
* else the color inputted will be used.
*/
bool m_UseCustomColor;
/**
* Color setup
*/
CGUIColor m_Color;
/**
* Font name
*/
CStrIntern m_Font;
/**
* Settings
*/
bool m_Bold, m_Italic, m_Underlined;
/**
* Tooltip text
*/
CStrW m_Tooltip;
/**
* *IF* an icon, then this is not nullptr.
*/
std::list::pointer m_pSpriteCall;
};
// The SSpriteCall CGUISpriteInstance makes this uncopyable to avoid invalidating its draw cache.
// Also take advantage of exchanging the containers directly with move semantics.
NONCOPYABLE(CGUIText);
MOVABLE(CGUIText);
/**
* Generates empty text.
*/
CGUIText() = default;
/**
* Generate a CGUIText object from the inputted string.
* The function will break down the string and its
* tags to calculate exactly which rendering queries
* will be sent to the Renderer. Also, horizontal alignment
* is taken into acount in this method but NOT vertical alignment.
*
* @param string Text to generate CGUIText object from.
* @param font Default font, notice both Default color and default font
* can be changed by tags.
* @param width Width, 0 if no word-wrapping.
* @param bufferZone Space between text and edge, and space between text and images.
* @param align Horizontal alignment (left / center / right).
* @param pObject Optional parameter for error output. Used *only* if error parsing fails,
* and we need to be able to output which object the error occurred in to aid the user.
*/
CGUIText(const CGUI& pGUI, const CGUIString& string, const CStrW& fontW, const float width, const float bufferZone, const EAlign align, const IGUIObject* pObject);
/**
* Draw this CGUIText object
*/
void Draw(CGUI& pGUI, CCanvas2D& canvas, const CGUIColor& DefaultColor, const CVector2D& pos, CRect clipping) const;
const CSize2D& GetSize() const { return m_Size; }
const std::list& GetSpriteCalls() const { return m_SpriteCalls; }
const std::vector& GetTextCalls() const { return m_TextCalls; }
// Helper functions of the constructor
bool ProcessLine(
const CGUI& pGUI,
const CGUIString& string,
const CStrIntern& font,
const IGUIObject* pObject,
const SGenerateTextImages& images,
const EAlign align,
const float prelimLineHeight,
const float width,
const float bufferZone,
bool& firstLine,
- float& x,
float& y,
int& i,
int& from);
void SetupSpriteCalls(
const CGUI& pGUI,
const std::array, 2>& feedbackImages,
const float y,
const float width,
const float bufferZone,
const int i,
const int posLastImage,
SGenerateTextImages& images);
float GetLineOffset(
const EAlign align,
const float widthRangeFrom,
const float widthRangeTo,
const CSize2D& lineSize) const;
void ComputeLineRange(
const SGenerateTextImages& images,
const float y,
const float width,
const float prelimLineHeight,
float& widthRangeFrom,
float& widthRangeTo) const;
void ComputeLineSize(
const CGUI& pGUI,
const CGUIString& string,
const CStrIntern& font,
const bool firstLine,
const float width,
+ const float widthRangeFrom,
const float widthRangeTo,
const int i,
const int tempFrom,
- float& x,
CSize2D& lineSize) const;
bool AssembleCalls(
const CGUI& pGUI,
const CGUIString& string,
const CStrIntern& font,
const IGUIObject* pObject,
const bool firstLine,
const float width,
const float widthRangeTo,
const float dx,
const float y,
const int tempFrom,
const int i,
- float& x,
int& from);
/**
* List of TextCalls, for instance "Hello", "there!"
*/
std::vector m_TextCalls;
/**
* List of sprites, or "icons" that should be rendered
* along with the text.
*/
std::list m_SpriteCalls; // list for consistent mem addresses
// so that we can point to elements.
/**
* Width and height of the whole output, used when setting up
* scrollbars and such.
*/
CSize2D m_Size;
};
struct SGenerateTextImage
{
// The image's starting location in Y
float m_YFrom;
// The image's end location in Y
float m_YTo;
// The image width in other words
float m_Indentation;
void SetupSpriteCall(
const bool left, CGUIText::SSpriteCall& spriteCall, const float width, const float y,
const CSize2D& size, const CStr& textureName, const float bufferZone);
};
#endif // INCLUDED_GUITEXT