diff --git a/CMakeLists.txt b/CMakeLists.txt index 126aced10..64424f455 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 c16f41476..81b8d47ec 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/UI/MiscScreens.cpp b/UI/MiscScreens.cpp index c94c9e6df..0070fa7fe 100644 --- a/UI/MiscScreens.cpp +++ b/UI/MiscScreens.cpp @@ -19,6 +19,7 @@ #include "base/functional.h" #include "base/colorutil.h" +#include "base/display.h" #include "base/timeutil.h" #include "gfx_es2/draw_buffer.h" #include "math/curves.h" @@ -231,7 +232,8 @@ void PromptScreen::CreateViews() { ViewGroup *leftColumn = new AnchorLayout(new LinearLayoutParams(1.0f)); root_->Add(leftColumn); - leftColumn->Add(new TextView(message_, ALIGN_LEFT, false, new AnchorLayoutParams(10, 10, NONE, NONE)))->SetClip(false); + float leftColumnWidth = dp_xres - actionMenuMargins.left - actionMenuMargins.right - 300.0f; + leftColumn->Add(new TextView(message_, ALIGN_LEFT | FLAG_WRAP_TEXT, false, new AnchorLayoutParams(leftColumnWidth, WRAP_CONTENT, 10, 10, NONE, NONE)))->SetClip(false); ViewGroup *rightColumnItems = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(300, FILL_PARENT, actionMenuMargins)); root_->Add(rightColumnItems); diff --git a/ext/native/Android.mk b/ext/native/Android.mk index 87e000427..4c55cf79d 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 298997743..dad19e0db 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(const 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; + + const AtlasFont &atlasfont_; + const 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]; @@ -367,6 +393,16 @@ void DrawBuffer::MeasureTextCount(int font, const char *text, int count, float * if (h) *h = atlasfont.height * fontscaley * lines; } +void DrawBuffer::MeasureTextRect(int font, const char *text, int count, const Bounds &bounds, float *w, float *h, int align) { + std::string toMeasure = std::string(text, count); + if (align & FLAG_WRAP_TEXT) { + AtlasWordWrapper wrapper(*atlas->fonts[font], fontscalex, toMeasure.c_str(), bounds.w); + toMeasure = wrapper.Wrapped(); + } + + MeasureTextCount(font, toMeasure.c_str(), (int)toMeasure.length(), w, h); +} + void DrawBuffer::MeasureText(int font, const char *text, float *w, float *h) { return MeasureTextCount(font, text, (int)strlen(text), w, h); } @@ -402,7 +438,12 @@ void DrawBuffer::DrawTextRect(int font, const char *text, float x, float y, floa y += h; } - DrawText(font, text, x, y, color, align); + std::string toDraw = text; + if (align & FLAG_WRAP_TEXT) { + AtlasWordWrapper wrapper(*atlas->fonts[font], fontscalex, toDraw.c_str(), w); + toDraw = wrapper.Wrapped(); + } + DrawText(font, toDraw.c_str(), x, y, color, align); } // ROTATE_* doesn't yet work right. diff --git a/ext/native/gfx_es2/draw_buffer.h b/ext/native/gfx_es2/draw_buffer.h index 1f95bca35..32658f1bf 100644 --- a/ext/native/gfx_es2/draw_buffer.h +++ b/ext/native/gfx_es2/draw_buffer.h @@ -38,7 +38,8 @@ enum { // Avoids using system font drawing as it's too slow. // Not actually used here but is reserved for whatever system wraps DrawBuffer. FLAG_DYNAMIC_ASCII = 2048, - FLAG_NO_PREFIX = 4096 // means to not process ampersands + FLAG_NO_PREFIX = 4096, // means to not process ampersands + FLAG_WRAP_TEXT = 8192, }; class Thin3DShaderSet; @@ -126,7 +127,8 @@ public: // NOTE: Count is in plain chars not utf-8 chars! void MeasureTextCount(int font, const char *text, int count, float *w, float *h); - + void MeasureTextRect(int font, const char *text, int count, const Bounds &bounds, float *w, float *h, int align = 0); + void DrawTextRect(int font, const char *text, float x, float y, float w, float h, Color color = 0xFFFFFFFF, int align = 0); void DrawText(int font, const char *text, float x, float y, Color color = 0xFFFFFFFF, int align = 0); void DrawTextShadow(int font, const char *text, float x, float y, Color color = 0xFFFFFFFF, int align = 0); diff --git a/ext/native/gfx_es2/draw_text.cpp b/ext/native/gfx_es2/draw_text.cpp index c3352307f..dddffda12 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,25 +132,58 @@ 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_; } +void TextDrawer::MeasureStringRect(const char *str, size_t len, const Bounds &bounds, float *w, float *h, int align) { + auto iter = fontMap_.find(fontHash_); + if (iter != fontMap_.end()) { + SelectObject(ctx_->hDC, iter->second->hFont); + } + + std::string toMeasure = std::string(str, len); + if (align & FLAG_WRAP_TEXT) { + bool rotated = (align & (ROTATE_90DEG_LEFT | ROTATE_90DEG_RIGHT)) != 0; + WrapString(toMeasure, toMeasure.c_str(), rotated ? bounds.h : bounds.w); + } + + std::vector lines; + SplitString(toMeasure, '\n', lines); + float total_w = 0.0f; + float total_h = 0.0f; + for (size_t i = 0; i < lines.size(); i++) { + SIZE size; + std::wstring wstr = ConvertUTF8ToWString(lines[i].length() == 0 ? " " : lines[i]); + GetTextExtentPoint32(ctx_->hDC, wstr.c_str(), (int)wstr.size(), &size); + if (total_w < size.cx * fontScaleX_) { + total_w = size.cx * fontScaleX_; + } + total_h += size.cy * fontScaleY_; + } + *w = total_w; + *h = total_h; +} + void TextDrawer::DrawString(DrawBuffer &target, const char *str, float x, float y, uint32_t color, int align) { if (!strlen(str)) return; uint32_t stringHash = hash::Fletcher((const uint8_t *)str, strlen(str)); uint32_t entryHash = stringHash ^ fontHash_; - + target.Flush(true); TextStringEntry *entry; @@ -250,10 +301,33 @@ 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 + *w = 0; + *h = 0; +#endif +} + +void TextDrawer::MeasureStringRect(const char *str, size_t len, const Bounds &bounds, float *w, float *h, int align) { + std::string toMeasure = std::string(str, len); + if (align & FLAG_WRAP_TEXT) { + bool rotated = (align & (ROTATE_90DEG_LEFT | ROTATE_90DEG_RIGHT)) != 0; + WrapString(toMeasure, toMeasure.c_str(), rotated ? bounds.h : bounds.w); + } + +#ifdef USING_QT_UI + QFont* font = fontMap_.find(fontHash_)->second; + QFontMetrics fm(*font); + QSize size = fm.size(0, QString::fromUtf8(toMeasure.c_str(), (int)toMeasure.size())); *w = (float)size.width() * fontScaleX_; *h = (float)size.height() * fontScaleY_; #else @@ -325,6 +399,11 @@ void TextDrawer::DrawString(DrawBuffer &target, const char *str, float x, float #endif +void TextDrawer::WrapString(std::string &out, const char *str, float maxW) { + TextDrawerWordWrapper wrapper(this, str, maxW); + out = wrapper.Wrapped(); +} + void TextDrawer::SetFontScale(float xscale, float yscale) { fontScaleX_ = xscale; fontScaleY_ = xscale; @@ -344,7 +423,13 @@ void TextDrawer::DrawStringRect(DrawBuffer &target, const char *str, const Bound y = bounds.y2(); } - DrawString(target, str, x, y, color, align); + std::string toDraw = str; + if (align & FLAG_WRAP_TEXT) { + bool rotated = (align & (ROTATE_90DEG_LEFT | ROTATE_90DEG_RIGHT)) != 0; + WrapString(toDraw, str, rotated ? bounds.h : bounds.w); + } + + DrawString(target, toDraw.c_str(), x, y, color, align); } void TextDrawer::OncePerFrame() { diff --git a/ext/native/gfx_es2/draw_text.h b/ext/native/gfx_es2/draw_text.h index 58caab3a0..48c2e11ad 100644 --- a/ext/native/gfx_es2/draw_text.h +++ b/ext/native/gfx_es2/draw_text.h @@ -49,6 +49,8 @@ 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 MeasureStringRect(const char *str, size_t len, const Bounds &bounds, float *w, float *h, int align = ALIGN_TOPLEFT); 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. @@ -57,6 +59,8 @@ public: private: Thin3DContext *thin3d_; + void WrapString(std::string &out, const char *str, float maxWidth); + int frameCount_; float fontScaleX_; float fontScaleY_; diff --git a/ext/native/native.vcxproj b/ext/native/native.vcxproj index ba4fccf8a..c85b5d243 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 c3226a514..15d0c1176 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/ui/ui_context.cpp b/ext/native/ui/ui_context.cpp index 020c23623..c344d8852 100644 --- a/ext/native/ui/ui_context.cpp +++ b/ext/native/ui/ui_context.cpp @@ -138,8 +138,23 @@ void UIContext::MeasureTextCount(const UI::FontStyle &style, const char *str, in Draw()->MeasureTextCount(style.atlasFont, str, count, x, y); } else { textDrawer_->SetFontScale(fontScaleX_, fontScaleY_); - std::string subset(str, count); - textDrawer_->MeasureString(subset.c_str(), x, y); + textDrawer_->MeasureString(str, count, x, y); + } +} + +void UIContext::MeasureTextRect(const UI::FontStyle &style, const char *str, int count, const Bounds &bounds, float *x, float *y, int align) const { + if ((align & FLAG_WRAP_TEXT) == 0) { + MeasureTextCount(style, str, count, x, y, align); + return; + } + + if (!textDrawer_ || (align & FLAG_DYNAMIC_ASCII)) { + float sizeFactor = (float)style.sizePts / 24.0f; + Draw()->SetFontScale(fontScaleX_ * sizeFactor, fontScaleY_ * sizeFactor); + Draw()->MeasureTextRect(style.atlasFont, str, count, bounds, x, y, align); + } else { + textDrawer_->SetFontScale(fontScaleX_, fontScaleY_); + textDrawer_->MeasureStringRect(str, count, bounds, x, y, align); } } diff --git a/ext/native/ui/ui_context.h b/ext/native/ui/ui_context.h index 3fba861d6..22f944148 100644 --- a/ext/native/ui/ui_context.h +++ b/ext/native/ui/ui_context.h @@ -61,6 +61,7 @@ public: void SetFontScale(float scaleX, float scaleY); void MeasureTextCount(const UI::FontStyle &style, const char *str, int count, float *x, float *y, int align = 0) const; void MeasureText(const UI::FontStyle &style, const char *str, float *x, float *y, int align = 0) const; + void MeasureTextRect(const UI::FontStyle &style, const char *str, int count, const Bounds &bounds, float *x, float *y, int align = 0) const; void DrawText(const char *str, float x, float y, uint32_t color, int align = 0); void DrawTextShadow(const char *str, float x, float y, uint32_t color, int align = 0); void DrawTextRect(const char *str, const Bounds &bounds, uint32_t color, int align = 0); diff --git a/ext/native/ui/view.cpp b/ext/native/ui/view.cpp index 7014e4b36..e1d1ba26e 100644 --- a/ext/native/ui/view.cpp +++ b/ext/native/ui/view.cpp @@ -630,20 +630,9 @@ void Thin3DTextureView::Draw(UIContext &dc) { } void TextView::GetContentDimensions(const UIContext &dc, float &w, float &h) const { - // MeasureText doesn't seem to work with line breaks, so do something more sophisticated. - std::vector lines; - SplitString(text_, '\n', lines); - float total_w = 0.f; - float total_h = 0.f; - for (size_t i = 0; i < lines.size(); i++) { - float temp_w, temp_h; - dc.MeasureText(small_ ? dc.theme->uiFontSmall : dc.theme->uiFont, lines[i].c_str(), &temp_w, &temp_h); - if (temp_w > total_w) - total_w = temp_w; - total_h += temp_h; - } - w = total_w; - h = total_h; + // We don't have the bounding w/h yet, so stick with hardset layout params. + Bounds bounds(0, 0, layoutParams_->width, layoutParams_->height); + dc.MeasureTextRect(small_ ? dc.theme->uiFontSmall : dc.theme->uiFont, text_.c_str(), (int)text_.length(), bounds, &w, &h, textAlign_); } void TextView::Draw(UIContext &dc) { diff --git a/ext/native/util/CMakeLists.txt b/ext/native/util/CMakeLists.txt index dc29a9b72..9d632a0ad 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 000000000..6f4010645 --- /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 000000000..a5f95d6bc --- /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_; +};