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