mirror of
https://github.com/love2d/love.git
synced 2026-08-15 07:41:11 +02:00
Rewrote Font:getWrap (and thus love.graphics.printf's logic) to behave better. Resolves issue #799.
It now wraps text at the specified wrap limit even when there are no spaces in a line, and has improved performance.
This commit is contained in:
@@ -346,7 +346,7 @@ float Font::getHeight() const
|
||||
return (float) height;
|
||||
}
|
||||
|
||||
std::vector<Font::DrawCommand> Font::generateVertices(const std::string &text, std::vector<GlyphVertex> &vertices, float extra_spacing, Vector offset, TextInfo *info)
|
||||
std::vector<Font::DrawCommand> Font::generateVertices(const Codepoints &codepoints, std::vector<GlyphVertex> &vertices, float extra_spacing, Vector offset, TextInfo *info)
|
||||
{
|
||||
// Spacing counter and newline handling.
|
||||
float dx = offset.x;
|
||||
@@ -360,87 +360,78 @@ std::vector<Font::DrawCommand> Font::generateVertices(const std::string &text, s
|
||||
|
||||
// Pre-allocate space for the maximum possible number of vertices.
|
||||
size_t vertstartsize = vertices.size();
|
||||
vertices.reserve(vertstartsize + text.length() * 4);
|
||||
vertices.reserve(vertstartsize + codepoints.size());
|
||||
|
||||
uint32 prevglyph = 0;
|
||||
|
||||
try
|
||||
for (int i = 0; i < (int) codepoints.size(); i++)
|
||||
{
|
||||
utf8::iterator<std::string::const_iterator> i(text.begin(), text.begin(), text.end());
|
||||
utf8::iterator<std::string::const_iterator> end(text.end(), text.begin(), text.end());
|
||||
uint32 g = codepoints[i];
|
||||
|
||||
while (i != end)
|
||||
if (g == '\n')
|
||||
{
|
||||
uint32 g = *i++;
|
||||
if (dx > maxwidth)
|
||||
maxwidth = (int) dx;
|
||||
|
||||
if (g == '\n')
|
||||
{
|
||||
if (dx > maxwidth)
|
||||
maxwidth = (int) dx;
|
||||
|
||||
// Wrap newline, but do not print it.
|
||||
dy += floorf(getHeight() * getLineHeight() + 0.5f);
|
||||
dx = offset.x;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32 cacheid = textureCacheID;
|
||||
|
||||
const Glyph &glyph = findGlyph(g);
|
||||
|
||||
// If findGlyph invalidates the texture cache, re-start the loop.
|
||||
if (cacheid != textureCacheID)
|
||||
{
|
||||
i = utf8::iterator<std::string::const_iterator>(text.begin(), text.begin(), text.end());
|
||||
maxwidth = 0;
|
||||
dx = offset.x;
|
||||
dy = offset.y;
|
||||
drawcommands.clear();
|
||||
vertices.resize(vertstartsize);
|
||||
prevglyph = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add kerning to the current horizontal offset.
|
||||
dx += getKerning(prevglyph, g);
|
||||
|
||||
if (glyph.texture != 0)
|
||||
{
|
||||
// Copy the vertices and set their proper relative positions.
|
||||
for (int j = 0; j < 4; j++)
|
||||
{
|
||||
vertices.push_back(glyph.vertices[j]);
|
||||
vertices.back().x += dx;
|
||||
vertices.back().y += dy + lineheight;
|
||||
}
|
||||
|
||||
// Check if glyph texture has changed since the last iteration.
|
||||
if (drawcommands.empty() || drawcommands.back().texture != glyph.texture)
|
||||
{
|
||||
// Add a new draw command if the texture has changed.
|
||||
DrawCommand cmd;
|
||||
cmd.startvertex = (int) vertices.size() - 4;
|
||||
cmd.vertexcount = 0;
|
||||
cmd.texture = glyph.texture;
|
||||
drawcommands.push_back(cmd);
|
||||
}
|
||||
|
||||
drawcommands.back().vertexcount += 4;
|
||||
}
|
||||
|
||||
// Advance the x position for the next glyph.
|
||||
dx += glyph.spacing;
|
||||
|
||||
// Account for extra spacing given to space characters.
|
||||
if (g == ' ' && extra_spacing != 0.0f)
|
||||
dx = floorf(dx + extra_spacing);
|
||||
|
||||
prevglyph = g;
|
||||
// Wrap newline, but do not print it.
|
||||
dy += floorf(getHeight() * getLineHeight() + 0.5f);
|
||||
dx = offset.x;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch (utf8::exception &e)
|
||||
{
|
||||
throw love::Exception("UTF-8 decoding error: %s", e.what());
|
||||
|
||||
uint32 cacheid = textureCacheID;
|
||||
|
||||
const Glyph &glyph = findGlyph(g);
|
||||
|
||||
// If findGlyph invalidates the texture cache, re-start the loop.
|
||||
if (cacheid != textureCacheID)
|
||||
{
|
||||
i = 0;
|
||||
maxwidth = 0;
|
||||
dx = offset.x;
|
||||
dy = offset.y;
|
||||
drawcommands.clear();
|
||||
vertices.resize(vertstartsize);
|
||||
prevglyph = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add kerning to the current horizontal offset.
|
||||
dx += getKerning(prevglyph, g);
|
||||
|
||||
if (glyph.texture != 0)
|
||||
{
|
||||
// Copy the vertices and set their proper relative positions.
|
||||
for (int j = 0; j < 4; j++)
|
||||
{
|
||||
vertices.push_back(glyph.vertices[j]);
|
||||
vertices.back().x += dx;
|
||||
vertices.back().y += dy + lineheight;
|
||||
}
|
||||
|
||||
// Check if glyph texture has changed since the last iteration.
|
||||
if (drawcommands.empty() || drawcommands.back().texture != glyph.texture)
|
||||
{
|
||||
// Add a new draw command if the texture has changed.
|
||||
DrawCommand cmd;
|
||||
cmd.startvertex = (int) vertices.size() - 4;
|
||||
cmd.vertexcount = 0;
|
||||
cmd.texture = glyph.texture;
|
||||
drawcommands.push_back(cmd);
|
||||
}
|
||||
|
||||
drawcommands.back().vertexcount += 4;
|
||||
}
|
||||
|
||||
// Advance the x position for the next glyph.
|
||||
dx += glyph.spacing;
|
||||
|
||||
// Account for extra spacing given to space characters.
|
||||
if (g == ' ' && extra_spacing != 0.0f)
|
||||
dx = floorf(dx + extra_spacing);
|
||||
|
||||
prevglyph = g;
|
||||
|
||||
}
|
||||
|
||||
// Sort draw commands by texture first, and quad position in memory second
|
||||
@@ -455,43 +446,61 @@ std::vector<Font::DrawCommand> Font::generateVertices(const std::string &text, s
|
||||
info->width = maxwidth - offset.x;;
|
||||
info->height = (int) dy + (dx > 0.0f ? floorf(getHeight() * getLineHeight() + 0.5f) : 0) - offset.y;
|
||||
}
|
||||
|
||||
|
||||
return drawcommands;
|
||||
}
|
||||
|
||||
std::vector<Font::DrawCommand> Font::generateVertices(const std::string &text, std::vector<GlyphVertex> &vertices, float extra_spacing, Vector offset, TextInfo *info)
|
||||
{
|
||||
Codepoints codepoints;
|
||||
codepoints.reserve(text.size());
|
||||
|
||||
try
|
||||
{
|
||||
utf8::iterator<std::string::const_iterator> i(text.begin(), text.begin(), text.end());
|
||||
utf8::iterator<std::string::const_iterator> end(text.end(), text.begin(), text.end());
|
||||
|
||||
while (i != end)
|
||||
{
|
||||
uint32 g = *i++;
|
||||
codepoints.push_back(g);
|
||||
}
|
||||
}
|
||||
catch (utf8::exception &e)
|
||||
{
|
||||
throw love::Exception("UTF-8 decoding error: %s", e.what());
|
||||
}
|
||||
|
||||
return generateVertices(codepoints, vertices, extra_spacing, offset, info);
|
||||
}
|
||||
|
||||
std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const std::string &text, float wrap, AlignMode align, std::vector<GlyphVertex> &vertices, TextInfo *info)
|
||||
{
|
||||
if (wrap < 0.0f)
|
||||
wrap = std::numeric_limits<float>::max();
|
||||
|
||||
wrap = std::max(wrap, 0.0f);
|
||||
|
||||
uint32 cacheid = textureCacheID;
|
||||
|
||||
std::vector<DrawCommand> drawcommands;
|
||||
vertices.reserve(text.length() * 4);
|
||||
|
||||
// wrappedlines indicates which lines were automatically wrapped. It
|
||||
// has the same number of elements as lines_to_draw.
|
||||
std::vector<bool> wrappedlines;
|
||||
std::vector<int> widths;
|
||||
std::vector<std::string> lines;
|
||||
std::vector<Codepoints> lines;
|
||||
|
||||
// We only need the list of wrapped lines in 'justify' mode.
|
||||
getWrap(text, wrap, lines, &widths, align == ALIGN_JUSTIFY ? &wrappedlines : nullptr);
|
||||
getWrap(text, wrap, lines, &widths);
|
||||
|
||||
float extraspacing = 0.0f;
|
||||
int numspaces = 0;
|
||||
int i = 0;
|
||||
float y = 0.0f;
|
||||
float maxwidth = 0;
|
||||
float maxwidth = 0.0f;
|
||||
|
||||
for (const std::string &line : lines)
|
||||
for (int i = 0; i < (int) lines.size(); i++)
|
||||
{
|
||||
extraspacing = 0.0f;
|
||||
const auto &line = lines[i];
|
||||
|
||||
float width = (float) widths[i];
|
||||
love::Vector offset(0.0f, floorf(y));
|
||||
float extraspacing = 0.0f;
|
||||
|
||||
if (width > maxwidth)
|
||||
maxwidth = width;
|
||||
maxwidth = std::max(width, maxwidth);
|
||||
|
||||
switch (align)
|
||||
{
|
||||
@@ -502,12 +511,14 @@ std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const std::string
|
||||
offset.x = floorf((wrap - width) / 2.0f);
|
||||
break;
|
||||
case ALIGN_JUSTIFY:
|
||||
numspaces = (int) std::count(line.begin(), line.end(), ' ');
|
||||
if (wrappedlines[i] && numspaces >= 1)
|
||||
extraspacing = (wrap - width) / float(numspaces);
|
||||
{
|
||||
float numspaces = (float) std::count(line.begin(), line.end(), ' ');
|
||||
if (width < wrap && numspaces >= 1)
|
||||
extraspacing = (wrap - width) / numspaces;
|
||||
else
|
||||
extraspacing = 0.0f;
|
||||
break;
|
||||
}
|
||||
case ALIGN_LEFT:
|
||||
default:
|
||||
break;
|
||||
@@ -537,7 +548,6 @@ std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const std::string
|
||||
}
|
||||
|
||||
y += getHeight() * getLineHeight();
|
||||
i++;
|
||||
}
|
||||
|
||||
if (info != nullptr)
|
||||
@@ -636,15 +646,16 @@ int Font::getWidth(const std::string &str)
|
||||
while (getline(iss, line, '\n'))
|
||||
{
|
||||
int width = 0;
|
||||
uint32 prevglyph = 0;
|
||||
try
|
||||
{
|
||||
uint32 prevglyph = 0;
|
||||
|
||||
utf8::iterator<std::string::const_iterator> i(line.begin(), line.begin(), line.end());
|
||||
utf8::iterator<std::string::const_iterator> end(line.end(), line.begin(), line.end());
|
||||
|
||||
while (i != end)
|
||||
{
|
||||
uint32 c = *i++;
|
||||
|
||||
const Glyph &g = findGlyph(c);
|
||||
width += g.spacing + getKerning(prevglyph, c);
|
||||
|
||||
@@ -668,69 +679,117 @@ int Font::getWidth(char character)
|
||||
return g.spacing;
|
||||
}
|
||||
|
||||
void Font::getWrap(const std::string &text, float wrap, std::vector<std::string> &lines, std::vector<int> *linewidths, std::vector<bool> *wrappedlines)
|
||||
void Font::getWrap(const std::string &text, float wraplimit, std::vector<Codepoints> &lines, std::vector<int> *linewidths)
|
||||
{
|
||||
const float width_space = (float) getWidth(' ');
|
||||
|
||||
std::istringstream iss(text);
|
||||
std::string line;
|
||||
std::ostringstream string_builder;
|
||||
|
||||
// Split text at newlines.
|
||||
std::string line;
|
||||
std::istringstream iss(text);
|
||||
|
||||
// A wrapped line of text.
|
||||
Codepoints wline;
|
||||
|
||||
while (std::getline(iss, line, '\n'))
|
||||
{
|
||||
std::vector<std::string> words;
|
||||
std::istringstream word_iss(line);
|
||||
|
||||
// split line into words
|
||||
std::copy(std::istream_iterator<std::string>(word_iss), std::istream_iterator<std::string>(), std::back_inserter(words));
|
||||
|
||||
float width = 0.0f;
|
||||
float oldwidth = 0.0f;
|
||||
string_builder.str("");
|
||||
uint32 prevglyph = 0;
|
||||
|
||||
// Put words back together until a wrap occurs.
|
||||
for (const std::string &word : words)
|
||||
wline.clear();
|
||||
wline.reserve(line.length());
|
||||
|
||||
try
|
||||
{
|
||||
float wordwidth = (float) getWidth(word);
|
||||
width += wordwidth;
|
||||
utf8::iterator<std::string::const_iterator> i(line.begin(), line.begin(), line.end());
|
||||
utf8::iterator<std::string::const_iterator> end(line.end(), line.begin(), line.end());
|
||||
|
||||
// On wordwrap, push line to line buffer and clear string builder.
|
||||
if (width > wrap && oldwidth > 0)
|
||||
utf8::iterator<std::string::const_iterator> lastspaceit = end;
|
||||
|
||||
while (i != end)
|
||||
{
|
||||
int realw = (int) width;
|
||||
uint32 c = *i;
|
||||
|
||||
// Remove trailing space.
|
||||
std::string tmp = string_builder.str();
|
||||
lines.push_back(tmp.substr(0,tmp.size()-1));
|
||||
string_builder.str("");
|
||||
width = wordwidth;
|
||||
realw -= (int) width;
|
||||
const Glyph &g = findGlyph(c);
|
||||
float newwidth = width + g.spacing + getKerning(prevglyph, c);
|
||||
|
||||
if (linewidths)
|
||||
linewidths->push_back(realw);
|
||||
// Wrap the line if it exceeds the wrap limit. Don't wrap yet if
|
||||
// we're processing a newline character, though.
|
||||
if (c != ' ' && newwidth > wraplimit)
|
||||
{
|
||||
// If this is the first character in the line and it exceeds
|
||||
// the limit, skip it completely.
|
||||
if (wline.empty())
|
||||
++i;
|
||||
else if (lastspaceit != end)
|
||||
{
|
||||
// 'Rewind' to the last seen space, if the line has one.
|
||||
// FIXME: This should preferably use vector::erase.
|
||||
while (!wline.empty() && wline.back() != ' ')
|
||||
wline.pop_back();
|
||||
|
||||
// Indicate that this line was automatically wrapped.
|
||||
if (wrappedlines)
|
||||
wrappedlines->push_back(true);
|
||||
i = lastspaceit;
|
||||
++i; // Start the next line after the space.
|
||||
}
|
||||
|
||||
lines.push_back(wline);
|
||||
|
||||
if (linewidths)
|
||||
linewidths->push_back(width);
|
||||
|
||||
prevglyph = 0; // Reset kerning information.
|
||||
width = 0.0f;
|
||||
wline.clear();
|
||||
lastspaceit = end;
|
||||
continue;
|
||||
}
|
||||
|
||||
width = newwidth;
|
||||
prevglyph = c;
|
||||
|
||||
wline.push_back(c);
|
||||
|
||||
// Keep track of the last seen space, so we can "rewind" to it
|
||||
// when wrapping.
|
||||
if (c == ' ')
|
||||
lastspaceit = i;
|
||||
|
||||
++i;
|
||||
}
|
||||
|
||||
string_builder << word << " ";
|
||||
|
||||
width += width_space;
|
||||
oldwidth = width;
|
||||
}
|
||||
catch (utf8::exception &e)
|
||||
{
|
||||
throw love::Exception("UTF-8 decoding error: %s", e.what());
|
||||
}
|
||||
|
||||
// Push last line.
|
||||
// Remove trailing newlines.
|
||||
if (!wline.empty() && wline.back() == '\n')
|
||||
wline.pop_back();
|
||||
|
||||
lines.push_back(wline);
|
||||
|
||||
if (linewidths)
|
||||
linewidths->push_back(width);
|
||||
}
|
||||
}
|
||||
|
||||
std::string tmp = string_builder.str();
|
||||
lines.push_back(tmp.substr(0,tmp.size()-1));
|
||||
void Font::getWrap(const std::string &text, float wraplimit, std::vector<std::string> &lines, std::vector<int> *linewidths)
|
||||
{
|
||||
std::vector<Codepoints> codepointlines;
|
||||
getWrap(text, wraplimit, codepointlines, linewidths);
|
||||
|
||||
// Indicate that this line was not automatically wrapped.
|
||||
if (wrappedlines)
|
||||
wrappedlines->push_back(false);
|
||||
std::string line;
|
||||
|
||||
for (const Codepoints &codepoints : codepointlines)
|
||||
{
|
||||
line.clear();
|
||||
line.reserve(codepoints.size());
|
||||
|
||||
for (uint32 codepoint : codepoints)
|
||||
{
|
||||
char character[5] = {'\0'};
|
||||
char *end = utf8::unchecked::append(codepoint, character);
|
||||
line.append(character, end - character);
|
||||
}
|
||||
|
||||
lines.push_back(line);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ class Font : public Object, public Volatile
|
||||
{
|
||||
public:
|
||||
|
||||
typedef std::vector<uint32> Codepoints;
|
||||
|
||||
enum AlignMode
|
||||
{
|
||||
ALIGN_LEFT,
|
||||
@@ -93,7 +95,9 @@ public:
|
||||
|
||||
virtual ~Font();
|
||||
|
||||
std::vector<DrawCommand> generateVertices(const Codepoints &str, std::vector<GlyphVertex> &vertices, float extra_spacing = 0.0f, Vector offset = {}, TextInfo *info = nullptr);
|
||||
std::vector<DrawCommand> generateVertices(const std::string &text, std::vector<GlyphVertex> &vertices, float extra_spacing = 0.0f, Vector offset = Vector(), TextInfo *info = nullptr);
|
||||
|
||||
std::vector<DrawCommand> generateVerticesFormatted(const std::string &text, float wrap, AlignMode align, std::vector<GlyphVertex> &vertices, TextInfo *info = nullptr);
|
||||
|
||||
void drawVertices(const std::vector<DrawCommand> &drawcommands);
|
||||
@@ -140,13 +144,12 @@ public:
|
||||
* and optionally the number of lines
|
||||
*
|
||||
* @param text The input text
|
||||
* @param wrap The number of pixels to wrap at
|
||||
* @param wraplimit The number of pixels to wrap at
|
||||
* @param max_width Optional output of the maximum width
|
||||
* @param wrapped_lines Optional output indicating which lines were
|
||||
* auto-wrapped. Indices correspond to indices of the returned value.
|
||||
* Returns a vector with the lines.
|
||||
**/
|
||||
void getWrap(const std::string &text, float wrap, std::vector<std::string> &lines, std::vector<int> *line_widths = 0, std::vector<bool> *wrapped_lines = 0);
|
||||
void getWrap(const std::string &text, float wraplimit, std::vector<std::string> &lines, std::vector<int> *line_widths = nullptr);
|
||||
void getWrap(const std::string &text, float wraplimit, std::vector<Codepoints> &lines, std::vector<int> *line_widths = nullptr);
|
||||
|
||||
/**
|
||||
* Sets the line height (which should be a number to multiply the font size by,
|
||||
|
||||
Reference in New Issue
Block a user