Browse Source

text_render: Support per-glyph overrides

Previously, an override font had to contain all the 256 glyphs in the
row that it was overriding. Experiments have shown that this would
cause huge file increase in CJK scripts (+200MiB or so).

Instead, allows the override font to only override some glyphs in a row.

For all glyphs (sprites) that have width 0, we now fall back to
the base font.
pull/8187/head
Gleb Mazovetskiy 6 months ago committed by Anders Jenbo
parent
commit
c31258b0f5
  1. 103
      Source/engine/render/text_render.cpp

103
Source/engine/render/text_render.cpp

@ -45,7 +45,39 @@ namespace {
constexpr char32_t ZWSP = U'\u200B'; // Zero-width space
ankerl::unordered_dense::map<uint32_t, OptionalOwnedClxSpriteList> Fonts;
struct OwnedFontStack {
OptionalOwnedClxSpriteList baseFont;
OptionalOwnedClxSpriteList overrideFont;
};
struct FontStack {
OptionalClxSpriteList baseFont;
OptionalClxSpriteList overrideFont;
FontStack() = default;
explicit FontStack(const OwnedFontStack &owned)
{
if (owned.baseFont.has_value()) baseFont.emplace(*owned.baseFont);
if (owned.overrideFont.has_value()) overrideFont.emplace(*owned.overrideFont);
}
[[nodiscard]] bool has_value() const // NOLINT(readability-identifier-naming)
{
return baseFont.has_value() || overrideFont.has_value();
}
[[nodiscard]] ClxSprite glyph(size_t i) const
{
if (overrideFont.has_value()) {
ClxSprite overrideGlyph = (*overrideFont)[i];
if (overrideGlyph.width() != 0) return overrideGlyph;
}
return (*baseFont)[i];
}
};
ankerl::unordered_dense::map<uint32_t, OwnedFontStack> Fonts;
std::array<int, 6> FontSizes = { 12, 24, 30, 42, 46, 22 };
constexpr std::array<int, 6> LineHeights = { 12, 26, 38, 42, 50, 22 };
@ -153,7 +185,7 @@ uint32_t GetFontId(GameFontTables size, uint16_t row)
return (size << 16) | row;
}
OptionalClxSpriteList LoadFont(GameFontTables size, text_color color, uint16_t row)
FontStack LoadFont(GameFontTables size, text_color color, uint16_t row)
{
if (ColorTranslations[color] != nullptr && !ColorTranslationsData[color]) {
ColorTranslationsData[color].emplace();
@ -163,48 +195,52 @@ OptionalClxSpriteList LoadFont(GameFontTables size, text_color color, uint16_t r
const uint32_t fontId = GetFontId(size, row);
auto hotFont = Fonts.find(fontId);
if (hotFont != Fonts.end()) {
return OptionalClxSpriteList(*hotFont->second);
return FontStack(hotFont->second);
}
OptionalOwnedClxSpriteList &font = Fonts[fontId];
OwnedFontStack &font = Fonts[fontId];
char path[32];
// Try loading the language-specific variant first:
const std::string_view language_code = GetLanguageCode();
const std::string_view language_tag = language_code.substr(0, 2);
if (language_tag == "zh" || language_tag == "ja" || language_tag == "ko"
|| (language_tag == "tr" && row == 0)) {
GetFontPath(language_code, size, row, ".clx", &path[0]);
font = LoadOptionalClx(path);
}
if (!font) {
// Fall back to the base variant:
GetFontPath(size, row, ".clx", &path[0]);
font = LoadOptionalClx(path);
// Load language-specific glyphs:
const std::string_view languageCode = GetLanguageCode();
const std::string_view lang = languageCode.substr(0, 2);
if (lang == "zh" || lang == "ja" || lang == "ko"
|| (lang == "tr" && row == 0)) {
GetFontPath(languageCode, size, row, ".clx", &path[0]);
font.overrideFont = LoadOptionalClx(path);
}
// Load the base glyphs:
GetFontPath(size, row, ".clx", &path[0]);
font.baseFont = LoadOptionalClx(path);
#ifndef UNPACKED_MPQS
if (!font) {
if (!font.baseFont.has_value()) {
// Could be an old devilutionx.mpq or fonts.mpq with PCX instead of CLX.
//
// We'll show an error elsewhere (in `CheckArchivesUpToDate`) and we need to load
// the font files to display it.
char pcxPath[32];
GetFontPath(size, row, "", &pcxPath[0]);
font = LoadPcxSpriteList(pcxPath, /*numFramesOrFrameHeight=*/256, /*transparentColor=*/1);
font.baseFont = LoadPcxSpriteList(pcxPath, /*numFramesOrFrameHeight=*/256, /*transparentColor=*/1);
}
#endif
if (!font) {
if (!font.baseFont.has_value()) {
LogError("Error loading font: {}", path);
}
return OptionalClxSpriteList(*font);
return FontStack(font);
}
class CurrentFont {
public:
OptionalClxSpriteList sprite;
FontStack fontStack;
[[nodiscard]] ClxSprite glyph(size_t i) const
{
return fontStack.glyph(i);
}
bool load(GameFontTables size, text_color color, char32_t next)
{
@ -213,11 +249,11 @@ public:
return true;
}
sprite = LoadFont(size, color, unicodeRow);
fontStack = LoadFont(size, color, unicodeRow);
hasAttemptedLoad_ = true;
currentUnicodeRow_ = unicodeRow;
return sprite;
return fontStack.has_value();
}
void clear()
@ -424,9 +460,10 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect,
Point position = characterPosition;
MaybeWrap(position, 2, rightMargin, position.x, opts.lineHeight);
if (GetAnimationFrame(2, 500) != 0) {
OptionalClxSpriteList baseFont = LoadFont(size, color, 0);
if (baseFont)
DrawFont(out, position, (*baseFont)['|'], color, outline);
FontStack baseFont = LoadFont(size, color, 0);
if (baseFont.has_value()) {
DrawFont(out, position, baseFont.glyph('|'), color, outline);
}
}
if (opts.renderedCursorPositionOut != nullptr) {
*opts.renderedCursorPositionOut = position;
@ -448,7 +485,7 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect,
}
const uint8_t frame = next & 0xFF;
const uint16_t width = (*currentFont.sprite)[frame].width();
const uint16_t width = currentFont.glyph(frame).width();
if (next == U'\n' || characterPosition.x + width > rightMargin) {
if (next == '\n')
maybeDrawCursor();
@ -473,7 +510,7 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect,
continue;
}
const ClxSprite glyph = (*currentFont.sprite)[frame];
const ClxSprite glyph = currentFont.glyph(frame);
const auto byteIndex = static_cast<int>(text.size() - remaining.size());
// Draw highlight
@ -528,7 +565,7 @@ int GetLineWidth(std::string_view text, GameFontTables size, int spacing, int *c
}
const uint8_t frame = next & 0xFF;
lineWidth += (*currentFont.sprite)[frame].width() + spacing;
lineWidth += currentFont.glyph(frame).width() + spacing;
++codepoints;
}
if (charactersInLine != nullptr)
@ -599,7 +636,7 @@ int GetLineWidth(std::string_view fmt, DrawStringFormatArg *args, std::size_t ar
}
const uint8_t frame = next & 0xFF;
lineWidth += (*currentFont.sprite)[frame].width() + spacing;
lineWidth += currentFont.glyph(frame).width() + spacing;
++codepoints;
}
if (charactersInLine != nullptr)
@ -660,7 +697,7 @@ std::string WordWrapString(std::string_view text, unsigned width, GameFontTables
}
}
lineWidth += (*currentFont.sprite)[frame].width() + spacing;
lineWidth += currentFont.glyph(frame).width() + spacing;
}
if (IsBreakableWhitespace(codepoint)) {
@ -840,7 +877,7 @@ void DrawStringWithColors(const Surface &out, std::string_view fmt, DrawStringFo
}
const uint8_t frame = next & 0xFF;
const uint16_t width = (*currentFont.sprite)[frame].width();
const uint16_t width = currentFont.glyph(frame).width();
if (next == U'\n' || characterPosition.x + width > rightMargin) {
const int nextLineY = characterPosition.y + opts.lineHeight;
if (nextLineY >= bottomMargin)
@ -871,7 +908,7 @@ void DrawStringWithColors(const Surface &out, std::string_view fmt, DrawStringFo
continue;
}
DrawFont(clippedOut, characterPosition, (*currentFont.sprite)[frame], curColor, outlined);
DrawFont(clippedOut, characterPosition, currentFont.glyph(frame), curColor, outlined);
characterPosition.x += width + opts.spacing;
}

Loading…
Cancel
Save