Index: ps/trunk/source/gui/CChart.cpp =================================================================== --- ps/trunk/source/gui/CChart.cpp +++ ps/trunk/source/gui/CChart.cpp @@ -20,6 +20,7 @@ #include "CChart.h" #include "gui/CGUIColor.h" +#include "gui/CGUIString.h" #include "gui/GUIMatrix.h" #include "graphics/ShaderManager.h" #include "i18n/L10n.h" Index: ps/trunk/source/gui/CGUIList.h =================================================================== --- ps/trunk/source/gui/CGUIList.h +++ ps/trunk/source/gui/CGUIList.h @@ -15,11 +15,10 @@ * along with 0 A.D. If not, see . */ - #ifndef INCLUDED_CGUILIST #define INCLUDED_CGUILIST -#include "GUItext.h" +#include "gui/CGUIString.h" class CGUIList { Index: ps/trunk/source/gui/CGUIString.h =================================================================== --- ps/trunk/source/gui/CGUIString.h +++ ps/trunk/source/gui/CGUIString.h @@ -0,0 +1,229 @@ +/* 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 . + */ + +/* + * GUI text, handles text stuff + * + * --Overview-- + * Mainly contains struct SGUIText and friends. + * Actual text processing is made in CGUI::GenerateText() + * + * --More info-- + * Check GUI.h + * + */ + +#ifndef INCLUDED_CGUISTRING +#define INCLUDED_CGUISTRING + +#include + +#include "gui/CGUISprite.h" +#include "gui/GUItext.h" +#include "ps/CStrIntern.h" + +class CGUI; + +/** + * String class, substitute for CStr, but that parses + * the tags and builds up a list of all text that will + * be different when outputted. + * + * The difference between CGUIString and SGUIText is that + * CGUIString is a string-class that parses the tags + * when the value is set. The SGUIText is just a container + * which stores the positions and settings of all text-calls + * that will have to be made to the Renderer. + */ +class CGUIString +{ +public: + /** + * A chunk of text that represents one call to the renderer. + * In other words, all text in one chunk, will be drawn + * exactly with the same settings. + */ + struct TextChunk + { + /** + * A tag looks like this "Hello [b]there[/b] little" + */ + struct Tag + { + /** + * Tag Type + */ + enum TagType + { + TAG_B, + TAG_I, + TAG_FONT, + TAG_SIZE, + TAG_COLOR, + TAG_IMGLEFT, + TAG_IMGRIGHT, + TAG_ICON, + TAG_INVALID + }; + + struct TagAttribute + { + std::wstring attrib; + std::wstring value; + }; + + /** + * Set tag from string + * + * @param tagtype TagType by string, like 'img' for [img] + * @return True if m_TagType was set. + */ + bool SetTagType(const CStrW& tagtype); + TagType GetTagType(const CStrW& tagtype) const; + + + /** + * In [b="Hello"][/b] + * m_TagType is TAG_B + */ + TagType m_TagType; + + /** + * In [b="Hello"][/b] + * m_TagValue is 'Hello' + */ + std::wstring m_TagValue; + + /** + * Some tags need an additional attributes + */ + std::vector m_TagAttributes; + }; + + /** + * m_From and m_To is the range of the string + */ + int m_From, m_To; + + /** + * Tags that are present. [a][b] + */ + std::vector m_Tags; + }; + + /** + * All data generated in GenerateTextCall() + */ + struct SFeedback + { + // Avoid copying the vector and list containers. + NONCOPYABLE(SFeedback); + MOVABLE(SFeedback); + SFeedback() = default; + + // Constants + static const int Left = 0; + static const int Right = 1; + + /** + * Reset all member data. + */ + void Reset(); + + /** + * Image stacks, for left and right floating images. + */ + std::vector m_Images[2]; // left and right + + /** + * Text and Sprite Calls. + */ + std::vector m_TextCalls; + std::list m_SpriteCalls; // list for consistent mem addresses + // so that we can point to elements. + + /** + * Width and Height *feedback* + */ + CSize m_Size; + + /** + * If the word inputted was a new line. + */ + bool m_NewLine; + }; + + /** + * Set the value, the string will automatically + * be parsed when set. + */ + void SetValue(const CStrW& str); + + /** + * Get String, with tags + */ + const CStrW& GetOriginalString() const { return m_OriginalString; } + + /** + * Get String, stripped of tags + */ + const CStrW& GetRawString() const { return m_RawString; } + + /** + * Generate Text Call from specified range. The range + * must span only within ONE TextChunk though. Otherwise + * it can't be fit into a single Text Call + * + * Notice it won't make it complete, you will have to add + * X/Y values and such. + * + * @param pGUI Pointer to CGUI object making this call, for e.g. icon retrieval. + * @param Feedback contains all info that is generated. + * @param DefaultFont Default Font + * @param from From character n, + * @param to to character n. + * @param FirstLine Whether this is the first line of text, to calculate its height correctly + * @param pObject Only for Error outputting, optional! If NULL + * then no Errors will be reported! Useful when you need + * to make several GenerateTextCall in different phases, + * it avoids duplicates. + */ + void GenerateTextCall(const CGUI* pGUI, SFeedback& Feedback, CStrIntern DefaultFont, const int& from, const int& to, const bool FirstLine, const IGUIObject* pObject = NULL) const; + + /** + * Words + */ + std::vector m_Words; + +private: + /** + * TextChunks + */ + std::vector m_TextChunks; + + /** + * The full raw string. Stripped of tags. + */ + CStrW m_RawString; + + /** + * The original string value passed to SetValue. + */ + CStrW m_OriginalString; +}; + +#endif // INCLUDED_CGUISTRING Index: ps/trunk/source/gui/CGUIString.cpp =================================================================== --- ps/trunk/source/gui/CGUIString.cpp +++ ps/trunk/source/gui/CGUIString.cpp @@ -0,0 +1,472 @@ +/* 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 . + */ + +#include "precompiled.h" + +#include + +#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->HasIcon(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 + const SGUIIcon& icon = pGUI->GetIcon(path); + + const 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::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.emplace_back(std::move(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::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.emplace_back(std::move(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 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()); +} Index: ps/trunk/source/gui/GUItext.h =================================================================== --- ps/trunk/source/gui/GUItext.h +++ ps/trunk/source/gui/GUItext.h @@ -15,28 +15,15 @@ * along with 0 A.D. If not, see . */ -/* - * GUI text, handles text stuff - * - * --Overview-- - * Mainly contains struct SGUIText and friends. - * Actual text processing is made in CGUI::GenerateText() - * - * --More info-- - * Check GUI.h - * - */ - #ifndef INCLUDED_GUITEXT #define INCLUDED_GUITEXT #include -#include "CGUISprite.h" #include "gui/CGUIColor.h" +#include "gui/CGUISprite.h" #include "ps/CStrIntern.h" - -class CGUI; +#include "ps/Shapes.h" /** * An SGUIText object is a parsed string, divided into @@ -163,192 +150,4 @@ CSize m_Size; }; -/** - * String class, substitute for CStr, but that parses - * the tags and builds up a list of all text that will - * be different when outputted. - * - * The difference between CGUIString and SGUIText is that - * CGUIString is a string-class that parses the tags - * when the value is set. The SGUIText is just a container - * which stores the positions and settings of all text-calls - * that will have to be made to the Renderer. - */ -class CGUIString -{ -public: - /** - * A chunk of text that represents one call to the renderer. - * In other words, all text in one chunk, will be drawn - * exactly with the same settings. - */ - struct TextChunk - { - /** - * A tag looks like this "Hello [b]there[/b] little" - */ - struct Tag - { - /** - * Tag Type - */ - enum TagType - { - TAG_B, - TAG_I, - TAG_FONT, - TAG_SIZE, - TAG_COLOR, - TAG_IMGLEFT, - TAG_IMGRIGHT, - TAG_ICON, - TAG_INVALID - }; - - struct TagAttribute - { - std::wstring attrib; - std::wstring value; - }; - - /** - * Set tag from string - * - * @param tagtype TagType by string, like 'img' for [img] - * @return True if m_TagType was set. - */ - bool SetTagType(const CStrW& tagtype); - TagType GetTagType(const CStrW& tagtype) const; - - - /** - * In [b="Hello"][/b] - * m_TagType is TAG_B - */ - TagType m_TagType; - - /** - * In [b="Hello"][/b] - * m_TagValue is 'Hello' - */ - std::wstring m_TagValue; - - /** - * Some tags need an additional attributes - */ - std::vector m_TagAttributes; - }; - - /** - * m_From and m_To is the range of the string - */ - int m_From, m_To; - - /** - * Tags that are present. [a][b] - */ - std::vector m_Tags; - }; - - /** - * All data generated in GenerateTextCall() - */ - struct SFeedback - { - // Avoid copying the vector and list containers. - NONCOPYABLE(SFeedback); - MOVABLE(SFeedback); - SFeedback() = default; - - // Constants - static const int Left = 0; - static const int Right = 1; - - /** - * Reset all member data. - */ - void Reset(); - - /** - * Image stacks, for left and right floating images. - */ - std::vector m_Images[2]; // left and right - - /** - * Text and Sprite Calls. - */ - std::vector m_TextCalls; - std::list m_SpriteCalls; // list for consistent mem addresses - // so that we can point to elements. - - /** - * Width and Height *feedback* - */ - CSize m_Size; - - /** - * If the word inputted was a new line. - */ - bool m_NewLine; - }; - - /** - * Set the value, the string will automatically - * be parsed when set. - */ - void SetValue(const CStrW& str); - - /** - * Get String, with tags - */ - const CStrW& GetOriginalString() const { return m_OriginalString; } - - /** - * Get String, stripped of tags - */ - const CStrW& GetRawString() const { return m_RawString; } - - /** - * Generate Text Call from specified range. The range - * must span only within ONE TextChunk though. Otherwise - * it can't be fit into a single Text Call - * - * Notice it won't make it complete, you will have to add - * X/Y values and such. - * - * @param pGUI Pointer to CGUI object making this call, for e.g. icon retrieval. - * @param Feedback contains all info that is generated. - * @param DefaultFont Default Font - * @param from From character n, - * @param to to character n. - * @param FirstLine Whether this is the first line of text, to calculate its height correctly - * @param pObject Only for Error outputting, optional! If NULL - * then no Errors will be reported! Useful when you need - * to make several GenerateTextCall in different phases, - * it avoids duplicates. - */ - void GenerateTextCall(const CGUI* pGUI, SFeedback& Feedback, CStrIntern DefaultFont, const int& from, const int& to, const bool FirstLine, const IGUIObject* pObject = NULL) const; - - /** - * Words - */ - std::vector m_Words; - -private: - /** - * TextChunks - */ - std::vector m_TextChunks; - - /** - * The full raw string. Stripped of tags. - */ - CStrW m_RawString; - - /** - * The original string value passed to SetValue. - */ - CStrW m_OriginalString; -}; - #endif // INCLUDED_GUITEXT Index: ps/trunk/source/gui/GUItext.cpp =================================================================== --- ps/trunk/source/gui/GUItext.cpp +++ ps/trunk/source/gui/GUItext.cpp @@ -1,472 +0,0 @@ -/* 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 . - */ - -#include "precompiled.h" - -#include - -#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->HasIcon(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 - const SGUIIcon& icon = pGUI->GetIcon(path); - - const 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::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.emplace_back(std::move(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::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.emplace_back(std::move(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 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()); -}