Browse Source

Added Text objects via love.graphics.newText(font [, textstring]). Reworked the internal code of Font objects to use fewer individual textures and less VRAM per object, and to be compatible with OpenGL ES. Slightly improved the performance of love.graphics.print and love.graphics.printf.

Text objects are Drawable objects that represent text on the screen. They decouple text drawing from text parsing and vertex uploading, in a similar manner to SpriteBatches. They also optionally allow efficient batching of text from the same Font in different relative coordinate transformations, without having many draw calls.

Text objects currently have the following methods:

Text:set(textstring) -- replaces any existing text in the object with the new string.
Text:setf(textstring, wraplimit, alignmode) -- replaces any existing text with formatted text using the new string.

Text:getFont() -- gets the Font object used for this Text object.
Text:getWidth() -- gets the width in pixels of the most recently added text in the Text object.
Text:getHeight() -- gets the height in pixels of the most recently added text in the Text object.

Text:add(textstring, x, y, angle, sx, sy, ox, oy, kx, ky) -- adds a new string to the text object using the specified relative coordinate transformation, without replacing existing text. This behaves much like SpriteBatch:add.
Text:addf(textstring, wraplimit, alignmode, x, y, angle, sx, sy, ox, oy, kx, ky) -- adds a new formatted string to the text object using the specified relative coordinate transformation.
Text:clear() -- clears all text from the object.

--HG--
branch : minor
Alex Szpakowski 10 years ago
parent
commit
970704c2b7

+ 4 - 0
CMakeLists.txt

@@ -294,6 +294,8 @@ set(LOVE_SRC_MODULE_GRAPHICS_OPENGL
 	src/modules/graphics/opengl/Shader.h
 	src/modules/graphics/opengl/SpriteBatch.cpp
 	src/modules/graphics/opengl/SpriteBatch.h
+	src/modules/graphics/opengl/Text.cpp
+	src/modules/graphics/opengl/Text.h
 	src/modules/graphics/opengl/Texture.h
 	src/modules/graphics/opengl/VertexBuffer.cpp
 	src/modules/graphics/opengl/VertexBuffer.h
@@ -315,6 +317,8 @@ set(LOVE_SRC_MODULE_GRAPHICS_OPENGL
 	src/modules/graphics/opengl/wrap_Shader.h
 	src/modules/graphics/opengl/wrap_SpriteBatch.cpp
 	src/modules/graphics/opengl/wrap_SpriteBatch.h
+	src/modules/graphics/opengl/wrap_Text.cpp
+	src/modules/graphics/opengl/wrap_Text.h
 	src/modules/graphics/opengl/wrap_Texture.cpp
 	src/modules/graphics/opengl/wrap_Texture.h
 )

+ 12 - 0
platform/macosx/love-framework.xcodeproj/project.pbxproj

@@ -317,6 +317,8 @@
 		FAC86E641724552C00EED715 /* wrap_Quad.h in Headers */ = {isa = PBXBuildFile; fileRef = FAC86E621724552C00EED715 /* wrap_Quad.h */; };
 		FAC86E6B1724555D00EED715 /* Quad.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FAC86E671724555D00EED715 /* Quad.cpp */; };
 		FAC86E6C1724555D00EED715 /* Quad.h in Headers */ = {isa = PBXBuildFile; fileRef = FAC86E681724555D00EED715 /* Quad.h */; };
+		FAD400A6196C5EBC004208B9 /* Text.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FAD400A5196C5EBC004208B9 /* Text.cpp */; };
+		FAD5970C196E174000E8EA36 /* wrap_Text.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FAD5970B196E174000E8EA36 /* wrap_Text.cpp */; };
 		FADD58DD18C30367005FC3BF /* FormatHandler.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FADD58DC18C30367005FC3BF /* FormatHandler.cpp */; };
 		FADE620919368D5C00C25B97 /* STBHandler.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FADE620719368D5C00C25B97 /* STBHandler.cpp */; };
 		FADE620A19368D5C00C25B97 /* STBHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = FADE620819368D5C00C25B97 /* STBHandler.h */; };
@@ -904,6 +906,10 @@
 		FAC86E621724552C00EED715 /* wrap_Quad.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = wrap_Quad.h; sourceTree = "<group>"; };
 		FAC86E671724555D00EED715 /* Quad.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Quad.cpp; sourceTree = "<group>"; };
 		FAC86E681724555D00EED715 /* Quad.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Quad.h; sourceTree = "<group>"; };
+		FAD400A5196C5EBC004208B9 /* Text.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Text.cpp; sourceTree = "<group>"; };
+		FAD400A7196C5ECC004208B9 /* Text.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Text.h; sourceTree = "<group>"; };
+		FAD5970B196E174000E8EA36 /* wrap_Text.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = wrap_Text.cpp; sourceTree = "<group>"; };
+		FAD5970D196E17C100E8EA36 /* wrap_Text.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = wrap_Text.h; sourceTree = "<group>"; };
 		FADD58DC18C30367005FC3BF /* FormatHandler.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FormatHandler.cpp; sourceTree = "<group>"; };
 		FADE620719368D5C00C25B97 /* STBHandler.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = STBHandler.cpp; sourceTree = "<group>"; };
 		FADE620819368D5C00C25B97 /* STBHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STBHandler.h; sourceTree = "<group>"; };
@@ -1755,6 +1761,8 @@
 				FA577A8616C71CF000860150 /* Shader.h */,
 				4D700D182EAA46273D1E2CC4 /* SpriteBatch.cpp */,
 				727D23FA1CC755B902471A45 /* SpriteBatch.h */,
+				FAD400A5196C5EBC004208B9 /* Text.cpp */,
+				FAD400A7196C5ECC004208B9 /* Text.h */,
 				FA9B49281875EFB900201DA9 /* Texture.h */,
 				426B1C4475DC54505B824B7F /* VertexBuffer.cpp */,
 				577B66502A5360AE60733B10 /* VertexBuffer.h */,
@@ -1776,6 +1784,8 @@
 				FA577A8816C71CF000860150 /* wrap_Shader.h */,
 				02C16FDB537A702F4D42534E /* wrap_SpriteBatch.cpp */,
 				2BE75A693BE206B22DAE1B2E /* wrap_SpriteBatch.h */,
+				FAD5970B196E174000E8EA36 /* wrap_Text.cpp */,
+				FAD5970D196E17C100E8EA36 /* wrap_Text.h */,
 				FA01BE1C1878E35B00640047 /* wrap_Texture.cpp */,
 				FA01BE1D1878E35B00640047 /* wrap_Texture.h */,
 			);
@@ -2481,6 +2491,8 @@
 				FA34E7D2174B515D00E04D3F /* System.cpp in Sources */,
 				FA1EF7C41799FEAC00FF380C /* wrap_System.cpp in Sources */,
 				FA435EA317B36E9C004C3F22 /* Polyline.cpp in Sources */,
+				FAD5970C196E174000E8EA36 /* wrap_Text.cpp in Sources */,
+				FAD400A6196C5EBC004208B9 /* Text.cpp in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 0 - 22
src/common/Matrix.cpp

@@ -177,28 +177,6 @@ void Matrix::shear(float kx, float ky)
 	this->operator *=(t);
 }
 
-//                 | x |
-//                 | y |
-//                 | 0 |
-//                 | 1 |
-// | e0 e4 e8  e12 |
-// | e1 e5 e9  e13 |
-// | e2 e6 e10 e14 |
-// | e3 e7 e11 e15 |
-
-void Matrix::transform(Vertex *dst, const Vertex *src, int size) const
-{
-	for (int i = 0; i<size; i++)
-	{
-		// Store in temp variables in case src = dst
-		float x = (e[0]*src[i].x) + (e[4]*src[i].y) + (0) + (e[12]);
-		float y = (e[1]*src[i].x) + (e[5]*src[i].y) + (0) + (e[13]);
-
-		dst[i].x = x;
-		dst[i].y = y;
-	}
-}
-
 Matrix Matrix::ortho(float left, float right, float bottom, float top)
 {
 	Matrix m;

+ 25 - 1
src/common/Matrix.h

@@ -153,7 +153,8 @@ public:
 	 * @param src The source vertices.
 	 * @param size The number of vertices.
 	 **/
-	void transform(Vertex *dst, const Vertex *src, int size) const;
+	template <typename V>
+	void transform(V *dst, const V *src, int size) const;
 
 	/**
 	 * Creates a new orthographic projection matrix with depth in the range of
@@ -173,6 +174,29 @@ private:
 
 }; // Matrix
 
+//                 | x |
+//                 | y |
+//                 | 0 |
+//                 | 1 |
+// | e0 e4 e8  e12 |
+// | e1 e5 e9  e13 |
+// | e2 e6 e10 e14 |
+// | e3 e7 e11 e15 |
+
+template <typename V>
+void Matrix::transform(V *dst, const V *src, int size) const
+{
+	for (int i = 0; i < size; i++)
+	{
+		// Store in temp variables in case src = dst
+		float x = (e[0]*src[i].x) + (e[4]*src[i].y) + (0) + (e[12]);
+		float y = (e[1]*src[i].x) + (e[5]*src[i].y) + (0) + (e[13]);
+
+		dst[i].x = x;
+		dst[i].y = y;
+	}
+}
+
 } //love
 
 #endif// LOVE_MATRIX_H

+ 4 - 4
src/common/Vector.h

@@ -216,15 +216,15 @@ inline float Vector::normalize(float length)
  **/
 
 inline Vector::Vector()
+	: x(0.0f)
+	, y(0.0f)
 {
-	x = 1;
-	y = 1;
 }
 
 inline Vector::Vector(float x, float y)
+	: x(x)
+	, y(y)
 {
-	this->x = x;
-	this->y = y;
 }
 
 inline Vector Vector::operator + (const Vector &v) const

+ 1 - 0
src/common/runtime.cpp

@@ -652,6 +652,7 @@ StringMap<Type, TYPE_MAX_ENUM>::Entry typeEntries[] =
 	{"Canvas", GRAPHICS_CANVAS_ID},
 	{"Shader", GRAPHICS_SHADER_ID},
 	{"Mesh", GRAPHICS_MESH_ID},
+	{"Text", GRAPHICS_TEXT_ID},
 
 	// Image
 	{"ImageData", IMAGE_IMAGE_DATA_ID},

+ 2 - 0
src/common/types.h

@@ -55,6 +55,7 @@ enum Type
 	GRAPHICS_CANVAS_ID,
 	GRAPHICS_SHADER_ID,
 	GRAPHICS_MESH_ID,
+	GRAPHICS_TEXT_ID,
 
 	// Image
 	IMAGE_IMAGE_DATA_ID,
@@ -141,6 +142,7 @@ const bits GRAPHICS_SPRITE_BATCH_T = (bits(1) << GRAPHICS_SPRITE_BATCH_ID) | GRA
 const bits GRAPHICS_CANVAS_T = (bits(1) << GRAPHICS_CANVAS_ID) | GRAPHICS_TEXTURE_T;
 const bits GRAPHICS_SHADER_T = (bits(1) << GRAPHICS_SHADER_ID) | OBJECT_T;
 const bits GRAPHICS_MESH_T = (bits(1) << GRAPHICS_MESH_ID) | GRAPHICS_DRAWABLE_T;
+const bits GRAPHICS_TEXT_T = (bits(1) << GRAPHICS_TEXT_ID) | GRAPHICS_DRAWABLE_T;
 
 // Image.
 const bits IMAGE_IMAGE_DATA_T = (bits(1) << IMAGE_IMAGE_DATA_ID) | DATA_T;

+ 0 - 20
src/modules/graphics/Graphics.cpp

@@ -39,16 +39,6 @@ bool Graphics::getConstant(DrawMode in, const char  *&out)
 	return drawModes.find(in, out);
 }
 
-bool Graphics::getConstant(const char *in, AlignMode &out)
-{
-	return alignModes.find(in, out);
-}
-
-bool Graphics::getConstant(AlignMode in, const char  *&out)
-{
-	return alignModes.find(in, out);
-}
-
 bool Graphics::getConstant(const char *in, BlendMode &out)
 {
 	return blendModes.find(in, out);
@@ -137,16 +127,6 @@ StringMap<Graphics::DrawMode, Graphics::DRAW_MAX_ENUM>::Entry Graphics::drawMode
 
 StringMap<Graphics::DrawMode, Graphics::DRAW_MAX_ENUM> Graphics::drawModes(Graphics::drawModeEntries, sizeof(Graphics::drawModeEntries));
 
-StringMap<Graphics::AlignMode, Graphics::ALIGN_MAX_ENUM>::Entry Graphics::alignModeEntries[] =
-{
-	{ "left", Graphics::ALIGN_LEFT },
-	{ "right", Graphics::ALIGN_RIGHT },
-	{ "center", Graphics::ALIGN_CENTER },
-	{ "justify", Graphics::ALIGN_JUSTIFY },
-};
-
-StringMap<Graphics::AlignMode, Graphics::ALIGN_MAX_ENUM> Graphics::alignModes(Graphics::alignModeEntries, sizeof(Graphics::alignModeEntries));
-
 StringMap<Graphics::BlendMode, Graphics::BLEND_MAX_ENUM>::Entry Graphics::blendModeEntries[] =
 {
 	{ "alpha", Graphics::BLEND_ALPHA },

+ 0 - 15
src/modules/graphics/Graphics.h

@@ -44,15 +44,6 @@ public:
 		DRAW_MAX_ENUM
 	};
 
-	enum AlignMode
-	{
-		ALIGN_LEFT,
-		ALIGN_CENTER,
-		ALIGN_RIGHT,
-		ALIGN_JUSTIFY,
-		ALIGN_MAX_ENUM
-	};
-
 	enum BlendMode
 	{
 		BLEND_ALPHA,
@@ -172,9 +163,6 @@ public:
 	static bool getConstant(const char *in, DrawMode &out);
 	static bool getConstant(DrawMode in, const char  *&out);
 
-	static bool getConstant(const char *in, AlignMode &out);
-	static bool getConstant(AlignMode in, const char  *&out);
-
 	static bool getConstant(const char *in, BlendMode &out);
 	static bool getConstant(BlendMode in, const char  *&out);
 
@@ -204,9 +192,6 @@ private:
 	static StringMap<DrawMode, DRAW_MAX_ENUM>::Entry drawModeEntries[];
 	static StringMap<DrawMode, DRAW_MAX_ENUM> drawModes;
 
-	static StringMap<AlignMode, ALIGN_MAX_ENUM>::Entry alignModeEntries[];
-	static StringMap<AlignMode, ALIGN_MAX_ENUM> alignModes;
-
 	static StringMap<BlendMode, BLEND_MAX_ENUM>::Entry blendModeEntries[];
 	static StringMap<BlendMode, BLEND_MAX_ENUM> blendModes;
 

+ 397 - 209
src/modules/graphics/opengl/Font.cpp

@@ -29,6 +29,7 @@
 #include <math.h>
 #include <sstream>
 #include <algorithm> // for max
+#include <limits>
 
 namespace love
 {
@@ -39,57 +40,44 @@ namespace opengl
 
 int Font::fontCount = 0;
 
-const int Font::TEXTURE_WIDTHS[]  = {128, 256, 256, 512, 512, 1024, 1024};
-const int Font::TEXTURE_HEIGHTS[] = {128, 128, 256, 256, 512, 512,  1024};
-
 Font::Font(love::font::Rasterizer *r, const Texture::Filter &filter)
 	: rasterizer(r)
 	, height(r->getHeight())
 	, lineHeight(1)
-	, mSpacing(1)
+	, textureWidth(128)
+	, textureHeight(128)
 	, filter(filter)
 	, useSpacesAsTab(false)
+	, indexBuffer(20) // We make this bigger at draw-time, if needed.
+	, textureCacheID(0)
 	, textureMemorySize(0)
 {
 	this->filter.mipmap = Texture::FILTER_NONE;
 
 	// Try to find the best texture size match for the font size. default to the
 	// largest texture size if no rough match is found.
-	textureSizeIndex = NUM_TEXTURE_SIZES - 1;
-	for (int i = 0; i < NUM_TEXTURE_SIZES; i++)
+	while (true)
 	{
-		// Make a rough estimate of the total used texture size, based on glyph
-		// height. The estimated size is likely larger than the actual total
-		// size, which is good because texture switching is expensive.
-		if ((height * 0.8) * height * 95 <= TEXTURE_WIDTHS[i] * TEXTURE_HEIGHTS[i])
-		{
-			textureSizeIndex = i;
+		if ((height * 0.8) * height * 30 <= textureWidth * textureHeight)
 			break;
-		}
-	}
 
-	textureWidth = TEXTURE_WIDTHS[textureSizeIndex];
-	textureHeight = TEXTURE_HEIGHTS[textureSizeIndex];
+		TextureSize nextsize = getNextTextureSize();
 
-	love::font::GlyphData *gd = nullptr;
+		if (nextsize.width <= textureWidth && nextsize.height <= textureHeight)
+			break;
 
-	try
-	{
-		gd = r->getGlyphData(32); // Space character.
-		type = (gd->getFormat() == love::font::GlyphData::FORMAT_LUMINANCE_ALPHA) ? FONT_TRUETYPE : FONT_IMAGE;
+		textureWidth = nextsize.width;
+		textureHeight = nextsize.height;
+	}
 
-		if (!r->hasGlyph(9)) // No tab character in the Rasterizer.
-			useSpacesAsTab = true;
+	love::font::GlyphData *gd = r->getGlyphData(32); // Space character.
+	type = (gd->getFormat() == font::GlyphData::FORMAT_LUMINANCE_ALPHA) ? FONT_TRUETYPE : FONT_IMAGE;
+	gd->release();
 
-		loadVolatile();
-	}
-	catch (love::Exception &)
-	{
-		delete gd;
-		throw;
-	}
+	if (!r->hasGlyph(9)) // No tab character in the Rasterizer.
+		useSpacesAsTab = true;
 
-	delete gd;
+	loadVolatile();
 
 	++fontCount;
 }
@@ -101,109 +89,135 @@ Font::~Font()
 	--fontCount;
 }
 
-bool Font::initializeTexture(GLenum format)
+Font::TextureSize Font::getNextTextureSize() const
 {
-	GLint internalformat = (format == GL_LUMINANCE_ALPHA) ? GL_LUMINANCE8_ALPHA8 : GL_RGBA8;
+	TextureSize size = {textureWidth, textureHeight};
 
-	// clear errors before initializing
-	while (glGetError() != GL_NO_ERROR);
+	int maxsize = std::min(4096, gl.getMaxTextureSize());
+
+	if (size.width * 2 <= maxsize || size.height * 2 <= maxsize)
+	{
+		// {128, 128} -> {256, 128} -> {256, 256} -> {512, 256} -> etc.
+		if (size.width == size.height)
+			size.width *= 2;
+		else
+			size.height *= 2;
+	}
 
-	glTexImage2D(GL_TEXTURE_2D,
-				 0,
-				 internalformat,
-				 (GLsizei)textureWidth,
-				 (GLsizei)textureHeight,
-				 0,
-				 format,
-				 GL_UNSIGNED_BYTE,
-				 NULL);
-
-	return glGetError() == GL_NO_ERROR;
+	return size;
 }
 
 void Font::createTexture()
 {
-	textureX = textureY = rowHeight = TEXTURE_PADDING;
+	GLenum format = type == FONT_TRUETYPE ? GL_LUMINANCE_ALPHA : GL_RGBA;
+	size_t bpp = format == GL_LUMINANCE_ALPHA ? 2 : 4;
+
+	size_t prevmemsize = textureMemorySize;
+	if (prevmemsize > 0)
+	{
+		textureMemorySize -= (textureWidth * textureHeight * bpp);
+		gl.updateTextureMemorySize(prevmemsize, textureMemorySize);
+	}
 
-	GLuint t;
-	glGenTextures(1, &t);
-	textures.push_back(t);
+	GLuint t = 0;
+	TextureSize size = {textureWidth, textureHeight};
+	TextureSize nextsize = getNextTextureSize();
+	bool recreatetexture = false;
+
+	// If we have an existing texture already, we'll try replacing it with a
+	// larger-sized one rather than creating a second one. Having a single
+	// texture reduces texture switches and draw calls when rendering.
+	if ((nextsize.width > size.width || nextsize.height > size.height)
+		&& !textures.empty())
+	{
+		recreatetexture = true;
+		size = nextsize;
+		t = textures.back();
+	}
+	else
+		glGenTextures(1, &t);
 
 	gl.bindTexture(t);
 
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+	gl.setTextureFilter(filter);
 
 	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
 	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 
-	GLenum format = (type == FONT_TRUETYPE ? GL_LUMINANCE_ALPHA : GL_RGBA);
+	GLenum internalformat = type == FONT_TRUETYPE ? GL_LUMINANCE8_ALPHA8 : GL_RGBA8;
 
-	// Initialize the texture, attempting smaller sizes if initialization fails.
-	bool initialized = false;
-	while (textureSizeIndex >= 0)
-	{
-		textureWidth = TEXTURE_WIDTHS[textureSizeIndex];
-		textureHeight = TEXTURE_HEIGHTS[textureSizeIndex];
+	// in GLES2, the internalformat and format params of TexImage have to match.
+	if (GLAD_ES_VERSION_2_0 && !GLAD_ES_VERSION_3_0)
+		internalformat = format;
 
-		initialized = initializeTexture(format);
+	// Initialize the texture with transparent black.
+	std::vector<GLubyte> emptydata(size.width * size.height * bpp, 0);
 
-		if (initialized || textureSizeIndex <= 0)
-			break;
+	// Clear errors before initializing.
+	while (glGetError() != GL_NO_ERROR);
 
-		--textureSizeIndex;
-	}
+	glTexImage2D(GL_TEXTURE_2D, 0, internalformat, size.width, size.height, 0,
+	             format, GL_UNSIGNED_BYTE, &emptydata[0]);
 
-	if (!initialized)
+	if (glGetError() != GL_NO_ERROR)
 	{
-		// Clean up before throwing.
-		gl.deleteTexture(t);
-		gl.bindTexture(gl.getDefaultTexture());
-		textures.pop_back();
-
+		if (!recreatetexture)
+			gl.deleteTexture(t);
 		throw love::Exception("Could not create font texture!");
 	}
 
-	// Fill the texture with transparent black.
-	std::vector<GLubyte> emptyData(textureWidth * textureHeight * (type == FONT_TRUETYPE ? 2 : 4), 0);
-	glTexSubImage2D(GL_TEXTURE_2D,
-					0,
-					0, 0,
-					(GLsizei)textureWidth,
-					(GLsizei)textureHeight,
-					format,
-					GL_UNSIGNED_BYTE,
-					&emptyData[0]);
-
-	setFilter(filter);
+	textureWidth  = size.width;
+	textureHeight = size.height;
 
-	size_t prevmemsize = textureMemorySize;
+	rowHeight = textureX = textureY = TEXTURE_PADDING;
 
-	textureMemorySize += emptyData.size();
+	prevmemsize = textureMemorySize;
+	textureMemorySize += emptydata.size();
 	gl.updateTextureMemorySize(prevmemsize, textureMemorySize);
+
+	// Re-add the old glyphs if we re-created the existing texture object.
+	if (recreatetexture)
+	{
+		textureCacheID++;
+
+		std::vector<uint32> glyphstoadd;
+
+		for (const auto &glyphpair : glyphs)
+			glyphstoadd.push_back(glyphpair.first);
+
+		glyphs.clear();
+
+		for (uint32 g : glyphstoadd)
+			addGlyph(g);
+	}
+	else
+		textures.push_back(t);
 }
 
-Font::Glyph *Font::addGlyph(uint32 glyph)
+love::font::GlyphData *Font::getRasterizerGlyphData(uint32 glyph)
 {
-	love::font::GlyphData *gd = nullptr;
-
 	// Use spaces for the tab 'glyph'.
 	if (glyph == 9 && useSpacesAsTab)
 	{
 		love::font::GlyphData *spacegd = rasterizer->getGlyphData(32);
+		love::font::GlyphData::Format fmt = spacegd->getFormat();
 
 		love::font::GlyphMetrics gm = {};
 		gm.advance = spacegd->getAdvance() * SPACES_PER_TAB;
 		gm.bearingX = spacegd->getBearingX();
 		gm.bearingY = spacegd->getBearingY();
-		love::font::GlyphData::Format f = spacegd->getFormat();
 
 		spacegd->release();
 
-		gd = new love::font::GlyphData(glyph, gm, f);
+		return new love::font::GlyphData(glyph, gm, fmt);
 	}
-	else
-		gd = rasterizer->getGlyphData(glyph);
+
+	return rasterizer->getGlyphData(glyph);
+}
+
+const Font::Glyph &Font::addGlyph(uint32 glyph)
+{
+	love::font::GlyphData *gd = getRasterizerGlyphData(glyph);
 
 	int w = gd->getWidth();
 	int h = gd->getHeight();
@@ -229,89 +243,87 @@ Font::Glyph *Font::addGlyph(uint32 glyph)
 		}
 	}
 
-	Glyph *g = new Glyph;
+	Glyph g;
 
-	g->texture = 0;
-	g->spacing = gd->getAdvance();
+	g.texture = 0;
+	g.spacing = gd->getAdvance();
 
-	memset(g->vertices, 0, sizeof(GlyphVertex) * 4);
+	memset(g.vertices, 0, sizeof(GlyphVertex) * 4);
 
-	// don't waste space for empty glyphs. also fixes a division by zero bug with ati drivers
+	// don't waste space for empty glyphs. also fixes a divide by zero bug with ATI drivers
 	if (w > 0 && h > 0)
 	{
 		const GLuint t = textures.back();
 
 		gl.bindTexture(t);
-		glTexSubImage2D(GL_TEXTURE_2D,
-						0,
-						textureX,
-						textureY,
-						w, h,
-						(type == FONT_TRUETYPE ? GL_LUMINANCE_ALPHA : GL_RGBA),
-						GL_UNSIGNED_BYTE,
-						gd->getData());
+		glTexSubImage2D(GL_TEXTURE_2D, 0, textureX, textureY, w, h,
+		                (type == FONT_TRUETYPE ? GL_LUMINANCE_ALPHA : GL_RGBA),
+		                GL_UNSIGNED_BYTE, gd->getData());
+
+		g.texture = t;
 
-		g->texture = t;
+		float tX     = (float) textureX,     tY      = (float) textureY;
+		float tWidth = (float) textureWidth, tHeight = (float) textureHeight;
 
 		const GlyphVertex verts[4] = {
-			{    0.0f,     0.0f, float(textureX)/float(textureWidth),   float(textureY)/float(textureHeight)},
-			{    0.0f, float(h), float(textureX)/float(textureWidth),   float(textureY+h)/float(textureHeight)},
-			{float(w), float(h), float(textureX+w)/float(textureWidth), float(textureY+h)/float(textureHeight)},
-			{float(w),     0.0f, float(textureX+w)/float(textureWidth), float(textureY)/float(textureHeight)},
+			{    0.0f,     0.0f,     tX/tWidth,     tY/tHeight},
+			{    0.0f, float(h),     tX/tWidth, (tY+h)/tHeight},
+			{float(w), float(h), (tX+w)/tWidth, (tY+h)/tHeight},
+			{float(w),     0.0f, (tX+w)/tWidth,     tY/tHeight}
 		};
 
-		// copy vertex data to the glyph and set proper bearing
+		// Copy vertex data to the glyph and set proper bearing.
 		for (int i = 0; i < 4; i++)
 		{
-			g->vertices[i] = verts[i];
-			g->vertices[i].x += gd->getBearingX();
-			g->vertices[i].y -= gd->getBearingY();
+			g.vertices[i] = verts[i];
+			g.vertices[i].x += gd->getBearingX();
+			g.vertices[i].y -= gd->getBearingY();
 		}
 	}
 
 	if (w > 0)
 		textureX += (w + TEXTURE_PADDING);
+
 	if (h > 0)
 		rowHeight = std::max(rowHeight, h + TEXTURE_PADDING);
 
-	delete gd;
+	gd->release();
 
-	glyphs[glyph] = g;
+	const auto p = glyphs.insert(std::make_pair(glyph, g));
 
-	return g;
+	return p.first->second;
 }
 
-Font::Glyph *Font::findGlyph(uint32 glyph)
+const Font::Glyph &Font::findGlyph(uint32 glyph)
 {
-	auto it = glyphs.find(glyph);
+	const auto it = glyphs.find(glyph);
 
 	if (it != glyphs.end())
 		return it->second;
-	else
-		return addGlyph(glyph);
+
+	return addGlyph(glyph);
 }
 
 float Font::getHeight() const
 {
-	return static_cast<float>(height);
+	return (float) height;
 }
 
-void Font::print(const std::string &text, float x, float y, float extra_spacing, float angle, float sx, float sy, float ox, float oy, float kx, float ky)
+std::vector<Font::DrawCommand> Font::generateVertices(const std::string &text, std::vector<GlyphVertex> &vertices, float extra_spacing, Vector offset, TextInfo *info)
 {
 	// Spacing counter and newline handling.
-	float dx = 0.0f;
-	float dy = 0.0f;
+	float dx = offset.x;
+	float dy = offset.y;
 
 	float lineheight = getBaseline();
+	int maxwidth = 0;
 
 	// Keeps track of when we need to switch textures in our vertex array.
-	std::vector<GlyphArrayDrawInfo> glyphinfolist;
+	std::vector<DrawCommand> drawcommands;
 
 	// Pre-allocate space for the maximum possible number of vertices.
-	std::vector<GlyphVertex> glyphverts;
-	glyphverts.reserve(text.length() * 4);
-
-	int vertexcount = 0;
+	size_t vertstartsize = vertices.size();
+	vertices.reserve(vertstartsize + text.length() * 4);
 
 	try
 	{
@@ -324,41 +336,57 @@ void Font::print(const std::string &text, float x, float y, float extra_spacing,
 
 			if (g == '\n')
 			{
+				if (dx > maxwidth)
+					maxwidth = (int) dx;
+
 				// Wrap newline, but do not print it.
 				dy += floorf(getHeight() * getLineHeight() + 0.5f);
-				dx = 0.0f;
+				dx = offset.x;
 				continue;
 			}
 
-			Glyph *glyph = findGlyph(g);
+			uint32 cacheid = textureCacheID;
 
-			if (glyph->texture != 0)
+			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);
+				continue;
+			}
+
+			if (glyph.texture != 0)
 			{
 				// Copy the vertices and set their proper relative positions.
 				for (int j = 0; j < 4; j++)
 				{
-					glyphverts.push_back(glyph->vertices[j]);
-					glyphverts.back().x += dx;
-					glyphverts.back().y += dy + lineheight;
+					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 (glyphinfolist.size() == 0 || glyphinfolist.back().texture != glyph->texture)
+				if (drawcommands.empty() || drawcommands.back().texture != glyph.texture)
 				{
-					// keep track of each sub-section of the string whose glyphs use different textures than the previous section
-					GlyphArrayDrawInfo gdrawinfo;
-					gdrawinfo.startvertex = vertexcount;
-					gdrawinfo.vertexcount = 0;
-					gdrawinfo.texture = glyph->texture;
-					glyphinfolist.push_back(gdrawinfo);
+					// 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);
 				}
 
-				vertexcount += 4;
-				glyphinfolist.back().vertexcount += 4;
+				drawcommands.back().vertexcount += 4;
 			}
 
 			// Advance the x position for the next glyph.
-			dx += glyph->spacing;
+			dx += glyph.spacing;
 
 			// Account for extra spacing given to space characters.
 			if (g == ' ' && extra_spacing != 0.0f)
@@ -370,15 +398,151 @@ void Font::print(const std::string &text, float x, float y, float extra_spacing,
 		throw love::Exception("UTF-8 decoding error: %s", e.what());
 	}
 
-	if (vertexcount <= 0 || glyphinfolist.size() == 0)
-		return;
+	// Sort draw commands by texture first, and quad position in memory second
+	// (using the struct's < operator).
+	std::sort(drawcommands.begin(), drawcommands.end());
 
-	// Sort glyph draw info list by texture first, and quad position in memory
-	// second (using the struct's < operator).
-	std::sort(glyphinfolist.begin(), glyphinfolist.end());
+	if (dx > maxwidth)
+		maxwidth = (int) dx;
 
-	Matrix t;
-	t.setTransformation(ceilf(x), ceilf(y), angle, sx, sy, ox, oy, kx, ky);
+	if (info != nullptr)
+	{
+		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::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();
+
+	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;
+
+	// We only need the list of wrapped lines in 'justify' mode.
+	getWrap(text, wrap, lines, &widths, align == ALIGN_JUSTIFY ? &wrappedlines : nullptr);
+
+	float extraspacing = 0.0f;
+	int numspaces = 0;
+	int i = 0;
+	float y = 0.0f;
+	float maxwidth = 0;
+
+	for (const std::string &line : lines)
+	{
+		extraspacing = 0.0f;
+		float width = (float) widths[i];
+		love::Vector offset(0.0f, floorf(y));
+
+		if (width > maxwidth)
+			maxwidth = width;
+
+		switch (align)
+		{
+		case ALIGN_RIGHT:
+			offset.x = floorf(wrap - width);
+			break;
+		case ALIGN_CENTER:
+			offset.x = floorf((wrap - width) / 2.0f);
+			break;
+		case ALIGN_JUSTIFY:
+			numspaces = std::count(line.begin(), line.end(), ' ');
+			if (wrappedlines[i] && numspaces >= 1)
+				extraspacing = (wrap - width) / float(numspaces);
+			else
+				extraspacing = 0.0f;
+			break;
+		case ALIGN_LEFT:
+		default:
+			break;
+		}
+
+		std::vector<DrawCommand> commands = generateVertices(line, vertices, extraspacing, offset);
+
+		if (!commands.empty())
+		{
+			// 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.)
+			auto firstcmd = commands.begin();
+			auto prevcmd = drawcommands.back();
+			if (!drawcommands.empty() && prevcmd.texture == firstcmd->texture
+				&& (prevcmd.startvertex + prevcmd.vertexcount) == firstcmd->startvertex)
+			{
+				drawcommands.back().vertexcount += firstcmd->vertexcount;
+				++firstcmd;
+			}
+
+			// Append the new draw commands to the list we're building.
+			drawcommands.insert(drawcommands.end(), firstcmd, commands.end());
+		}
+
+		y += getHeight() * getLineHeight();
+		i++;
+	}
+
+	if (info != nullptr)
+	{
+		info->width = (int) maxwidth;
+		info->height = (int) y;
+	}
+
+	if (cacheid != textureCacheID)
+	{
+		vertices.clear();
+		drawcommands = generateVerticesFormatted(text, wrap, align, vertices);
+	}
+
+	return drawcommands;
+}
+
+void Font::drawVertices(const std::vector<DrawCommand> &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)
+		totalverts = std::max(cmd.startvertex + cmd.vertexcount, totalverts);
+
+	if ((size_t) totalverts / 4 > indexBuffer.getSize())
+		indexBuffer = VertexIndex((size_t) totalverts / 4);
+
+	gl.prepareDraw();
+
+	const GLenum gltype = indexBuffer.getType();
+	const size_t elemsize = indexBuffer.getElementSize();
+
+	VertexBuffer::Bind bind(*indexBuffer.getVertexBuffer());
+
+	// 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)
+	{
+		GLsizei count = (cmd.vertexcount / 4) * 6;
+		size_t offset = (cmd.startvertex / 4) * 6 * elemsize;
+
+		// TODO: Use glDrawElementsBaseVertex when supported?
+		gl.bindTexture(cmd.texture);
+		gl.drawElements(GL_TRIANGLES, count, gltype, indexBuffer.getPointer(offset));
+	}
+}
+
+void Font::printv(const Matrix &t, const std::vector<DrawCommand> &drawcommands, const std::vector<GlyphVertex> &vertices)
+{
+	if (vertices.empty() || drawcommands.empty())
+		return;
 
 	OpenGL::TempTransform transform(gl);
 	transform.get() *= t;
@@ -386,31 +550,52 @@ void Font::print(const std::string &text, float x, float y, float extra_spacing,
 	glEnableVertexAttribArray(ATTRIB_POS);
 	glEnableVertexAttribArray(ATTRIB_TEXCOORD);
 
-	glVertexAttribPointer(ATTRIB_POS, 2, GL_FLOAT, GL_FALSE, sizeof(GlyphVertex), &glyphverts[0].x);
-	glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, sizeof(GlyphVertex), &glyphverts[0].s);
-
-	gl.prepareDraw();
+	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);
 
-	// We need to draw a new vertex array for every section of the string which
-	// uses a different texture than the previous section.
-	std::vector<GlyphArrayDrawInfo>::const_iterator it;
-	for (it = glyphinfolist.begin(); it != glyphinfolist.end(); ++it)
+	try
+	{
+		drawVertices(drawcommands);
+	}
+	catch (love::Exception &)
 	{
-		gl.bindTexture(it->texture);
-		gl.drawArrays(GL_QUADS, it->startvertex, it->vertexcount);
+		glDisableVertexAttribArray(ATTRIB_TEXCOORD);
+		glDisableVertexAttribArray(ATTRIB_POS);
+		throw;
 	}
 
 	glDisableVertexAttribArray(ATTRIB_TEXCOORD);
 	glDisableVertexAttribArray(ATTRIB_POS);
 }
 
+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)
+{
+	std::vector<GlyphVertex> vertices;
+	std::vector<DrawCommand> drawcommands = generateVertices(text, vertices);
+
+	Matrix t;
+	t.setTransformation(ceilf(x), ceilf(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)
+{
+	std::vector<GlyphVertex> vertices;
+	std::vector<DrawCommand> drawcommands = generateVerticesFormatted(text, wrap, align, vertices);
+
+	Matrix t;
+	t.setTransformation(ceilf(x), ceilf(y), angle, sx, sy, ox, oy, kx, ky);
+
+	printv(t, drawcommands, vertices);
+}
+
 int Font::getWidth(const std::string &str)
 {
 	if (str.size() == 0) return 0;
 
 	std::istringstream iss(str);
 	std::string line;
-	Glyph *g;
 	int max_width = 0;
 
 	while (getline(iss, line, '\n'))
@@ -423,8 +608,8 @@ int Font::getWidth(const std::string &str)
 			while (i != end)
 			{
 				uint32 c = *i++;
-				g = findGlyph(c);
-				width += static_cast<int>(g->spacing * mSpacing);
+				const Glyph &g = findGlyph(c);
+				width += g.spacing;
 			}
 		}
 		catch(utf8::exception &e)
@@ -441,16 +626,14 @@ int Font::getWidth(const std::string &str)
 
 int Font::getWidth(char character)
 {
-	Glyph *g = findGlyph(character);
-	return g->spacing;
+	const Glyph &g = findGlyph(character);
+	return g.spacing;
 }
 
-std::vector<std::string> Font::getWrap(const std::string &text, float wrap, int *max_width, std::vector<bool> *wrappedlines)
+void Font::getWrap(const std::string &text, float wrap, std::vector<std::string> &lines, std::vector<int> *linewidths, std::vector<bool> *wrappedlines)
 {
 	using namespace std;
-	const float width_space = static_cast<float>(getWidth(' '));
-	vector<string> lines_to_draw;
-	int maxw = 0;
+	const float width_space = (float) getWidth(' ');
 
 	//split text at newlines
 	istringstream iss(text);
@@ -481,12 +664,13 @@ std::vector<std::string> Font::getWrap(const std::string &text, float wrap, int
 
 				// remove trailing space
 				string tmp = string_builder.str();
-				lines_to_draw.push_back(tmp.substr(0,tmp.size()-1));
+				lines.push_back(tmp.substr(0,tmp.size()-1));
 				string_builder.str("");
 				width = static_cast<float>(getWidth(word));
 				realw -= (int) width;
-				if (realw > maxw)
-					maxw = realw;
+
+				if (linewidths)
+					linewidths->push_back(realw);
 
 				// Indicate that this line was automatically wrapped.
 				if (wrappedlines)
@@ -497,25 +681,21 @@ std::vector<std::string> Font::getWrap(const std::string &text, float wrap, int
 			oldwidth = width;
 		}
 		// push last line
-		if (width > maxw)
-			maxw = (int) width;
+		if (linewidths)
+			linewidths->push_back(width);
+
 		string tmp = string_builder.str();
-		lines_to_draw.push_back(tmp.substr(0,tmp.size()-1));
+		lines.push_back(tmp.substr(0,tmp.size()-1));
 
 		// Indicate that this line was not automatically wrapped.
 		if (wrappedlines)
 			wrappedlines->push_back(false);
 	}
-
-	if (max_width)
-		 *max_width = maxw;
-
-	return lines_to_draw;
 }
 
 void Font::setLineHeight(float height)
 {
-	this->lineHeight = height;
+	lineHeight = height;
 }
 
 float Font::getLineHeight() const
@@ -523,16 +703,6 @@ float Font::getLineHeight() const
 	return lineHeight;
 }
 
-void Font::setSpacing(float amount)
-{
-	mSpacing = amount;
-}
-
-float Font::getSpacing() const
-{
-	return mSpacing;
-}
-
 void Font::setFilter(const Texture::Filter &f)
 {
 	if (!Texture::validateFilter(f, false))
@@ -540,9 +710,9 @@ void Font::setFilter(const Texture::Filter &f)
 
 	filter = f;
 
-	for (auto it = textures.begin(); it != textures.end(); ++it)
+	for (GLuint texture : textures)
 	{
-		gl.bindTexture(*it);
+		gl.bindTexture(texture);
 		gl.setTextureFilter(filter);
 	}
 }
@@ -555,26 +725,19 @@ const Texture::Filter &Font::getFilter()
 bool Font::loadVolatile()
 {
 	createTexture();
+	textureCacheID++;
 	return true;
 }
 
 void Font::unloadVolatile()
 {
 	// nuke everything from orbit
-	std::map<uint32, Glyph *>::iterator it = glyphs.begin();
-	Glyph *g;
-	while (it != glyphs.end())
-	{
-		g = it->second;
-		delete g;
-		glyphs.erase(it++);
-	}
-	std::vector<GLuint>::iterator iter = textures.begin();
-	while (iter != textures.end())
-	{
-		gl.deleteTexture(*iter);
-		iter++;
-	}
+
+	glyphs.clear();
+
+	for (GLuint texture : textures)
+		gl.deleteTexture(texture);
+
 	textures.clear();
 
 	gl.updateTextureMemorySize(textureMemorySize, 0);
@@ -607,6 +770,31 @@ bool Font::hasGlyphs(const std::string &text) const
 	return rasterizer->hasGlyphs(text);
 }
 
+uint32 Font::getTextureCacheID() const
+{
+	return textureCacheID;
+}
+
+bool Font::getConstant(const char *in, AlignMode &out)
+{
+	return alignModes.find(in, out);
+}
+
+bool Font::getConstant(AlignMode in, const char  *&out)
+{
+	return alignModes.find(in, out);
+}
+
+StringMap<Font::AlignMode, Font::ALIGN_MAX_ENUM>::Entry Font::alignModeEntries[] =
+{
+	{ "left", Font::ALIGN_LEFT },
+	{ "right", Font::ALIGN_RIGHT },
+	{ "center", Font::ALIGN_CENTER },
+	{ "justify", Font::ALIGN_JUSTIFY },
+};
+
+StringMap<Font::AlignMode, Font::ALIGN_MAX_ENUM> Font::alignModes(Font::alignModeEntries, sizeof(Font::alignModeEntries));
+
 } // opengl
 } // graphics
 } // love

+ 77 - 48
src/modules/graphics/opengl/Font.h

@@ -22,15 +22,20 @@
 #define LOVE_GRAPHICS_OPENGL_FONT_H
 
 // STD
-#include <map>
+#include <unordered_map>
 #include <string>
 #include <vector>
 
 // LOVE
+#include "common/config.h"
 #include "common/Object.h"
+#include "common/Matrix.h"
+#include "common/Vector.h"
+
 #include "font/Rasterizer.h"
 #include "graphics/Texture.h"
 #include "graphics/Volatile.h"
+#include "VertexBuffer.h"
 
 #include "OpenGL.h"
 
@@ -45,17 +50,60 @@ class Font : public Object, public Volatile
 {
 public:
 
+	enum AlignMode
+	{
+		ALIGN_LEFT,
+		ALIGN_CENTER,
+		ALIGN_RIGHT,
+		ALIGN_JUSTIFY,
+		ALIGN_MAX_ENUM
+	};
+
+	struct GlyphVertex
+	{
+		float x, y;
+		float s, t;
+	};
+
+	struct TextInfo
+	{
+		int width;
+		int height;
+	};
+
+	// Used to determine when to change textures in the generated vertex array.
+	struct DrawCommand
+	{
+		GLuint texture;
+		int startvertex;
+		int vertexcount;
+
+		// used when sorting with std::sort.
+		bool operator < (const DrawCommand &other) const
+		{
+			// Texture binds are expensive, so we should sort by that first.
+			if (texture != other.texture)
+				return texture < other.texture;
+			else
+				return startvertex < other.startvertex;
+		}
+	};
+
 	Font(love::font::Rasterizer *r, const Texture::Filter &filter = Texture::getDefaultFilter());
 
 	virtual ~Font();
 
+	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);
+
 	/**
 	 * Prints the text at the designated position with rotation and scaling.
 	 *
 	 * @param text A string.
 	 * @param x The x-coordinate.
 	 * @param y The y-coordinate.
-	 * @param extra_spacing Additional spacing added to spaces (" ").
 	 * @param angle The amount of rotation.
 	 * @param sx Scale along the x axis.
 	 * @param sy Scale along the y axis.
@@ -64,7 +112,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 extra_spacing = 0.0f, 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::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 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);
 
 	/**
 	 * Returns the height of the font.
@@ -96,7 +146,7 @@ public:
 	 *        auto-wrapped. Indices correspond to indices of the returned value.
 	 * Returns a vector with the lines.
 	 **/
-	std::vector<std::string> getWrap(const std::string &text, float wrap, int *max_width = 0, std::vector<bool> *wrapped_lines = 0);
+	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);
 
 	/**
 	 * Sets the line height (which should be a number to multiply the font size by,
@@ -110,19 +160,6 @@ public:
 	 **/
 	float getLineHeight() const;
 
-	/**
-	 * Sets the spacing modifier (changes the spacing between the characters the
-	 * same way that the line height does [multiplication]).
-	 * Note: The spacing must be set BEFORE the font is loaded to have any effect.
-	 * @param amount The amount of modification.
-	 **/
-	void setSpacing(float amount);
-
-	/**
-	 * Returns the spacing modifier.
-	 **/
-	float getSpacing() const;
-
 	void setFilter(const Texture::Filter &f);
 	const Texture::Filter &getFilter();
 
@@ -138,6 +175,11 @@ public:
 	bool hasGlyph(uint32 glyph) const;
 	bool hasGlyphs(const std::string &text) const;
 
+	uint32 getTextureCacheID() const;
+
+	static bool getConstant(const char *in, AlignMode &out);
+	static bool getConstant(AlignMode in, const char  *&out);
+
 	static int fontCount;
 
 private:
@@ -149,12 +191,6 @@ private:
 		FONT_UNKNOWN
 	};
 
-	struct GlyphVertex
-	{
-		float x, y;
-		float s, t;
-	};
-
 	struct Glyph
 	{
 		GLuint texture;
@@ -162,36 +198,24 @@ private:
 		GlyphVertex vertices[4];
 	};
 
-	// used to determine when to change textures in the vertex array generated when printing text
-	struct GlyphArrayDrawInfo
+	struct TextureSize
 	{
-		GLuint texture;
-		int startvertex;
-		int vertexcount;
-
-		// used when sorting with std::sort
-		// sorts by texture first (binding textures is expensive) and relative position in memory second
-		bool operator < (const GlyphArrayDrawInfo &other) const
-		{
-			if (texture != other.texture)
-				return texture < other.texture;
-			else
-				return startvertex < other.startvertex;
-		}
+		int width;
+		int height;
 	};
 
-	bool initializeTexture(GLenum format);
+	TextureSize getNextTextureSize() const;
 	void createTexture();
-	Glyph *addGlyph(uint32 glyph);
-	Glyph *findGlyph(uint32 glyph);
+	love::font::GlyphData *getRasterizerGlyphData(uint32 glyph);
+	const Glyph &addGlyph(uint32 glyph);
+	const Glyph &findGlyph(uint32 glyph);
+	void printv(const Matrix &t, const std::vector<DrawCommand> &drawcommands, const std::vector<GlyphVertex> &vertices);
 
 	Object::StrongRef<love::font::Rasterizer> rasterizer;
 
 	int height;
 	float lineHeight;
-	float mSpacing; // modifies the spacing by multiplying it with this value
 
-	int textureSizeIndex;
 	int textureWidth;
 	int textureHeight;
 
@@ -199,7 +223,7 @@ private:
 	std::vector<GLuint> textures;
 
 	// maps glyphs to glyph texture information
-	std::map<uint32, Glyph *> glyphs;
+	std::unordered_map<uint32, Glyph> glyphs;
 
 	FontType type;
 	Texture::Filter filter;
@@ -209,17 +233,22 @@ private:
 
 	bool useSpacesAsTab;
 
-	size_t textureMemorySize;
+	// Index buffer used for drawing quads with GL_TRIANGLES.
+	VertexIndex indexBuffer;
 
-	static const int NUM_TEXTURE_SIZES = 7;
-	static const int TEXTURE_WIDTHS[NUM_TEXTURE_SIZES];
-	static const int TEXTURE_HEIGHTS[NUM_TEXTURE_SIZES];
+	// ID which is incremented when the texture cache is invalidated.
+	uint32 textureCacheID;
+
+	size_t textureMemorySize;
 
 	static const int TEXTURE_PADDING = 1;
 
 	// This will be used if the Rasterizer doesn't have a tab character itself.
 	static const int SPACES_PER_TAB = 4;
 
+	static StringMap<AlignMode, ALIGN_MAX_ENUM>::Entry alignModeEntries[];
+	static StringMap<AlignMode, ALIGN_MAX_ENUM> alignModes;
+
 }; // Font
 
 } // opengl

+ 9 - 57
src/modules/graphics/opengl/Graphics.cpp

@@ -677,6 +677,11 @@ Mesh *Graphics::newMesh(int vertexcount, Mesh::DrawMode mode)
 	return new Mesh(vertexcount, mode);
 }
 
+Text *Graphics::newText(Font *font, const std::string &text)
+{
+	return new Text(font, text);
+}
+
 void Graphics::setColor(const Color &c)
 {
 	gl.setColor(c);
@@ -955,68 +960,15 @@ void Graphics::print(const std::string &str, float x, float y , float angle, flo
 	DisplayState &state = states.back();
 
 	if (state.font.get() != nullptr)
-		state.font->print(str, x, y, 0.0, angle, sx, sy, ox, oy, kx, ky);
+		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, AlignMode align, float angle, float sx, float sy, float ox, float oy, float kx, float 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)
 {
 	DisplayState &state = states.back();
 
-	if (state.font.get() == nullptr)
-		return;
-
-	if (wrap < 0.0f)
-		throw love::Exception("Horizontal wrap limit cannot be negative.");
-
-	using std::string;
-	using std::vector;
-
-	// wrappedlines indicates which lines were automatically wrapped. It's
-	// guaranteed to have the same number of elements as lines_to_draw.
-	vector<bool> wrappedlines;
-	vector<string> lines_to_draw = state.font->getWrap(str, wrap, 0, &wrappedlines);
-
-	static Matrix t;
-	t.setTransformation(ceilf(x), ceilf(y), angle, sx, sy, ox, oy, kx, ky);
-
-	OpenGL::TempTransform transform(gl);
-	transform.get() *= t;
-
-	x = y = 0.0f;
-
-	// now for the actual printing
-	vector<string>::const_iterator line_iter, line_end = lines_to_draw.end();
-	float extra_spacing = 0.0f;
-	int num_spaces = 0;
-	int i = 0;
-
-	for (line_iter = lines_to_draw.begin(); line_iter != line_end; ++line_iter)
-	{
-		float width = static_cast<float>(state.font->getWidth(*line_iter));
-		switch (align)
-		{
-		case ALIGN_RIGHT:
-			state.font->print(*line_iter, ceilf(x + (wrap - width)), ceilf(y), 0.0f);
-			break;
-		case ALIGN_CENTER:
-			state.font->print(*line_iter, ceilf(x + (wrap - width) / 2), ceilf(y), 0.0f);
-			break;
-		case ALIGN_JUSTIFY:
-			num_spaces = std::count(line_iter->begin(), line_iter->end(), ' ');
-			if (wrappedlines[i] && num_spaces >= 1)
-				extra_spacing = (wrap - width) / float(num_spaces);
-			else
-				extra_spacing = 0.0f;
-			state.font->print(*line_iter, ceilf(x), ceilf(y), extra_spacing);
-			break;
-		case ALIGN_LEFT:
-		default:
-			state.font->print(*line_iter, ceilf(x), ceilf(y), 0.0f);
-			break;
-		}
-		y += state.font->getHeight() * state.font->getLineHeight();
-		i++;
-	}
+	if (state.font.get() != nullptr)
+		state.font->printf(str, x, y, wrap, align, angle, sx, sy, ox, oy, kx, ky);
 }
 
 /**

+ 4 - 1
src/modules/graphics/opengl/Graphics.h

@@ -45,6 +45,7 @@
 #include "Canvas.h"
 #include "Shader.h"
 #include "Mesh.h"
+#include "Text.h"
 
 namespace love
 {
@@ -158,6 +159,8 @@ public:
 	Mesh *newMesh(const std::vector<Vertex> &vertices, Mesh::DrawMode mode = Mesh::DRAW_MODE_FAN);
 	Mesh *newMesh(int vertexcount, Mesh::DrawMode mode = Mesh::DRAW_MODE_FAN);
 
+	Text *newText(Font *font, const std::string &text = "");
+
 	/**
 	 * Sets the foreground color.
 	 * @param c The new foreground color.
@@ -325,7 +328,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, AlignMode align, float angle, float sx, float sy, float ox, float oy, float kx, float ky);
+	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);
 
 	/**
 	 * Draws a point at (x,y).

+ 280 - 0
src/modules/graphics/opengl/Text.cpp

@@ -0,0 +1,280 @@
+/**
+ * Copyright (c) 2006-2015 LOVE Development Team
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ **/
+
+#include "Text.h"
+#include "common/Matrix.h"
+
+#include <algorithm>
+
+namespace love
+{
+namespace graphics
+{
+namespace opengl
+{
+
+Text::Text(Font *font, const std::string &text)
+	: font(font)
+	, vbo(nullptr)
+	, text_info()
+	, vert_offset(0)
+	, texture_cache_id((uint32) -1)
+{
+	set(text);
+}
+
+Text::~Text()
+{
+	delete vbo;
+}
+
+void Text::uploadVertices(const std::vector<Font::GlyphVertex> &vertices, size_t vertoffset)
+{
+	size_t offset = vertoffset * sizeof(Font::GlyphVertex);
+	size_t datasize = vertices.size() * sizeof(Font::GlyphVertex);
+	uint8 *vbodata = nullptr;
+
+	// If we haven't created a VBO or the vertices are too big, make a new one.
+	if (datasize > 0 && (!vbo || (offset + datasize) > vbo->getSize()))
+	{
+		// Make it bigger than necessary to reduce potential future allocations.
+		size_t newsize = size_t((offset + datasize) * 1.5);
+		if (vbo != nullptr)
+			newsize = std::max(size_t(vbo->getSize() * 1.5), newsize);
+
+		VertexBuffer *new_vbo = VertexBuffer::Create(newsize, GL_ARRAY_BUFFER, GL_DYNAMIC_DRAW);
+
+		if (vbo != nullptr)
+		{
+			try
+			{
+				VertexBuffer::Bind bind(*vbo);
+				vbodata = (uint8 *) vbo->map();
+			}
+			catch (love::Exception &)
+			{
+				delete new_vbo;
+				throw;
+			}
+
+			VertexBuffer::Bind bind(*new_vbo);
+			new_vbo->fill(0, vbo->getSize(), vbodata);
+		}
+
+		delete vbo;
+		vbo = new_vbo;
+	}
+
+	if (vbo != nullptr)
+	{
+		VertexBuffer::Bind bind(*vbo);
+		vbodata = (uint8 *) vbo->map();
+		memcpy(vbodata + offset, &vertices[0], datasize);
+		// We unmap when we draw, to avoid unnecessary full map()/unmap() calls.
+	}
+}
+
+void Text::regenerateVertices()
+{
+	// If the font's texture cache was invalidated then we need to recreate the
+	// text's vertices, since glyph texcoords might have changed.
+	if (font->getTextureCacheID() != texture_cache_id)
+	{
+		std::vector<TextData> textdata = text_data;
+
+		clear();
+
+		for (const TextData &t : textdata)
+			addTextData(t);
+
+		texture_cache_id = font->getTextureCacheID();
+	}
+}
+
+void Text::addTextData(const TextData &t)
+{
+	std::vector<Font::GlyphVertex> vertices;
+	std::vector<Font::DrawCommand> 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);
+	else
+		new_commands = font->generateVerticesFormatted(t.text, t.wrap, t.align, vertices, &text_info);
+
+	if (t.use_matrix)
+		t.matrix.transform(&vertices[0], &vertices[0], (int) vertices.size());
+
+	size_t voffset = vert_offset;
+
+	if (!t.append_vertices)
+	{
+		voffset = 0;
+		draw_commands.clear();
+	}
+
+	uploadVertices(vertices, voffset);
+
+	if (!new_commands.empty())
+	{
+		// The start vertex should be adjusted to account for the vertex offset.
+		for (Font::DrawCommand &cmd : new_commands)
+			cmd.startvertex += (int) voffset;
+
+		// 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.)
+		auto firstcmd = new_commands.begin();
+		auto prevcmd = draw_commands.back();
+		if (!draw_commands.empty() && prevcmd.texture == firstcmd->texture
+			&& (prevcmd.startvertex + prevcmd.vertexcount) == firstcmd->startvertex)
+		{
+			draw_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());
+	}
+
+	vert_offset = voffset + vertices.size();
+	text_data.push_back(t);
+
+	// Font::generateVertices can invalidate the font's texture cache.
+	if (font->getTextureCacheID() != texture_cache_id)
+		regenerateVertices();
+}
+
+void Text::set(const std::string &text)
+{
+	if (text.empty())
+		return set();
+
+	addTextData({text, -1.0f, Font::ALIGN_MAX_ENUM, false, false, Matrix()});
+}
+
+void Text::set(const std::string &text, float wrap, Font::AlignMode align)
+{
+	if (text.empty())
+		return set();
+
+	addTextData({text, wrap, align, false, false, Matrix()});
+}
+
+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)
+{
+	if (text.empty())
+		return;
+
+	Matrix m;
+	m.setTransformation(x, y, angle, sx, sy, ox, oy, kx, ky);
+
+	addTextData({text, -1.0f, Font::ALIGN_MAX_ENUM, true, true, m});
+}
+
+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)
+{
+	if (text.empty())
+		return;
+
+	Matrix m;
+	m.setTransformation(x, y, angle, sx, sy, ox, oy, kx, ky);
+
+	addTextData({text, wrap, align, true, true, m});
+}
+
+void Text::clear()
+{
+	text_data.clear();
+	draw_commands.clear();
+	texture_cache_id = font->getTextureCacheID();
+	text_info = {};
+	vert_offset = 0;
+}
+
+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())
+		return;
+
+	// Re-generate the text if the Font's texture cache was invalidated.
+	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 stride = sizeof(Font::GlyphVertex);
+
+	Matrix t;
+	t.setTransformation(ceilf(x), ceilf(y), angle, sx, sy, ox, oy, kx, ky);
+
+	OpenGL::TempTransform transform(gl);
+	transform.get() *= t;
+
+	{
+		VertexBuffer::Bind bind(*vbo);
+		vbo->unmap(); // Make sure all pending data is flushed to the GPU.
+
+		// 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));
+	}
+
+	glEnableVertexAttribArray(ATTRIB_POS);
+	glEnableVertexAttribArray(ATTRIB_TEXCOORD);
+
+	try
+	{
+		font->drawVertices(draw_commands);
+	}
+	catch (love::Exception &)
+	{
+		glDisableVertexAttribArray(ATTRIB_TEXCOORD);
+		glDisableVertexAttribArray(ATTRIB_POS);
+		throw;
+	}
+
+	glDisableVertexAttribArray(ATTRIB_TEXCOORD);
+	glDisableVertexAttribArray(ATTRIB_POS);
+}
+
+Font *Text::getFont() const
+{
+	return font.get();
+}
+
+int Text::getWidth() const
+{
+	return text_info.width;
+}
+
+int Text::getHeight() const
+{
+	return text_info.height;
+}
+
+} // opengl
+} // graphics
+} // love

+ 102 - 0
src/modules/graphics/opengl/Text.h

@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2006-2015 LOVE Development Team
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ **/
+
+#ifndef LOVE_GRAPHICS_OPENGL_TEXT_H
+#define LOVE_GRAPHICS_OPENGL_TEXT_H
+
+// LOVE
+#include "common/config.h"
+#include "graphics/Drawable.h"
+#include "Font.h"
+#include "VertexBuffer.h"
+
+namespace love
+{
+namespace graphics
+{
+namespace opengl
+{
+
+class Text : public Drawable
+{
+public:
+
+	Text(Font *font, const std::string &text = "");
+	virtual ~Text();
+
+	void set(const std::string &text);
+	void set(const std::string &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 clear();
+
+	// Implements Drawable.
+	virtual void draw(float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky);
+
+	Font *getFont() const;
+
+	/**
+	 * Gets the width of the currently set text.
+	 **/
+	int getWidth() const;
+
+	/**
+	 * Gets the height of the currently set text.
+	 **/
+	int getHeight() const;
+
+private:
+
+	struct TextData
+	{
+		std::string text;
+		float wrap;
+		Font::AlignMode align;
+		bool use_matrix;
+		bool append_vertices;
+		Matrix matrix;
+	};
+
+	void uploadVertices(const std::vector<Font::GlyphVertex> &vertices, size_t vertoffset);
+	void regenerateVertices();
+	void addTextData(const TextData &s);
+
+	StrongRef<Font> font;
+	VertexBuffer *vbo;
+
+	std::vector<Font::DrawCommand> draw_commands;
+
+	std::vector<TextData> text_data;
+	Font::TextInfo text_info;
+
+	size_t vert_offset;
+
+	// Used so we know when the font's texture cache is invalidated.
+	uint32 texture_cache_id;
+
+}; // Text
+
+} // opengl
+} // graphics
+} // love
+
+#endif // LOVE_GRAPHICS_OPENGL_TEXT_H

+ 9 - 1
src/modules/graphics/opengl/wrap_Font.cpp

@@ -55,12 +55,20 @@ int w_Font_getWrap(lua_State *L)
 	const char *str = luaL_checkstring(L, 2);
 	float wrap = (float) luaL_checknumber(L, 3);
 	int max_width = 0, numlines = 0;
+	std::vector<std::string> lines;
+	std::vector<int> widths;
 
 	luax_catchexcept(L, [&]() {
-		std::vector<std::string> lines = t->getWrap(str, wrap, &max_width);
+		t->getWrap(str, wrap, lines, &widths);
 		numlines = lines.size();
 	});
 
+	for (int width : widths)
+	{
+		if (width > max_width)
+			max_width = width;
+	}
+
 	lua_pushinteger(L, max_width);
 	lua_pushinteger(L, numlines);
 	return 2;

+ 21 - 2
src/modules/graphics/opengl/wrap_Graphics.cpp

@@ -566,6 +566,23 @@ int w_newMesh(lua_State *L)
 	return 1;
 }
 
+int w_newText(lua_State *L)
+{
+	Font *font = luax_checkfont(L, 1);
+	Text *t = nullptr;
+
+	if (lua_isnoneornil(L, 2))
+		luax_catchexcept(L, [&](){ t = instance()->newText(font); });
+	else
+	{
+		std::string text = luax_checkstring(L, 2);
+		luax_catchexcept(L, [&](){ t = instance()->newText(font, text); });
+	}
+
+	luax_pushtype(L, "Text", GRAPHICS_TEXT_T, t);
+	return 1;
+}
+
 int w_setColor(lua_State *L)
 {
 	Color c;
@@ -1186,14 +1203,14 @@ int w_printf(lua_State *L)
 	float ox = 0.0f, oy = 0.0f;
 	float kx = 0.0f, ky = 0.0f;
 
-	Graphics::AlignMode align = Graphics::ALIGN_LEFT;
+	Font::AlignMode align = Font::ALIGN_LEFT;
 
 	if (lua_gettop(L) >= 5)
 	{
 		if (!lua_isnil(L, 5))
 		{
 			const char *str = luaL_checkstring(L, 5);
-			if (!Graphics::getConstant(str, align))
+			if (!Font::getConstant(str, align))
 				return luaL_error(L, "Incorrect alignment: %s", str);
 		}
 
@@ -1433,6 +1450,7 @@ static const luaL_Reg functions[] =
 	{ "newCanvas", w_newCanvas },
 	{ "newShader", w_newShader },
 	{ "newMesh", w_newMesh },
+	{ "newText", w_newText },
 
 	{ "setColor", w_setColor },
 	{ "getColor", w_getColor },
@@ -1522,6 +1540,7 @@ static const lua_CFunction types[] =
 	luaopen_canvas,
 	luaopen_shader,
 	luaopen_mesh,
+	luaopen_text,
 	0
 };
 

+ 2 - 0
src/modules/graphics/opengl/wrap_Graphics.h

@@ -30,6 +30,7 @@
 #include "wrap_Canvas.h"
 #include "wrap_Shader.h"
 #include "wrap_Mesh.h"
+#include "wrap_Text.h"
 #include "Graphics.h"
 
 namespace love
@@ -60,6 +61,7 @@ int w_newParticleSystem(lua_State *L);
 int w_newCanvas(lua_State *L);  // comments in function
 int w_newShader(lua_State *L);
 int w_newMesh(lua_State *L);
+int w_newText(lua_State *L);
 int w_setColor(lua_State *L);
 int w_getColor(lua_State *L);
 int w_setBackgroundColor(lua_State *L);

+ 180 - 0
src/modules/graphics/opengl/wrap_Text.cpp

@@ -0,0 +1,180 @@
+/**
+ * Copyright (c) 2006-2015 LOVE Development Team
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ **/
+
+#include "wrap_Text.h"
+
+namespace love
+{
+namespace graphics
+{
+namespace opengl
+{
+
+Text *luax_checktext(lua_State *L, int idx)
+{
+	return luax_checktype<Text>(L, idx, "Text", GRAPHICS_TEXT_T);
+}
+
+int w_Text_set(lua_State *L)
+{
+	Text *t = luax_checktext(L, 1);
+
+	if (lua_isnoneornil(L, 2))
+	{
+		// No argument: clear all current text.
+		luax_catchexcept(L, [&](){ t->set(); });
+	}
+	else if (lua_isnoneornil(L, 3))
+	{
+		// Single argument: unformatted text.
+		std::string newtext = luax_checkstring(L, 2);
+		luax_catchexcept(L, [&](){ t->set(newtext); });
+	}
+	else
+	{
+		// Multiple arguments: formatted text.
+		float wraplimit = (float) luaL_checknumber(L, 3);
+
+		Font::AlignMode align;
+		const char *alignstr = luaL_checkstring(L, 4);
+		if (!Font::getConstant(alignstr, align))
+			return luaL_error(L, "Invalid align mode: %s", alignstr);
+
+		std::string newtext = luax_checkstring(L, 2);
+
+		luax_catchexcept(L, [&](){ t->set(newtext, wraplimit, align); });
+	}
+
+	return 0;
+}
+
+int w_Text_setf(lua_State *L)
+{
+	Text *t = luax_checktext(L, 1);
+
+	float wraplimit = (float) luaL_checknumber(L, 3);
+
+	Font::AlignMode align;
+	const char *alignstr = luaL_checkstring(L, 4);
+	if (!Font::getConstant(alignstr, align))
+		return luaL_error(L, "Invalid align mode: %s", alignstr);
+
+	std::string newtext = luax_checkstring(L, 2);
+
+	luax_catchexcept(L, [&](){ t->set(newtext, wraplimit, align); });
+
+	return 0;
+}
+
+int w_Text_add(lua_State *L)
+{
+	Text *t = luax_checktext(L, 1);
+	std::string text = luax_checkstring(L, 2);
+
+	float x  = (float) luaL_optnumber(L, 3, 0.0);
+	float y  = (float) luaL_optnumber(L, 4, 0.0);
+	float a  = (float) luaL_optnumber(L, 5, 0.0);
+	float sx = (float) luaL_optnumber(L, 6, 1.0);
+	float sy = (float) luaL_optnumber(L, 7, sx);
+	float ox = (float) luaL_optnumber(L, 8, 0.0);
+	float oy = (float) luaL_optnumber(L, 9, 0.0);
+	float kx = (float) luaL_optnumber(L, 10, 0.0);
+	float ky = (float) luaL_optnumber(L, 11, 0.0);
+
+	luax_catchexcept(L, [&](){ t->add(text, x, y, a, sx, sy, ox, oy, kx, ky); });
+	return 0;
+}
+
+int w_Text_addf(lua_State *L)
+{
+	Text *t = luax_checktext(L, 1);
+	std::string text = luax_checkstring(L, 2);
+	float wrap = (float) luaL_checknumber(L, 3);
+
+	Font::AlignMode align = Font::ALIGN_MAX_ENUM;
+	const char *alignstr = luaL_checkstring(L, 4);
+
+	if (!Font::getConstant(alignstr, align))
+		return luaL_error(L, "Invalid align mode: %s", alignstr);
+
+	float x  = (float) luaL_optnumber(L, 5, 0.0);
+	float y  = (float) luaL_optnumber(L, 6, 0.0);
+	float a  = (float) luaL_optnumber(L, 7, 0.0);
+	float sx = (float) luaL_optnumber(L, 8, 1.0);
+	float sy = (float) luaL_optnumber(L, 9, sx);
+	float ox = (float) luaL_optnumber(L, 10, 0.0);
+	float oy = (float) luaL_optnumber(L, 11, 0.0);
+	float kx = (float) luaL_optnumber(L, 12, 0.0);
+	float ky = (float) luaL_optnumber(L, 13, 0.0);
+
+	luax_catchexcept(L, [&](){ t->addf(text, wrap, align, x, y, a, sx, sy, ox, oy, kx, ky); });
+	return 0;
+}
+
+int w_Text_clear(lua_State *L)
+{
+	Text *t = luax_checktext(L, 1);
+	luax_catchexcept(L, [&](){ t->clear(); });
+	return 0;
+}
+
+int w_Text_getFont(lua_State *L)
+{
+	Text *t = luax_checktext(L, 1);
+	Font *f = t->getFont();
+	luax_pushtype(L, "Font", GRAPHICS_FONT_T, f);
+	return 1;
+}
+
+int w_Text_getWidth(lua_State *L)
+{
+	Text *t = luax_checktext(L, 1);
+	lua_pushnumber(L, t->getWidth());
+	return 1;
+}
+
+int w_Text_getHeight(lua_State *L)
+{
+	Text *t = luax_checktext(L, 1);
+	lua_pushnumber(L, t->getHeight());
+	return 1;
+}
+
+static const luaL_Reg functions[] =
+{
+	{ "set", w_Text_set },
+	{ "setf", w_Text_setf },
+	{ "add", w_Text_add },
+	{ "addf", w_Text_addf },
+	{ "clear", w_Text_clear },
+	{ "getFont", w_Text_getFont },
+	{ "getWidth", w_Text_getWidth },
+	{ "getHeight", w_Text_getHeight },
+	{ 0, 0 }
+};
+
+extern "C" int luaopen_text(lua_State *L)
+{
+	return luax_register_type(L, "Text", functions);
+}
+
+} // opengl
+} // graphics
+} // love

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

@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2006-2015 LOVE Development Team
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ **/
+
+#ifndef LOVE_GRAPHICS_OPENGL_WRAP_TEXT_H
+#define LOVE_GRAPHICS_OPENGL_WRAP_TEXT_H
+
+#include "Text.h"
+#include "common/runtime.h"
+
+namespace love
+{
+namespace graphics
+{
+namespace opengl
+{
+
+Text *luax_checktext(lua_State *L, int idx);
+int w_Text_set(lua_State *L);
+int w_Text_setf(lua_State *L);
+int w_Text_add(lua_State *L);
+int w_Text_addf(lua_State *L);
+int w_Text_clear(lua_State *L);
+int w_Text_getFont(lua_State *L);
+int w_Text_getWidth(lua_State *L);
+int w_Text_getHeight(lua_State *L);
+extern "C" int luaopen_text(lua_State *L);
+
+} // opengl
+} // graphics
+} // love
+
+#endif // LOVE_GRAPHICS_OPENGL_WRAP_TEXT_H
+