/* Copyright (C) 2015 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.h" #include "lib/utf8.h" #include "graphics/FontMetrics.h" #include "ps/CLogger.h" // List of word demlimitor bounds // The list contains ranges of word delimitors. 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_DELIMITORS = 4*2; static const u16 WordDelimitors[NUM_WORD_DELIMITORS] = { ' ' , ' ', // 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::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(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::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 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; } // fall-through 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 // fall-through 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; } // fall-through 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_DELIMITORS; n += 2) { if (c <= WordDelimitors[n+1]) { if (c >= WordDelimitors[n]) m_Words.push_back((int)i+1); // assume the WordDelimitors 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()); }