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"
22 #include "graphics/Canvas2D.h"
23 #include "graphics/FontMetrics.h"
24 #include "graphics/TextRenderer.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"
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
;
48 spriteCall
.m_Area
.left
= bufferZone
;
49 spriteCall
.m_Area
.right
= size
.Width
+ bufferZone
;
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())
69 CStrIntern
font(fontW
.ToUTF8());
70 float y
= bufferZone
; // drawing pointer
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
))
109 // Loop through our images queues, to see if images have been added.
110 void CGUIText::SetupSpriteCalls(
112 const std::array
<std::vector
<CStr
>, 2>& feedbackImages
,
115 const float bufferZone
,
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
)
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.
135 if (!images
[j
].empty())
136 _y
= std::max(y
, images
[j
].back().m_YTo
);
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
158 void CGUIText::ComputeLineSize(
160 const CGUIString
& string
,
161 const CStrIntern
& font
,
162 const bool firstLine
,
164 const float widthRangeFrom
,
165 const float widthRangeTo
,
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
);
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
" ");
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
)
204 lineSize
.Width
+= feedback2
.m_Size
.Width
;
208 bool CGUIText::ProcessLine(
210 const CGUIString
& string
,
211 const CStrIntern
& font
,
212 const IGUIObject
* pObject
,
213 const SGenerateTextImages
& images
,
215 const float prelimLineHeight
,
217 const float bufferZone
,
223 // Change 'from' to 'i', but first keep a copy of its value.
227 float widthRangeFrom
= bufferZone
;
228 float widthRangeTo
= width
- bufferZone
;
229 ComputeLineRange(images
, y
, width
, prelimLineHeight
, widthRangeFrom
, widthRangeTo
);
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
);
241 m_Size
.Width
= std::max(m_Size
.Width
, lineSize
.Width
+ bufferZone
* 2);
242 m_Size
.Height
= std::max(m_Size
.Height
, y
+ bufferZone
);
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.
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
,
267 const float prelimLineHeight
,
268 float& widthRangeFrom
,
269 float& widthRangeTo
) const
271 // Floating images are only applicable if word-wrapping is enabled.
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
)
289 widthRangeFrom
= std::max(widthRangeFrom
, img
.m_Indentation
);
291 widthRangeTo
= std::min(widthRangeTo
, width
- img
.m_Indentation
);
296 // compute offset based on what kind of alignment
297 float CGUIText::GetLineOffset(
299 const float widthRangeFrom
,
300 const float widthRangeTo
,
301 const CSize2D
& lineSize
) const
306 return widthRangeFrom
;
309 return (widthRangeTo
+ widthRangeFrom
- lineSize
.Width
) / 2;
312 return widthRangeTo
- lineSize
.Width
;
315 debug_warn(L
"Broken EAlign in CGUIText()");
320 bool CGUIText::AssembleCalls(
322 const CGUIString
& string
,
323 const CStrIntern
& font
,
324 const IGUIObject
* pObject
,
325 const bool firstLine
,
327 const float widthRangeTo
,
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
;
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());
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
)
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
; });
380 std::make_move_iterator(feedback2
.m_TextCalls
.begin()),
381 std::make_move_iterator(newEnd
));
382 m_SpriteCalls
.insert(
384 std::make_move_iterator(feedback2
.m_SpriteCalls
.begin()),
385 std::make_move_iterator(feedback2
.m_SpriteCalls
.end()));
389 else if (x
> widthRangeTo
&& j
== tempFrom
)
392 // do not break, since we want it to be added to m_TextCalls
394 else if (x
> widthRangeTo
)
401 // Add the whole feedback2.m_TextCalls to our m_TextCalls.
404 std::make_move_iterator(feedback2
.m_TextCalls
.begin()),
405 std::make_move_iterator(feedback2
.m_TextCalls
.end()));
407 m_SpriteCalls
.insert(
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)
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();
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
)
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
);
464 deviceCommandContext
->SetScissors(0, nullptr);