Browse Source

WordWrapString: Wrap on punctuation

Useful for Chinese and Japanese, which do not have spaces between words,
nor whitespace after punctuation.
pull/2999/head
Gleb Mazovetskiy 5 years ago committed by Anders Jenbo
parent
commit
0c694edaba
  1. 3
      CMakeLists.txt
  2. 3
      Source/DiabloUI/selconn.cpp
  3. 9
      Source/DiabloUI/selgame.cpp
  4. 4
      Source/DiabloUI/selok.cpp
  5. 4
      Source/DiabloUI/selyesno.cpp
  6. 4
      Source/control.cpp
  7. 78
      Source/engine/render/text_render.cpp
  8. 2
      Source/engine/render/text_render.hpp
  9. 3
      Source/error.cpp
  10. 3
      Source/help.cpp
  11. 3
      Source/minitext.cpp
  12. 23
      Source/panels/charpanel.cpp
  13. 3
      Source/plrmsg.cpp
  14. 12
      Source/utils/utf8.h

3
CMakeLists.txt

@ -992,9 +992,8 @@ if(NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
endif() endif()
if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
target_compile_options(libdevilutionx PUBLIC "/W3") target_compile_options(libdevilutionx PUBLIC "/W3" "/Zc:__cplusplus" "/utf-8")
target_compile_definitions(libdevilutionx PUBLIC _CRT_SECURE_NO_WARNINGS) target_compile_definitions(libdevilutionx PUBLIC _CRT_SECURE_NO_WARNINGS)
target_compile_options(libdevilutionx PUBLIC "/Zc:__cplusplus")
endif() endif()
if(APPLE) if(APPLE)

3
Source/DiabloUI/selconn.cpp

@ -112,7 +112,8 @@ void SelconnFocus(int value)
} }
strncpy(selconn_MaxPlayers, fmt::format(_("Players Supported: {:d}"), players).c_str(), sizeof(selconn_MaxPlayers)); strncpy(selconn_MaxPlayers, fmt::format(_("Players Supported: {:d}"), players).c_str(), sizeof(selconn_MaxPlayers));
WordWrapString(selconn_Description, DESCRIPTION_WIDTH); const std::string wrapped = WordWrapString(selconn_Description, DESCRIPTION_WIDTH);
strncpy(selconn_Description, wrapped.data(), sizeof(selconn_Description) - 1);
} }
void SelconnSelect(int value) void SelconnSelect(int value)

9
Source/DiabloUI/selgame.cpp

@ -109,7 +109,8 @@ void selgame_GameSelection_Focus(int value)
strncpy(selgame_Description, _("Enter an IP or a hostname and join a game already in progress at that address."), sizeof(selgame_Description) - 1); strncpy(selgame_Description, _("Enter an IP or a hostname and join a game already in progress at that address."), sizeof(selgame_Description) - 1);
break; break;
} }
WordWrapString(selgame_Description, DESCRIPTION_WIDTH); const std::string wrapped = WordWrapString(selgame_Description, DESCRIPTION_WIDTH);
strncpy(selgame_Description, wrapped.data(), sizeof(selgame_Description) - 1);
} }
/** /**
@ -212,7 +213,8 @@ void selgame_Diff_Focus(int value)
strncpy(selgame_Description, _("Hell Difficulty\nThe most powerful of the underworld's creatures lurk at the gateway into Hell. Only the most experienced characters should venture in this realm."), sizeof(selgame_Description) - 1); strncpy(selgame_Description, _("Hell Difficulty\nThe most powerful of the underworld's creatures lurk at the gateway into Hell. Only the most experienced characters should venture in this realm."), sizeof(selgame_Description) - 1);
break; break;
} }
WordWrapString(selgame_Description, DESCRIPTION_WIDTH); const std::string wrapped = WordWrapString(selgame_Description, DESCRIPTION_WIDTH);
strncpy(selgame_Description, wrapped.data(), sizeof(selgame_Description) - 1);
} }
bool IsDifficultyAllowed(int value) bool IsDifficultyAllowed(int value)
@ -339,7 +341,8 @@ void selgame_Speed_Focus(int value)
strncpy(selgame_Description, _("Fastest Speed\nThe minions of the underworld will rush to attack without hesitation. Only a true speed demon should enter at this pace."), sizeof(selgame_Description) - 1); strncpy(selgame_Description, _("Fastest Speed\nThe minions of the underworld will rush to attack without hesitation. Only a true speed demon should enter at this pace."), sizeof(selgame_Description) - 1);
break; break;
} }
WordWrapString(selgame_Description, DESCRIPTION_WIDTH); const std::string wrapped = WordWrapString(selgame_Description, DESCRIPTION_WIDTH);
strncpy(selgame_Description, wrapped.data(), sizeof(selgame_Description) - 1);
} }
void selgame_Speed_Esc() void selgame_Speed_Esc()

4
Source/DiabloUI/selok.cpp

@ -68,8 +68,8 @@ void UiSelOkDialog(const char *title, const char *body, bool background)
vecSelOkDialogItems.push_back(std::make_unique<UiListItem>(_("OK"), 0)); vecSelOkDialogItems.push_back(std::make_unique<UiListItem>(_("OK"), 0));
vecSelOkDialog.push_back(std::make_unique<UiList>(vecSelOkDialogItems, PANEL_LEFT + 230, (UI_OFFSET_Y + 390), 180, 35, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); vecSelOkDialog.push_back(std::make_unique<UiList>(vecSelOkDialogItems, PANEL_LEFT + 230, (UI_OFFSET_Y + 390), 180, 35, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold));
strncpy(dialogText, body, sizeof(dialogText) - 1); const std::string wrapped = WordWrapString(body, MESSAGE_WIDTH, GameFont24);
WordWrapString(dialogText, MESSAGE_WIDTH, GameFont24); strncpy(dialogText, wrapped.data(), sizeof(dialogText) - 1);
UiInitList(0, nullptr, selok_Select, selok_Esc, vecSelOkDialog, false, nullptr); UiInitList(0, nullptr, selok_Select, selok_Esc, vecSelOkDialog, false, nullptr);

4
Source/DiabloUI/selyesno.cpp

@ -55,8 +55,8 @@ bool UiSelHeroYesNoDialog(const char *title, const char *body)
vecSelYesNoDialogItems.push_back(std::make_unique<UiListItem>(_("No"), 1)); vecSelYesNoDialogItems.push_back(std::make_unique<UiListItem>(_("No"), 1));
vecSelYesNoDialog.push_back(std::make_unique<UiList>(vecSelYesNoDialogItems, PANEL_LEFT + 230, (UI_OFFSET_Y + 390), 180, 35, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); vecSelYesNoDialog.push_back(std::make_unique<UiList>(vecSelYesNoDialogItems, PANEL_LEFT + 230, (UI_OFFSET_Y + 390), 180, 35, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold));
strncpy(selyesno_confirmationMessage, body, sizeof(selyesno_confirmationMessage) - 1); const std::string wrapped = WordWrapString(body, MESSAGE_WIDTH, GameFont24);
WordWrapString(selyesno_confirmationMessage, MESSAGE_WIDTH, GameFont24); strncpy(selyesno_confirmationMessage, wrapped.data(), sizeof(selyesno_confirmationMessage) - 1);
UiInitList(vecSelYesNoDialogItems.size(), nullptr, SelyesnoSelect, SelyesnoEsc, vecSelYesNoDialog, true, nullptr); UiInitList(vecSelYesNoDialogItems.size(), nullptr, SelyesnoSelect, SelyesnoEsc, vecSelYesNoDialog, true, nullptr);

4
Source/control.cpp

@ -1642,12 +1642,12 @@ void DrawGoldSplit(const Surface &out, int amount)
tempstr[BufferSize - 1] = '\0'; tempstr[BufferSize - 1] = '\0';
// Pre-wrap the string at spaces, otherwise DrawString would hard wrap in the middle of words // Pre-wrap the string at spaces, otherwise DrawString would hard wrap in the middle of words
WordWrapString(tempstr, 200); const std::string wrapped = WordWrapString(tempstr, 200);
// The split gold dialog is roughly 4 lines high, but we need at least one line for the player to input an amount. // The split gold dialog is roughly 4 lines high, but we need at least one line for the player to input an amount.
// Using a clipping region 50 units high (approx 3 lines with a lineheight of 17) to ensure there is enough room left // Using a clipping region 50 units high (approx 3 lines with a lineheight of 17) to ensure there is enough room left
// for the text entered by the player. // for the text entered by the player.
DrawString(out, tempstr, { GetPanelPosition(UiPanels::Inventory, { dialogX + 31, 75 }), { 200, 50 } }, UiFlags::ColorWhitegold | UiFlags::AlignCenter, 1, 17); DrawString(out, wrapped, { GetPanelPosition(UiPanels::Inventory, { dialogX + 31, 75 }), { 200, 50 } }, UiFlags::ColorWhitegold | UiFlags::AlignCenter, 1, 17);
tempstr[0] = '\0'; tempstr[0] = '\0';
if (amount > 0) { if (amount > 0) {

78
Source/engine/render/text_render.cpp

@ -170,14 +170,16 @@ int GetLineWidth(string_view text, GameFontTables size, int spacing, int *charac
{ {
int lineWidth = 0; int lineWidth = 0;
std::string textBuffer(text); std::string textBuffer;
textBuffer.resize(textBuffer.size() + 4); // Buffer must be padded before calling utf8_decode() textBuffer.reserve(textBuffer.size() + 3); // Buffer must be padded before calling utf8_decode()
textBuffer.append(text.data(), text.size());
textBuffer.resize(textBuffer.size() + 3);
const char *textData = textBuffer.data(); const char *textData = textBuffer.data();
size_t i = 0; size_t i = 0;
uint32_t currentUnicodeRow = 0; uint32_t currentUnicodeRow = 0;
std::array<uint8_t, 256> *kerning = nullptr; std::array<uint8_t, 256> *kerning = nullptr;
uint32_t next; char32_t next;
int error; int error;
for (; *textData != '\0'; i++) { for (; *textData != '\0'; i++) {
textData = utf8_decode(textData, &next, &error); textData = utf8_decode(textData, &next, &error);
@ -217,27 +219,37 @@ int AdjustSpacingToFitHorizontally(int &lineWidth, int maxSpacing, int character
return maxSpacing - spacingRedux; return maxSpacing - spacingRedux;
} }
void WordWrapString(char *text, size_t width, GameFontTables size, int spacing) std::string WordWrapString(string_view text, size_t width, GameFontTables size, int spacing)
{ {
int lastKnownSpaceAt = -1; int lastBreakablePos = -1;
size_t lineWidth = 0; int lastBreakableLen;
char32_t lastBreakableCodePoint;
std::string textBuffer(text);
textBuffer.resize(textBuffer.size() + 4); // Buffer must be padded before calling utf8_decode() std::string input;
const char *textData = textBuffer.data(); std::string output;
input.reserve(input.size() + 3); // Buffer must be padded before calling utf8_decode()
input.append(text.data(), text.size());
input.resize(input.size() + 3);
output.reserve(text.size());
const char *begin = input.data();
const char *cur = begin;
const char *processedEnd = cur;
uint32_t currentUnicodeRow = 0; uint32_t currentUnicodeRow = 0;
size_t lineWidth = 0;
std::array<uint8_t, 256> *kerning = nullptr; std::array<uint8_t, 256> *kerning = nullptr;
uint32_t next; char32_t next;
int error; int error;
while (*textData != '\0') { while (*cur != '\0') {
textData = utf8_decode(textData, &next, &error); cur = utf8_decode(cur, &next, &error);
if (error) if (error != 0)
next = '?'; next = U'?';
if (next == '\n') { // Existing line break, scan next line if (next == U'\n') { // Existing line break, scan next line
lastKnownSpaceAt = -1; lastBreakablePos = -1;
lineWidth = 0; lineWidth = 0;
output.append(processedEnd, cur);
processedEnd = cur;
continue; continue;
} }
@ -252,8 +264,16 @@ void WordWrapString(char *text, size_t width, GameFontTables size, int spacing)
} }
lineWidth += (*kerning)[frame] + spacing; lineWidth += (*kerning)[frame] + spacing;
if (next == ' ') { if (IsAnyOf(next, U' ', U',', U'.', U'?', U'!')) {
lastKnownSpaceAt = textData - textBuffer.data() - 1; lastBreakablePos = static_cast<int>(cur - begin - 1);
lastBreakableLen = 1;
lastBreakableCodePoint = next;
continue;
}
if (IsAnyOf(next, U' ', U'', U'', U'', U'')) {
lastBreakablePos = static_cast<int>(cur - begin - 3);
lastBreakableLen = 3;
lastBreakableCodePoint = next;
continue; continue;
} }
@ -261,16 +281,24 @@ void WordWrapString(char *text, size_t width, GameFontTables size, int spacing)
continue; // String is still within the limit, continue to the next symbol continue; // String is still within the limit, continue to the next symbol
} }
if (lastKnownSpaceAt == -1) { // Single word longer than width if (lastBreakablePos == -1) { // Single word longer than width
continue; continue;
} }
// Break line and continue to next line // Break line and continue to next line
text[lastKnownSpaceAt] = '\n'; const char *end = &input[lastBreakablePos];
textData = &textBuffer.data()[lastKnownSpaceAt + 1]; if (!IsAnyOf(lastBreakableCodePoint, U' ', U' ')) {
lastKnownSpaceAt = -1; end += lastBreakableLen;
}
output.append(processedEnd, end);
output += '\n';
cur = &input[lastBreakablePos + lastBreakableLen];
processedEnd = cur;
lastBreakablePos = -1;
lineWidth = 0; lineWidth = 0;
} }
output.append(processedEnd, cur);
return output;
} }
/** /**
@ -317,7 +345,7 @@ uint32_t DrawString(const Surface &out, string_view text, const Rectangle &rect,
const char *textData = textBuffer.data(); const char *textData = textBuffer.data();
const char *previousPosition = textData; const char *previousPosition = textData;
uint32_t next; char32_t next;
uint32_t currentUnicodeRow = 0; uint32_t currentUnicodeRow = 0;
int error; int error;
for (; *textData != '\0'; previousPosition = textData) { for (; *textData != '\0'; previousPosition = textData) {

2
Source/engine/render/text_render.hpp

@ -61,7 +61,7 @@ void UnloadFonts(GameFontTables size, text_color color);
* @return Line width in pixels * @return Line width in pixels
*/ */
int GetLineWidth(string_view text, GameFontTables size = GameFont12, int spacing = 1, int *charactersInLine = nullptr); int GetLineWidth(string_view text, GameFontTables size = GameFont12, int spacing = 1, int *charactersInLine = nullptr);
void WordWrapString(char *text, size_t width, GameFontTables size = GameFont12, int spacing = 1); [[nodiscard]] std::string WordWrapString(string_view text, size_t width, GameFontTables size = GameFont12, int spacing = 1);
/** /**
* @brief Draws a line of text within a clipping rectangle (positioned relative to the origin of the output buffer). * @brief Draws a line of text within a clipping rectangle (positioned relative to the origin of the output buffer).

3
Source/error.cpp

@ -35,8 +35,7 @@ void InitNextLines()
char tempstr[1536]; // Longest test is about 768 chars * 2 for unicode char tempstr[1536]; // Longest test is about 768 chars * 2 for unicode
strcpy(tempstr, message.data()); strcpy(tempstr, message.data());
WordWrapString(tempstr, LineWidth, GameFont12, 1); const std::string paragraphs = WordWrapString(tempstr, LineWidth, GameFont12, 1);
const string_view paragraphs = tempstr;
size_t previous = 0; size_t previous = 0;
while (true) { while (true) {

3
Source/help.cpp

@ -106,8 +106,7 @@ void InitHelp()
for (const auto *text : HelpText) { for (const auto *text : HelpText) {
strcpy(tempString, _(text)); strcpy(tempString, _(text));
WordWrapString(tempString, 577); const std::string paragraph = WordWrapString(tempString, 577);
const string_view paragraph = tempString;
size_t previous = 0; size_t previous = 0;
while (true) { while (true) {

3
Source/minitext.cpp

@ -44,8 +44,7 @@ void LoadText(const char *text)
char tempstr[1536]; // Longest test is about 768 chars * 2 for unicode char tempstr[1536]; // Longest test is about 768 chars * 2 for unicode
strcpy(tempstr, text); strcpy(tempstr, text);
WordWrapString(tempstr, 543, GameFont30); const std::string paragraphs = WordWrapString(tempstr, 543, GameFont30);
const string_view paragraphs = tempstr;
size_t previous = 0; size_t previous = 0;
while (true) { while (true) {

23
Source/panels/charpanel.cpp

@ -199,16 +199,19 @@ void DrawPanelField(const Surface &out, Point pos, int len)
void DrawShadowString(const Surface &out, const PanelEntry &entry) void DrawShadowString(const Surface &out, const PanelEntry &entry)
{ {
if (entry.label == "") if (entry.label.empty())
return; return;
std::string text_tmp = _(entry.label.c_str()); constexpr int Spacing = 0;
char buffer[64]; const std::string &textStr = LanguageTranslate(entry.label.c_str());
int spacing = 0; string_view text;
strcpy(buffer, text_tmp.c_str()); std::string wrapped;
if (entry.labelLength > 0) if (entry.labelLength > 0) {
WordWrapString(buffer, entry.labelLength, GameFont12, spacing); wrapped = WordWrapString(textStr, entry.labelLength, GameFont12, Spacing);
std::string text(buffer); text = wrapped;
} else {
text = textStr;
}
UiFlags style = UiFlags::VerticalCenter; UiFlags style = UiFlags::VerticalCenter;
@ -221,8 +224,8 @@ void DrawShadowString(const Surface &out, const PanelEntry &entry)
labelPosition += Displacement { -entry.labelLength - 3, 0 }; labelPosition += Displacement { -entry.labelLength - 3, 0 };
} }
DrawString(out, text, { labelPosition + Displacement { -2, 2 }, { entry.labelLength, 20 } }, style | UiFlags::ColorBlack, spacing, 10); DrawString(out, text, { labelPosition + Displacement { -2, 2 }, { entry.labelLength, 20 } }, style | UiFlags::ColorBlack, Spacing, 10);
DrawString(out, text, { labelPosition, { entry.labelLength, 20 } }, style | UiFlags::ColorWhite, spacing, 10); DrawString(out, text, { labelPosition, { entry.labelLength, 20 } }, style | UiFlags::ColorWhite, Spacing, 10);
} }
void DrawStatButtons(const Surface &out) void DrawStatButtons(const Surface &out)

3
Source/plrmsg.cpp

@ -31,8 +31,7 @@ void PrintChatMessage(const Surface &out, int x, int y, int width, char *text, U
if (text[i] == '\n') if (text[i] == '\n')
text[i] = ' '; text[i] = ' ';
} }
WordWrapString(text, width); DrawString(out, WordWrapString(text, width), { { x, y }, { width, 0 } }, style, 1, 10);
DrawString(out, text, { { x, y }, { width, 0 } }, style, 1, 10);
} }
} // namespace } // namespace

12
Source/utils/utf8.h

@ -24,7 +24,7 @@
* occurs, this pointer will be a guess that depends on the particular * occurs, this pointer will be a guess that depends on the particular
* error, but it will always advance at least one byte. * error, but it will always advance at least one byte.
*/ */
inline const char *utf8_decode(const char *buf, uint32_t *c, int *e) inline const char *utf8_decode(const char *buf, char32_t *c, int *e)
{ {
static const char lengths[] = { static const char lengths[] = {
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
@ -47,10 +47,10 @@ inline const char *utf8_decode(const char *buf, uint32_t *c, int *e)
/* Assume a four-byte character and load four bytes. Unused bits are /* Assume a four-byte character and load four bytes. Unused bits are
* shifted out. * shifted out.
*/ */
*c = (uint32_t)(s[0] & masks[len]) << 18; *c = static_cast<char32_t>((s[0] & masks[len]) << 18);
*c |= (uint32_t)(s[1] & 0x3f) << 12; *c |= static_cast<char32_t>((s[1] & 0x3f) << 12);
*c |= (uint32_t)(s[2] & 0x3f) << 6; *c |= static_cast<char32_t>((s[2] & 0x3f) << 6);
*c |= (uint32_t)(s[3] & 0x3f) << 0; *c |= static_cast<char32_t>((s[3] & 0x3f) << 0);
*c >>= shiftc[len]; *c >>= shiftc[len];
/* Accumulate the various error conditions. */ /* Accumulate the various error conditions. */
@ -73,7 +73,7 @@ inline int FindLastUtf8Symbols(const char *text)
const char *textData = textBuffer.data(); const char *textData = textBuffer.data();
const char *previousPosition = textData; const char *previousPosition = textData;
uint32_t next; char32_t next;
int error; int error;
for (; *textData != '\0'; previousPosition = textData) { for (; *textData != '\0'; previousPosition = textData) {
textData = utf8_decode(textData, &next, &error); textData = utf8_decode(textData, &next, &error);

Loading…
Cancel
Save