Browse Source

Added per-character color support to love.graphics.print/printf and Text objects. Resolves issue #865.

The aforementioned functions now optionally accept an array instead of a plain string. The array is in the format of: {{r, g, b [, a]}, "first colored text, ", {r, g, b [, a]}, "second colored text", ...}, where the color values specify the color to use in the next section of the text.
Alex Szpakowski 9 years ago
parent
commit
389b91cf6f

+ 231 - 121
src/modules/graphics/opengl/Font.cpp

@@ -38,6 +38,11 @@ namespace graphics
 namespace opengl
 {
 
+static inline uint16 normToUint16(double n)
+{
+	return (uint16) (n * LOVE_UINT16_MAX);
+}
+
 int Font::fontCount = 0;
 
 Font::Font(love::font::Rasterizer *r, const Texture::Filter &filter)
@@ -272,18 +277,20 @@ const Font::Glyph &Font::addGlyph(uint32 glyph)
 
 		g.texture = t;
 
-		float tX     = (float) textureX,     tY      = (float) textureY;
-		float tWidth = (float) textureWidth, tHeight = (float) textureHeight;
+		double tX     = (double) textureX,     tY      = (double) textureY;
+		double tWidth = (double) textureWidth, tHeight = (double) textureHeight;
+
+		Color c(255, 255, 255, 255);
 
 		// 0----2
 		// |  / |
 		// | /  |
 		// 1----3
 		const GlyphVertex verts[4] = {
-			{    0.0f,     0.0f,     tX/tWidth,     tY/tHeight},
-			{    0.0f, float(h),     tX/tWidth, (tY+h)/tHeight},
-			{float(w),     0.0f, (tX+w)/tWidth,     tY/tHeight},
-			{float(w), float(h), (tX+w)/tWidth, (tY+h)/tHeight}
+			{float(0), float(0), normToUint16((tX+0)/tWidth), normToUint16((tY+0)/tHeight), c},
+			{float(0), float(h), normToUint16((tX+0)/tWidth), normToUint16((tY+h)/tHeight), c},
+			{float(w), float(0), normToUint16((tX+w)/tWidth), normToUint16((tY+0)/tHeight), c},
+			{float(w), float(h), normToUint16((tX+w)/tWidth), normToUint16((tY+h)/tHeight), c}
 		};
 
 		// Copy vertex data to the glyph and set proper bearing.
@@ -341,7 +348,7 @@ float Font::getKerning(uint32 leftglyph, uint32 rightglyph)
 	return k;
 }
 
-void Font::codepointsFromString(const std::string &text, Codepoints &codepoints)
+void Font::getCodepointsFromString(const std::string &text, Codepoints &codepoints)
 {
 	codepoints.reserve(text.size());
 
@@ -362,12 +369,36 @@ void Font::codepointsFromString(const std::string &text, Codepoints &codepoints)
 	}
 }
 
+void Font::getCodepointsFromString(const std::vector<ColoredString> &strs, ColoredCodepoints &codepoints)
+{
+	if (strs.empty())
+		return;
+
+	codepoints.cps.reserve(strs[0].str.size());
+
+	for (const ColoredString &cstr : strs)
+	{
+		IndexedColor c = {cstr.color, (int) codepoints.cps.size()};
+		codepoints.colors.push_back(c);
+
+		getCodepointsFromString(cstr.str, codepoints.cps);
+	}
+
+	if (codepoints.colors.size() == 1)
+	{
+		IndexedColor c = codepoints.colors[0];
+
+		if (c.index == 0 && c.color == Color(255, 255, 255, 255))
+			codepoints.colors.pop_back();
+	}
+}
+
 float Font::getHeight() const
 {
 	return (float) height;
 }
 
-std::vector<Font::DrawCommand> Font::generateVertices(const Codepoints &codepoints, std::vector<GlyphVertex> &vertices, float extra_spacing, Vector offset, TextInfo *info)
+Font::DrawCommands Font::generateVertices(const ColoredCodepoints &codepoints, std::vector<GlyphVertex> &vertices, float extra_spacing, Vector offset, TextInfo *info)
 {
 	// Spacing counter and newline handling.
 	float dx = offset.x;
@@ -377,17 +408,29 @@ std::vector<Font::DrawCommand> Font::generateVertices(const Codepoints &codepoin
 	int maxwidth = 0;
 
 	// Keeps track of when we need to switch textures in our vertex array.
-	std::vector<DrawCommand> drawcommands;
+	DrawCommands drawcmds;
 
 	// Pre-allocate space for the maximum possible number of vertices.
 	size_t vertstartsize = vertices.size();
-	vertices.reserve(vertstartsize + codepoints.size());
+	vertices.reserve(vertstartsize + codepoints.cps.size() * 4);
 
 	uint32 prevglyph = 0;
 
-	for (int i = 0; i < (int) codepoints.size(); i++)
+	Color curcolor(255, 255, 255, 255);
+	int curcolori = -1;
+	int ncolors = (int) codepoints.colors.size();
+
+	if (ncolors == 0 || (ncolors == 1 && codepoints.colors[0].color == curcolor))
+		drawcmds.usecolors = false;
+	else
+		drawcmds.usecolors = true;
+
+	for (int i = 0; i < (int) codepoints.cps.size(); i++)
 	{
-		uint32 g = codepoints[i];
+		uint32 g = codepoints.cps[i];
+
+		if (curcolori + 1 < ncolors && codepoints.colors[curcolori + 1].index == i)
+			curcolor = codepoints.colors[++curcolori].color;
 
 		if (g == '\n')
 		{
@@ -411,9 +454,11 @@ std::vector<Font::DrawCommand> Font::generateVertices(const Codepoints &codepoin
 			maxwidth = 0;
 			dx = offset.x;
 			dy = offset.y;
-			drawcommands.clear();
+			drawcmds.commands.clear();
 			vertices.resize(vertstartsize);
 			prevglyph = 0;
+			curcolori = -1;
+			curcolor = Color(255, 255, 255, 255);
 			continue;
 		}
 
@@ -422,26 +467,27 @@ std::vector<Font::DrawCommand> Font::generateVertices(const Codepoints &codepoin
 
 		if (glyph.texture != 0)
 		{
-			// Copy the vertices and set their proper relative positions.
+			// Copy the vertices and set their colors and relative positions.
 			for (int j = 0; j < 4; j++)
 			{
 				vertices.push_back(glyph.vertices[j]);
 				vertices.back().x += dx;
 				vertices.back().y += dy + lineheight;
+				vertices.back().color = curcolor;
 			}
 
 			// Check if glyph texture has changed since the last iteration.
-			if (drawcommands.empty() || drawcommands.back().texture != glyph.texture)
+			if (drawcmds.commands.empty() || drawcmds.commands.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);
+				drawcmds.commands.push_back(cmd);
 			}
 
-			drawcommands.back().vertexcount += 4;
+			drawcmds.commands.back().vertexcount += 4;
 		}
 
 		// Advance the x position for the next glyph.
@@ -464,7 +510,7 @@ std::vector<Font::DrawCommand> Font::generateVertices(const Codepoints &codepoin
 			return a.startvertex < b.startvertex;
 	};
 
-	std::sort(drawcommands.begin(), drawcommands.end(), drawsort);
+	std::sort(drawcmds.commands.begin(), drawcmds.commands.end(), drawsort);
 
 	if (dx > maxwidth)
 		maxwidth = (int) dx;
@@ -475,28 +521,28 @@ std::vector<Font::DrawCommand> Font::generateVertices(const Codepoints &codepoin
 		info->height = (int) dy + (dx > 0.0f ? floorf(getHeight() * getLineHeight() + 0.5f) : 0) - offset.y;
 	}
 	
-	return drawcommands;
+	return drawcmds;
 }
 
-std::vector<Font::DrawCommand> Font::generateVertices(const std::string &text, std::vector<GlyphVertex> &vertices, float extra_spacing, Vector offset, TextInfo *info)
+Font::DrawCommands Font::generateVertices(const std::string &text, std::vector<GlyphVertex> &vertices, float extra_spacing, Vector offset, TextInfo *info)
 {
-	Codepoints codepoints;
-	codepointsFromString(text, codepoints);
+	ColoredCodepoints codepoints;
+	getCodepointsFromString(text, codepoints.cps);
 	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)
+Font::DrawCommands Font::generateVerticesFormatted(const ColoredCodepoints &text, float wrap, AlignMode align, std::vector<GlyphVertex> &vertices, TextInfo *info)
 {
 	
 	wrap = std::max(wrap, 0.0f);
 
 	uint32 cacheid = textureCacheID;
 
-	std::vector<DrawCommand> drawcommands;
-	vertices.reserve(text.length() * 4);
+	DrawCommands drawcmds;
+	vertices.reserve(text.cps.size() * 4);
 
 	std::vector<int> widths;
-	std::vector<Codepoints> lines;
+	std::vector<ColoredCodepoints> lines;
 
 	getWrap(text, wrap, lines, &widths);
 
@@ -523,7 +569,7 @@ std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const std::string
 			break;
 		case ALIGN_JUSTIFY:
 		{
-			float numspaces = (float) std::count(line.begin(), line.end(), ' ');
+			float numspaces = (float) std::count(line.cps.begin(), line.cps.end(), ' ');
 			if (width < wrap && numspaces >= 1)
 				extraspacing = (wrap - width) / numspaces;
 			else
@@ -535,27 +581,29 @@ std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const std::string
 			break;
 		}
 
-		std::vector<DrawCommand> commands = generateVertices(line, vertices, extraspacing, offset);
+		DrawCommands cmds = generateVertices(line, vertices, extraspacing, offset);
 
-		if (!commands.empty())
+		if (!cmds.commands.empty())
 		{
-			auto firstcmd = commands.begin();
+			auto firstcmd = cmds.commands.begin();
 
 			// If the first draw command in the new list has the same texture
 			// as the last one in the existing list we're building and its
 			// vertices are in-order, we can combine them (saving a draw call.)
-			if (!drawcommands.empty())
+			if (!drawcmds.commands.empty())
 			{
-				auto prevcmd = drawcommands.back();
+				auto prevcmd = drawcmds.commands.back();
 				if (prevcmd.texture == firstcmd->texture && (prevcmd.startvertex + prevcmd.vertexcount) == firstcmd->startvertex)
 				{
-					drawcommands.back().vertexcount += firstcmd->vertexcount;
+					drawcmds.commands.back().vertexcount += firstcmd->vertexcount;
 					++firstcmd;
 				}
 			}
 
 			// Append the new draw commands to the list we're building.
-			drawcommands.insert(drawcommands.end(), firstcmd, commands.end());
+			drawcmds.commands.insert(drawcmds.commands.end(), firstcmd, cmds.commands.end());
+
+			drawcmds.usecolors = drawcmds.usecolors || cmds.usecolors;
 		}
 
 		y += getHeight() * getLineHeight();
@@ -570,19 +618,19 @@ std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const std::string
 	if (cacheid != textureCacheID)
 	{
 		vertices.clear();
-		drawcommands = generateVerticesFormatted(text, wrap, align, vertices);
+		drawcmds = generateVerticesFormatted(text, wrap, align, vertices);
 	}
 
-	return drawcommands;
+	return drawcmds;
 }
 
-void Font::drawVertices(const std::vector<DrawCommand> &drawcommands)
+void Font::drawVertices(const DrawCommands &drawcommands)
 {
 	// Vertex attribute pointers need to be set before calling this function.
 	// This assumes that the attribute pointers are constant for all vertices.
 
 	int totalverts = 0;
-	for (const DrawCommand &cmd : drawcommands)
+	for (const DrawCommand &cmd : drawcommands.commands)
 		totalverts = std::max(cmd.startvertex + cmd.vertexcount, totalverts);
 
 	if ((size_t) totalverts / 4 > quadIndices.getSize())
@@ -597,7 +645,7 @@ void Font::drawVertices(const std::vector<DrawCommand> &drawcommands)
 
 	// We need a separate draw call for every section of the text which uses a
 	// different texture than the previous section.
-	for (const DrawCommand &cmd : drawcommands)
+	for (const DrawCommand &cmd : drawcommands.commands)
 	{
 		GLsizei count = (cmd.vertexcount / 4) * 6;
 		size_t offset = (cmd.startvertex / 4) * 6 * elemsize;
@@ -608,9 +656,9 @@ void Font::drawVertices(const std::vector<DrawCommand> &drawcommands)
 	}
 }
 
-void Font::printv(const Matrix4 &t, const std::vector<DrawCommand> &drawcommands, const std::vector<GlyphVertex> &vertices)
+void Font::printv(const Matrix4 &t, const DrawCommands &drawcommands, const std::vector<GlyphVertex> &vertices)
 {
-	if (vertices.empty() || drawcommands.empty())
+	if (vertices.empty() || drawcommands.commands.empty())
 		return;
 
 	OpenGL::TempDebugGroup debuggroup("Font print");
@@ -618,28 +666,42 @@ void Font::printv(const Matrix4 &t, const std::vector<DrawCommand> &drawcommands
 	OpenGL::TempTransform transform(gl);
 	transform.get() *= t;
 
-	gl.useVertexAttribArrays(ATTRIBFLAG_POS | ATTRIBFLAG_TEXCOORD);
-
 	glVertexAttribPointer(ATTRIB_POS, 2, GL_FLOAT, GL_FALSE, sizeof(GlyphVertex), &vertices[0].x);
-	glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, sizeof(GlyphVertex), &vertices[0].s);
+	glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_UNSIGNED_SHORT, GL_TRUE, sizeof(GlyphVertex), &vertices[0].s);
+
+	uint32 enabledattribs = ATTRIBFLAG_POS | ATTRIBFLAG_TEXCOORD;
+
+	if (drawcommands.usecolors)
+	{
+		glVertexAttribPointer(ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(GlyphVertex), &vertices[0].color.r);
+		enabledattribs |= ATTRIBFLAG_COLOR;
+	}
+
+	gl.useVertexAttribArrays(enabledattribs);
 
 	drawVertices(drawcommands);
 }
 
-void Font::print(const std::string &text, float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky)
+void Font::print(const std::vector<ColoredString> &text, float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky)
 {
+	ColoredCodepoints codepoints;
+	getCodepointsFromString(text, codepoints);
+
 	std::vector<GlyphVertex> vertices;
-	std::vector<DrawCommand> drawcommands = generateVertices(text, vertices);
+	DrawCommands drawcommands = generateVertices(codepoints, vertices);
 
 	Matrix4 t(x, y, angle, sx, sy, ox, oy, kx, ky);
 
 	printv(t, drawcommands, vertices);
 }
 
-void Font::printf(const std::string &text, float x, float y, float wrap, AlignMode align, float angle, float sx, float sy, float ox, float oy, float kx, float ky)
+void Font::printf(const std::vector<ColoredString> &text, float x, float y, float wrap, AlignMode align, float angle, float sx, float sy, float ox, float oy, float kx, float ky)
 {
+	ColoredCodepoints codepoints;
+	getCodepointsFromString(text, codepoints);
+
 	std::vector<GlyphVertex> vertices;
-	std::vector<DrawCommand> drawcommands = generateVerticesFormatted(text, wrap, align, vertices);
+	DrawCommands drawcommands = generateVerticesFormatted(codepoints, wrap, align, vertices);
 
 	Matrix4 t(x, y, angle, sx, sy, ox, oy, kx, ky);
 
@@ -690,104 +752,149 @@ int Font::getWidth(char character)
 	return g.spacing;
 }
 
-void Font::getWrap(const std::string &text, float wraplimit, std::vector<Codepoints> &lines, std::vector<int> *linewidths)
+void Font::getWrap(const ColoredCodepoints &codepoints, float wraplimit, std::vector<ColoredCodepoints> &lines, std::vector<int> *linewidths)
 {
-	// Split text at newlines.
-	std::string line;
-	std::istringstream iss(text);
+	// Per-line info.
+	float width = 0.0f;
+	float widthbeforelastspace = 0.0f;
+	float widthoftrailingspace = 0.0f;
+	uint32 prevglyph = 0;
+
+	int lastspaceindex = -1;
+
+	// Keeping the indexed colors "in sync" is a bit tricky, since we split
+	// things up and we might skip some glyphs but we don't want to skip any
+	// color which starts at those indices.
+	Color curcolor(255, 255, 255, 255);
+	bool addcurcolor = false;
+	int curcolori = -1;
+	int endcolori = (int) codepoints.colors.size() - 1;
 
 	// A wrapped line of text.
-	Codepoints wline;
+	ColoredCodepoints wline;
 
-	while (std::getline(iss, line, '\n'))
+	int i = 0;
+	while (i < (int) codepoints.cps.size())
 	{
-		float width = 0.0f;
-		float widthbeforelastspace = 0.0f;
-		float widthoftrailingspace = 0.0f;
-		uint32 prevglyph = 0;
+		uint32 c = codepoints.cps[i];
 
-		wline.clear();
-		wline.reserve(line.length());
+		// Determine the current color before doing anything else, to make sure
+		// it's still applied to future glyphs even if this one is skipped.
+		if (curcolori < endcolori && codepoints.colors[curcolori + 1].index == i)
+		{
+			curcolor = codepoints.colors[curcolori + 1].color;
+			curcolori++;
+			addcurcolor = true;
+		}
 
-		try
+		// Split text at newlines.
+		if (c == '\n')
 		{
-			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());
+			lines.push_back(wline);
 
-			utf8::iterator<std::string::const_iterator> lastspaceit = end;
+			// Ignore the width of any trailing spaces, for individual lines.
+			if (linewidths)
+				linewidths->push_back(width - widthoftrailingspace);
 
-			while (i != end)
+			// Make sure the new line keeps any color that was set previously.
+			addcurcolor = true;
+
+			width = widthbeforelastspace = widthoftrailingspace = 0.0f;
+			prevglyph = 0; // Reset kerning information.
+			lastspaceindex = -1;
+			wline.cps.clear();
+			wline.colors.clear();
+			i++;
+
+			continue;
+		}
+
+		const Glyph &g = findGlyph(c);
+		float charwidth = g.spacing + getKerning(prevglyph, c);
+		float newwidth = width + charwidth;
+
+		// 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.cps.empty())
+				i++;
+			else if (lastspaceindex != -1)
 			{
-				uint32 c = *i;
+				// 'Rewind' to the last seen space, if the line has one.
+				// FIXME: This could be more efficient...
+				while (!wline.cps.empty() && wline.cps.back() != ' ')
+					wline.cps.pop_back();
 
-				const Glyph &g = findGlyph(c);
-				float charwidth = g.spacing + getKerning(prevglyph, c);
-				float newwidth = width + charwidth;
+				while (!wline.colors.empty() && wline.colors.back().index >= (int) wline.cps.size())
+					wline.colors.pop_back();
 
-				// 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)
+				// Also 'rewind' to the color that the last character is using.
+				for (int colori = curcolori; colori >= 0; colori--)
 				{
-					// 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)
+					if (codepoints.colors[colori].index <= lastspaceindex)
 					{
-						// '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();
+						curcolor = codepoints.colors[colori].color;
+						curcolori = colori;
+						break;
+					}
+				}
 
-						// Ignore the width of trailing spaces in wrapped lines.
-						width = widthbeforelastspace;
+				// Ignore the width of trailing spaces in wrapped lines.
+				width = widthbeforelastspace;
 
-						i = lastspaceit;
-						++i; // Start the next line after the space.
-					}
+				i = lastspaceindex;
+				i++; // Start the next line after the space.
+			}
 
-					lines.push_back(wline);
+			lines.push_back(wline);
 
-					if (linewidths)
-						linewidths->push_back(width);
+			if (linewidths)
+				linewidths->push_back(width);
 
-					prevglyph = 0; // Reset kerning information.
-					width = widthbeforelastspace = widthoftrailingspace = 0.0f;
-					wline.clear();
-					lastspaceit = end;
-					continue;
-				}
+			addcurcolor = true;
 
-				if (prevglyph != ' ' && c == ' ')
-					widthbeforelastspace = width;
+			prevglyph = 0;
+			width = widthbeforelastspace = widthoftrailingspace = 0.0f;
+			wline.cps.clear();
+			wline.colors.clear();
+			lastspaceindex = -1;
 
-				width = newwidth;
-				prevglyph = c;
+			continue;
+		}
 
-				wline.push_back(c);
+		if (prevglyph != ' ' && c == ' ')
+			widthbeforelastspace = width;
 
-				// Keep track of the last seen space, so we can "rewind" to it
-				// when wrapping.
-				if (c == ' ')
-				{
-					lastspaceit = i;
-					widthoftrailingspace += charwidth;
-				}
-				else if (c != '\n')
-					widthoftrailingspace = 0.0f;
+		width = newwidth;
+		prevglyph = c;
 
-				++i;
-			}
+		if (addcurcolor)
+		{
+			wline.colors.push_back({curcolor, (int) wline.cps.size()});
+			addcurcolor = false;
 		}
-		catch (utf8::exception &e)
+
+		wline.cps.push_back(c);
+
+		// Keep track of the last seen space, so we can "rewind" to it when
+		// wrapping.
+		if (c == ' ')
 		{
-			throw love::Exception("UTF-8 decoding error: %s", e.what());
+			lastspaceindex = i;
+			widthoftrailingspace += charwidth;
 		}
+		else if (c != '\n')
+			widthoftrailingspace = 0.0f;
 
-		// Remove trailing newlines.
-		if (!wline.empty() && wline.back() == '\n')
-			wline.pop_back();
+		i++;
+	}
 
+	// Push the last line.
+	if (!wline.cps.empty())
+	{
 		lines.push_back(wline);
 
 		// Ignore the width of any trailing spaces, for individual lines.
@@ -798,17 +905,20 @@ void Font::getWrap(const std::string &text, float wraplimit, std::vector<Codepoi
 
 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);
+	ColoredCodepoints codepoints;
+	getCodepointsFromString(text, codepoints.cps);
+
+	std::vector<ColoredCodepoints> codepointlines;
+	getWrap(codepoints, wraplimit, codepointlines, linewidths);
 
 	std::string line;
 
-	for (const Codepoints &codepoints : codepointlines)
+	for (const ColoredCodepoints &codepoints : codepointlines)
 	{
 		line.clear();
-		line.reserve(codepoints.size());
+		line.reserve(codepoints.cps.size());
 
-		for (uint32 codepoint : codepoints)
+		for (uint32 codepoint : codepoints.cps)
 		{
 			char character[5] = {'\0'};
 			char *end = utf8::unchecked::append(codepoint, character);

+ 38 - 11
src/modules/graphics/opengl/Font.h

@@ -61,10 +61,29 @@ public:
 		ALIGN_MAX_ENUM
 	};
 
+	struct ColoredString
+	{
+		std::string str;
+		Color color;
+	};
+
+	struct IndexedColor
+	{
+		Color color;
+		int index;
+	};
+
+	struct ColoredCodepoints
+	{
+		std::vector<uint32> cps;
+		std::vector<IndexedColor> colors;
+	};
+
 	struct GlyphVertex
 	{
-		float x, y;
-		float s, t;
+		float  x, y;
+		uint16 s, t;
+		Color  color;
 	};
 
 	struct TextInfo
@@ -81,16 +100,25 @@ public:
 		int vertexcount;
 	};
 
+	struct DrawCommands
+	{
+		std::vector<DrawCommand> commands;
+		bool usecolors;
+	};
+
 	Font(love::font::Rasterizer *r, const Texture::Filter &filter = Texture::getDefaultFilter());
 
 	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);
+	DrawCommands generateVertices(const ColoredCodepoints &codepoints, std::vector<GlyphVertex> &vertices, float extra_spacing = 0.0f, Vector offset = {}, TextInfo *info = nullptr);
+	DrawCommands generateVertices(const std::string &text, std::vector<GlyphVertex> &vertices, float extra_spacing = 0.0f, Vector offset = Vector(), TextInfo *info = nullptr);
+
+	DrawCommands generateVerticesFormatted(const ColoredCodepoints &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 DrawCommands &drawcommands);
 
-	void drawVertices(const std::vector<DrawCommand> &drawcommands);
+	static void getCodepointsFromString(const std::string &str, Codepoints &codepoints);
+	static void getCodepointsFromString(const std::vector<ColoredString> &strs, ColoredCodepoints &codepoints);
 
 	/**
 	 * Prints the text at the designated position with rotation and scaling.
@@ -106,9 +134,9 @@ public:
 	 * @param kx Shear along the x axis.
 	 * @param ky Shear along the y axis.
 	 **/
-	void print(const std::string &text, float x, float y, float angle = 0.0f, float sx = 1.0f, float sy = 1.0f, float ox = 0.0f, float oy = 0.0f, float kx = 0.0f, float ky = 0.0f);
+	void print(const std::vector<ColoredString> &text, float x, float y, float angle = 0.0f, float sx = 1.0f, float sy = 1.0f, float ox = 0.0f, float oy = 0.0f, float kx = 0.0f, float ky = 0.0f);
 
-	void printf(const std::string &text, float x, float y, float wrap, AlignMode align, float angle = 0.0f, float sx = 1.0f, float sy = 1.0f, float ox = 0.0f, float oy = 0.0f, float kx = 0.0f, float ky = 0.0f);
+	void printf(const std::vector<ColoredString> &text, float x, float y, float wrap, AlignMode align, float angle = 0.0f, float sx = 1.0f, float sy = 1.0f, float ox = 0.0f, float oy = 0.0f, float kx = 0.0f, float ky = 0.0f);
 
 	/**
 	 * Returns the height of the font.
@@ -139,7 +167,7 @@ public:
 	 * Returns a vector with the lines.
 	 **/
 	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);
+	void getWrap(const ColoredCodepoints &codepoints, float wraplimit, std::vector<ColoredCodepoints> &lines, std::vector<int> *line_widths = nullptr);
 
 	/**
 	 * Sets the line height (which should be a number to multiply the font size by,
@@ -205,8 +233,7 @@ private:
 	const Glyph &addGlyph(uint32 glyph);
 	const Glyph &findGlyph(uint32 glyph);
 	float getKerning(uint32 leftglyph, uint32 rightglyph);
-	void codepointsFromString(const std::string &str, Codepoints &codepoints);
-	void printv(const Matrix4 &t, const std::vector<DrawCommand> &drawcommands, const std::vector<GlyphVertex> &vertices);
+	void printv(const Matrix4 &t, const DrawCommands &drawcommands, const std::vector<GlyphVertex> &vertices);
 
 	std::vector<StrongRef<love::font::Rasterizer>> rasterizers;
 

+ 3 - 3
src/modules/graphics/opengl/Graphics.cpp

@@ -814,7 +814,7 @@ Mesh *Graphics::newMesh(const std::vector<Mesh::AttribFormat> &vertexformat, con
 	return new Mesh(vertexformat, data, datasize, drawmode, usage);
 }
 
-Text *Graphics::newText(Font *font, const std::string &text)
+Text *Graphics::newText(Font *font, const std::vector<Font::ColoredString> &text)
 {
 	return new Text(font, text);
 }
@@ -1105,7 +1105,7 @@ bool Graphics::isWireframe() const
 	return states.back().wireframe;
 }
 
-void Graphics::print(const std::string &str, float x, float y , float angle, float sx, float sy, float ox, float oy, float kx, float ky)
+void Graphics::print(const std::vector<Font::ColoredString> &str, float x, float y , float angle, float sx, float sy, float ox, float oy, float kx, float ky)
 {
 	checkSetDefaultFont();
 
@@ -1115,7 +1115,7 @@ void Graphics::print(const std::string &str, float x, float y , float angle, flo
 		state.font->print(str, x, y, angle, sx, sy, ox, oy, kx, ky);
 }
 
-void Graphics::printf(const std::string &str, float x, float y, float wrap, Font::AlignMode align, float angle, float sx, float sy, float ox, float oy, float kx, float ky)
+void Graphics::printf(const std::vector<Font::ColoredString> &str, float x, float y, float wrap, Font::AlignMode align, float angle, float sx, float sy, float ox, float oy, float kx, float ky)
 {
 	checkSetDefaultFont();
 

+ 3 - 3
src/modules/graphics/opengl/Graphics.h

@@ -183,7 +183,7 @@ public:
 	Mesh *newMesh(const std::vector<Mesh::AttribFormat> &vertexformat, int vertexcount, Mesh::DrawMode drawmode, Mesh::Usage usage);
 	Mesh *newMesh(const std::vector<Mesh::AttribFormat> &vertexformat, const void *data, size_t datasize, Mesh::DrawMode drawmode, Mesh::Usage usage);
 
-	Text *newText(Font *font, const std::string &text = "");
+	Text *newText(Font *font, const std::vector<Font::ColoredString> &text = {});
 
 	bool isGammaCorrect() const;
 
@@ -328,7 +328,7 @@ public:
 	 * @param kx Shear along the x-axis.
 	 * @param ky Shear along the y-axis.
 	 **/
-	void print(const std::string &str, float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky);
+	void print(const std::vector<Font::ColoredString> &str, float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky);
 
 	/**
 	 * Draw formatted text on screen at the specified coordinates.
@@ -346,7 +346,7 @@ public:
 	 * @param kx Shear along the x-axis.
 	 * @param ky Shear along the y-axis.
 	 **/
-	void printf(const std::string &str, float x, float y, float wrap, Font::AlignMode align, float angle, float sx, float sy, float ox, float oy, float kx, float ky);
+	void printf(const std::vector<Font::ColoredString> &str, float x, float y, float wrap, Font::AlignMode align, float angle, float sx, float sy, float ox, float oy, float kx, float ky);
 
 	/**
 	 * Draws a point at (x,y).

+ 47 - 36
src/modules/graphics/opengl/Text.cpp

@@ -30,7 +30,7 @@ namespace graphics
 namespace opengl
 {
 
-Text::Text(Font *font, const std::string &text)
+Text::Text(Font *font, const std::vector<Font::ColoredString> &text)
 	: font(font)
 	, vbo(nullptr)
 	, text_info()
@@ -111,13 +111,13 @@ void Text::regenerateVertices()
 void Text::addTextData(const TextData &t)
 {
 	std::vector<Font::GlyphVertex> vertices;
-	std::vector<Font::DrawCommand> new_commands;
+	Font::DrawCommands new_commands;
 
 	// We only have formatted text if the align mode is valid.
 	if (t.align == Font::ALIGN_MAX_ENUM)
-		new_commands = font->generateVertices(t.text, vertices, 0.0f, Vector(0.0f, 0.0f), &text_info);
+		new_commands = font->generateVertices(t.codepoints, vertices, 0.0f, Vector(0.0f, 0.0f), &text_info);
 	else
-		new_commands = font->generateVerticesFormatted(t.text, t.wrap, t.align, vertices, &text_info);
+		new_commands = font->generateVerticesFormatted(t.codepoints, t.wrap, t.align, vertices, &text_info);
 
 	if (t.use_matrix)
 		t.matrix.transform(&vertices[0], &vertices[0], (int) vertices.size());
@@ -127,34 +127,37 @@ void Text::addTextData(const TextData &t)
 	if (!t.append_vertices)
 	{
 		voffset = 0;
-		draw_commands.clear();
+		draw_commands.commands.clear();
+		draw_commands.usecolors = false;
 	}
 
 	uploadVertices(vertices, voffset);
 
-	if (!new_commands.empty())
+	if (!new_commands.commands.empty())
 	{
 		// The start vertex should be adjusted to account for the vertex offset.
-		for (Font::DrawCommand &cmd : new_commands)
+		for (Font::DrawCommand &cmd : new_commands.commands)
 			cmd.startvertex += (int) voffset;
 
-		auto firstcmd = new_commands.begin();
+		auto firstcmd = new_commands.commands.begin();
 
 		// If the first draw command in the new list has the same texture as the
 		// last one in the existing list we're building and its vertices are
 		// in-order, we can combine them (saving a draw call.)
-		if (!draw_commands.empty())
+		if (!draw_commands.commands.empty())
 		{
-			auto prevcmd = draw_commands.back();
+			auto prevcmd = draw_commands.commands.back();
 			if (prevcmd.texture == firstcmd->texture && (prevcmd.startvertex + prevcmd.vertexcount) == firstcmd->startvertex)
 			{
-				draw_commands.back().vertexcount += firstcmd->vertexcount;
+				draw_commands.commands.back().vertexcount += firstcmd->vertexcount;
 				++firstcmd;
 			}
 		}
 
 		// Append the new draw commands to the list we're building.
-		draw_commands.insert(draw_commands.end(), firstcmd, new_commands.end());
+		draw_commands.commands.insert(draw_commands.commands.end(), firstcmd, new_commands.commands.end());
+
+		draw_commands.usecolors = draw_commands.usecolors || new_commands.usecolors;
 	}
 
 	vert_offset = voffset + vertices.size();
@@ -165,20 +168,20 @@ void Text::addTextData(const TextData &t)
 		regenerateVertices();
 }
 
-void Text::set(const std::string &text)
+void Text::set(const std::vector<Font::ColoredString> &text)
 {
-	if (text.empty())
-		return set();
-
-	addTextData({text, -1.0f, Font::ALIGN_MAX_ENUM, false, false, Matrix3()});
+	return set(text, -1.0f, Font::ALIGN_MAX_ENUM);
 }
 
-void Text::set(const std::string &text, float wrap, Font::AlignMode align)
+void Text::set(const std::vector<Font::ColoredString> &text, float wrap, Font::AlignMode align)
 {
-	if (text.empty())
+	if (text.empty() || (text.size() == 1 && text[0].str.empty()))
 		return set();
 
-	addTextData({text, wrap, align, false, false, Matrix3()});
+	Font::ColoredCodepoints codepoints;
+	Font::getCodepointsFromString(text, codepoints);
+
+	addTextData({codepoints, wrap, align, false, false, Matrix3()});
 }
 
 void Text::set()
@@ -186,30 +189,29 @@ void Text::set()
 	clear();
 }
 
-void Text::add(const std::string &text, float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky)
+void Text::add(const std::vector<Font::ColoredString> &text, float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky)
 {
-	if (text.empty())
-		return;
-
-	Matrix3 m(x, y, angle, sx, sy, ox, oy, kx, ky);
-
-	addTextData({text, -1.0f, Font::ALIGN_MAX_ENUM, true, true, m});
+	return addf(text, -1.0f, Font::ALIGN_MAX_ENUM, x, y, angle, sx, sy, ox, oy, kx, ky);
 }
 
-void Text::addf(const std::string &text, float wrap, Font::AlignMode align, float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky)
+void Text::addf(const std::vector<Font::ColoredString> &text, float wrap, Font::AlignMode align, float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky)
 {
-	if (text.empty())
+	if (text.empty() || (text.size() == 1 && text[0].str.empty()))
 		return;
 
+	Font::ColoredCodepoints codepoints;
+	Font::getCodepointsFromString(text, codepoints);
+
 	Matrix3 m(x, y, angle, sx, sy, ox, oy, kx, ky);
 
-	addTextData({text, wrap, align, true, true, m});
+	addTextData({codepoints, wrap, align, true, true, m});
 }
 
 void Text::clear()
 {
 	text_data.clear();
-	draw_commands.clear();
+	draw_commands.commands.clear();
+	draw_commands.usecolors = false;
 	texture_cache_id = font->getTextureCacheID();
 	text_info = {};
 	vert_offset = 0;
@@ -217,7 +219,7 @@ void Text::clear()
 
 void Text::draw(float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky)
 {
-	if (vbo == nullptr || draw_commands.empty())
+	if (vbo == nullptr || draw_commands.commands.empty())
 		return;
 
 	OpenGL::TempDebugGroup debuggroup("Text object draw");
@@ -226,8 +228,9 @@ void Text::draw(float x, float y, float angle, float sx, float sy, float ox, flo
 	if (font->getTextureCacheID() != texture_cache_id)
 		regenerateVertices();
 
-	const size_t pos_offset = offsetof(Font::GlyphVertex, x);
-	const size_t tex_offset = offsetof(Font::GlyphVertex, s);
+	const size_t pos_offset   = offsetof(Font::GlyphVertex, x);
+	const size_t tex_offset   = offsetof(Font::GlyphVertex, s);
+	const size_t color_offset = offsetof(Font::GlyphVertex, color.r);
 	const size_t stride = sizeof(Font::GlyphVertex);
 
 	OpenGL::TempTransform transform(gl);
@@ -239,10 +242,18 @@ void Text::draw(float x, float y, float angle, float sx, float sy, float ox, flo
 
 		// Font::drawVertices expects AttribPointer calls to be done already.
 		glVertexAttribPointer(ATTRIB_POS, 2, GL_FLOAT, GL_FALSE, stride, vbo->getPointer(pos_offset));
-		glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, stride, vbo->getPointer(tex_offset));
+		glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_UNSIGNED_SHORT, GL_TRUE, stride, vbo->getPointer(tex_offset));
+
+		if (draw_commands.usecolors)
+			glVertexAttribPointer(ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, stride, vbo->getPointer(color_offset));
 	}
 
-	gl.useVertexAttribArrays(ATTRIBFLAG_POS | ATTRIBFLAG_TEXCOORD);
+	uint32 enabledattribs = ATTRIBFLAG_POS | ATTRIBFLAG_TEXCOORD;
+
+	if (draw_commands.usecolors)
+		enabledattribs |= ATTRIBFLAG_COLOR;
+
+	gl.useVertexAttribArrays(enabledattribs);
 
 	font->drawVertices(draw_commands);
 }

+ 7 - 7
src/modules/graphics/opengl/Text.h

@@ -38,15 +38,15 @@ class Text : public Drawable
 {
 public:
 
-	Text(Font *font, const std::string &text = "");
+	Text(Font *font, const std::vector<Font::ColoredString> &text = {});
 	virtual ~Text();
 
-	void set(const std::string &text);
-	void set(const std::string &text, float wrap, Font::AlignMode align);
+	void set(const std::vector<Font::ColoredString> &text);
+	void set(const std::vector<Font::ColoredString> &text, float wrap, Font::AlignMode align);
 	void set();
 
-	void add(const std::string &text, float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky);
-	void addf(const std::string &text, float wrap, Font::AlignMode align, float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky);
+	void add(const std::vector<Font::ColoredString> &text, float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky);
+	void addf(const std::vector<Font::ColoredString> &text, float wrap, Font::AlignMode align, float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky);
 	void clear();
 
 	// Implements Drawable.
@@ -69,7 +69,7 @@ private:
 
 	struct TextData
 	{
-		std::string text;
+		Font::ColoredCodepoints codepoints;
 		float wrap;
 		Font::AlignMode align;
 		bool use_matrix;
@@ -84,7 +84,7 @@ private:
 	StrongRef<Font> font;
 	GLBuffer *vbo;
 
-	std::vector<Font::DrawCommand> draw_commands;
+	Font::DrawCommands draw_commands;
 
 	std::vector<TextData> text_data;
 	Font::TextInfo text_info;

+ 9 - 3
src/modules/graphics/opengl/wrap_Graphics.cpp

@@ -851,7 +851,9 @@ int w_newText(lua_State *L)
 		luax_catchexcept(L, [&](){ t = instance()->newText(font); });
 	else
 	{
-		std::string text = luax_checkstring(L, 2);
+		std::vector<Font::ColoredString> text;
+		luax_checkcoloredstring(L, 2, text);
+
 		luax_catchexcept(L, [&](){ t = instance()->newText(font, text); });
 	}
 
@@ -1466,7 +1468,9 @@ int w_draw(lua_State *L)
 
 int w_print(lua_State *L)
 {
-	std::string str = luax_checkstring(L, 1);
+	std::vector<Font::ColoredString> str;
+	luax_checkcoloredstring(L, 1, str);
+
 	float x = (float)luaL_optnumber(L, 2, 0.0);
 	float y = (float)luaL_optnumber(L, 3, 0.0);
 	float angle = (float)luaL_optnumber(L, 4, 0.0f);
@@ -1485,7 +1489,9 @@ int w_print(lua_State *L)
 
 int w_printf(lua_State *L)
 {
-	std::string str = luax_checkstring(L, 1);
+	std::vector<Font::ColoredString> str;
+	luax_checkcoloredstring(L, 1, str);
+
 	float x = (float)luaL_checknumber(L, 2);
 	float y = (float)luaL_checknumber(L, 3);
 	float wrap = (float)luaL_checknumber(L, 4);

+ 54 - 5
src/modules/graphics/opengl/wrap_Text.cpp

@@ -32,6 +32,47 @@ Text *luax_checktext(lua_State *L, int idx)
 	return luax_checktype<Text>(L, idx, GRAPHICS_TEXT_ID);
 }
 
+void luax_checkcoloredstring(lua_State *L, int idx, std::vector<Font::ColoredString> &strings)
+{
+	Font::ColoredString coloredstr;
+	coloredstr.color = Color(255, 255, 255, 255);
+
+	if (lua_istable(L, idx))
+	{
+		int len = luax_objlen(L, idx);
+
+		for (int i = 1; i <= len; i++)
+		{
+			lua_rawgeti(L, idx, i);
+
+			if (lua_istable(L, -1))
+			{
+				for (int j = 1; j <= 4; j++)
+					lua_rawgeti(L, -j, j);
+
+				coloredstr.color.r = (unsigned char) luaL_checknumber(L, -4);
+				coloredstr.color.g = (unsigned char) luaL_checknumber(L, -3);
+				coloredstr.color.b = (unsigned char) luaL_checknumber(L, -2);
+				coloredstr.color.a = (unsigned char) luaL_optnumber(L, -1, 255);
+
+				lua_pop(L, 4);
+			}
+			else
+			{
+				coloredstr.str = luaL_checkstring(L, -1);
+				strings.push_back(coloredstr);
+			}
+
+			lua_pop(L, 1);
+		}
+	}
+	else
+	{
+		coloredstr.str = luaL_checkstring(L, idx);
+		strings.push_back(coloredstr);
+	}
+}
+
 int w_Text_set(lua_State *L)
 {
 	Text *t = luax_checktext(L, 1);
@@ -44,7 +85,8 @@ int w_Text_set(lua_State *L)
 	else if (lua_isnoneornil(L, 3))
 	{
 		// Single argument: unformatted text.
-		std::string newtext = luax_checkstring(L, 2);
+		std::vector<Font::ColoredString> newtext;
+		luax_checkcoloredstring(L, 2, newtext);
 		luax_catchexcept(L, [&](){ t->set(newtext); });
 	}
 	else
@@ -57,7 +99,8 @@ int w_Text_set(lua_State *L)
 		if (!Font::getConstant(alignstr, align))
 			return luaL_error(L, "Invalid align mode: %s", alignstr);
 
-		std::string newtext = luax_checkstring(L, 2);
+		std::vector<Font::ColoredString> newtext;
+		luax_checkcoloredstring(L, 2, newtext);
 
 		luax_catchexcept(L, [&](){ t->set(newtext, wraplimit, align); });
 	}
@@ -76,7 +119,8 @@ int w_Text_setf(lua_State *L)
 	if (!Font::getConstant(alignstr, align))
 		return luaL_error(L, "Invalid align mode: %s", alignstr);
 
-	std::string newtext = luax_checkstring(L, 2);
+	std::vector<Font::ColoredString> newtext;
+	luax_checkcoloredstring(L, 2, newtext);
 
 	luax_catchexcept(L, [&](){ t->set(newtext, wraplimit, align); });
 
@@ -86,7 +130,9 @@ int w_Text_setf(lua_State *L)
 int w_Text_add(lua_State *L)
 {
 	Text *t = luax_checktext(L, 1);
-	std::string text = luax_checkstring(L, 2);
+
+	std::vector<Font::ColoredString> text;
+	luax_checkcoloredstring(L, 2, text);
 
 	float x  = (float) luaL_optnumber(L, 3, 0.0);
 	float y  = (float) luaL_optnumber(L, 4, 0.0);
@@ -105,7 +151,10 @@ int w_Text_add(lua_State *L)
 int w_Text_addf(lua_State *L)
 {
 	Text *t = luax_checktext(L, 1);
-	std::string text = luax_checkstring(L, 2);
+
+	std::vector<Font::ColoredString> text;
+	luax_checkcoloredstring(L, 2, text);
+
 	float wrap = (float) luaL_checknumber(L, 3);
 
 	Font::AlignMode align = Font::ALIGN_MAX_ENUM;

+ 1 - 0
src/modules/graphics/opengl/wrap_Text.h

@@ -32,6 +32,7 @@ namespace opengl
 {
 
 Text *luax_checktext(lua_State *L, int idx);
+void luax_checkcoloredstring(lua_State *L, int idx, std::vector<Font::ColoredString> &strings);
 int w_Text_set(lua_State *L);
 int w_Text_setf(lua_State *L);
 int w_Text_add(lua_State *L);