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"
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"
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
;
46 spriteCall
.m_Area
.left
= bufferZone
;
47 spriteCall
.m_Area
.right
= size
.Width
+ bufferZone
;
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())
67 CStrIntern
font(fontW
.ToUTF8());
68 float y
= bufferZone
; // drawing pointer
69 float lineWidth
= 0.f
;
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
))
122 // Loop through our images queues, to see if images have been added.
123 void CGUIText::SetupSpriteCalls(
125 const std::array
<std::vector
<CStr
>, 2>& feedbackImages
,
128 const float bufferZone
,
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
)
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.
148 if (!images
[j
].empty())
149 _y
= std::max(y
, images
[j
].back().m_YTo
);
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
171 void CGUIText::ComputeLineSize(
173 const CGUIString
& string
,
174 const CStrIntern
& font
,
175 const bool firstLine
,
177 const float widthRangeFrom
,
178 const float widthRangeTo
,
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
);
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
)
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
)
225 lineSize
.Width
+= feedback2
.m_Size
.Width
;
227 // Remove the space if necessary.
228 lineSize
.Width
-= spaceCorrection
;
231 bool CGUIText::ProcessLine(
233 const CGUIString
& string
,
234 const CStrIntern
& font
,
235 const IGUIObject
* pObject
,
236 const SGenerateTextImages
& images
,
238 const float prelimLineHeight
,
240 const float bufferZone
,
246 // Change 'from' to 'i', but first keep a copy of its value.
250 float widthRangeFrom
= bufferZone
;
251 float widthRangeTo
= width
- bufferZone
;
252 ComputeLineRange(images
, y
, width
, prelimLineHeight
, widthRangeFrom
, widthRangeTo
);
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
);
264 m_Size
.Width
= std::max(m_Size
.Width
, lineSize
.Width
+ bufferZone
* 2);
265 m_Size
.Height
= std::max(m_Size
.Height
, y
+ bufferZone
);
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.
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
,
290 const float prelimLineHeight
,
291 float& widthRangeFrom
,
292 float& widthRangeTo
) const
294 // Floating images are only applicable if word-wrapping is enabled.
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
)
312 widthRangeFrom
= std::max(widthRangeFrom
, img
.m_Indentation
);
314 widthRangeTo
= std::min(widthRangeTo
, width
- img
.m_Indentation
);
319 // compute offset based on what kind of alignment
320 float CGUIText::GetLineOffset(
322 const float widthRangeFrom
,
323 const float widthRangeTo
,
324 const CSize2D
& lineSize
) const
329 return widthRangeFrom
;
332 return (widthRangeTo
+ widthRangeFrom
- lineSize
.Width
) / 2;
335 return widthRangeTo
- lineSize
.Width
;
338 debug_warn(L
"Broken EAlign in CGUIText()");
343 bool CGUIText::AssembleCalls(
345 const CGUIString
& string
,
346 const CStrIntern
& font
,
347 const IGUIObject
* pObject
,
348 const bool firstLine
,
350 const float widthRangeTo
,
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
;
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
)
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
; });
405 std::make_move_iterator(feedback2
.m_TextCalls
.begin()),
406 std::make_move_iterator(newEnd
));
407 m_SpriteCalls
.insert(
409 std::make_move_iterator(feedback2
.m_SpriteCalls
.begin()),
410 std::make_move_iterator(feedback2
.m_SpriteCalls
.end()));
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.
421 // To avoid doing redundant computations, set up j to exit the loop right away.
424 else if (x
- spaceCorrection
> widthRangeTo
)
431 // Add the whole feedback2.m_TextCalls to our m_TextCalls.
434 std::make_move_iterator(feedback2
.m_TextCalls
.begin()),
435 std::make_move_iterator(feedback2
.m_TextCalls
.end()));
437 m_SpriteCalls
.insert(
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)
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
;
455 if (clipping
.GetWidth() <= 0.0f
|| clipping
.GetHeight() <= 0.0f
)
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
)
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
);