Browse Source

Initial (WIP) Harfbuzz text shaping integration.

This a significant refactor / rewrite of much of the love.graphics Font code.
Text positioning is now split into TextShaper classes in the love.font module. This isn't exposed to users yet.
BMFonts and ImageFonts use the same text shaping as before (in the new TextShaper API). Fonts loaded via FreeType now use Harfbuzz for text shaping.

The new code isn't at feature-parity with the old code yet. Harfbuzz-based text shaping enables kerning support in more fonts, character combining into ligature glyphs, and directions other than left-to-right (this isn't supported in love yet).
Sasha Szpakowski 2 years ago
parent
commit
7a0f6621e7

+ 9 - 0
CMakeLists.txt

@@ -99,6 +99,7 @@ if(MEGA)
 
 	set(LOVE_LINK_LIBRARIES
 		${MEGA_FREETYPE}
+		${MEGA_HARFBUZZ}
 		${MEGA_LIBOGG}
 		${MEGA_LIBVORBISFILE}
 		${MEGA_LIBVORBIS}
@@ -169,6 +170,7 @@ Please see https://github.com/love2d/megasource
 	endif()
 
 	find_package(Freetype REQUIRED)
+	find_package(harfbuzz REQUIRED)
 	find_package(ModPlug REQUIRED)
 	find_package(OpenAL REQUIRED)
 	find_package(OpenGL REQUIRED)
@@ -196,6 +198,7 @@ Please see https://github.com/love2d/megasource
 		${OPENGL_gl_LIBRARY}
 		${SDL2_LIBRARY}
 		${FREETYPE_LIBRARY}
+		${HARFBUZZ_LIBRARY}
 		${OPENAL_LIBRARY}
 		${MODPLUG_LIBRARY}
 		${THEORA_LIBRARY}
@@ -473,12 +476,16 @@ set(LOVE_SRC_MODULE_FONT_ROOT
 	src/modules/font/BMFontRasterizer.h
 	src/modules/font/Font.cpp
 	src/modules/font/Font.h
+	src/modules/font/GenericShaper.cpp
+	src/modules/font/GenericShaper.h
 	src/modules/font/GlyphData.cpp
 	src/modules/font/GlyphData.h
 	src/modules/font/ImageRasterizer.cpp
 	src/modules/font/ImageRasterizer.h
 	src/modules/font/Rasterizer.cpp
 	src/modules/font/Rasterizer.h
+	src/modules/font/TextShaper.cpp
+	src/modules/font/TextShaper.h
 	src/modules/font/TrueTypeRasterizer.cpp
 	src/modules/font/TrueTypeRasterizer.h
 	src/modules/font/wrap_Font.cpp
@@ -492,6 +499,8 @@ set(LOVE_SRC_MODULE_FONT_ROOT
 set(LOVE_SRC_MODULE_FONT_FREETYPE
 	src/modules/font/freetype/Font.cpp
 	src/modules/font/freetype/Font.h
+	src/modules/font/freetype/HarfbuzzShaper.cpp
+	src/modules/font/freetype/HarfbuzzShaper.h
 	src/modules/font/freetype/TrueTypeRasterizer.cpp
 	src/modules/font/freetype/TrueTypeRasterizer.h
 )

+ 49 - 15
src/modules/font/BMFontRasterizer.cpp

@@ -20,6 +20,7 @@
 
 // LOVE
 #include "BMFontRasterizer.h"
+#include "GenericShaper.h"
 #include "filesystem/Filesystem.h"
 #include "image/Image.h"
 
@@ -164,6 +165,14 @@ BMFontRasterizer::~BMFontRasterizer()
 
 void BMFontRasterizer::parseConfig(const std::string &configtext)
 {
+	{
+		BMFontCharacter nullchar = {};
+		nullchar.page = -1;
+		nullchar.glyph = 0;
+		characters.push_back(nullchar);
+		characterIndices[0] = (int)characters.size() - 1;
+	}
+
 	std::stringstream ss(configtext);
 	std::string line;
 
@@ -237,7 +246,10 @@ void BMFontRasterizer::parseConfig(const std::string &configtext)
 			c.metrics.bearingY = -cline.getAttributeInt("yoffset");
 			c.metrics.advance  =  cline.getAttributeInt("xadvance");
 
-			characters[id] = c;
+			c.glyph = id;
+
+			characters.push_back(c);
+			characterIndices[id] = (int) characters.size() - 1;
 		}
 		else if (tag == "kerning")
 		{
@@ -257,13 +269,15 @@ void BMFontRasterizer::parseConfig(const std::string &configtext)
 	bool guessheight = lineHeight == 0;
 
 	// Verify the glyph character attributes.
-	for (const auto &cpair : characters)
+	for (const auto &c : characters)
 	{
-		const BMFontCharacter &c = cpair.second;
+		if (c.glyph == 0)
+			continue;
+
 		int width = c.metrics.width;
 		int height = c.metrics.height;
 
-		if (!unicode && cpair.first > 127)
+		if (!unicode && c.glyph > 127)
 			throw love::Exception("Invalid BMFont character id (only unicode and ASCII are supported)");
 
 		if (c.page < 0 || images[c.page].get() == nullptr)
@@ -272,13 +286,13 @@ void BMFontRasterizer::parseConfig(const std::string &configtext)
 		const image::ImageData *id = images[c.page].get();
 
 		if (!id->inside(c.x, c.y))
-			throw love::Exception("Invalid coordinates for BMFont character %u.", cpair.first);
+			throw love::Exception("Invalid coordinates for BMFont character %u.", c.glyph);
 
 		if (width > 0 && !id->inside(c.x + width - 1, c.y))
-			throw love::Exception("Invalid width %d for BMFont character %u.", width, cpair.first);
+			throw love::Exception("Invalid width %d for BMFont character %u.", width, c.glyph);
 
 		if (height > 0 && !id->inside(c.x, c.y + height - 1))
-			throw love::Exception("Invalid height %d for BMFont character %u.", height, cpair.first);
+			throw love::Exception("Invalid height %d for BMFont character %u.", height, c.glyph);
 
 		if (guessheight)
 			lineHeight = std::max(lineHeight, c.metrics.height);
@@ -292,22 +306,37 @@ int BMFontRasterizer::getLineHeight() const
 	return lineHeight;
 }
 
-GlyphData *BMFontRasterizer::getGlyphData(uint32 glyph) const
+int BMFontRasterizer::getGlyphSpacing(uint32 glyph) const
 {
-	auto it = characters.find(glyph);
+	auto it = characterIndices.find(glyph);
+	if (it == characterIndices.end())
+		return 0;
+
+	return characters[it->second].metrics.advance;
+}
 
+int BMFontRasterizer::getGlyphIndex(uint32 glyph) const
+{
+	auto it = characterIndices.find(glyph);
+	if (it == characterIndices.end())
+		return 0;
+	return it->second;
+}
+
+GlyphData *BMFontRasterizer::getGlyphDataForIndex(int index) const
+{
 	// Return an empty GlyphData if we don't have the glyph character.
-	if (it == characters.end())
-		return new GlyphData(glyph, GlyphMetrics(), PIXELFORMAT_RGBA8_UNORM);
+	if (index < 0 || index >= (int) characters.size())
+		return new GlyphData(0, GlyphMetrics(), PIXELFORMAT_RGBA8_UNORM);
 
-	const BMFontCharacter &c = it->second;
+	const BMFontCharacter& c = characters[index];
 	const auto &imagepair = images.find(c.page);
 
 	if (imagepair == images.end())
-		return new GlyphData(glyph, GlyphMetrics(), PIXELFORMAT_RGBA8_UNORM);
+		return new GlyphData(c.glyph, GlyphMetrics(), PIXELFORMAT_RGBA8_UNORM);
 
 	image::ImageData *imagedata = imagepair->second.get();
-	GlyphData *g = new GlyphData(glyph, c.metrics, PIXELFORMAT_RGBA8_UNORM);
+	GlyphData *g = new GlyphData(c.glyph, c.metrics, PIXELFORMAT_RGBA8_UNORM);
 
 	size_t pixelsize = imagedata->getPixelSize();
 
@@ -333,7 +362,7 @@ int BMFontRasterizer::getGlyphCount() const
 
 bool BMFontRasterizer::hasGlyph(uint32 glyph) const
 {
-	return characters.find(glyph) != characters.end();
+	return characterIndices.find(glyph) != characterIndices.end();
 }
 
 float BMFontRasterizer::getKerning(uint32 leftglyph, uint32 rightglyph) const
@@ -352,6 +381,11 @@ Rasterizer::DataType BMFontRasterizer::getDataType() const
 	return DATA_IMAGE;
 }
 
+TextShaper *BMFontRasterizer::newTextShaper()
+{
+	return new GenericShaper(this);
+}
+
 bool BMFontRasterizer::accepts(love::filesystem::FileData *fontdef)
 {
 	const char *data = (const char *) fontdef->getData();

+ 9 - 3
src/modules/font/BMFontRasterizer.h

@@ -47,11 +47,14 @@ public:
 
 	// Implements Rasterizer.
 	int getLineHeight() const override;
-	GlyphData *getGlyphData(uint32 glyph) const override;
+	int getGlyphSpacing(uint32 glyph) const override;
+	int getGlyphIndex(uint32 glyph) const override;
+	GlyphData *getGlyphDataForIndex(int index) const override;
 	int getGlyphCount() const override;
 	bool hasGlyph(uint32 glyph) const override;
 	float getKerning(uint32 leftglyph, uint32 rightglyph) const override;
 	DataType getDataType() const override;
+	TextShaper *newTextShaper() override;
 
 	static bool accepts(love::filesystem::FileData *fontdef);
 
@@ -63,6 +66,7 @@ private:
 		int y;
 		int page;
 		GlyphMetrics metrics;
+		uint32 glyph;
 	};
 
 	void parseConfig(const std::string &config);
@@ -72,8 +76,10 @@ private:
 	// Image pages, indexed by their page id.
 	std::unordered_map<int, StrongRef<image::ImageData>> images;
 
-	// Glyph characters, indexed by their glyph id.
-	std::unordered_map<uint32, BMFontCharacter> characters;
+	std::vector<BMFontCharacter> characters;
+
+	// Glyph character indices, indexed by their glyph id.
+	std::unordered_map<uint32, int> characterIndices;
 
 	// Kerning information, indexed by two (packed) characters.
 	std::unordered_map<uint64, int> kerning;

+ 203 - 0
src/modules/font/GenericShaper.cpp

@@ -0,0 +1,203 @@
+/**
+ * Copyright (c) 2006-2022 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.
+ **/
+
+// LOVE
+#include "GenericShaper.h"
+#include "Rasterizer.h"
+#include "common/Optional.h"
+
+namespace love
+{
+namespace font
+{
+
+GenericShaper::GenericShaper(Rasterizer *rasterizer)
+	: TextShaper(rasterizer)
+{
+}
+
+GenericShaper::~GenericShaper()
+{
+}
+
+void GenericShaper::computeGlyphPositions(const ColoredCodepoints &codepoints, Range range, Vector2 offset, float extraspacing, std::vector<GlyphPosition> *positions, std::vector<IndexedColor> *colors, TextInfo *info)
+{
+	if (!range.isValid())
+		range = Range(0, codepoints.cps.size());
+
+	if (rasterizers[0]->getDataType() == Rasterizer::DATA_TRUETYPE)
+		offset.y += getBaseline();
+
+	// Spacing counter and newline handling.
+	Vector2 curpos = offset;
+
+	int maxwidth = 0;
+	uint32 prevglyph = 0;
+
+	if (positions)
+		positions->reserve(range.getSize());
+
+	int colorindex = 0;
+	int ncolors = (int) codepoints.colors.size();
+	Optional<Colorf> colorToAdd;
+
+	// Make sure the right color is applied to the start of the glyph list,
+	// when the start isn't 0.
+	if (colors && range.getOffset() > 0 && !codepoints.colors.empty())
+	{
+		for (; colorindex < ncolors; colorindex++)
+		{
+			if (codepoints.colors[colorindex].index >= (int) range.getOffset())
+				break;
+			colorToAdd.set(codepoints.colors[colorindex].color);
+		}
+	}
+
+	for (int i = (int) range.getMin(); i <= (int) range.getMax(); i++)
+	{
+		uint32 g = codepoints.cps[i];
+
+		// Do this before anything else so we don't miss colors corresponding
+		// to newlines. The actual add to the list happens after newline
+		// handling, to make sure the resulting index is valid in the positions
+		// array.
+		if (colors && colorindex < ncolors && codepoints.colors[colorindex].index == i)
+		{
+			colorToAdd.set(codepoints.colors[colorindex].color);
+			colorindex++;
+		}
+
+		if (g == '\n')
+		{
+			if (curpos.x > maxwidth)
+				maxwidth = (int)curpos.x;
+
+			// Wrap newline, but do not output a position for it.
+			curpos.y += floorf(getHeight() * getLineHeight() + 0.5f);
+			curpos.x = offset.x;
+			prevglyph = 0;
+			continue;
+		}
+
+		// Ignore carriage returns
+		if (g == '\r')
+		{
+			prevglyph = g;
+			continue;
+		}
+
+		if (colorToAdd.hasValue && colors && positions)
+		{
+			IndexedColor c = {colorToAdd.value, positions->size()};
+			colors->push_back(c);
+			colorToAdd.clear();
+		}
+
+		// Add kerning to the current horizontal offset.
+		curpos.x += getKerning(prevglyph, g);
+
+		GlyphIndex glyphindex;
+		int advance = getGlyphAdvance(g, &glyphindex);
+
+		if (positions)
+			positions->push_back({ Vector2(curpos.x, curpos.y), glyphindex });
+
+		// Advance the x position for the next glyph.
+		curpos.x += advance;
+
+		// Account for extra spacing given to space characters.
+		if (g == ' ' && extraspacing != 0.0f)
+			curpos.x = floorf(curpos.x + extraspacing);
+
+		prevglyph = g;
+	}
+
+	if (curpos.x > maxwidth)
+		maxwidth = (int)curpos.x;
+
+	if (info != nullptr)
+	{
+		info->width = maxwidth - offset.x;
+		info->height = curpos.y - offset.y;
+		if (curpos.x > offset.x)
+			info->height += floorf(getHeight() * getLineHeight() + 0.5f);
+	}
+}
+
+int GenericShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Range range, float wraplimit, float *width)
+{
+	if (!range.isValid())
+		range = Range(0, codepoints.cps.size());
+
+	uint32 prevglyph = 0;
+
+	float w = 0.0f;
+	float outwidth = 0.0f;
+	float widthbeforelastspace = 0.0f;
+	int wrapindex = -1;
+	int lastspaceindex = -1;
+
+	for (int i = (int)range.getMin(); i <= (int)range.getMax(); i++)
+	{
+		uint32 g = codepoints.cps[i];
+
+		if (g == '\r')
+		{
+			prevglyph = g;
+			continue;
+		}
+
+		float newwidth = w + getKerning(prevglyph, g) + getGlyphAdvance(g);
+
+		// Only wrap when there's a non-space character.
+		if (newwidth > wraplimit && g != ' ')
+		{
+			// Rewind to the last seen space when wrapping.
+			if (lastspaceindex != -1)
+			{
+				wrapindex = lastspaceindex;
+				outwidth = widthbeforelastspace;
+			}
+			break;
+		}
+
+		// Don't count trailing spaces in the output width.
+		if (g == ' ')
+		{
+			lastspaceindex = i;
+			if (prevglyph != ' ')
+				widthbeforelastspace = w;
+		}
+		else
+			outwidth = newwidth;
+
+		w = newwidth;
+		prevglyph = g;
+		wrapindex = i;
+	}
+
+	if (width)
+		*width = outwidth;
+
+	return wrapindex;
+}
+
+} // font
+} // love

+ 46 - 0
src/modules/font/GenericShaper.h

@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2006-2022 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.
+ **/
+
+#pragma once
+
+// LOVE
+#include "TextShaper.h"
+
+namespace love
+{
+namespace font
+{
+
+class GenericShaper : public love::font::TextShaper
+{
+public:
+
+	GenericShaper(Rasterizer *rasterizer);
+	virtual ~GenericShaper();
+
+	void computeGlyphPositions(const ColoredCodepoints &codepoints, Range range, Vector2 offset, float extraspacing, std::vector<GlyphPosition> *positions, std::vector<IndexedColor> *colors, TextInfo *info) override;
+	int computeWordWrapIndex(const ColoredCodepoints& codepoints, Range range, float wraplimit, float *width) override;
+
+private:
+
+}; // GenericShaper
+
+} // font
+} // love

+ 0 - 4
src/modules/font/GlyphData.cpp

@@ -24,10 +24,6 @@
 // UTF-8
 #include "libraries/utf8/utf8.h"
 
-// stdlib
-#include <iostream>
-#include <cstddef>
-
 namespace love
 {
 namespace font

+ 48 - 15
src/modules/font/ImageRasterizer.cpp

@@ -20,8 +20,9 @@
 
 // LOVE
 #include "ImageRasterizer.h"
-
+#include "GenericShaper.h"
 #include "common/Exception.h"
+
 #include <string.h>
 
 namespace love
@@ -31,10 +32,9 @@ namespace font
 
 static_assert(sizeof(Color32) == 4, "sizeof(Color32) must equal 4 bytes!");
 
-ImageRasterizer::ImageRasterizer(love::image::ImageData *data, uint32 *glyphs, int numglyphs, int extraspacing, float dpiscale)
+ImageRasterizer::ImageRasterizer(love::image::ImageData *data, const uint32 *glyphs, int numglyphs, int extraspacing, float dpiscale)
 	: imageData(data)
-	, glyphs(glyphs)
-	, numglyphs(numglyphs)
+	, numglyphs(numglyphs + 1) // Always have a null glyph at the start of the array.
 	, extraSpacing(extraspacing)
 {
 	this->dpiScale = dpiscale;
@@ -42,7 +42,7 @@ ImageRasterizer::ImageRasterizer(love::image::ImageData *data, uint32 *glyphs, i
 	if (data->getFormat() != PIXELFORMAT_RGBA8_UNORM)
 		throw love::Exception("Only 32-bit RGBA images are supported in Image Fonts!");
 
-	load();
+	load(glyphs, numglyphs);
 }
 
 ImageRasterizer::~ImageRasterizer()
@@ -54,16 +54,33 @@ int ImageRasterizer::getLineHeight() const
 	return getHeight();
 }
 
-GlyphData *ImageRasterizer::getGlyphData(uint32 glyph) const
+int ImageRasterizer::getGlyphSpacing(uint32 glyph) const
+{
+	auto it = glyphIndices.find(glyph);
+	if (it == glyphIndices.end())
+		return 0;
+	return imageGlyphs[it->second].width + extraSpacing;
+}
+
+int ImageRasterizer::getGlyphIndex(uint32 glyph) const
+{
+	auto it = glyphIndices.find(glyph);
+	if (it == glyphIndices.end())
+		return 0;
+	return it->second;
+}
+
+GlyphData *ImageRasterizer::getGlyphDataForIndex(int index) const
 {
 	GlyphMetrics gm = {};
+	uint32 glyph = 0;
 
 	// Set relevant glyph metrics if the glyph is in this ImageFont
-	std::map<uint32, ImageGlyphData>::const_iterator it = imageGlyphs.find(glyph);
-	if (it != imageGlyphs.end())
+	if (index >= 0 && index < (int) imageGlyphs.size())
 	{
-		gm.width = it->second.width;
-		gm.advance = it->second.width + extraSpacing;
+		gm.width = imageGlyphs[index].width;
+		gm.advance = imageGlyphs[index].width + extraSpacing;
+		glyph = imageGlyphs[index].glyph;
 	}
 
 	gm.height = metrics.height;
@@ -82,7 +99,7 @@ GlyphData *ImageRasterizer::getGlyphData(uint32 glyph) const
 	// copy glyph pixels from imagedata to glyphdata
 	for (int i = 0; i < g->getWidth() * g->getHeight(); i++)
 	{
-		Color32 p = imagepixels[it->second.x + (i % gm.width) + (imageData->getWidth() * (i / gm.width))];
+		Color32 p = imagepixels[imageGlyphs[index].x + (i % gm.width) + (imageData->getWidth() * (i / gm.width))];
 
 		// Use transparency instead of the spacer color
 		if (p == spacer)
@@ -94,7 +111,7 @@ GlyphData *ImageRasterizer::getGlyphData(uint32 glyph) const
 	return g;
 }
 
-void ImageRasterizer::load()
+void ImageRasterizer::load(const uint32 *glyphs, int glyphcount)
 {
 	auto pixels = (const Color32 *) imageData->getData();
 
@@ -113,7 +130,16 @@ void ImageRasterizer::load()
 	int start = 0;
 	int end = 0;
 
-	for (int i = 0; i < numglyphs; ++i)
+	{
+		ImageGlyphData nullglyph;
+		nullglyph.x = 0;
+		nullglyph.width = 0;
+		nullglyph.glyph = 0;
+		imageGlyphs.push_back(nullglyph);
+		glyphIndices[0] = (int) imageGlyphs.size() - 1;
+	}
+
+	for (int i = 0; i < glyphcount; ++i)
 	{
 		start = end;
 
@@ -133,8 +159,10 @@ void ImageRasterizer::load()
 		ImageGlyphData imageGlyph;
 		imageGlyph.x = start;
 		imageGlyph.width = end - start;
+		imageGlyph.glyph = glyphs[i];
 
-		imageGlyphs[glyphs[i]] = imageGlyph;
+		imageGlyphs.push_back(imageGlyph);
+		glyphIndices[glyphs[i]] = (int) imageGlyphs.size() - 1;
 	}
 }
 
@@ -145,7 +173,7 @@ int ImageRasterizer::getGlyphCount() const
 
 bool ImageRasterizer::hasGlyph(uint32 glyph) const
 {
-	return imageGlyphs.find(glyph) != imageGlyphs.end();
+	return glyphIndices.find(glyph) != glyphIndices.end();
 }
 
 Rasterizer::DataType ImageRasterizer::getDataType() const
@@ -153,5 +181,10 @@ Rasterizer::DataType ImageRasterizer::getDataType() const
 	return DATA_IMAGE;
 }
 
+TextShaper *ImageRasterizer::newTextShaper()
+{
+	return new GenericShaper(this);
+}
+
 } // font
 } // love

+ 10 - 7
src/modules/font/ImageRasterizer.h

@@ -39,15 +39,18 @@ namespace font
 class ImageRasterizer : public Rasterizer
 {
 public:
-	ImageRasterizer(love::image::ImageData *imageData, uint32 *glyphs, int numglyphs, int extraspacing, float dpiscale);
+	ImageRasterizer(love::image::ImageData *imageData, const uint32 *glyphs, int numglyphs, int extraspacing, float dpiscale);
 	virtual ~ImageRasterizer();
 
 	// Implement Rasterizer
 	int getLineHeight() const override;
-	GlyphData *getGlyphData(uint32 glyph) const override;
+	int getGlyphSpacing(uint32 glyph) const override;
+	int getGlyphIndex(uint32 glyph) const override;
+	GlyphData *getGlyphDataForIndex(int index) const override;
 	int getGlyphCount() const override;
 	bool hasGlyph(uint32 glyph) const override;
 	DataType getDataType() const override;
+	TextShaper *newTextShaper() override;
 
 
 private:
@@ -57,23 +60,23 @@ private:
 	{
 		int x;
 		int width;
+		uint32 glyph;
 	};
 
 	// Load all the glyph positions into memory
-	void load();
+	void load(const uint32 *glyphs, int glyphcount);
 
 	// The image data
 	StrongRef<love::image::ImageData> imageData;
 
-	// The glyphs in the font
-	uint32 *glyphs;
-
 	// Number of glyphs in the font
 	int numglyphs;
 
 	int extraSpacing;
 
-	std::map<uint32, ImageGlyphData> imageGlyphs;
+
+	std::vector<ImageGlyphData> imageGlyphs;
+	std::map<uint32, int> glyphIndices;
 
 	// Color used to identify glyph separation in the source ImageData
 	Color32 spacer;

+ 5 - 0
src/modules/font/Rasterizer.cpp

@@ -55,6 +55,11 @@ int Rasterizer::getDescent() const
 	return metrics.descent;
 }
 
+GlyphData *Rasterizer::getGlyphData(uint32 glyph) const
+{
+	return getGlyphDataForIndex(getGlyphIndex(glyph));
+}
+
 GlyphData *Rasterizer::getGlyphData(const std::string &text) const
 {
 	uint32 codepoint = 0;

+ 23 - 2
src/modules/font/Rasterizer.h

@@ -31,6 +31,8 @@ namespace love
 namespace font
 {
 
+class TextShaper;
+
 /**
  * Holds the specific font metrics.
  **/
@@ -84,17 +86,32 @@ public:
 	 **/
 	virtual int getLineHeight() const = 0;
 
+	/**
+	 * Gets the spacing of the given unicode glyph.
+	 **/
+	virtual int getGlyphSpacing(uint32 glyph) const = 0;
+
+	/**
+	 * Gets a rasterizer-specific index associated with the given glyph.
+	 **/
+	virtual int getGlyphIndex(uint32 glyph) const = 0;
+
 	/**
 	 * Gets a specific glyph.
 	 * @param glyph The (UNICODE) glyph codepoint to get data for.
 	 **/
-	virtual GlyphData *getGlyphData(uint32 glyph) const = 0;
+	GlyphData *getGlyphData(uint32 glyph) const;
 
 	/**
 	 * Gets a specific glyph.
 	 * @param text The (UNICODE) glyph character to get the data for.
 	 **/
-	virtual GlyphData *getGlyphData(const std::string &text) const;
+	GlyphData *getGlyphData(const std::string &text) const;
+
+	/**
+	 * Gets a specific glyph for the given rasterizer glyph index.
+	 **/
+	virtual GlyphData *getGlyphDataForIndex(int index) const = 0;
 
 	/**
 	 * Gets the number of glyphs the rasterizer has data for.
@@ -120,6 +137,10 @@ public:
 
 	virtual DataType getDataType() const = 0;
 
+	virtual ptrdiff_t getHandle() const { return 0; }
+
+	virtual TextShaper *newTextShaper() = 0;
+
 	float getDPIScale() const;
 
 protected:

+ 373 - 0
src/modules/font/TextShaper.cpp

@@ -0,0 +1,373 @@
+/**
+ * Copyright (c) 2006-2022 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.
+ **/
+
+// LOVE
+#include "TextShaper.h"
+#include "Rasterizer.h"
+#include "common/Exception.h"
+
+#include "libraries/utf8/utf8.h"
+
+namespace love
+{
+namespace font
+{
+
+void getCodepointsFromString(const std::string& text, std::vector<uint32>& codepoints)
+{
+	codepoints.reserve(text.size());
+
+	try
+	{
+		utf8::iterator<std::string::const_iterator> i(text.begin(), text.begin(), text.end());
+		utf8::iterator<std::string::const_iterator> end(text.end(), text.begin(), text.end());
+
+		while (i != end)
+		{
+			uint32 g = *i++;
+			codepoints.push_back(g);
+		}
+	}
+	catch (utf8::exception& e)
+	{
+		throw love::Exception("UTF-8 decoding error: %s", e.what());
+	}
+}
+
+void getCodepointsFromString(const std::vector<ColoredString>& strs, ColoredCodepoints& codepoints)
+{
+	if (strs.empty())
+		return;
+
+	codepoints.cps.reserve(strs[0].str.size());
+
+	for (const ColoredString& cstr : strs)
+	{
+		// No need to add the color if the string is empty anyway, and the code
+		// further on assumes no two colors share the same starting position.
+		if (cstr.str.size() == 0)
+			continue;
+
+		IndexedColor c = { cstr.color, (int)codepoints.cps.size() };
+		codepoints.colors.push_back(c);
+
+		getCodepointsFromString(cstr.str, codepoints.cps);
+	}
+
+	if (codepoints.colors.size() == 1)
+	{
+		IndexedColor c = codepoints.colors[0];
+
+		if (c.index == 0 && c.color == Colorf(1.0f, 1.0f, 1.0f, 1.0f))
+			codepoints.colors.pop_back();
+	}
+}
+
+love::Type TextShaper::type("TextShaper", &Object::type);
+
+TextShaper::TextShaper(Rasterizer *rasterizer)
+	: rasterizers{rasterizer}
+	, dpiScales{rasterizer->getDPIScale()}
+	, height(floorf(rasterizer->getHeight() / rasterizer->getDPIScale() + 0.5f))
+	, lineHeight(1)
+{
+}
+
+TextShaper::~TextShaper()
+{
+}
+
+float TextShaper::getHeight() const
+{
+	return height;
+}
+
+void TextShaper::setLineHeight(float h)
+{
+	lineHeight = h;
+}
+
+float TextShaper::getLineHeight() const
+{
+	return lineHeight;
+}
+
+int TextShaper::getAscent() const
+{
+	return floorf(rasterizers[0]->getAscent() / rasterizers[0]->getDPIScale() + 0.5f);
+}
+
+int TextShaper::getDescent() const
+{
+	return floorf(rasterizers[0]->getDescent() / rasterizers[0]->getDPIScale() + 0.5f);
+}
+
+float TextShaper::getBaseline() const
+{
+	float ascent = getAscent();
+	if (ascent != 0.0f)
+		return ascent;
+	else if (rasterizers[0]->getDataType() == font::Rasterizer::DATA_TRUETYPE)
+		return floorf(getHeight() / 1.25f + 0.5f); // 1.25 is magic line height for true type fonts
+	else
+		return 0.0f;
+}
+
+bool TextShaper::hasGlyph(uint32 glyph) const
+{
+	for (const StrongRef<Rasterizer>& r : rasterizers)
+	{
+		if (r->hasGlyph(glyph))
+			return true;
+	}
+
+	return false;
+}
+
+bool TextShaper::hasGlyphs(const std::string& text) const
+{
+	if (text.size() == 0)
+		return false;
+
+	try
+	{
+		utf8::iterator<std::string::const_iterator> i(text.begin(), text.begin(), text.end());
+		utf8::iterator<std::string::const_iterator> end(text.end(), text.begin(), text.end());
+
+		while (i != end)
+		{
+			uint32 codepoint = *i++;
+
+			if (!hasGlyph(codepoint))
+				return false;
+		}
+	}
+	catch (utf8::exception& e)
+	{
+		throw love::Exception("UTF-8 decoding error: %s", e.what());
+	}
+
+	return true;
+}
+
+float TextShaper::getKerning(uint32 leftglyph, uint32 rightglyph)
+{
+	uint64 packedglyphs = ((uint64)leftglyph << 32) | (uint64)rightglyph;
+
+	const auto it = kerning.find(packedglyphs);
+	if (it != kerning.end())
+		return it->second;
+
+	float k = 0.0f;
+	bool found = false;
+
+	for (const auto& r : rasterizers)
+	{
+		if (r->hasGlyph(leftglyph) && r->hasGlyph(rightglyph))
+		{
+			found = true;
+			k = floorf(r->getKerning(leftglyph, rightglyph) / r->getDPIScale() + 0.5f);
+			break;
+		}
+	}
+
+	if (!found)
+		k = floorf(rasterizers[0]->getKerning(leftglyph, rightglyph) / rasterizers[0]->getDPIScale() + 0.5f);
+
+	kerning[packedglyphs] = k;
+	return k;
+}
+
+float TextShaper::getKerning(const std::string& leftchar, const std::string& rightchar)
+{
+	uint32 left = 0;
+	uint32 right = 0;
+
+	try
+	{
+		left = utf8::peek_next(leftchar.begin(), leftchar.end());
+		right = utf8::peek_next(rightchar.begin(), rightchar.end());
+	}
+	catch (utf8::exception& e)
+	{
+		throw love::Exception("UTF-8 decoding error: %s", e.what());
+	}
+
+	return getKerning(left, right);
+}
+
+int TextShaper::getGlyphAdvance(uint32 glyph, GlyphIndex *glyphindex)
+{
+	// TODO: use spaces for tab
+
+	const auto it = glyphAdvances.find(glyph);
+	if (it != glyphAdvances.end())
+	{
+		if (glyphindex)
+			*glyphindex = it->second.second;
+		return it->second.first;
+	}
+
+	int rasterizeri = 0;
+
+	for (size_t i = 0; i < rasterizers.size(); i++)
+	{
+		if (rasterizers[i]->hasGlyph(glyph))
+		{
+			rasterizeri = (int) i;
+			break;
+		}
+	}
+
+	const auto &r = rasterizers[rasterizeri];
+	int advance = floorf(r->getGlyphSpacing(glyph) / r->getDPIScale() + 0.5f);
+
+	GlyphIndex glyphi = {r->getGlyphIndex(glyph), rasterizeri};
+
+	glyphAdvances[glyph] = std::make_pair(advance, glyphi);
+	if (glyphindex)
+		*glyphindex = glyphi;
+	return advance;
+}
+
+int TextShaper::getWidth(const std::string &str)
+{
+	if (str.size() == 0) return 0;
+
+	ColoredCodepoints codepoints;
+	getCodepointsFromString(str, codepoints.cps);
+
+	TextInfo info;
+	computeGlyphPositions(codepoints, Range(), Vector2(0.0f, 0.0f), 0.0f, nullptr, nullptr, &info);
+
+	return info.width;
+}
+
+static size_t findNewline(const ColoredCodepoints& codepoints, size_t start)
+{
+	for (size_t i = start; i < codepoints.cps.size(); i++)
+	{
+		if (codepoints.cps[i] == '\n')
+		{
+			return i;
+		}
+	}
+
+	return codepoints.cps.size();
+}
+
+void TextShaper::getWrap(const ColoredCodepoints& codepoints, float wraplimit, std::vector<Range>& lineranges, std::vector<int>* linewidths)
+{
+	size_t nextnewline = findNewline(codepoints, 0);
+
+	for (size_t i = 0; i < codepoints.cps.size();)
+	{
+		if (nextnewline < i)
+			nextnewline = findNewline(codepoints, i);
+
+		if (nextnewline == i) // Empty line.
+		{
+			lineranges.push_back(Range());
+			if (linewidths)
+				linewidths->push_back(0);
+			i++;
+		}
+		else
+		{
+			Range r(i, nextnewline - i);
+			float width = 0.0f;
+			int wrapindex = computeWordWrapIndex(codepoints, r, wraplimit, &width);
+
+			if (wrapindex >= (int) i)
+			{
+				r = Range(i, (size_t) wrapindex + 1 - i);
+				i = (size_t)wrapindex + 1;
+			}
+			else
+			{
+				r = Range();
+				i++;
+			}
+
+			// We've already handled this line, skip the newline character.
+			if (nextnewline == i)
+				i++;
+
+			lineranges.push_back(r);
+			if (linewidths)
+				linewidths->push_back(width);
+		}
+	}
+}
+
+void TextShaper::getWrap(const std::vector<ColoredString>& text, float wraplimit, std::vector<std::string>& lines, std::vector<int>* linewidths)
+{
+	ColoredCodepoints cps;
+	getCodepointsFromString(text, cps);
+
+	std::vector<Range> codepointranges;
+	getWrap(cps, wraplimit, codepointranges, linewidths);
+
+	std::string line;
+
+	for (const auto& range : codepointranges)
+	{
+		line.clear();
+
+		if (range.isValid())
+		{
+			line.reserve(range.getSize());
+
+			for (size_t i = range.getMin(); i <= range.getMax(); i++)
+			{
+				char character[5] = { '\0' };
+				char* end = utf8::unchecked::append(cps.cps[i], character);
+				line.append(character, end - character);
+			}
+		}
+
+		lines.push_back(line);
+	}
+}
+
+void TextShaper::setFallbacks(const std::vector<Rasterizer*>& fallbacks)
+{
+	for (Rasterizer* r : fallbacks)
+	{
+		if (r->getDataType() != rasterizers[0]->getDataType())
+			throw love::Exception("Font fallbacks must be of the same font type.");
+	}
+
+	// Clear caches.
+	kerning.clear();
+	glyphAdvances.clear();
+
+	rasterizers.resize(1);
+	dpiScales.resize(1);
+
+	for (Rasterizer* r : fallbacks)
+	{
+		rasterizers.push_back(r);
+		dpiScales.push_back(r->getDPIScale());
+	}
+}
+
+} // font
+} // love

+ 148 - 0
src/modules/font/TextShaper.h

@@ -0,0 +1,148 @@
+/**
+ * Copyright (c) 2006-2022 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.
+ **/
+
+#pragma once
+
+// LOVE
+#include "common/Object.h"
+#include "common/Vector.h"
+#include "common/int.h"
+#include "common/Color.h"
+#include "common/Range.h"
+
+#include <vector>
+#include <string>
+#include <unordered_map>
+
+namespace love
+{
+namespace font
+{
+
+class Rasterizer;
+
+struct ColoredString
+{
+	std::string str;
+	Colorf color;
+};
+
+struct IndexedColor
+{
+	Colorf color;
+	int index;
+};
+
+struct ColoredCodepoints
+{
+	std::vector<uint32> cps;
+	std::vector<IndexedColor> colors;
+};
+
+void getCodepointsFromString(const std::string& str, std::vector<uint32>& codepoints);
+void getCodepointsFromString(const std::vector<ColoredString>& strs, ColoredCodepoints& codepoints);
+
+class TextShaper : public Object
+{
+public:
+
+	struct GlyphIndex
+	{
+		int index;
+		int rasterizerIndex;
+	};
+
+	struct GlyphPosition
+	{
+		Vector2 position;
+		GlyphIndex glyphIndex;
+	};
+
+	struct TextInfo
+	{
+		int width;
+		int height;
+	};
+
+	static love::Type type;
+
+	virtual ~TextShaper();
+
+	const std::vector<StrongRef<Rasterizer>> &getRasterizers() const { return rasterizers; }
+
+	float getHeight() const;
+
+	/**
+	 * Sets the line height (which should be a number to multiply the font size by,
+	 * example: line height = 1.2 and size = 12 means that rendered line height = 12*1.2)
+	 * @param height The new line height.
+	 **/
+	void setLineHeight(float height);
+
+	/**
+	 * Returns the line height.
+	 **/
+	float getLineHeight() const;
+
+	// Extra font metrics
+	int getAscent() const;
+	int getDescent() const;
+	float getBaseline() const;
+
+	bool hasGlyph(uint32 glyph) const;
+	bool hasGlyphs(const std::string &text) const;
+
+	float getKerning(uint32 leftglyph, uint32 rightglyph);
+	float getKerning(const std::string& leftchar, const std::string& rightchar);
+
+	int getGlyphAdvance(uint32 glyph, GlyphIndex *glyphindex = nullptr);
+
+	int getWidth(const std::string &str);
+
+	void getWrap(const std::vector<ColoredString>& text, float wraplimit, std::vector<std::string>& lines, std::vector<int>* line_widths = nullptr);
+	void getWrap(const ColoredCodepoints& codepoints, float wraplimit, std::vector<Range> &lineranges, std::vector<int> *linewidths = nullptr);
+
+	virtual void setFallbacks(const std::vector<Rasterizer *> &fallbacks);
+
+	virtual void computeGlyphPositions(const ColoredCodepoints &codepoints, Range range, Vector2 offset, float extraspacing, std::vector<GlyphPosition> *positions, std::vector<IndexedColor> *colors, TextInfo *info) = 0;
+	virtual int computeWordWrapIndex(const ColoredCodepoints& codepoints, Range range, float wraplimit, float *width) = 0;
+
+protected:
+
+	TextShaper(Rasterizer *rasterizer);
+
+	std::vector<StrongRef<Rasterizer>> rasterizers;
+	std::vector<float> dpiScales;
+
+private:
+
+	int height;
+	float lineHeight;
+
+	// maps glyphs to advance and glyph+rasterizer index.
+	std::unordered_map<uint32, std::pair<int, GlyphIndex>> glyphAdvances;
+
+	// map of left/right glyph pairs to horizontal kerning.
+	std::unordered_map<uint64, float> kerning;
+
+}; // TextShaper
+
+} // font
+} // love

+ 290 - 0
src/modules/font/freetype/HarfbuzzShaper.cpp

@@ -0,0 +1,290 @@
+/**
+ * Copyright (c) 2006-2022 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.
+ **/
+
+// LOVE
+#include "HarfbuzzShaper.h"
+#include "TrueTypeRasterizer.h"
+#include "common/Optional.h"
+
+// harfbuzz
+#include <hb.h>
+#include <hb-ft.h>
+
+namespace love
+{
+namespace font
+{
+namespace freetype
+{
+
+HarfbuzzShaper::HarfbuzzShaper(TrueTypeRasterizer *rasterizer)
+	: TextShaper(rasterizer)
+{
+	hbFonts.push_back(hb_ft_font_create_referenced((FT_Face)rasterizer->getHandle()));
+
+	if (hbFonts[0] == nullptr || hbFonts[0] == hb_font_get_empty())
+		throw love::Exception("");
+}
+
+HarfbuzzShaper::~HarfbuzzShaper()
+{
+	for (hb_font_t *font : hbFonts)
+		hb_font_destroy(font);
+}
+
+void HarfbuzzShaper::setFallbacks(const std::vector<Rasterizer*>& fallbacks)
+{
+	for (size_t i = 1; i < hbFonts.size(); i++)
+		hb_font_destroy(hbFonts[i]);
+
+	TextShaper::setFallbacks(fallbacks);
+
+	hbFonts.resize(rasterizers.size());
+
+	for (size_t i = 1; i < hbFonts.size(); i++)
+		hbFonts[i] = hb_ft_font_create_referenced((FT_Face)rasterizers[i]->getHandle());
+}
+
+void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints, Range range, Vector2 offset, float extraspacing, std::vector<GlyphPosition> *positions, std::vector<IndexedColor> *colors, TextInfo *info)
+{
+	if (!range.isValid())
+		range = Range(0, codepoints.cps.size());
+
+	offset.y += getBaseline();
+	Vector2 curpos = offset;
+
+	hb_buffer_t *hbbuffer = hb_buffer_create();
+
+	hb_buffer_add_codepoints(hbbuffer, codepoints.cps.data(), codepoints.cps.size(), (unsigned int) range.getOffset(), (int) range.getSize());
+
+	// TODO: Expose APIs for direction and script?
+	hb_buffer_guess_segment_properties(hbbuffer);
+
+	hb_shape(hbFonts[0], hbbuffer, nullptr, 0);
+
+	int glyphcount = (int) hb_buffer_get_length(hbbuffer);
+	hb_glyph_info_t* glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr);
+	hb_glyph_position_t* glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr);
+
+	if (positions)
+		positions->reserve(positions->size() + glyphcount);
+
+	if (rasterizers.size() > 1)
+	{
+		std::vector<Range> fallbackranges;
+
+		for (int i = 0; i < glyphcount; i++)
+		{
+			bool adding = false;
+			if (glyphinfos[i].codepoint == 0)
+			{
+				if (fallbackranges.empty() || fallbackranges.back().getMax() != glyphinfos[i-1].cluster)
+					fallbackranges.push_back(Range(glyphinfos[i].cluster, 1));
+				else
+					fallbackranges.back().encapsulate(glyphinfos[i].cluster);
+			}
+		}
+
+		if (!fallbackranges.empty())
+		{
+			for (size_t rasti = 1; rasti < rasterizers.size(); rasti++)
+			{
+				hb_buffer_t* hbbuffer = hb_buffer_create();
+
+				hb_buffer_destroy(hbbuffer);
+			}
+		}
+	}
+
+	int colorindex = 0;
+	int ncolors = (int)codepoints.colors.size();
+	Optional<Colorf> colorToAdd;
+
+	// Make sure the right color is applied to the start of the glyph list,
+	// when the start isn't 0.
+	if (colors && range.getOffset() > 0 && !codepoints.colors.empty())
+	{
+		for (; colorindex < ncolors; colorindex++)
+		{
+			if (codepoints.colors[colorindex].index >= (int) range.getOffset())
+				break;
+			colorToAdd.set(codepoints.colors[colorindex].color);
+		}
+	}
+
+	int maxwidth = (int)curpos.x;
+
+	// TODO: fallbacks
+	// TODO: use spaces for tab
+	for (int i = 0; i < glyphcount; i++)
+	{
+		const hb_glyph_info_t& info = glyphinfos[i];
+		const hb_glyph_position_t& glyphpos = glyphpositions[i];
+
+		// TODO: this doesn't handle situations where the user inserted a color
+		// change in the middle of some characters that get combined into a single
+		// cluster.
+		if (colors && colorindex < ncolors && codepoints.colors[colorindex].index == info.cluster)
+		{
+			colorToAdd.set(codepoints.colors[colorindex].color);
+			colorindex++;
+		}
+
+		uint32 clustercodepoint = codepoints.cps[info.cluster];
+
+		// Harfbuzz doesn't handle newlines itself, but it does leave them in
+		// the glyph list so we can do it manually.
+		if (clustercodepoint == '\n')
+		{
+			if (curpos.x > maxwidth)
+				maxwidth = (int)curpos.x;
+
+			// Wrap newline, but do not output a position for it.
+			curpos.y += floorf(getHeight() * getLineHeight() + 0.5f);
+			curpos.x = offset.x;
+			continue;
+		}
+
+		// Ignore carriage returns
+		if (clustercodepoint == '\r')
+			continue;
+
+		if (colorToAdd.hasValue && colors && positions)
+		{
+			IndexedColor c = {colorToAdd.value, positions->size()};
+			colors->push_back(c);
+			colorToAdd.clear();
+		}
+
+		if (positions)
+		{
+			// Despite the name this is a glyph index at this point.
+			GlyphIndex gindex = { info.codepoint, 0 };
+			GlyphPosition p = { curpos, gindex };
+
+			// Harfbuzz position coordinate systems are based on the given font.
+			// Freetype uses 26.6 fixed point coordinates, so harfbuzz does too.
+			p.position.x += floorf((glyphpos.x_offset >> 6) / dpiScales[0] + 0.5f);
+			p.position.y += floorf((glyphpos.y_offset >> 6) / dpiScales[0] + 0.5f);
+
+			positions->push_back(p);
+		}
+
+		curpos.x += floorf((glyphpos.x_advance >> 6) / dpiScales[0] + 0.5f);
+		curpos.y += floorf((glyphpos.y_advance >> 6) / dpiScales[0] + 0.5f);
+
+		// Account for extra spacing given to space characters.
+		if (clustercodepoint == ' ' && extraspacing != 0.0f)
+			curpos.x = floorf(curpos.x + extraspacing);
+	}
+
+	hb_buffer_destroy(hbbuffer);
+
+	if (curpos.x > maxwidth)
+		maxwidth = (int)curpos.x;
+
+	if (info != nullptr)
+	{
+		info->width = maxwidth - offset.x;
+		info->height = curpos.y - offset.y;
+		if (curpos.x > offset.x)
+			info->height += floorf(getHeight() * getLineHeight() + 0.5f);
+	}
+}
+
+int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Range range, float wraplimit, float *width)
+{
+	if (!range.isValid())
+		range = Range(0, codepoints.cps.size());
+
+	hb_buffer_t* hbbuffer = hb_buffer_create();
+
+	hb_buffer_add_codepoints(hbbuffer, codepoints.cps.data(), codepoints.cps.size(), (unsigned int)range.getOffset(), (int)range.getSize());
+
+	// TODO: Expose APIs for direction and script?
+	hb_buffer_guess_segment_properties(hbbuffer);
+
+	hb_shape(hbFonts[0], hbbuffer, nullptr, 0);
+
+	int glyphcount = (int)hb_buffer_get_length(hbbuffer);
+	hb_glyph_info_t* glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr);
+	hb_glyph_position_t* glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr);
+
+	float w = 0.0f;
+	float outwidth = 0.0f;
+	float widthbeforelastspace = 0.0f;
+	int wrapindex = -1;
+	int lastspaceindex = -1;
+
+	uint32 prevcodepoint = 0;
+
+	for (int i = 0; i < glyphcount; i++)
+	{
+		const hb_glyph_info_t& info = glyphinfos[i];
+		const hb_glyph_position_t& glyphpos = glyphpositions[i];
+
+		uint32 clustercodepoint = codepoints.cps[info.cluster];
+
+		if (clustercodepoint == '\r')
+		{
+			prevcodepoint = clustercodepoint;
+			continue;
+		}
+
+		float newwidth = w + floorf((glyphpos.x_advance >> 6) / dpiScales[0] + 0.5f);
+
+		// Only wrap when there's a non-space character.
+		if (newwidth > wraplimit && clustercodepoint != ' ')
+		{
+			// Rewind to the last seen space when wrapping.
+			if (lastspaceindex != -1)
+			{
+				wrapindex = lastspaceindex;
+				outwidth = widthbeforelastspace;
+			}
+			break;
+		}
+
+		// Don't count trailing spaces in the output width.
+		if (clustercodepoint == ' ')
+		{
+			lastspaceindex = info.cluster;
+			if (prevcodepoint != ' ')
+				widthbeforelastspace = w;
+		}
+		else
+			outwidth = newwidth;
+
+		w = newwidth;
+		prevcodepoint = clustercodepoint;
+		wrapindex = info.cluster;
+	}
+
+	hb_buffer_destroy(hbbuffer);
+
+	if (width)
+		*width = outwidth;
+
+	return wrapindex;
+}
+
+} // freetype
+} // font
+} // love

+ 59 - 0
src/modules/font/freetype/HarfbuzzShaper.h

@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2006-2022 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.
+ **/
+
+#pragma once
+
+// LOVE
+#include "font/TextShaper.h"
+
+extern "C"
+{
+typedef struct hb_font_t hb_font_t;
+}
+
+namespace love
+{
+namespace font
+{
+namespace freetype
+{
+
+class TrueTypeRasterizer;
+
+class HarfbuzzShaper : public love::font::TextShaper
+{
+public:
+
+	HarfbuzzShaper(TrueTypeRasterizer *rasterizer);
+	virtual ~HarfbuzzShaper();
+
+	void setFallbacks(const std::vector<Rasterizer*>& fallbacks) override;
+	void computeGlyphPositions(const ColoredCodepoints &codepoints, Range range, Vector2 offset, float extraspacing, std::vector<GlyphPosition> *positions, std::vector<IndexedColor> *colors, TextInfo *info) override;
+	int computeWordWrapIndex(const ColoredCodepoints& codepoints, Range range, float wraplimit, float *width) override;
+
+private:
+
+	std::vector<hb_font_t *> hbFonts;
+
+}; // HarfbuzzShaper
+
+} // freetype
+} // font
+} // love

+ 34 - 4
src/modules/font/freetype/TrueTypeRasterizer.cpp

@@ -20,6 +20,7 @@
 
 // LOVE
 #include "TrueTypeRasterizer.h"
+#include "HarfbuzzShaper.h"
 #include "common/Exception.h"
 
 // C
@@ -75,7 +76,30 @@ int TrueTypeRasterizer::getLineHeight() const
 	return (int)(getHeight() * 1.25);
 }
 
-GlyphData *TrueTypeRasterizer::getGlyphData(uint32 glyph) const
+int TrueTypeRasterizer::getGlyphSpacing(uint32 glyph) const
+{
+	FT_Glyph ftglyph;
+	FT_Error err = FT_Err_Ok;
+	FT_UInt loadoption = hintingToLoadOption(hinting);
+
+	// Initialize
+	err = FT_Load_Glyph(face, FT_Get_Char_Index(face, glyph), FT_LOAD_DEFAULT | loadoption);
+	if (err != FT_Err_Ok)
+		return 0;
+
+	err = FT_Get_Glyph(face->glyph, &ftglyph);
+	if (err != FT_Err_Ok)
+		return 0;
+
+	return (int)(ftglyph->advance.x >> 16);
+}
+
+int TrueTypeRasterizer::getGlyphIndex(uint32 glyph) const
+{
+	return FT_Get_Char_Index(face, glyph);
+}
+
+GlyphData *TrueTypeRasterizer::getGlyphDataForIndex(int index) const
 {
 	love::font::GlyphMetrics glyphMetrics = {};
 	FT_Glyph ftglyph;
@@ -84,7 +108,7 @@ GlyphData *TrueTypeRasterizer::getGlyphData(uint32 glyph) const
 	FT_UInt loadoption = hintingToLoadOption(hinting);
 
 	// Initialize
-	err = FT_Load_Glyph(face, FT_Get_Char_Index(face, glyph), FT_LOAD_DEFAULT | loadoption);
+	err = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT | loadoption);
 
 	if (err != FT_Err_Ok)
 		throw love::Exception("TrueType Font glyph error: FT_Load_Glyph failed (0x%x)", err);
@@ -104,7 +128,7 @@ GlyphData *TrueTypeRasterizer::getGlyphData(uint32 glyph) const
 		throw love::Exception("TrueType Font glyph error: FT_Glyph_To_Bitmap failed (0x%x)", err);
 
 	FT_BitmapGlyph bitmap_glyph = (FT_BitmapGlyph) ftglyph;
-	FT_Bitmap &bitmap = bitmap_glyph->bitmap; //just to make things easier
+	const FT_Bitmap &bitmap = bitmap_glyph->bitmap; //just to make things easier
 
 	// Get metrics
 	glyphMetrics.bearingX = bitmap_glyph->left;
@@ -113,7 +137,8 @@ GlyphData *TrueTypeRasterizer::getGlyphData(uint32 glyph) const
 	glyphMetrics.width = bitmap.width;
 	glyphMetrics.advance = (int) (ftglyph->advance.x >> 16);
 
-	GlyphData *glyphData = new GlyphData(glyph, glyphMetrics, PIXELFORMAT_LA8_UNORM);
+	// TODO: https://stackoverflow.com/questions/60526004/how-to-get-glyph-unicode-using-freetype/69730502#69730502
+	GlyphData *glyphData = new GlyphData(0, glyphMetrics, PIXELFORMAT_LA8_UNORM);
 
 	const uint8 *pixels = bitmap.buffer;
 	uint8 *dest = (uint8 *) glyphData->getData();
@@ -186,6 +211,11 @@ Rasterizer::DataType TrueTypeRasterizer::getDataType() const
 	return DATA_TRUETYPE;
 }
 
+TextShaper *TrueTypeRasterizer::newTextShaper()
+{
+	return new HarfbuzzShaper(this);
+}
+
 bool TrueTypeRasterizer::accepts(FT_Library library, love::Data *data)
 {
 	const FT_Byte *fbase = (const FT_Byte *) data->getData();

+ 6 - 1
src/modules/font/freetype/TrueTypeRasterizer.h

@@ -49,11 +49,16 @@ public:
 
 	// Implement Rasterizer
 	int getLineHeight() const override;
-	GlyphData *getGlyphData(uint32 glyph) const override;
+	int getGlyphSpacing(uint32 glyph) const override;
+	int getGlyphIndex(uint32 glyph) const override;
+	GlyphData *getGlyphDataForIndex(int index) const override;
 	int getGlyphCount() const override;
 	bool hasGlyph(uint32 glyph) const override;
 	float getKerning(uint32 leftglyph, uint32 rightglyph) const override;
 	DataType getDataType() const override;
+	TextShaper *newTextShaper() override;
+
+	ptrdiff_t getHandle() const override { return (ptrdiff_t) face; }
 
 	static bool accepts(FT_Library library, love::Data *data);
 

+ 1 - 1
src/modules/graphics/Deprecations.cpp

@@ -90,7 +90,7 @@ void Deprecations::draw(Graphics *gfx)
 	int maxcount = 4;
 	int remaining = std::max(0, total - maxcount);
 
-	std::vector<Font::ColoredString> strings;
+	std::vector<font::ColoredString> strings;
 	Colorf white(1, 1, 1, 1);
 
 	// Grab the newest deprecation notices first.

+ 120 - 484
src/modules/graphics/Font.cpp

@@ -21,8 +21,6 @@
 #include "Font.h"
 #include "font/GlyphData.h"
 
-#include "libraries/utf8/utf8.h"
-
 #include "common/math.h"
 #include "common/Matrix.h"
 #include "Graphics.h"
@@ -42,15 +40,23 @@ static inline uint16 normToUint16(double n)
 	return (uint16) (n * LOVE_UINT16_MAX);
 }
 
+static inline uint64 packGlyphIndex(love::font::TextShaper::GlyphIndex glyphindex)
+{
+	return ((uint64)glyphindex.rasterizerIndex << 32) | (uint64)glyphindex.index;
+}
+
+static inline love::font::TextShaper::GlyphIndex unpackGlyphIndex(uint64 packedindex)
+{
+	return {(int) (packedindex & 0xFFFFFFFF), (int) (packedindex >> 32)};
+}
+
 love::Type Font::type("Font", &Object::type);
 int Font::fontCount = 0;
 
 const CommonFormat Font::vertexFormat = CommonFormat::XYf_STus_RGBAub;
 
 Font::Font(love::font::Rasterizer *r, const SamplerState &s)
-	: rasterizers({r})
-	, height(r->getHeight())
-	, lineHeight(1)
+	: shaper(r->newTextShaper(), Acquire::NORETAIN)
 	, textureWidth(128)
 	, textureHeight(128)
 	, samplerState()
@@ -66,7 +72,7 @@ Font::Font(love::font::Rasterizer *r, const SamplerState &s)
 	// largest texture size if no rough match is found.
 	while (true)
 	{
-		if ((height * 0.8) * height * 30 <= textureWidth * textureHeight)
+		if ((shaper->getHeight() * 0.8) * shaper->getHeight() * 30 <= textureWidth * textureHeight)
 			break;
 
 		TextureSize nextsize = getNextTextureSize();
@@ -86,7 +92,8 @@ Font::Font(love::font::Rasterizer *r, const SamplerState &s)
 	if (pixelFormat == PIXELFORMAT_LA8_UNORM && !gfx->isPixelFormatSupported(pixelFormat, PIXELFORMATUSAGEFLAGS_SAMPLE))
 		pixelFormat = PIXELFORMAT_RGBA8_UNORM;
 
-	if (!r->hasGlyph(9)) // No tab character in the Rasterizer.
+	uint32 tab = '\t';
+	if (!r->hasGlyph(tab)) // No tab character in the Rasterizer.
 		useSpacesAsTab = true;
 
 	loadVolatile();
@@ -170,7 +177,7 @@ void Font::createTexture()
 		// and transparent black otherwise.
 		std::vector<uint8> emptydata(datasize, 0);
 
-		if (rasterizers[0]->getDataType() == font::Rasterizer::DATA_TRUETYPE)
+		if (shaper->getRasterizers()[0]->getDataType() == font::Rasterizer::DATA_TRUETYPE)
 		{
 			if (pixelFormat == PIXELFORMAT_LA8_UNORM)
 			{
@@ -204,15 +211,15 @@ void Font::createTexture()
 	{
 		textureCacheID++;
 
-		std::vector<uint32> glyphstoadd;
+		std::vector<love::font::TextShaper::GlyphIndex> glyphstoadd;
 
 		for (const auto &glyphpair : glyphs)
-			glyphstoadd.push_back(glyphpair.first);
+			glyphstoadd.push_back(unpackGlyphIndex(glyphpair.first));
 
 		glyphs.clear();
 		
-		for (uint32 g : glyphstoadd)
-			addGlyph(g);
+		for (auto glyphindex : glyphstoadd)
+			addGlyph(glyphindex);
 	}
 }
 
@@ -222,12 +229,14 @@ void Font::unloadVolatile()
 	textures.clear();
 }
 
-love::font::GlyphData *Font::getRasterizerGlyphData(uint32 glyph, float &dpiscale)
+love::font::GlyphData *Font::getRasterizerGlyphData(love::font::TextShaper::GlyphIndex glyphindex, float &dpiscale)
 {
-	// Use spaces for the tab 'glyph'.
-	if (glyph == 9 && useSpacesAsTab)
+	const auto &r = shaper->getRasterizers()[glyphindex.rasterizerIndex];
+
+	// Use spaces for the tab 'glyph'. FIXME
+	if (/*glyph == '\t' &&*/ false && useSpacesAsTab)
 	{
-		love::font::GlyphData *spacegd = rasterizers[0]->getGlyphData(32);
+		love::font::GlyphData *spacegd = r->getGlyphData(32);
 		PixelFormat fmt = spacegd->getFormat();
 
 		love::font::GlyphMetrics gm = {};
@@ -237,27 +246,18 @@ love::font::GlyphData *Font::getRasterizerGlyphData(uint32 glyph, float &dpiscal
 
 		spacegd->release();
 
-		dpiscale = rasterizers[0]->getDPIScale();
-		return new love::font::GlyphData(glyph, gm, fmt);
-	}
-
-	for (const StrongRef<love::font::Rasterizer> &r : rasterizers)
-	{
-		if (r->hasGlyph(glyph))
-		{
-			dpiscale = r->getDPIScale();
-			return r->getGlyphData(glyph);
-		}
+		dpiscale = r->getDPIScale();
+		return new love::font::GlyphData('\t', gm, fmt);
 	}
 
-	dpiscale = rasterizers[0]->getDPIScale();
-	return rasterizers[0]->getGlyphData(glyph);
+	dpiscale = r->getDPIScale();
+	return r->getGlyphDataForIndex(glyphindex.index);
 }
 
-const Font::Glyph &Font::addGlyph(uint32 glyph)
+const Font::Glyph &Font::addGlyph(love::font::TextShaper::GlyphIndex glyphindex)
 {
 	float glyphdpiscale = getDPIScale();
-	StrongRef<love::font::GlyphData> gd(getRasterizerGlyphData(glyph, glyphdpiscale), Acquire::NORETAIN);
+	StrongRef<love::font::GlyphData> gd(getRasterizerGlyphData(glyphindex, glyphdpiscale), Acquire::NORETAIN);
 
 	int w = gd->getWidth();
 	int h = gd->getHeight();
@@ -279,15 +279,13 @@ const Font::Glyph &Font::addGlyph(uint32 glyph)
 
 			// Makes sure the above code for checking if the glyph can fit at
 			// the current position in the texture is run again for this glyph.
-			return addGlyph(glyph);
+			return addGlyph(glyphindex);
 		}
 	}
 
 	Glyph g;
 
-	g.texture = 0;
-	g.spacing = floorf(gd->getAdvance() / glyphdpiscale + 0.5f);
-
+	g.texture = nullptr;
 	memset(g.vertices, 0, sizeof(GlyphVertex) * 4);
 
 	// Don't waste space for empty glyphs.
@@ -357,151 +355,77 @@ const Font::Glyph &Font::addGlyph(uint32 glyph)
 		rowHeight = std::max(rowHeight, h + TEXTURE_PADDING);
 	}
 
-	glyphs[glyph] = g;
-	return glyphs[glyph];
+	uint64 packedindex = packGlyphIndex(glyphindex);
+	glyphs[packedindex] = g;
+	return glyphs[packedindex];
 }
 
-const Font::Glyph &Font::findGlyph(uint32 glyph)
+const Font::Glyph &Font::findGlyph(love::font::TextShaper::GlyphIndex glyphindex)
 {
-	const auto it = glyphs.find(glyph);
+	uint64 packedindex = packGlyphIndex(glyphindex);
+	const auto it = glyphs.find(packedindex);
 
 	if (it != glyphs.end())
 		return it->second;
 
-	return addGlyph(glyph);
+	return addGlyph(glyphindex);
 }
 
 float Font::getKerning(uint32 leftglyph, uint32 rightglyph)
 {
-	uint64 packedglyphs = ((uint64) leftglyph << 32) | (uint64) rightglyph;
-
-	const auto it = kerning.find(packedglyphs);
-	if (it != kerning.end())
-		return it->second;
-
-	float k = floorf(rasterizers[0]->getKerning(leftglyph, rightglyph) / dpiScale + 0.5f);
-
-	for (const auto &r : rasterizers)
-	{
-		if (r->hasGlyph(leftglyph) && r->hasGlyph(rightglyph))
-		{
-			k = floorf(r->getKerning(leftglyph, rightglyph) / r->getDPIScale() + 0.5f);
-			break;
-		}
-	}
-
-	kerning[packedglyphs] = k;
-	return k;
+	return shaper->getKerning(leftglyph, rightglyph);
 }
 
 float Font::getKerning(const std::string &leftchar, const std::string &rightchar)
 {
-	uint32 left = 0;
-	uint32 right = 0;
-
-	try
-	{
-		left = utf8::peek_next(leftchar.begin(), leftchar.end());
-		right = utf8::peek_next(rightchar.begin(), rightchar.end());
-	}
-	catch (utf8::exception &e)
-	{
-		throw love::Exception("UTF-8 decoding error: %s", e.what());
-	}
-
-	return getKerning(left, right);
-}
-
-void Font::getCodepointsFromString(const std::string &text, Codepoints &codepoints)
-{
-	codepoints.reserve(text.size());
-
-	try
-	{
-		utf8::iterator<std::string::const_iterator> i(text.begin(), text.begin(), text.end());
-		utf8::iterator<std::string::const_iterator> end(text.end(), text.begin(), text.end());
-
-		while (i != end)
-		{
-			uint32 g = *i++;
-			codepoints.push_back(g);
-		}
-	}
-	catch (utf8::exception &e)
-	{
-		throw love::Exception("UTF-8 decoding error: %s", e.what());
-	}
-}
-
-void Font::getCodepointsFromString(const std::vector<ColoredString> &strs, ColoredCodepoints &codepoints)
-{
-	if (strs.empty())
-		return;
-
-	codepoints.cps.reserve(strs[0].str.size());
-
-	for (const ColoredString &cstr : strs)
-	{
-		// No need to add the color if the string is empty anyway, and the code
-		// further on assumes no two colors share the same starting position.
-		if (cstr.str.size() == 0)
-			continue;
-
-		IndexedColor c = {cstr.color, (int) codepoints.cps.size()};
-		codepoints.colors.push_back(c);
-
-		getCodepointsFromString(cstr.str, codepoints.cps);
-	}
-
-	if (codepoints.colors.size() == 1)
-	{
-		IndexedColor c = codepoints.colors[0];
-
-		if (c.index == 0 && c.color == Colorf(1.0f, 1.0f, 1.0f, 1.0f))
-			codepoints.colors.pop_back();
-	}
+	return shaper->getKerning(leftchar, rightchar);
 }
 
 float Font::getHeight() const
 {
-	return (float) floorf(height / dpiScale + 0.5f);
+	return shaper->getHeight();
 }
 
-std::vector<Font::DrawCommand> Font::generateVertices(const ColoredCodepoints &codepoints, const Colorf &constantcolor, std::vector<GlyphVertex> &vertices, float extra_spacing, Vector2 offset, TextInfo *info)
+std::vector<Font::DrawCommand> Font::generateVertices(const love::font::ColoredCodepoints &codepoints, Range range, const Colorf &constantcolor, std::vector<GlyphVertex> &vertices, float extra_spacing, Vector2 offset, love::font::TextShaper::TextInfo *info)
 {
-	// Spacing counter and newline handling.
-	float dx = offset.x;
-	float dy = offset.y;
+	std::vector<love::font::TextShaper::GlyphPosition> glyphpositions;
+	std::vector<love::font::IndexedColor> colors;
+	shaper->computeGlyphPositions(codepoints, range, offset, extra_spacing, &glyphpositions, &colors, info);
 
-	float heightoffset = 0.0f;
+	size_t vertstartsize = vertices.size();
+	vertices.reserve(vertstartsize + glyphpositions.size() * 4);
 
-	if (rasterizers[0]->getDataType() == font::Rasterizer::DATA_TRUETYPE)
-		heightoffset = getBaseline();
+	Colorf linearconstantcolor = gammaCorrectColor(constantcolor);
+	Color32 curcolor = toColor32(constantcolor);
 
-	int maxwidth = 0;
+	int curcolori = 0;
+	int ncolors = (int)colors.size();
 
 	// Keeps track of when we need to switch textures in our vertex array.
 	std::vector<DrawCommand> commands;
 
-	// Pre-allocate space for the maximum possible number of vertices.
-	size_t vertstartsize = vertices.size();
-	vertices.reserve(vertstartsize + codepoints.cps.size() * 4);
+	for (int i = 0; i < (int) glyphpositions.size(); i++)
+	{
+		const auto &info = glyphpositions[i];
 
-	uint32 prevglyph = 0;
+		uint32 cacheid = textureCacheID;
 
-	Colorf linearconstantcolor = gammaCorrectColor(constantcolor);
+		const Glyph &glyph = findGlyph(info.glyphIndex);
 
-	Color32 curcolor = toColor32(constantcolor);
-	int curcolori = -1;
-	int ncolors = (int) codepoints.colors.size();
-
-	for (int i = 0; i < (int) codepoints.cps.size(); i++)
-	{
-		uint32 g = codepoints.cps[i];
+		// If findGlyph invalidates the texture cache, restart the loop.
+		if (cacheid != textureCacheID)
+		{
+			i = -1; // The next iteration will increment this to 0.
+			commands.clear();
+			vertices.resize(vertstartsize);
+			curcolori = 0;
+			curcolor = toColor32(constantcolor);
+			continue;
+		}
 
-		if (curcolori + 1 < ncolors && codepoints.colors[curcolori + 1].index == i)
+		if (curcolori < ncolors && colors[curcolori].index == i)
 		{
-			Colorf c = codepoints.colors[++curcolori].color;
+			Colorf c = colors[curcolori].color;
 
 			c.r = std::min(std::max(c.r, 0.0f), 1.0f);
 			c.g = std::min(std::max(c.g, 0.0f), 1.0f);
@@ -513,54 +437,17 @@ std::vector<Font::DrawCommand> Font::generateVertices(const ColoredCodepoints &c
 			unGammaCorrectColor(c);
 
 			curcolor = toColor32(c);
+			curcolori++;
 		}
 
-		if (g == '\n')
-		{
-			if (dx > maxwidth)
-				maxwidth = (int) dx;
-
-			// Wrap newline, but do not print it.
-			dy += floorf(getHeight() * getLineHeight() + 0.5f);
-			dx = offset.x;
-			prevglyph = 0;
-			continue;
-		}
-
-		// Ignore carriage returns
-		if (g == '\r')
-			continue;
-
-		uint32 cacheid = textureCacheID;
-
-		const Glyph &glyph = findGlyph(g);
-
-		// If findGlyph invalidates the texture cache, re-start the loop.
-		if (cacheid != textureCacheID)
-		{
-			i = -1; // The next iteration will increment this to 0.
-			maxwidth = 0;
-			dx = offset.x;
-			dy = offset.y;
-			commands.clear();
-			vertices.resize(vertstartsize);
-			prevglyph = 0;
-			curcolori = -1;
-			curcolor = toColor32(constantcolor);
-			continue;
-		}
-
-		// Add kerning to the current horizontal offset.
-		dx += getKerning(prevglyph, g);
-
 		if (glyph.texture != nullptr)
 		{
 			// Copy the vertices and set their colors and relative positions.
 			for (int j = 0; j < 4; j++)
 			{
 				vertices.push_back(glyph.vertices[j]);
-				vertices.back().x += dx;
-				vertices.back().y += dy + heightoffset;
+				vertices.back().x += info.position.x;
+				vertices.back().y += info.position.y;
 				vertices.back().color = curcolor;
 			}
 
@@ -569,7 +456,7 @@ std::vector<Font::DrawCommand> Font::generateVertices(const ColoredCodepoints &c
 			{
 				// Add a new draw command if the texture has changed.
 				DrawCommand cmd;
-				cmd.startvertex = (int) vertices.size() - 4;
+				cmd.startvertex = (int)vertices.size() - 4;
 				cmd.vertexcount = 0;
 				cmd.texture = glyph.texture;
 				commands.push_back(cmd);
@@ -577,15 +464,6 @@ std::vector<Font::DrawCommand> Font::generateVertices(const ColoredCodepoints &c
 
 			commands.back().vertexcount += 4;
 		}
-
-		// Advance the x position for the next glyph.
-		dx += glyph.spacing;
-
-		// Account for extra spacing given to space characters.
-		if (g == ' ' && extra_spacing != 0.0f)
-			dx = floorf(dx + extra_spacing);
-
-		prevglyph = g;
 	}
 
 	const auto drawsort = [](const DrawCommand &a, const DrawCommand &b) -> bool
@@ -599,19 +477,10 @@ std::vector<Font::DrawCommand> Font::generateVertices(const ColoredCodepoints &c
 
 	std::sort(commands.begin(), commands.end(), drawsort);
 
-	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 commands;
 }
 
-std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const ColoredCodepoints &text, const Colorf &constantcolor, float wrap, AlignMode align, std::vector<GlyphVertex> &vertices, TextInfo *info)
+std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const love::font::ColoredCodepoints &text, const Colorf &constantcolor, float wrap, AlignMode align, std::vector<GlyphVertex> &vertices, love::font::TextShaper::TextInfo *info)
 {
 	wrap = std::max(wrap, 0.0f);
 
@@ -620,17 +489,22 @@ std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const ColoredCode
 	std::vector<DrawCommand> drawcommands;
 	vertices.reserve(text.cps.size() * 4);
 
+	std::vector<Range> ranges;
 	std::vector<int> widths;
-	std::vector<ColoredCodepoints> lines;
-
-	getWrap(text, wrap, lines, &widths);
+	shaper->getWrap(text, wrap, ranges, &widths);
 
 	float y = 0.0f;
 	float maxwidth = 0.0f;
 
-	for (int i = 0; i < (int) lines.size(); i++)
+	for (int i = 0; i < (int)ranges.size(); i++)
 	{
-		const auto &line = lines[i];
+		const auto& range = ranges[i];
+
+		if (!range.isValid())
+		{
+			y += getHeight() * getLineHeight();
+			continue;
+		}
 
 		float width = (float) widths[i];
 		love::Vector2 offset(0.0f, floorf(y));
@@ -648,7 +522,9 @@ std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const ColoredCode
 				break;
 			case ALIGN_JUSTIFY:
 			{
-				float numspaces = (float) std::count(line.cps.begin(), line.cps.end(), ' ');
+				auto start = text.cps.begin() + range.getOffset();
+				auto end = start + range.getSize();
+				float numspaces = std::count(start, end, ' ');
 				if (width < wrap && numspaces >= 1)
 					extraspacing = (wrap - width) / numspaces;
 				else
@@ -660,7 +536,7 @@ std::vector<Font::DrawCommand> Font::generateVerticesFormatted(const ColoredCode
 				break;
 		}
 
-		std::vector<DrawCommand> newcommands = generateVertices(line, constantcolor, vertices, extraspacing, offset);
+		std::vector<DrawCommand> newcommands = generateVertices(text, range, constantcolor, vertices, extraspacing, offset);
 
 		if (!newcommands.empty())
 		{
@@ -724,21 +600,21 @@ void Font::printv(graphics::Graphics *gfx, const Matrix4 &t, const std::vector<D
 	}
 }
 
-void Font::print(graphics::Graphics *gfx, const std::vector<ColoredString> &text, const Matrix4 &m, const Colorf &constantcolor)
+void Font::print(graphics::Graphics *gfx, const std::vector<love::font::ColoredString> &text, const Matrix4 &m, const Colorf &constantcolor)
 {
-	ColoredCodepoints codepoints;
-	getCodepointsFromString(text, codepoints);
+	love::font::ColoredCodepoints codepoints;
+	love::font::getCodepointsFromString(text, codepoints);
 
 	std::vector<GlyphVertex> vertices;
-	std::vector<DrawCommand> drawcommands = generateVertices(codepoints, constantcolor, vertices);
+	std::vector<DrawCommand> drawcommands = generateVertices(codepoints, Range(), constantcolor, vertices);
 
 	printv(gfx, m, drawcommands, vertices);
 }
 
-void Font::printf(graphics::Graphics *gfx, const std::vector<ColoredString> &text, float wrap, AlignMode align, const Matrix4 &m, const Colorf &constantcolor)
+void Font::printf(graphics::Graphics *gfx, const std::vector<love::font::ColoredString> &text, float wrap, AlignMode align, const Matrix4 &m, const Colorf &constantcolor)
 {
-	ColoredCodepoints codepoints;
-	getCodepointsFromString(text, codepoints);
+	love::font::ColoredCodepoints codepoints;
+	love::font::getCodepointsFromString(text, codepoints);
 
 	std::vector<GlyphVertex> vertices;
 	std::vector<DrawCommand> drawcommands = generateVerticesFormatted(codepoints, constantcolor, wrap, align, vertices);
@@ -748,241 +624,32 @@ void Font::printf(graphics::Graphics *gfx, const std::vector<ColoredString> &tex
 
 int Font::getWidth(const std::string &str)
 {
-	if (str.size() == 0) return 0;
-
-	std::istringstream iss(str);
-	std::string line;
-	int max_width = 0;
-
-	while (getline(iss, line, '\n'))
-	{
-		int width = 0;
-		uint32 prevglyph = 0;
-		try
-		{
-			utf8::iterator<std::string::const_iterator> i(line.begin(), line.begin(), line.end());
-			utf8::iterator<std::string::const_iterator> end(line.end(), line.begin(), line.end());
-
-			while (i != end)
-			{
-				uint32 c = *i++;
-
-				// Ignore carriage returns
-				if (c == '\r')
-					continue;
-
-				const Glyph &g = findGlyph(c);
-				width += g.spacing + getKerning(prevglyph, c);
-
-				prevglyph = c;
-			}
-		}
-		catch (utf8::exception &e)
-		{
-			throw love::Exception("UTF-8 decoding error: %s", e.what());
-		}
-
-		max_width = std::max(max_width, width);
-	}
-
-	return max_width;
+	return shaper->getWidth(str);
 }
 
 int Font::getWidth(uint32 glyph)
 {
-	const Glyph &g = findGlyph(glyph);
-	return g.spacing;
+	return shaper->getGlyphAdvance(glyph);
 }
 
-void Font::getWrap(const ColoredCodepoints &codepoints, float wraplimit, std::vector<ColoredCodepoints> &lines, std::vector<int> *linewidths)
+void Font::getWrap(const love::font::ColoredCodepoints &codepoints, float wraplimit, std::vector<Range> &ranges, std::vector<int> *linewidths)
 {
-	// Per-line info.
-	float width = 0.0f;
-	float widthbeforelastspace = 0.0f;
-	float widthoftrailingspace = 0.0f;
-	uint32 prevglyph = 0;
-
-	int lastspaceindex = -1;
-
-	// Keeping the indexed colors "in sync" is a bit tricky, since we split
-	// things up and we might skip some glyphs but we don't want to skip any
-	// color which starts at those indices.
-	Colorf curcolor(1.0f, 1.0f, 1.0f, 1.0f);
-	bool addcurcolor = false;
-	int curcolori = -1;
-	int endcolori = (int) codepoints.colors.size() - 1;
-
-	// A wrapped line of text.
-	ColoredCodepoints wline;
-
-	int i = 0;
-	while (i < (int) codepoints.cps.size())
-	{
-		uint32 c = codepoints.cps[i];
-
-		// Determine the current color before doing anything else, to make sure
-		// it's still applied to future glyphs even if this one is skipped.
-		if (curcolori < endcolori && codepoints.colors[curcolori + 1].index == i)
-		{
-			curcolor = codepoints.colors[curcolori + 1].color;
-			curcolori++;
-			addcurcolor = true;
-		}
-
-		// Split text at newlines.
-		if (c == '\n')
-		{
-			lines.push_back(wline);
-
-			// Ignore the width of any trailing spaces, for individual lines.
-			if (linewidths)
-				linewidths->push_back(width - widthoftrailingspace);
-
-			// Make sure the new line keeps any color that was set previously.
-			addcurcolor = true;
-
-			width = widthbeforelastspace = widthoftrailingspace = 0.0f;
-			prevglyph = 0; // Reset kerning information.
-			lastspaceindex = -1;
-			wline.cps.clear();
-			wline.colors.clear();
-			i++;
-
-			continue;
-		}
-
-		// Ignore carriage returns
-		if (c == '\r')
-		{
-			i++;
-			continue;
-		}
-
-		const Glyph &g = findGlyph(c);
-		float charwidth = g.spacing + getKerning(prevglyph, c);
-		float newwidth = width + charwidth;
-
-		// Wrap the line if it exceeds the wrap limit. Don't wrap yet if we're
-		// processing a newline character, though.
-		if (c != ' ' && newwidth > wraplimit)
-		{
-			// If this is the first character in the line and it exceeds the
-			// limit, skip it completely.
-			if (wline.cps.empty())
-				i++;
-			else if (lastspaceindex != -1)
-			{
-				// 'Rewind' to the last seen space, if the line has one.
-				// FIXME: This could be more efficient...
-				while (!wline.cps.empty() && wline.cps.back() != ' ')
-					wline.cps.pop_back();
-
-				while (!wline.colors.empty() && wline.colors.back().index >= (int) wline.cps.size())
-					wline.colors.pop_back();
-
-				// Also 'rewind' to the color that the last character is using.
-				for (int colori = curcolori; colori >= 0; colori--)
-				{
-					if (codepoints.colors[colori].index <= lastspaceindex)
-					{
-						curcolor = codepoints.colors[colori].color;
-						curcolori = colori;
-						break;
-					}
-				}
-
-				// Ignore the width of trailing spaces in wrapped lines.
-				width = widthbeforelastspace;
-
-				i = lastspaceindex;
-				i++; // Start the next line after the space.
-			}
-
-			lines.push_back(wline);
-
-			if (linewidths)
-				linewidths->push_back(width);
-
-			addcurcolor = true;
-
-			prevglyph = 0;
-			width = widthbeforelastspace = widthoftrailingspace = 0.0f;
-			wline.cps.clear();
-			wline.colors.clear();
-			lastspaceindex = -1;
-
-			continue;
-		}
-
-		if (prevglyph != ' ' && c == ' ')
-			widthbeforelastspace = width;
-
-		width = newwidth;
-		prevglyph = c;
-
-		if (addcurcolor)
-		{
-			wline.colors.push_back({curcolor, (int) wline.cps.size()});
-			addcurcolor = false;
-		}
-
-		wline.cps.push_back(c);
-
-		// Keep track of the last seen space, so we can "rewind" to it when
-		// wrapping.
-		if (c == ' ')
-		{
-			lastspaceindex = i;
-			widthoftrailingspace += charwidth;
-		}
-		else if (c != '\n')
-			widthoftrailingspace = 0.0f;
-
-		i++;
-	}
-
-	// Push the last line.
-	lines.push_back(wline);
-
-	// Ignore the width of any trailing spaces, for individual lines.
-	if (linewidths)
-		linewidths->push_back(width - widthoftrailingspace);
+	shaper->getWrap(codepoints, wraplimit, ranges, linewidths);
 }
 
-void Font::getWrap(const std::vector<ColoredString> &text, float wraplimit, std::vector<std::string> &lines, std::vector<int> *linewidths)
+void Font::getWrap(const std::vector<love::font::ColoredString> &text, float wraplimit, std::vector<std::string> &lines, std::vector<int> *linewidths)
 {
-	ColoredCodepoints cps;
-	getCodepointsFromString(text, cps);
-
-	std::vector<ColoredCodepoints> codepointlines;
-	getWrap(cps, wraplimit, codepointlines, linewidths);
-
-	std::string line;
-
-	for (const ColoredCodepoints &codepoints : codepointlines)
-	{
-		line.clear();
-		line.reserve(codepoints.cps.size());
-
-		for (uint32 codepoint : codepoints.cps)
-		{
-			char character[5] = {'\0'};
-			char *end = utf8::unchecked::append(codepoint, character);
-			line.append(character, end - character);
-		}
-
-		lines.push_back(line);
-	}
+	shaper->getWrap(text, wraplimit, lines, linewidths);
 }
 
 void Font::setLineHeight(float height)
 {
-	lineHeight = height;
+	shaper->setLineHeight(height);
 }
 
 float Font::getLineHeight() const
 {
-	return lineHeight;
+	return shaper->getLineHeight();
 }
 
 void Font::setSamplerState(const SamplerState &s)
@@ -1002,75 +669,44 @@ const SamplerState &Font::getSamplerState() const
 
 int Font::getAscent() const
 {
-	return floorf(rasterizers[0]->getAscent() / dpiScale + 0.5f);
+	return shaper->getAscent();
 }
 
 int Font::getDescent() const
 {
-	return floorf(rasterizers[0]->getDescent() / dpiScale + 0.5f);
+	return shaper->getDescent();
 }
 
 float Font::getBaseline() const
 {
-	float ascent = getAscent();
-	if (ascent != 0.0f)
-		return ascent;
-	else if (rasterizers[0]->getDataType() == font::Rasterizer::DATA_TRUETYPE)
-		return floorf(getHeight() / 1.25f + 0.5f); // 1.25 is magic line height for true type fonts
-	else
-		return 0.0f;
+	return shaper->getBaseline();
 }
 
 bool Font::hasGlyph(uint32 glyph) const
 {
-	for (const StrongRef<love::font::Rasterizer> &r : rasterizers)
-	{
-		if (r->hasGlyph(glyph))
-			return true;
-	}
-
-	return false;
+	return shaper->hasGlyph(glyph);
 }
 
 bool Font::hasGlyphs(const std::string &text) const
 {
-	if (text.size() == 0)
-		return false;
-
-	try
-	{
-		utf8::iterator<std::string::const_iterator> i(text.begin(), text.begin(), text.end());
-		utf8::iterator<std::string::const_iterator> end(text.end(), text.begin(), text.end());
-		
-		while (i != end)
-		{
-			uint32 codepoint = *i++;
-			
-			if (!hasGlyph(codepoint))
-				return false;
-		}
-	}
-	catch (utf8::exception &e)
-	{
-		throw love::Exception("UTF-8 decoding error: %s", e.what());
-	}
-
-	return true;
+	return shaper->hasGlyphs(text);
 }
 
 void Font::setFallbacks(const std::vector<Font *> &fallbacks)
 {
-	for (const Font *f : fallbacks)
-	{
-		if (f->rasterizers[0]->getDataType() != this->rasterizers[0]->getDataType())
-			throw love::Exception("Font fallbacks must be of the same font type.");
-	}
+	std::vector<love::font::Rasterizer*> rasterizerfallbacks;
+	for (const Font* f : fallbacks)
+		rasterizerfallbacks.push_back(f->shaper->getRasterizers()[0]);
+
+	shaper->setFallbacks(rasterizerfallbacks);
 
-	rasterizers.resize(1);
+	// Invalidate existing textures.
+	textureCacheID++;
+	glyphs.clear();
+	while (textures.size() > 1)
+		textures.pop_back();
 
-	// NOTE: this won't invalidate already-rasterized glyphs.
-	for (const Font *f : fallbacks)
-		rasterizers.push_back(f->rasterizers[0]);
+	rowHeight = textureX = textureY = TEXTURE_PADDING;
 }
 
 float Font::getDPIScale() const

+ 16 - 49
src/modules/graphics/Font.h

@@ -33,6 +33,7 @@
 #include "common/Vector.h"
 
 #include "font/Rasterizer.h"
+#include "font/TextShaper.h"
 #include "Texture.h"
 #include "vertex.h"
 #include "Volatile.h"
@@ -64,30 +65,6 @@ public:
 		ALIGN_MAX_ENUM
 	};
 
-	struct ColoredString
-	{
-		std::string str;
-		Colorf color;
-	};
-
-	struct IndexedColor
-	{
-		Colorf color;
-		int index;
-	};
-
-	struct ColoredCodepoints
-	{
-		std::vector<uint32> cps;
-		std::vector<IndexedColor> colors;
-	};
-
-	struct TextInfo
-	{
-		int width;
-		int height;
-	};
-
 	// Used to determine when to change textures in the generated vertex array.
 	struct DrawCommand
 	{
@@ -100,20 +77,17 @@ public:
 
 	virtual ~Font();
 
-	std::vector<DrawCommand> generateVertices(const ColoredCodepoints &codepoints, const Colorf &constantColor, std::vector<GlyphVertex> &vertices,
-	                                          float extra_spacing = 0.0f, Vector2 offset = {}, TextInfo *info = nullptr);
-
-	std::vector<DrawCommand> generateVerticesFormatted(const ColoredCodepoints &text, const Colorf &constantColor, float wrap, AlignMode align,
-	                                                   std::vector<GlyphVertex> &vertices, TextInfo *info = nullptr);
+	std::vector<DrawCommand> generateVertices(const love::font::ColoredCodepoints &codepoints, Range range, const Colorf &constantColor, std::vector<GlyphVertex> &vertices,
+	                                          float extra_spacing = 0.0f, Vector2 offset = {}, love::font::TextShaper::TextInfo *info = nullptr);
 
-	static void getCodepointsFromString(const std::string &str, Codepoints &codepoints);
-	static void getCodepointsFromString(const std::vector<ColoredString> &strs, ColoredCodepoints &codepoints);
+	std::vector<DrawCommand> generateVerticesFormatted(const love::font::ColoredCodepoints &text, const Colorf &constantColor, float wrap, AlignMode align,
+	                                                   std::vector<GlyphVertex> &vertices, love::font::TextShaper::TextInfo *info = nullptr);
 
 	/**
 	 * Draws the specified text.
 	 **/
-	void print(graphics::Graphics *gfx, const std::vector<ColoredString> &text, const Matrix4 &m, const Colorf &constantColor);
-	void printf(graphics::Graphics *gfx, const std::vector<ColoredString> &text, float wrap, AlignMode align, const Matrix4 &m, const Colorf &constantColor);
+	void print(graphics::Graphics *gfx, const std::vector<love::font::ColoredString> &text, const Matrix4 &m, const Colorf &constantColor);
+	void printf(graphics::Graphics *gfx, const std::vector<love::font::ColoredString> &text, float wrap, AlignMode align, const Matrix4 &m, const Colorf &constantColor);
 
 	/**
 	 * Returns the height of the font.
@@ -141,8 +115,8 @@ public:
 	 * @param max_width Optional output of the maximum width
 	 * Returns a vector with the lines.
 	 **/
-	void getWrap(const std::vector<ColoredString> &text, float wraplimit, std::vector<std::string> &lines, std::vector<int> *line_widths = nullptr);
-	void getWrap(const ColoredCodepoints &codepoints, float wraplimit, std::vector<ColoredCodepoints> &lines, std::vector<int> *line_widths = nullptr);
+	void getWrap(const std::vector<love::font::ColoredString> &text, float wraplimit, std::vector<std::string> &lines, std::vector<int> *line_widths = nullptr);
+	void getWrap(const love::font::ColoredCodepoints &codepoints, float wraplimit, std::vector<Range> &ranges, std::vector<int> *line_widths = nullptr);
 
 	/**
 	 * Sets the line height (which should be a number to multiply the font size by,
@@ -191,7 +165,6 @@ private:
 	struct Glyph
 	{
 		Texture *texture;
-		int spacing;
 		GlyphVertex vertices[4];
 	};
 
@@ -204,26 +177,20 @@ private:
 	void createTexture();
 
 	TextureSize getNextTextureSize() const;
-	love::font::GlyphData *getRasterizerGlyphData(uint32 glyph, float &dpiscale);
-	const Glyph &addGlyph(uint32 glyph);
-	const Glyph &findGlyph(uint32 glyph);
+	love::font::GlyphData *getRasterizerGlyphData(love::font::TextShaper::GlyphIndex glyphindex, float &dpiscale);
+	const Glyph &addGlyph(love::font::TextShaper::GlyphIndex glyphindex);
+	const Glyph &findGlyph(love::font::TextShaper::GlyphIndex glyphindex);
 	void printv(Graphics *gfx, const Matrix4 &t, const std::vector<DrawCommand> &drawcommands, const std::vector<GlyphVertex> &vertices);
 
-	std::vector<StrongRef<love::font::Rasterizer>> rasterizers;
-
-	int height;
-	float lineHeight;
+	StrongRef<love::font::TextShaper> shaper;
 
 	int textureWidth;
 	int textureHeight;
 
-	std::vector<StrongRef<love::graphics::Texture>> textures;
-
-	// maps glyphs to glyph texture information
-	std::unordered_map<uint32, Glyph> glyphs;
+	std::vector<StrongRef<Texture>> textures;
 
-	// map of left/right glyph pairs to horizontal kerning.
-	std::unordered_map<uint64, float> kerning;
+	// maps packed glyph index values to glyph texture information
+	std::unordered_map<uint64, Glyph> glyphs;
 
 	PixelFormat pixelFormat;
 

+ 5 - 5
src/modules/graphics/Graphics.cpp

@@ -439,7 +439,7 @@ Mesh *Graphics::newMesh(const std::vector<Mesh::BufferAttribute> &attributes, Pr
 	return new Mesh(attributes, drawmode);
 }
 
-love::graphics::TextBatch *Graphics::newTextBatch(graphics::Font *font, const std::vector<Font::ColoredString> &text)
+love::graphics::TextBatch *Graphics::newTextBatch(graphics::Font *font, const std::vector<love::font::ColoredString> &text)
 {
 	return new TextBatch(font, text);
 }
@@ -1893,7 +1893,7 @@ void Graphics::drawShaderVertices(Buffer *indexbuffer, int indexcount, int insta
 	draw(cmd);
 }
 
-void Graphics::print(const std::vector<Font::ColoredString> &str, const Matrix4 &m)
+void Graphics::print(const std::vector<love::font::ColoredString> &str, const Matrix4 &m)
 {
 	checkSetDefaultFont();
 
@@ -1901,12 +1901,12 @@ void Graphics::print(const std::vector<Font::ColoredString> &str, const Matrix4
 		print(str, states.back().font.get(), m);
 }
 
-void Graphics::print(const std::vector<Font::ColoredString> &str, Font *font, const Matrix4 &m)
+void Graphics::print(const std::vector<love::font::ColoredString> &str, Font *font, const Matrix4 &m)
 {
 	font->print(this, str, m, states.back().color);
 }
 
-void Graphics::printf(const std::vector<Font::ColoredString> &str, float wrap, Font::AlignMode align, const Matrix4 &m)
+void Graphics::printf(const std::vector<love::font::ColoredString> &str, float wrap, Font::AlignMode align, const Matrix4 &m)
 {
 	checkSetDefaultFont();
 
@@ -1914,7 +1914,7 @@ void Graphics::printf(const std::vector<Font::ColoredString> &str, float wrap, F
 		printf(str, states.back().font.get(), wrap, align, m);
 }
 
-void Graphics::printf(const std::vector<Font::ColoredString> &str, Font *font, float wrap, Font::AlignMode align, const Matrix4 &m)
+void Graphics::printf(const std::vector<love::font::ColoredString> &str, Font *font, float wrap, Font::AlignMode align, const Matrix4 &m)
 {
 	font->printf(this, str, wrap, align, m, states.back().color);
 }

+ 5 - 5
src/modules/graphics/Graphics.h

@@ -460,7 +460,7 @@ public:
 	Mesh *newMesh(const std::vector<Buffer::DataDeclaration> &vertexformat, const void *data, size_t datasize, PrimitiveType drawmode, BufferDataUsage usage);
 	Mesh *newMesh(const std::vector<Mesh::BufferAttribute> &attributes, PrimitiveType drawmode);
 
-	TextBatch *newTextBatch(Font *font, const std::vector<Font::ColoredString> &text = {});
+	TextBatch *newTextBatch(Font *font, const std::vector<love::font::ColoredString> &text = {});
 
 	data::ByteData *readbackBuffer(Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset);
 	GraphicsReadback *readbackBufferAsync(Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset);
@@ -702,14 +702,14 @@ public:
 	/**
 	 * Draws text at the specified coordinates
 	 **/
-	void print(const std::vector<Font::ColoredString> &str, const Matrix4 &m);
-	void print(const std::vector<Font::ColoredString> &str, Font *font, const Matrix4 &m);
+	void print(const std::vector<love::font::ColoredString> &str, const Matrix4 &m);
+	void print(const std::vector<love::font::ColoredString> &str, Font *font, const Matrix4 &m);
 
 	/**
 	 * Draws formatted text on screen at the specified coordinates.
 	 **/
-	void printf(const std::vector<Font::ColoredString> &str, float wrap, Font::AlignMode align, const Matrix4 &m);
-	void printf(const std::vector<Font::ColoredString> &str, Font *font, float wrap, Font::AlignMode align, const Matrix4 &m);
+	void printf(const std::vector<love::font::ColoredString> &str, float wrap, Font::AlignMode align, const Matrix4 &m);
+	void printf(const std::vector<love::font::ColoredString> &str, Font *font, float wrap, Font::AlignMode align, const Matrix4 &m);
 
 	/**
 	 * Draws a series of points at the specified positions.

+ 11 - 11
src/modules/graphics/TextBatch.cpp

@@ -30,7 +30,7 @@ namespace graphics
 
 love::Type TextBatch::type("TextBatch", &Drawable::type);
 
-TextBatch::TextBatch(Font *font, const std::vector<Font::ColoredString> &text)
+TextBatch::TextBatch(Font *font, const std::vector<love::font::ColoredString> &text)
 	: font(font)
 	, vertexAttributes(Font::vertexFormat, 0)
 	, vertexData(nullptr)
@@ -112,13 +112,13 @@ void TextBatch::addTextData(const TextData &t)
 	std::vector<Font::GlyphVertex> vertices;
 	std::vector<Font::DrawCommand> newcommands;
 
-	Font::TextInfo textinfo;
+	love::font::TextShaper::TextInfo textinfo;
 
 	Colorf constantcolor = Colorf(1.0f, 1.0f, 1.0f, 1.0f);
 
 	// We only have formatted text if the align mode is valid.
 	if (t.align == Font::ALIGN_MAX_ENUM)
-		newcommands = font->generateVertices(t.codepoints, constantcolor, vertices, 0.0f, Vector2(0.0f, 0.0f), &textinfo);
+		newcommands = font->generateVertices(t.codepoints, Range(), constantcolor, vertices, 0.0f, Vector2(0.0f, 0.0f), &textinfo);
 	else
 		newcommands = font->generateVerticesFormatted(t.codepoints, constantcolor, t.wrap, t.align, vertices, &textinfo);
 
@@ -172,31 +172,31 @@ void TextBatch::addTextData(const TextData &t)
 		regenerateVertices();
 }
 
-void TextBatch::set(const std::vector<Font::ColoredString> &text)
+void TextBatch::set(const std::vector<love::font::ColoredString> &text)
 {
 	return set(text, -1.0f, Font::ALIGN_MAX_ENUM);
 }
 
-void TextBatch::set(const std::vector<Font::ColoredString> &text, float wrap, Font::AlignMode align)
+void TextBatch::set(const std::vector<love::font::ColoredString> &text, float wrap, Font::AlignMode align)
 {
 	if (text.empty() || (text.size() == 1 && text[0].str.empty()))
 		return clear();
 
-	Font::ColoredCodepoints codepoints;
-	Font::getCodepointsFromString(text, codepoints);
+	love::font::ColoredCodepoints codepoints;
+	love::font::getCodepointsFromString(text, codepoints);
 
 	addTextData({codepoints, wrap, align, {}, false, false, Matrix4()});
 }
 
-int TextBatch::add(const std::vector<Font::ColoredString> &text, const Matrix4 &m)
+int TextBatch::add(const std::vector<love::font::ColoredString> &text, const Matrix4 &m)
 {
 	return addf(text, -1.0f, Font::ALIGN_MAX_ENUM, m);
 }
 
-int TextBatch::addf(const std::vector<Font::ColoredString> &text, float wrap, Font::AlignMode align, const Matrix4 &m)
+int TextBatch::addf(const std::vector<love::font::ColoredString> &text, float wrap, Font::AlignMode align, const Matrix4 &m)
 {
-	Font::ColoredCodepoints codepoints;
-	Font::getCodepointsFromString(text, codepoints);
+	love::font::ColoredCodepoints codepoints;
+	love::font::getCodepointsFromString(text, codepoints);
 
 	addTextData({codepoints, wrap, align, {}, true, true, m});
 

+ 7 - 7
src/modules/graphics/TextBatch.h

@@ -40,14 +40,14 @@ public:
 
 	static love::Type type;
 
-	TextBatch(Font *font, const std::vector<Font::ColoredString> &text = {});
+	TextBatch(Font *font, const std::vector<love::font::ColoredString> &text = {});
 	virtual ~TextBatch();
 
-	void set(const std::vector<Font::ColoredString> &text);
-	void set(const std::vector<Font::ColoredString> &text, float wrap, Font::AlignMode align);
+	void set(const std::vector<love::font::ColoredString> &text);
+	void set(const std::vector<love::font::ColoredString> &text, float wrap, Font::AlignMode align);
 
-	int add(const std::vector<Font::ColoredString> &text, const Matrix4 &m);
-	int addf(const std::vector<Font::ColoredString> &text, float wrap, Font::AlignMode align, const Matrix4 &m);
+	int add(const std::vector<love::font::ColoredString> &text, const Matrix4 &m);
+	int addf(const std::vector<love::font::ColoredString> &text, float wrap, Font::AlignMode align, const Matrix4 &m);
 
 	void clear();
 
@@ -71,10 +71,10 @@ private:
 
 	struct TextData
 	{
-		Font::ColoredCodepoints codepoints;
+		love::font::ColoredCodepoints codepoints;
 		float wrap;
 		Font::AlignMode align;
-		Font::TextInfo textInfo;
+		love::font::TextShaper::TextInfo textInfo;
 		bool useMatrix;
 		bool appendVertices;
 		Matrix4 matrix;

+ 3 - 3
src/modules/graphics/wrap_Font.cpp

@@ -30,9 +30,9 @@ namespace love
 namespace graphics
 {
 
-void luax_checkcoloredstring(lua_State *L, int idx, std::vector<Font::ColoredString> &strings)
+void luax_checkcoloredstring(lua_State *L, int idx, std::vector<love::font::ColoredString> &strings)
 {
-	Font::ColoredString coloredstr;
+	love::font::ColoredString coloredstr;
 	coloredstr.color = Colorf(1.0f, 1.0f, 1.0f, 1.0f);
 
 	if (lua_istable(L, idx))
@@ -103,7 +103,7 @@ int w_Font_getWrap(lua_State *L)
 {
 	Font *t = luax_checkfont(L, 1);
 
-	std::vector<Font::ColoredString> text;
+	std::vector<love::font::ColoredString> text;
 	luax_checkcoloredstring(L, 2, text);
 
 	float wrap = (float) luaL_checknumber(L, 3);

+ 1 - 1
src/modules/graphics/wrap_Font.h

@@ -30,7 +30,7 @@ namespace graphics
 {
 
 Font *luax_checkfont(lua_State *L, int idx);
-void luax_checkcoloredstring(lua_State *L, int idx, std::vector<Font::ColoredString> &strings);
+void luax_checkcoloredstring(lua_State *L, int idx, std::vector<love::font::ColoredString> &strings);
 extern "C" int luaopen_font(lua_State *L);
 
 } // graphics

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

@@ -2112,7 +2112,7 @@ int w_newTextBatch(lua_State *L)
 		luax_catchexcept(L, [&](){ t = instance()->newTextBatch(font); });
 	else
 	{
-		std::vector<Font::ColoredString> text;
+		std::vector<love::font::ColoredString> text;
 		luax_checkcoloredstring(L, 2, text);
 
 		luax_catchexcept(L, [&](){ t = instance()->newTextBatch(font, text); });
@@ -3132,7 +3132,7 @@ int w_drawShaderVertices(lua_State *L)
 
 int w_print(lua_State *L)
 {
-	std::vector<Font::ColoredString> str;
+	std::vector<love::font::ColoredString> str;
 	luax_checkcoloredstring(L, 1, str);
 
 	if (luax_istype(L, 2, Font::type))
@@ -3157,7 +3157,7 @@ int w_print(lua_State *L)
 
 int w_printf(lua_State *L)
 {
-	std::vector<Font::ColoredString> str;
+	std::vector<love::font::ColoredString> str;
 	luax_checkcoloredstring(L, 1, str);
 
 	Font *font = nullptr;

+ 4 - 4
src/modules/graphics/wrap_TextBatch.cpp

@@ -36,7 +36,7 @@ int w_TextBatch_set(lua_State *L)
 {
 	TextBatch *t = luax_checktextbatch(L, 1);
 
-	std::vector<Font::ColoredString> newtext;
+	std::vector<love::font::ColoredString> newtext;
 	luax_checkcoloredstring(L, 2, newtext);
 
 	luax_catchexcept(L, [&](){ t->set(newtext); });
@@ -54,7 +54,7 @@ int w_TextBatch_setf(lua_State *L)
 	if (!Font::getConstant(alignstr, align))
 		return luax_enumerror(L, "align mode", Font::getConstants(align), alignstr);
 
-	std::vector<Font::ColoredString> newtext;
+	std::vector<love::font::ColoredString> newtext;
 	luax_checkcoloredstring(L, 2, newtext);
 
 	luax_catchexcept(L, [&](){ t->set(newtext, wraplimit, align); });
@@ -68,7 +68,7 @@ int w_TextBatch_add(lua_State *L)
 
 	int index = 0;
 
-	std::vector<Font::ColoredString> text;
+	std::vector<love::font::ColoredString> text;
 	luax_checkcoloredstring(L, 2, text);
 
 	if (luax_istype(L, 3, math::Transform::type))
@@ -102,7 +102,7 @@ int w_TextBatch_addf(lua_State *L)
 
 	int index = 0;
 
-	std::vector<Font::ColoredString> text;
+	std::vector<love::font::ColoredString> text;
 	luax_checkcoloredstring(L, 2, text);
 
 	float wrap = (float) luaL_checknumber(L, 3);