• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Return-To-The-Roots / s25client / 13471763342

22 Feb 2025 09:43AM UTC coverage: 50.319% (-0.007%) from 50.326%
13471763342

Pull #1726

github

web-flow
Merge 2bee78282 into 541bc230f
Pull Request #1726: Don't use vendored dependencies in "dev-tools" (by default) if RTTR_USE_SYSTEM_LIBS is set

22365 of 44446 relevant lines covered (50.32%)

36473.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.68
/libs/s25main/ogl/glFont.cpp
1
// Copyright (C) 2005 - 2021 Settlers Freaks (sf-team at siedler25.org)
2
//
3
// SPDX-License-Identifier: GPL-2.0-or-later
4

5
#include "glFont.h"
6
#include "FontStyle.h"
7
#include "Loader.h"
8
#include "drivers/VideoDriverWrapper.h"
9
#include "glArchivItem_Bitmap_Raw.h"
10
#include "helpers/containerUtils.h"
11
#include "libsiedler2/ArchivItem_Bitmap_Player.h"
12
#include "libsiedler2/ArchivItem_Font.h"
13
#include "libsiedler2/PixelBufferBGRA.h"
14
#include "libsiedler2/libsiedler2.h"
15
#include "s25util/utf8.h"
16
#include <boost/algorithm/string.hpp>
17
#include <boost/nowide/detail/utf.hpp>
18
#include <cmath>
19
#include <vector>
20

21
constexpr bool RTTR_PRINT_FONTS = false;
22
namespace utf = boost::nowide::detail::utf;
23
using utf8 = utf::utf_traits<char>;
24

25
//////////////////////////////////////////////////////////////////////////
26

27
glFont::glFont(const libsiedler2::ArchivItem_Font& font) : maxCharSize(font.getDx(), font.getDy()), asciiMapping{}
16✔
28
{
29
    fontWithOutline = std::make_unique<glArchivItem_Bitmap_Raw>();
16✔
30
    fontNoOutline = std::make_unique<glArchivItem_Bitmap_Raw>();
16✔
31

32
    // first, we have to find how much chars we really have
33
    unsigned numChars = 0;
16✔
34
    for(unsigned i = 0; i < font.size(); ++i)
200,366✔
35
    {
36
        if(font[i])
200,350✔
37
            ++numChars;
66,619✔
38
    }
39

40
    if(numChars == 0)
16✔
41
        return;
×
42

43
    const auto numCharsPerLine = static_cast<unsigned>(std::sqrt(static_cast<double>(numChars)));
16✔
44
    // Calc lines required (rounding up)
45
    const unsigned numLines = (numChars + numCharsPerLine - 1) / numCharsPerLine;
16✔
46

47
    constexpr Extent spacing(1, 1);
16✔
48
    Extent texSize = (maxCharSize + spacing * 2u) * Extent(numCharsPerLine, numLines) + spacing * 2u;
16✔
49
    libsiedler2::PixelBufferBGRA bufferWithOutline(texSize.x, texSize.y);
80✔
50
    libsiedler2::PixelBufferBGRA bufferNoOutline(texSize.x, texSize.y);
80✔
51

52
    const libsiedler2::ArchivItem_Palette* const palette = LOADER.GetPaletteN("colors");
32✔
53
    Position curPos(spacing);
16✔
54
    numChars = 0;
16✔
55
    for(unsigned i = 0; i < font.size(); ++i)
200,366✔
56
    {
57
        const auto* c = dynamic_cast<const libsiedler2::ArchivItem_Bitmap_Player*>(font[i]);
200,350✔
58
        if(!c)
200,350✔
59
            continue;
133,731✔
60

61
        if((numChars % numCharsPerLine) == 0 && numChars > 0)
66,619✔
62
        {
63
            curPos.y += maxCharSize.y + spacing.y * 2;
585✔
64
            curPos.x = spacing.x;
585✔
65
        }
66

67
        // Spezialpalette (blaue Spielerfarben sind Grau) verwenden, damit man per OpenGL einfärben kann!
68
        c->print(bufferNoOutline, palette, 128, curPos.x, curPos.y, 0, 0, 0, 0, true);
66,619✔
69
        c->print(bufferWithOutline, palette, 128, curPos.x, curPos.y);
66,619✔
70

71
        CharInfo ci(curPos, std::min<unsigned short>(maxCharSize.x + 2, c->getWidth()));
66,619✔
72

73
        AddCharInfo(i, ci);
66,619✔
74
        curPos.x += maxCharSize.x + spacing.x * 2;
66,619✔
75
        ++numChars;
66,619✔
76
    }
77

78
    fontNoOutline->create(bufferNoOutline);
16✔
79
    fontWithOutline->create(bufferWithOutline);
16✔
80

81
    // Set the placeholder for non-existent glyphs. Use '?' (should always be possible)
82
    if(CharExist(0xFFFD))
16✔
83
        placeHolder = GetCharInfo(0xFFFD);
3✔
84
    else if(CharExist('?'))
13✔
85
        placeHolder = GetCharInfo('?');
13✔
86
    else
87
        throw std::runtime_error("Cannot find '?' glyph in font. What shall I use as fallback?");
×
88

89
    if(RTTR_PRINT_FONTS)
90
    {
91
        libsiedler2::Archiv items;
92
        items.pushC(*fontNoOutline);
93
        libsiedler2::Write("font" + font.getName() + "_noOutline.bmp", items);
94
        items.setC(0, *fontWithOutline);
95
        libsiedler2::Write("font" + font.getName() + "_Outline.bmp", items);
96
    }
97
}
98

99
bool glFont::CharExist(char32_t c) const
59✔
100
{
101
    if(c < asciiMapping.size())
59✔
102
        return asciiMapping[c].first;
27✔
103
    return helpers::contains(utf8_mapping, c);
32✔
104
}
105

106
const glFont::CharInfo& glFont::GetCharInfo(char32_t c) const
28,959✔
107
{
108
    if(c < asciiMapping.size())
28,959✔
109
    {
110
        if(asciiMapping[c].first)
15,309✔
111
            return asciiMapping[c].second;
15,309✔
112
    } else
113
    {
114
        auto it = utf8_mapping.find(c);
13,650✔
115
        if(it != utf8_mapping.end())
13,650✔
116
            return it->second;
13,650✔
117
    }
118
    return placeHolder;
×
119
}
120

121
void glFont::AddCharInfo(char32_t c, const CharInfo& info)
66,619✔
122
{
123
    if(c < asciiMapping.size())
66,619✔
124
        asciiMapping[c] = std::make_pair(true, info);
2,407✔
125
    else
126
        utf8_mapping[c] = info;
64,212✔
127
}
66,619✔
128

129
/**
130
 *  @brief fügt ein einzelnes Zeichen zur Zeichenliste hinzu
131
 */
132
inline void glFont::DrawChar(char32_t curChar, VertexArrays& vertices, DrawPoint& curPos) const
685✔
133
{
134
    CharInfo ci = GetCharInfo(curChar);
685✔
135

136
    GlPoint texCoord1(ci.pos);
685✔
137
    GlPoint texCoord2(ci.pos + DrawPoint(ci.width, maxCharSize.y));
685✔
138

139
    vertices.texCoords.push_back(texCoord1);
685✔
140
    vertices.texCoords.push_back(GlPoint(texCoord1.x, texCoord2.y));
685✔
141
    vertices.texCoords.push_back(texCoord2);
685✔
142
    vertices.texCoords.push_back(GlPoint(texCoord2.x, texCoord1.y));
685✔
143

144
    GlPoint curPos1(curPos);
685✔
145
    GlPoint curPos2(curPos + DrawPoint(ci.width, maxCharSize.y));
685✔
146

147
    vertices.vertices.push_back(curPos1);
685✔
148
    vertices.vertices.push_back(GlPoint(curPos1.x, curPos2.y));
685✔
149
    vertices.vertices.push_back(curPos2);
685✔
150
    vertices.vertices.push_back(GlPoint(curPos2.x, curPos1.y));
685✔
151

152
    curPos.x += ci.width;
685✔
153
}
685✔
154

155
/**
156
 *  Zeichnet einen Text.
157
 *
158
 *  @param[in] x      X-Koordinate
159
 *  @param[in] y      Y-Koordinate
160
 *  @param[in] text   Der Text
161
 *  @param[in] format Format des Textes (verodern)
162
 *                      @p FontStyle::LEFT    - Text links ( standard )
163
 *                      @p FontStyle::CENTER  - Text mittig
164
 *                      @p FontStyle::RIGHT   - Text rechts
165
 *                      @p FontStyle::TOP     - Text oben ( standard )
166
 *                      @p FontStyle::VCENTER - Text vertikal zentriert
167
 *                      @p FontStyle::BOTTOM  - Text unten
168
 *  @param[in] color    Farbe des Textes
169
 *  @param[in] maxWidth maximale Länge
170
 *  @param     end      Suffix for displaying a truncation of the text (...)
171
 */
172
void glFont::Draw(DrawPoint pos, const std::string& text, FontStyle format, unsigned color, unsigned short maxWidth,
115✔
173
                  const std::string& end) const
174
{
175
    RTTR_Assert(s25util::isValidUTF8(text));
115✔
176

177
    unsigned maxNumChars;
178
    unsigned short textWidth;
179
    bool drawEnd;
180
    if(maxWidth == 0xFFFF)
115✔
181
    {
182
        maxNumChars = text.size();
95✔
183
        textWidth = getWidth(text);
95✔
184
        drawEnd = false;
95✔
185
    } else
186
    {
187
        RTTR_Assert(s25util::isValidUTF8(end));
20✔
188
        textWidth = getWidth(text, maxWidth, &maxNumChars);
20✔
189
        if(!end.empty() && maxNumChars < text.size())
20✔
190
        {
191
            unsigned short endWidth = getWidth(end);
1✔
192

193
            // If "end" does not fit, draw nothing
194
            if(textWidth < endWidth)
1✔
195
                return;
1✔
196

197
            // Wieviele Buchstaben gehen in den "Rest" (ohne "end")
198
            textWidth = getWidth(text, textWidth - endWidth, &maxNumChars) + endWidth;
1✔
199
            drawEnd = true;
1✔
200
        } else
201
            drawEnd = false;
19✔
202
    }
203

204
    if(maxNumChars == 0)
115✔
205
        return;
1✔
206
    const auto itEnd = text.cbegin() + maxNumChars;
114✔
207

208
    // Vertical alignment (assumes 1 line only!)
209
    if(format.is(FontStyle::BOTTOM))
114✔
210
        pos.y -= maxCharSize.y;
×
211
    else if(format.is(FontStyle::VCENTER))
114✔
212
        pos.y -= maxCharSize.y / 2;
22✔
213
    // Horizontal alignment
214
    if(format.is(FontStyle::RIGHT))
114✔
215
        pos.x -= textWidth;
78✔
216
    else if(format.is(FontStyle::CENTER))
36✔
217
        pos.x -= textWidth / 2;
21✔
218

219
    texList.texCoords.clear();
114✔
220
    texList.vertices.clear();
114✔
221

222
    for(auto it = text.begin(); it != itEnd;)
796✔
223
    {
224
        const utf::code_point curChar = utf8::decode(it, itEnd);
682✔
225
        DrawChar(curChar, texList, pos);
682✔
226
    }
227

228
    if(drawEnd)
114✔
229
    {
230
        for(auto it = end.begin(); it != end.end();)
4✔
231
        {
232
            const utf::code_point curChar = utf8::decode(it, end.end());
3✔
233
            DrawChar(curChar, texList, pos);
3✔
234
        }
235
    }
236

237
    if(texList.vertices.empty())
114✔
238
        return;
×
239

240
    // Get texture first as it might need to be created
241
    glArchivItem_Bitmap& usedFont = format.is(FontStyle::NO_OUTLINE) ? *fontNoOutline : *fontWithOutline;
114✔
242
    unsigned texture = usedFont.GetTexture();
114✔
243
    if(!texture)
114✔
244
        return;
×
245
    const GlPoint texSize(usedFont.GetTexSize());
114✔
246
    RTTR_Assert(texList.texCoords.size() == texList.vertices.size());
114✔
247
    RTTR_Assert(texList.texCoords.size() % 4u == 0);
114✔
248
    for(GlPoint& pt : texList.texCoords)
2,854✔
249
        pt /= texSize;
2,740✔
250

251
    glVertexPointer(2, GL_FLOAT, 0, &texList.vertices[0]);
114✔
252
    glTexCoordPointer(2, GL_FLOAT, 0, &texList.texCoords[0]);
114✔
253
    VIDEODRIVER.BindTexture(texture);
114✔
254
    glColor4ub(GetRed(color), GetGreen(color), GetBlue(color), GetAlpha(color));
114✔
255
    glDrawArrays(GL_QUADS, 0, texList.vertices.size());
114✔
256
}
257

258
template<bool T_limitWidth>
259
unsigned glFont::getWidthInternal(const std::string::const_iterator& begin, const std::string::const_iterator& end,
1,958✔
260
                                  unsigned maxWidth, unsigned* maxNumChars) const
261
{
262
    unsigned curLen = 0;
1,958✔
263
    for(auto it = begin; it != end;)
29,018✔
264
    {
265
        const auto itCurChar = it;
27,764✔
266
        const utf::code_point curChar = utf8::decode(it, end);
27,764✔
267
        const unsigned cw = CharWidth(curChar);
27,764✔
268
        // If we limit the width and the text will be longer, stop before it
269
        // Do not stop if this is the first char
270
        if(T_limitWidth && curLen != 0 && curLen + cw > maxWidth)
21,023✔
271
        {
272
            *maxNumChars = static_cast<unsigned>(std::distance(begin, itCurChar));
704✔
273
            return curLen;
704✔
274
        }
275
        curLen += cw;
27,060✔
276
    }
277

278
    if(T_limitWidth)
279
        *maxNumChars = static_cast<unsigned>(std::distance(begin, end));
556✔
280
    return curLen;
1,254✔
281
}
282

283
unsigned glFont::getWidth(const std::string& text) const
698✔
284
{
285
    return getWidthInternal<false>(text.begin(), text.end(), 0, nullptr);
698✔
286
}
287

288
unsigned glFont::getWidth(const std::string& text, unsigned maxWidth, unsigned* maxNumChars) const
1,260✔
289
{
290
    return getWidthInternal<true>(text.begin(), text.end(), maxWidth, maxNumChars);
1,260✔
291
}
292

293
Rect glFont::getBounds(DrawPoint pos, const std::string& text, FontStyle format) const
9✔
294
{
295
    if(text.empty())
9✔
296
        return Rect(Position(pos), 0, 0);
×
297
    unsigned width = getWidth(text);
9✔
298
    unsigned numLines = static_cast<unsigned>(std::count(text.begin(), text.end(), '\n')) + 1;
9✔
299
    Rect result(Position(pos), width, numLines * getHeight());
9✔
300
    Position offset(0, 0);
9✔
301
    if(format.is(FontStyle::RIGHT))
9✔
302
        offset.x = width;
2✔
303
    else if(format.is(FontStyle::CENTER))
7✔
304
        offset.x = width / 2;
2✔
305
    if(format.is(FontStyle::BOTTOM))
9✔
306
        offset.y = getHeight();
2✔
307
    else if(format.is(FontStyle::VCENTER))
7✔
308
        offset.y = getHeight() / 2;
2✔
309
    result.move(-offset);
9✔
310
    return result;
9✔
311
}
312

313
std::vector<std::string> glFont::WrapInfo::CreateSingleStrings(const std::string& text) const
12✔
314
{
315
    std::vector<std::string> destStrings;
12✔
316
    destStrings.reserve(lines.size());
12✔
317
    for(const auto& line : lines)
72✔
318
        destStrings.emplace_back(text.substr(line.start, line.len));
60✔
319
    return destStrings;
12✔
320
}
321

322
/**
323
 *  Gibt Infos, über die Unterbrechungspunkte in einem Text
324
 *
325
 *  @param[in]     text            Text, der auf Zeilen verteilt werden soll
326
 *  @param[in]     primary_width   Maximale Breite der ersten Zeile
327
 *  @param[in]     secondary_width Maximale Breite der weiteren Zeilen
328
 */
329
glFont::WrapInfo glFont::GetWrapInfo(const std::string& text, const unsigned short primary_width,
31✔
330
                                     const unsigned short secondary_width) const
331
{
332
    RTTR_Assert(s25util::isValidUTF8(text)); // Can only handle UTF-8 strings!
31✔
333

334
    // Current line width
335
    unsigned line_width = 0;
31✔
336
    // Width of current word
337
    unsigned word_width = 0;
31✔
338
    unsigned curMaxLineWidth = primary_width;
31✔
339

340
    WrapInfo wi;
31✔
341

342
    auto it = text.begin();
31✔
343
    const auto itEnd = text.end();
31✔
344
    auto itWordStart = it;
31✔
345
    auto itLineStart = it;
31✔
346

347
    const unsigned spaceWidth = CharWidth(' ');
31✔
348

349
    const auto makeLineRange = [&text, &itLineStart](const auto& itLineEnd) {
330✔
350
        return WrapInfo::LineRange{static_cast<unsigned>(itLineStart - text.begin()),
110✔
351
                                   static_cast<unsigned>(itLineEnd - itLineStart)};
110✔
352
    };
31✔
353

354
    while(true)
355
    {
356
        // Save iterator to current char as we might want to break BEFORE it
357
        const auto itCurChar = it;
577✔
358
        const utf::code_point curChar = (it != itEnd) ? utf8::decode(it, itEnd) : 0;
577✔
359
        // Word ended
360
        if(curChar == 0 || curChar == '\n' || curChar == ' ')
577✔
361
        {
362
            // Is the current word to long for the current line
363
            if(word_width + line_width > curMaxLineWidth)
128✔
364
            {
365
                // Word does not fit -> Start new line
366

367
                // Can we fit the word in one line?
368
                if(word_width <= secondary_width)
4✔
369
                {
370
                    // Break before word
371
                    wi.lines.emplace_back(makeLineRange(itWordStart));
2✔
372
                    // New line starts at index of word start
373
                    itLineStart = itWordStart;
2✔
374
                    line_width = 0;
2✔
375
                } else
376
                {
377
                    // Word does not even fit on one line -> Put as many letters in one line as possible
378
                    for(auto itWord = itWordStart; itWord != itCurChar;)
12✔
379
                    {
380
                        const auto itPotentialBreak = itWord;
10✔
381
                        const utf::code_point letter_width = CharWidth(utf8::decode(itWord, itEnd));
10✔
382

383
                        // Can we fit the letter onto current line?
384
                        if(line_width + letter_width <= curMaxLineWidth)
10✔
385
                            line_width += letter_width; // Add it
7✔
386
                        else
387
                        {
388
                            // Create new line at this letter
389
                            wi.lines.emplace_back(makeLineRange(itPotentialBreak));
3✔
390
                            // New line starts at index of word start
391
                            itLineStart = itPotentialBreak;
3✔
392
                            line_width = letter_width;
3✔
393
                        }
394
                    }
395

396
                    // Restart word
397
                    word_width = 0;
2✔
398
                }
399
                curMaxLineWidth = secondary_width;
4✔
400
            }
401
            if(curChar == 0)
128✔
402
            {
403
                wi.lines.emplace_back(makeLineRange(itCurChar));
31✔
404
                break;
31✔
405
            } else if(curChar == ' ')
97✔
406
            {
407
                // Set up this line if we are going to continue it (not at line break or text end)
408
                // Line contains word and whitespace
409
                line_width += word_width + spaceWidth;
23✔
410
                word_width = 0;
23✔
411
                itWordStart = it;
23✔
412
            } else
413
            {
414
                // If line break add new line (after all the word-breaking above)
415
                wi.lines.emplace_back(makeLineRange(itCurChar));
74✔
416
                itLineStart = itWordStart = it;
74✔
417
                word_width = line_width = 0;
74✔
418
            }
97✔
419
        } else
420
        {
421
            // Some char -> Add its width
422
            word_width += CharWidth(curChar);
449✔
423
        }
424
    }
546✔
425
    for(auto& line : wi.lines)
141✔
426
    {
427
        while(line.len > 0 && text[line.start + line.len - 1] == ' ')
113✔
428
            --line.len;
3✔
429
    }
430
    return wi;
62✔
431
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc