From bbc83bcdabb3692eb5a63ce9de07ad211ad02592 Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sat, 25 Sep 2021 11:37:10 -0700 Subject: [PATCH] UI: Correct text wrap with shy or Unicode spaces. --- Common/Data/Text/WrapText.cpp | 46 +++++++++++++++++++++++------------ Common/Data/Text/WrapText.h | 7 +++++- unittest/UnitTest.cpp | 15 ++++++++++-- 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/Common/Data/Text/WrapText.cpp b/Common/Data/Text/WrapText.cpp index 570bcdb5d9..312047a8b3 100644 --- a/Common/Data/Text/WrapText.cpp +++ b/Common/Data/Text/WrapText.cpp @@ -78,12 +78,15 @@ std::string WordWrapper::Wrapped() { bool WordWrapper::WrapBeforeWord() { if (flags_ & FLAG_WRAP_TEXT) { if (x_ + wordWidth_ > maxW_ && !out_.empty()) { - if (IsShy(out_[out_.size() - 1])) { + if (IsShy(lastChar_)) { // Soft hyphen, replace it with a real hyphen since we wrapped at it. // TODO: There's an edge case here where the hyphen might not fit. - out_[out_.size() - 1] = '-'; + out_[out_.size() - 2] = '-'; + out_[out_.size() - 1] = '\n'; + } else { + out_ += "\n"; } - out_ += "\n"; + lastChar_ = '\n'; lastLineStart_ = out_.size(); x_ = 0.0f; forceEarlyWrap_ = false; @@ -101,16 +104,19 @@ bool WordWrapper::WrapBeforeWord() { } void WordWrapper::AddEllipsis() { - if (!out_.empty() && IsSpace(out_[out_.size() - 1])) { - out_[out_.size() - 1] = '.'; - out_ += ".."; + if (!out_.empty() && IsSpaceOrShy(lastChar_)) { + UTF8 utf(out_.c_str(), (int)out_.size()); + utf.bwd(); + out_.resize(utf.byteIndex()); + out_ += "..."; } else { out_ += "..."; } + lastChar_ = '.'; x_ += ellipsisWidth_; } -void WordWrapper::AppendWord(int endIndex, bool addNewline) { +void WordWrapper::AppendWord(int endIndex, int lastChar, bool addNewline) { int lastWordStartIndex = lastIndex_; if (WrapBeforeWord()) { // Advance to the first non-whitespace UTF-8 character in the following word (if any) to prevent starting the new line with a whitespace @@ -138,6 +144,7 @@ void WordWrapper::AppendWord(int endIndex, bool addNewline) { } if (addNewline && (flags_ & FLAG_WRAP_TEXT)) { out_ += "\n"; + lastChar_ = '\n'; lastLineStart_ = out_.size(); scanForNewline_ = false; x_ = 0.0f; @@ -148,6 +155,13 @@ void WordWrapper::AppendWord(int endIndex, bool addNewline) { lastLineStart_ += pos; } + if (lastChar == -1 && !out_.empty()) { + UTF8 utf(out_.c_str(), (int)out_.size()); + utf.bwd(); + lastChar = utf.next(); + } + lastChar_ = lastChar; + if (lastLineStart_ != out_.size()) { // To account for kerning around spaces, we recalculate the entire line width. x_ = MeasureWidth(out_.c_str() + lastLineStart_, out_.size() - lastLineStart_); @@ -186,7 +200,7 @@ void WordWrapper::Wrap() { // Is this a newline character, hard wrapping? if (c == '\n') { // This will include the newline character. - AppendWord(afterIndex, false); + AppendWord(afterIndex, c, false); // We wrapped once, so stop forcing. forceEarlyWrap_ = false; scanForNewline_ = false; @@ -203,9 +217,9 @@ void WordWrapper::Wrap() { // Measure the entire word for kerning purposes. May not be 100% perfect. float newWordWidth = MeasureWidth(str_ + lastIndex_, afterIndex - lastIndex_); - // Is this the end of a word (space)? - if (wordWidth_ > 0.0f && IsSpace(c)) { - AppendWord(afterIndex, false); + // Is this the end of a word (space)? We'll also output up to a soft hyphen. + if (wordWidth_ > 0.0f && IsSpaceOrShy(c)) { + AppendWord(afterIndex, c, false); skipNextWord_ = false; continue; } @@ -233,7 +247,7 @@ void WordWrapper::Wrap() { if (wordWidth_ > 0.0f && newWordWidth > maxW_) { // If we had a good place for an ellipsis, let's do that. if (lastEllipsisIndex_ != -1) { - AppendWord(lastEllipsisIndex_, false); + AppendWord(lastEllipsisIndex_, -1, false); AddEllipsis(); skipNextWord_ = true; continue; @@ -254,7 +268,7 @@ void WordWrapper::Wrap() { continue; } // Now, add the word so far (without this latest character) and break. - AppendWord(beforeIndex, true); + AppendWord(beforeIndex, -1, true); forceEarlyWrap_ = false; // The current character will be handled as part of the next word. continue; @@ -263,7 +277,7 @@ void WordWrapper::Wrap() { if ((flags_ & FLAG_ELLIPSIZE_TEXT) && wordWidth_ > 0.0f && x_ + newWordWidth + ellipsisWidth_ > maxW_) { if ((flags_ & FLAG_WRAP_TEXT) == 0 && x_ + wordWidth_ + ellipsisWidth_ <= maxW_) { // Now, add the word so far (without this latest character) and show the ellipsis. - AppendWord(lastEllipsisIndex_ != -1 ? lastEllipsisIndex_ : beforeIndex, false); + AppendWord(lastEllipsisIndex_ != -1 ? lastEllipsisIndex_ : beforeIndex, -1, false); AddEllipsis(); forceEarlyWrap_ = false; skipNextWord_ = true; @@ -276,10 +290,10 @@ void WordWrapper::Wrap() { // Is this the end of a word via punctuation / CJK? if (wordWidth_ > 0.0f && (IsCJK(c) || IsPunctuation(c) || forceEarlyWrap_)) { // CJK doesn't require spaces, so we treat each letter as its own word. - AppendWord(afterIndex, false); + AppendWord(afterIndex, c, false); } } // Now insert the rest of the string - the last word. - AppendWord((int)len, false); + AppendWord((int)len, 0, false); } diff --git a/Common/Data/Text/WrapText.h b/Common/Data/Text/WrapText.h index e074af80bb..bcdd19a6fd 100644 --- a/Common/Data/Text/WrapText.h +++ b/Common/Data/Text/WrapText.h @@ -14,13 +14,16 @@ protected: virtual float MeasureWidth(const char *str, size_t bytes) = 0; void Wrap(); bool WrapBeforeWord(); - void AppendWord(int endIndex, bool addNewline); + void AppendWord(int endIndex, int lastChar, bool addNewline); void AddEllipsis(); static bool IsCJK(uint32_t c); static bool IsPunctuation(uint32_t c); static bool IsSpace(uint32_t c); static bool IsShy(uint32_t c); + static bool IsSpaceOrShy(uint32_t c) { + return IsSpace(c) || IsShy(c); + } const char *const str_; const float maxW_; @@ -33,6 +36,8 @@ protected: int lastEllipsisIndex_ = -1; // Index of last line start. size_t lastLineStart_ = 0; + // Last character written to out_. + int lastChar_ = 0; // Position the current word starts at. float x_ = 0.0f; // Most recent width of word since last index. diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 3741dcd0c2..b0e8a71bd0 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -671,12 +671,16 @@ protected: float MeasureWidth(const char *str, size_t bytes) override { // Simple case for unit testing. int w = 0; - for (size_t i = 0; i < bytes; ++i) { - switch (str[i]) { + for (UTF8 utf(str); !utf.end() && utf.byteIndex() < bytes; ) { + uint32_t c = utf.next(); + switch (c) { case ' ': case '.': w += 1; break; + case 0x00AD: + // No width for soft hyphens. + break; default: w += 2; break; @@ -714,6 +718,13 @@ static bool TestWrapText() { EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 10, FLAG_ELLIPSIZE_TEXT, "Hel..."); EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 10, FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT, "Hello \ngoo..."); + // How about the shy character? + const std::string shyTestString = StringFromFormat("Very%c%clong", 0xC2, 0xAD); + EXPECT_WORDWRAP_EQ_STR(shyTestString.c_str(), 10, 0, shyTestString); + EXPECT_WORDWRAP_EQ_STR(shyTestString.c_str(), 10, FLAG_WRAP_TEXT, "Very-\nlong"); + EXPECT_WORDWRAP_EQ_STR(shyTestString.c_str(), 10, FLAG_ELLIPSIZE_TEXT, "Very..."); + EXPECT_WORDWRAP_EQ_STR(shyTestString.c_str(), 10, FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT, "Very-\nlong"); + return true; }