Browse Source

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.
Alex Szpakowski 9 years ago
parent
commit
f92e6552d4
2 changed files with 193 additions and 131 deletions
  1. 186 127
      src/modules/graphics/opengl/Font.cpp
  2. 7 4
      src/modules/graphics/opengl/Font.h

+ 186 - 127
src/modules/graphics/opengl/Font.cpp

@@ -346,7 +346,7 @@ float Font::getHeight() const
 	return (float) height;
 	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.
 	// Spacing counter and newline handling.
 	float dx = offset.x;
 	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.
 	// Pre-allocate space for the maximum possible number of vertices.
 	size_t vertstartsize = vertices.size();
 	size_t vertstartsize = vertices.size();
-	vertices.reserve(vertstartsize + text.length() * 4);
+	vertices.reserve(vertstartsize + codepoints.size());
 
 
 	uint32 prevglyph = 0;
 	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;
+		}
 
 
-				// Wrap newline, but do not print it.
-				dy += floorf(getHeight() * getLineHeight() + 0.5f);
-				dx = offset.x;
-				continue;
-			}
+		uint32 cacheid = textureCacheID;
 
 
-			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;
+		}
 
 
-			const Glyph &glyph = findGlyph(g);
+		// Add kerning to the current horizontal offset.
+		dx += getKerning(prevglyph, g);
 
 
-			// If findGlyph invalidates the texture cache, re-start the loop.
-			if (cacheid != textureCacheID)
+		if (glyph.texture != 0)
+		{
+			// Copy the vertices and set their proper relative positions.
+			for (int j = 0; j < 4; j++)
 			{
 			{
-				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;
+				vertices.push_back(glyph.vertices[j]);
+				vertices.back().x += dx;
+				vertices.back().y += dy + lineheight;
 			}
 			}
 
 
-			// Add kerning to the current horizontal offset.
-			dx += getKerning(prevglyph, g);
-
-			if (glyph.texture != 0)
+			// Check if glyph texture has changed since the last iteration.
+			if (drawcommands.empty() || drawcommands.back().texture != glyph.texture)
 			{
 			{
-				// 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;
-				}
+				// 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);
+			}
 
 
-				// 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;
+		}
 
 
-				drawcommands.back().vertexcount += 4;
-			}
+		// Advance the x position for the next glyph.
+		dx += glyph.spacing;
 
 
-			// 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);
 
 
-			// Account for extra spacing given to space characters.
-			if (g == ' ' && extra_spacing != 0.0f)
-				dx = floorf(dx + extra_spacing);
+		prevglyph = g;
 
 
-			prevglyph = g;
-		}
-	}
-	catch (utf8::exception &e)
-	{
-		throw love::Exception("UTF-8 decoding error: %s", e.what());
 	}
 	}
 
 
 	// Sort draw commands by texture first, and quad position in memory second
 	// 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->width = maxwidth - offset.x;;
 		info->height = (int) dy + (dx > 0.0f ? floorf(getHeight() * getLineHeight() + 0.5f) : 0) - offset.y;
 		info->height = (int) dy + (dx > 0.0f ? floorf(getHeight() * getLineHeight() + 0.5f) : 0) - offset.y;
 	}
 	}
-
+	
 	return drawcommands;
 	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)
 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;
 	uint32 cacheid = textureCacheID;
 
 
 	std::vector<DrawCommand> drawcommands;
 	std::vector<DrawCommand> drawcommands;
 	vertices.reserve(text.length() * 4);
 	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<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 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];
 		float width = (float) widths[i];
 		love::Vector offset(0.0f, floorf(y));
 		love::Vector offset(0.0f, floorf(y));
+		float extraspacing = 0.0f;
 
 
-		if (width > maxwidth)
-			maxwidth = width;
+		maxwidth = std::max(width, maxwidth);
 
 
 		switch (align)
 		switch (align)
 		{
 		{
@@ -502,12 +511,14 @@ std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const std::string
 			offset.x = floorf((wrap - width) / 2.0f);
 			offset.x = floorf((wrap - width) / 2.0f);
 			break;
 			break;
 		case ALIGN_JUSTIFY:
 		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
 			else
 				extraspacing = 0.0f;
 				extraspacing = 0.0f;
 			break;
 			break;
+		}
 		case ALIGN_LEFT:
 		case ALIGN_LEFT:
 		default:
 		default:
 			break;
 			break;
@@ -537,7 +548,6 @@ std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const std::string
 		}
 		}
 
 
 		y += getHeight() * getLineHeight();
 		y += getHeight() * getLineHeight();
-		i++;
 	}
 	}
 
 
 	if (info != nullptr)
 	if (info != nullptr)
@@ -636,15 +646,16 @@ int Font::getWidth(const std::string &str)
 	while (getline(iss, line, '\n'))
 	while (getline(iss, line, '\n'))
 	{
 	{
 		int width = 0;
 		int width = 0;
+		uint32 prevglyph = 0;
 		try
 		try
 		{
 		{
-			uint32 prevglyph = 0;
-
 			utf8::iterator<std::string::const_iterator> i(line.begin(), line.begin(), line.end());
 			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());
 			utf8::iterator<std::string::const_iterator> end(line.end(), line.begin(), line.end());
+
 			while (i != end)
 			while (i != end)
 			{
 			{
 				uint32 c = *i++;
 				uint32 c = *i++;
+
 				const Glyph &g = findGlyph(c);
 				const Glyph &g = findGlyph(c);
 				width += g.spacing + getKerning(prevglyph, c);
 				width += g.spacing + getKerning(prevglyph, c);
 
 
@@ -668,69 +679,117 @@ int Font::getWidth(char character)
 	return g.spacing;
 	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);
+	// Split text at newlines.
 	std::string line;
 	std::string line;
-	std::ostringstream string_builder;
+	std::istringstream iss(text);
+
+	// A wrapped line of text.
+	Codepoints wline;
 
 
-	// Split text at newlines.
 	while (std::getline(iss, line, '\n'))
 	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 width = 0.0f;
-		float oldwidth = 0.0f;
-		string_builder.str("");
+		uint32 prevglyph = 0;
+
+		wline.clear();
+		wline.reserve(line.length());
 
 
-		// Put words back together until a wrap occurs.
-		for (const std::string &word : words)
+		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());
+
+			utf8::iterator<std::string::const_iterator> lastspaceit = end;
 
 
-			// On wordwrap, push line to line buffer and clear string builder.
-			if (width > wrap && oldwidth > 0)
+			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();
+
+						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;
+				}
 
 
-				// Indicate that this line was automatically wrapped.
-				if (wrappedlines)
-					wrappedlines->push_back(true);
-			}
+				width = newwidth;
+				prevglyph = c;
+
+				wline.push_back(c);
 
 
-			string_builder << word << " ";
+				// Keep track of the last seen space, so we can "rewind" to it
+				// when wrapping.
+				if (c == ' ')
+					lastspaceit = i;
 
 
-			width += width_space;
-			oldwidth = width;
+				++i;
+			}
 		}
 		}
+		catch (utf8::exception &e)
+		{
+			throw love::Exception("UTF-8 decoding error: %s", e.what());
+		}
+
+		// Remove trailing newlines.
+		if (!wline.empty() && wline.back() == '\n')
+			wline.pop_back();
+
+		lines.push_back(wline);
 
 
-		// Push last line.
 		if (linewidths)
 		if (linewidths)
 			linewidths->push_back(width);
 			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);
+
+	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);
+		}
 
 
-		// Indicate that this line was not automatically wrapped.
-		if (wrappedlines)
-			wrappedlines->push_back(false);
+		lines.push_back(line);
 	}
 	}
 }
 }
 
 

+ 7 - 4
src/modules/graphics/opengl/Font.h

@@ -50,6 +50,8 @@ class Font : public Object, public Volatile
 {
 {
 public:
 public:
 
 
+	typedef std::vector<uint32> Codepoints;
+
 	enum AlignMode
 	enum AlignMode
 	{
 	{
 		ALIGN_LEFT,
 		ALIGN_LEFT,
@@ -93,7 +95,9 @@ public:
 
 
 	virtual ~Font();
 	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> 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);
 	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);
 	void drawVertices(const std::vector<DrawCommand> &drawcommands);
@@ -140,13 +144,12 @@ public:
 	 * and optionally the number of lines
 	 * and optionally the number of lines
 	 *
 	 *
 	 * @param text The input text
 	 * @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 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.
 	 * 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,
 	 * Sets the line height (which should be a number to multiply the font size by,