Browse Source

Merge branch 'text-shaping' into 12.0-development

Sasha Szpakowski 2 years ago
parent
commit
8572d4f935

+ 9 - 0
CMakeLists.txt

@@ -103,6 +103,7 @@ if(MEGA)
 
 	set(LOVE_LINK_LIBRARIES
 		${MEGA_FREETYPE}
+		${MEGA_HARFBUZZ}
 		${MEGA_LIBOGG}
 		${MEGA_LIBVORBISFILE}
 		${MEGA_LIBVORBIS}
@@ -173,6 +174,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)
@@ -200,6 +202,7 @@ Please see https://github.com/love2d/megasource
 		${OPENGL_gl_LIBRARY}
 		${SDL2_LIBRARY}
 		${FREETYPE_LIBRARY}
+		${HARFBUZZ_LIBRARY}
 		${OPENAL_LIBRARY}
 		${MODPLUG_LIBRARY}
 		${THEORA_LIBRARY}
@@ -477,12 +480,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
@@ -496,6 +503,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
 )

+ 4 - 1
platform/unix/configure.ac

@@ -67,7 +67,10 @@ ACLOVE_DEP_PTHREAD
 
 # Conditional dependencies
 AS_VAR_IF([enable_module_audio], [yes], [ACLOVE_DEP_OPENAL], [])
-AS_VAR_IF([enable_module_font], [yes], [ACLOVE_DEP_FREETYPE2], [])
+AS_VAR_IF([enable_module_font], [yes], [
+	ACLOVE_DEP_FREETYPE2
+	ACLOVE_DEP_HARFBUZZ
+], [])
 AS_VAR_IF([enable_module_sound], [yes], [
 	ACLOVE_DEP_LIBMODPLUG
 	ACLOVE_DEP_VORBISFILE

+ 2 - 1
platform/unix/debian/control.in

@@ -8,12 +8,13 @@ Build-Depends: debhelper (>= 9),
                libtool,
                g++ (>= 4.7.0),
                libfreetype6-dev,
+               libharfbuzz-dev,
                luajit,
                libluajit-5.1-dev,
                libmodplug-dev,
                libopenal-dev,
                libphysfs-dev,
-               libsdl2-dev (>= 2.0.1),
+               libsdl2-dev (>= 2.0.9),
                libogg-dev,
                libvorbis-dev,
                libtheora-dev,

+ 3 - 0
platform/unix/deps.m4

@@ -1,6 +1,9 @@
 AC_DEFUN([ACLOVE_DEP_FREETYPE2], [
 	PKG_CHECK_MODULES([freetype2], [freetype2], [], [LOVE_MSG_ERROR([FreeType2])])])
 
+AC_DEFUN([ACLOVE_DEP_HARFBUZZ], [
+	PKG_CHECK_MODULES([harfbuzz], [harfbuzz], [], [LOVE_MSG_ERROR([Harfbuzz])])])
+
 AC_DEFUN([ACLOVE_DEP_OPENAL], [
 	PKG_CHECK_MODULES([openal], [openal], [], [LOVE_MSG_ERROR([OpenAL])])])
 

+ 2 - 2
platform/unix/genmodules

@@ -107,7 +107,7 @@ inc_libraries="$inc_current/libraries"
 
 cat > src/Makefile.am << EOF
 AM_CPPFLAGS = -I$inc_current -I$inc_modules -I$inc_libraries -I$inc_libraries/enet/libenet/include -I$inc_libraries/box2d \$(LOVE_INCLUDES) \$(FILE_OFFSET)\
-	\$(SDL_CFLAGS) \$(lua_CFLAGS) \$(freetype2_CFLAGS)\
+	\$(SDL_CFLAGS) \$(lua_CFLAGS) \$(freetype2_CFLAGS) \$(harfbuzz_CFLAGS)\
 	\$(openal_CFLAGS) \$(zlib_CFLAGS) \$(libmodplug_CFLAGS)\
 	\$(vorbisfile_CFLAGS) \$(theora_CFLAGS)
 AUTOMAKE_OPTIONS = subdir-objects
@@ -139,7 +139,7 @@ endif
 lib_LTLIBRARIES = liblove${love_suffix}.la
 liblove${love_amsuffix}_la_LDFLAGS = -module -export-dynamic \$(LDFLAGS) -release \$(PACKAGE_VERSION)
 liblove${love_amsuffix}_la_LIBADD = \
-	\$(SDL_LIBS) \$(freetype2_LIBS) \$(lua_LIBS)\
+	\$(SDL_LIBS) \$(freetype2_LIBS) \$(harfbuzz_LIBS) \$(lua_LIBS)\
 	\$(openal_LIBS) \$(zlib_LIBS) \$(libmodplug_LIBS)\
 	\$(vorbisfile_LIBS) \$(theora_LIBS)
 

+ 41 - 0
platform/xcode/liblove.xcodeproj/project.pbxproj

@@ -50,6 +50,17 @@
 		217DFC101D9F6D490055D849 /* url.lua.h in Headers */ = {isa = PBXBuildFile; fileRef = 217DFBD41D9F6D490055D849 /* url.lua.h */; };
 		217DFC111D9F6D490055D849 /* usocket.c in Sources */ = {isa = PBXBuildFile; fileRef = 217DFBD51D9F6D490055D849 /* usocket.c */; };
 		217DFC121D9F6D490055D849 /* usocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 217DFBD61D9F6D490055D849 /* usocket.h */; };
+		D923E7D3296B85B9002FF1B3 /* harfbuzz.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D923E7D2296B85B9002FF1B3 /* harfbuzz.xcframework */; };
+		D9DAB9222961F0EE00C64820 /* HarfbuzzShaper.h in Headers */ = {isa = PBXBuildFile; fileRef = D9DAB9202961F0EE00C64820 /* HarfbuzzShaper.h */; };
+		D9DAB9232961F0EE00C64820 /* HarfbuzzShaper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D9DAB9212961F0EE00C64820 /* HarfbuzzShaper.cpp */; };
+		D9DAB9242961F0EE00C64820 /* HarfbuzzShaper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D9DAB9212961F0EE00C64820 /* HarfbuzzShaper.cpp */; };
+		D9DAB9292961F10000C64820 /* GenericShaper.h in Headers */ = {isa = PBXBuildFile; fileRef = D9DAB9252961F0FF00C64820 /* GenericShaper.h */; };
+		D9DAB92A2961F10000C64820 /* GenericShaper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D9DAB9262961F0FF00C64820 /* GenericShaper.cpp */; };
+		D9DAB92B2961F10000C64820 /* GenericShaper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D9DAB9262961F0FF00C64820 /* GenericShaper.cpp */; };
+		D9DAB92C2961F10000C64820 /* TextShaper.h in Headers */ = {isa = PBXBuildFile; fileRef = D9DAB9272961F0FF00C64820 /* TextShaper.h */; };
+		D9DAB92D2961F10000C64820 /* TextShaper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D9DAB9282961F10000C64820 /* TextShaper.cpp */; };
+		D9DAB92E2961F10000C64820 /* TextShaper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D9DAB9282961F10000C64820 /* TextShaper.cpp */; };
+		D9DAB9322963CD7500C64820 /* harfbuzz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9DAB9312963CD7500C64820 /* harfbuzz.framework */; };
 		FA0A3A5F23366CE9001C269E /* floattypes.h in Headers */ = {isa = PBXBuildFile; fileRef = FA0A3A5D23366CE9001C269E /* floattypes.h */; };
 		FA0A3A6023366CE9001C269E /* floattypes.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA0A3A5E23366CE9001C269E /* floattypes.cpp */; };
 		FA0A3A6123366CE9001C269E /* floattypes.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FA0A3A5E23366CE9001C269E /* floattypes.cpp */; };
@@ -1391,6 +1402,14 @@
 		217DFBD41D9F6D490055D849 /* url.lua.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = url.lua.h; sourceTree = "<group>"; };
 		217DFBD51D9F6D490055D849 /* usocket.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = usocket.c; sourceTree = "<group>"; };
 		217DFBD61D9F6D490055D849 /* usocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = usocket.h; sourceTree = "<group>"; };
+		D923E7D2296B85B9002FF1B3 /* harfbuzz.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = harfbuzz.xcframework; path = ios/libraries/harfbuzz.xcframework; sourceTree = "<group>"; };
+		D9DAB9202961F0EE00C64820 /* HarfbuzzShaper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HarfbuzzShaper.h; sourceTree = "<group>"; };
+		D9DAB9212961F0EE00C64820 /* HarfbuzzShaper.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = HarfbuzzShaper.cpp; sourceTree = "<group>"; };
+		D9DAB9252961F0FF00C64820 /* GenericShaper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GenericShaper.h; sourceTree = "<group>"; };
+		D9DAB9262961F0FF00C64820 /* GenericShaper.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = GenericShaper.cpp; sourceTree = "<group>"; };
+		D9DAB9272961F0FF00C64820 /* TextShaper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextShaper.h; sourceTree = "<group>"; };
+		D9DAB9282961F10000C64820 /* TextShaper.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TextShaper.cpp; sourceTree = "<group>"; };
+		D9DAB9312963CD7500C64820 /* harfbuzz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = harfbuzz.framework; path = macosx/Frameworks/harfbuzz.framework; sourceTree = "<group>"; };
 		FA08F5AE16C7525600F007B5 /* liblove-macosx.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "liblove-macosx.plist"; path = "macosx/liblove-macosx.plist"; sourceTree = "<group>"; };
 		FA0A3A5D23366CE9001C269E /* floattypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = floattypes.h; sourceTree = "<group>"; };
 		FA0A3A5E23366CE9001C269E /* floattypes.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = floattypes.cpp; sourceTree = "<group>"; };
@@ -2288,6 +2307,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				FACFB751276D7E3B0089F78D /* freetype.xcframework in Frameworks */,
+				D923E7D3296B85B9002FF1B3 /* harfbuzz.xcframework in Frameworks */,
 				FA84DE7A277D4C88002674C6 /* modplug.xcframework in Frameworks */,
 				FA84DE7C277E045E002674C6 /* ogg.xcframework in Frameworks */,
 				FACFB753276D7F860089F78D /* Lua.xcframework in Frameworks */,
@@ -2311,6 +2331,7 @@
 				FA577AC516C7513400860150 /* libmodplug.framework in Frameworks */,
 				FADF4CC62663D0EC004F95C1 /* libz.tbd in Frameworks */,
 				FA577AC816C7513C00860150 /* ogg.framework in Frameworks */,
+				D9DAB9322963CD7500C64820 /* harfbuzz.framework in Frameworks */,
 				FA577ACA16C7514100860150 /* OpenGL.framework in Frameworks */,
 				FA577ACD16C7514C00860150 /* vorbis.framework in Frameworks */,
 			);
@@ -2810,6 +2831,8 @@
 				FA0B7B731A95902C000E1D17 /* Font.cpp */,
 				FA0B7B741A95902C000E1D17 /* Font.h */,
 				FA0B7B751A95902C000E1D17 /* freetype */,
+				D9DAB9262961F0FF00C64820 /* GenericShaper.cpp */,
+				D9DAB9252961F0FF00C64820 /* GenericShaper.h */,
 				FA0B7B7A1A95902C000E1D17 /* GlyphData.cpp */,
 				FA0B7B7B1A95902C000E1D17 /* GlyphData.h */,
 				FA0B7B7C1A95902C000E1D17 /* ImageRasterizer.cpp */,
@@ -2817,6 +2840,8 @@
 				FA522D5923FA5ED40059EE3C /* NotoSans-Regular.ttf.gzip.h */,
 				FA0B7B7E1A95902C000E1D17 /* Rasterizer.cpp */,
 				FA0B7B7F1A95902C000E1D17 /* Rasterizer.h */,
+				D9DAB9282961F10000C64820 /* TextShaper.cpp */,
+				D9DAB9272961F0FF00C64820 /* TextShaper.h */,
 				FAB2D5A81AABDD8A008224A4 /* TrueTypeRasterizer.cpp */,
 				FAB2D5A91AABDD8A008224A4 /* TrueTypeRasterizer.h */,
 				FA0B7B811A95902C000E1D17 /* wrap_Font.cpp */,
@@ -2834,6 +2859,8 @@
 			children = (
 				FA0B7B761A95902C000E1D17 /* Font.cpp */,
 				FA0B7B771A95902C000E1D17 /* Font.h */,
+				D9DAB9212961F0EE00C64820 /* HarfbuzzShaper.cpp */,
+				D9DAB9202961F0EE00C64820 /* HarfbuzzShaper.h */,
 				FA0B7B781A95902C000E1D17 /* TrueTypeRasterizer.cpp */,
 				FA0B7B791A95902C000E1D17 /* TrueTypeRasterizer.h */,
 			);
@@ -3366,6 +3393,7 @@
 				FA577A7916C71A1700860150 /* Cocoa.framework */,
 				FAA627CD18E7E1560080752D /* CoreServices.framework */,
 				FAD43ECB1FF312D800831BB8 /* freetype.framework */,
+				D9DAB9312963CD7500C64820 /* harfbuzz.framework */,
 				FA577A8216C71A5300860150 /* libmodplug.framework */,
 				FADF4CC52663D0EC004F95C1 /* libz.tbd */,
 				FA577A6D16C719EA00860150 /* Lua.framework */,
@@ -3533,6 +3561,7 @@
 		FA5D24A31A96D2C300C6FC8F /* ios */ = {
 			isa = PBXGroup;
 			children = (
+				D923E7D2296B85B9002FF1B3 /* harfbuzz.xcframework */,
 				FACFB750276D7E2B0089F78D /* freetype.xcframework */,
 				FACFB752276D7F6F0089F78D /* Lua.xcframework */,
 				FA84DE79277D4C88002674C6 /* modplug.xcframework */,
@@ -4100,6 +4129,7 @@
 				FABDA9B82552448300B5C523 /* b2_motor_joint.h in Headers */,
 				FA0B7AC11A958EA3000E1D17 /* callbacks.h in Headers */,
 				FA3C5E491F8D80CA0003C579 /* ShaderStage.h in Headers */,
+				D9DAB9292961F10000C64820 /* GenericShaper.h in Headers */,
 				FA0B7D8F1A95902C000E1D17 /* ddsHandler.h in Headers */,
 				FAB2D5AC1AABDD8A008224A4 /* TrueTypeRasterizer.h in Headers */,
 				FABDAA042552448300B5C523 /* b2_edge_shape.h in Headers */,
@@ -4137,6 +4167,7 @@
 				FA0B7EDD1A95902D000E1D17 /* Touch.h in Headers */,
 				FA0B7EDE1A95902D000E1D17 /* Touch.h in Headers */,
 				FAC7CD861FE35E95006A60C7 /* physfs.h in Headers */,
+				D9DAB92C2961F10000C64820 /* TextShaper.h in Headers */,
 				FAF6C9E523C2DE2900D7B5BC /* spirv.hpp in Headers */,
 				FA522D4F23F9FE380059EE3C /* MP3Decoder.h in Headers */,
 				217DFBEE1D9F6D490055D849 /* luasocket.h in Headers */,
@@ -4181,6 +4212,7 @@
 				FAF6C9E823C2DE2900D7B5BC /* GLSL.ext.EXT.h in Headers */,
 				FACA02F71F5E396B0084B28F /* wrap_DataModule.h in Headers */,
 				FABDA9BE2552448300B5C523 /* Box2D.h in Headers */,
+				D9DAB9222961F0EE00C64820 /* HarfbuzzShaper.h in Headers */,
 				FA56AA3A1FAFF02000A43D5F /* memory.h in Headers */,
 				FA0B7E441A95902C000E1D17 /* wrap_CircleShape.h in Headers */,
 				FA0B7EB41A95902C000E1D17 /* System.h in Headers */,
@@ -4660,6 +4692,7 @@
 				FADF540E1E3D7CDD00012CC0 /* wrap_Video.cpp in Sources */,
 				FA0B7D4C1A95902C000E1D17 /* Shader.cpp in Sources */,
 				FA0B792A1A958E3B000E1D17 /* Matrix.cpp in Sources */,
+				D9DAB92B2961F10000C64820 /* GenericShaper.cpp in Sources */,
 				FAF140981E20934C00F898D2 /* PpTokens.cpp in Sources */,
 				FAF140AA1E20934C00F898D2 /* SymbolTable.cpp in Sources */,
 				FABDA9892552448300B5C523 /* b2_contact.cpp in Sources */,
@@ -4778,6 +4811,7 @@
 				FA59A2D31C06481400328DBA /* ParticleSystem.cpp in Sources */,
 				FA0B7E131A95902C000E1D17 /* GearJoint.cpp in Sources */,
 				FABDA99B2552448300B5C523 /* b2_polygon_contact.cpp in Sources */,
+				D9DAB9242961F0EE00C64820 /* HarfbuzzShaper.cpp in Sources */,
 				FA0B7DC21A95902C000E1D17 /* wrap_Joystick.cpp in Sources */,
 				FA0B7CD41A95902C000E1D17 /* Source.cpp in Sources */,
 				FAA3A9AF1B7D465A00CED060 /* android.cpp in Sources */,
@@ -4926,6 +4960,7 @@
 				FABDA9FE2552448300B5C523 /* b2_edge_shape.cpp in Sources */,
 				FA0B7D7A1A95902C000E1D17 /* Quad.cpp in Sources */,
 				FA620A3B1AA305F6005DB4C2 /* types.cpp in Sources */,
+				D9DAB92E2961F10000C64820 /* TextShaper.cpp in Sources */,
 				FA0B7DD41A95902C000E1D17 /* BezierCurve.cpp in Sources */,
 				FA0B7E7C1A95902C000E1D17 /* wrap_World.cpp in Sources */,
 				FAF6C9F923C2DE2900D7B5BC /* doc.cpp in Sources */,
@@ -5087,6 +5122,7 @@
 				FA0B7EA31A95902C000E1D17 /* SoundData.cpp in Sources */,
 				FA0B79291A958E3B000E1D17 /* Matrix.cpp in Sources */,
 				FA8951A21AA2EDF300EC385A /* wrap_Event.cpp in Sources */,
+				D9DAB92A2961F10000C64820 /* GenericShaper.cpp in Sources */,
 				FAF140691E20934C00F898D2 /* glslang_tab.cpp in Sources */,
 				FA0B7ABF1A958EA3000E1D17 /* host.c in Sources */,
 				FA0B7D4B1A95902C000E1D17 /* Shader.cpp in Sources */,
@@ -5205,6 +5241,7 @@
 				FAF140551E20934C00F898D2 /* Link.cpp in Sources */,
 				FABDA9792552448200B5C523 /* b2_joint.cpp in Sources */,
 				FAF140841E20934C00F898D2 /* ParseHelper.cpp in Sources */,
+				D9DAB9232961F0EE00C64820 /* HarfbuzzShaper.cpp in Sources */,
 				FA0B7D7F1A95902C000E1D17 /* Volatile.cpp in Sources */,
 				FA1BA0B11E16FD0800AA2803 /* Shader.cpp in Sources */,
 				FABDA99A2552448300B5C523 /* b2_polygon_contact.cpp in Sources */,
@@ -5353,6 +5390,7 @@
 				FA0B7E881A95902C000E1D17 /* Decoder.cpp in Sources */,
 				FA0B7E3C1A95902C000E1D17 /* wrap_Body.cpp in Sources */,
 				FA0B7D791A95902C000E1D17 /* Quad.cpp in Sources */,
+				D9DAB92D2961F10000C64820 /* TextShaper.cpp in Sources */,
 				FABDA9FD2552448300B5C523 /* b2_edge_shape.cpp in Sources */,
 				FAC756F51E4F99B400B91289 /* Effect.cpp in Sources */,
 				FA620A3A1AA305F6005DB4C2 /* types.cpp in Sources */,
@@ -5716,6 +5754,7 @@
 					"$(PROJECT_DIR)/macosx/Frameworks/freetype.framework/Headers",
 					"$(PROJECT_DIR)/macosx/Frameworks/Lua.framework/Headers",
 					"$(PROJECT_DIR)/macosx/Frameworks/SDL2.framework/Headers",
+					"$(PROJECT_DIR)/macosx/Frameworks/harfbuzz.framework/Headers",
 				);
 				INFOPLIST_FILE = "macosx/liblove-macosx.plist";
 				LD_DYLIB_INSTALL_NAME = "@rpath/$(EXECUTABLE_PATH)";
@@ -5751,6 +5790,7 @@
 					"$(PROJECT_DIR)/macosx/Frameworks/freetype.framework/Headers",
 					"$(PROJECT_DIR)/macosx/Frameworks/Lua.framework/Headers",
 					"$(PROJECT_DIR)/macosx/Frameworks/SDL2.framework/Headers",
+					"$(PROJECT_DIR)/macosx/Frameworks/harfbuzz.framework/Headers",
 				);
 				INFOPLIST_FILE = "macosx/liblove-macosx.plist";
 				LD_DYLIB_INSTALL_NAME = "@rpath/$(EXECUTABLE_PATH)";
@@ -5787,6 +5827,7 @@
 					"$(PROJECT_DIR)/macosx/Frameworks/freetype.framework/Headers",
 					"$(PROJECT_DIR)/macosx/Frameworks/Lua.framework/Headers",
 					"$(PROJECT_DIR)/macosx/Frameworks/SDL2.framework/Headers",
+					"$(PROJECT_DIR)/macosx/Frameworks/harfbuzz.framework/Headers",
 				);
 				INFOPLIST_FILE = "macosx/liblove-macosx.plist";
 				LD_DYLIB_INSTALL_NAME = "@rpath/$(EXECUTABLE_PATH)";

+ 4 - 0
platform/xcode/love.xcodeproj/project.pbxproj

@@ -15,6 +15,7 @@
 		A93E6EED10420BA8007D418B /* love.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A93E6A3410420AC0007D418B /* love.cpp */; };
 		A9F169AD109E825000FC83D1 /* libmodplug.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = A9F16926109E7BAD00FC83D1 /* libmodplug.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CE73F8001EEB64150052DAB3 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE73F7FF1EEB64150052DAB3 /* AVFoundation.framework */; };
+		D9DAB9372963CF6900C64820 /* harfbuzz.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = D9DAB9352963CF5F00C64820 /* harfbuzz.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		FA0797991BF480A200034B7C /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA0797981BF480A200034B7C /* GameController.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
 		FA08F69616C766E000F007B5 /* love.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA08F69116C765A200F007B5 /* love.framework */; };
 		FA08F69716C766E700F007B5 /* love.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = FA08F69116C765A200F007B5 /* love.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
@@ -87,6 +88,7 @@
 				FAD4B1731C1F50A3004CF150 /* theora.framework in Copy Frameworks */,
 				FAAFF04716CB120000CCDE45 /* OpenAL-Soft.framework in Copy Frameworks */,
 				FAD43ED01FF3136500831BB8 /* freetype.framework in Copy Frameworks */,
+				D9DAB9372963CF6900C64820 /* harfbuzz.framework in Copy Frameworks */,
 				A9F169AD109E825000FC83D1 /* libmodplug.framework in Copy Frameworks */,
 				A9255F58104324E100BA1496 /* ogg.framework in Copy Frameworks */,
 				A9255E031043195A00BA1496 /* vorbis.framework in Copy Frameworks */,
@@ -108,6 +110,7 @@
 		A97E3842132A9EDE00198A2F /* love-macosx.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "love-macosx.plist"; path = "macosx/love-macosx.plist"; sourceTree = "<group>"; };
 		A9F16926109E7BAD00FC83D1 /* libmodplug.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = libmodplug.framework; path = macosx/Frameworks/libmodplug.framework; sourceTree = "<group>"; };
 		CE73F7FF1EEB64150052DAB3 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.3.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; };
+		D9DAB9352963CF5F00C64820 /* harfbuzz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = harfbuzz.framework; path = macosx/Frameworks/harfbuzz.framework; sourceTree = "<group>"; };
 		FA0797981BF480A200034B7C /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS9.1.sdk/System/Library/Frameworks/GameController.framework; sourceTree = DEVELOPER_DIR; };
 		FA08F69116C765A200F007B5 /* love.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = love.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		FA0B7F061A95AAF3000E1D17 /* love.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = love.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -233,6 +236,7 @@
 			children = (
 				1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */,
 				FAD43ECF1FF3133700831BB8 /* freetype.framework */,
+				D9DAB9352963CF5F00C64820 /* harfbuzz.framework */,
 				A9F16926109E7BAD00FC83D1 /* libmodplug.framework */,
 				FA08F69116C765A200F007B5 /* love.framework */,
 				A93E6E5310420B57007D418B /* Lua.framework */,

+ 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-2023 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, (int) 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 && !isWhitespace(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 (isWhitespace(g))
+		{
+			lastspaceindex = i;
+			if (!isWhitespace(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-2023 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:

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

@@ -0,0 +1,381 @@
+/**
+ * Copyright (c) 2006-2023 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)
+	, useSpacesForTab(false)
+{
+	if (!rasterizer->hasGlyph('\t'))
+		useSpacesForTab = true;
+}
+
+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)
+{
+	const auto it = glyphAdvances.find(glyph);
+	if (it != glyphAdvances.end())
+	{
+		if (glyphindex)
+			*glyphindex = it->second.second;
+		return it->second.first;
+	}
+
+	int rasterizeri = 0;
+	uint32 realglyph = glyph;
+
+	if (glyph == '\t' && isUsingSpacesForTab())
+		realglyph = ' ';
+
+	for (size_t i = 0; i < rasterizers.size(); i++)
+	{
+		if (rasterizers[i]->hasGlyph(realglyph))
+		{
+			rasterizeri = (int) i;
+			break;
+		}
+	}
+
+	const auto &r = rasterizers[rasterizeri];
+	int advance = floorf(r->getGlyphSpacing(realglyph) / r->getDPIScale() + 0.5f);
+
+	if (glyph == '\t' && realglyph == ' ')
+		advance *= SPACES_PER_TAB;
+
+	GlyphIndex glyphi = {r->getGlyphIndex(realglyph), 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

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

@@ -0,0 +1,156 @@
+/**
+ * Copyright (c) 2006-2023 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;
+	};
+
+	// This will be used if the Rasterizer doesn't have a tab character itself.
+	static const int SPACES_PER_TAB = 4;
+
+	static love::Type type;
+
+	virtual ~TextShaper();
+
+	const std::vector<StrongRef<Rasterizer>> &getRasterizers() const { return rasterizers; }
+	bool isUsingSpacesForTab() const { return useSpacesForTab; }
+
+	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> *linewidths = 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);
+
+	static inline bool isWhitespace(uint32 codepoint) { return codepoint == ' ' || codepoint == '\t'; }
+
+	std::vector<StrongRef<Rasterizer>> rasterizers;
+	std::vector<float> dpiScales;
+
+private:
+
+	int height;
+	float lineHeight;
+
+	bool useSpacesForTab;
+
+	// 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

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

@@ -0,0 +1,409 @@
+/**
+ * Copyright (c) 2006-2023 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)
+	, spaceGlyphIndex()
+	, tabSpacesAdvanceX(0)
+	, tabSpacesAdvanceY(0)
+{
+	hbFonts.push_back(hb_ft_font_create_referenced((FT_Face)rasterizer->getHandle()));
+	hbBuffers.push_back(hb_buffer_create());
+
+	if (hbFonts[0] == nullptr || hbFonts[0] == hb_font_get_empty())
+		throw love::Exception("Could not create Harfbuzz font object.");
+
+	if (hbBuffers[0] == nullptr || hbBuffers[0] == hb_buffer_get_empty())
+		throw love::Exception("Could not create Harfbuzz buffer object.");
+
+	updateSpacesForTabInfo();
+}
+
+HarfbuzzShaper::~HarfbuzzShaper()
+{
+	for (hb_buffer_t *buffer : hbBuffers)
+		hb_buffer_destroy(buffer);
+	for (hb_font_t *font : hbFonts)
+		hb_font_destroy(font);
+}
+
+void HarfbuzzShaper::setFallbacks(const std::vector<Rasterizer*> &fallbacks)
+{
+	for (size_t i = 1; i < rasterizers.size(); i++)
+	{
+		hb_buffer_destroy(hbBuffers[i]);
+		hb_font_destroy(hbFonts[i]);
+	}
+
+	TextShaper::setFallbacks(fallbacks);
+
+	hbFonts.resize(rasterizers.size());
+	hbBuffers.resize(rasterizers.size());
+
+	for (size_t i = 1; i < rasterizers.size(); i++)
+	{
+		hbFonts[i] = hb_ft_font_create_referenced((FT_Face)rasterizers[i]->getHandle());
+		hbBuffers[i] = hb_buffer_create();
+	}
+
+	updateSpacesForTabInfo();
+}
+
+void HarfbuzzShaper::updateSpacesForTabInfo()
+{
+	if (!isUsingSpacesForTab())
+		return;
+
+	hb_codepoint_t glyphid = 0;
+	for (size_t i = 0; i < hbFonts.size(); i++)
+	{
+		hb_font_t *hbfont = hbFonts[i];
+		if (hb_font_get_glyph(hbfont, ' ', 0, &glyphid))
+		{
+			spaceGlyphIndex.index = glyphid;
+			spaceGlyphIndex.rasterizerIndex = i;
+			tabSpacesAdvanceX = hb_font_get_glyph_h_advance(hbfont, glyphid) * SPACES_PER_TAB;
+			tabSpacesAdvanceY = hb_font_get_glyph_v_advance(hbfont, glyphid) * SPACES_PER_TAB;
+			break;
+		}
+	}
+}
+
+bool HarfbuzzShaper::isValidGlyph(uint32 glyphindex, const std::vector<uint32> &codepoints, uint32 codepointindex)
+{
+	if (glyphindex != 0)
+		return true;
+
+	uint32 codepoint = codepoints[codepointindex];
+	if (codepoint == '\n' || codepoint == '\r' || (codepoint == '\t' && isUsingSpacesForTab()))
+		return true;
+
+	return false;
+}
+
+void HarfbuzzShaper::computeBufferRanges(const ColoredCodepoints &codepoints, Range range, std::vector<BufferRange> &bufferranges)
+{
+	bufferranges.clear();
+
+	// Less computation for the typical case (no fallback fonts).
+	if (rasterizers.size() == 1)
+	{
+		hb_buffer_reset(hbBuffers[0]);
+		hb_buffer_add_codepoints(hbBuffers[0], 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(hbBuffers[0]);
+
+		hb_shape(hbFonts[0], hbBuffers[0], nullptr, 0);
+
+		bufferranges.push_back({0, (int) range.first, Range(0, hb_buffer_get_length(hbBuffers[0]))});
+		return;
+	}
+
+	std::vector<Range> fallbackranges = { range };
+
+	// For each font, figure out the ranges of valid glyphs in the given string,
+	// and add the rest to a list to be shaped by the next fallback font.
+	// Harfbuzz doesn't have its own fallback API.
+	for (size_t rasti = 0; rasti < rasterizers.size(); rasti++)
+	{
+		hb_buffer_t *hbb = hbBuffers[rasti];
+		hb_buffer_reset(hbb);
+
+		for (Range r : fallbackranges)
+			hb_buffer_add_codepoints(hbb, codepoints.cps.data(), codepoints.cps.size(), (unsigned int)r.getOffset(), (int)r.getSize());
+
+		hb_buffer_guess_segment_properties(hbb);
+
+		hb_shape(hbFonts[rasti], hbb, nullptr, 0);
+
+		int glyphcount = (int)hb_buffer_get_length(hbb);
+		const hb_glyph_info_t *glyphinfos = hb_buffer_get_glyph_infos(hbb, nullptr);
+
+		fallbackranges.clear();
+
+		for (int i = 0; i < glyphcount; i++)
+		{
+			if (isValidGlyph(glyphinfos[i].codepoint, codepoints.cps, glyphinfos[i].cluster))
+			{
+				if (bufferranges.empty() || bufferranges.back().index != rasti || bufferranges.back().range.getMax() != i)
+					bufferranges.push_back({(int)rasti, (int)glyphinfos[i].cluster, Range(i, 1)});
+				else
+					bufferranges.back().range.last++;
+			}
+			else if (rasti == rasterizers.size() - 1)
+			{
+				// Use the first font for remaining invalid glyphs when no
+				// fallback font supports them.
+				if (bufferranges.empty() || bufferranges.back().index != 0 || bufferranges.back().range.getMax() != i)
+					bufferranges.push_back({0, (int)glyphinfos[i].cluster, Range(i, 1)});
+				else
+					bufferranges.back().range.last++;
+			}
+			else
+			{
+				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);
+			}
+		}
+	}
+
+	std::sort(bufferranges.begin(), bufferranges.end(), [](const BufferRange &a, const BufferRange &b)
+	{
+		if (a.codepointStart != b.codepointStart)
+			return a.codepointStart < b.codepointStart;
+		if (a.index != b.index)
+			return a.index < b.index;
+		return a.range.first < b.range.first;
+	});
+}
+
+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;
+
+	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);
+		}
+	}
+
+	std::vector<BufferRange> bufferranges;
+	computeBufferRanges(codepoints, range, bufferranges);
+
+	int maxwidth = (int)curpos.x;
+
+	for (const auto &bufferrange : bufferranges)
+	{
+		if (positions)
+			positions->reserve(positions->size() + bufferrange.range.getSize());
+
+		hb_buffer_t *hbbuffer = hbBuffers[bufferrange.index];
+
+		const hb_glyph_info_t *glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr);
+		hb_glyph_position_t *glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr);
+		hb_direction_t direction = hb_buffer_get_direction(hbbuffer);
+
+		for (size_t i = bufferrange.range.first; i <= bufferrange.range.last; i++)
+		{
+			const hb_glyph_info_t &info = glyphinfos[i];
+			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;
+
+			// This is a glyph index at this point, despite the name.
+			GlyphIndex gindex = { (int) info.codepoint, bufferrange.index };
+
+			if (clustercodepoint == '\t' && isUsingSpacesForTab())
+			{
+				gindex = spaceGlyphIndex;
+
+				// This should be safe to overwrite.
+				// TODO: RTL support?
+				glyphpos.x_offset = 0;
+				glyphpos.y_offset = 0;
+				glyphpos.x_advance = HB_DIRECTION_IS_HORIZONTAL(direction) ? tabSpacesAdvanceX : 0;
+				glyphpos.y_advance = HB_DIRECTION_IS_VERTICAL(direction) ? tabSpacesAdvanceY : 0;
+			}
+
+			if (colorToAdd.hasValue && colors && positions)
+			{
+				IndexedColor c = {colorToAdd.value, (int) positions->size()};
+				colors->push_back(c);
+				colorToAdd.clear();
+			}
+
+			if (positions)
+			{
+				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);
+		}
+	}
+
+	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());
+
+	float w = 0.0f;
+	float outwidth = 0.0f;
+	float widthbeforelastspace = 0.0f;
+	int wrapindex = -1;
+	int lastspaceindex = -1;
+
+	uint32 prevcodepoint = 0;
+
+	std::vector<BufferRange> bufferranges;
+	computeBufferRanges(codepoints, range, bufferranges);
+
+	for (const auto &bufferrange : bufferranges)
+	{
+		hb_buffer_t *hbbuffer = hbBuffers[bufferrange.index];
+
+		const hb_glyph_info_t *glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr);
+		hb_glyph_position_t *glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr);
+		hb_direction_t direction = hb_buffer_get_direction(hbbuffer);
+
+		for (size_t i = bufferrange.range.first; i <= bufferrange.range.last; i++)
+		{
+			const hb_glyph_info_t &info = glyphinfos[i];
+			hb_glyph_position_t &glyphpos = glyphpositions[i];
+
+			uint32 clustercodepoint = codepoints.cps[info.cluster];
+
+			if (clustercodepoint == '\r')
+			{
+				prevcodepoint = clustercodepoint;
+				continue;
+			}
+
+			if (clustercodepoint == '\t' && isUsingSpacesForTab())
+			{
+				// This should be safe to overwrite.
+				// TODO: RTL support?
+				glyphpos.x_offset = 0;
+				glyphpos.y_offset = 0;
+				glyphpos.x_advance = HB_DIRECTION_IS_HORIZONTAL(direction) ? tabSpacesAdvanceX : 0;
+				glyphpos.y_advance = HB_DIRECTION_IS_VERTICAL(direction) ? tabSpacesAdvanceY : 0;
+			}
+
+			float newwidth = w + floorf((glyphpos.x_advance >> 6) / dpiScales[0] + 0.5f);
+
+			// Only wrap when there's a non-space character.
+			if (newwidth > wraplimit && !isWhitespace(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 (isWhitespace(clustercodepoint))
+			{
+				lastspaceindex = info.cluster;
+				if (!isWhitespace(prevcodepoint))
+					widthbeforelastspace = w;
+			}
+			else
+				outwidth = newwidth;
+
+			w = newwidth;
+			prevcodepoint = clustercodepoint;
+			wrapindex = info.cluster;
+		}
+	}
+
+	if (width)
+		*width = outwidth;
+
+	return wrapindex;
+}
+
+} // freetype
+} // font
+} // love

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

@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2006-2023 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;
+typedef struct hb_buffer_t hb_buffer_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:
+
+	struct BufferRange
+	{
+		int index;
+		int codepointStart;
+		Range range;
+	};
+
+	void updateSpacesForTabInfo();
+	bool isValidGlyph(uint32 glyphindex, const std::vector<uint32> &codepoints, uint32 codepointindex);
+	void computeBufferRanges(const ColoredCodepoints &codepoints, Range range, std::vector<BufferRange> &bufferranges);
+
+	std::vector<hb_font_t *> hbFonts;
+	std::vector<hb_buffer_t *> hbBuffers;
+
+	GlyphIndex spaceGlyphIndex;
+	int tabSpacesAdvanceX;
+	int tabSpacesAdvanceY;
+
+}; // 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.

+ 112 - 499
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,20 +40,27 @@ 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()
 	, dpiScale(r->getDPIScale())
-	, useSpacesAsTab(false)
 	, textureCacheID(0)
 {
 	samplerState.minFilter = s.minFilter;
@@ -66,7 +71,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,9 +91,6 @@ 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.
-		useSpacesAsTab = true;
-
 	loadVolatile();
 	++fontCount;
 }
@@ -170,7 +172,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 +206,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,42 +224,17 @@ 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)
-	{
-		love::font::GlyphData *spacegd = rasterizers[0]->getGlyphData(32);
-		PixelFormat fmt = spacegd->getFormat();
-
-		love::font::GlyphMetrics gm = {};
-		gm.advance = spacegd->getAdvance() * SPACES_PER_TAB;
-		gm.bearingX = spacegd->getBearingX();
-		gm.bearingY = spacegd->getBearingY();
-
-		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 = rasterizers[0]->getDPIScale();
-	return rasterizers[0]->getGlyphData(glyph);
+	const auto &r = shaper->getRasterizers()[glyphindex.rasterizerIndex];
+	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 +256,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 +332,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);
-
-	uint32 prevglyph = 0;
+	for (int i = 0; i < (int) glyphpositions.size(); i++)
+	{
+		const auto &info = glyphpositions[i];
 
-	Colorf linearconstantcolor = gammaCorrectColor(constantcolor);
+		uint32 cacheid = textureCacheID;
 
-	Color32 curcolor = toColor32(constantcolor);
-	int curcolori = -1;
-	int ncolors = (int) codepoints.colors.size();
+		const Glyph &glyph = findGlyph(info.glyphIndex);
 
-	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 +414,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 +433,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 +441,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 +454,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 +466,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 +499,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 +513,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 +577,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 +601,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 +646,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 - 54
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;
 
@@ -234,8 +201,6 @@ private:
 	int textureX, textureY;
 	int rowHeight;
 
-	bool useSpacesAsTab;
-
 	// ID which is incremented when the texture cache is invalidated.
 	uint32 textureCacheID;
 
@@ -244,9 +209,6 @@ private:
 	// use, for edge antialiasing.
 	static const int TEXTURE_PADDING = 2;
 
-	// This will be used if the Rasterizer doesn't have a tab character itself.
-	static const int SPACES_PER_TAB = 4;
-
 	static StringMap<AlignMode, ALIGN_MAX_ENUM>::Entry alignModeEntries[];
 	static StringMap<AlignMode, ALIGN_MAX_ENUM> alignModes;
 	

+ 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);