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()
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_options(libdevilutionx PUBLIC "/Zc:__cplusplus")
endif()
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));
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)

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);
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);
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)
@ -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);
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()

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));
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);
WordWrapString(dialogText, MESSAGE_WIDTH, GameFont24);
const std::string wrapped = WordWrapString(body, MESSAGE_WIDTH, GameFont24);
strncpy(dialogText, wrapped.data(), sizeof(dialogText) - 1);
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));
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);
WordWrapString(selyesno_confirmationMessage, MESSAGE_WIDTH, GameFont24);
const std::string wrapped = WordWrapString(body, MESSAGE_WIDTH, GameFont24);
strncpy(selyesno_confirmationMessage, wrapped.data(), sizeof(selyesno_confirmationMessage) - 1);
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';
// 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.
// 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.
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';
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;
std::string textBuffer(text);
textBuffer.resize(textBuffer.size() + 4); // Buffer must be padded before calling utf8_decode()
std::string textBuffer;
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();
size_t i = 0;
uint32_t currentUnicodeRow = 0;
std::array<uint8_t, 256> *kerning = nullptr;
uint32_t next;
char32_t next;
int error;
for (; *textData != '\0'; i++) {
textData = utf8_decode(textData, &next, &error);
@ -217,27 +219,37 @@ int AdjustSpacingToFitHorizontally(int &lineWidth, int maxSpacing, int character
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;
size_t lineWidth = 0;
std::string textBuffer(text);
textBuffer.resize(textBuffer.size() + 4); // Buffer must be padded before calling utf8_decode()
const char *textData = textBuffer.data();
int lastBreakablePos = -1;
int lastBreakableLen;
char32_t lastBreakableCodePoint;
std::string input;
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;
size_t lineWidth = 0;
std::array<uint8_t, 256> *kerning = nullptr;
uint32_t next;
char32_t next;
int error;
while (*textData != '\0') {
textData = utf8_decode(textData, &next, &error);
if (error)
next = '?';
while (*cur != '\0') {
cur = utf8_decode(cur, &next, &error);
if (error != 0)
next = U'?';
if (next == '\n') { // Existing line break, scan next line
lastKnownSpaceAt = -1;
if (next == U'\n') { // Existing line break, scan next line
lastBreakablePos = -1;
lineWidth = 0;
output.append(processedEnd, cur);
processedEnd = cur;
continue;
}
@ -252,8 +264,16 @@ void WordWrapString(char *text, size_t width, GameFontTables size, int spacing)
}
lineWidth += (*kerning)[frame] + spacing;
if (next == ' ') {
lastKnownSpaceAt = textData - textBuffer.data() - 1;
if (IsAnyOf(next, U' ', U',', U'.', U'?', U'!')) {
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;
}
@ -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
}
if (lastKnownSpaceAt == -1) { // Single word longer than width
if (lastBreakablePos == -1) { // Single word longer than width
continue;
}
// Break line and continue to next line
text[lastKnownSpaceAt] = '\n';
textData = &textBuffer.data()[lastKnownSpaceAt + 1];
lastKnownSpaceAt = -1;
const char *end = &input[lastBreakablePos];
if (!IsAnyOf(lastBreakableCodePoint, U' ', U' ')) {
end += lastBreakableLen;
}
output.append(processedEnd, end);
output += '\n';
cur = &input[lastBreakablePos + lastBreakableLen];
processedEnd = cur;
lastBreakablePos = -1;
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 *previousPosition = textData;
uint32_t next;
char32_t next;
uint32_t currentUnicodeRow = 0;
int error;
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
*/
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).

3
Source/error.cpp

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

3
Source/help.cpp

@ -106,8 +106,7 @@ void InitHelp()
for (const auto *text : HelpText) {
strcpy(tempString, _(text));
WordWrapString(tempString, 577);
const string_view paragraph = tempString;
const std::string paragraph = WordWrapString(tempString, 577);
size_t previous = 0;
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
strcpy(tempstr, text);
WordWrapString(tempstr, 543, GameFont30);
const string_view paragraphs = tempstr;
const std::string paragraphs = WordWrapString(tempstr, 543, GameFont30);
size_t previous = 0;
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)
{
if (entry.label == "")
if (entry.label.empty())
return;
std::string text_tmp = _(entry.label.c_str());
char buffer[64];
int spacing = 0;
strcpy(buffer, text_tmp.c_str());
if (entry.labelLength > 0)
WordWrapString(buffer, entry.labelLength, GameFont12, spacing);
std::string text(buffer);
constexpr int Spacing = 0;
const std::string &textStr = LanguageTranslate(entry.label.c_str());
string_view text;
std::string wrapped;
if (entry.labelLength > 0) {
wrapped = WordWrapString(textStr, entry.labelLength, GameFont12, Spacing);
text = wrapped;
} else {
text = textStr;
}
UiFlags style = UiFlags::VerticalCenter;
@ -221,8 +224,8 @@ void DrawShadowString(const Surface &out, const PanelEntry &entry)
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, { entry.labelLength, 20 } }, style | UiFlags::ColorWhite, 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);
}
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')
text[i] = ' ';
}
WordWrapString(text, width);
DrawString(out, text, { { x, y }, { width, 0 } }, style, 1, 10);
DrawString(out, WordWrapString(text, width), { { x, y }, { width, 0 } }, style, 1, 10);
}
} // namespace

12
Source/utils/utf8.h

@ -24,7 +24,7 @@
* occurs, this pointer will be a guess that depends on the particular
* 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[] = {
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
* shifted out.
*/
*c = (uint32_t)(s[0] & masks[len]) << 18;
*c |= (uint32_t)(s[1] & 0x3f) << 12;
*c |= (uint32_t)(s[2] & 0x3f) << 6;
*c |= (uint32_t)(s[3] & 0x3f) << 0;
*c = static_cast<char32_t>((s[0] & masks[len]) << 18);
*c |= static_cast<char32_t>((s[1] & 0x3f) << 12);
*c |= static_cast<char32_t>((s[2] & 0x3f) << 6);
*c |= static_cast<char32_t>((s[3] & 0x3f) << 0);
*c >>= shiftc[len];
/* Accumulate the various error conditions. */
@ -73,7 +73,7 @@ inline int FindLastUtf8Symbols(const char *text)
const char *textData = textBuffer.data();
const char *previousPosition = textData;
uint32_t next;
char32_t next;
int error;
for (; *textData != '\0'; previousPosition = textData) {
textData = utf8_decode(textData, &next, &error);

Loading…
Cancel
Save