diff --git a/CMakeLists.txt b/CMakeLists.txt index 3623f3a68..8533da5cb 100644 --- a/CMakeLists.txt +++ b/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 ) diff --git a/platform/macosx/love-framework.xcodeproj/project.pbxproj b/platform/macosx/love-framework.xcodeproj/project.pbxproj index 142651be0..b0ceb2b58 100644 --- a/platform/macosx/love-framework.xcodeproj/project.pbxproj +++ b/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 = ""; }; FAC86E671724555D00EED715 /* Quad.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Quad.cpp; sourceTree = ""; }; FAC86E681724555D00EED715 /* Quad.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Quad.h; sourceTree = ""; }; + FAD400A5196C5EBC004208B9 /* Text.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Text.cpp; sourceTree = ""; }; + FAD400A7196C5ECC004208B9 /* Text.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Text.h; sourceTree = ""; }; + FAD5970B196E174000E8EA36 /* wrap_Text.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = wrap_Text.cpp; sourceTree = ""; }; + FAD5970D196E17C100E8EA36 /* wrap_Text.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = wrap_Text.h; sourceTree = ""; }; FADD58DC18C30367005FC3BF /* FormatHandler.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FormatHandler.cpp; sourceTree = ""; }; FADE620719368D5C00C25B97 /* STBHandler.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = STBHandler.cpp; sourceTree = ""; }; FADE620819368D5C00C25B97 /* STBHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STBHandler.h; sourceTree = ""; }; @@ -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; }; diff --git a/src/common/Matrix.cpp b/src/common/Matrix.cpp index d2307ee74..67240348f 100644 --- a/src/common/Matrix.cpp +++ b/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 + 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 +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 diff --git a/src/common/Vector.h b/src/common/Vector.h index 3aeb9fdba..76520e8d0 100644 --- a/src/common/Vector.h +++ b/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 diff --git a/src/common/runtime.cpp b/src/common/runtime.cpp index 1922bf087..809c969a4 100644 --- a/src/common/runtime.cpp +++ b/src/common/runtime.cpp @@ -652,6 +652,7 @@ StringMap::Entry typeEntries[] = {"Canvas", GRAPHICS_CANVAS_ID}, {"Shader", GRAPHICS_SHADER_ID}, {"Mesh", GRAPHICS_MESH_ID}, + {"Text", GRAPHICS_TEXT_ID}, // Image {"ImageData", IMAGE_IMAGE_DATA_ID}, diff --git a/src/common/types.h b/src/common/types.h index 4b96fd62e..2bf749e05 100644 --- a/src/common/types.h +++ b/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; diff --git a/src/modules/graphics/Graphics.cpp b/src/modules/graphics/Graphics.cpp index 185cab66e..d8e3bacd0 100644 --- a/src/modules/graphics/Graphics.cpp +++ b/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::Entry Graphics::drawMode StringMap Graphics::drawModes(Graphics::drawModeEntries, sizeof(Graphics::drawModeEntries)); -StringMap::Entry Graphics::alignModeEntries[] = -{ - { "left", Graphics::ALIGN_LEFT }, - { "right", Graphics::ALIGN_RIGHT }, - { "center", Graphics::ALIGN_CENTER }, - { "justify", Graphics::ALIGN_JUSTIFY }, -}; - -StringMap Graphics::alignModes(Graphics::alignModeEntries, sizeof(Graphics::alignModeEntries)); - StringMap::Entry Graphics::blendModeEntries[] = { { "alpha", Graphics::BLEND_ALPHA }, diff --git a/src/modules/graphics/Graphics.h b/src/modules/graphics/Graphics.h index 795cb88b3..4ed788d73 100644 --- a/src/modules/graphics/Graphics.h +++ b/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::Entry drawModeEntries[]; static StringMap drawModes; - static StringMap::Entry alignModeEntries[]; - static StringMap alignModes; - static StringMap::Entry blendModeEntries[]; static StringMap blendModes; diff --git a/src/modules/graphics/opengl/Font.cpp b/src/modules/graphics/opengl/Font.cpp index ec0e8e684..0bee40958 100644 --- a/src/modules/graphics/opengl/Font.cpp +++ b/src/modules/graphics/opengl/Font.cpp @@ -29,6 +29,7 @@ #include #include #include // for max +#include 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; - } + + TextureSize nextsize = getNextTextureSize(); + + if (nextsize.width <= textureWidth && nextsize.height <= textureHeight) + break; + + textureWidth = nextsize.width; + textureHeight = nextsize.height; } - textureWidth = TEXTURE_WIDTHS[textureSizeIndex]; - textureHeight = TEXTURE_HEIGHTS[textureSizeIndex]; + love::font::GlyphData *gd = r->getGlyphData(32); // Space character. + type = (gd->getFormat() == font::GlyphData::FORMAT_LUMINANCE_ALPHA) ? FONT_TRUETYPE : FONT_IMAGE; + gd->release(); - love::font::GlyphData *gd = nullptr; + if (!r->hasGlyph(9)) // No tab character in the Rasterizer. + useSpacesAsTab = true; - try - { - gd = r->getGlyphData(32); // Space character. - type = (gd->getFormat() == love::font::GlyphData::FORMAT_LUMINANCE_ALPHA) ? FONT_TRUETYPE : FONT_IMAGE; - - if (!r->hasGlyph(9)) // No tab character in the Rasterizer. - useSpacesAsTab = true; - - loadVolatile(); - } - catch (love::Exception &) - { - delete gd; - throw; - } - - 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()); - glTexImage2D(GL_TEXTURE_2D, - 0, - internalformat, - (GLsizei)textureWidth, - (GLsizei)textureHeight, - 0, - format, - GL_UNSIGNED_BYTE, - NULL); + 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; + } - 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; - GLuint t; - glGenTextures(1, &t); - textures.push_back(t); + size_t prevmemsize = textureMemorySize; + if (prevmemsize > 0) + { + textureMemorySize -= (textureWidth * textureHeight * bpp); + gl.updateTextureMemorySize(prevmemsize, textureMemorySize); + } + + 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) + // 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; + + // Initialize the texture with transparent black. + std::vector emptydata(size.width * size.height * bpp, 0); + + // Clear errors before initializing. + while (glGetError() != GL_NO_ERROR); + + glTexImage2D(GL_TEXTURE_2D, 0, internalformat, size.width, size.height, 0, + format, GL_UNSIGNED_BYTE, &emptydata[0]); + + if (glGetError() != GL_NO_ERROR) { - textureWidth = TEXTURE_WIDTHS[textureSizeIndex]; - textureHeight = TEXTURE_HEIGHTS[textureSizeIndex]; - - initialized = initializeTexture(format); - - if (initialized || textureSizeIndex <= 0) - break; - - --textureSizeIndex; - } - - if (!initialized) - { - // 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 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]); + textureWidth = size.width; + textureHeight = size.height; - setFilter(filter); + rowHeight = textureX = textureY = TEXTURE_PADDING; - size_t prevmemsize = textureMemorySize; - - 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 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(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::generateVertices(const std::string &text, std::vector &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 glyphinfolist; + std::vector drawcommands; // Pre-allocate space for the maximum possible number of vertices. - std::vector 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(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,47 +398,204 @@ 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) + // Sort draw commands by texture first, and quad position in memory second + // (using the struct's < operator). + std::sort(drawcommands.begin(), drawcommands.end()); + + if (dx > maxwidth) + maxwidth = (int) dx; + + 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::generateVerticesFormatted(const std::string &text, float wrap, AlignMode align, std::vector &vertices, TextInfo *info) +{ + if (wrap < 0.0f) + wrap = std::numeric_limits::max(); + + uint32 cacheid = textureCacheID; + + std::vector 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 wrappedlines; + std::vector widths; + std::vector 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 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 &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 &drawcommands, const std::vector &vertices) +{ + if (vertices.empty() || drawcommands.empty()) return; - // 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()); - - Matrix t; - t.setTransformation(ceilf(x), ceilf(y), angle, sx, sy, ox, oy, kx, ky); - OpenGL::TempTransform transform(gl); transform.get() *= t; 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); + 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); - gl.prepareDraw(); - - // 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::const_iterator it; - for (it = glyphinfolist.begin(); it != glyphinfolist.end(); ++it) + try { - gl.bindTexture(it->texture); - gl.drawArrays(GL_QUADS, it->startvertex, it->vertexcount); + drawVertices(drawcommands); + } + catch (love::Exception &) + { + 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 vertices; + std::vector 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 vertices; + std::vector 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(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 Font::getWrap(const std::string &text, float wrap, int *max_width, std::vector *wrappedlines) +void Font::getWrap(const std::string &text, float wrap, std::vector &lines, std::vector *linewidths, std::vector *wrappedlines) { using namespace std; - const float width_space = static_cast(getWidth(' ')); - vector 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 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(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 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::iterator it = glyphs.begin(); - Glyph *g; - while (it != glyphs.end()) - { - g = it->second; - delete g; - glyphs.erase(it++); - } - std::vector::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::Entry Font::alignModeEntries[] = +{ + { "left", Font::ALIGN_LEFT }, + { "right", Font::ALIGN_RIGHT }, + { "center", Font::ALIGN_CENTER }, + { "justify", Font::ALIGN_JUSTIFY }, +}; + +StringMap Font::alignModes(Font::alignModeEntries, sizeof(Font::alignModeEntries)); + } // opengl } // graphics } // love diff --git a/src/modules/graphics/opengl/Font.h b/src/modules/graphics/opengl/Font.h index 0529072a2..a43d12a87 100644 --- a/src/modules/graphics/opengl/Font.h +++ b/src/modules/graphics/opengl/Font.h @@ -22,15 +22,20 @@ #define LOVE_GRAPHICS_OPENGL_FONT_H // STD -#include +#include #include #include // 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 generateVertices(const std::string &text, std::vector &vertices, float extra_spacing = 0.0f, Vector offset = Vector(), TextInfo *info = nullptr); + std::vector generateVerticesFormatted(const std::string &text, float wrap, AlignMode align, std::vector &vertices, TextInfo *info = nullptr); + + void drawVertices(const std::vector &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 getWrap(const std::string &text, float wrap, int *max_width = 0, std::vector *wrapped_lines = 0); + void getWrap(const std::string &text, float wrap, std::vector &lines, std::vector *line_widths = 0, std::vector *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 &drawcommands, const std::vector &vertices); Object::StrongRef 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 textures; // maps glyphs to glyph texture information - std::map glyphs; + std::unordered_map 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::Entry alignModeEntries[]; + static StringMap alignModes; + }; // Font } // opengl diff --git a/src/modules/graphics/opengl/Graphics.cpp b/src/modules/graphics/opengl/Graphics.cpp index 46ecdd814..2d91859f7 100644 --- a/src/modules/graphics/opengl/Graphics.cpp +++ b/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 wrappedlines; - vector 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::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(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); } /** diff --git a/src/modules/graphics/opengl/Graphics.h b/src/modules/graphics/opengl/Graphics.h index 2dac246cc..76f0ce06d 100644 --- a/src/modules/graphics/opengl/Graphics.h +++ b/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 &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). diff --git a/src/modules/graphics/opengl/Text.cpp b/src/modules/graphics/opengl/Text.cpp new file mode 100644 index 000000000..4085b6e3f --- /dev/null +++ b/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 + +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 &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 = text_data; + + clear(); + + for (const TextData &t : textdata) + addTextData(t); + + texture_cache_id = font->getTextureCacheID(); + } +} + +void Text::addTextData(const TextData &t) +{ + std::vector vertices; + std::vector 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 diff --git a/src/modules/graphics/opengl/Text.h b/src/modules/graphics/opengl/Text.h new file mode 100644 index 000000000..bfb2bbb6f --- /dev/null +++ b/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 &vertices, size_t vertoffset); + void regenerateVertices(); + void addTextData(const TextData &s); + + StrongRef font; + VertexBuffer *vbo; + + std::vector draw_commands; + + std::vector 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 diff --git a/src/modules/graphics/opengl/wrap_Font.cpp b/src/modules/graphics/opengl/wrap_Font.cpp index 34203f070..9fba381e9 100644 --- a/src/modules/graphics/opengl/wrap_Font.cpp +++ b/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 lines; + std::vector widths; luax_catchexcept(L, [&]() { - std::vector 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; diff --git a/src/modules/graphics/opengl/wrap_Graphics.cpp b/src/modules/graphics/opengl/wrap_Graphics.cpp index 0590111f3..730451079 100644 --- a/src/modules/graphics/opengl/wrap_Graphics.cpp +++ b/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 }; diff --git a/src/modules/graphics/opengl/wrap_Graphics.h b/src/modules/graphics/opengl/wrap_Graphics.h index 4ac1bf7c4..ad224cf23 100644 --- a/src/modules/graphics/opengl/wrap_Graphics.h +++ b/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); diff --git a/src/modules/graphics/opengl/wrap_Text.cpp b/src/modules/graphics/opengl/wrap_Text.cpp new file mode 100644 index 000000000..020d48970 --- /dev/null +++ b/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(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 diff --git a/src/modules/graphics/opengl/wrap_Text.h b/src/modules/graphics/opengl/wrap_Text.h new file mode 100644 index 000000000..b156b71a6 --- /dev/null +++ b/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 +