Line data Source code
1 : /* Copyright (C) 2022 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
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.
8 : *
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.
13 : *
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/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "CGUIString.h"
21 :
22 : #include "graphics/FontMetrics.h"
23 : #include "gui/CGUI.h"
24 : #include "gui/ObjectBases/IGUIObject.h"
25 : #include "lib/utf8.h"
26 : #include "ps/CLogger.h"
27 :
28 : #include <algorithm>
29 : #include <array>
30 :
31 : // List of word delimiter bounds
32 : // The list contains ranges of word delimiters. The odd indexed chars are the start
33 : // of a range, the even are the end of a range. The list must be sorted in INCREASING ORDER
34 : static const int NUM_WORD_DELIMITERS = 4*2;
35 : static const u16 WordDelimiters[NUM_WORD_DELIMITERS] = {
36 : ' ' , ' ', // spaces
37 : '-' , '-', // hyphens
38 : 0x3000, 0x31FF, // ideographic symbols
39 : 0x3400, 0x9FFF
40 : // TODO add unicode blocks of other languages that don't use spaces
41 : };
42 :
43 1935 : void CGUIString::SFeedback::Reset()
44 : {
45 1935 : m_Images[Left].clear();
46 1935 : m_Images[Right].clear();
47 1935 : m_TextCalls.clear();
48 1935 : m_SpriteCalls.clear();
49 1935 : m_Size = CSize2D();
50 1935 : m_NewLine = false;
51 1935 : m_EndsWithSpace = false;
52 1935 : }
53 :
54 1935 : void CGUIString::GenerateTextCall(const CGUI& pGUI, SFeedback& Feedback, CStrIntern DefaultFont, const int& from, const int& to, const bool FirstLine, const IGUIObject* pObject) const
55 : {
56 : // Reset width and height, because they will be determined with incrementation
57 : // or comparisons.
58 1935 : Feedback.Reset();
59 :
60 : // Check out which text chunk this is within.
61 3870 : for (const TextChunk& textChunk : m_TextChunks)
62 : {
63 : // Get the area that is overlapped by both the TextChunk and
64 : // by the from/to inputted.
65 1935 : int _from = std::max(from, textChunk.m_From);
66 1935 : int _to = std::min(to, textChunk.m_To);
67 :
68 : // If from is larger than to, then they are not overlapping
69 1935 : if (_to == _from && textChunk.m_From == textChunk.m_To)
70 : {
71 : // These should never be able to have more than one tag.
72 0 : ENSURE(textChunk.m_Tags.size() == 1);
73 :
74 : // Icons and images are placed on exactly one position
75 : // in the words-list, and they can be counted twice if placed
76 : // on an edge. But there is always only one logical preference
77 : // that we want. This check filters the unwanted.
78 :
79 : // it's in the end of one word, and the icon
80 : // should really belong to the beginning of the next one
81 0 : if (_to == to && to >= 1 && to < (int)m_RawString.length())
82 : {
83 0 : if (m_RawString[to-1] == ' ' ||
84 0 : m_RawString[to-1] == '-' ||
85 0 : m_RawString[to-1] == '\n')
86 0 : continue;
87 : }
88 : // This std::string is just a break
89 0 : if (_from == from && from >= 1)
90 : {
91 0 : if (m_RawString[from] == '\n' &&
92 0 : m_RawString[from-1] != '\n' &&
93 0 : m_RawString[from-1] != ' ' &&
94 0 : m_RawString[from-1] != '-')
95 0 : continue;
96 : }
97 :
98 0 : const TextChunk::Tag& tag = textChunk.m_Tags[0];
99 0 : ENSURE(tag.m_TagType == TextChunk::Tag::TAG_IMGLEFT ||
100 : tag.m_TagType == TextChunk::Tag::TAG_IMGRIGHT ||
101 : tag.m_TagType == TextChunk::Tag::TAG_ICON);
102 :
103 0 : const std::string& path = utf8_from_wstring(tag.m_TagValue);
104 0 : if (!pGUI.HasIcon(path))
105 : {
106 0 : if (pObject)
107 0 : LOGERROR("Trying to use an icon, imgleft or imgright-tag with an undefined icon (\"%s\").", path.c_str());
108 0 : continue;
109 : }
110 :
111 0 : switch (tag.m_TagType)
112 : {
113 0 : case TextChunk::Tag::TAG_IMGLEFT:
114 0 : Feedback.m_Images[SFeedback::Left].push_back(path);
115 0 : break;
116 0 : case TextChunk::Tag::TAG_IMGRIGHT:
117 0 : Feedback.m_Images[SFeedback::Right].push_back(path);
118 0 : break;
119 0 : case TextChunk::Tag::TAG_ICON:
120 : {
121 : // We'll need to setup a text-call that will point
122 : // to the icon, this is to be able to iterate
123 : // through the text-calls without having to
124 : // complex the structure virtually for nothing more.
125 0 : CGUIText::STextCall TextCall;
126 :
127 : // Also add it to the sprites being rendered.
128 0 : CGUIText::SSpriteCall SpriteCall;
129 :
130 : // Get Icon from icon database in pGUI
131 0 : const SGUIIcon& icon = pGUI.GetIcon(path);
132 :
133 0 : const CSize2D& size = icon.m_Size;
134 :
135 : // append width, and make maximum height the height.
136 0 : Feedback.m_Size.Width += size.Width;
137 0 : Feedback.m_Size.Height = std::max(Feedback.m_Size.Height, size.Height);
138 :
139 : // These are also needed later
140 0 : TextCall.m_Size = size;
141 0 : SpriteCall.m_Area = size;
142 :
143 : // Handle additional attributes
144 0 : for (const TextChunk::Tag::TagAttribute& tagAttrib : tag.m_TagAttributes)
145 : {
146 0 : if (tagAttrib.attrib == L"displace" && !tagAttrib.value.empty())
147 : {
148 : // Displace the sprite
149 0 : CSize2D displacement;
150 : // Parse the value
151 0 : if (!CGUI::ParseString<CSize2D>(&pGUI, tagAttrib.value, displacement))
152 0 : LOGERROR("Error parsing 'displace' value for tag [ICON]");
153 : else
154 0 : SpriteCall.m_Area += displacement;
155 : }
156 0 : else if (tagAttrib.attrib == L"tooltip")
157 0 : TextCall.m_Tooltip = tagAttrib.value;
158 : }
159 :
160 0 : SpriteCall.m_Sprite = icon.m_SpriteName;
161 :
162 : // Add sprite call
163 0 : Feedback.m_SpriteCalls.push_back(std::move(SpriteCall));
164 :
165 : // Finalize text call
166 0 : TextCall.m_pSpriteCall = &Feedback.m_SpriteCalls.back();
167 :
168 : // Add text call
169 0 : Feedback.m_TextCalls.emplace_back(std::move(TextCall));
170 :
171 0 : break;
172 : }
173 0 : NODEFAULT;
174 0 : }
175 : }
176 1935 : else if (_to > _from && !Feedback.m_NewLine)
177 : {
178 3870 : CGUIText::STextCall TextCall;
179 :
180 : // Set defaults
181 1935 : TextCall.m_Font = DefaultFont;
182 1935 : TextCall.m_UseCustomColor = false;
183 :
184 1935 : TextCall.m_String = m_RawString.substr(_from, _to-_from);
185 :
186 : // Go through tags and apply changes.
187 1935 : for (const TextChunk::Tag& tag : textChunk.m_Tags)
188 : {
189 0 : switch (tag.m_TagType)
190 : {
191 0 : case TextChunk::Tag::TAG_COLOR:
192 0 : TextCall.m_UseCustomColor = true;
193 :
194 0 : if (!CGUI::ParseString<CGUIColor>(&pGUI, tag.m_TagValue, TextCall.m_Color) && pObject)
195 0 : LOGERROR("Error parsing the value of a [color]-tag in GUI text when reading object \"%s\".", pObject->GetPresentableName().c_str());
196 0 : break;
197 0 : case TextChunk::Tag::TAG_FONT:
198 : // TODO Gee: (2004-08-15) Check if Font exists?
199 0 : TextCall.m_Font = CStrIntern(utf8_from_wstring(tag.m_TagValue));
200 0 : break;
201 0 : case TextChunk::Tag::TAG_TOOLTIP:
202 0 : TextCall.m_Tooltip = tag.m_TagValue;
203 0 : break;
204 0 : default:
205 0 : LOGERROR("Encountered unexpected tag applied to text");
206 0 : break;
207 : }
208 : }
209 :
210 : // Calculate the size of the font.
211 1935 : CSize2D size;
212 : int cx, cy;
213 3870 : CFontMetrics font (TextCall.m_Font);
214 1935 : font.CalculateStringSize(TextCall.m_String.c_str(), cx, cy);
215 :
216 1935 : size.Width = static_cast<float>(cx);
217 : // For anything other than the first line, the line spacing
218 : // needs to be considered rather than just the height of the text.
219 1935 : if (FirstLine)
220 1188 : size.Height = static_cast<float>(font.GetHeight());
221 : else
222 747 : size.Height = static_cast<float>(font.GetLineSpacing());
223 :
224 : // Append width, and make maximum height the height.
225 1935 : Feedback.m_Size.Width += size.Width;
226 1935 : Feedback.m_Size.Height = std::max(Feedback.m_Size.Height, size.Height);
227 :
228 : // These are also needed later
229 1935 : TextCall.m_Size = size;
230 :
231 1935 : if (!TextCall.m_String.empty() && TextCall.m_String[0] == '\n')
232 45 : Feedback.m_NewLine = true;
233 : // Multiple empty spaces are treated as individual words (one per space),
234 : // and for coherence we'll do the 'ignore space after the word' thing
235 : // only if the word actually has some other text in it, so process this only if size >= 2
236 1890 : else if (TextCall.m_String.size() >= 2)
237 : {
238 1731 : const wchar_t lastChar = TextCall.m_String.back();
239 : // If the last word ends with a 'space', we'll ignore it when aligning, so mark it.
240 1731 : Feedback.m_EndsWithSpace = lastChar == ' ' || lastChar == 0x3000;
241 : }
242 : else
243 159 : Feedback.m_EndsWithSpace = false;
244 :
245 : // Add text-chunk
246 1935 : Feedback.m_TextCalls.emplace_back(std::move(TextCall));
247 : }
248 : }
249 1935 : }
250 :
251 0 : bool CGUIString::TextChunk::Tag::SetTagType(const CStrW& tagtype)
252 : {
253 0 : TagType t = GetTagType(tagtype);
254 0 : if (t == TAG_INVALID)
255 0 : return false;
256 :
257 0 : m_TagType = t;
258 0 : return true;
259 : }
260 :
261 0 : CGUIString::TextChunk::Tag::TagType CGUIString::TextChunk::Tag::GetTagType(const CStrW& tagtype) const
262 : {
263 0 : if (tagtype == L"color")
264 0 : return TAG_COLOR;
265 0 : if (tagtype == L"font")
266 0 : return TAG_FONT;
267 0 : if (tagtype == L"icon")
268 0 : return TAG_ICON;
269 0 : if (tagtype == L"imgleft")
270 0 : return TAG_IMGLEFT;
271 0 : if (tagtype == L"imgright")
272 0 : return TAG_IMGRIGHT;
273 0 : if (tagtype == L"tooltip")
274 0 : return TAG_TOOLTIP;
275 :
276 0 : return TAG_INVALID;
277 : }
278 :
279 7 : void CGUIString::SetValue(const CStrW& str)
280 : {
281 7 : m_OriginalString = str;
282 :
283 7 : m_TextChunks.clear();
284 7 : m_Words.clear();
285 7 : m_RawString.clear();
286 :
287 : // Current Text Chunk
288 13 : CGUIString::TextChunk CurrentTextChunk;
289 7 : CurrentTextChunk.m_From = 0;
290 :
291 7 : int l = str.length();
292 7 : int rawpos = 0;
293 13 : CStrW tag;
294 13 : std::vector<CStrW> tags;
295 7 : bool closing = false;
296 311 : for (int p = 0; p < l; ++p)
297 : {
298 608 : TextChunk::Tag tag_;
299 304 : switch (str[p])
300 : {
301 0 : case L'[':
302 0 : CurrentTextChunk.m_To = rawpos;
303 : // Add the current chunks if it is not empty
304 0 : if (CurrentTextChunk.m_From != rawpos)
305 0 : m_TextChunks.push_back(CurrentTextChunk);
306 0 : CurrentTextChunk.m_From = rawpos;
307 :
308 0 : closing = false;
309 0 : if (++p == l)
310 : {
311 0 : LOGERROR("Partial tag at end of string '%s'", utf8_from_wstring(str));
312 0 : break;
313 : }
314 0 : if (str[p] == L'/')
315 : {
316 0 : closing = true;
317 0 : if (tags.empty())
318 : {
319 0 : LOGERROR("Encountered closing tag without having any open tags. At %d in '%s'", p, utf8_from_wstring(str));
320 0 : break;
321 : }
322 0 : if (++p == l)
323 : {
324 0 : LOGERROR("Partial closing tag at end of string '%s'", utf8_from_wstring(str));
325 0 : break;
326 : }
327 : }
328 0 : tag.clear();
329 : // Parse tag
330 0 : for (; p < l && str[p] != L']'; ++p)
331 : {
332 0 : CStrW name, param;
333 0 : switch (str[p])
334 : {
335 0 : case L' ':
336 0 : if (closing) // We still parse them to make error handling cleaner
337 0 : LOGERROR("Closing tags do not support parameters (at pos %d '%s')", p, utf8_from_wstring(str));
338 :
339 : // parse something="something else"
340 0 : for (++p; p < l && str[p] != L'='; ++p)
341 0 : name.push_back(str[p]);
342 :
343 0 : if (p == l)
344 : {
345 0 : LOGERROR("Parameter without value at pos %d '%s'", p, utf8_from_wstring(str));
346 0 : break;
347 : }
348 : FALLTHROUGH;
349 : case L'=':
350 : // parse a quoted parameter
351 0 : if (closing) // We still parse them to make error handling cleaner
352 0 : LOGERROR("Closing tags do not support parameters (at pos %d '%s')", p, utf8_from_wstring(str));
353 :
354 0 : if (++p == l)
355 : {
356 0 : LOGERROR("Expected parameter, got end of string '%s'", utf8_from_wstring(str));
357 0 : break;
358 : }
359 0 : if (str[p] != L'"')
360 : {
361 0 : LOGERROR("Unquoted parameters are not supported (at pos %d '%s')", p, utf8_from_wstring(str));
362 0 : break;
363 : }
364 0 : for (++p; p < l && str[p] != L'"'; ++p)
365 : {
366 0 : switch (str[p])
367 : {
368 0 : case L'\\':
369 0 : if (++p == l)
370 : {
371 0 : LOGERROR("Escape character at end of string '%s'", utf8_from_wstring(str));
372 0 : break;
373 : }
374 : // NOTE: We do not support \n in tag parameters
375 : FALLTHROUGH;
376 : default:
377 0 : param.push_back(str[p]);
378 : }
379 : }
380 :
381 0 : if (!name.empty())
382 : {
383 0 : TextChunk::Tag::TagAttribute a = {name, param};
384 0 : tag_.m_TagAttributes.push_back(a);
385 : }
386 : else
387 0 : tag_.m_TagValue = param;
388 0 : break;
389 0 : default:
390 0 : tag.push_back(str[p]);
391 0 : break;
392 : }
393 0 : }
394 :
395 0 : if (!tag_.SetTagType(tag))
396 : {
397 0 : LOGERROR("Invalid tag '%s' at %d in '%s'", utf8_from_wstring(tag), p, utf8_from_wstring(str));
398 0 : break;
399 : }
400 0 : if (!closing)
401 : {
402 0 : if (tag_.m_TagType == TextChunk::Tag::TAG_IMGRIGHT
403 0 : || tag_.m_TagType == TextChunk::Tag::TAG_IMGLEFT
404 0 : || tag_.m_TagType == TextChunk::Tag::TAG_ICON)
405 : {
406 0 : TextChunk FreshTextChunk = { rawpos, rawpos };
407 0 : FreshTextChunk.m_Tags.push_back(tag_);
408 0 : m_TextChunks.push_back(FreshTextChunk);
409 : }
410 : else
411 : {
412 0 : tags.push_back(tag);
413 0 : CurrentTextChunk.m_Tags.push_back(tag_);
414 : }
415 : }
416 : else
417 : {
418 0 : if (tag != tags.back())
419 : {
420 0 : 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));
421 0 : break;
422 : }
423 :
424 0 : tags.pop_back();
425 0 : CurrentTextChunk.m_Tags.pop_back();
426 : }
427 0 : break;
428 0 : case L'\\':
429 0 : if (++p == l)
430 : {
431 0 : LOGERROR("Escape character at end of string '%s'", utf8_from_wstring(str));
432 0 : break;
433 : }
434 0 : if (str[p] == L'n')
435 : {
436 0 : ++rawpos;
437 0 : m_RawString.push_back(L'\n');
438 0 : break;
439 : }
440 : FALLTHROUGH;
441 : default:
442 304 : ++rawpos;
443 304 : m_RawString.push_back(str[p]);
444 304 : break;
445 : }
446 : }
447 :
448 : // Add the chunk after the last tag
449 7 : if (CurrentTextChunk.m_From != rawpos)
450 : {
451 7 : CurrentTextChunk.m_To = rawpos;
452 7 : m_TextChunks.push_back(CurrentTextChunk);
453 : }
454 :
455 :
456 : // Add a delimiter at start and at end, it helps when
457 : // processing later, because we don't have make exceptions for
458 : // those cases.
459 7 : m_Words.push_back(0);
460 :
461 : // Add word boundaries in increasing order
462 311 : for (u32 i = 0; i < m_RawString.length(); ++i)
463 : {
464 304 : wchar_t c = m_RawString[i];
465 308 : if (c == '\n')
466 : {
467 4 : m_Words.push_back((int)i);
468 4 : m_Words.push_back((int)i+1);
469 4 : continue;
470 : }
471 813 : for (int n = 0; n < NUM_WORD_DELIMITERS; n += 2)
472 : {
473 813 : if (c <= WordDelimiters[n+1])
474 : {
475 300 : if (c >= WordDelimiters[n])
476 43 : m_Words.push_back((int)i+1);
477 : // assume the WordDelimiters list is stored in increasing order
478 300 : break;
479 : }
480 : }
481 : }
482 :
483 7 : m_Words.push_back((int)m_RawString.length());
484 :
485 : // Remove duplicates (only if larger than 2)
486 7 : if (m_Words.size() <= 2)
487 1 : return;
488 :
489 6 : m_Words.erase(std::unique(m_Words.begin(), m_Words.end()), m_Words.end());
490 : }
|