Browse Source

text renderer: colors layout fix + improved tests

1. Fixes DrawStringwithColors kerning fit handling and newline handling.
2. Splits tests into one file per function call, making it easier to compare diffs and add new tests.
    Also adds `--update_expected` argument to update the expected files with actual results.
pull/7806/head
Gleb Mazovetskiy 1 year ago
parent
commit
5a14cc57f3
  1. 2
      .gitignore
  2. 155
      Source/engine/render/text_render.cpp
  3. 4
      Source/engine/render/text_render.hpp
  4. 9
      Source/plrmsg.cpp
  5. 7
      test/CMakeLists.txt
  6. BIN
      test/fixtures/text_render_integration_test/basic-colors.png
  7. BIN
      test/fixtures/text_render_integration_test/basic.png
  8. BIN
      test/fixtures/text_render_integration_test/expected.png
  9. BIN
      test/fixtures/text_render_integration_test/kerning_fit_spacing-colors.png
  10. BIN
      test/fixtures/text_render_integration_test/kerning_fit_spacing.png
  11. BIN
      test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center-colors.png
  12. BIN
      test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center.png
  13. BIN
      test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center__newlines.png
  14. BIN
      test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center__newlines_in_fmt-colors.png
  15. BIN
      test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center__newlines_in_value-colors.png
  16. BIN
      test/fixtures/text_render_integration_test/kerning_fit_spacing__align_right-colors.png
  17. BIN
      test/fixtures/text_render_integration_test/kerning_fit_spacing__align_right.png
  18. 277
      test/text_render_integration_test.cpp

2
.gitignore vendored

@ -467,4 +467,4 @@ uwp-project/Assets/ui_art
/.s390x-ccache/
# Test fixtures
/test/fixtures/text_render_integration_test/actual.png
/test/fixtures/text_render_integration_test/*-Actual.png

155
Source/engine/render/text_render.cpp

@ -556,7 +556,10 @@ int GetLineWidth(std::string_view text, GameFontTables size, int spacing, int *c
return lineWidth != 0 ? (lineWidth - spacing) : 0;
}
int GetLineWidth(std::string_view fmt, DrawStringFormatArg *args, std::size_t argsLen, size_t argsOffset, GameFontTables size, int spacing, int *charactersInLine)
bool IsConsumed(std::string_view s) { return s.empty() || s[0] == '\0'; };
int GetLineWidth(std::string_view fmt, DrawStringFormatArg *args, std::size_t argsLen, size_t argsOffset, GameFontTables size, int spacing, int *charactersInLine,
std::optional<size_t> firstArgOffset)
{
int lineWidth = 0;
CurrentFont currentFont;
@ -564,32 +567,48 @@ int GetLineWidth(std::string_view fmt, DrawStringFormatArg *args, std::size_t ar
uint32_t codepoints = 0;
char32_t prev = U'\0';
char32_t next;
std::string_view remaining = fmt;
FmtArgParser fmtArgParser { fmt, args, argsLen, argsOffset };
std::string_view rest = fmt;
while (!rest.empty()) {
if ((prev == U'{' || prev == U'}') && static_cast<char>(prev) == rest[0]) {
rest.remove_prefix(1);
continue;
}
const std::optional<std::size_t> fmtArgPos = fmtArgParser(rest);
if (fmtArgPos) {
int argCodePoints;
lineWidth += GetLineWidth(args[*fmtArgPos].GetFormatted(), size, spacing, &argCodePoints);
codepoints += argCodePoints;
prev = U'\0';
continue;
}
size_t cpLen;
next = ConsumeFirstUtf8CodePoint(&rest);
if (next == Utf8DecodeError)
break;
if (next == ZWSP) {
prev = next;
continue;
// The current formatted argument value being processed.
std::string_view curFormatted;
// The string that we're currently processing: either `remaining` or `curFormatted`.
std::string_view *str;
if (firstArgOffset.has_value()) {
curFormatted = args[argsOffset - 1].GetFormatted().substr(*firstArgOffset);
}
for (; !(IsConsumed(curFormatted) && IsConsumed(remaining));
str->remove_prefix(cpLen), prev = next) {
const bool isProcessingFormatArgValue = !IsConsumed(curFormatted);
str = isProcessingFormatArgValue ? &curFormatted : &remaining;
next = DecodeFirstUtf8CodePoint(*str, &cpLen);
if (next == Utf8DecodeError) break;
// {{ and }} escapes in fmt.
if (!isProcessingFormatArgValue && (prev == U'{' || prev == U'}') && prev == next) continue;
// ZWSP are line-breaking opportunities that can otherwise be skipped for rendering as they have 0-width.
if (next == ZWSP) continue;
if (next == U'\n') break;
if (!isProcessingFormatArgValue) {
const std::optional<std::size_t> fmtArgPos = fmtArgParser(*str);
if (fmtArgPos.has_value()) {
// `fmtArgParser` has already consumed `*str`. Ensure the loop doesn't consume any more.
cpLen = 0;
// The loop assigns `prev = next`.
// We reset it to U'\0' to ensure that {{ and }} escapes are not processed accross
// the boundary of the format string and a formatted value.
next = U'\0';
currentFont.clear();
const DrawStringFormatArg &arg = args[*fmtArgPos];
curFormatted = arg.GetFormatted();
continue;
}
}
if (next == U'\n')
break;
if (!currentFont.load(size, text_color::ColorDialogWhite, next)) {
next = U'?';
@ -600,8 +619,7 @@ int GetLineWidth(std::string_view fmt, DrawStringFormatArg *args, std::size_t ar
const uint8_t frame = next & 0xFF;
lineWidth += (*currentFont.sprite)[frame].width() + spacing;
codepoints++;
prev = next;
++codepoints;
}
if (charactersInLine != nullptr)
*charactersInLine = codepoints;
@ -781,11 +799,11 @@ void DrawStringWithColors(const Surface &out, std::string_view fmt, DrawStringFo
const Surface clippedOut = ClipSurface(out, rect);
CurrentFont currentFont;
int curSpacing = opts.spacing;
const int originalSpacing = opts.spacing;
if (HasAnyOf(opts.flags, UiFlags::KerningFitSpacing)) {
curSpacing = AdjustSpacingToFitHorizontally(lineWidth, opts.spacing, charactersInLine, rect.size.width);
if (curSpacing != opts.spacing && HasAnyOf(opts.flags, UiFlags::AlignCenter | UiFlags::AlignRight)) {
const int adjustedLineWidth = GetLineWidth(fmt, args, argsLen, 0, size, curSpacing, &charactersInLine);
opts.spacing = AdjustSpacingToFitHorizontally(lineWidth, originalSpacing, charactersInLine, rect.size.width);
if (opts.spacing != originalSpacing && HasAnyOf(opts.flags, UiFlags::AlignCenter | UiFlags::AlignRight)) {
const int adjustedLineWidth = GetLineWidth(fmt, args, argsLen, 0, size, opts.spacing, &charactersInLine);
characterPosition.x = GetLineStartX(opts.flags, rect, adjustedLineWidth);
}
}
@ -795,28 +813,47 @@ void DrawStringWithColors(const Surface &out, std::string_view fmt, DrawStringFo
std::string_view remaining = fmt;
FmtArgParser fmtArgParser { fmt, args, argsLen };
size_t cpLen;
for (; !remaining.empty() && remaining[0] != '\0'
&& (next = DecodeFirstUtf8CodePoint(remaining, &cpLen)) != Utf8DecodeError;
remaining.remove_prefix(cpLen), prev = next) {
if (((prev == U'{' || prev == U'}') && prev == next)
|| next == ZWSP)
continue;
const std::optional<std::size_t> fmtArgPos = fmtArgParser(remaining);
if (fmtArgPos) {
DoDrawString(clippedOut, args[*fmtArgPos].GetFormatted(), rect, characterPosition, lineWidth, charactersInLine, rightMargin, bottomMargin, size,
GetColorFromFlags(args[*fmtArgPos].GetFlags()), outlined, opts);
// `fmtArgParser` has already consumed `remaining`. Ensure the loop doesn't consume any more.
cpLen = 0;
// The loop assigns `prev = next`. We want `prev` to be `\0` after this.
next = U'\0';
currentFont.clear();
continue;
// The current formatted argument value being processed.
std::string_view curFormatted;
text_color curFormattedColor;
// The string that we're currently processing: either `remaining` or `curFormatted`.
std::string_view *str;
for (; !(IsConsumed(curFormatted) && IsConsumed(remaining));
str->remove_prefix(cpLen), prev = next) {
const bool isProcessingFormatArgValue = !IsConsumed(curFormatted);
str = isProcessingFormatArgValue ? &curFormatted : &remaining;
next = DecodeFirstUtf8CodePoint(*str, &cpLen);
if (next == Utf8DecodeError) break;
// {{ and }} escapes in fmt.
if (!isProcessingFormatArgValue && (prev == U'{' || prev == U'}') && prev == next) continue;
// ZWSP are line-breaking opportunities that can otherwise be skipped for rendering as they have 0-width.
if (next == ZWSP) continue;
if (!isProcessingFormatArgValue) {
const std::optional<std::size_t> fmtArgPos = fmtArgParser(*str);
if (fmtArgPos.has_value()) {
// `fmtArgParser` has already consumed `*str`. Ensure the loop doesn't consume any more.
cpLen = 0;
// The loop assigns `prev = next`.
// We reset it to U'\0' to ensure that {{ and }} escapes are not processed accross
// the boundary of the format string and a formatted value.
next = U'\0';
currentFont.clear();
const DrawStringFormatArg &arg = args[*fmtArgPos];
curFormatted = arg.GetFormatted();
curFormattedColor = GetColorFromFlags(arg.GetFlags());
continue;
}
}
if (!currentFont.load(size, color, next)) {
const text_color curColor = isProcessingFormatArgValue ? curFormattedColor : color;
if (!currentFont.load(size, curColor, next)) {
next = U'?';
if (!currentFont.load(size, color, next)) {
if (!currentFont.load(size, curColor, next)) {
app_fatal("Missing fonts");
}
}
@ -830,14 +867,22 @@ void DrawStringWithColors(const Surface &out, std::string_view fmt, DrawStringFo
characterPosition.y = nextLineY;
if (HasAnyOf(opts.flags, UiFlags::KerningFitSpacing)) {
int nextLineWidth = GetLineWidth(remaining.substr(cpLen), args, argsLen, fmtArgParser.offset(), size, opts.spacing, &charactersInLine);
curSpacing = AdjustSpacingToFitHorizontally(nextLineWidth, opts.spacing, charactersInLine, rect.size.width);
int nextLineWidth = isProcessingFormatArgValue
? GetLineWidth(remaining, args, argsLen, fmtArgParser.offset(), size, originalSpacing, &charactersInLine,
/*firstArgOffset=*/args[fmtArgParser.offset() - 1].GetFormatted().size() - (curFormatted.size() - cpLen))
: GetLineWidth(remaining.substr(cpLen), args, argsLen, fmtArgParser.offset(), size, originalSpacing, &charactersInLine);
opts.spacing = AdjustSpacingToFitHorizontally(nextLineWidth, originalSpacing, charactersInLine, rect.size.width);
}
if (HasAnyOf(opts.flags, (UiFlags::AlignCenter | UiFlags::AlignRight))) {
if (HasAnyOf(opts.flags, UiFlags::AlignCenter | UiFlags::AlignRight)) {
lineWidth = width;
if (remaining.size() > cpLen)
lineWidth += curSpacing + GetLineWidth(remaining.substr(cpLen), args, argsLen, fmtArgParser.offset(), size, curSpacing);
if (str->size() > cpLen) {
lineWidth += opts.spacing
+ (isProcessingFormatArgValue
? GetLineWidth(remaining, args, argsLen, fmtArgParser.offset(), size, opts.spacing, &charactersInLine,
/*firstArgOffset=*/args[fmtArgParser.offset() - 1].GetFormatted().size() - (curFormatted.size() - cpLen))
: GetLineWidth(remaining.substr(cpLen), args, argsLen, fmtArgParser.offset(), size, opts.spacing, &charactersInLine));
}
}
characterPosition.x = GetLineStartX(opts.flags, rect, lineWidth);
@ -845,8 +890,8 @@ void DrawStringWithColors(const Surface &out, std::string_view fmt, DrawStringFo
continue;
}
DrawFont(clippedOut, characterPosition, (*currentFont.sprite)[frame], color, outlined);
characterPosition.x += width + curSpacing;
DrawFont(clippedOut, characterPosition, (*currentFont.sprite)[frame], curColor, outlined);
characterPosition.x += width + opts.spacing;
}
if (HasAnyOf(opts.flags, UiFlags::PentaCursor)) {

4
Source/engine/render/text_render.hpp

@ -184,9 +184,11 @@ int GetLineWidth(std::string_view text, GameFontTables size = GameFont12, int sp
* @param size Font size to use
* @param spacing Extra spacing to add per character
* @param charactersInLine Receives characters read until newline or terminator
* @param firstArgOffset If given, starts counting at `args[argsOffset - 1].GetFormatted().substr(*firstArgOffset)`.
* @return Line width in pixels
*/
int GetLineWidth(std::string_view fmt, DrawStringFormatArg *args, size_t argsLen, size_t argsOffset, GameFontTables size, int spacing, int *charactersInLine = nullptr);
int GetLineWidth(std::string_view fmt, DrawStringFormatArg *args, size_t argsLen, size_t argsOffset, GameFontTables size, int spacing, int *charactersInLine = nullptr,
std::optional<size_t> firstArgOffset = std::nullopt);
int GetLineHeight(std::string_view text, GameFontTables fontIndex);

9
Source/plrmsg.cpp

@ -6,6 +6,7 @@
#include "plrmsg.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <fmt/format.h>
@ -123,11 +124,11 @@ void DrawPlrMsg(const Surface &out)
DrawHalfTransparentRectTo(out, x - 3, y, width + 6, message.lineHeight * chatlines);
std::vector<DrawStringFormatArg> args {
{ std::string_view(text.data(), message.prefixLength), UiFlags::ColorWhitegold },
{ std::string_view(text.data() + message.prefixLength, text.size() - message.prefixLength), message.style }
std::array<DrawStringFormatArg, 2> args {
DrawStringFormatArg { std::string_view(text.data(), message.prefixLength), UiFlags::ColorWhitegold },
DrawStringFormatArg { std::string_view(text.data() + message.prefixLength, text.size() - message.prefixLength), message.style }
};
DrawStringWithColors(out, "{:s}{:s}", args, { { x, y }, { width, 0 } },
DrawStringWithColors(out, "{:s}{:s}", args.data(), args.size(), { { x, y }, { width, 0 } },
{ .flags = UiFlags::None, .lineHeight = message.lineHeight });
}
}

7
test/CMakeLists.txt

@ -37,9 +37,6 @@ set(tests
timedemo_test
writehero_test
)
if(NOT USE_SDL1)
list(APPEND tests text_render_integration_test)
endif()
set(standalone_tests
codec_test
crawl_test
@ -52,6 +49,9 @@ set(standalone_tests
str_cat_test
utf8_test
)
if(NOT USE_SDL1)
list(APPEND standalone_tests text_render_integration_test)
endif()
set(benchmarks
clx_render_benchmark
crawl_benchmark
@ -106,6 +106,7 @@ target_link_dependencies(parse_int_test PRIVATE libdevilutionx_parse_int)
target_link_dependencies(path_test PRIVATE libdevilutionx_pathfinding libdevilutionx_direction app_fatal_for_testing)
target_link_dependencies(path_benchmark PRIVATE libdevilutionx_pathfinding app_fatal_for_testing)
target_link_dependencies(str_cat_test PRIVATE libdevilutionx_strings)
target_link_dependencies(text_render_integration_test PRIVATE libdevilutionx_so GTest::gtest GTest::gmock)
target_link_dependencies(utf8_test PRIVATE libdevilutionx_utf8)
target_include_directories(writehero_test PRIVATE ../3rdParty/PicoSHA2)

BIN
test/fixtures/text_render_integration_test/basic-colors.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
test/fixtures/text_render_integration_test/basic.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
test/fixtures/text_render_integration_test/expected.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

BIN
test/fixtures/text_render_integration_test/kerning_fit_spacing-colors.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
test/fixtures/text_render_integration_test/kerning_fit_spacing.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center-colors.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center__newlines.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center__newlines_in_fmt-colors.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center__newlines_in_value-colors.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
test/fixtures/text_render_integration_test/kerning_fit_spacing__align_right-colors.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
test/fixtures/text_render_integration_test/kerning_fit_spacing__align_right.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

277
test/text_render_integration_test.cpp

@ -1,7 +1,12 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <string>
#include <string_view>
#include <variant>
#include <SDL.h>
#include <expected.hpp>
@ -17,13 +22,157 @@
#include "engine/surface.hpp"
#include "utils/paths.h"
#include "utils/sdl_wrap.h"
#include "utils/str_cat.hpp"
#include "utils/surface_to_png.hpp"
// Invoke with --update_expected to update the expected files with actual results.
static bool UpdateExpected;
namespace devilution {
namespace {
constexpr char FixturesPath[] = "../test/fixtures/text_render_integration_test/";
struct TestFixture {
std::string name;
int width;
int height;
std::string_view fmt;
std::vector<DrawStringFormatArg> args {};
TextRenderOptions opts { .flags = UiFlags::ColorUiGold };
friend void PrintTo(const TestFixture &f, std::ostream *os)
{
*os << f.name;
}
};
const TestFixture Fixtures[] {
TestFixture {
.name = "basic",
.width = 96,
.height = 15,
.fmt = "DrawString",
},
TestFixture {
.name = "basic-colors",
.width = 186,
.height = 15,
.fmt = "{}{}{}{}",
.args = {
{ "Draw", UiFlags::ColorUiSilver },
{ "String", UiFlags::ColorUiGold },
{ "With", UiFlags::ColorUiSilverDark },
{ "Colors", UiFlags::ColorUiGoldDark },
},
},
TestFixture {
.name = "kerning_fit_spacing",
.width = 120,
.height = 15,
.fmt = "KerningFitSpacing",
.opts = {
.flags = UiFlags::KerningFitSpacing | UiFlags::ColorUiSilver,
},
},
TestFixture {
.name = "kerning_fit_spacing-colors",
.width = 120,
.height = 15,
.fmt = "{}{}{}",
.args = {
{ "Kerning", UiFlags::ColorUiSilver },
{ "Fit", UiFlags::ColorUiGold },
{ "Spacing", UiFlags::ColorUiSilverDark },
},
.opts = {
.flags = UiFlags::KerningFitSpacing,
},
},
TestFixture {
.name = "kerning_fit_spacing__align_center",
.width = 170,
.height = 15,
.fmt = "KerningFitSpacing | AlignCenter",
.opts = {
.flags = UiFlags::KerningFitSpacing | UiFlags::AlignCenter | UiFlags::ColorUiSilver,
},
},
TestFixture {
.name = "kerning_fit_spacing__align_center-colors",
.width = 170,
.height = 15,
.fmt = "{}{}{}",
.args = {
{ "KerningFitSpacing", UiFlags::ColorUiSilver },
{ " | ", UiFlags::ColorUiGold },
{ "AlignCenter", UiFlags::ColorUiSilverDark },
},
.opts = {
.flags = UiFlags::KerningFitSpacing | UiFlags::AlignCenter,
},
},
TestFixture {
.name = "kerning_fit_spacing__align_center__newlines",
.width = 170,
.height = 42,
.fmt = "KerningFitSpacing | AlignCenter\nShort line\nAnother overly long line",
.opts = {
.flags = UiFlags::KerningFitSpacing | UiFlags::AlignCenter | UiFlags::ColorUiSilver,
},
},
TestFixture {
.name = "kerning_fit_spacing__align_center__newlines_in_fmt-colors",
.width = 170,
.height = 42,
.fmt = "{}\n{}\n{}",
.args = {
{ "KerningFitSpacing | AlignCenter", UiFlags::ColorUiSilver },
{ "Short line", UiFlags::ColorUiGold },
{ "Another overly long line", UiFlags::ColorUiSilverDark },
},
.opts = {
.flags = UiFlags::KerningFitSpacing | UiFlags::AlignCenter,
},
},
TestFixture {
.name = "kerning_fit_spacing__align_center__newlines_in_value-colors",
.width = 170,
.height = 42,
.fmt = "{}{}",
.args = {
{ "KerningFitSpacing | AlignCenter\nShort line\nAnother overly ", UiFlags::ColorUiSilver },
{ "long line", UiFlags::ColorUiGold },
},
.opts = {
.flags = UiFlags::KerningFitSpacing | UiFlags::AlignCenter,
},
},
TestFixture {
.name = "kerning_fit_spacing__align_right",
.width = 170,
.height = 15,
.fmt = "KerningFitSpacing | AlignRight",
.opts = {
.flags = UiFlags::KerningFitSpacing | UiFlags::AlignRight | UiFlags::ColorUiSilver,
},
},
TestFixture {
.name = "kerning_fit_spacing__align_right-colors",
.width = 170,
.height = 15,
.fmt = "{}{}{}",
.args = {
{ "KerningFitSpacing", UiFlags::ColorUiSilver },
{ " | ", UiFlags::ColorUiGold },
{ "AlignRight", UiFlags::ColorUiSilverDark },
},
.opts = {
.flags = UiFlags::KerningFitSpacing | UiFlags::AlignRight,
},
},
};
SDLPaletteUniquePtr LoadPalette()
{
struct Color {
@ -31,7 +180,7 @@ SDLPaletteUniquePtr LoadPalette()
};
std::array<Color, 256> palData;
LoadFileInMem("ui_art\\diablo.pal", palData);
SDLPaletteUniquePtr palette = SDLWrap::AllocPalette(256);
SDLPaletteUniquePtr palette = SDLWrap::AllocPalette(palData.size());
for (unsigned i = 0; i < palData.size(); i++) {
palette->colors[i] = SDL_Color {
palData[i].r, palData[i].g, palData[i].b, SDL_ALPHA_OPAQUE
@ -64,73 +213,85 @@ void DrawWithBorder(const Surface &out, const Rectangle &area, tl::function_ref<
Size { area.size.width - 2, area.size.height - 2 } });
}
TEST(TextRenderIntegrationTest, GoldenTest)
MATCHER_P(FileContentsEq, expectedPath,
StrCat(negation ? "doesn't have" : "has", " the same contents as ", ::testing::PrintToString(expectedPath)))
{
if (ReadFile(arg) != ReadFile(expectedPath)) {
if (UpdateExpected) {
CopyFileOverwrite(arg.c_str(), expectedPath.c_str());
std::clog << " Updated expected file at " << expectedPath << std::endl;
return true;
}
return false;
}
return true;
}
class TextRenderIntegrationTest : public ::testing::TestWithParam<TestFixture> {
public:
static void SetUpTestSuite()
{
palette = LoadPalette();
}
static void TearDownTestSuite()
{
palette = nullptr;
}
protected:
static SDLPaletteUniquePtr palette;
};
SDLPaletteUniquePtr TextRenderIntegrationTest::palette;
TEST_P(TextRenderIntegrationTest, RenderAndCompareTest)
{
SDLPaletteUniquePtr palette = LoadPalette();
OwnedSurface out { Size { 200, 140 } };
const TestFixture &fixture = GetParam();
OwnedSurface out = OwnedSurface { fixture.width + 20, fixture.height + 20 };
SDL_SetSurfacePalette(out.surface, palette.get());
ASSERT_NE(out.surface, nullptr);
int y = -15;
DrawWithBorder(out, Rectangle { Point { 0, y += 15 }, Size { 96, 15 } }, [&](const Rectangle &rect) {
DrawString(out, "DrawString", rect,
TextRenderOptions { .flags = UiFlags::ColorUiGold });
});
DrawWithBorder(out, Rectangle { Point { 0, y += 15 }, Size { 120, 15 } }, [&](const Rectangle &rect) {
DrawString(out, "KerningFitSpacing", rect,
TextRenderOptions { .flags = UiFlags::KerningFitSpacing | UiFlags::ColorUiSilver });
});
DrawWithBorder(out, Rectangle { Point { 0, y += 15 }, Size { 170, 15 } }, [&](const Rectangle &rect) {
DrawString(out, "KerningFitSpacing | AlignCenter", rect,
TextRenderOptions { .flags = UiFlags::KerningFitSpacing | UiFlags::AlignCenter | UiFlags::ColorUiSilver });
});
DrawWithBorder(out, Rectangle { Point { 0, y += 15 }, Size { 170, 15 } }, [&](const Rectangle &rect) {
DrawString(out, "KerningFitSpacing | AlignRight", rect,
TextRenderOptions { .flags = UiFlags::KerningFitSpacing | UiFlags::AlignRight | UiFlags::ColorUiSilver });
});
y += 4;
DrawWithBorder(out, Rectangle { Point { 0, y += 15 }, Size { 186, 15 } }, [&](const Rectangle &rect) {
DrawStringWithColors(out, "{}{}{}{}",
{ { "Draw", UiFlags::ColorUiSilver },
{ "String", UiFlags::ColorUiGold },
{ "With", UiFlags::ColorUiSilverDark },
{ "Colors", UiFlags::ColorUiGoldDark } },
rect);
});
DrawWithBorder(out, Rectangle { Point { 0, y += 15 }, Size { 120, 15 } }, [&](const Rectangle &rect) {
DrawStringWithColors(out, "{}{}{}",
{ { "Kerning", UiFlags::ColorUiSilver },
{ "Fit", UiFlags::ColorUiGold },
{ "Spacing", UiFlags::ColorUiSilverDark } },
rect,
TextRenderOptions { .flags = UiFlags::KerningFitSpacing });
});
DrawWithBorder(out, Rectangle { Point { 0, y += 15 }, Size { 170, 15 } }, [&](const Rectangle &rect) {
DrawStringWithColors(out, "{}{}{}",
{ { "KerningFitSpacing", UiFlags::ColorUiSilver },
{ " | ", UiFlags::ColorUiGold },
{ "AlignCenter", UiFlags::ColorUiSilverDark } },
rect,
TextRenderOptions { .flags = UiFlags::KerningFitSpacing | UiFlags::AlignCenter | UiFlags::ColorUiSilver });
});
DrawWithBorder(out, Rectangle { Point { 0, y += 15 }, Size { 170, 15 } }, [&](const Rectangle &rect) {
DrawStringWithColors(out, "{}{}{}",
{ { "KerningFitSpacing", UiFlags::ColorUiSilver },
{ " | ", UiFlags::ColorUiGold },
{ "AlignRight", UiFlags::ColorUiSilverDark } },
rect,
TextRenderOptions { .flags = UiFlags::KerningFitSpacing | UiFlags::AlignRight | UiFlags::ColorUiSilver });
DrawWithBorder(out, Rectangle { Point { 10, 10 }, Size { fixture.width, fixture.height } }, [&](const Rectangle &rect) {
if (fixture.args.empty()) {
DrawString(out, fixture.fmt, rect, fixture.opts);
} else {
DrawStringWithColors(out, fixture.fmt, fixture.args, rect, fixture.opts);
}
});
const std::string actualPath = paths::BasePath() + FixturesPath + "actual.png";
const std::string expectedPath = paths::BasePath() + FixturesPath + "expected.png";
const std::string actualPath = StrCat(paths::BasePath(), FixturesPath, GetParam().name, "-Actual.png");
const std::string expectedPath = StrCat(paths::BasePath(), FixturesPath, GetParam().name, ".png");
SDL_RWops *actual = SDL_RWFromFile(actualPath.c_str(), "wb");
ASSERT_NE(actual, nullptr) << SDL_GetError();
ASSERT_TRUE(WriteSurfaceToFilePng(out, actual).has_value());
EXPECT_EQ(ReadFile(actualPath), ReadFile(expectedPath)) << "\n"
<< expectedPath << "\n"
<< actualPath;
EXPECT_THAT(actualPath, FileContentsEq(expectedPath));
}
INSTANTIATE_TEST_SUITE_P(GoldenTests, TextRenderIntegrationTest,
testing::ValuesIn(Fixtures),
[](const testing::TestParamInfo<TestFixture> &info) {
std::string name = info.param.name;
std::replace(name.begin(), name.end(), '-', '_');
return name;
});
} // namespace
} // namespace devilution
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
if (argc >= 2) {
for (int i = 1; i < argc; ++i) {
if (argv[i] != std::string_view("--update_expected")) {
std::cerr << "unknown argument: " << argv[i] << "\nUsage: "
<< argv[0] << " [--update_expected]" << "\n";
return 64;
}
}
UpdateExpected = true;
}
return RUN_ALL_TESTS();
}

Loading…
Cancel
Save