[Gameplay] Reduce loom cost
[0ad.git] / source / gui / CGUIText.cpp
blobfbf8f23c4d94f21a5fb498c337ff2cd9fb4d1eb7
1 /* Copyright (C) 2023 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 "CGUIText.h"
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"
30 #include <cmath>
31 #include <optional>
33 extern int g_xres, g_yres;
35 // TODO Gee: CRect => CPoint ?
36 void SGenerateTextImage::SetupSpriteCall(
37 const bool left, CGUIText::SSpriteCall& spriteCall, const float width, const float y,
38 const CSize2D& size, const CStr& textureName, const float bufferZone)
40 // TODO Gee: Temp hardcoded values
41 spriteCall.m_Area.top = y + bufferZone;
42 spriteCall.m_Area.bottom = y + bufferZone + size.Height;
44 if (left)
46 spriteCall.m_Area.left = bufferZone;
47 spriteCall.m_Area.right = size.Width + bufferZone;
49 else
51 spriteCall.m_Area.left = width - bufferZone - size.Width;
52 spriteCall.m_Area.right = width - bufferZone;
55 spriteCall.m_Sprite = textureName;
57 m_YFrom = spriteCall.m_Area.top - bufferZone;
58 m_YTo = spriteCall.m_Area.bottom + bufferZone;
59 m_Indentation = size.Width + bufferZone * 2;
62 CGUIText::CGUIText(const CGUI& pGUI, const CGUIString& string, const CStrW& fontW, const float width, const float bufferZone, const EAlign align, const IGUIObject* pObject)
64 if (string.m_Words.empty())
65 return;
67 CStrIntern font(fontW.ToUTF8());
68 float y = bufferZone; // drawing pointer
69 float lineWidth = 0.f;
70 int from = 0;
72 bool firstLine = true; // Necessary because text in the first line is shorter
73 // (it doesn't count the line spacing)
75 // Images on the left or the right side.
76 SGenerateTextImages images;
77 int posLastImage = -1; // Position in the string where last img (either left or right) were encountered.
78 // in order to avoid duplicate processing.
80 // The calculated width of each word includes the space between the current
81 // word and the next. When we're wrapping, we need subtract the width of the
82 // space after the last word on the line before the wrap.
83 CFontMetrics currentFont(font);
84 float spaceWidth = currentFont.GetCharacterWidth(L' ');
86 // Go through string word by word.
87 // a word is defined as [start, end[ in string.m_Words so we skip the last item.
88 for (int i = 0; i < static_cast<int>(string.m_Words.size()) - 1; ++i)
90 // Pre-process each line one time, so we know which floating images
91 // will be added for that line.
93 // Generated stuff is stored in feedback.
94 CGUIString::SFeedback feedback;
96 // Preliminary line height, used for word-wrapping with floating images.
97 float prelimLineHeight = 0.f;
99 // Width and height of all text calls generated.
100 string.GenerateTextCall(pGUI, feedback, font, string.m_Words[i], string.m_Words[i+1], firstLine);
102 SetupSpriteCalls(pGUI, feedback.m_Images, y, width, bufferZone, i, posLastImage, images);
104 posLastImage = std::max(posLastImage, i);
106 lineWidth += feedback.m_Size.Width;
108 prelimLineHeight = std::max(prelimLineHeight, feedback.m_Size.Height);
110 float spaceCorrection = feedback.m_EndsWithSpace ? spaceWidth : 0.f;
112 // If width is 0, then there's no word-wrapping, disable NewLine.
113 if ((width != 0 && from != i && (lineWidth - spaceCorrection + 2 * bufferZone > width || feedback.m_NewLine)) || i == static_cast<int>(string.m_Words.size()) - 2)
115 if (ProcessLine(pGUI, string, font, pObject, images, align, prelimLineHeight, width, bufferZone, firstLine, y, i, from))
116 return;
117 lineWidth = 0.f;
122 // Loop through our images queues, to see if images have been added.
123 void CGUIText::SetupSpriteCalls(
124 const CGUI& pGUI,
125 const std::array<std::vector<CStr>, 2>& feedbackImages,
126 const float y,
127 const float width,
128 const float bufferZone,
129 const int i,
130 const int posLastImage,
131 SGenerateTextImages& images)
133 // Check if this has already been processed.
134 // Also, floating images are only applicable if Word-Wrapping is on
135 if (width == 0 || i <= posLastImage)
136 return;
138 // Loop left/right
139 for (int j = 0; j < 2; ++j)
140 for (const CStr& imgname : feedbackImages[j])
142 SSpriteCall spriteCall;
143 SGenerateTextImage image;
145 // Y is if no other floating images is above, y. Else it is placed
146 // after the last image, like a stack downwards.
147 float _y;
148 if (!images[j].empty())
149 _y = std::max(y, images[j].back().m_YTo);
150 else
151 _y = y;
153 const SGUIIcon& icon = pGUI.GetIcon(imgname);
154 image.SetupSpriteCall(j == CGUIString::SFeedback::Left, spriteCall, width, _y, icon.m_Size, icon.m_SpriteName, bufferZone);
156 // Check if image is the lowest thing.
157 m_Size.Height = std::max(m_Size.Height, image.m_YTo);
159 images[j].emplace_back(image);
160 m_SpriteCalls.emplace_back(std::move(spriteCall));
164 // Now we'll do another loop to figure out the height and width of
165 // the line (the height of the largest character and the width is
166 // the sum of all of the individual widths). This
167 // couldn't be determined in the first loop (main loop)
168 // because it didn't regard images, so we don't know
169 // if all characters processed, will actually be involved
170 // in that line.
171 void CGUIText::ComputeLineSize(
172 const CGUI& pGUI,
173 const CGUIString& string,
174 const CStrIntern& font,
175 const bool firstLine,
176 const float width,
177 const float widthRangeFrom,
178 const float widthRangeTo,
179 const int i,
180 const int tempFrom,
181 CSize2D& lineSize) const
183 // The calculated width of each word includes the space between the current
184 // word and the next. When we're wrapping, we need subtract the width of the
185 // space after the last word on the line before the wrap.
186 CFontMetrics currentFont(font);
187 float spaceWidth = currentFont.GetCharacterWidth(L' ');
189 float spaceCorrection = 0.f;
191 float x = widthRangeFrom;
192 for (int j = tempFrom; j <= i; ++j)
194 // We don't want to use feedback now, so we'll have to use another one.
195 CGUIString::SFeedback feedback2;
197 // Don't attach object, it'll suppress the errors
198 // we want them to be reported in the final GenerateTextCall()
199 // so that we don't get duplicates.
200 string.GenerateTextCall(pGUI, feedback2, font, string.m_Words[j], string.m_Words[j+1], firstLine);
202 // Append X value.
203 x += feedback2.m_Size.Width;
205 const float currentSpaceCorrection = feedback2.m_EndsWithSpace ? spaceWidth : 0.0f;
207 const bool isLineOverflow = x - currentSpaceCorrection > widthRangeTo;
208 if (width != 0 && isLineOverflow && j != tempFrom && !feedback2.m_NewLine)
209 break;
211 // Update after the line-break detection, because otherwise spaceCorrection above
212 // will refer to the wrapped word and not the last-word-before-the-line-break.
213 spaceCorrection = currentSpaceCorrection;
215 // Let lineSize.cy be the maximum m_Height we encounter.
216 lineSize.Height = std::max(lineSize.Height, feedback2.m_Size.Height);
218 // If the current word is an explicit new line ("\n"),
219 // break now before adding the width of this character.
220 // ("\n" doesn't have a glyph, thus is given the same width as
221 // the "missing glyph" character by CFont::GetCharacterWidth().)
222 if (width != 0 && feedback2.m_NewLine)
223 break;
225 lineSize.Width += feedback2.m_Size.Width;
227 // Remove the space if necessary.
228 lineSize.Width -= spaceCorrection;
231 bool CGUIText::ProcessLine(
232 const CGUI& pGUI,
233 const CGUIString& string,
234 const CStrIntern& font,
235 const IGUIObject* pObject,
236 const SGenerateTextImages& images,
237 const EAlign align,
238 const float prelimLineHeight,
239 const float width,
240 const float bufferZone,
241 bool& firstLine,
242 float& y,
243 int& i,
244 int& from)
246 // Change 'from' to 'i', but first keep a copy of its value.
247 int tempFrom = from;
248 from = i;
250 float widthRangeFrom = bufferZone;
251 float widthRangeTo = width - bufferZone;
252 ComputeLineRange(images, y, width, prelimLineHeight, widthRangeFrom, widthRangeTo);
254 CSize2D lineSize;
255 ComputeLineSize(pGUI, string, font, firstLine, width, widthRangeFrom, widthRangeTo, i, tempFrom, lineSize);
257 // Move down, because font drawing starts from the baseline
258 y += lineSize.Height;
260 // Do the real processing now
261 const bool done = AssembleCalls(pGUI, string, font, pObject, firstLine, width, widthRangeTo, GetLineOffset(align, widthRangeFrom, widthRangeTo, lineSize), y, tempFrom, i, from);
263 // Update dimensions
264 m_Size.Width = std::max(m_Size.Width, lineSize.Width + bufferZone * 2);
265 m_Size.Height = std::max(m_Size.Height, y + bufferZone);
267 firstLine = false;
269 // Now if we entered as from = i, then we want
270 // i being one minus that, so that it will become
271 // the same i in the next loop. The difference is that
272 // we're on a new line now.
273 i = from - 1;
275 return done;
278 // Decide width of the line. We need to iterate our floating images.
279 // this won't be exact because we're assuming the lineSize.cy
280 // will be as our preliminary calculation said. But that may change,
281 // although we'd have to add a couple of more loops to try straightening
282 // this problem out, and it is very unlikely to happen noticeably if one
283 // structures his text in a stylistically pure fashion. Even if not, it
284 // is still quite unlikely it will happen.
285 // Loop through left and right side, from and to.
286 void CGUIText::ComputeLineRange(
287 const SGenerateTextImages& images,
288 const float y,
289 const float width,
290 const float prelimLineHeight,
291 float& widthRangeFrom,
292 float& widthRangeTo) const
294 // Floating images are only applicable if word-wrapping is enabled.
295 if (width == 0)
296 return;
298 for (int j = 0; j < 2; ++j)
299 for (const SGenerateTextImage& img : images[j])
301 // We're working with two intervals here, the image's and the line height's.
302 // let's find the union of these two.
303 float unionFrom, unionTo;
305 unionFrom = std::max(y, img.m_YFrom);
306 unionTo = std::min(y + prelimLineHeight, img.m_YTo);
308 // The union is not empty
309 if (unionTo > unionFrom)
311 if (j == 0)
312 widthRangeFrom = std::max(widthRangeFrom, img.m_Indentation);
313 else
314 widthRangeTo = std::min(widthRangeTo, width - img.m_Indentation);
319 // compute offset based on what kind of alignment
320 float CGUIText::GetLineOffset(
321 const EAlign align,
322 const float widthRangeFrom,
323 const float widthRangeTo,
324 const CSize2D& lineSize) const
326 switch (align)
328 case EAlign::LEFT:
329 return widthRangeFrom;
331 case EAlign::CENTER:
332 return (widthRangeTo + widthRangeFrom - lineSize.Width) / 2;
334 case EAlign::RIGHT:
335 return widthRangeTo - lineSize.Width;
337 default:
338 debug_warn(L"Broken EAlign in CGUIText()");
339 return 0.f;
343 bool CGUIText::AssembleCalls(
344 const CGUI& pGUI,
345 const CGUIString& string,
346 const CStrIntern& font,
347 const IGUIObject* pObject,
348 const bool firstLine,
349 const float width,
350 const float widthRangeTo,
351 const float dx,
352 const float y,
353 const int tempFrom,
354 const int i,
355 int& from)
357 bool done = false;
358 float x = dx;
360 for (int j = tempFrom; j <= i; ++j)
362 // We don't want to use feedback now, so we'll have to use another one.
363 CGUIString::SFeedback feedback2;
365 // Defaults
366 string.GenerateTextCall(pGUI, feedback2, font, string.m_Words[j], string.m_Words[j+1], firstLine, pObject);
368 // Iterate all and set X/Y values
369 // Since X values are not set, we need to make an internal
370 // iteration with an increment that will append the internal
371 // x, that is what xPointer is for.
372 float xPointer = 0.f;
374 for (STextCall& tc : feedback2.m_TextCalls)
376 tc.m_Pos = CVector2D(x + xPointer, y);
378 xPointer += tc.m_Size.Width;
380 if (tc.m_pSpriteCall)
381 tc.m_pSpriteCall->m_Area += tc.m_Pos - CSize2D(0, tc.m_pSpriteCall->m_Area.GetHeight());
384 x += feedback2.m_Size.Width;
386 if (width != 0) // only if word-wrapping is applicable
388 // Check if we need to wrap, using the same algorithm as ComputeLineSize
389 // This means we must ignore the 'space before the next word' for the purposes of wrapping.
390 CFontMetrics currentFont(font);
391 float spaceWidth = currentFont.GetCharacterWidth(L' ');
392 float spaceCorrection = feedback2.m_EndsWithSpace ? spaceWidth : 0.f;
394 if (feedback2.m_NewLine)
396 from = j + 1;
398 // Sprite call can exist within only a newline segment,
399 // therefore we need this.
400 if (!feedback2.m_SpriteCalls.empty())
402 auto newEnd = std::remove_if(feedback2.m_TextCalls.begin(), feedback2.m_TextCalls.end(), [](const STextCall& call) { return !call.m_pSpriteCall; });
403 m_TextCalls.insert(
404 m_TextCalls.end(),
405 std::make_move_iterator(feedback2.m_TextCalls.begin()),
406 std::make_move_iterator(newEnd));
407 m_SpriteCalls.insert(
408 m_SpriteCalls.end(),
409 std::make_move_iterator(feedback2.m_SpriteCalls.begin()),
410 std::make_move_iterator(feedback2.m_SpriteCalls.end()));
412 break;
414 else if (x - spaceCorrection > widthRangeTo && j == tempFrom)
416 // The first word overrides the width limit, what we do,
417 // in those cases, is just drawing that word even
418 // though it'll extend the object.
419 // Ergo: do not break, since we want it to be added to m_TextCalls.
420 from = j+1;
421 // To avoid doing redundant computations, set up j to exit the loop right away.
422 j = i + 1;
424 else if (x - spaceCorrection > widthRangeTo)
426 from = j;
427 break;
431 // Add the whole feedback2.m_TextCalls to our m_TextCalls.
432 m_TextCalls.insert(
433 m_TextCalls.end(),
434 std::make_move_iterator(feedback2.m_TextCalls.begin()),
435 std::make_move_iterator(feedback2.m_TextCalls.end()));
437 m_SpriteCalls.insert(
438 m_SpriteCalls.end(),
439 std::make_move_iterator(feedback2.m_SpriteCalls.begin()),
440 std::make_move_iterator(feedback2.m_SpriteCalls.end()));
442 if (j == static_cast<int>(string.m_Words.size()) - 2)
443 done = true;
446 return done;
449 void CGUIText::Draw(CGUI& pGUI, CCanvas2D& canvas, const CGUIColor& DefaultColor, const CVector2D& pos, CRect clipping) const
451 const bool isClipped = clipping != CRect();
452 std::optional<CCanvas2D::ScopedScissor> scopedScissor;
453 if (isClipped)
455 if (clipping.GetWidth() <= 0.0f || clipping.GetHeight() <= 0.0f)
456 return;
457 scopedScissor.emplace(canvas, clipping);
460 CTextRenderer textRenderer;
461 textRenderer.SetClippingRect(clipping);
462 textRenderer.Translate(0.0f, 0.0f);
464 for (const STextCall& tc : m_TextCalls)
466 // If this is just a placeholder for a sprite call, continue
467 if (tc.m_pSpriteCall)
468 continue;
470 textRenderer.SetCurrentColor(tc.m_UseCustomColor ? tc.m_Color : DefaultColor);
471 textRenderer.SetCurrentFont(tc.m_Font);
472 textRenderer.Put(floorf(pos.X + tc.m_Pos.X), floorf(pos.Y + tc.m_Pos.Y), &tc.m_String);
475 canvas.DrawText(textRenderer);
477 for (const SSpriteCall& sc : m_SpriteCalls)
478 pGUI.DrawSprite(sc.m_Sprite, canvas, sc.m_Area + pos);