Merge pull request #8847 from unknownbrackets/ui-textwrap

Allow text wrapping in UI TextViews
This commit is contained in:
Henrik Rydgård 2016-07-05 13:25:17 +02:00 committed by GitHub
commit 1f8afd7467
16 changed files with 395 additions and 24 deletions

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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.

View File

@ -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,6 +127,7 @@ 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);

View File

@ -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 <QtOpenGL/QGLWidget>
#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,18 +132,51 @@ 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<std::string> 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;
@ -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() {

View File

@ -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_;

View File

@ -234,6 +234,7 @@
<ClInclude Include="gfx\gl_debug_log.h" />
<ClInclude Include="gfx\gl_lost_manager.h" />
<ClInclude Include="gfx\texture_atlas.h" />
<ClInclude Include="util\text\wrap_text.h" />
<ClInclude Include="gfx_es2\draw_buffer.h" />
<ClInclude Include="gfx_es2\draw_text.h" />
<ClInclude Include="gfx_es2\gl3stub.h" />
@ -689,6 +690,7 @@
<ClCompile Include="gfx\gl_debug_log.cpp" />
<ClCompile Include="gfx\gl_lost_manager.cpp" />
<ClCompile Include="gfx\texture_atlas.cpp" />
<ClCompile Include="util\text\wrap_text.cpp" />
<ClCompile Include="gfx_es2\draw_buffer.cpp" />
<ClCompile Include="gfx_es2\draw_text.cpp" />
<ClCompile Include="gfx_es2\gl3stub.c">

View File

@ -311,6 +311,9 @@
<ClInclude Include="net\sinks.h">
<Filter>net</Filter>
</ClInclude>
<ClInclude Include="util\text\wrap_text.h">
<Filter>util</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="gfx\gl_debug_log.cpp">
@ -751,6 +754,9 @@
<ClCompile Include="net\sinks.cpp">
<Filter>net</Filter>
</ClCompile>
<ClCompile Include="util\text\wrap_text.cpp">
<Filter>util</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<Filter Include="gfx">

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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<std::string> 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) {

View File

@ -1,6 +1,7 @@
set(SRCS
hash/hash.cpp
text/utf8.cpp
text/wrap_text.cpp
)
set(SRCS ${SRCS})

View File

@ -0,0 +1,183 @@
#include <cstring>
#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);
}

View File

@ -0,0 +1,35 @@
#pragma once
#include <string>
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_;
};