mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
Make CGUISpriteInstance non-copyable to further harden Philips protection from8f4f8e240fagainst unintentional copies of its DrawCall cache such as inc19f3608a5. Remove its copy constructor from849f50a500, make it movable, and until it becomes otherwise necessary, force move assignment when sprites are assigned. Improves the fixes of the compiler warnings about deprecated implicit copy constructors in8a32b0b3d4by avoiding the copies instead of copying explicitly. Add ToJSVal, FromJSVal for CGUISprinteInstance to make JSI_IGUIObject::getProperty and setProperty more consistent. Rename Sprite operator= to SetName to reduce ambiguity. Pass CRect by reference in CGUISpriteInstance::Draw. Differential Revision: https://code.wildfiregames.com/D2133 Comments By: wraitii This was SVN commit r22570.
472 lines
13 KiB
C++
472 lines
13 KiB
C++
/* Copyright (C) 2019 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 "precompiled.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include "gui/GUI.h"
|
|
#include "lib/utf8.h"
|
|
#include "graphics/FontMetrics.h"
|
|
#include "ps/CLogger.h"
|
|
|
|
|
|
// List of word delimiter bounds
|
|
// The list contains ranges of word delimiters. The odd indexed chars are the start
|
|
// of a range, the even are the end of a range. The list must be sorted in INCREASING ORDER
|
|
static const int NUM_WORD_DELIMITERS = 4*2;
|
|
static const u16 WordDelimiters[NUM_WORD_DELIMITERS] = {
|
|
' ' , ' ', // spaces
|
|
'-' , '-', // hyphens
|
|
0x3000, 0x31FF, // ideographic symbols
|
|
0x3400, 0x9FFF
|
|
// TODO add unicode blocks of other languages that don't use spaces
|
|
};
|
|
|
|
void CGUIString::SFeedback::Reset()
|
|
{
|
|
m_Images[Left].clear();
|
|
m_Images[Right].clear();
|
|
m_TextCalls.clear();
|
|
m_SpriteCalls.clear();
|
|
m_Size = CSize();
|
|
m_NewLine = false;
|
|
}
|
|
|
|
void CGUIString::GenerateTextCall(const CGUI* pGUI, SFeedback& Feedback, CStrIntern DefaultFont, const int& from, const int& to, const bool FirstLine, const IGUIObject* pObject) const
|
|
{
|
|
// Reset width and height, because they will be determined with incrementation
|
|
// or comparisons.
|
|
Feedback.Reset();
|
|
|
|
// Check out which text chunk this is within.
|
|
for (const TextChunk& textChunk : m_TextChunks)
|
|
{
|
|
// Get the area that is overlapped by both the TextChunk and
|
|
// by the from/to inputted.
|
|
int _from = std::max(from, textChunk.m_From);
|
|
int _to = std::min(to, textChunk.m_To);
|
|
|
|
// If from is larger than to, then they are not overlapping
|
|
if (_to == _from && textChunk.m_From == textChunk.m_To)
|
|
{
|
|
// These should never be able to have more than one tag.
|
|
ENSURE(textChunk.m_Tags.size() == 1);
|
|
|
|
// Icons and images are placed on exactly one position
|
|
// in the words-list, and they can be counted twice if placed
|
|
// on an edge. But there is always only one logical preference
|
|
// that we want. This check filters the unwanted.
|
|
|
|
// it's in the end of one word, and the icon
|
|
// should really belong to the beginning of the next one
|
|
if (_to == to && to >= 1 && to < (int)m_RawString.length())
|
|
{
|
|
if (m_RawString[to-1] == ' ' ||
|
|
m_RawString[to-1] == '-' ||
|
|
m_RawString[to-1] == '\n')
|
|
continue;
|
|
}
|
|
// This std::string is just a break
|
|
if (_from == from && from >= 1)
|
|
{
|
|
if (m_RawString[from] == '\n' &&
|
|
m_RawString[from-1] != '\n' &&
|
|
m_RawString[from-1] != ' ' &&
|
|
m_RawString[from-1] != '-')
|
|
continue;
|
|
}
|
|
|
|
const TextChunk::Tag& tag = textChunk.m_Tags[0];
|
|
ENSURE(tag.m_TagType == TextChunk::Tag::TAG_IMGLEFT ||
|
|
tag.m_TagType == TextChunk::Tag::TAG_IMGRIGHT ||
|
|
tag.m_TagType == TextChunk::Tag::TAG_ICON);
|
|
|
|
const std::string& path = utf8_from_wstring(tag.m_TagValue);
|
|
if (!pGUI->IconExists(path))
|
|
{
|
|
if (pObject)
|
|
LOGERROR("Trying to use an icon, imgleft or imgright-tag with an undefined icon (\"%s\").", path.c_str());
|
|
continue;
|
|
}
|
|
|
|
switch (tag.m_TagType)
|
|
{
|
|
case TextChunk::Tag::TAG_IMGLEFT:
|
|
Feedback.m_Images[SFeedback::Left].push_back(path);
|
|
break;
|
|
case TextChunk::Tag::TAG_IMGRIGHT:
|
|
Feedback.m_Images[SFeedback::Right].push_back(path);
|
|
break;
|
|
case TextChunk::Tag::TAG_ICON:
|
|
{
|
|
// We'll need to setup a text-call that will point
|
|
// to the icon, this is to be able to iterate
|
|
// through the text-calls without having to
|
|
// complex the structure virtually for nothing more.
|
|
SGUIText::STextCall TextCall;
|
|
|
|
// Also add it to the sprites being rendered.
|
|
SGUIText::SSpriteCall SpriteCall;
|
|
|
|
// Get Icon from icon database in pGUI
|
|
SGUIIcon icon = pGUI->GetIcon(path);
|
|
|
|
CSize size = icon.m_Size;
|
|
|
|
// append width, and make maximum height the height.
|
|
Feedback.m_Size.cx += size.cx;
|
|
Feedback.m_Size.cy = std::max(Feedback.m_Size.cy, size.cy);
|
|
|
|
// These are also needed later
|
|
TextCall.m_Size = size;
|
|
SpriteCall.m_Area = size;
|
|
|
|
// Handle additional attributes
|
|
for (const TextChunk::Tag::TagAttribute& tagAttrib : tag.m_TagAttributes)
|
|
{
|
|
if (tagAttrib.attrib == L"displace" && !tagAttrib.value.empty())
|
|
{
|
|
// Displace the sprite
|
|
CSize displacement;
|
|
// Parse the value
|
|
if (!GUI<CSize>::ParseString(tagAttrib.value, displacement))
|
|
LOGERROR("Error parsing 'displace' value for tag [ICON]");
|
|
else
|
|
SpriteCall.m_Area += displacement;
|
|
}
|
|
else if (tagAttrib.attrib == L"tooltip")
|
|
SpriteCall.m_Tooltip = tagAttrib.value;
|
|
else if (tagAttrib.attrib == L"tooltip_style")
|
|
SpriteCall.m_TooltipStyle = tagAttrib.value;
|
|
}
|
|
|
|
SpriteCall.m_Sprite = icon.m_SpriteName;
|
|
SpriteCall.m_CellID = icon.m_CellID;
|
|
|
|
// Add sprite call
|
|
Feedback.m_SpriteCalls.push_back(std::move(SpriteCall));
|
|
|
|
// Finalize text call
|
|
TextCall.m_pSpriteCall = &Feedback.m_SpriteCalls.back();
|
|
|
|
// Add text call
|
|
Feedback.m_TextCalls.push_back(TextCall);
|
|
|
|
break;
|
|
}
|
|
NODEFAULT;
|
|
}
|
|
}
|
|
else if (_to > _from && !Feedback.m_NewLine)
|
|
{
|
|
SGUIText::STextCall TextCall;
|
|
|
|
// Set defaults
|
|
TextCall.m_Font = DefaultFont;
|
|
TextCall.m_UseCustomColor = false;
|
|
|
|
TextCall.m_String = m_RawString.substr(_from, _to-_from);
|
|
|
|
// Go through tags and apply changes.
|
|
for (const TextChunk::Tag& tag : textChunk.m_Tags)
|
|
{
|
|
switch (tag.m_TagType)
|
|
{
|
|
case TextChunk::Tag::TAG_COLOR:
|
|
TextCall.m_UseCustomColor = true;
|
|
|
|
if (!GUI<CGUIColor>::ParseString(tag.m_TagValue, TextCall.m_Color) && pObject)
|
|
LOGERROR("Error parsing the value of a [color]-tag in GUI text when reading object \"%s\".", pObject->GetPresentableName().c_str());
|
|
break;
|
|
case TextChunk::Tag::TAG_FONT:
|
|
// TODO Gee: (2004-08-15) Check if Font exists?
|
|
TextCall.m_Font = CStrIntern(utf8_from_wstring(tag.m_TagValue));
|
|
break;
|
|
default:
|
|
LOGERROR("Encountered unexpected tag applied to text");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Calculate the size of the font
|
|
CSize size;
|
|
int cx, cy;
|
|
CFontMetrics font (TextCall.m_Font);
|
|
font.CalculateStringSize(TextCall.m_String.c_str(), cx, cy);
|
|
// For anything other than the first line, the line spacing
|
|
// needs to be considered rather than just the height of the text
|
|
if (!FirstLine)
|
|
cy = font.GetLineSpacing();
|
|
|
|
size.cx = (float)cx;
|
|
size.cy = (float)cy;
|
|
|
|
// Append width, and make maximum height the height.
|
|
Feedback.m_Size.cx += size.cx;
|
|
Feedback.m_Size.cy = std::max(Feedback.m_Size.cy, size.cy);
|
|
|
|
// These are also needed later
|
|
TextCall.m_Size = size;
|
|
|
|
if (!TextCall.m_String.empty() && TextCall.m_String[0] == '\n')
|
|
Feedback.m_NewLine = true;
|
|
|
|
// Add text-chunk
|
|
Feedback.m_TextCalls.push_back(TextCall);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool CGUIString::TextChunk::Tag::SetTagType(const CStrW& tagtype)
|
|
{
|
|
TagType t = GetTagType(tagtype);
|
|
if (t == TAG_INVALID)
|
|
return false;
|
|
|
|
m_TagType = t;
|
|
return true;
|
|
}
|
|
|
|
CGUIString::TextChunk::Tag::TagType CGUIString::TextChunk::Tag::GetTagType(const CStrW& tagtype) const
|
|
{
|
|
if (tagtype == L"color")
|
|
return TAG_COLOR;
|
|
if (tagtype == L"font")
|
|
return TAG_FONT;
|
|
if (tagtype == L"icon")
|
|
return TAG_ICON;
|
|
if (tagtype == L"imgleft")
|
|
return TAG_IMGLEFT;
|
|
if (tagtype == L"imgright")
|
|
return TAG_IMGRIGHT;
|
|
|
|
return TAG_INVALID;
|
|
}
|
|
|
|
void CGUIString::SetValue(const CStrW& str)
|
|
{
|
|
m_OriginalString = str;
|
|
|
|
m_TextChunks.clear();
|
|
m_Words.clear();
|
|
m_RawString.clear();
|
|
|
|
// Current Text Chunk
|
|
CGUIString::TextChunk CurrentTextChunk;
|
|
CurrentTextChunk.m_From = 0;
|
|
|
|
int l = str.length();
|
|
int rawpos = 0;
|
|
CStrW tag;
|
|
std::vector<CStrW> tags;
|
|
bool closing = false;
|
|
for (int p = 0; p < l; ++p)
|
|
{
|
|
TextChunk::Tag tag_;
|
|
switch (str[p])
|
|
{
|
|
case L'[':
|
|
CurrentTextChunk.m_To = rawpos;
|
|
// Add the current chunks if it is not empty
|
|
if (CurrentTextChunk.m_From != rawpos)
|
|
m_TextChunks.push_back(CurrentTextChunk);
|
|
CurrentTextChunk.m_From = rawpos;
|
|
|
|
closing = false;
|
|
if (++p == l)
|
|
{
|
|
LOGERROR("Partial tag at end of string '%s'", utf8_from_wstring(str));
|
|
break;
|
|
}
|
|
if (str[p] == L'/')
|
|
{
|
|
closing = true;
|
|
if (tags.empty())
|
|
{
|
|
LOGERROR("Encountered closing tag without having any open tags. At %d in '%s'", p, utf8_from_wstring(str));
|
|
break;
|
|
}
|
|
if (++p == l)
|
|
{
|
|
LOGERROR("Partial closing tag at end of string '%s'", utf8_from_wstring(str));
|
|
break;
|
|
}
|
|
}
|
|
tag.clear();
|
|
// Parse tag
|
|
for (; p < l && str[p] != L']'; ++p)
|
|
{
|
|
CStrW name, param;
|
|
switch (str[p])
|
|
{
|
|
case L' ':
|
|
if (closing) // We still parse them to make error handling cleaner
|
|
LOGERROR("Closing tags do not support parameters (at pos %d '%s')", p, utf8_from_wstring(str));
|
|
|
|
// parse something="something else"
|
|
for (++p; p < l && str[p] != L'='; ++p)
|
|
name.push_back(str[p]);
|
|
|
|
if (p == l)
|
|
{
|
|
LOGERROR("Parameter without value at pos %d '%s'", p, utf8_from_wstring(str));
|
|
break;
|
|
}
|
|
FALLTHROUGH;
|
|
case L'=':
|
|
// parse a quoted parameter
|
|
if (closing) // We still parse them to make error handling cleaner
|
|
LOGERROR("Closing tags do not support parameters (at pos %d '%s')", p, utf8_from_wstring(str));
|
|
|
|
if (++p == l)
|
|
{
|
|
LOGERROR("Expected parameter, got end of string '%s'", utf8_from_wstring(str));
|
|
break;
|
|
}
|
|
if (str[p] != L'"')
|
|
{
|
|
LOGERROR("Unquoted parameters are not supported (at pos %d '%s')", p, utf8_from_wstring(str));
|
|
break;
|
|
}
|
|
for (++p; p < l && str[p] != L'"'; ++p)
|
|
{
|
|
switch (str[p])
|
|
{
|
|
case L'\\':
|
|
if (++p == l)
|
|
{
|
|
LOGERROR("Escape character at end of string '%s'", utf8_from_wstring(str));
|
|
break;
|
|
}
|
|
// NOTE: We do not support \n in tag parameters
|
|
FALLTHROUGH;
|
|
default:
|
|
param.push_back(str[p]);
|
|
}
|
|
}
|
|
|
|
if (!name.empty())
|
|
{
|
|
TextChunk::Tag::TagAttribute a = {name, param};
|
|
tag_.m_TagAttributes.push_back(a);
|
|
}
|
|
else
|
|
tag_.m_TagValue = param;
|
|
break;
|
|
default:
|
|
tag.push_back(str[p]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!tag_.SetTagType(tag))
|
|
{
|
|
LOGERROR("Invalid tag '%s' at %d in '%s'", utf8_from_wstring(tag), p, utf8_from_wstring(str));
|
|
break;
|
|
}
|
|
if (!closing)
|
|
{
|
|
if (tag_.m_TagType == TextChunk::Tag::TAG_IMGRIGHT
|
|
|| tag_.m_TagType == TextChunk::Tag::TAG_IMGLEFT
|
|
|| tag_.m_TagType == TextChunk::Tag::TAG_ICON)
|
|
{
|
|
TextChunk FreshTextChunk = { rawpos, rawpos };
|
|
FreshTextChunk.m_Tags.push_back(tag_);
|
|
m_TextChunks.push_back(FreshTextChunk);
|
|
}
|
|
else
|
|
{
|
|
tags.push_back(tag);
|
|
CurrentTextChunk.m_Tags.push_back(tag_);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (tag != tags.back())
|
|
{
|
|
LOGERROR("Closing tag '%s' does not match last opened tag '%s' at %d in '%s'", utf8_from_wstring(tag), utf8_from_wstring(tags.back()), p, utf8_from_wstring(str));
|
|
break;
|
|
}
|
|
|
|
tags.pop_back();
|
|
CurrentTextChunk.m_Tags.pop_back();
|
|
}
|
|
break;
|
|
case L'\\':
|
|
if (++p == l)
|
|
{
|
|
LOGERROR("Escape character at end of string '%s'", utf8_from_wstring(str));
|
|
break;
|
|
}
|
|
if (str[p] == L'n')
|
|
{
|
|
++rawpos;
|
|
m_RawString.push_back(L'\n');
|
|
break;
|
|
}
|
|
FALLTHROUGH;
|
|
default:
|
|
++rawpos;
|
|
m_RawString.push_back(str[p]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Add the chunk after the last tag
|
|
if (CurrentTextChunk.m_From != rawpos)
|
|
{
|
|
CurrentTextChunk.m_To = rawpos;
|
|
m_TextChunks.push_back(CurrentTextChunk);
|
|
}
|
|
|
|
|
|
// Add a delimiter at start and at end, it helps when
|
|
// processing later, because we don't have make exceptions for
|
|
// those cases.
|
|
m_Words.push_back(0);
|
|
|
|
// Add word boundaries in increasing order
|
|
for (u32 i = 0; i < m_RawString.length(); ++i)
|
|
{
|
|
wchar_t c = m_RawString[i];
|
|
if (c == '\n')
|
|
{
|
|
m_Words.push_back((int)i);
|
|
m_Words.push_back((int)i+1);
|
|
continue;
|
|
}
|
|
for (int n = 0; n < NUM_WORD_DELIMITERS; n += 2)
|
|
{
|
|
if (c <= WordDelimiters[n+1])
|
|
{
|
|
if (c >= WordDelimiters[n])
|
|
m_Words.push_back((int)i+1);
|
|
// assume the WordDelimiters list is stored in increasing order
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
m_Words.push_back((int)m_RawString.length());
|
|
|
|
// Remove duplicates (only if larger than 2)
|
|
if (m_Words.size() <= 2)
|
|
return;
|
|
|
|
m_Words.erase(std::unique(m_Words.begin(), m_Words.end()), m_Words.end());
|
|
}
|