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

Return-To-The-Roots / s25client / 19015014167

02 Nov 2025 04:28PM UTC coverage: 50.474% (-0.005%) from 50.479%
19015014167

push

github

Flow86
Move EnableCCache include after submodule check

It is in libutil which may not exist or be up to date, so check those first

22504 of 44585 relevant lines covered (50.47%)

33867.94 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{}
17✔
28
{
29
    fontWithOutline = std::make_unique<glArchivItem_Bitmap_Raw>();
17✔
30
    fontNoOutline = std::make_unique<glArchivItem_Bitmap_Raw>();
17✔
31

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

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

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

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

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

61
        if((numChars % numCharsPerLine) == 0 && numChars > 0)
66,624✔
62
        {
63
            curPos.y += maxCharSize.y + spacing.y * 2;
587✔
64
            curPos.x = spacing.x;
587✔
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,624✔
69
        c->print(bufferWithOutline, palette, 128, curPos.x, curPos.y);
66,624✔
70

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

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

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

81
    // Set the placeholder for non-existent glyphs. Use '?' (should always be possible)
82
    if(CharExist(0xFFFD))
17✔
83
        placeHolder = GetCharInfo(0xFFFD);
3✔
84
    else if(CharExist('?'))
14✔
85
        placeHolder = GetCharInfo('?');
14✔
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
61✔
100
{
101
    if(c < asciiMapping.size())
61✔
102
        return asciiMapping[c].first;
32✔
103
    return helpers::contains(utf8_mapping, c);
29✔
104
}
105

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

121
void glFont::AddCharInfo(char32_t c, const CharInfo& info)
66,624✔
122
{
123
    if(c < asciiMapping.size())
66,624✔
124
        asciiMapping[c] = std::make_pair(true, info);
2,412✔
125
    else
126
        utf8_mapping[c] = info;
64,212✔
127
}
66,624✔
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,
179✔
173
                  const std::string& end) const
174
{
175
    RTTR_Assert(s25util::isValidUTF8(text));
179✔
176

177
    unsigned maxNumChars;
178
    unsigned short textWidth;
179
    bool drawEnd;
180
    if(maxWidth == 0xFFFF)
179✔
181
    {
182
        maxNumChars = text.size();
159✔
183
        textWidth = getWidth(text);
159✔
184
        drawEnd = false;
159✔
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;
65✔
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)
179✔
205
        return;
65✔
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.data());
114✔
252
    glTexCoordPointer(2, GL_FLOAT, 0, texList.texCoords.data());
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,387✔
260
                                  unsigned maxWidth, unsigned* maxNumChars) const
261
{
262
    unsigned curLen = 0;
1,387✔
263
    for(auto it = begin; it != end;)
18,881✔
264
    {
265
        const auto itCurChar = it;
17,850✔
266
        const utf::code_point curChar = utf8::decode(it, end);
17,850✔
267
        const unsigned cw = CharWidth(curChar);
17,850✔
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)
11,966✔
271
        {
272
            *maxNumChars = static_cast<unsigned>(std::distance(begin, itCurChar));
356✔
273
            return curLen;
356✔
274
        }
275
        curLen += cw;
17,494✔
276
    }
277

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

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

288
unsigned glFont::getWidth(const std::string& text, unsigned maxWidth, unsigned* maxNumChars) const
742✔
289
{
290
    return getWidthInternal<true>(text.begin(), text.end(), maxWidth, maxNumChars);
742✔
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