Files
2025-09-18 17:55:52 -04:00

923 lines
32 KiB
C++

#include "stdafx.h"
#include "TypesetterDynamic.h"
#include "GfxBase/Adorn.h"
#include "StringConv.h"
#include "TextureManager.h"
#include "VisualEngine.h"
#include "TextureAtlas.h"
#include "GfxCore/Texture.h"
#include "GfxCore/Device.h"
#include "freetype/include/ft2build.h"
#include FT_FREETYPE_H
#include FT_MODULE_H
#include FT_TRUETYPE_DRIVER_H
#include FT_LCD_FILTER_H
LOGGROUP(Graphics)
FASTFLAG(FontSmoothScalling)
DYNAMIC_FASTFLAG(TextScaleDontWrapInWords)
FASTINTVARIABLE(FontSizePadding, 1)
#define FONT_PADDING 1.0f
namespace RBX
{
namespace Graphics
{
#define UTF8_ACCEPT 0
#define UTF8_REJECT 1
static const uint8_t utf8d[] = {
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df
0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef
0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff
0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2
1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4
1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6
1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8
};
uint32_t inline utf8DecodeInternal(uint32_t* state, uint32_t* codep, uint32_t byte) {
uint32_t type = utf8d[byte];
*codep = (*state != UTF8_ACCEPT) ?
(byte & 0x3fu) | (*codep << 6) :
(0xff >> type) & (byte);
*state = utf8d[256 + *state*16 + type];
return *state;
}
static int round_int( double r )
{
return (r > 0.0) ? (r + 0.5) : (r - 0.5);
}
class GlyphProvider
{
public:
struct SizeSpecificData
{
unsigned ascender;
unsigned descender;
unsigned height;
};
GlyphProvider(const std::string& fontPath, TextureAtlas* textureAtlas, unsigned fontId)
: textureAtlas(textureAtlas)
, fontId(fontId)
, library(NULL)
, face(NULL)
{
memset(&errorGlyph, 0, sizeof(errorGlyph));
memset(&errorSizeData, 0, sizeof(errorSizeData));
FT_Error error = FT_Init_FreeType( &library );
RBXASSERT(error == FT_Err_Ok);
if (!library)
{
FASTLOG(FLog::Graphics, "ERROR: Cannot init free type library");
return;
}
std::ifstream file(utf8_decode(fontPath).c_str(), std::ios::in | std::ios::binary);
if (!file)
{
FASTLOG(FLog::Graphics, "ERROR: Cannot load font file");
return;
}
std::ostringstream data;
data << file.rdbuf();
dataBuffer = data.str();
error = FT_New_Memory_Face(library, (const unsigned char*)dataBuffer.c_str(), dataBuffer.size(), 0, &face);
RBXASSERT(error == FT_Err_Ok);
if (!face)
{
FASTLOG(FLog::Graphics, "ERROR: Cannot create font face from memory");
return;
}
FT_UInt interpreter_version = TT_INTERPRETER_VERSION_38;
FT_Property_Set(library, "truetype", "interpreter-version", &interpreter_version );
}
const shared_ptr<Texture>& getTexture() const { return textureAtlas ? textureAtlas->getTexture() : errorTex; }
unsigned roundToPixelsSigned(int size)
{
return (size > 0 ? (size + 32) : (size - 32)) / 64;
}
SizeSpecificData& getSizeData(unsigned size)
{
if (!face || !library)
return errorSizeData;
boost::unordered_map<unsigned, SizeSpecificData>::iterator it = sizeData.find(size);
if (it != sizeData.end())
{
return it->second;
}
else
{
float sizeMult = (size + FInt::FontSizePadding) / (float)face->height;
// set size data
sizeData[size] = SizeSpecificData();
sizeData[size].ascender = round_int(face->ascender * sizeMult);
sizeData[size].descender = round_int(face->descender * sizeMult);
sizeData[size].height = round_int(face->height * sizeMult);
return sizeData[size];
}
}
int getKerning(unsigned previous, unsigned current, unsigned size)
{
if (!face || !library)
return 0;
GlyphKey key = GlyphKey(current, previous);
float sizeMult = (size + FInt::FontSizePadding) / (float)face->height;
KerningMap::iterator it = kerningMap.find(key);
if (it != kerningMap.end())
return round_int(it->second * sizeMult);
unsigned previousChId = FT_Get_Char_Index(face, previous);
unsigned currentChId = FT_Get_Char_Index(face, current);
FT_Vector delta;
FT_Get_Kerning(face, previousChId, currentChId, FT_KERNING_UNSCALED, &delta);
kerningMap[key] = delta.x;
return round_int(delta.x * sizeMult);
}
boost::uint64_t getAtlasId(unsigned character, unsigned size)
{
boost::uint64_t atlasId = fontId * (boost::uint64_t)0xffffffff;
atlasId += size * 0x10FFFF; // max UTF8 value
atlasId += character;
return atlasId;
}
void releaseAtlas()
{
textureAtlas = NULL;
}
void setAtlas(TextureAtlas* atlas)
{
textureAtlas = atlas;
}
bool setGlyphToSlot(unsigned character, unsigned size)
{
FT_Size_RequestRec_ request = {FT_SIZE_REQUEST_TYPE_REAL_DIM, 0, (size + FInt::FontSizePadding) * 64, 0, 0};
FT_Error error = FT_Request_Size(face, &request);
RBXASSERT(error == FT_Err_Ok);
if (error)
return false;
unsigned glyph_index = FT_Get_Char_Index(face, character);
error = FT_Load_Glyph(face, glyph_index, FT_LOAD_DEFAULT);
RBXASSERT(error == FT_Err_Ok);
if (error)
return false;
return true;
}
const TypesetterDynamic::Glyph& getGlyphMetrics(unsigned character, unsigned size)
{
if (!face || !library)
return errorGlyph;
GlyphKey key(character, size);
GlyphMap::iterator it = glyphMap.find(key);
if (it != glyphMap.end())
return it->second;
if (!setGlyphToSlot(character, size))
return errorGlyph;
const FT_GlyphSlot& slot = face->glyph;
TypesetterDynamic::Glyph& glyph = glyphMap[key] = TypesetterDynamic::Glyph();
glyph.height = roundToPixelsSigned(slot->metrics.height);
glyph.width = roundToPixelsSigned(slot->metrics.width);
glyph.xadvance = roundToPixelsSigned(slot->advance.x);
glyph.xoffset = roundToPixelsSigned(slot->metrics.horiBearingX);
glyph.yoffset = roundToPixelsSigned(slot->metrics.horiBearingY);
return glyph;
}
const TextureRegion& getGlyphUVs(unsigned character, unsigned size)
{
if (!face || !library || !textureAtlas)
return errorTexRegion;
const TypesetterDynamic::Glyph& glyph = getGlyphMetrics(character, size);
// try get UV from tex atlas
boost::uint64_t atlasId = getAtlasId(character, size);
const TextureRegion* texRegion;
if (textureAtlas->getRegion(atlasId, texRegion))
return *texRegion;
// early out heuristic
Vector2int16 lastFailedAtlasSize = textureAtlas->getLastFailedSize();
if (lastFailedAtlasSize.y <= (int)size)
return errorTexRegion;
// rasterize, insert to atlas and profit
if (lastFailedAtlasSize.x > glyph.width && lastFailedAtlasSize.y > glyph.height)
{
if (!setGlyphToSlot(character, size))
return errorTexRegion;
if (face->glyph->format != FT_GLYPH_FORMAT_BITMAP)
{
FT_Error error = FT_Render_Glyph( face->glyph, FT_RENDER_MODE_NORMAL );
RBXASSERT(error == FT_Err_Ok);
if (error)
return errorTexRegion;
}
FT_Bitmap& bitmap = face->glyph->bitmap;
if (textureAtlas->insert(atlasId, Vector2int16(bitmap.width, bitmap.rows), bitmap.pitch, bitmap.buffer, texRegion))
return *texRegion;
}
return errorTexRegion;
}
private:
FT_Library library;
FT_Face face;
typedef std::pair<unsigned, unsigned> GlyphKey;
typedef boost::unordered_map<GlyphKey, TypesetterDynamic::Glyph> GlyphMap;
typedef boost::unordered_map<GlyphKey, int> KerningMap;
GlyphMap glyphMap;
KerningMap kerningMap;
boost::unordered_map<unsigned, SizeSpecificData> sizeData;
TypesetterDynamic::Glyph errorGlyph;
TextureRegion errorTexRegion;
shared_ptr<Texture> errorTex;
SizeSpecificData errorSizeData;
unsigned fontId;
std::string dataBuffer;
TextureAtlas* textureAtlas;
};
TypesetterDynamic::TypesetterDynamic(TextureAtlas* textureAtlas, RBX::Graphics::TextureManager* textureManager, const std::string& fontPath, float legacyHeightScale, unsigned fontId, bool retina)
: legacyHeightScale(legacyHeightScale)
, retinaScale(retina ? 2 : 1)
{
glyphProvider = new GlyphProvider(fontPath, textureAtlas, fontId);
}
TypesetterDynamic::~TypesetterDynamic()
{
delete glyphProvider;
}
static void drawRect(Adorn* adorn, bool useClipping, const Rect2D& clippingRect, const Rotation2D& rotation, const Rect2D& glyphRect, const Vector2& uvtl, const Vector2& uvbr, const Color4& color)
{
if (!rotation.empty())
{
adorn->rect2d(glyphRect, uvtl, uvbr, color, rotation);
}
else if (useClipping)
{
Rect2D clippedRect = clippingRect.intersect(glyphRect);
if (clippedRect == glyphRect)
{
adorn->rect2d(glyphRect, uvtl, uvbr, color, Rotation2D());
}
else if (clippedRect != RBX::Rect2D::xywh(0,0,0,0))
{
adorn->rect2d(glyphRect, uvtl, uvbr, color, clippingRect);
}
}
else
{
adorn->rect2d(glyphRect, uvtl, uvbr, color, Rotation2D());
}
}
static float fontAdjustX(float x, int width, float scale, Text::XAlign align)
{
switch (align)
{
case Text::XALIGN_CENTER:
return x - (width / 2) * scale;
case Text::XALIGN_RIGHT:
return x - width * scale;
default:
return x;
}
}
static float fontAdjustY(float y, int height, float scale, Text::YAlign align)
{
switch (align)
{
case Text::YALIGN_CENTER:
return y - (height / 2) * scale;
case Text::YALIGN_BOTTOM:
return y - height * scale;
default:
return y;
}
}
static inline bool isCharacterWhitespace(unsigned ch)
{
return ch == ' ' || ch == '\t';
}
static inline bool isCharacterZeroWidth(char ch)
{
return ch == '\1';
}
static const RBX::Vector2 kBorderOffsets[] =
{
RBX::Vector2(-1, -1),
RBX::Vector2(+1, -1),
RBX::Vector2(-1, +1),
RBX::Vector2(+1, +1),
RBX::Vector2(0, 0),
};
void TypesetterDynamic::loadResources(RBX::Graphics::TextureManager* textureManager, RBX::Graphics::TextureAtlas* glyphAtlas)
{
glyphProvider->setAtlas(glyphAtlas);
}
void TypesetterDynamic::releaseResources()
{
glyphProvider->releaseAtlas();
}
const shared_ptr<Texture>& TypesetterDynamic::getTexture() const
{
return glyphProvider->getTexture();
}
void TypesetterDynamic::drawCursor(Adorn* adorn, bool useClipping, const Rect2D& clippingRect, const Rotation2D& rotation, const Rect2D& glyphRect, const Color4& color) const
{
adorn->setIgnoreTexture(true);
drawRect(adorn, useClipping, clippingRect, rotation, glyphRect, Vector2::one(), Vector2::one(), color);
adorn->setIgnoreTexture(false);
}
void TypesetterDynamic::render(Adorn* adorn,
const Color4& color, const Color4& outline, float alpha, RBX::Text::XAlign xalign, std::vector<GlyphLine>& lines, float scale,
unsigned measureSize, unsigned renderSize, unsigned ascender,
float startx, float starty, float height,
const Rect2D& clippingRect, const Rotation2D& rotation) const
{
shared_ptr<Texture> texture = glyphProvider->getTexture();
if (!texture)
return;
bool useClipping = (clippingRect != RBX::Rect2D::xyxy(-1,-1,-1,-1));
Vector2 uvOffset = Vector2(FONT_PADDING / texture->getWidth(), FONT_PADDING / texture->getHeight());
unsigned previousChar = 0;
for (int offsetIndex = (outline.a > 0.05f ? 0 : 4); offsetIndex < 5; ++offsetIndex)
{
float dx = kBorderOffsets[offsetIndex].x, dy = kBorderOffsets[offsetIndex].y;
Color4 drawColor = (offsetIndex == 4) ? color : outline;
drawColor.a *= alpha;
for (size_t li = 0; li < lines.size(); ++li)
{
const GlyphLine& line = lines[li];
float linex = fontAdjustX(startx, line.width, scale, xalign);
float liney = starty + line.yoffset * scale;
float x = 0;
for (size_t i = 0; i < line.length; ++i)
{
unsigned unicodeChar = line.data[i];
// Cursor character
if (unicodeChar == '\1')
{
drawCursor(adorn, useClipping, clippingRect, rotation,
Rect2D::xywh(linex + dx + x , liney + dy, 1, height),
drawColor);
continue;
}
Glyph glyph = glyphProvider->getGlyphMetrics(unicodeChar, measureSize);
x += glyphProvider->getKerning(previousChar, unicodeChar, measureSize);
if (!isCharacterWhitespace(unicodeChar))
{
Rect2D rect = Rect2D::xywh(linex + dx + (x + glyph.xoffset) * scale, liney + dy + (ascender - glyph.yoffset) * scale, glyph.width * scale, glyph.height * scale);
rect = rect.expand(FONT_PADDING);
TextureRegion textureRegion = glyphProvider->getGlyphUVs(unicodeChar, renderSize);
Rect2D uvs = Rect2D::xyxy(textureRegion.x / (float)texture->getWidth(), textureRegion.y / (float)texture->getHeight(),
(textureRegion.x + textureRegion.width) / (float)texture->getWidth(), (textureRegion.y + textureRegion.height) / (float)texture->getHeight());
drawRect(adorn, useClipping, clippingRect, rotation,
rect,
Vector2(uvs.x0() - uvOffset.x, uvs.y0() - uvOffset.y),
Vector2(uvs.x1() + uvOffset.x, uvs.y1() + uvOffset.y),
drawColor);
}
x += glyph.xadvance;
previousChar = unicodeChar;
}
}
}
}
Vector2 TypesetterDynamic::drawImpl(Adorn* adorn, const std::vector<unsigned>& stringUnicode,
const Vector2& position, float size, const Color4& color, const Color4& outline,
RBX::Text::XAlign xalign, RBX::Text::YAlign yalign, const Vector2& availableSpace,
const Rect2D& clippingRect, const Rotation2D& rotation) const
{
bool useAvailableSpace = availableSpace.x != 0;
float height = size * legacyHeightScale * retinaScale;
float scale = 1.0f / retinaScale;
std::vector<GlyphLine> lines;
Vector2int16 intAvailableSpace = Vector2int16(availableSpace / scale);
Vector2int16 intSize = layout(stringUnicode, &lines, height, intAvailableSpace, useAvailableSpace, false, NULL);
unsigned ascender = glyphProvider->getSizeData(height).ascender;
float startx, starty;
if (adorn->useFontSmoothScalling())
{
startx = (position.x);
starty = fontAdjustY((position.y), intSize.y, scale, yalign);
}
else
{
startx = round_int(position.x) ;
starty = fontAdjustY(round_int(position.y), intSize.y, scale, yalign);
}
shared_ptr<Texture> texture = glyphProvider->getTexture();
if (!texture)
return Vector2::zero();
render(adorn, color, outline, 1, xalign, lines, scale, height, height, ascender, startx, starty, height, clippingRect, rotation);
return Vector2(intSize) * scale;
}
Vector2 TypesetterDynamic::drawScaledImpl(Adorn* adorn, const std::vector<unsigned>& stringUnicode, const Vector2& position,
const Color4& color, const Color4& outline, RBX::Text::XAlign xalign, RBX::Text::YAlign yalign,
const Vector2& availableSpace, const Rect2D& clippingRect, const Rotation2D& rotation) const
{
float height = 48; // 48 is our highest font scale right now
std::vector<GlyphLine> lines;
Vector2int16 intSize = layoutRatio(stringUnicode, &lines, height, availableSpace);
// ratio is the smaller one from XY ratios to make text fit
float floatIntSizeRatio = std::min((float)availableSpace.x / (float)intSize.x, (float)availableSpace.y / (float)intSize.y);
floatIntSizeRatio = std::min(1.0f, floatIntSizeRatio);
intSize.x = float(intSize.x) * floatIntSizeRatio;
intSize.y = float(intSize.y) * floatIntSizeRatio;
// recompute based on result
float referenceSize = height;
unsigned ascenderReference = glyphProvider->getSizeData(referenceSize).ascender;
height = height * floatIntSizeRatio;
// compute size of glyph that we will use as texture.
float renderedGlyphSize = std::ceil(height / 6) * 6;
Vector2 measuredSize = measureLining(lines, referenceSize);
Vector2 realSize = measuredSize * floatIntSizeRatio;
if (!adorn)
return realSize;
// hide when glyphs get too small
float alpha = alpha = -2 + height * 0.5f;
alpha = G3D::clamp(alpha, 0, 1);
if (alpha == 0)
return realSize;
float startx = position.x;
float starty = fontAdjustY(position.y, measuredSize.y, floatIntSizeRatio, yalign);
shared_ptr<Texture> texture = glyphProvider->getTexture();
if (!texture)
return Vector2::zero();
render(adorn, color, outline, alpha, xalign, lines, floatIntSizeRatio, referenceSize, renderedGlyphSize, ascenderReference, startx, starty, height, clippingRect, rotation);
return realSize;
}
Vector2 TypesetterDynamic::draw(Adorn* adorn, const std::string& s,
const Vector2& position, float size, bool autoScale, const Color4& color, const Color4& outline,
RBX::Text::XAlign xalign, RBX::Text::YAlign yalign, const Vector2& availableSpace,
const Rect2D& clippingRect, const Rotation2D& rotation) const
{
bool useAvailableSpace = availableSpace.x != 0;
std::vector<unsigned> stringUnicode;
decodeUTF8(s, &stringUnicode);
if (autoScale && useAvailableSpace)
return drawScaledImpl(adorn, stringUnicode, position, color, outline, xalign, yalign, availableSpace, clippingRect, rotation);
return drawImpl(adorn, stringUnicode, position, size, color, outline, xalign, yalign, availableSpace, clippingRect, rotation);
}
int TypesetterDynamic::getCursorPositionInText(const std::string& s, const RBX::Vector2& position, float size,
RBX::Text::XAlign xalign, RBX::Text::YAlign yalign, const RBX::Vector2& availableSpace, const Rotation2D& rotation,
RBX::Vector2 cursorPos) const
{
std::vector<unsigned> stringUnicode;
decodeUTF8(s, &stringUnicode);
bool useAvailableSpace = availableSpace.x != 0;
float height = size * legacyHeightScale * retinaScale;
float scale = 1.0f / retinaScale;
std::vector<GlyphLine> lines;
Vector2int16 intAvailableSpace = Vector2int16(availableSpace / scale);
Vector2int16 intSize = layout(stringUnicode, &lines, height, intAvailableSpace, useAvailableSpace, false, NULL);
float startx = round_int(position.x);
float starty = fontAdjustY(round_int(position.y), intSize.y, scale, yalign);
Vector2 localCursor = rotation.inverse().rotate(cursorPos);
unsigned previousChar = 0;
for (size_t li = 0; li < lines.size(); ++li)
{
const GlyphLine& line = lines[li];
float linex = fontAdjustX(startx, line.width, scale, xalign);
float liney = starty + line.yoffset * scale;
if (liney + height > localCursor.y || li + 1 == lines.size())
{
int lineOffset = line.data - stringUnicode.data();
int x = 0;
for (size_t i = 0; i < line.length; ++i)
{
unsigned unicodeChar = line.data[i];
if (isCharacterZeroWidth(unicodeChar))
continue;
Glyph glyph = glyphProvider->getGlyphMetrics(unicodeChar, size);
x += glyphProvider->getKerning(previousChar, unicodeChar, height);
x += glyph.xadvance;
if (linex + x * scale > localCursor.x)
{
// Select current glyph if we're within 2/3 of the advance
if (linex + x * scale - glyph.xadvance * scale * 0.33f > localCursor.x)
return lineOffset + i;
else
return lineOffset + i + 1;
}
previousChar = unicodeChar;
}
return lineOffset + line.length;
}
}
return -1;
}
Vector2 TypesetterDynamic::measure(const std::string& s, float size, const Vector2& availableSpace, bool* textFits) const
{
std::vector<unsigned> stringUnicode;
decodeUTF8(s, &stringUnicode);
bool useAvailableSpace = availableSpace.x != 0;
float height = size * legacyHeightScale * retinaScale;
float scale = 1.0 / retinaScale;
Vector2int16 intAvailableSpace = Vector2int16(availableSpace / scale);
Vector2int16 intSize = layout(stringUnicode, NULL, height, intAvailableSpace, useAvailableSpace, DFFlag::TextScaleDontWrapInWords, textFits);
if (DFFlag::TextScaleDontWrapInWords && *textFits == false) {
intSize = layout(stringUnicode, NULL, height, intAvailableSpace, useAvailableSpace, false, textFits);
}
return Vector2(intSize) * scale;
}
template <typename GlyphLine> static inline void pushLine(std::vector<GlyphLine>* lines, const std::vector<unsigned>& stringUnicode, int lineStart, int lineEnd, int yoffset, int width)
{
if (lines)
{
GlyphLine line = {stringUnicode.data() + lineStart, lineEnd - lineStart, yoffset, width};
lines->push_back(line);
}
}
Vector2int16 TypesetterDynamic::layoutRatio(const std::vector<unsigned>& stringUnicode, std::vector<GlyphLine>* lines, int size, const Vector2& availableSpace) const
{
float ratio = availableSpace.x / availableSpace.y;
int height = size;
float min = 10;
float max = height * stringUnicode.size(); // this is just to have a reasonable max value to start with, height is bigger then width when talking about fonts
static const float ratioBiasScale = 1.01f;
max = std::max(max, height * ratio * ratioBiasScale); // make sure that text will fit vertically (its is at least as big as height of font). Multiply by small paddig to be really sure
float actualRatio = -1; // real one will never be bellow zero
static const float tolerance = 0.05f;
bool textFits = false;
float x = max;
Vector2int16 availableSpaceByRatio;
float bestRatioSoFar = FLT_MAX;
std::vector<GlyphLine> bestLinesSoFar = *lines;
Vector2int16 bestSizeSoFar;
unsigned cnt = 0;
static const unsigned kMaxIterations = 10;
// binary search for nice fit, but do not search to long. Max kMaxIterations. In worse case we will have some solution
// good thing is it will be same for same string and ratio
while( cnt < kMaxIterations )
{
float y = x / ratio;
actualRatio = x / y;
availableSpaceByRatio = Vector2int16(x, y);
if (lines)
lines->clear();
Vector2int16 intSize = layout(stringUnicode, lines, height, availableSpaceByRatio, true, true, &textFits);
actualRatio = (float)intSize.x / (float)intSize.y;
if (textFits)
{
if (std::abs(actualRatio - ratio) < std::abs(bestRatioSoFar - ratio))
{
bestLinesSoFar = *lines;
bestRatioSoFar = actualRatio;
bestSizeSoFar = intSize;
}
max = x;
x = min + (x - min) / 2;
if (actualRatio >= ratio - tolerance && actualRatio <= ratio + tolerance)
break;
}
else
{
min = x;
x = x + (max - x) / 2;
}
if (max - x <= 0 || x - min <= 0) // we can make it bigger or smaller, so lets just stop searching and go with the result we have
break;
++cnt;
}
*lines = bestLinesSoFar;
return bestSizeSoFar;
}
Vector2 TypesetterDynamic::measureLining(std::vector<GlyphLine>& lines, unsigned size) const
{
Vector2 ret;
int height = size;
float y = 0;
for (size_t li = 0; li < lines.size(); ++li)
{
const GlyphLine& line = lines[li];
float x = 0;
y += height;
unsigned previousChar = 0;
for (size_t i = 0; i < line.length; ++i)
{
unsigned unicodeChar = line.data[i];
Glyph glyph = glyphProvider->getGlyphMetrics(unicodeChar, size);
x += glyphProvider->getKerning(previousChar, unicodeChar, size);
x += glyph.xadvance;
previousChar = unicodeChar;
}
if (ret.x < x)
ret.x = x;
if (ret.y < y)
ret.y = y;
}
return ret;
}
void TypesetterDynamic::decodeUTF8(const std::string& string, std::vector<unsigned>* out) const
{
out->clear();
out->reserve(string.size());
uint32_t codepoint;
uint32_t state = 0;
for (unsigned i = 0; i < string.size(); ++i)
if (utf8DecodeInternal(&state, &codepoint, (unsigned char)string.c_str()[i]) == UTF8_ACCEPT)
out->push_back(codepoint);
}
Vector2int16 TypesetterDynamic::layout(const std::vector<unsigned>& stringUnicode, std::vector<GlyphLine>* lines, int size, const Vector2int16& availableSpace, bool useAvailableSpace, bool onlyProperFit, bool* textFits) const
{
int x = 0, y = 0;
int height = size;
int lastKerningOffset = 0;
int lineStart = 0;
int lineMaxWidth = 0;
int wordStart = 0;
int wordWidth = 0;
int whitespaceWidth = 0;
unsigned previousChar = 0;
for (size_t i = 0; i < stringUnicode.size(); ++i)
{
unsigned currentUnicode = stringUnicode[i];
if (isCharacterZeroWidth(currentUnicode))
{
lastKerningOffset = 0;
continue;
}
if (currentUnicode == '\n')
{
// hard line break
pushLine(lines, stringUnicode, lineStart, i, y, x);
lineStart = i + 1;
lineMaxWidth = std::max(lineMaxWidth, x);
wordStart = i + 1;
wordWidth = 0;
whitespaceWidth = 0;
x = 0;
y += height;
continue;
}
Glyph glyph = glyphProvider->getGlyphMetrics(currentUnicode, size);
int xoffset = glyphProvider->getKerning(previousChar, currentUnicode, size) + glyph.xadvance;
if (isCharacterWhitespace(currentUnicode))
{
if (wordWidth != 0)
whitespaceWidth = 0;
wordStart = i;
wordWidth = 0;
whitespaceWidth += xoffset;
}
else
{
if (wordWidth == 0)
wordStart = i;
wordWidth += xoffset;
}
x += xoffset;
if (useAvailableSpace && x > availableSpace.x)
{
// soft line break
if (wordWidth <= availableSpace.x)
{
// word fits on next line; move it there, discard whitespace
pushLine(lines, stringUnicode, lineStart, wordStart, y, x - wordWidth - whitespaceWidth);
lineStart = wordStart;
lineMaxWidth = std::max(lineMaxWidth, x - wordWidth - whitespaceWidth);
x = wordWidth;
}
else
{
if (onlyProperFit)
{
// early out
if (textFits)
*textFits = false;
return Vector2int16(lineMaxWidth, y);
}
// word does not fit, cut
pushLine(lines, stringUnicode, lineStart, i, y, x - xoffset);
lineStart = i;
lineMaxWidth = std::max(lineMaxWidth, x - xoffset);
x = xoffset;
}
wordWidth = x;
whitespaceWidth = 0;
y += height;
}
previousChar = currentUnicode;
}
// last line
pushLine(lines, stringUnicode, lineStart, stringUnicode.size(), y, x);
lineMaxWidth = std::max(lineMaxWidth, x);
y += height;
// clamp line count
if (useAvailableSpace && y > availableSpace.y)
{
int clampedLineCount = std::max(1, availableSpace.y / height);
if (lines)
lines->resize(clampedLineCount);
if (textFits)
*textFits = false;
return Vector2int16(lineMaxWidth, clampedLineCount * height);
}
else
{
if (textFits)
*textFits = true;
return Vector2int16(lineMaxWidth, y);
}
}
}
}