diff --git a/CMakeLists.txt b/CMakeLists.txt index 126aced101..64424f4555 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1042,6 +1042,8 @@ add_library(native STATIC ext/native/util/text/utf8.cpp ext/native/util/text/parsers.h ext/native/util/text/parsers.cpp + ext/native/util/text/wrap_text.h + ext/native/util/text/wrap_text.cpp ext/native/util/const_map.h ext/native/ext/jpge/jpgd.cpp ext/native/ext/jpge/jpgd.h diff --git a/Qt/Native.pro b/Qt/Native.pro index c16f41476d..81b8d47ecf 100644 --- a/Qt/Native.pro +++ b/Qt/Native.pro @@ -101,6 +101,7 @@ SOURCES += \ $$P/ext/native/thread/*.cpp \ $$P/ext/native/ui/*.cpp \ $$P/ext/native/util/hash/hash.cpp \ + $$P/ext/native/util/text/wrap_text.cpp \ $$P/ext/native/util/text/utf8.cpp \ $$P/ext/native/util/text/parsers.cpp @@ -134,5 +135,6 @@ HEADERS += \ $$P/ext/native/ui/*.h \ $$P/ext/native/util/hash/hash.h \ $$P/ext/native/util/random/*.h \ + $$P/ext/native/util/text/wrap_text.h \ $$P/ext/native/util/text/utf8.h \ $$P/ext/native/util/text/parsers.h diff --git a/ext/native/Android.mk b/ext/native/Android.mk index 87e0004273..4c55cf79dc 100644 --- a/ext/native/Android.mk +++ b/ext/native/Android.mk @@ -92,6 +92,7 @@ LOCAL_SRC_FILES :=\ ui/virtual_input.cpp \ util/text/utf8.cpp \ util/text/parsers.cpp \ + util/text/wrap_text.cpp \ util/hash/hash.cpp LOCAL_CFLAGS := -O3 -DUSING_GLES2 -fsigned-char -fno-strict-aliasing -Wall -Wno-multichar -D__STDC_CONSTANT_MACROS diff --git a/ext/native/gfx_es2/draw_buffer.cpp b/ext/native/gfx_es2/draw_buffer.cpp index 2989977430..3fb2f55bb6 100644 --- a/ext/native/gfx_es2/draw_buffer.cpp +++ b/ext/native/gfx_es2/draw_buffer.cpp @@ -13,6 +13,7 @@ #include "gfx_es2/draw_text.h" #include "gfx_es2/glsl_program.h" #include "util/text/utf8.h" +#include "util/text/wrap_text.h" enum { // Enough? @@ -331,6 +332,31 @@ void DrawBuffer::DrawImage2GridH(ImageID atlas_image, float x1, float y1, float DrawTexRect(xb, y1, x2, y2, um, v1, u2, v2, color); } +class AtlasWordWrapper : public WordWrapper { +public: + // Note: maxW may be height if rotated. + AtlasWordWrapper(AtlasFont &atlasfont, float scale, const char *str, float maxW) : WordWrapper(str, maxW), atlasfont_(atlasfont), scale_(scale) { + } + +protected: + float MeasureWidth(const char *str, size_t bytes) override; + + AtlasFont &atlasfont_; + float scale_; +}; + +float AtlasWordWrapper::MeasureWidth(const char *str, size_t bytes) { + float w = 0.0f; + for (UTF8 utf(str); utf.byteIndex() < bytes; ) { + const AtlasChar *ch = atlasfont_.getChar(utf.next()); + if (!ch) + ch = atlasfont_.getChar('?'); + + w += ch->wx * scale_; + } + return w; +} + void DrawBuffer::MeasureTextCount(int font, const char *text, int count, float *w, float *h) { const AtlasFont &atlasfont = *atlas->fonts[font]; diff --git a/ext/native/gfx_es2/draw_text.cpp b/ext/native/gfx_es2/draw_text.cpp index c3352307f7..7da3e936c4 100644 --- a/ext/native/gfx_es2/draw_text.cpp +++ b/ext/native/gfx_es2/draw_text.cpp @@ -2,6 +2,7 @@ #include "base/stringutil.h" #include "thin3d/thin3d.h" #include "util/hash/hash.h" +#include "util/text/wrap_text.h" #include "util/text/utf8.h" #include "gfx_es2/draw_text.h" @@ -12,6 +13,23 @@ #include #endif +class TextDrawerWordWrapper : public WordWrapper { +public: + TextDrawerWordWrapper(TextDrawer *drawer, const char *str, float maxW) : WordWrapper(str, maxW), drawer_(drawer) { + } + +protected: + float MeasureWidth(const char *str, size_t bytes) override; + + TextDrawer *drawer_; +}; + +float TextDrawerWordWrapper::MeasureWidth(const char *str, size_t bytes) { + float w, h; + drawer_->MeasureString(str, bytes, &w, &h); + return w; +} + #if defined(_WIN32) && !defined(USING_QT_UI) #define WIN32_LEAN_AND_MEAN @@ -114,13 +132,17 @@ void TextDrawer::SetFont(uint32_t fontHandle) { } void TextDrawer::MeasureString(const char *str, float *w, float *h) { + MeasureString(str, strlen(str), w, h); +} + +void TextDrawer::MeasureString(const char *str, size_t len, float *w, float *h) { auto iter = fontMap_.find(fontHash_); if (iter != fontMap_.end()) { SelectObject(ctx_->hDC, iter->second->hFont); } SIZE size; - std::wstring wstr = ConvertUTF8ToWString(ReplaceAll(str, "\n", "\r\n")); + std::wstring wstr = ConvertUTF8ToWString(ReplaceAll(std::string(str, len), "\n", "\r\n")); GetTextExtentPoint32(ctx_->hDC, wstr.c_str(), (int)wstr.size(), &size); *w = size.cx * fontScaleX_; *h = size.cy * fontScaleY_; @@ -250,10 +272,14 @@ void TextDrawer::SetFont(uint32_t fontHandle) { } void TextDrawer::MeasureString(const char *str, float *w, float *h) { + MeasureString(str, strlen(str), w, h); +} + +void TextDrawer::MeasureString(const char *str, size_t len, float *w, float *h) { #ifdef USING_QT_UI QFont* font = fontMap_.find(fontHash_)->second; QFontMetrics fm(*font); - QSize size = fm.size(0, QString::fromUtf8(str)); + QSize size = fm.size(0, QString::fromUtf8(str, (int)len)); *w = (float)size.width() * fontScaleX_; *h = (float)size.height() * fontScaleY_; #else diff --git a/ext/native/gfx_es2/draw_text.h b/ext/native/gfx_es2/draw_text.h index 58caab3a09..faeac41a31 100644 --- a/ext/native/gfx_es2/draw_text.h +++ b/ext/native/gfx_es2/draw_text.h @@ -49,6 +49,7 @@ public: void SetFontScale(float xscale, float yscale); void MeasureString(const char *str, float *w, float *h); + void MeasureString(const char *str, size_t len, float *w, float *h); void DrawString(DrawBuffer &target, const char *str, float x, float y, uint32_t color, int align = ALIGN_TOPLEFT); void DrawStringRect(DrawBuffer &target, const char *str, const Bounds &bounds, uint32_t color, int align); // Use for housekeeping like throwing out old strings. diff --git a/ext/native/native.vcxproj b/ext/native/native.vcxproj index ba4fccf8a6..c85b5d243f 100644 --- a/ext/native/native.vcxproj +++ b/ext/native/native.vcxproj @@ -234,6 +234,7 @@ + @@ -689,6 +690,7 @@ + diff --git a/ext/native/native.vcxproj.filters b/ext/native/native.vcxproj.filters index c3226a5147..15d0c1176f 100644 --- a/ext/native/native.vcxproj.filters +++ b/ext/native/native.vcxproj.filters @@ -311,6 +311,9 @@ net + + util + @@ -751,6 +754,9 @@ net + + util + diff --git a/ext/native/util/CMakeLists.txt b/ext/native/util/CMakeLists.txt index dc29a9b726..9d632a0ad9 100644 --- a/ext/native/util/CMakeLists.txt +++ b/ext/native/util/CMakeLists.txt @@ -1,6 +1,7 @@ set(SRCS hash/hash.cpp text/utf8.cpp + text/wrap_text.cpp ) set(SRCS ${SRCS}) diff --git a/ext/native/util/text/wrap_text.cpp b/ext/native/util/text/wrap_text.cpp new file mode 100644 index 0000000000..6f40106455 --- /dev/null +++ b/ext/native/util/text/wrap_text.cpp @@ -0,0 +1,183 @@ +#include +#include "util/text/utf8.h" +#include "util/text/wrap_text.h" + +bool WordWrapper::IsCJK(uint32_t c) { + if (c < 0x1000) { + return false; + } + + // CJK characters can be wrapped more freely. + bool result = (c >= 0x1100 && c <= 0x11FF); // Hangul Jamo. + result = result || (c >= 0x2E80 && c <= 0x2FFF); // Kangxi Radicals etc. +#if 0 + result = result || (c >= 0x3040 && c <= 0x31FF); // Hiragana, Katakana, Hangul Compatibility Jamo etc. + result = result || (c >= 0x3200 && c <= 0x32FF); // CJK Enclosed + result = result || (c >= 0x3300 && c <= 0x33FF); // CJK Compatibility + result = result || (c >= 0x3400 && c <= 0x4DB5); // CJK Unified Ideographs Extension A +#else + result = result || (c >= 0x3040 && c <= 0x4DB5); // Above collapsed +#endif + result = result || (c >= 0x4E00 && c <= 0x9FBB); // CJK Unified Ideographs + result = result || (c >= 0xAC00 && c <= 0xD7AF); // Hangul Syllables + result = result || (c >= 0xF900 && c <= 0xFAD9); // CJK Compatibility Ideographs + result = result || (c >= 0x20000 && c <= 0x2A6D6); // CJK Unified Ideographs Extension B + result = result || (c >= 0x2F800 && c <= 0x2FA1D); // CJK Compatibility Supplement + return result; +} + +bool WordWrapper::IsPunctuation(uint32_t c) { + switch (c) { + // TODO: This list of punctuation is very incomplete. + case ',': + case '.': + case ':': + case '!': + case ')': + case '?': + case 0x00AD: // SOFT HYPHEN + case 0x3001: // IDEOGRAPHIC COMMA + case 0x3002: // IDEOGRAPHIC FULL STOP + case 0x06D4: // ARABIC FULL STOP + case 0xFF01: // FULLWIDTH EXCLAMATION MARK + case 0xFF09: // FULLWIDTH RIGHT PARENTHESIS + case 0xFF1F: // FULLWIDTH QUESTION MARK + return true; + + default: + return false; + } +} + +bool WordWrapper::IsSpace(uint32_t c) { + switch (c) { + case '\t': + case ' ': + case 0x2002: // EN SPACE + case 0x2003: // EM SPACE + case 0x3000: // IDEOGRAPHIC SPACE + return true; + + default: + return false; + } +} + +bool WordWrapper::IsShy(uint32_t c) { + return c == 0x00AD; // SOFT HYPHEN +} + +std::string WordWrapper::Wrapped() { + if (out_.empty()) { + Wrap(); + } + return out_; +} + +void WordWrapper::WrapBeforeWord() { + if (x_ + wordWidth_ > maxW_) { + if (IsShy(out_[out_.size() - 1])) { + // 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_ += "\n"; + x_ = 0.0f; + forceEarlyWrap_ = false; + } +} + +void WordWrapper::AppendWord(int endIndex, bool addNewline) { + WrapBeforeWord(); + // This will include the newline. + out_ += std::string(str_ + lastIndex_, endIndex - lastIndex_); + if (addNewline) { + out_ += "\n"; + } + lastIndex_ = endIndex; +} + +void WordWrapper::Wrap() { + out_.clear(); + + // First, let's check if it fits as-is. + size_t len = strlen(str_); + if (MeasureWidth(str_, len) <= maxW_) { + // If it fits, we don't need to go through each character. + out_ = str_; + return; + } + + for (UTF8 utf(str_); !utf.end(); ) { + int beforeIndex = utf.byteIndex(); + uint32_t c = utf.next(); + int afterIndex = utf.byteIndex(); + + // Is this a newline character, hard wrapping? + 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; + continue; + } + + float newWordWidth = 0.0f; + if (c == '\n') { + newWordWidth = wordWidth_; + } else { + // Measure the entire word for kerning purposes. May not be 100% perfect. + newWordWidth = MeasureWidth(str_ + lastIndex_, afterIndex - lastIndex_); + } + + // Is this the end of a word (space)? + if (wordWidth_ > 0.0f && IsSpace(c)) { + AppendWord(afterIndex, false); + // We include the space in the x increase. + // If the space takes it over, we'll wrap on the next word. + x_ += newWordWidth; + wordWidth_ = 0.0f; + 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_) { + // 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. + forceEarlyWrap_ = true; + + // Now rewind back to where the word started so we can wrap at the opportune moment. + wordWidth_ = 0.0f; + while (utf.byteIndex() > lastIndex_) { + utf.bwd(); + } + continue; + } + // Now, add the word so far (without this latest character) and break. + AppendWord(beforeIndex, true); + x_ = 0.0f; + wordWidth_ = 0.0f; + forceEarlyWrap_ = false; + // The current character will be handled as part of the next word. + continue; + } + + wordWidth_ = newWordWidth; + + // 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; + } + } + + // Now insert the rest of the string - the last word. + AppendWord((int)len, false); +} diff --git a/ext/native/util/text/wrap_text.h b/ext/native/util/text/wrap_text.h new file mode 100644 index 0000000000..a5f95d6bc8 --- /dev/null +++ b/ext/native/util/text/wrap_text.h @@ -0,0 +1,35 @@ +#pragma once + +#include + +class WordWrapper { +public: + WordWrapper(const char *str, float maxW) + : str_(str), maxW_(maxW), lastIndex_(0), x_(0.0f), forceEarlyWrap_(false) { + } + + std::string Wrapped(); + +protected: + virtual float MeasureWidth(const char *str, size_t bytes) = 0; + void Wrap(); + void WrapBeforeWord(); + void AppendWord(int endIndex, bool addNewline); + + static bool IsCJK(uint32_t c); + static bool IsPunctuation(uint32_t c); + static bool IsSpace(uint32_t c); + static bool IsShy(uint32_t c); + + const char *const str_; + const float maxW_; + std::string out_; + // Index of last output / start of current word. + int lastIndex_; + // Position the current word starts at. + float x_; + // Most recent width of word since last index. + float wordWidth_; + // Force the next word to cut partially and wrap. + bool forceEarlyWrap_; +};