1 | /* Copyright (C) 2021 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 "CGUIText.h"
|
---|
21 |
|
---|
22 | #include "graphics/Canvas2D.h"
|
---|
23 | #include "graphics/FontMetrics.h"
|
---|
24 | #include "graphics/TextRenderer.h"
|
---|
25 | #include "gui/CGUI.h"
|
---|
26 | #include "gui/ObjectBases/IGUIObject.h"
|
---|
27 | #include "gui/SettingTypes/CGUIString.h"
|
---|
28 | #include "ps/CStrInternStatic.h"
|
---|
29 | #include "renderer/Renderer.h"
|
---|
30 |
|
---|
31 | #include <math.h>
|
---|
32 |
|
---|
33 | extern int g_xres, g_yres;
|
---|
34 | extern float g_GuiScale;
|
---|
35 |
|
---|
36 | // TODO Gee: CRect => CPoint ?
|
---|
37 | void SGenerateTextImage::SetupSpriteCall(
|
---|
38 | const bool Left, CGUIText::SSpriteCall& SpriteCall, const float width, const float y,
|
---|
39 | const CSize2D& Size, const CStr& TextureName, const float BufferZone)
|
---|
40 | {
|
---|
41 | // TODO Gee: Temp hardcoded values
|
---|
42 | SpriteCall.m_Area.top = y + BufferZone;
|
---|
43 | SpriteCall.m_Area.bottom = y + BufferZone + Size.Height;
|
---|
44 |
|
---|
45 | if (Left)
|
---|
46 | {
|
---|
47 | SpriteCall.m_Area.left = BufferZone;
|
---|
48 | SpriteCall.m_Area.right = Size.Width + BufferZone;
|
---|
49 | }
|
---|
50 | else
|
---|
51 | {
|
---|
52 | SpriteCall.m_Area.left = width-BufferZone - Size.Width;
|
---|
53 | SpriteCall.m_Area.right = width-BufferZone;
|
---|
54 | }
|
---|
55 |
|
---|
56 | SpriteCall.m_Sprite = TextureName;
|
---|
57 |
|
---|
58 | m_YFrom = SpriteCall.m_Area.top - BufferZone;
|
---|
59 | m_YTo = SpriteCall.m_Area.bottom + BufferZone;
|
---|
60 | m_Indentation = Size.Width + BufferZone * 2;
|
---|
61 | }
|
---|
62 |
|
---|
63 | CGUIText::CGUIText(const CGUI& pGUI, const CGUIString& string, const CStrW& FontW, const float Width, const float BufferZone, const EAlign align, const IGUIObject* pObject)
|
---|
64 | {
|
---|
65 | if (string.m_Words.empty())
|
---|
66 | return;
|
---|
67 |
|
---|
68 | CStrIntern Font(FontW.ToUTF8());
|
---|
69 | float x = BufferZone, y = BufferZone; // drawing pointer
|
---|
70 | int from = 0;
|
---|
71 |
|
---|
72 | bool FirstLine = true; // Necessary because text in the first line is shorter
|
---|
73 | // (it doesn't count the line spacing)
|
---|
74 |
|
---|
75 | // Images on the left or the right side.
|
---|
76 | SGenerateTextImages Images;
|
---|
77 | int pos_last_img = -1; // Position in the string where last img (either left or right) were encountered.
|
---|
78 | // in order to avoid duplicate processing.
|
---|
79 |
|
---|
80 | // Go through string word by word
|
---|
81 | for (int i = 0; i < static_cast<int>(string.m_Words.size()) - 1; ++i)
|
---|
82 | {
|
---|
83 | // Pre-process each line one time, so we know which floating images
|
---|
84 | // will be added for that line.
|
---|
85 |
|
---|
86 | // Generated stuff is stored in Feedback.
|
---|
87 | CGUIString::SFeedback Feedback;
|
---|
88 |
|
---|
89 | // Preliminary line height, used for word-wrapping with floating images.
|
---|
90 | float prelim_line_height = 0.f;
|
---|
91 |
|
---|
92 | // Width and height of all text calls generated.
|
---|
93 | string.GenerateTextCall(pGUI, Feedback, Font, string.m_Words[i], string.m_Words[i+1], FirstLine);
|
---|
94 |
|
---|
95 | SetupSpriteCalls(pGUI, Feedback.m_Images, y, Width, BufferZone, i, pos_last_img, Images);
|
---|
96 |
|
---|
97 | pos_last_img = std::max(pos_last_img, i);
|
---|
98 |
|
---|
99 | x += Feedback.m_Size.Width;
|
---|
100 | prelim_line_height = std::max(prelim_line_height, Feedback.m_Size.Height);
|
---|
101 |
|
---|
102 | // If Width is 0, then there's no word-wrapping, disable NewLine.
|
---|
103 | if (((Width != 0 && (x > Width - BufferZone || Feedback.m_NewLine)) || i == static_cast<int>(string.m_Words.size()) - 2) &&
|
---|
104 | ProcessLine(pGUI, string, Font, pObject, Images, align, prelim_line_height, Width, BufferZone, FirstLine, x, y, i, from))
|
---|
105 | return;
|
---|
106 | }
|
---|
107 | }
|
---|
108 |
|
---|
109 | // Loop through our images queues, to see if images have been added.
|
---|
110 | void CGUIText::SetupSpriteCalls(
|
---|
111 | const CGUI& pGUI,
|
---|
112 | const std::array<std::vector<CStr>, 2>& FeedbackImages,
|
---|
113 | const float y,
|
---|
114 | const float Width,
|
---|
115 | const float BufferZone,
|
---|
116 | const int i,
|
---|
117 | const int pos_last_img,
|
---|
118 | SGenerateTextImages& Images)
|
---|
119 | {
|
---|
120 | // Check if this has already been processed.
|
---|
121 | // Also, floating images are only applicable if Word-Wrapping is on
|
---|
122 | if (Width == 0 || i <= pos_last_img)
|
---|
123 | return;
|
---|
124 |
|
---|
125 | // Loop left/right
|
---|
126 | for (int j = 0; j < 2; ++j)
|
---|
127 | for (const CStr& imgname : FeedbackImages[j])
|
---|
128 | {
|
---|
129 | SSpriteCall SpriteCall;
|
---|
130 | SGenerateTextImage Image;
|
---|
131 |
|
---|
132 | // Y is if no other floating images is above, y. Else it is placed
|
---|
133 | // after the last image, like a stack downwards.
|
---|
134 | float _y;
|
---|
135 | if (!Images[j].empty())
|
---|
136 | _y = std::max(y, Images[j].back().m_YTo);
|
---|
137 | else
|
---|
138 | _y = y;
|
---|
139 |
|
---|
140 | const SGUIIcon& icon = pGUI.GetIcon(imgname);
|
---|
141 | Image.SetupSpriteCall(j == CGUIString::SFeedback::Left, SpriteCall, Width, _y, icon.m_Size, icon.m_SpriteName, BufferZone);
|
---|
142 |
|
---|
143 | // Check if image is the lowest thing.
|
---|
144 | m_Size.Height = std::max(m_Size.Height, Image.m_YTo);
|
---|
145 |
|
---|
146 | Images[j].emplace_back(Image);
|
---|
147 | m_SpriteCalls.emplace_back(std::move(SpriteCall));
|
---|
148 | }
|
---|
149 | }
|
---|
150 |
|
---|
151 | // Now we'll do another loop to figure out the height and width of
|
---|
152 | // the line (the height of the largest character and the width is
|
---|
153 | // the sum of all of the individual widths). This
|
---|
154 | // couldn't be determined in the first loop (main loop)
|
---|
155 | // because it didn't regard images, so we don't know
|
---|
156 | // if all characters processed, will actually be involved
|
---|
157 | // in that line.
|
---|
158 | void CGUIText::ComputeLineSize(
|
---|
159 | const CGUI& pGUI,
|
---|
160 | const CGUIString& string,
|
---|
161 | const CStrIntern& Font,
|
---|
162 | const bool FirstLine,
|
---|
163 | const float Width,
|
---|
164 | const float width_range_to,
|
---|
165 | const int i,
|
---|
166 | const int temp_from,
|
---|
167 | float& x,
|
---|
168 | CSize2D& line_size) const
|
---|
169 | {
|
---|
170 | for (int j = temp_from; j <= i; ++j)
|
---|
171 | {
|
---|
172 | // We don't want to use Feedback now, so we'll have to use another one.
|
---|
173 | CGUIString::SFeedback Feedback2;
|
---|
174 |
|
---|
175 | // Don't attach object, it'll suppress the errors
|
---|
176 | // we want them to be reported in the final GenerateTextCall()
|
---|
177 | // so that we don't get duplicates.
|
---|
178 | string.GenerateTextCall(pGUI, Feedback2, Font, string.m_Words[j], string.m_Words[j+1], FirstLine);
|
---|
179 |
|
---|
180 | // Append X value.
|
---|
181 | x += Feedback2.m_Size.Width;
|
---|
182 |
|
---|
183 | if (Width != 0 && x > width_range_to && j != temp_from && !Feedback2.m_NewLine)
|
---|
184 | {
|
---|
185 | // The calculated width of each word includes the space between the current
|
---|
186 | // word and the next. When we're wrapping, we need subtract the width of the
|
---|
187 | // space after the last word on the line before the wrap.
|
---|
188 | CFontMetrics currentFont(Font);
|
---|
189 | line_size.Width -= currentFont.GetCharacterWidth(*L" ");
|
---|
190 | break;
|
---|
191 | }
|
---|
192 |
|
---|
193 | // Let line_size.cy be the maximum m_Height we encounter.
|
---|
194 | line_size.Height = std::max(line_size.Height, Feedback2.m_Size.Height);
|
---|
195 |
|
---|
196 | // If the current word is an explicit new line ("\n"),
|
---|
197 | // break now before adding the width of this character.
|
---|
198 | // ("\n" doesn't have a glyph, thus is given the same width as
|
---|
199 | // the "missing glyph" character by CFont::GetCharacterWidth().)
|
---|
200 | if (Width != 0 && Feedback2.m_NewLine)
|
---|
201 | break;
|
---|
202 |
|
---|
203 | line_size.Width += Feedback2.m_Size.Width;
|
---|
204 | }
|
---|
205 | }
|
---|
206 |
|
---|
207 | bool CGUIText::ProcessLine(
|
---|
208 | const CGUI& pGUI,
|
---|
209 | const CGUIString& string,
|
---|
210 | const CStrIntern& Font,
|
---|
211 | const IGUIObject* pObject,
|
---|
212 | const SGenerateTextImages& Images,
|
---|
213 | const EAlign align,
|
---|
214 | const float prelim_line_height,
|
---|
215 | const float Width,
|
---|
216 | const float BufferZone,
|
---|
217 | bool& FirstLine,
|
---|
218 | float& x,
|
---|
219 | float& y,
|
---|
220 | int& i,
|
---|
221 | int& from)
|
---|
222 | {
|
---|
223 | // Change 'from' to 'i', but first keep a copy of its value.
|
---|
224 | int temp_from = from;
|
---|
225 | from = i;
|
---|
226 |
|
---|
227 | float width_range_from = BufferZone;
|
---|
228 | float width_range_to = Width - BufferZone;
|
---|
229 | ComputeLineRange(Images, y, Width, prelim_line_height, width_range_from, width_range_to);
|
---|
230 |
|
---|
231 | // Reset X for the next loop
|
---|
232 | x = width_range_from;
|
---|
233 |
|
---|
234 | CSize2D line_size;
|
---|
235 | ComputeLineSize(pGUI, string, Font, FirstLine, Width, width_range_to, i, temp_from, x, line_size);
|
---|
236 |
|
---|
237 | // Reset x once more
|
---|
238 | x = width_range_from;
|
---|
239 |
|
---|
240 | // Move down, because font drawing starts from the baseline
|
---|
241 | y += line_size.Height;
|
---|
242 |
|
---|
243 | const float dx = GetLineOffset(align, width_range_from, width_range_to, line_size);
|
---|
244 |
|
---|
245 | // Do the real processing now
|
---|
246 | const bool done = AssembleCalls(pGUI, string, Font, pObject, FirstLine, Width, width_range_to, dx, y, temp_from, i, x, from);
|
---|
247 |
|
---|
248 | // Reset X
|
---|
249 | x = BufferZone;
|
---|
250 |
|
---|
251 | // Update dimensions
|
---|
252 | m_Size.Width = std::max(m_Size.Width, line_size.Width + BufferZone * 2);
|
---|
253 | m_Size.Height = std::max(m_Size.Height, y + BufferZone);
|
---|
254 |
|
---|
255 | FirstLine = false;
|
---|
256 |
|
---|
257 | // Now if we entered as from = i, then we want
|
---|
258 | // i being one minus that, so that it will become
|
---|
259 | // the same i in the next loop. The difference is that
|
---|
260 | // we're on a new line now.
|
---|
261 | i = from - 1;
|
---|
262 |
|
---|
263 | return done;
|
---|
264 | }
|
---|
265 |
|
---|
266 | // Decide width of the line. We need to iterate our floating images.
|
---|
267 | // this won't be exact because we're assuming the line_size.cy
|
---|
268 | // will be as our preliminary calculation said. But that may change,
|
---|
269 | // although we'd have to add a couple of more loops to try straightening
|
---|
270 | // this problem out, and it is very unlikely to happen noticeably if one
|
---|
271 | // structures his text in a stylistically pure fashion. Even if not, it
|
---|
272 | // is still quite unlikely it will happen.
|
---|
273 | // Loop through left and right side, from and to.
|
---|
274 | void CGUIText::ComputeLineRange(
|
---|
275 | const SGenerateTextImages& Images,
|
---|
276 | const float y,
|
---|
277 | const float Width,
|
---|
278 | const float prelim_line_height,
|
---|
279 | float& width_range_from,
|
---|
280 | float& width_range_to) const
|
---|
281 | {
|
---|
282 | // Floating images are only applicable if word-wrapping is enabled.
|
---|
283 | if (Width == 0)
|
---|
284 | return;
|
---|
285 |
|
---|
286 | for (int j = 0; j < 2; ++j)
|
---|
287 | for (const SGenerateTextImage& img : Images[j])
|
---|
288 | {
|
---|
289 | // We're working with two intervals here, the image's and the line height's.
|
---|
290 | // let's find the union of these two.
|
---|
291 | float union_from, union_to;
|
---|
292 |
|
---|
293 | union_from = std::max(y, img.m_YFrom);
|
---|
294 | union_to = std::min(y + prelim_line_height, img.m_YTo);
|
---|
295 |
|
---|
296 | // The union is not empty
|
---|
297 | if (union_to > union_from)
|
---|
298 | {
|
---|
299 | if (j == 0)
|
---|
300 | width_range_from = std::max(width_range_from, img.m_Indentation);
|
---|
301 | else
|
---|
302 | width_range_to = std::min(width_range_to, Width - img.m_Indentation);
|
---|
303 | }
|
---|
304 | }
|
---|
305 | }
|
---|
306 |
|
---|
307 | // compute offset based on what kind of alignment
|
---|
308 | float CGUIText::GetLineOffset(
|
---|
309 | const EAlign align,
|
---|
310 | const float width_range_from,
|
---|
311 | const float width_range_to,
|
---|
312 | const CSize2D& line_size) const
|
---|
313 | {
|
---|
314 | switch (align)
|
---|
315 | {
|
---|
316 | case EAlign::LEFT:
|
---|
317 | // don't add an offset
|
---|
318 | return 0.f;
|
---|
319 |
|
---|
320 | case EAlign::CENTER:
|
---|
321 | return ((width_range_to - width_range_from) - line_size.Width) / 2;
|
---|
322 |
|
---|
323 | case EAlign::RIGHT:
|
---|
324 | return width_range_to - line_size.Width;
|
---|
325 |
|
---|
326 | default:
|
---|
327 | debug_warn(L"Broken EAlign in CGUIText()");
|
---|
328 | return 0.f;
|
---|
329 | }
|
---|
330 | }
|
---|
331 |
|
---|
332 | bool CGUIText::AssembleCalls(
|
---|
333 | const CGUI& pGUI,
|
---|
334 | const CGUIString& string,
|
---|
335 | const CStrIntern& Font,
|
---|
336 | const IGUIObject* pObject,
|
---|
337 | const bool FirstLine,
|
---|
338 | const float Width,
|
---|
339 | const float width_range_to,
|
---|
340 | const float dx,
|
---|
341 | const float y,
|
---|
342 | const int temp_from,
|
---|
343 | const int i,
|
---|
344 | float& x,
|
---|
345 | int& from)
|
---|
346 | {
|
---|
347 | bool done = false;
|
---|
348 |
|
---|
349 | for (int j = temp_from; j <= i; ++j)
|
---|
350 | {
|
---|
351 | // We don't want to use Feedback now, so we'll have to use another one.
|
---|
352 | CGUIString::SFeedback Feedback2;
|
---|
353 |
|
---|
354 | // Defaults
|
---|
355 | string.GenerateTextCall(pGUI, Feedback2, Font, string.m_Words[j], string.m_Words[j+1], FirstLine, pObject);
|
---|
356 |
|
---|
357 | // Iterate all and set X/Y values
|
---|
358 | // Since X values are not set, we need to make an internal
|
---|
359 | // iteration with an increment that will append the internal
|
---|
360 | // x, that is what x_pointer is for.
|
---|
361 | float x_pointer = 0.f;
|
---|
362 |
|
---|
363 | for (STextCall& tc : Feedback2.m_TextCalls)
|
---|
364 | {
|
---|
365 | tc.m_Pos = CVector2D(dx + x + x_pointer, y);
|
---|
366 |
|
---|
367 | x_pointer += tc.m_Size.Width;
|
---|
368 |
|
---|
369 | if (tc.m_pSpriteCall)
|
---|
370 | tc.m_pSpriteCall->m_Area += tc.m_Pos - CSize2D(0, tc.m_pSpriteCall->m_Area.GetHeight());
|
---|
371 | }
|
---|
372 |
|
---|
373 | // Append X value.
|
---|
374 | x += Feedback2.m_Size.Width;
|
---|
375 |
|
---|
376 | // The first word overrides the width limit, what we
|
---|
377 | // do, in those cases, are just drawing that word even
|
---|
378 | // though it'll extend the object.
|
---|
379 | if (Width != 0) // only if word-wrapping is applicable
|
---|
380 | {
|
---|
381 | if (Feedback2.m_NewLine)
|
---|
382 | {
|
---|
383 | from = j + 1;
|
---|
384 |
|
---|
385 | // Sprite call can exist within only a newline segment,
|
---|
386 | // therefore we need this.
|
---|
387 | if (!Feedback2.m_SpriteCalls.empty())
|
---|
388 | {
|
---|
389 | auto newEnd = std::remove_if(Feedback2.m_TextCalls.begin(), Feedback2.m_TextCalls.end(), [](const STextCall& call) { return !call.m_pSpriteCall; });
|
---|
390 | m_TextCalls.insert(
|
---|
391 | m_TextCalls.end(),
|
---|
392 | std::make_move_iterator(Feedback2.m_TextCalls.begin()),
|
---|
393 | std::make_move_iterator(newEnd));
|
---|
394 | m_SpriteCalls.insert(
|
---|
395 | m_SpriteCalls.end(),
|
---|
396 | std::make_move_iterator(Feedback2.m_SpriteCalls.begin()),
|
---|
397 | std::make_move_iterator(Feedback2.m_SpriteCalls.end()));
|
---|
398 | }
|
---|
399 | break;
|
---|
400 | }
|
---|
401 | else if (x > width_range_to && j == temp_from)
|
---|
402 | {
|
---|
403 | from = j+1;
|
---|
404 | // do not break, since we want it to be added to m_TextCalls
|
---|
405 | }
|
---|
406 | else if (x > width_range_to)
|
---|
407 | {
|
---|
408 | from = j;
|
---|
409 | break;
|
---|
410 | }
|
---|
411 | }
|
---|
412 |
|
---|
413 | // Add the whole Feedback2.m_TextCalls to our m_TextCalls.
|
---|
414 | m_TextCalls.insert(
|
---|
415 | m_TextCalls.end(),
|
---|
416 | std::make_move_iterator(Feedback2.m_TextCalls.begin()),
|
---|
417 | std::make_move_iterator(Feedback2.m_TextCalls.end()));
|
---|
418 |
|
---|
419 | m_SpriteCalls.insert(
|
---|
420 | m_SpriteCalls.end(),
|
---|
421 | std::make_move_iterator(Feedback2.m_SpriteCalls.begin()),
|
---|
422 | std::make_move_iterator(Feedback2.m_SpriteCalls.end()));
|
---|
423 |
|
---|
424 | if (j == static_cast<int>(string.m_Words.size()) - 2)
|
---|
425 | done = true;
|
---|
426 | }
|
---|
427 |
|
---|
428 | return done;
|
---|
429 | }
|
---|
430 |
|
---|
431 | void CGUIText::Draw(CGUI& pGUI, CCanvas2D& canvas, const CGUIColor& DefaultColor, const CVector2D& pos, CRect clipping) const
|
---|
432 | {
|
---|
433 | bool isClipped = clipping != CRect();
|
---|
434 | if (isClipped)
|
---|
435 | {
|
---|
436 | // Make clipping rect as small as possible to prevent rounding errors
|
---|
437 | clipping.top = std::ceil(clipping.top);
|
---|
438 | clipping.bottom = std::floor(clipping.bottom);
|
---|
439 | clipping.left = std::ceil(clipping.left);
|
---|
440 | clipping.right = std::floor(clipping.right);
|
---|
441 |
|
---|
442 | glEnable(GL_SCISSOR_TEST);
|
---|
443 | glScissor(
|
---|
444 | std::ceil(clipping.left * g_GuiScale),
|
---|
445 | std::ceil(g_yres - clipping.bottom * g_GuiScale),
|
---|
446 | std::floor(clipping.GetWidth() * g_GuiScale),
|
---|
447 | std::floor(clipping.GetHeight() * g_GuiScale));
|
---|
448 | }
|
---|
449 |
|
---|
450 | CTextRenderer textRenderer;
|
---|
451 | textRenderer.SetClippingRect(clipping);
|
---|
452 | textRenderer.Translate(0.0f, 0.0f);
|
---|
453 |
|
---|
454 | for (const STextCall& tc : m_TextCalls)
|
---|
455 | {
|
---|
456 | // If this is just a placeholder for a sprite call, continue
|
---|
457 | if (tc.m_pSpriteCall)
|
---|
458 | continue;
|
---|
459 |
|
---|
460 | textRenderer.SetCurrentColor(tc.m_UseCustomColor ? tc.m_Color : DefaultColor);
|
---|
461 | textRenderer.SetCurrentFont(tc.m_Font);
|
---|
462 | textRenderer.Put(floorf(pos.X + tc.m_Pos.X), floorf(pos.Y + tc.m_Pos.Y), &tc.m_String);
|
---|
463 | }
|
---|
464 |
|
---|
465 | canvas.DrawText(textRenderer);
|
---|
466 |
|
---|
467 | for (const SSpriteCall& sc : m_SpriteCalls)
|
---|
468 | pGUI.DrawSprite(sc.m_Sprite, canvas, sc.m_Area + pos);
|
---|
469 |
|
---|
470 | if (isClipped)
|
---|
471 | glDisable(GL_SCISSOR_TEST);
|
---|
472 | }
|
---|