mirror of
https://github.com/hrydgard/ppsspp.git
synced 2025-04-02 11:01:50 -04:00
Merge pull request #14931 from unknownbrackets/ui-wrap
Cleanup some issues in word wrapping
This commit is contained in:
commit
166cc072ce
3 changed files with 201 additions and 51 deletions
|
@ -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;
|
||||
|
@ -91,20 +94,32 @@ bool WordWrapper::WrapBeforeWord() {
|
|||
}
|
||||
}
|
||||
if (flags_ & FLAG_ELLIPSIZE_TEXT) {
|
||||
if (x_ + wordWidth_ > maxW_) {
|
||||
if (!out_.empty() && IsSpace(out_[out_.size() - 1])) {
|
||||
out_[out_.size() - 1] = '.';
|
||||
out_ += "..";
|
||||
} else {
|
||||
out_ += "...";
|
||||
const bool hasEllipsis = out_.size() > 3 && out_.substr(out_.size() - 3) == "...";
|
||||
if (x_ + wordWidth_ > maxW_ && !hasEllipsis) {
|
||||
AddEllipsis();
|
||||
skipNextWord_ = true;
|
||||
if ((flags_ & FLAG_WRAP_TEXT) == 0) {
|
||||
scanForNewline_ = true;
|
||||
}
|
||||
x_ = maxW_;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void WordWrapper::AppendWord(int endIndex, bool addNewline) {
|
||||
void WordWrapper::AddEllipsis() {
|
||||
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, 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
|
||||
|
@ -118,24 +133,47 @@ void WordWrapper::AppendWord(int endIndex, bool addNewline) {
|
|||
}
|
||||
}
|
||||
|
||||
lastEllipsisIndex_ = -1;
|
||||
if (skipNextWord_) {
|
||||
lastIndex_ = endIndex;
|
||||
return;
|
||||
}
|
||||
|
||||
// This will include the newline.
|
||||
if (x_ < maxW_) {
|
||||
if (x_ <= maxW_) {
|
||||
out_.append(str_ + lastWordStartIndex, str_ + endIndex);
|
||||
} else {
|
||||
scanForNewline_ = true;
|
||||
}
|
||||
if (addNewline && (flags_ & FLAG_WRAP_TEXT)) {
|
||||
out_ += "\n";
|
||||
lastChar_ = '\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");
|
||||
size_t pos = out_.find_last_of("\n");
|
||||
if (pos != out_.npos) {
|
||||
lastLineStart_ += pos;
|
||||
lastLineStart_ = pos + 1;
|
||||
}
|
||||
|
||||
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_);
|
||||
} else {
|
||||
x_ = 0.0f;
|
||||
}
|
||||
}
|
||||
lastIndex_ = endIndex;
|
||||
wordWidth_ = 0.0f;
|
||||
}
|
||||
|
||||
void WordWrapper::Wrap() {
|
||||
|
@ -164,10 +202,12 @@ 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, false);
|
||||
x_ = 0.0f;
|
||||
wordWidth_ = 0.0f;
|
||||
AppendWord(afterIndex, c, false);
|
||||
// We wrapped once, so stop forcing.
|
||||
forceEarlyWrap_ = false;
|
||||
scanForNewline_ = false;
|
||||
|
@ -183,19 +223,47 @@ 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);
|
||||
// To account for kerning around spaces, we recalculate the entire line width.
|
||||
x_ = MeasureWidth(out_.c_str() + lastLineStart_, out_.size() - lastLineStart_);
|
||||
wordWidth_ = 0.0f;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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_, -1, false);
|
||||
AddEllipsis();
|
||||
skipNextWord_ = true;
|
||||
if ((flags_ & FLAG_WRAP_TEXT) == 0) {
|
||||
scanForNewline_ = 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.
|
||||
|
@ -209,30 +277,22 @@ void WordWrapper::Wrap() {
|
|||
continue;
|
||||
}
|
||||
// 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;
|
||||
AppendWord(beforeIndex, -1, true);
|
||||
forceEarlyWrap_ = false;
|
||||
// The current character will be handled as part of the next word.
|
||||
continue;
|
||||
}
|
||||
|
||||
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()) {
|
||||
x_ = MeasureWidth(out_.c_str() + lastLineStart_, out_.size() - lastLineStart_);
|
||||
} else {
|
||||
x_ = 0.0f;
|
||||
}
|
||||
wordWidth_ = 0.0f;
|
||||
AppendWord(lastEllipsisIndex_ != -1 ? lastEllipsisIndex_ : beforeIndex, -1, false);
|
||||
AddEllipsis();
|
||||
forceEarlyWrap_ = false;
|
||||
// The current character will be handled as part of the next word.
|
||||
skipNextWord_ = true;
|
||||
if ((flags_ & FLAG_WRAP_TEXT) == 0) {
|
||||
scanForNewline_ = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -242,12 +302,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);
|
||||
x_ += wordWidth_;
|
||||
wordWidth_ = 0.0f;
|
||||
AppendWord(afterIndex, c, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Now insert the rest of the string - the last word.
|
||||
AppendWord((int)len, false);
|
||||
AppendWord((int)len, 0, false);
|
||||
}
|
||||
|
|
|
@ -14,12 +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_;
|
||||
|
@ -28,8 +32,12 @@ 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;
|
||||
// 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.
|
||||
|
@ -40,4 +48,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;
|
||||
};
|
||||
|
|
|
@ -40,13 +40,15 @@
|
|||
#include <jni.h>
|
||||
#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,85 @@ 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 (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;
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
// 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");
|
||||
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");
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
typedef bool (*TestFunc)();
|
||||
struct TestItem {
|
||||
const char *name;
|
||||
|
@ -699,6 +780,7 @@ TestItem availableTests[] = {
|
|||
TEST_ITEM(Path),
|
||||
TEST_ITEM(AndroidContentURI),
|
||||
TEST_ITEM(ThreadManager),
|
||||
TEST_ITEM(WrapText),
|
||||
};
|
||||
|
||||
int main(int argc, const char *argv[]) {
|
||||
|
|
Loading…
Add table
Reference in a new issue