diff --git a/.gitignore b/.gitignore index 1c90cb068..44deb49c4 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Source/engine/render/text_render.cpp b/Source/engine/render/text_render.cpp index 19a530f8b..963246b64 100644 --- a/Source/engine/render/text_render.cpp +++ b/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 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(prev) == rest[0]) { - rest.remove_prefix(1); - continue; - } - const std::optional 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 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 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 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)) { diff --git a/Source/engine/render/text_render.hpp b/Source/engine/render/text_render.hpp index c60a4e394..ef05c77c2 100644 --- a/Source/engine/render/text_render.hpp +++ b/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 firstArgOffset = std::nullopt); int GetLineHeight(std::string_view text, GameFontTables fontIndex); diff --git a/Source/plrmsg.cpp b/Source/plrmsg.cpp index 51b708218..08293902d 100644 --- a/Source/plrmsg.cpp +++ b/Source/plrmsg.cpp @@ -6,6 +6,7 @@ #include "plrmsg.h" #include +#include #include #include @@ -123,11 +124,11 @@ void DrawPlrMsg(const Surface &out) DrawHalfTransparentRectTo(out, x - 3, y, width + 6, message.lineHeight * chatlines); - std::vector 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 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 }); } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 17d1a430e..35d003fc2 100644 --- a/test/CMakeLists.txt +++ b/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) diff --git a/test/fixtures/text_render_integration_test/basic-colors.png b/test/fixtures/text_render_integration_test/basic-colors.png new file mode 100644 index 000000000..dbbf19a54 Binary files /dev/null and b/test/fixtures/text_render_integration_test/basic-colors.png differ diff --git a/test/fixtures/text_render_integration_test/basic.png b/test/fixtures/text_render_integration_test/basic.png new file mode 100644 index 000000000..75a40d744 Binary files /dev/null and b/test/fixtures/text_render_integration_test/basic.png differ diff --git a/test/fixtures/text_render_integration_test/expected.png b/test/fixtures/text_render_integration_test/expected.png deleted file mode 100644 index 85648074c..000000000 Binary files a/test/fixtures/text_render_integration_test/expected.png and /dev/null differ diff --git a/test/fixtures/text_render_integration_test/kerning_fit_spacing-colors.png b/test/fixtures/text_render_integration_test/kerning_fit_spacing-colors.png new file mode 100644 index 000000000..2baed8422 Binary files /dev/null and b/test/fixtures/text_render_integration_test/kerning_fit_spacing-colors.png differ diff --git a/test/fixtures/text_render_integration_test/kerning_fit_spacing.png b/test/fixtures/text_render_integration_test/kerning_fit_spacing.png new file mode 100644 index 000000000..e26efe87a Binary files /dev/null and b/test/fixtures/text_render_integration_test/kerning_fit_spacing.png differ diff --git a/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center-colors.png b/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center-colors.png new file mode 100644 index 000000000..6a1cdead7 Binary files /dev/null and b/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center-colors.png differ diff --git a/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center.png b/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center.png new file mode 100644 index 000000000..ed5246211 Binary files /dev/null and b/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center.png differ diff --git a/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center__newlines.png b/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center__newlines.png new file mode 100644 index 000000000..aba0ee3d3 Binary files /dev/null and b/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center__newlines.png differ diff --git a/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center__newlines_in_fmt-colors.png b/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center__newlines_in_fmt-colors.png new file mode 100644 index 000000000..8babc23c7 Binary files /dev/null and b/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center__newlines_in_fmt-colors.png differ diff --git a/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center__newlines_in_value-colors.png b/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center__newlines_in_value-colors.png new file mode 100644 index 000000000..8cee148bc Binary files /dev/null and b/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_center__newlines_in_value-colors.png differ diff --git a/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_right-colors.png b/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_right-colors.png new file mode 100644 index 000000000..060119efd Binary files /dev/null and b/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_right-colors.png differ diff --git a/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_right.png b/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_right.png new file mode 100644 index 000000000..e9bc45201 Binary files /dev/null and b/test/fixtures/text_render_integration_test/kerning_fit_spacing__align_right.png differ diff --git a/test/text_render_integration_test.cpp b/test/text_render_integration_test.cpp index 1cd25e20f..d5f6f6793 100644 --- a/test/text_render_integration_test.cpp +++ b/test/text_render_integration_test.cpp @@ -1,7 +1,12 @@ +#include #include #include #include +#include +#include +#include +#include #include #include @@ -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 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 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 { +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 &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(); +}