Browse Source

Draw text line by line (#8379)

pull/8389/head
Niv Baehr 3 months ago committed by GitHub
parent
commit
0802c27532
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 10
      CMake/Tests.cmake
  2. 147
      Source/engine/render/text_render.cpp
  3. 2
      Source/engine/render/text_render.hpp
  4. BIN
      test/fixtures/text_render_integration_test/cursor-end.png
  5. BIN
      test/fixtures/text_render_integration_test/cursor-middle.png
  6. BIN
      test/fixtures/text_render_integration_test/cursor-start.png
  7. BIN
      test/fixtures/text_render_integration_test/highlight-full.png
  8. BIN
      test/fixtures/text_render_integration_test/highlight-partial.png
  9. BIN
      test/fixtures/text_render_integration_test/multiline_cursor-end_first_line.png
  10. BIN
      test/fixtures/text_render_integration_test/multiline_cursor-end_second_line.png
  11. BIN
      test/fixtures/text_render_integration_test/multiline_cursor-middle_second_line.png
  12. BIN
      test/fixtures/text_render_integration_test/multiline_cursor-start_second_line.png
  13. BIN
      test/fixtures/text_render_integration_test/multiline_highlight.png
  14. 110
      test/text_render_integration_test.cpp

10
CMake/Tests.cmake

@ -170,6 +170,16 @@ if(DEVILUTIONX_SCREENSHOT_FORMAT STREQUAL DEVILUTIONX_SCREENSHOT_FORMAT_PNG AND
kerning_fit_spacing__align_right.png
vertical_overflow.png
vertical_overflow-colors.png
cursor-start.png
cursor-middle.png
cursor-end.png
multiline_cursor-end_first_line.png
multiline_cursor-start_second_line.png
multiline_cursor-middle_second_line.png
multiline_cursor-end_second_line.png
highlight-partial.png
highlight-full.png
multiline_highlight.png
SRC_PREFIX test/fixtures/text_render_integration_test/
OUTPUT_DIR "${DEVILUTIONX_TEST_FIXTURES_OUTPUT_DIRECTORY}/text_render_integration_test"
OUTPUT_VARIABLE _text_render_integration_test_fixtures

147
Source/engine/render/text_render.cpp

@ -437,6 +437,88 @@ int GetLineStartX(UiFlags flags, const Rectangle &rect, int lineWidth)
return rect.position.x;
}
void DrawLine(
const Surface &out,
std::string_view text,
Point characterPosition,
Rectangle rect,
UiFlags flags,
int curSpacing,
GameFontTables size,
text_color color,
bool outline,
const TextRenderOptions &opts,
size_t lineStartPos,
int totalWidth)
{
CurrentFont currentFont;
std::string_view lineCopy = text;
size_t currentPos = 0;
size_t cpLen;
const auto maybeDrawCursor = [&]() {
const auto byteIndex = static_cast<int>(lineStartPos + currentPos);
Point position = characterPosition;
if (opts.cursorPosition == byteIndex) {
if (GetAnimationFrame(2, 500) != 0 || opts.cursorStatic) {
FontStack baseFont = LoadFont(size, color, 0);
if (baseFont.has_value()) {
DrawFont(out, position, baseFont.glyph('|'), color, outline);
}
}
if (opts.renderedCursorPositionOut != nullptr) {
*opts.renderedCursorPositionOut = position;
}
}
};
// Start from the beginning of the line
characterPosition.x = GetLineStartX(flags, rect, totalWidth);
while (!lineCopy.empty()) {
char32_t c = DecodeFirstUtf8CodePoint(lineCopy, &cpLen);
if (c == Utf8DecodeError) break;
if (c == ZWSP) {
lineCopy.remove_prefix(cpLen);
continue;
}
if (!currentFont.load(size, color, c)) {
c = U'?';
if (!currentFont.load(size, color, c)) {
app_fatal("Missing fonts");
}
}
const uint8_t frame = c & 0xFF;
const ClxSprite glyph = currentFont.glyph(frame);
const int charWidth = glyph.width();
const auto byteIndex = static_cast<int>(lineStartPos + currentPos);
// Draw highlight
if (byteIndex >= opts.highlightRange.begin && byteIndex < opts.highlightRange.end) {
const bool lastInRange = static_cast<int>(byteIndex + cpLen) == opts.highlightRange.end;
FillRect(out, characterPosition.x, characterPosition.y,
glyph.width() + (lastInRange ? 0 : curSpacing), glyph.height(),
opts.highlightColor);
}
DrawFont(out, characterPosition, glyph, color, outline);
maybeDrawCursor();
// Move to the next position
characterPosition.x += charWidth + curSpacing;
currentPos += cpLen;
lineCopy.remove_prefix(cpLen);
}
assert(currentPos == text.size());
maybeDrawCursor();
}
uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect, Point &characterPosition,
int lineWidth, int charactersInLine, int rightMargin, int bottomMargin, GameFontTables size, text_color color, bool outline,
TextRenderOptions &opts)
@ -455,19 +537,26 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect,
std::string_view remaining = text;
size_t cpLen;
const auto maybeDrawCursor = [&]() {
if (opts.cursorPosition == static_cast<int>(text.size() - remaining.size())) {
Point position = characterPosition;
MaybeWrap(position, 2, rightMargin, position.x, opts.lineHeight);
if (GetAnimationFrame(2, 500) != 0) {
FontStack baseFont = LoadFont(size, color, 0);
if (baseFont.has_value()) {
DrawFont(out, position, baseFont.glyph('|'), color, outline);
}
}
if (opts.renderedCursorPositionOut != nullptr) {
*opts.renderedCursorPositionOut = position;
}
// Track line boundaries
size_t lineStartPos = 0;
size_t lineEndPos = 0;
const auto drawLine = [&]() {
std::string_view lineText = text.substr(lineStartPos, lineEndPos - lineStartPos);
if (!lineText.empty()) {
DrawLine(
out,
lineText,
characterPosition,
rect,
opts.flags,
curSpacing,
size,
color,
outline,
opts,
lineStartPos,
lineWidth);
}
};
@ -487,8 +576,10 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect,
const uint8_t frame = next & 0xFF;
const uint16_t width = currentFont.glyph(frame).width();
if (next == U'\n' || characterPosition.x + width > rightMargin) {
if (next == '\n')
maybeDrawCursor();
lineEndPos = text.size() - remaining.size();
drawLine();
const int nextLineY = characterPosition.y + opts.lineHeight;
if (nextLineY >= bottomMargin)
break;
@ -506,26 +597,26 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect,
}
characterPosition.x = GetLineStartX(opts.flags, rect, lineWidth);
// Start a new line
lineStartPos = next == U'\n' ? (text.size() - remaining.size() + cpLen) : (text.size() - remaining.size());
lineEndPos = lineStartPos;
if (next == U'\n')
continue;
}
const ClxSprite glyph = currentFont.glyph(frame);
const auto byteIndex = static_cast<int>(text.size() - remaining.size());
// Draw highlight
if (byteIndex >= opts.highlightRange.begin && byteIndex < opts.highlightRange.end) {
const bool lastInRange = static_cast<int>(byteIndex + cpLen) == opts.highlightRange.end;
FillRect(out, characterPosition.x, characterPosition.y,
glyph.width() + (lastInRange ? 0 : curSpacing), glyph.height(),
opts.highlightColor);
}
// Update end position as we add characters
lineEndPos = text.size() - remaining.size() + cpLen;
DrawFont(out, characterPosition, glyph, color, outline);
maybeDrawCursor();
// Update position for the next character
characterPosition.x += width + curSpacing;
}
maybeDrawCursor();
// Draw any remaining characters in the last line
if (lineStartPos < lineEndPos) {
drawLine();
}
return static_cast<uint32_t>(remaining.data() - text.data());
}

2
Source/engine/render/text_render.hpp

@ -155,6 +155,8 @@ struct TextRenderOptions {
/** @brief If a cursor is rendered, the surface coordinates are saved here. */
std::optional<Point> *renderedCursorPositionOut = nullptr;
bool cursorStatic = false;
};
/**

BIN
test/fixtures/text_render_integration_test/cursor-end.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
test/fixtures/text_render_integration_test/cursor-middle.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
test/fixtures/text_render_integration_test/cursor-start.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
test/fixtures/text_render_integration_test/highlight-full.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
test/fixtures/text_render_integration_test/highlight-partial.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
test/fixtures/text_render_integration_test/multiline_cursor-end_first_line.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
test/fixtures/text_render_integration_test/multiline_cursor-end_second_line.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
test/fixtures/text_render_integration_test/multiline_cursor-middle_second_line.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
test/fixtures/text_render_integration_test/multiline_cursor-start_second_line.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
test/fixtures/text_render_integration_test/multiline_highlight.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

110
test/text_render_integration_test.cpp

@ -211,6 +211,116 @@ const TestFixture Fixtures[] {
{ "Two", UiFlags::ColorUiSilverDark },
},
},
TestFixture {
.name = "cursor-start",
.width = 120,
.height = 15,
.fmt = "Hello World",
.opts = {
.flags = UiFlags::ColorUiGold,
.cursorPosition = 0, // Cursor at start
.cursorStatic = true,
},
},
TestFixture {
.name = "cursor-middle",
.width = 120,
.height = 15,
.fmt = "Hello World",
.opts = {
.flags = UiFlags::ColorUiGold,
.cursorPosition = 5, // Cursor after "Hello",
.cursorStatic = true,
},
},
TestFixture {
.name = "cursor-end",
.width = 120,
.height = 15,
.fmt = "Hello World",
.opts = {
.flags = UiFlags::ColorUiGold,
.cursorPosition = 11, // Cursor at end
.cursorStatic = true,
},
},
TestFixture {
.name = "multiline_cursor-end_first_line",
.width = 100,
.height = 50,
.fmt = "First line\nSecond line",
.opts = {
.flags = UiFlags::ColorUiGold,
.cursorPosition = 10, // Cursor at end of first line
.cursorStatic = true,
},
},
TestFixture {
.name = "multiline_cursor-start_second_line",
.width = 100,
.height = 50,
.fmt = "First line\nSecond line",
.opts = {
.flags = UiFlags::ColorUiGold,
.cursorPosition = 11, // Cursor at start of second line
.cursorStatic = true,
},
},
TestFixture {
.name = "multiline_cursor-middle_second_line",
.width = 100,
.height = 50,
.fmt = "First line\nSecond line",
.opts = {
.flags = UiFlags::ColorUiGold,
.cursorPosition = 14, // Cursor at second line, at the 'o' of "Second"
.cursorStatic = true,
},
},
TestFixture {
.name = "multiline_cursor-end_second_line",
.width = 100,
.height = 50,
.fmt = "First line\nSecond line",
.opts = {
.flags = UiFlags::ColorUiGold,
.cursorPosition = 22, // Cursor at start of second line
.cursorStatic = true,
},
},
TestFixture {
.name = "highlight-partial",
.width = 120,
.height = 15,
.fmt = "Hello World",
.opts = {
.flags = UiFlags::ColorUiGold,
.highlightRange = { 5, 10 }, // Highlight " Worl"
.highlightColor = PAL8_BLUE,
},
},
TestFixture {
.name = "highlight-full",
.width = 120,
.height = 15,
.fmt = "Hello World",
.opts = {
.flags = UiFlags::ColorUiGold,
.highlightRange = { 0, 11 }, // Highlight entire text
.highlightColor = PAL8_BLUE,
},
},
TestFixture {
.name = "multiline_highlight",
.width = 70,
.height = 50,
.fmt = "Hello\nWorld",
.opts = {
.flags = UiFlags::ColorUiGold,
.highlightRange = { 3, 8 }, // Highlight "lo\nWo"
.highlightColor = PAL8_BLUE,
},
},
};
SDLPaletteUniquePtr LoadPalette()

Loading…
Cancel
Save