mirror of
https://github.com/copyrighttxt/watrbx-game-engine.git
synced 2026-09-04 20:57:49 +00:00
775 lines
25 KiB
C++
775 lines
25 KiB
C++
#include "stdafx.h"
|
|
#include "TypesetterBitmap.h"
|
|
|
|
#include "GfxBase/Adorn.h"
|
|
#include "StringConv.h"
|
|
#include "TextureManager.h"
|
|
#include "GfxCore/Texture.h"
|
|
|
|
FASTFLAG(FontSmoothScalling)
|
|
DYNAMIC_FASTFLAGVARIABLE(TextScaleDontWrapInWords, false)
|
|
|
|
#define FONT_PADDING 1.0f
|
|
|
|
namespace RBX
|
|
{
|
|
namespace Graphics
|
|
{
|
|
static void readData(std::istream& in, void* data, int size)
|
|
{
|
|
in.read(static_cast<char*>(data), size);
|
|
|
|
if (in.gcount() != size)
|
|
throw RBX::runtime_error("Unexpected end of file while reading %d bytes", size);
|
|
}
|
|
|
|
TypesetterBitmap::TypesetterBitmap(TextureManager* textureManager, const std::string& fontPath, const std::string& texturePath, float legacyHeightScale, bool retina)
|
|
: legacyHeightScale(legacyHeightScale)
|
|
, retinaScale(retina ? 2 : 1)
|
|
{
|
|
std::ifstream in(utf8_decode(fontPath).c_str(), std::ios::in | std::ios::binary);
|
|
|
|
char header[4];
|
|
readData(in, header, 4);
|
|
|
|
int fileSize = 0;
|
|
readData(in, &fileSize, sizeof(fileSize));
|
|
|
|
if (memcmp(header, "RBXF", 4) != 0 || fileSize < 16)
|
|
throw RBX::runtime_error("Corrupt file header");
|
|
|
|
fontData.reset(new char[fileSize]);
|
|
|
|
readData(in, fontData.get(), fileSize);
|
|
|
|
// Setup pointers to various tables
|
|
info = *reinterpret_cast<Info*>(fontData.get());
|
|
|
|
glyphRemap = reinterpret_cast<const unsigned char*>(fontData.get() + sizeof(Info));
|
|
sizeTable = reinterpret_cast<const float*>(reinterpret_cast<const char*>(glyphRemap) + 256);
|
|
glyphTable = reinterpret_cast<const Glyph*>(reinterpret_cast<const char*>(sizeTable) + info.sizeCount * sizeof(float));
|
|
kerningTable = reinterpret_cast<const char*>(glyphTable) + info.sizeCount * info.glyphCount * sizeof(Glyph);
|
|
|
|
// Load font texture
|
|
texture = textureManager->load(ContentId(texturePath), TextureManager::Fallback_BlackTransparent);
|
|
fontTexturePath = texturePath;
|
|
}
|
|
|
|
void TypesetterBitmap::loadResources(RBX::Graphics::TextureManager* textureManager, RBX::Graphics::TextureAtlas* glyphAtlas)
|
|
{
|
|
if (!fontTexturePath.empty() && !texture.getTexture().get())
|
|
{
|
|
texture = textureManager->load(ContentId(fontTexturePath), TextureManager::Fallback_BlackTransparent);
|
|
}
|
|
}
|
|
|
|
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 void drawCursor(Adorn* adorn, bool useClipping, const Rect2D& clippingRect, const Rotation2D& rotation, const Rect2D& glyphRect, const Color4& color)
|
|
{
|
|
adorn->setIgnoreTexture(true);
|
|
|
|
drawRect(adorn, useClipping, clippingRect, rotation, glyphRect, Vector2::one(), Vector2::one(), color);
|
|
|
|
adorn->setIgnoreTexture(false);
|
|
}
|
|
|
|
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(char 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),
|
|
};
|
|
|
|
Vector2 TypesetterBitmap::drawImpl(Adorn* adorn, const std::string& s,
|
|
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 useClipping = (clippingRect != RBX::Rect2D::xyxy(-1,-1,-1,-1));
|
|
bool useAvailableSpace = availableSpace.x != 0;
|
|
|
|
float height = size * legacyHeightScale * retinaScale;
|
|
int sizeIndex = getSizeIndex(height);
|
|
float scale = height / sizeTable[sizeIndex] / retinaScale;
|
|
|
|
std::vector<GlyphLine> lines;
|
|
|
|
Vector2int16 intAvailableSpace = Vector2int16(availableSpace / scale);
|
|
Vector2int16 intSize = layout(s, &lines, sizeIndex, intAvailableSpace, useAvailableSpace, false, NULL);
|
|
|
|
const Glyph* glyphTableSize = glyphTable + info.glyphCount * sizeIndex;
|
|
|
|
float startx, starty;
|
|
if (adorn->useFontSmoothScalling())
|
|
{
|
|
startx = (position.x);
|
|
starty = fontAdjustY((position.y), intSize.y, scale, yalign);
|
|
}
|
|
else
|
|
{
|
|
startx = floorf(position.x + 0.5f);
|
|
starty = fontAdjustY(floorf(position.y + 0.5f), intSize.y, scale, yalign);
|
|
}
|
|
|
|
Vector2 uvOffset(FONT_PADDING / static_cast<float>(texture.getTexture().get()->getWidth()),
|
|
FONT_PADDING / static_cast<float>(texture.getTexture().get()->getHeight()));
|
|
|
|
for (int offsetIndex = (outline.a > 0.05f ? 0 : 4); offsetIndex < 5; ++offsetIndex)
|
|
{
|
|
float dx = kBorderOffsets[offsetIndex].x, dy = kBorderOffsets[offsetIndex].y;
|
|
const Color4& drawColor = (offsetIndex == 4) ? color : outline;
|
|
|
|
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;
|
|
|
|
int lastKerningOffset = 0;
|
|
int x = 0;
|
|
|
|
for (size_t i = 0; i < line.length; ++i)
|
|
{
|
|
char ch = line.data[i];
|
|
|
|
int glyphIndex = glyphRemap[static_cast<unsigned char>(ch)];
|
|
|
|
if (glyphIndex == 255)
|
|
{
|
|
if (!isCharacterZeroWidth(ch))
|
|
lastKerningOffset = 0;
|
|
|
|
// Cursor character
|
|
if (ch == '\1')
|
|
{
|
|
drawCursor(adorn, useClipping, clippingRect, rotation,
|
|
Rect2D::xywh(linex + dx + x * scale, liney + dy, 1, height),
|
|
drawColor);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
const Glyph& glyph = glyphTableSize[glyphIndex];
|
|
|
|
x += kerningTable[lastKerningOffset + glyphIndex];
|
|
|
|
if (!isCharacterWhitespace(ch))
|
|
{
|
|
Rect2D rect = Rect2D::xywh(linex + dx + (x + glyph.xoffset) * scale, liney + dy + glyph.yoffset * scale, glyph.width * scale, glyph.height * scale);
|
|
rect = rect.expand(FONT_PADDING);
|
|
|
|
drawRect(adorn, useClipping, clippingRect, rotation,
|
|
rect,
|
|
Vector2(glyph.u0 - uvOffset.x, glyph.v0 - uvOffset.y),
|
|
Vector2(glyph.u1 + uvOffset.x, glyph.v1 + uvOffset.y),
|
|
drawColor);
|
|
}
|
|
|
|
x += glyph.xadvance;
|
|
|
|
lastKerningOffset = glyph.kerningOffset;
|
|
}
|
|
}
|
|
}
|
|
|
|
return Vector2(intSize) * scale;
|
|
}
|
|
|
|
Vector2 TypesetterBitmap::drawScaledImpl(Adorn* adorn, const std::string& s, 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
|
|
{
|
|
bool useClipping = (clippingRect != RBX::Rect2D::xyxy(-1,-1,-1,-1));
|
|
|
|
float height = 48 * legacyHeightScale * retinaScale; // 48 is our highest font scale right now
|
|
int sizeIndex = getSizeIndex(height);
|
|
|
|
std::vector<GlyphLine> lines;
|
|
|
|
Vector2int16 intSize = layoutRatio(s, &lines, sizeIndex, 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
|
|
height = height * floatIntSizeRatio;
|
|
sizeIndex = getSizeIndex(height);
|
|
|
|
// we will be interpolating between current size and step smaller size
|
|
int stepSmallerSizeIndex = std::max(0, sizeIndex - 1);
|
|
|
|
float bigScale = sizeTable[sizeIndex];
|
|
float smallScale = sizeTable[stepSmallerSizeIndex];
|
|
|
|
float sizeLerpVal = 1;
|
|
if (bigScale != smallScale)
|
|
{
|
|
sizeLerpVal = (height - smallScale) / (bigScale - smallScale);
|
|
sizeLerpVal = 1 - sizeLerpVal;
|
|
}
|
|
|
|
// this step is to assure that nothing overflows outside bounds
|
|
Vector2 realSize1 = measureLining(lines, sizeIndex);
|
|
Vector2 realSize2 = measureLining(lines, stepSmallerSizeIndex);
|
|
Vector2 totallyRealSize = lerp(realSize1, realSize2, sizeLerpVal);
|
|
|
|
if (!adorn)
|
|
return totallyRealSize;
|
|
|
|
float alpha = 1.0f;
|
|
float scale = 1.0f;
|
|
if (totallyRealSize.x > availableSpace.x )
|
|
{
|
|
alpha = availableSpace.x / totallyRealSize.x;
|
|
// this will make step faster
|
|
alpha -= 0.75f;
|
|
alpha *= 6;
|
|
alpha = G3D::clamp(alpha, 0, 1);
|
|
|
|
scale *= availableSpace.x / totallyRealSize.x;
|
|
}
|
|
|
|
if (alpha == 0)
|
|
return totallyRealSize;
|
|
|
|
const Glyph* glyphTableSize = glyphTable + info.glyphCount * sizeIndex;
|
|
const Glyph* glyphTableSizeSmaller = glyphTable + info.glyphCount * stepSmallerSizeIndex;
|
|
|
|
float startx = position.x;
|
|
float starty = fontAdjustY(position.y, totallyRealSize.y, scale, yalign);
|
|
|
|
Vector2 lineSizeRatios = Vector2(totallyRealSize.x / (float)intSize.x, totallyRealSize.y / (float)intSize.y);
|
|
Vector2 uvOffset(FONT_PADDING / texture.getTexture().get()->getWidth(), FONT_PADDING / texture.getTexture().get()->getHeight());
|
|
|
|
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 * lineSizeRatios.x, scale * floatIntSizeRatio, xalign);
|
|
float liney = starty + line.yoffset * lineSizeRatios.y * scale * floatIntSizeRatio;
|
|
|
|
int lastKerningOffset = 0;
|
|
int lastKerningOffset2 = 0;
|
|
float x = 0;
|
|
float x2 = 0;
|
|
|
|
for (size_t i = 0; i < line.length; ++i)
|
|
{
|
|
char ch = line.data[i];
|
|
|
|
int glyphIndex = glyphRemap[static_cast<unsigned char>(ch)];
|
|
|
|
if (glyphIndex == 255)
|
|
{
|
|
if (!isCharacterZeroWidth(ch))
|
|
lastKerningOffset = 0;
|
|
|
|
// Cursor character
|
|
if (ch == '\1')
|
|
{
|
|
drawCursor(adorn, useClipping, clippingRect, rotation,
|
|
Rect2D::xywh(linex + dx + x * scale, liney + dy, 1, height),
|
|
drawColor);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
const Glyph& glyph = glyphTableSize[glyphIndex];
|
|
x += kerningTable[lastKerningOffset + glyphIndex];
|
|
|
|
const Glyph& glyph2 = glyphTableSizeSmaller[glyphIndex];
|
|
x2 += kerningTable[lastKerningOffset2 + glyphIndex];
|
|
|
|
if (!isCharacterWhitespace(ch))
|
|
{
|
|
Rect2D rect1 = Rect2D::xywh(linex + dx + (x + glyph.xoffset) * scale, liney + dy + glyph.yoffset * scale, glyph.width * scale, glyph.height * scale);
|
|
Rect2D rect2 = Rect2D::xywh(linex + dx + (x2 + glyph2.xoffset) * scale, liney + dy + glyph2.yoffset * scale, glyph2.width * scale, glyph2.height * scale);
|
|
|
|
Rect2D lerpRect = Rect2D::xyxy(
|
|
G3D::lerp(rect1.x0(), rect2.x0(), sizeLerpVal),
|
|
G3D::lerp(rect1.y0(), rect2.y0(), sizeLerpVal),
|
|
G3D::lerp(rect1.x1(), rect2.x1(), sizeLerpVal),
|
|
G3D::lerp(rect1.y1(), rect2.y1(), sizeLerpVal)
|
|
);
|
|
|
|
lerpRect = lerpRect.expand(FONT_PADDING);
|
|
|
|
drawRect(adorn, useClipping, clippingRect, rotation,
|
|
lerpRect,
|
|
Vector2(glyph.u0 - uvOffset.x, glyph.v0 - uvOffset.y),
|
|
Vector2(glyph.u1 + uvOffset.x, glyph.v1 + uvOffset.y),
|
|
drawColor);
|
|
}
|
|
|
|
x += glyph.xadvance;
|
|
x2 += glyph2.xadvance;
|
|
lastKerningOffset = glyph.kerningOffset;
|
|
lastKerningOffset2 = glyph2.kerningOffset;
|
|
}
|
|
}
|
|
}
|
|
|
|
return Vector2(intSize) * scale;
|
|
}
|
|
|
|
Vector2 TypesetterBitmap::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;
|
|
|
|
if (autoScale && useAvailableSpace)
|
|
return drawScaledImpl(adorn, s, position, color, outline, xalign, yalign, availableSpace, clippingRect, rotation);
|
|
|
|
return drawImpl(adorn, s, position, size, color, outline, xalign, yalign, availableSpace, clippingRect, rotation);
|
|
}
|
|
|
|
int TypesetterBitmap::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
|
|
{
|
|
bool useAvailableSpace = availableSpace.x != 0;
|
|
|
|
float height = size * legacyHeightScale * retinaScale;
|
|
int sizeIndex = getSizeIndex(height);
|
|
float scale = height / sizeTable[sizeIndex] / retinaScale;
|
|
|
|
std::vector<GlyphLine> lines;
|
|
|
|
Vector2int16 intAvailableSpace = Vector2int16(availableSpace / scale);
|
|
Vector2int16 intSize = layout(s, &lines, sizeIndex, intAvailableSpace, useAvailableSpace, false, NULL);
|
|
|
|
const Glyph* glyphTableSize = glyphTable + info.glyphCount * sizeIndex;
|
|
|
|
float startx = floorf(position.x + 0.5f);
|
|
float starty = fontAdjustY(floorf(position.y + 0.5f), intSize.y, scale, yalign);
|
|
|
|
Vector2 localCursor = rotation.inverse().rotate(cursorPos);
|
|
|
|
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 - s.c_str();
|
|
|
|
int lastKerningOffset = 0;
|
|
int x = 0;
|
|
|
|
for (size_t i = 0; i < line.length; ++i)
|
|
{
|
|
char ch = line.data[i];
|
|
|
|
int glyphIndex = glyphRemap[static_cast<unsigned char>(ch)];
|
|
|
|
if (glyphIndex == 255)
|
|
{
|
|
if (!isCharacterZeroWidth(ch))
|
|
lastKerningOffset = 0;
|
|
|
|
continue;
|
|
}
|
|
|
|
const Glyph& glyph = glyphTableSize[glyphIndex];
|
|
|
|
x += kerningTable[lastKerningOffset + glyphIndex];
|
|
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;
|
|
}
|
|
|
|
lastKerningOffset = glyph.kerningOffset;
|
|
}
|
|
|
|
return lineOffset + line.length;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
Vector2 TypesetterBitmap::measure(const std::string& s, float size, const Vector2& availableSpace, bool* textFits) const
|
|
{
|
|
bool useAvailableSpace = availableSpace.x != 0;
|
|
|
|
float height = size * legacyHeightScale * retinaScale;
|
|
int sizeIndex = getSizeIndex(height);
|
|
float scale = height / sizeTable[sizeIndex] / retinaScale;
|
|
|
|
Vector2int16 intAvailableSpace = Vector2int16(availableSpace / scale);
|
|
Vector2int16 intSize = layout(s, NULL, sizeIndex, intAvailableSpace, useAvailableSpace, DFFlag::TextScaleDontWrapInWords, textFits);
|
|
|
|
if (DFFlag::TextScaleDontWrapInWords && textFits == nullptr) {
|
|
intSize = layout(s, NULL, sizeIndex, intAvailableSpace, useAvailableSpace, false, textFits);
|
|
}
|
|
|
|
return Vector2(intSize) * scale;
|
|
}
|
|
|
|
int TypesetterBitmap::getSizeIndex(float height) const
|
|
{
|
|
for (int i = 0; i < info.sizeCount; ++i)
|
|
if (height <= sizeTable[i])
|
|
return i;
|
|
|
|
return info.sizeCount - 1;
|
|
}
|
|
|
|
template <typename GlyphLine> static inline void pushLine(std::vector<GlyphLine>* lines, const std::string& s, int lineStart, int lineEnd, int yoffset, int width)
|
|
{
|
|
if (lines)
|
|
{
|
|
GlyphLine line = {s.c_str() + lineStart, lineEnd - lineStart, yoffset, width};
|
|
|
|
lines->push_back(line);
|
|
}
|
|
}
|
|
|
|
Vector2int16 TypesetterBitmap::layoutRatio(const std::string& s, std::vector<GlyphLine>* lines, int sizeIndex, const Vector2& availableSpace) const
|
|
{
|
|
float ratio = availableSpace.x / availableSpace.y;
|
|
|
|
int height = sizeTable[sizeIndex];
|
|
|
|
float min = 10;
|
|
float max = height * s.length(); // 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();
|
|
|
|
bool textFitsValue = false;
|
|
|
|
Vector2int16 intSize = layout(s, lines, sizeIndex,
|
|
Vector2int16(static_cast<int16_t>(availableSpaceByRatio.x), static_cast<int16_t>(availableSpaceByRatio.y)),
|
|
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;
|
|
}
|
|
|
|
if (lines)
|
|
*lines = bestLinesSoFar;
|
|
return bestSizeSoFar;
|
|
}
|
|
|
|
Vector2 TypesetterBitmap::measureLining(std::vector<GlyphLine>& lines, int sizeIndex) const
|
|
{
|
|
Vector2 ret;
|
|
const Glyph* glyphTableSize = glyphTable + info.glyphCount * sizeIndex;
|
|
int height = sizeTable[sizeIndex];
|
|
|
|
float y = 0;
|
|
for (size_t li = 0; li < lines.size(); ++li)
|
|
{
|
|
const GlyphLine& line = lines[li];
|
|
|
|
int lastKerningOffset = 0;
|
|
float x = 0;
|
|
y += height;
|
|
|
|
for (size_t i = 0; i < static_cast<size_t>(line.length); ++i)
|
|
{
|
|
char ch = line.data[i];
|
|
|
|
int glyphIndex = glyphRemap[static_cast<unsigned char>(ch)];
|
|
|
|
if (glyphIndex == 255)
|
|
{
|
|
if (!isCharacterZeroWidth(ch))
|
|
lastKerningOffset = 0;
|
|
continue;
|
|
}
|
|
|
|
const Glyph& glyph = glyphTableSize[glyphIndex];
|
|
x += kerningTable[lastKerningOffset + glyphIndex];
|
|
x += glyph.xadvance;
|
|
lastKerningOffset = glyph.kerningOffset;
|
|
}
|
|
|
|
if (ret.x < x)
|
|
ret.x = x;
|
|
if (ret.y < y)
|
|
ret.y = y;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
|
|
Vector2int16 TypesetterBitmap::layout(const std::string& s, std::vector<GlyphLine>* lines, int sizeIndex, const Vector2int16& availableSpace, bool useAvailableSpace, bool onlyProperFit, bool* textFits) const
|
|
{
|
|
int x = 0, y = 0;
|
|
|
|
const Glyph* glyphTableSize = glyphTable + info.glyphCount * sizeIndex;
|
|
|
|
int height = sizeTable[sizeIndex];
|
|
|
|
int lastKerningOffset = 0;
|
|
|
|
int lineStart = 0;
|
|
int lineMaxWidth = 0;
|
|
|
|
int wordStart = 0;
|
|
int wordWidth = 0;
|
|
int whitespaceWidth = 0;
|
|
|
|
for (size_t i = 0; i < s.length(); ++i)
|
|
{
|
|
char ch = s[i];
|
|
|
|
int glyphIndex = glyphRemap[static_cast<unsigned char>(ch)];
|
|
|
|
if (glyphIndex == 255)
|
|
{
|
|
if (!isCharacterZeroWidth(ch))
|
|
lastKerningOffset = 0;
|
|
|
|
if (ch == '\n')
|
|
{
|
|
// hard line break
|
|
pushLine(lines, s, lineStart, i, y, x);
|
|
|
|
lineStart = i + 1;
|
|
lineMaxWidth = std::max(lineMaxWidth, x);
|
|
|
|
wordStart = i + 1;
|
|
wordWidth = 0;
|
|
whitespaceWidth = 0;
|
|
|
|
x = 0;
|
|
y += height;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
const Glyph& glyph = glyphTableSize[glyphIndex];
|
|
|
|
int xoffset = kerningTable[lastKerningOffset + glyphIndex] + glyph.xadvance;
|
|
|
|
if (isCharacterWhitespace(ch))
|
|
{
|
|
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, s, 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, s, lineStart, i, y, x - xoffset);
|
|
|
|
lineStart = i;
|
|
lineMaxWidth = std::max(lineMaxWidth, x - xoffset);
|
|
|
|
x = xoffset;
|
|
}
|
|
|
|
wordWidth = x;
|
|
whitespaceWidth = 0;
|
|
y += height;
|
|
}
|
|
|
|
lastKerningOffset = glyph.kerningOffset;
|
|
}
|
|
|
|
// last line
|
|
pushLine(lines, s, lineStart, s.length(), 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);
|
|
}
|
|
}
|
|
}
|
|
}
|