Remove three deprecated OpenGL extensions.
[0ad.git] / source / gui / CGUIText.cpp
blobea25f89224dc031d5d64baf1a0967e9438565a52
1 /* Copyright (C) 2022 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"
29 #include "ps/VideoMode.h"
30 #include "renderer/backend/gl/DeviceCommandContext.h"
31 #include "renderer/Renderer.h"
33 #include <math.h>
35 extern int g_xres, g_yres;
37 // TODO Gee: CRect => CPoint ?
38 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)
42 // TODO Gee: Temp hardcoded values
43 spriteCall.m_Area.top = y + bufferZone;
44 spriteCall.m_Area.bottom = y + bufferZone + size.Height;
46 if (left)
48 spriteCall.m_Area.left = bufferZone;
49 spriteCall.m_Area.right = size.Width + bufferZone;
51 else
53 spriteCall.m_Area.left = width - bufferZone - size.Width;
54 spriteCall.m_Area.right = width - bufferZone;
57 spriteCall.m_Sprite = textureName;
59 m_YFrom = spriteCall.m_Area.top - bufferZone;
60 m_YTo = spriteCall.m_Area.bottom + bufferZone;
61 m_Indentation = size.Width + bufferZone * 2;
64 CGUIText::CGUIText(const CGUI& pGUI, const CGUIString& string, const CStrW& fontW, const float width, const float bufferZone, const EAlign align, const IGUIObject* pObject)
66 if (string.m_Words.empty())
67 return;
69 CStrIntern font(fontW.ToUTF8());
70 float y = bufferZone; // drawing pointer
71 int from = 0;
73 bool firstLine = true; // Necessary because text in the first line is shorter
74 // (it doesn't count the line spacing)
76 // Images on the left or the right side.
77 SGenerateTextImages images;
78 int posLastImage = -1; // Position in the string where last img (either left or right) were encountered.
79 // in order to avoid duplicate processing.
81 // Go through string word by word
82 for (int i = 0; i < static_cast<int>(string.m_Words.size()) - 1; ++i)
84 // Pre-process each line one time, so we know which floating images
85 // will be added for that line.
87 // Generated stuff is stored in feedback.
88 CGUIString::SFeedback feedback;
90 // Preliminary line height, used for word-wrapping with floating images.
91 float prelimLineHeight = 0.f;
93 // Width and height of all text calls generated.
94 string.GenerateTextCall(pGUI, feedback, font, string.m_Words[i], string.m_Words[i+1], firstLine);
96 SetupSpriteCalls(pGUI, feedback.m_Images, y, width, bufferZone, i, posLastImage, images);
98 posLastImage = std::max(posLastImage, i);
100 prelimLineHeight = std::max(prelimLineHeight, feedback.m_Size.Height);
102 // If width is 0, then there's no word-wrapping, disable NewLine.
103 if (((width != 0 && (feedback.m_Size.Width + 2 * bufferZone > width || feedback.m_NewLine)) || i == static_cast<int>(string.m_Words.size()) - 2) &&
104 ProcessLine(pGUI, string, font, pObject, images, align, prelimLineHeight, width, bufferZone, firstLine, y, i, from))
105 return;
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 posLastImage,
118 SGenerateTextImages& images)
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 <= posLastImage)
123 return;
125 // Loop left/right
126 for (int j = 0; j < 2; ++j)
127 for (const CStr& imgname : feedbackImages[j])
129 SSpriteCall spriteCall;
130 SGenerateTextImage image;
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;
140 const SGUIIcon& icon = pGUI.GetIcon(imgname);
141 image.SetupSpriteCall(j == CGUIString::SFeedback::Left, spriteCall, width, _y, icon.m_Size, icon.m_SpriteName, bufferZone);
143 // Check if image is the lowest thing.
144 m_Size.Height = std::max(m_Size.Height, image.m_YTo);
146 images[j].emplace_back(image);
147 m_SpriteCalls.emplace_back(std::move(spriteCall));
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 widthRangeFrom,
165 const float widthRangeTo,
166 const int i,
167 const int tempFrom,
168 CSize2D& lineSize) const
170 float x = widthRangeFrom;
171 for (int j = tempFrom; j <= i; ++j)
173 // We don't want to use feedback now, so we'll have to use another one.
174 CGUIString::SFeedback feedback2;
176 // Don't attach object, it'll suppress the errors
177 // we want them to be reported in the final GenerateTextCall()
178 // so that we don't get duplicates.
179 string.GenerateTextCall(pGUI, feedback2, font, string.m_Words[j], string.m_Words[j+1], firstLine);
181 // Append X value.
182 x += feedback2.m_Size.Width;
184 if (width != 0 && x > widthRangeTo && j != tempFrom && !feedback2.m_NewLine)
186 // The calculated width of each word includes the space between the current
187 // word and the next. When we're wrapping, we need subtract the width of the
188 // space after the last word on the line before the wrap.
189 CFontMetrics currentFont(font);
190 lineSize.Width -= currentFont.GetCharacterWidth(*L" ");
191 break;
194 // Let lineSize.cy be the maximum m_Height we encounter.
195 lineSize.Height = std::max(lineSize.Height, feedback2.m_Size.Height);
197 // If the current word is an explicit new line ("\n"),
198 // break now before adding the width of this character.
199 // ("\n" doesn't have a glyph, thus is given the same width as
200 // the "missing glyph" character by CFont::GetCharacterWidth().)
201 if (width != 0 && feedback2.m_NewLine)
202 break;
204 lineSize.Width += feedback2.m_Size.Width;
208 bool CGUIText::ProcessLine(
209 const CGUI& pGUI,
210 const CGUIString& string,
211 const CStrIntern& font,
212 const IGUIObject* pObject,
213 const SGenerateTextImages& images,
214 const EAlign align,
215 const float prelimLineHeight,
216 const float width,
217 const float bufferZone,
218 bool& firstLine,
219 float& y,
220 int& i,
221 int& from)
223 // Change 'from' to 'i', but first keep a copy of its value.
224 int tempFrom = from;
225 from = i;
227 float widthRangeFrom = bufferZone;
228 float widthRangeTo = width - bufferZone;
229 ComputeLineRange(images, y, width, prelimLineHeight, widthRangeFrom, widthRangeTo);
231 CSize2D lineSize;
232 ComputeLineSize(pGUI, string, font, firstLine, width, widthRangeFrom, widthRangeTo, i, tempFrom, lineSize);
234 // Move down, because font drawing starts from the baseline
235 y += lineSize.Height;
237 // Do the real processing now
238 const bool done = AssembleCalls(pGUI, string, font, pObject, firstLine, width, widthRangeTo, GetLineOffset(align, widthRangeFrom, widthRangeTo, lineSize), y, tempFrom, i, from);
240 // Update dimensions
241 m_Size.Width = std::max(m_Size.Width, lineSize.Width + bufferZone * 2);
242 m_Size.Height = std::max(m_Size.Height, y + bufferZone);
244 firstLine = false;
246 // Now if we entered as from = i, then we want
247 // i being one minus that, so that it will become
248 // the same i in the next loop. The difference is that
249 // we're on a new line now.
250 i = from - 1;
252 return done;
255 // Decide width of the line. We need to iterate our floating images.
256 // this won't be exact because we're assuming the lineSize.cy
257 // will be as our preliminary calculation said. But that may change,
258 // although we'd have to add a couple of more loops to try straightening
259 // this problem out, and it is very unlikely to happen noticeably if one
260 // structures his text in a stylistically pure fashion. Even if not, it
261 // is still quite unlikely it will happen.
262 // Loop through left and right side, from and to.
263 void CGUIText::ComputeLineRange(
264 const SGenerateTextImages& images,
265 const float y,
266 const float width,
267 const float prelimLineHeight,
268 float& widthRangeFrom,
269 float& widthRangeTo) const
271 // Floating images are only applicable if word-wrapping is enabled.
272 if (width == 0)
273 return;
275 for (int j = 0; j < 2; ++j)
276 for (const SGenerateTextImage& img : images[j])
278 // We're working with two intervals here, the image's and the line height's.
279 // let's find the union of these two.
280 float unionFrom, unionTo;
282 unionFrom = std::max(y, img.m_YFrom);
283 unionTo = std::min(y + prelimLineHeight, img.m_YTo);
285 // The union is not empty
286 if (unionTo > unionFrom)
288 if (j == 0)
289 widthRangeFrom = std::max(widthRangeFrom, img.m_Indentation);
290 else
291 widthRangeTo = std::min(widthRangeTo, width - img.m_Indentation);
296 // compute offset based on what kind of alignment
297 float CGUIText::GetLineOffset(
298 const EAlign align,
299 const float widthRangeFrom,
300 const float widthRangeTo,
301 const CSize2D& lineSize) const
303 switch (align)
305 case EAlign::LEFT:
306 return widthRangeFrom;
308 case EAlign::CENTER:
309 return (widthRangeTo + widthRangeFrom - lineSize.Width) / 2;
311 case EAlign::RIGHT:
312 return widthRangeTo - lineSize.Width;
314 default:
315 debug_warn(L"Broken EAlign in CGUIText()");
316 return 0.f;
320 bool CGUIText::AssembleCalls(
321 const CGUI& pGUI,
322 const CGUIString& string,
323 const CStrIntern& font,
324 const IGUIObject* pObject,
325 const bool firstLine,
326 const float width,
327 const float widthRangeTo,
328 const float dx,
329 const float y,
330 const int tempFrom,
331 const int i,
332 int& from)
334 bool done = false;
335 float x = 0.f;
337 for (int j = tempFrom; j <= i; ++j)
339 // We don't want to use feedback now, so we'll have to use another one.
340 CGUIString::SFeedback feedback2;
342 // Defaults
343 string.GenerateTextCall(pGUI, feedback2, font, string.m_Words[j], string.m_Words[j+1], firstLine, pObject);
345 // Iterate all and set X/Y values
346 // Since X values are not set, we need to make an internal
347 // iteration with an increment that will append the internal
348 // x, that is what xPointer is for.
349 float xPointer = 0.f;
351 for (STextCall& tc : feedback2.m_TextCalls)
353 tc.m_Pos = CVector2D(dx + x + xPointer, y);
355 xPointer += tc.m_Size.Width;
357 if (tc.m_pSpriteCall)
358 tc.m_pSpriteCall->m_Area += tc.m_Pos - CSize2D(0, tc.m_pSpriteCall->m_Area.GetHeight());
361 // Append X value.
362 x += feedback2.m_Size.Width;
364 // The first word overrides the width limit, what we
365 // do, in those cases, are just drawing that word even
366 // though it'll extend the object.
367 if (width != 0) // only if word-wrapping is applicable
369 if (feedback2.m_NewLine)
371 from = j + 1;
373 // Sprite call can exist within only a newline segment,
374 // therefore we need this.
375 if (!feedback2.m_SpriteCalls.empty())
377 auto newEnd = std::remove_if(feedback2.m_TextCalls.begin(), feedback2.m_TextCalls.end(), [](const STextCall& call) { return !call.m_pSpriteCall; });
378 m_TextCalls.insert(
379 m_TextCalls.end(),
380 std::make_move_iterator(feedback2.m_TextCalls.begin()),
381 std::make_move_iterator(newEnd));
382 m_SpriteCalls.insert(
383 m_SpriteCalls.end(),
384 std::make_move_iterator(feedback2.m_SpriteCalls.begin()),
385 std::make_move_iterator(feedback2.m_SpriteCalls.end()));
387 break;
389 else if (x > widthRangeTo && j == tempFrom)
391 from = j+1;
392 // do not break, since we want it to be added to m_TextCalls
394 else if (x > widthRangeTo)
396 from = j;
397 break;
401 // Add the whole feedback2.m_TextCalls to our m_TextCalls.
402 m_TextCalls.insert(
403 m_TextCalls.end(),
404 std::make_move_iterator(feedback2.m_TextCalls.begin()),
405 std::make_move_iterator(feedback2.m_TextCalls.end()));
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 if (j == static_cast<int>(string.m_Words.size()) - 2)
413 done = true;
416 return done;
419 void CGUIText::Draw(CGUI& pGUI, CCanvas2D& canvas, const CGUIColor& DefaultColor, const CVector2D& pos, CRect clipping) const
421 Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext =
422 g_Renderer.GetDeviceCommandContext();
424 bool isClipped = clipping != CRect();
425 if (isClipped)
427 // Make clipping rect as small as possible to prevent rounding errors
428 clipping.top = std::ceil(clipping.top);
429 clipping.bottom = std::floor(clipping.bottom);
430 clipping.left = std::ceil(clipping.left);
431 clipping.right = std::floor(clipping.right);
433 const float scale = g_VideoMode.GetScale();
434 Renderer::Backend::GL::CDeviceCommandContext::Rect scissorRect;
435 scissorRect.x = std::ceil(clipping.left * scale);
436 scissorRect.y = std::ceil(g_yres - clipping.bottom * scale);
437 scissorRect.width = std::floor(clipping.GetWidth() * scale);
438 scissorRect.height = std::floor(clipping.GetHeight() * scale);
439 // TODO: move scissors to CCanvas2D.
440 deviceCommandContext->SetScissors(1, &scissorRect);
443 CTextRenderer textRenderer;
444 textRenderer.SetClippingRect(clipping);
445 textRenderer.Translate(0.0f, 0.0f);
447 for (const STextCall& tc : m_TextCalls)
449 // If this is just a placeholder for a sprite call, continue
450 if (tc.m_pSpriteCall)
451 continue;
453 textRenderer.SetCurrentColor(tc.m_UseCustomColor ? tc.m_Color : DefaultColor);
454 textRenderer.SetCurrentFont(tc.m_Font);
455 textRenderer.Put(floorf(pos.X + tc.m_Pos.X), floorf(pos.Y + tc.m_Pos.Y), &tc.m_String);
458 canvas.DrawText(textRenderer);
460 for (const SSpriteCall& sc : m_SpriteCalls)
461 pGUI.DrawSprite(sc.m_Sprite, canvas, sc.m_Area + pos);
463 if (isClipped)
464 deviceCommandContext->SetScissors(0, nullptr);