Don't crash while in the lobby when receiving an error IQ stanza without an error...
[0ad.git] / source / gui / GUItext.cpp
blob4f89b9d861bd006a5a47266ab4d522f4c5a84ea9
1 /* Copyright (C) 2017 Wildfire Games.
2 * This file is part of 0 A.D.
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
18 #include "precompiled.h"
20 #include <algorithm>
22 #include "GUI.h"
23 #include "lib/utf8.h"
24 #include "graphics/FontMetrics.h"
25 #include "ps/CLogger.h"
28 // List of word delimiter bounds
29 // The list contains ranges of word delimiters. The odd indexed chars are the start
30 // of a range, the even are the end of a range. The list must be sorted in INCREASING ORDER
31 static const int NUM_WORD_DELIMITERS = 4*2;
32 static const u16 WordDelimiters[NUM_WORD_DELIMITERS] = {
33 ' ' , ' ', // spaces
34 '-' , '-', // hyphens
35 0x3000, 0x31FF, // ideographic symbols
36 0x3400, 0x9FFF
37 // TODO add unicode blocks of other languages that don't use spaces
40 void CGUIString::SFeedback::Reset()
42 m_Images[Left].clear();
43 m_Images[Right].clear();
44 m_TextCalls.clear();
45 m_SpriteCalls.clear();
46 m_Size = CSize();
47 m_NewLine = false;
50 void CGUIString::GenerateTextCall(const CGUI* pGUI, SFeedback& Feedback, CStrIntern DefaultFont, const int& from, const int& to, const bool FirstLine, const IGUIObject* pObject) const
52 // Reset width and height, because they will be determined with incrementation
53 // or comparisons.
54 Feedback.Reset();
56 // Check out which text chunk this is within.
57 for (const TextChunk& textChunk : m_TextChunks)
59 // Get the area that is overlapped by both the TextChunk and
60 // by the from/to inputted.
61 int _from = std::max(from, textChunk.m_From);
62 int _to = std::min(to, textChunk.m_To);
64 // If from is larger than to, then they are not overlapping
65 if (_to == _from && textChunk.m_From == textChunk.m_To)
67 // These should never be able to have more than one tag.
68 ENSURE(textChunk.m_Tags.size() == 1);
70 // Icons and images are placed on exactly one position
71 // in the words-list, and they can be counted twice if placed
72 // on an edge. But there is always only one logical preference
73 // that we want. This check filters the unwanted.
75 // it's in the end of one word, and the icon
76 // should really belong to the beginning of the next one
77 if (_to == to && to >= 1 && to < (int)m_RawString.length())
79 if (m_RawString[to-1] == ' ' ||
80 m_RawString[to-1] == '-' ||
81 m_RawString[to-1] == '\n')
82 continue;
84 // This std::string is just a break
85 if (_from == from && from >= 1)
87 if (m_RawString[from] == '\n' &&
88 m_RawString[from-1] != '\n' &&
89 m_RawString[from-1] != ' ' &&
90 m_RawString[from-1] != '-')
91 continue;
94 const TextChunk::Tag& tag = textChunk.m_Tags[0];
95 ENSURE(tag.m_TagType == TextChunk::Tag::TAG_IMGLEFT ||
96 tag.m_TagType == TextChunk::Tag::TAG_IMGRIGHT ||
97 tag.m_TagType == TextChunk::Tag::TAG_ICON);
99 const std::string& path = utf8_from_wstring(tag.m_TagValue);
100 if (!pGUI->IconExists(path))
102 if (pObject)
103 LOGERROR("Trying to use an icon, imgleft or imgright-tag with an undefined icon (\"%s\").", path.c_str());
104 continue;
107 switch (tag.m_TagType)
109 case TextChunk::Tag::TAG_IMGLEFT:
110 Feedback.m_Images[SFeedback::Left].push_back(path);
111 break;
112 case TextChunk::Tag::TAG_IMGRIGHT:
113 Feedback.m_Images[SFeedback::Right].push_back(path);
114 break;
115 case TextChunk::Tag::TAG_ICON:
117 // We'll need to setup a text-call that will point
118 // to the icon, this is to be able to iterate
119 // through the text-calls without having to
120 // complex the structure virtually for nothing more.
121 SGUIText::STextCall TextCall;
123 // Also add it to the sprites being rendered.
124 SGUIText::SSpriteCall SpriteCall;
126 // Get Icon from icon database in pGUI
127 SGUIIcon icon = pGUI->GetIcon(path);
129 CSize size = icon.m_Size;
131 // append width, and make maximum height the height.
132 Feedback.m_Size.cx += size.cx;
133 Feedback.m_Size.cy = std::max(Feedback.m_Size.cy, size.cy);
135 // These are also needed later
136 TextCall.m_Size = size;
137 SpriteCall.m_Area = size;
139 // Handle additional attributes
140 for (const TextChunk::Tag::TagAttribute& tagAttrib : tag.m_TagAttributes)
142 if (tagAttrib.attrib == L"displace" && !tagAttrib.value.empty())
144 // Displace the sprite
145 CSize displacement;
146 // Parse the value
147 if (!GUI<CSize>::ParseString(tagAttrib.value, displacement))
148 LOGERROR("Error parsing 'displace' value for tag [ICON]");
149 else
150 SpriteCall.m_Area += displacement;
152 else if (tagAttrib.attrib == L"tooltip")
153 SpriteCall.m_Tooltip = tagAttrib.value;
154 else if (tagAttrib.attrib == L"tooltip_style")
155 SpriteCall.m_TooltipStyle = tagAttrib.value;
158 SpriteCall.m_Sprite = icon.m_SpriteName;
159 SpriteCall.m_CellID = icon.m_CellID;
161 // Add sprite call
162 Feedback.m_SpriteCalls.push_back(SpriteCall);
164 // Finalize text call
165 TextCall.m_pSpriteCall = &Feedback.m_SpriteCalls.back();
167 // Add text call
168 Feedback.m_TextCalls.push_back(TextCall);
170 break;
172 NODEFAULT;
175 else if (_to > _from && !Feedback.m_NewLine)
177 SGUIText::STextCall TextCall;
179 // Set defaults
180 TextCall.m_Font = DefaultFont;
181 TextCall.m_UseCustomColor = false;
183 TextCall.m_String = m_RawString.substr(_from, _to-_from);
185 // Go through tags and apply changes.
186 for (const TextChunk::Tag& tag : textChunk.m_Tags)
188 switch (tag.m_TagType)
190 case TextChunk::Tag::TAG_COLOR:
191 TextCall.m_UseCustomColor = true;
193 if (!GUI<CColor>::ParseString(tag.m_TagValue, TextCall.m_Color) && pObject)
194 LOGERROR("Error parsing the value of a [color]-tag in GUI text when reading object \"%s\".", pObject->GetPresentableName().c_str());
195 break;
196 case TextChunk::Tag::TAG_FONT:
197 // TODO Gee: (2004-08-15) Check if Font exists?
198 TextCall.m_Font = CStrIntern(utf8_from_wstring(tag.m_TagValue));
199 break;
200 default:
201 LOGERROR("Encountered unexpected tag applied to text");
202 break;
206 // Calculate the size of the font
207 CSize size;
208 int cx, cy;
209 CFontMetrics font (TextCall.m_Font);
210 font.CalculateStringSize(TextCall.m_String.c_str(), cx, cy);
211 // For anything other than the first line, the line spacing
212 // needs to be considered rather than just the height of the text
213 if (!FirstLine)
214 cy = font.GetLineSpacing();
216 size.cx = (float)cx;
217 size.cy = (float)cy;
219 // Append width, and make maximum height the height.
220 Feedback.m_Size.cx += size.cx;
221 Feedback.m_Size.cy = std::max(Feedback.m_Size.cy, size.cy);
223 // These are also needed later
224 TextCall.m_Size = size;
226 if (!TextCall.m_String.empty() && TextCall.m_String[0] == '\n')
227 Feedback.m_NewLine = true;
229 // Add text-chunk
230 Feedback.m_TextCalls.push_back(TextCall);
235 bool CGUIString::TextChunk::Tag::SetTagType(const CStrW& tagtype)
237 TagType t = GetTagType(tagtype);
238 if (t == TAG_INVALID)
239 return false;
241 m_TagType = t;
242 return true;
245 CGUIString::TextChunk::Tag::TagType CGUIString::TextChunk::Tag::GetTagType(const CStrW& tagtype) const
247 if (tagtype == L"color")
248 return TAG_COLOR;
249 if (tagtype == L"font")
250 return TAG_FONT;
251 if (tagtype == L"icon")
252 return TAG_ICON;
253 if (tagtype == L"imgleft")
254 return TAG_IMGLEFT;
255 if (tagtype == L"imgright")
256 return TAG_IMGRIGHT;
258 return TAG_INVALID;
261 void CGUIString::SetValue(const CStrW& str)
263 m_OriginalString = str;
265 m_TextChunks.clear();
266 m_Words.clear();
267 m_RawString.clear();
269 // Current Text Chunk
270 CGUIString::TextChunk CurrentTextChunk;
271 CurrentTextChunk.m_From = 0;
273 int l = str.length();
274 int rawpos = 0;
275 CStrW tag;
276 std::vector<CStrW> tags;
277 bool closing = false;
278 for (int p = 0; p < l; ++p)
280 TextChunk::Tag tag_;
281 switch (str[p])
283 case L'[':
284 CurrentTextChunk.m_To = rawpos;
285 // Add the current chunks if it is not empty
286 if (CurrentTextChunk.m_From != rawpos)
287 m_TextChunks.push_back(CurrentTextChunk);
288 CurrentTextChunk.m_From = rawpos;
290 closing = false;
291 if (++p == l)
293 LOGERROR("Partial tag at end of string '%s'", utf8_from_wstring(str));
294 break;
296 if (str[p] == L'/')
298 closing = true;
299 if (tags.empty())
301 LOGERROR("Encountered closing tag without having any open tags. At %d in '%s'", p, utf8_from_wstring(str));
302 break;
304 if (++p == l)
306 LOGERROR("Partial closing tag at end of string '%s'", utf8_from_wstring(str));
307 break;
310 tag.clear();
311 // Parse tag
312 for (; p < l && str[p] != L']'; ++p)
314 CStrW name, param;
315 switch (str[p])
317 case L' ':
318 if (closing) // We still parse them to make error handling cleaner
319 LOGERROR("Closing tags do not support parameters (at pos %d '%s')", p, utf8_from_wstring(str));
321 // parse something="something else"
322 for (++p; p < l && str[p] != L'='; ++p)
323 name.push_back(str[p]);
325 if (p == l)
327 LOGERROR("Parameter without value at pos %d '%s'", p, utf8_from_wstring(str));
328 break;
330 FALLTHROUGH;
331 case L'=':
332 // parse a quoted parameter
333 if (closing) // We still parse them to make error handling cleaner
334 LOGERROR("Closing tags do not support parameters (at pos %d '%s')", p, utf8_from_wstring(str));
336 if (++p == l)
338 LOGERROR("Expected parameter, got end of string '%s'", utf8_from_wstring(str));
339 break;
341 if (str[p] != L'"')
343 LOGERROR("Unquoted parameters are not supported (at pos %d '%s')", p, utf8_from_wstring(str));
344 break;
346 for (++p; p < l && str[p] != L'"'; ++p)
348 switch (str[p])
350 case L'\\':
351 if (++p == l)
353 LOGERROR("Escape character at end of string '%s'", utf8_from_wstring(str));
354 break;
356 // NOTE: We do not support \n in tag parameters
357 FALLTHROUGH;
358 default:
359 param.push_back(str[p]);
363 if (!name.empty())
365 TextChunk::Tag::TagAttribute a = {name, param};
366 tag_.m_TagAttributes.push_back(a);
368 else
369 tag_.m_TagValue = param;
370 break;
371 default:
372 tag.push_back(str[p]);
373 break;
377 if (!tag_.SetTagType(tag))
379 LOGERROR("Invalid tag '%s' at %d in '%s'", utf8_from_wstring(tag), p, utf8_from_wstring(str));
380 break;
382 if (!closing)
384 if (tag_.m_TagType == TextChunk::Tag::TAG_IMGRIGHT
385 || tag_.m_TagType == TextChunk::Tag::TAG_IMGLEFT
386 || tag_.m_TagType == TextChunk::Tag::TAG_ICON)
388 TextChunk FreshTextChunk = { rawpos, rawpos };
389 FreshTextChunk.m_Tags.push_back(tag_);
390 m_TextChunks.push_back(FreshTextChunk);
392 else
394 tags.push_back(tag);
395 CurrentTextChunk.m_Tags.push_back(tag_);
398 else
400 if (tag != tags.back())
402 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));
403 break;
406 tags.pop_back();
407 CurrentTextChunk.m_Tags.pop_back();
409 break;
410 case L'\\':
411 if (++p == l)
413 LOGERROR("Escape character at end of string '%s'", utf8_from_wstring(str));
414 break;
416 if (str[p] == L'n')
418 ++rawpos;
419 m_RawString.push_back(L'\n');
420 break;
422 FALLTHROUGH;
423 default:
424 ++rawpos;
425 m_RawString.push_back(str[p]);
426 break;
430 // Add the chunk after the last tag
431 if (CurrentTextChunk.m_From != rawpos)
433 CurrentTextChunk.m_To = rawpos;
434 m_TextChunks.push_back(CurrentTextChunk);
438 // Add a delimiter at start and at end, it helps when
439 // processing later, because we don't have make exceptions for
440 // those cases.
441 m_Words.push_back(0);
443 // Add word boundaries in increasing order
444 for (u32 i = 0; i < m_RawString.length(); ++i)
446 wchar_t c = m_RawString[i];
447 if (c == '\n')
449 m_Words.push_back((int)i);
450 m_Words.push_back((int)i+1);
451 continue;
453 for (int n = 0; n < NUM_WORD_DELIMITERS; n += 2)
455 if (c <= WordDelimiters[n+1])
457 if (c >= WordDelimiters[n])
458 m_Words.push_back((int)i+1);
459 // assume the WordDelimiters list is stored in increasing order
460 break;
465 m_Words.push_back((int)m_RawString.length());
467 // Remove duplicates (only if larger than 2)
468 if (m_Words.size() <= 2)
469 return;
471 m_Words.erase(std::unique(m_Words.begin(), m_Words.end()), m_Words.end());