From a83003b2285d88d9410b2bb3c0e9e9190d457927 Mon Sep 17 00:00:00 2001 From: iota97 Date: Fri, 24 Sep 2021 23:20:38 +0200 Subject: [PATCH 1/7] Fix text wrap edge case --- Common/Data/Text/WrapText.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Common/Data/Text/WrapText.cpp b/Common/Data/Text/WrapText.cpp index c88628acf5..abae02352d 100644 --- a/Common/Data/Text/WrapText.cpp +++ b/Common/Data/Text/WrapText.cpp @@ -119,7 +119,7 @@ void WordWrapper::AppendWord(int endIndex, bool addNewline) { } // This will include the newline. - if (x_ < maxW_) { + if (x_ <= maxW_) { out_.append(str_ + lastWordStartIndex, str_ + endIndex); } else { scanForNewline_ = true; From 92d13cc05b1f841d2e404cbfa781bbeb021ced32 Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sat, 25 Sep 2021 09:41:11 -0700 Subject: [PATCH 2/7] UI: Cleanup double ellipsis issues. We mostly only use FLAG_ELLIPSIZE_TEXT in PPGe, but it wasn't behaving well in some cases. --- Common/Data/Text/WrapText.cpp | 10 ++++++---- Common/Data/Text/WrapText.h | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Common/Data/Text/WrapText.cpp b/Common/Data/Text/WrapText.cpp index abae02352d..13b84fd85e 100644 --- a/Common/Data/Text/WrapText.cpp +++ b/Common/Data/Text/WrapText.cpp @@ -91,14 +91,16 @@ bool WordWrapper::WrapBeforeWord() { } } if (flags_ & FLAG_ELLIPSIZE_TEXT) { - if (x_ + wordWidth_ > maxW_) { + const bool hasEllipsis = out_.size() > 3 && out_.substr(out_.size() - 3) == "..."; + if (x_ + wordWidth_ > maxW_ && !hasEllipsis) { if (!out_.empty() && IsSpace(out_[out_.size() - 1])) { out_[out_.size() - 1] = '.'; out_ += ".."; } else { out_ += "..."; } - x_ = maxW_; + x_ += ellipsisWidth_; + skipNextWord_ = true; } } return false; @@ -119,7 +121,7 @@ void WordWrapper::AppendWord(int endIndex, bool addNewline) { } // This will include the newline. - if (x_ <= maxW_) { + if (x_ <= maxW_ && !skipNextWord_) { out_.append(str_ + lastWordStartIndex, str_ + endIndex); } else { scanForNewline_ = true; @@ -222,7 +224,7 @@ void WordWrapper::Wrap() { } if ((flags_ & FLAG_ELLIPSIZE_TEXT) && wordWidth_ > 0.0f && x_ + newWordWidth + ellipsisWidth_ > maxW_) { - if ((flags_ & FLAG_WRAP_TEXT) == 0) { + 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(beforeIndex, true); if (lastLineStart_ != out_.size()) { diff --git a/Common/Data/Text/WrapText.h b/Common/Data/Text/WrapText.h index 554210e3cf..dbb233ab7a 100644 --- a/Common/Data/Text/WrapText.h +++ b/Common/Data/Text/WrapText.h @@ -40,4 +40,6 @@ protected: bool forceEarlyWrap_ = false; // Skip all characters until the next newline. bool scanForNewline_ = false; + // Skip the next word, replaced with ellipsis. + bool skipNextWord_ = false; }; From 3f39bfeae9bb693b674295b3682ba7352fbf08d3 Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sat, 25 Sep 2021 10:58:45 -0700 Subject: [PATCH 3/7] UI: Cleanup more ellipsis cases, refactor. --- Common/Data/Text/WrapText.cpp | 92 +++++++++++++++++++++++------------ Common/Data/Text/WrapText.h | 3 ++ 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/Common/Data/Text/WrapText.cpp b/Common/Data/Text/WrapText.cpp index 13b84fd85e..570bcdb5d9 100644 --- a/Common/Data/Text/WrapText.cpp +++ b/Common/Data/Text/WrapText.cpp @@ -93,19 +93,23 @@ bool WordWrapper::WrapBeforeWord() { if (flags_ & FLAG_ELLIPSIZE_TEXT) { const bool hasEllipsis = out_.size() > 3 && out_.substr(out_.size() - 3) == "..."; if (x_ + wordWidth_ > maxW_ && !hasEllipsis) { - if (!out_.empty() && IsSpace(out_[out_.size() - 1])) { - out_[out_.size() - 1] = '.'; - out_ += ".."; - } else { - out_ += "..."; - } - x_ += ellipsisWidth_; + AddEllipsis(); skipNextWord_ = true; } } return false; } +void WordWrapper::AddEllipsis() { + if (!out_.empty() && IsSpace(out_[out_.size() - 1])) { + out_[out_.size() - 1] = '.'; + out_ += ".."; + } else { + out_ += "..."; + } + x_ += ellipsisWidth_; +} + void WordWrapper::AppendWord(int endIndex, bool addNewline) { int lastWordStartIndex = lastIndex_; if (WrapBeforeWord()) { @@ -120,8 +124,14 @@ void WordWrapper::AppendWord(int endIndex, bool addNewline) { } } + lastEllipsisIndex_ = -1; + if (skipNextWord_) { + lastIndex_ = endIndex; + return; + } + // This will include the newline. - if (x_ <= maxW_ && !skipNextWord_) { + if (x_ <= maxW_) { out_.append(str_ + lastWordStartIndex, str_ + endIndex); } else { scanForNewline_ = true; @@ -130,14 +140,23 @@ void WordWrapper::AppendWord(int endIndex, bool addNewline) { out_ += "\n"; lastLineStart_ = out_.size(); scanForNewline_ = false; + x_ = 0.0f; } else { // We may have appended a newline - check. size_t pos = out_.substr(lastLineStart_).find_last_of("\n"); if (pos != out_.npos) { lastLineStart_ += pos; } + + 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_); + } else { + x_ = 0.0f; + } } lastIndex_ = endIndex; + wordWidth_ = 0.0f; } void WordWrapper::Wrap() { @@ -168,11 +187,10 @@ void WordWrapper::Wrap() { if (c == '\n') { // This will include the newline character. AppendWord(afterIndex, false); - x_ = 0.0f; - wordWidth_ = 0.0f; // We wrapped once, so stop forcing. forceEarlyWrap_ = false; scanForNewline_ = false; + skipNextWord_ = false; continue; } @@ -188,16 +206,41 @@ void WordWrapper::Wrap() { // Is this the end of a word (space)? if (wordWidth_ > 0.0f && IsSpace(c)) { AppendWord(afterIndex, false); - // To account for kerning around spaces, we recalculate the entire line width. - x_ = MeasureWidth(out_.c_str() + lastLineStart_, out_.size() - lastLineStart_); - wordWidth_ = 0.0f; + skipNextWord_ = false; continue; } + // We're scanning for the next word. + if (skipNextWord_) + continue; + + if ((flags_ & FLAG_ELLIPSIZE_TEXT) != 0 && wordWidth_ > 0.0f && lastEllipsisIndex_ == -1) { + float checkX = x_; + // If we allow wrapping, assume we'll wrap as needed. + if ((flags_ & FLAG_WRAP_TEXT) != 0 && x_ >= maxW_) { + checkX = 0; + } + + // If we can only fit an ellipsis, time to output and skip ahead. + // Ignore x for newWordWidth, because we might wrap. + if (checkX + wordWidth_ + ellipsisWidth_ <= maxW_ && newWordWidth + ellipsisWidth_ > maxW_) { + lastEllipsisIndex_ = beforeIndex; + continue; + } + } + // Can the word fit on a line even all by itself so far? if (wordWidth_ > 0.0f && newWordWidth > maxW_) { - // Nope. Let's drop what's there so far onto its own line. - if (x_ > 0.0f && x_ + wordWidth_ > maxW_ && beforeIndex > lastIndex_) { + // If we had a good place for an ellipsis, let's do that. + if (lastEllipsisIndex_ != -1) { + AppendWord(lastEllipsisIndex_, false); + AddEllipsis(); + skipNextWord_ = true; + continue; + } + + // Doesn't fit. Let's drop what's there so far onto its own line. + if (x_ > 0.0f && x_ + wordWidth_ > maxW_ && beforeIndex > lastIndex_ && (flags_ & FLAG_WRAP_TEXT) != 0) { // Let's put as many characters as will fit on the previous line. // This word can't fit on one line even, so it's going to be cut into pieces anyway. // Better to avoid huge gaps, in that case. @@ -212,12 +255,6 @@ void WordWrapper::Wrap() { } // Now, add the word so far (without this latest character) and break. AppendWord(beforeIndex, true); - if (lastLineStart_ != out_.size()) { - x_ = MeasureWidth(out_.c_str() + lastLineStart_, out_.size() - lastLineStart_); - } else { - x_ = 0.0f; - } - wordWidth_ = 0.0f; forceEarlyWrap_ = false; // The current character will be handled as part of the next word. continue; @@ -226,15 +263,10 @@ 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(beforeIndex, true); - if (lastLineStart_ != out_.size()) { - x_ = MeasureWidth(out_.c_str() + lastLineStart_, out_.size() - lastLineStart_); - } else { - x_ = 0.0f; - } - wordWidth_ = 0.0f; + AppendWord(lastEllipsisIndex_ != -1 ? lastEllipsisIndex_ : beforeIndex, false); + AddEllipsis(); forceEarlyWrap_ = false; - // The current character will be handled as part of the next word. + skipNextWord_ = true; continue; } } @@ -245,8 +277,6 @@ void WordWrapper::Wrap() { 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); - x_ += wordWidth_; - wordWidth_ = 0.0f; } } diff --git a/Common/Data/Text/WrapText.h b/Common/Data/Text/WrapText.h index dbb233ab7a..e074af80bb 100644 --- a/Common/Data/Text/WrapText.h +++ b/Common/Data/Text/WrapText.h @@ -15,6 +15,7 @@ protected: void Wrap(); bool WrapBeforeWord(); void AppendWord(int endIndex, bool addNewline); + void AddEllipsis(); static bool IsCJK(uint32_t c); static bool IsPunctuation(uint32_t c); @@ -28,6 +29,8 @@ protected: // Index of last output / start of current word. int lastIndex_ = 0; + // Ideal place to put an ellipsis if we run out of space. + int lastEllipsisIndex_ = -1; // Index of last line start. size_t lastLineStart_ = 0; // Position the current word starts at. From 7d730f2a8bdc31b55e7d33effee7f8541205da8d Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sat, 25 Sep 2021 10:59:54 -0700 Subject: [PATCH 4/7] UI: Add unit tests for text wrapping. --- unittest/UnitTest.cpp | 69 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index b0585bc05b..3741dcd0c2 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -40,13 +40,15 @@ #include #endif +#include "Common/Data/Text/Parsers.h" +#include "Common/Data/Text/WrapText.h" +#include "Common/Data/Encoding/Utf8.h" +#include "Common/File/Path.h" +#include "Common/Input/InputState.h" +#include "Common/Math/math_util.h" +#include "Common/Render/DrawBuffer.h" #include "Common/System/NativeApp.h" #include "Common/System/System.h" -#include "Common/Input/InputState.h" -#include "Common/File/Path.h" -#include "Common/Math/math_util.h" -#include "Common/Data/Text/Parsers.h" -#include "Common/Data/Encoding/Utf8.h" #include "Common/ArmEmitter.h" #include "Common/BitScan.h" @@ -659,6 +661,62 @@ static bool TestAndroidContentURI() { return true; } +class UnitTestWordWrapper : public WordWrapper { +public: + UnitTestWordWrapper(const char *str, float maxW, int flags) + : WordWrapper(str, maxW, flags) { + } + +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]) { + case ' ': + case '.': + w += 1; + break; + default: + w += 2; + break; + } + } + + return w; + } +}; + +#define EXPECT_WORDWRAP_EQ_STR(a, l, f, b) if (UnitTestWordWrapper(a, l, f).Wrapped() != b) { printf("%s: Test Fail (%d, %s)\n%s\nvs\n%s\n", __FUNCTION__, l, #f, UnitTestWordWrapper(a, l, f).Wrapped().c_str(), std::string(b).c_str()); return false; } + +static bool TestWrapText() { + // If there's enough space, it shouldn't wrap. This is exactly enough. + EXPECT_WORDWRAP_EQ_STR("Hello", 10, 0, "Hello"); + EXPECT_WORDWRAP_EQ_STR("Hello", 10, FLAG_WRAP_TEXT, "Hello"); + EXPECT_WORDWRAP_EQ_STR("Hello", 10, FLAG_ELLIPSIZE_TEXT, "Hello"); + EXPECT_WORDWRAP_EQ_STR("Hello", 10, FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT, "Hello"); + + // Try a single word that doesn't fit in the space. + EXPECT_WORDWRAP_EQ_STR("Hello", 6, 0, "Hello"); + EXPECT_WORDWRAP_EQ_STR("Hello", 6, FLAG_WRAP_TEXT, "Hel\nlo"); + EXPECT_WORDWRAP_EQ_STR("Hello", 6, FLAG_ELLIPSIZE_TEXT, "H..."); + EXPECT_WORDWRAP_EQ_STR("Hello", 6, FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT, "H..."); + + // Now, multiple words. + EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 14, 0, "Hello goodbye"); + EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 14, FLAG_WRAP_TEXT, "Hello \ngoodbye"); + EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 14, FLAG_ELLIPSIZE_TEXT, "Hello..."); + EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 14, FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT, "Hello \ngoodbye"); + + // Now, multiple words, but only the first fits. + EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 10, 0, "Hello "); + EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 10, FLAG_WRAP_TEXT, "Hello \ngoodb\nye"); + 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..."); + + return true; +} + typedef bool (*TestFunc)(); struct TestItem { const char *name; @@ -699,6 +757,7 @@ TestItem availableTests[] = { TEST_ITEM(Path), TEST_ITEM(AndroidContentURI), TEST_ITEM(ThreadManager), + TEST_ITEM(WrapText), }; int main(int argc, const char *argv[]) { From bbc83bcdabb3692eb5a63ce9de07ad211ad02592 Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sat, 25 Sep 2021 11:37:10 -0700 Subject: [PATCH 5/7] 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; } From 2f570481b79b842d4f4578b3cb5c66ca87528e4e Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sat, 25 Sep 2021 11:46:00 -0700 Subject: [PATCH 6/7] UI: Cleanup ellipsis more. Arg, silly me. --- Common/Data/Text/WrapText.cpp | 9 +++++++++ unittest/UnitTest.cpp | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/Common/Data/Text/WrapText.cpp b/Common/Data/Text/WrapText.cpp index 312047a8b3..6832389695 100644 --- a/Common/Data/Text/WrapText.cpp +++ b/Common/Data/Text/WrapText.cpp @@ -98,6 +98,9 @@ bool WordWrapper::WrapBeforeWord() { if (x_ + wordWidth_ > maxW_ && !hasEllipsis) { AddEllipsis(); skipNextWord_ = true; + if ((flags_ & FLAG_WRAP_TEXT) == 0) { + scanForNewline_ = true; + } } } return false; @@ -250,6 +253,9 @@ void WordWrapper::Wrap() { AppendWord(lastEllipsisIndex_, -1, false); AddEllipsis(); skipNextWord_ = true; + if ((flags_ & FLAG_WRAP_TEXT) == 0) { + scanForNewline_ = true; + } continue; } @@ -281,6 +287,9 @@ void WordWrapper::Wrap() { AddEllipsis(); forceEarlyWrap_ = false; skipNextWord_ = true; + if ((flags_ & FLAG_WRAP_TEXT) == 0) { + scanForNewline_ = true; + } continue; } } diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index b0e8a71bd0..757b0e281b 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -712,6 +712,12 @@ static bool TestWrapText() { EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 14, FLAG_ELLIPSIZE_TEXT, "Hello..."); EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 14, FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT, "Hello \ngoodbye"); + // Multiple words with something short after... + EXPECT_WORDWRAP_EQ_STR("Hello goodbye yes", 14, 0, "Hello goodbye "); + EXPECT_WORDWRAP_EQ_STR("Hello goodbye yes", 14, FLAG_WRAP_TEXT, "Hello \ngoodbye \nyes"); + EXPECT_WORDWRAP_EQ_STR("Hello goodbye yes", 14, FLAG_ELLIPSIZE_TEXT, "Hello..."); + EXPECT_WORDWRAP_EQ_STR("Hello goodbye yes", 14, FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT, "Hello \ngoodbye \nyes"); + // Now, multiple words, but only the first fits. EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 10, 0, "Hello "); EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 10, FLAG_WRAP_TEXT, "Hello \ngoodb\nye"); From 7dc328761768596b7a13570c558602f336662e14 Mon Sep 17 00:00:00 2001 From: "Unknown W. Brackets" Date: Sat, 25 Sep 2021 12:01:41 -0700 Subject: [PATCH 7/7] UI: Handle newlines after ellipsis. --- Common/Data/Text/WrapText.cpp | 9 ++++++--- unittest/UnitTest.cpp | 6 ++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Common/Data/Text/WrapText.cpp b/Common/Data/Text/WrapText.cpp index 6832389695..8d661d285e 100644 --- a/Common/Data/Text/WrapText.cpp +++ b/Common/Data/Text/WrapText.cpp @@ -153,9 +153,9 @@ void WordWrapper::AppendWord(int endIndex, int lastChar, bool addNewline) { x_ = 0.0f; } else { // We may have appended a newline - check. - size_t pos = out_.substr(lastLineStart_).find_last_of("\n"); + size_t pos = out_.find_last_of("\n"); if (pos != out_.npos) { - lastLineStart_ += pos; + lastLineStart_ = pos + 1; } if (lastChar == -1 && !out_.empty()) { @@ -202,12 +202,15 @@ void WordWrapper::Wrap() { // Is this a newline character, hard wrapping? if (c == '\n') { + if (skipNextWord_) { + lastIndex_ = beforeIndex; + skipNextWord_ = false; + } // This will include the newline character. AppendWord(afterIndex, c, false); // We wrapped once, so stop forcing. forceEarlyWrap_ = false; scanForNewline_ = false; - skipNextWord_ = false; continue; } diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 757b0e281b..50611ba241 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -731,6 +731,12 @@ static bool TestWrapText() { 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"); + // Newlines should not be removed and should influence wrapping. + EXPECT_WORDWRAP_EQ_STR("Hello\ngoodbye yes\nno", 14, 0, "Hello\ngoodbye "); + EXPECT_WORDWRAP_EQ_STR("Hello\ngoodbye yes\nno", 14, FLAG_WRAP_TEXT, "Hello\ngoodbye \nyes\nno"); + EXPECT_WORDWRAP_EQ_STR("Hello\ngoodbye yes\nno", 14, FLAG_ELLIPSIZE_TEXT, "Hello\ngoodb...\nno"); + EXPECT_WORDWRAP_EQ_STR("Hello\ngoodbye yes\nno", 14, FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT, "Hello\ngoodbye \nyes\nno"); + return true; }