Browse Source

font rendering: reimplement tab-as-spaces support.

Sasha Szpakowski 2 years ago
parent
commit
9502942b7b

+ 3 - 3
src/modules/font/GenericShaper.cpp

@@ -167,7 +167,7 @@ int GenericShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Ran
 		float newwidth = w + getKerning(prevglyph, g) + getGlyphAdvance(g);
 		float newwidth = w + getKerning(prevglyph, g) + getGlyphAdvance(g);
 
 
 		// Only wrap when there's a non-space character.
 		// Only wrap when there's a non-space character.
-		if (newwidth > wraplimit && g != ' ')
+		if (newwidth > wraplimit && !isWhitespace(g))
 		{
 		{
 			// Rewind to the last seen space when wrapping.
 			// Rewind to the last seen space when wrapping.
 			if (lastspaceindex != -1)
 			if (lastspaceindex != -1)
@@ -179,10 +179,10 @@ int GenericShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Ran
 		}
 		}
 
 
 		// Don't count trailing spaces in the output width.
 		// Don't count trailing spaces in the output width.
-		if (g == ' ')
+		if (isWhitespace(g))
 		{
 		{
 			lastspaceindex = i;
 			lastspaceindex = i;
-			if (prevglyph != ' ')
+			if (!isWhitespace(prevglyph))
 				widthbeforelastspace = w;
 				widthbeforelastspace = w;
 		}
 		}
 		else
 		else

+ 27 - 19
src/modules/font/TextShaper.cpp

@@ -30,7 +30,7 @@ namespace love
 namespace font
 namespace font
 {
 {
 
 
-void getCodepointsFromString(const std::string& text, std::vector<uint32>& codepoints)
+void getCodepointsFromString(const std::string &text, std::vector<uint32> &codepoints)
 {
 {
 	codepoints.reserve(text.size());
 	codepoints.reserve(text.size());
 
 
@@ -45,20 +45,20 @@ void getCodepointsFromString(const std::string& text, std::vector<uint32>& codep
 			codepoints.push_back(g);
 			codepoints.push_back(g);
 		}
 		}
 	}
 	}
-	catch (utf8::exception& e)
+	catch (utf8::exception &e)
 	{
 	{
 		throw love::Exception("UTF-8 decoding error: %s", e.what());
 		throw love::Exception("UTF-8 decoding error: %s", e.what());
 	}
 	}
 }
 }
 
 
-void getCodepointsFromString(const std::vector<ColoredString>& strs, ColoredCodepoints& codepoints)
+void getCodepointsFromString(const std::vector<ColoredString> &strs, ColoredCodepoints &codepoints)
 {
 {
 	if (strs.empty())
 	if (strs.empty())
 		return;
 		return;
 
 
 	codepoints.cps.reserve(strs[0].str.size());
 	codepoints.cps.reserve(strs[0].str.size());
 
 
-	for (const ColoredString& cstr : strs)
+	for (const ColoredString &cstr : strs)
 	{
 	{
 		// No need to add the color if the string is empty anyway, and the code
 		// 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.
 		// further on assumes no two colors share the same starting position.
@@ -87,7 +87,10 @@ TextShaper::TextShaper(Rasterizer *rasterizer)
 	, dpiScales{rasterizer->getDPIScale()}
 	, dpiScales{rasterizer->getDPIScale()}
 	, height(floorf(rasterizer->getHeight() / rasterizer->getDPIScale() + 0.5f))
 	, height(floorf(rasterizer->getHeight() / rasterizer->getDPIScale() + 0.5f))
 	, lineHeight(1)
 	, lineHeight(1)
+	, useSpacesForTab(false)
 {
 {
+	if (!rasterizer->hasGlyph('\t'))
+		useSpacesForTab = true;
 }
 }
 
 
 TextShaper::~TextShaper()
 TextShaper::~TextShaper()
@@ -141,7 +144,7 @@ bool TextShaper::hasGlyph(uint32 glyph) const
 	return false;
 	return false;
 }
 }
 
 
-bool TextShaper::hasGlyphs(const std::string& text) const
+bool TextShaper::hasGlyphs(const std::string &text) const
 {
 {
 	if (text.size() == 0)
 	if (text.size() == 0)
 		return false;
 		return false;
@@ -178,7 +181,7 @@ float TextShaper::getKerning(uint32 leftglyph, uint32 rightglyph)
 	float k = 0.0f;
 	float k = 0.0f;
 	bool found = false;
 	bool found = false;
 
 
-	for (const auto& r : rasterizers)
+	for (const auto &r : rasterizers)
 	{
 	{
 		if (r->hasGlyph(leftglyph) && r->hasGlyph(rightglyph))
 		if (r->hasGlyph(leftglyph) && r->hasGlyph(rightglyph))
 		{
 		{
@@ -195,7 +198,7 @@ float TextShaper::getKerning(uint32 leftglyph, uint32 rightglyph)
 	return k;
 	return k;
 }
 }
 
 
-float TextShaper::getKerning(const std::string& leftchar, const std::string& rightchar)
+float TextShaper::getKerning(const std::string &leftchar, const std::string &rightchar)
 {
 {
 	uint32 left = 0;
 	uint32 left = 0;
 	uint32 right = 0;
 	uint32 right = 0;
@@ -215,8 +218,6 @@ float TextShaper::getKerning(const std::string& leftchar, const std::string& rig
 
 
 int TextShaper::getGlyphAdvance(uint32 glyph, GlyphIndex *glyphindex)
 int TextShaper::getGlyphAdvance(uint32 glyph, GlyphIndex *glyphindex)
 {
 {
-	// TODO: use spaces for tab
-
 	const auto it = glyphAdvances.find(glyph);
 	const auto it = glyphAdvances.find(glyph);
 	if (it != glyphAdvances.end())
 	if (it != glyphAdvances.end())
 	{
 	{
@@ -226,10 +227,14 @@ int TextShaper::getGlyphAdvance(uint32 glyph, GlyphIndex *glyphindex)
 	}
 	}
 
 
 	int rasterizeri = 0;
 	int rasterizeri = 0;
+	uint32 realglyph = glyph;
+
+	if (glyph == '\t' && isUsingSpacesForTab())
+		realglyph = ' ';
 
 
 	for (size_t i = 0; i < rasterizers.size(); i++)
 	for (size_t i = 0; i < rasterizers.size(); i++)
 	{
 	{
-		if (rasterizers[i]->hasGlyph(glyph))
+		if (rasterizers[i]->hasGlyph(realglyph))
 		{
 		{
 			rasterizeri = (int) i;
 			rasterizeri = (int) i;
 			break;
 			break;
@@ -237,9 +242,12 @@ int TextShaper::getGlyphAdvance(uint32 glyph, GlyphIndex *glyphindex)
 	}
 	}
 
 
 	const auto &r = rasterizers[rasterizeri];
 	const auto &r = rasterizers[rasterizeri];
-	int advance = floorf(r->getGlyphSpacing(glyph) / r->getDPIScale() + 0.5f);
+	int advance = floorf(r->getGlyphSpacing(realglyph) / r->getDPIScale() + 0.5f);
+
+	if (glyph == '\t' && realglyph == ' ')
+		advance *= SPACES_PER_TAB;
 
 
-	GlyphIndex glyphi = {r->getGlyphIndex(glyph), rasterizeri};
+	GlyphIndex glyphi = {r->getGlyphIndex(realglyph), rasterizeri};
 
 
 	glyphAdvances[glyph] = std::make_pair(advance, glyphi);
 	glyphAdvances[glyph] = std::make_pair(advance, glyphi);
 	if (glyphindex)
 	if (glyphindex)
@@ -260,7 +268,7 @@ int TextShaper::getWidth(const std::string &str)
 	return info.width;
 	return info.width;
 }
 }
 
 
-static size_t findNewline(const ColoredCodepoints& codepoints, size_t start)
+static size_t findNewline(const ColoredCodepoints &codepoints, size_t start)
 {
 {
 	for (size_t i = start; i < codepoints.cps.size(); i++)
 	for (size_t i = start; i < codepoints.cps.size(); i++)
 	{
 	{
@@ -273,7 +281,7 @@ static size_t findNewline(const ColoredCodepoints& codepoints, size_t start)
 	return codepoints.cps.size();
 	return codepoints.cps.size();
 }
 }
 
 
-void TextShaper::getWrap(const ColoredCodepoints& codepoints, float wraplimit, std::vector<Range>& lineranges, std::vector<int>* linewidths)
+void TextShaper::getWrap(const ColoredCodepoints &codepoints, float wraplimit, std::vector<Range> &lineranges, std::vector<int> *linewidths)
 {
 {
 	size_t nextnewline = findNewline(codepoints, 0);
 	size_t nextnewline = findNewline(codepoints, 0);
 
 
@@ -317,7 +325,7 @@ void TextShaper::getWrap(const ColoredCodepoints& codepoints, float wraplimit, s
 	}
 	}
 }
 }
 
 
-void TextShaper::getWrap(const std::vector<ColoredString>& text, float wraplimit, std::vector<std::string>& lines, std::vector<int>* linewidths)
+void TextShaper::getWrap(const std::vector<ColoredString> &text, float wraplimit, std::vector<std::string> &lines, std::vector<int> *linewidths)
 {
 {
 	ColoredCodepoints cps;
 	ColoredCodepoints cps;
 	getCodepointsFromString(text, cps);
 	getCodepointsFromString(text, cps);
@@ -327,7 +335,7 @@ void TextShaper::getWrap(const std::vector<ColoredString>& text, float wraplimit
 
 
 	std::string line;
 	std::string line;
 
 
-	for (const auto& range : codepointranges)
+	for (const auto &range : codepointranges)
 	{
 	{
 		line.clear();
 		line.clear();
 
 
@@ -347,9 +355,9 @@ void TextShaper::getWrap(const std::vector<ColoredString>& text, float wraplimit
 	}
 	}
 }
 }
 
 
-void TextShaper::setFallbacks(const std::vector<Rasterizer*>& fallbacks)
+void TextShaper::setFallbacks(const std::vector<Rasterizer*> &fallbacks)
 {
 {
-	for (Rasterizer* r : fallbacks)
+	for (Rasterizer *r : fallbacks)
 	{
 	{
 		if (r->getDataType() != rasterizers[0]->getDataType())
 		if (r->getDataType() != rasterizers[0]->getDataType())
 			throw love::Exception("Font fallbacks must be of the same font type.");
 			throw love::Exception("Font fallbacks must be of the same font type.");
@@ -362,7 +370,7 @@ void TextShaper::setFallbacks(const std::vector<Rasterizer*>& fallbacks)
 	rasterizers.resize(1);
 	rasterizers.resize(1);
 	dpiScales.resize(1);
 	dpiScales.resize(1);
 
 
-	for (Rasterizer* r : fallbacks)
+	for (Rasterizer *r : fallbacks)
 	{
 	{
 		rasterizers.push_back(r);
 		rasterizers.push_back(r);
 		dpiScales.push_back(r->getDPIScale());
 		dpiScales.push_back(r->getDPIScale());

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

@@ -81,11 +81,15 @@ public:
 		int height;
 		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;
 	static love::Type type;
 
 
 	virtual ~TextShaper();
 	virtual ~TextShaper();
 
 
 	const std::vector<StrongRef<Rasterizer>> &getRasterizers() const { return rasterizers; }
 	const std::vector<StrongRef<Rasterizer>> &getRasterizers() const { return rasterizers; }
+	bool isUsingSpacesForTab() const { return useSpacesForTab; }
 
 
 	float getHeight() const;
 	float getHeight() const;
 
 
@@ -128,6 +132,8 @@ protected:
 
 
 	TextShaper(Rasterizer *rasterizer);
 	TextShaper(Rasterizer *rasterizer);
 
 
+	static inline bool isWhitespace(uint32 codepoint) { return codepoint == ' ' || codepoint == '\t'; }
+
 	std::vector<StrongRef<Rasterizer>> rasterizers;
 	std::vector<StrongRef<Rasterizer>> rasterizers;
 	std::vector<float> dpiScales;
 	std::vector<float> dpiScales;
 
 
@@ -136,6 +142,8 @@ private:
 	int height;
 	int height;
 	float lineHeight;
 	float lineHeight;
 
 
+	bool useSpacesForTab;
+
 	// maps glyphs to advance and glyph+rasterizer index.
 	// maps glyphs to advance and glyph+rasterizer index.
 	std::unordered_map<uint32, std::pair<int, GlyphIndex>> glyphAdvances;
 	std::unordered_map<uint32, std::pair<int, GlyphIndex>> glyphAdvances;
 
 

+ 90 - 28
src/modules/font/freetype/HarfbuzzShaper.cpp

@@ -36,30 +36,70 @@ namespace freetype
 
 
 HarfbuzzShaper::HarfbuzzShaper(TrueTypeRasterizer *rasterizer)
 HarfbuzzShaper::HarfbuzzShaper(TrueTypeRasterizer *rasterizer)
 	: TextShaper(rasterizer)
 	: TextShaper(rasterizer)
+	, spaceGlyphIndex()
+	, tabSpacesAdvanceX(0)
+	, tabSpacesAdvanceY(0)
 {
 {
 	hbFonts.push_back(hb_ft_font_create_referenced((FT_Face)rasterizer->getHandle()));
 	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())
 	if (hbFonts[0] == nullptr || hbFonts[0] == hb_font_get_empty())
-		throw love::Exception("");
+		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()
 HarfbuzzShaper::~HarfbuzzShaper()
 {
 {
+	for (hb_buffer_t *buffer : hbBuffers)
+		hb_buffer_destroy(buffer);
 	for (hb_font_t *font : hbFonts)
 	for (hb_font_t *font : hbFonts)
 		hb_font_destroy(font);
 		hb_font_destroy(font);
 }
 }
 
 
-void HarfbuzzShaper::setFallbacks(const std::vector<Rasterizer*>& fallbacks)
+void HarfbuzzShaper::setFallbacks(const std::vector<Rasterizer*> &fallbacks)
 {
 {
-	for (size_t i = 1; i < hbFonts.size(); i++)
+	for (size_t i = 1; i < rasterizers.size(); i++)
+	{
+		hb_buffer_destroy(hbBuffers[i]);
 		hb_font_destroy(hbFonts[i]);
 		hb_font_destroy(hbFonts[i]);
+	}
 
 
 	TextShaper::setFallbacks(fallbacks);
 	TextShaper::setFallbacks(fallbacks);
 
 
 	hbFonts.resize(rasterizers.size());
 	hbFonts.resize(rasterizers.size());
+	hbBuffers.resize(rasterizers.size());
 
 
-	for (size_t i = 1; i < hbFonts.size(); i++)
+	for (size_t i = 1; i < rasterizers.size(); i++)
+	{
 		hbFonts[i] = hb_ft_font_create_referenced((FT_Face)rasterizers[i]->getHandle());
 		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;
+		}
+	}
 }
 }
 
 
 void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints, Range range, Vector2 offset, float extraspacing, std::vector<GlyphPosition> *positions, std::vector<IndexedColor> *colors, TextInfo *info)
 void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints, Range range, Vector2 offset, float extraspacing, std::vector<GlyphPosition> *positions, std::vector<IndexedColor> *colors, TextInfo *info)
@@ -70,18 +110,21 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints,
 	offset.y += getBaseline();
 	offset.y += getBaseline();
 	Vector2 curpos = offset;
 	Vector2 curpos = offset;
 
 
-	hb_buffer_t *hbbuffer = hb_buffer_create();
+	hb_buffer_t *hbbuffer = hbBuffers[0];
+	hb_buffer_reset(hbbuffer);
 
 
 	hb_buffer_add_codepoints(hbbuffer, codepoints.cps.data(), codepoints.cps.size(), (unsigned int) range.getOffset(), (int) range.getSize());
 	hb_buffer_add_codepoints(hbbuffer, codepoints.cps.data(), codepoints.cps.size(), (unsigned int) range.getOffset(), (int) range.getSize());
 
 
 	// TODO: Expose APIs for direction and script?
 	// TODO: Expose APIs for direction and script?
 	hb_buffer_guess_segment_properties(hbbuffer);
 	hb_buffer_guess_segment_properties(hbbuffer);
 
 
+	hb_direction_t direction = hb_buffer_get_direction(hbbuffer);
+
 	hb_shape(hbFonts[0], hbbuffer, nullptr, 0);
 	hb_shape(hbFonts[0], hbbuffer, nullptr, 0);
 
 
 	int glyphcount = (int) hb_buffer_get_length(hbbuffer);
 	int glyphcount = (int) hb_buffer_get_length(hbbuffer);
-	hb_glyph_info_t* glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr);
-	hb_glyph_position_t* glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr);
+	hb_glyph_info_t *glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr);
+	hb_glyph_position_t *glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr);
 
 
 	if (positions)
 	if (positions)
 		positions->reserve(positions->size() + glyphcount);
 		positions->reserve(positions->size() + glyphcount);
@@ -93,7 +136,7 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints,
 		for (int i = 0; i < glyphcount; i++)
 		for (int i = 0; i < glyphcount; i++)
 		{
 		{
 			bool adding = false;
 			bool adding = false;
-			if (glyphinfos[i].codepoint == 0)
+			if (glyphinfos[i].codepoint == 0 && (codepoints.cps[glyphinfos[i].cluster] != '\t' || !isUsingSpacesForTab()))
 			{
 			{
 				if (fallbackranges.empty() || fallbackranges.back().getMax() != glyphinfos[i-1].cluster)
 				if (fallbackranges.empty() || fallbackranges.back().getMax() != glyphinfos[i-1].cluster)
 					fallbackranges.push_back(Range(glyphinfos[i].cluster, 1));
 					fallbackranges.push_back(Range(glyphinfos[i].cluster, 1));
@@ -106,9 +149,7 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints,
 		{
 		{
 			for (size_t rasti = 1; rasti < rasterizers.size(); rasti++)
 			for (size_t rasti = 1; rasti < rasterizers.size(); rasti++)
 			{
 			{
-				hb_buffer_t* hbbuffer = hb_buffer_create();
 
 
-				hb_buffer_destroy(hbbuffer);
 			}
 			}
 		}
 		}
 	}
 	}
@@ -132,11 +173,10 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints,
 	int maxwidth = (int)curpos.x;
 	int maxwidth = (int)curpos.x;
 
 
 	// TODO: fallbacks
 	// TODO: fallbacks
-	// TODO: use spaces for tab
 	for (int i = 0; i < glyphcount; i++)
 	for (int i = 0; i < glyphcount; i++)
 	{
 	{
-		const hb_glyph_info_t& info = glyphinfos[i];
-		const hb_glyph_position_t& glyphpos = glyphpositions[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
 		// 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
 		// change in the middle of some characters that get combined into a single
@@ -166,6 +206,21 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints,
 		if (clustercodepoint == '\r')
 		if (clustercodepoint == '\r')
 			continue;
 			continue;
 
 
+		// This is a glyph index at this point, despite the name.
+		GlyphIndex gindex = { info.codepoint, 0 };
+
+		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)
 		if (colorToAdd.hasValue && colors && positions)
 		{
 		{
 			IndexedColor c = {colorToAdd.value, positions->size()};
 			IndexedColor c = {colorToAdd.value, positions->size()};
@@ -175,8 +230,6 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints,
 
 
 		if (positions)
 		if (positions)
 		{
 		{
-			// Despite the name this is a glyph index at this point.
-			GlyphIndex gindex = { info.codepoint, 0 };
 			GlyphPosition p = { curpos, gindex };
 			GlyphPosition p = { curpos, gindex };
 
 
 			// Harfbuzz position coordinate systems are based on the given font.
 			// Harfbuzz position coordinate systems are based on the given font.
@@ -195,8 +248,6 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints,
 			curpos.x = floorf(curpos.x + extraspacing);
 			curpos.x = floorf(curpos.x + extraspacing);
 	}
 	}
 
 
-	hb_buffer_destroy(hbbuffer);
-
 	if (curpos.x > maxwidth)
 	if (curpos.x > maxwidth)
 		maxwidth = (int)curpos.x;
 		maxwidth = (int)curpos.x;
 
 
@@ -209,23 +260,26 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints,
 	}
 	}
 }
 }
 
 
-int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Range range, float wraplimit, float *width)
+int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints &codepoints, Range range, float wraplimit, float *width)
 {
 {
 	if (!range.isValid())
 	if (!range.isValid())
 		range = Range(0, codepoints.cps.size());
 		range = Range(0, codepoints.cps.size());
 
 
-	hb_buffer_t* hbbuffer = hb_buffer_create();
+	hb_buffer_t *hbbuffer = hbBuffers[0];
+	hb_buffer_reset(hbbuffer);
 
 
 	hb_buffer_add_codepoints(hbbuffer, codepoints.cps.data(), codepoints.cps.size(), (unsigned int)range.getOffset(), (int)range.getSize());
 	hb_buffer_add_codepoints(hbbuffer, codepoints.cps.data(), codepoints.cps.size(), (unsigned int)range.getOffset(), (int)range.getSize());
 
 
 	// TODO: Expose APIs for direction and script?
 	// TODO: Expose APIs for direction and script?
 	hb_buffer_guess_segment_properties(hbbuffer);
 	hb_buffer_guess_segment_properties(hbbuffer);
 
 
+	hb_direction_t direction = hb_buffer_get_direction(hbbuffer);
+
 	hb_shape(hbFonts[0], hbbuffer, nullptr, 0);
 	hb_shape(hbFonts[0], hbbuffer, nullptr, 0);
 
 
 	int glyphcount = (int)hb_buffer_get_length(hbbuffer);
 	int glyphcount = (int)hb_buffer_get_length(hbbuffer);
-	hb_glyph_info_t* glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr);
-	hb_glyph_position_t* glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr);
+	hb_glyph_info_t *glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr);
+	hb_glyph_position_t *glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr);
 
 
 	float w = 0.0f;
 	float w = 0.0f;
 	float outwidth = 0.0f;
 	float outwidth = 0.0f;
@@ -237,8 +291,8 @@ int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Ra
 
 
 	for (int i = 0; i < glyphcount; i++)
 	for (int i = 0; i < glyphcount; i++)
 	{
 	{
-		const hb_glyph_info_t& info = glyphinfos[i];
-		const hb_glyph_position_t& glyphpos = glyphpositions[i];
+		const hb_glyph_info_t &info = glyphinfos[i];
+		hb_glyph_position_t &glyphpos = glyphpositions[i];
 
 
 		uint32 clustercodepoint = codepoints.cps[info.cluster];
 		uint32 clustercodepoint = codepoints.cps[info.cluster];
 
 
@@ -248,10 +302,20 @@ int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Ra
 			continue;
 			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);
 		float newwidth = w + floorf((glyphpos.x_advance >> 6) / dpiScales[0] + 0.5f);
 
 
 		// Only wrap when there's a non-space character.
 		// Only wrap when there's a non-space character.
-		if (newwidth > wraplimit && clustercodepoint != ' ')
+		if (newwidth > wraplimit && !isWhitespace(clustercodepoint))
 		{
 		{
 			// Rewind to the last seen space when wrapping.
 			// Rewind to the last seen space when wrapping.
 			if (lastspaceindex != -1)
 			if (lastspaceindex != -1)
@@ -263,10 +327,10 @@ int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Ra
 		}
 		}
 
 
 		// Don't count trailing spaces in the output width.
 		// Don't count trailing spaces in the output width.
-		if (clustercodepoint == ' ')
+		if (isWhitespace(clustercodepoint))
 		{
 		{
 			lastspaceindex = info.cluster;
 			lastspaceindex = info.cluster;
-			if (prevcodepoint != ' ')
+			if (!isWhitespace(prevcodepoint))
 				widthbeforelastspace = w;
 				widthbeforelastspace = w;
 		}
 		}
 		else
 		else
@@ -277,8 +341,6 @@ int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Ra
 		wrapindex = info.cluster;
 		wrapindex = info.cluster;
 	}
 	}
 
 
-	hb_buffer_destroy(hbbuffer);
-
 	if (width)
 	if (width)
 		*width = outwidth;
 		*width = outwidth;
 
 

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

@@ -26,6 +26,7 @@
 extern "C"
 extern "C"
 {
 {
 typedef struct hb_font_t hb_font_t;
 typedef struct hb_font_t hb_font_t;
+typedef struct hb_buffer_t hb_buffer_t;
 }
 }
 
 
 namespace love
 namespace love
@@ -50,7 +51,14 @@ public:
 
 
 private:
 private:
 
 
+	void updateSpacesForTabInfo();
+
 	std::vector<hb_font_t *> hbFonts;
 	std::vector<hb_font_t *> hbFonts;
+	std::vector<hb_buffer_t *> hbBuffers;
+
+	GlyphIndex spaceGlyphIndex;
+	int tabSpacesAdvanceX;
+	int tabSpacesAdvanceY;
 
 
 }; // HarfbuzzShaper
 }; // HarfbuzzShaper
 
 

+ 0 - 23
src/modules/graphics/Font.cpp

@@ -61,7 +61,6 @@ Font::Font(love::font::Rasterizer *r, const SamplerState &s)
 	, textureHeight(128)
 	, textureHeight(128)
 	, samplerState()
 	, samplerState()
 	, dpiScale(r->getDPIScale())
 	, dpiScale(r->getDPIScale())
-	, useSpacesAsTab(false)
 	, textureCacheID(0)
 	, textureCacheID(0)
 {
 {
 	samplerState.minFilter = s.minFilter;
 	samplerState.minFilter = s.minFilter;
@@ -92,10 +91,6 @@ Font::Font(love::font::Rasterizer *r, const SamplerState &s)
 	if (pixelFormat == PIXELFORMAT_LA8_UNORM && !gfx->isPixelFormatSupported(pixelFormat, PIXELFORMATUSAGEFLAGS_SAMPLE))
 	if (pixelFormat == PIXELFORMAT_LA8_UNORM && !gfx->isPixelFormatSupported(pixelFormat, PIXELFORMATUSAGEFLAGS_SAMPLE))
 		pixelFormat = PIXELFORMAT_RGBA8_UNORM;
 		pixelFormat = PIXELFORMAT_RGBA8_UNORM;
 
 
-	uint32 tab = '\t';
-	if (!r->hasGlyph(tab)) // No tab character in the Rasterizer.
-		useSpacesAsTab = true;
-
 	loadVolatile();
 	loadVolatile();
 	++fontCount;
 	++fontCount;
 }
 }
@@ -232,24 +227,6 @@ void Font::unloadVolatile()
 love::font::GlyphData *Font::getRasterizerGlyphData(love::font::TextShaper::GlyphIndex glyphindex, float &dpiscale)
 love::font::GlyphData *Font::getRasterizerGlyphData(love::font::TextShaper::GlyphIndex glyphindex, float &dpiscale)
 {
 {
 	const auto &r = shaper->getRasterizers()[glyphindex.rasterizerIndex];
 	const auto &r = shaper->getRasterizers()[glyphindex.rasterizerIndex];
-
-	// Use spaces for the tab 'glyph'. FIXME
-	if (/*glyph == '\t' &&*/ false && useSpacesAsTab)
-	{
-		love::font::GlyphData *spacegd = r->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 = r->getDPIScale();
-		return new love::font::GlyphData('\t', gm, fmt);
-	}
-
 	dpiscale = r->getDPIScale();
 	dpiscale = r->getDPIScale();
 	return r->getGlyphDataForIndex(glyphindex.index);
 	return r->getGlyphDataForIndex(glyphindex.index);
 }
 }

+ 0 - 5
src/modules/graphics/Font.h

@@ -201,8 +201,6 @@ private:
 	int textureX, textureY;
 	int textureX, textureY;
 	int rowHeight;
 	int rowHeight;
 
 
-	bool useSpacesAsTab;
-
 	// ID which is incremented when the texture cache is invalidated.
 	// ID which is incremented when the texture cache is invalidated.
 	uint32 textureCacheID;
 	uint32 textureCacheID;
 
 
@@ -211,9 +209,6 @@ private:
 	// use, for edge antialiasing.
 	// use, for edge antialiasing.
 	static const int TEXTURE_PADDING = 2;
 	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>::Entry alignModeEntries[];
 	static StringMap<AlignMode, ALIGN_MAX_ENUM> alignModes;
 	static StringMap<AlignMode, ALIGN_MAX_ENUM> alignModes;