mirror of
https://github.com/copyrighttxt/watrbx-game-engine.git
synced 2026-09-05 05:07:48 +00:00
GEEKING
This commit is contained in:
@@ -0,0 +1,923 @@
|
||||
#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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user