osb/source/rendering/StarTextPainter.cpp

588 lines
19 KiB
C++
Raw Normal View History

2023-06-20 04:33:09 +00:00
#include "StarTextPainter.hpp"
#include "StarJsonExtra.hpp"
#include <regex>
namespace Star {
namespace Text {
static auto stripEscapeRegex = std::regex(strf("\\{:c}[^;]*{:c}", CmdEsc, EndEsc));
2023-06-20 04:33:09 +00:00
String stripEscapeCodes(String const& s) {
return std::regex_replace(s.utf8(), stripEscapeRegex, "");
2023-06-20 04:33:09 +00:00
}
2023-06-28 10:08:11 +00:00
inline bool isEscapeCode(char c) {
return c == CmdEsc || c == StartEsc;
}
static std::string escapeChars = strf("{:c}{:c}", CmdEsc, StartEsc);
typedef function<bool(StringView text)> TextCallback;
typedef function<bool(StringView commands)> CommandsCallback;
void processText(StringView text, TextCallback textFunc, CommandsCallback commandsFunc = CommandsCallback(), bool includeCommandSides = false) {
std::string_view escChars(escapeChars);
std::string_view str = text.utf8();
while (true) {
size_t escape = str.find_first_of(escChars);
if (escape != NPos) {
escape = str.find_first_not_of(escChars, escape) - 1; // jump to the last ^
size_t end = str.find_first_of(EndEsc, escape);
if (end != NPos) {
if (escape != end && !textFunc(str.substr(0, escape)))
break;
if (commandsFunc) {
StringView commands = includeCommandSides
? str.substr(escape, end - escape + 1)
: str.substr(escape + 1, end - escape - 1);
if (!commands.empty() && !commandsFunc(commands))
break;
}
str = str.substr(end + 1);
continue;
}
}
if (!str.empty())
textFunc(str);
break;
}
}
// The below two functions aren't used anymore, not bothering with StringView for them
2023-06-20 04:33:09 +00:00
String preprocessEscapeCodes(String const& s) {
bool escape = false;
2023-06-28 10:08:11 +00:00
std::string result = s.utf8();
2023-06-20 04:33:09 +00:00
size_t escapeStartIdx = 0;
for (size_t i = 0; i < result.size(); i++) {
auto& c = result[i];
2023-06-28 10:08:11 +00:00
if (isEscapeCode(c)) {
2023-06-20 04:33:09 +00:00
escape = true;
escapeStartIdx = i;
}
if ((c <= SpecialCharLimit) && !(c == StartEsc))
escape = false;
if ((c == EndEsc) && escape)
result[escapeStartIdx] = StartEsc;
}
return {result};
}
String extractCodes(String const& s) {
bool escape = false;
StringList result;
String escapeCode;
for (auto c : preprocessEscapeCodes(s)) {
if (c == StartEsc)
escape = true;
if (c == EndEsc) {
escape = false;
for (auto command : escapeCode.split(','))
result.append(command);
escapeCode = "";
}
if (escape && (c != StartEsc))
escapeCode.append(c);
}
if (!result.size())
return "";
return "^" + result.join(",") + ";";
}
}
TextPositioning::TextPositioning() {
pos = Vec2F();
hAnchor = HorizontalAnchor::LeftAnchor;
vAnchor = VerticalAnchor::BottomAnchor;
}
TextPositioning::TextPositioning(Vec2F pos, HorizontalAnchor hAnchor, VerticalAnchor vAnchor,
Maybe<unsigned> wrapWidth, Maybe<unsigned> charLimit)
: pos(pos), hAnchor(hAnchor), vAnchor(vAnchor), wrapWidth(wrapWidth), charLimit(charLimit) {}
TextPositioning::TextPositioning(Json const& v) {
pos = v.opt("position").apply(jsonToVec2F).value();
hAnchor = HorizontalAnchorNames.getLeft(v.getString("horizontalAnchor", "left"));
vAnchor = VerticalAnchorNames.getLeft(v.getString("verticalAnchor", "top"));
wrapWidth = v.optUInt("wrapWidth");
charLimit = v.optUInt("charLimit");
}
Json TextPositioning::toJson() const {
return JsonObject{
{"position", jsonFromVec2F(pos)},
{"horizontalAnchor", HorizontalAnchorNames.getRight(hAnchor)},
{"verticalAnchor", VerticalAnchorNames.getRight(vAnchor)},
{"wrapWidth", jsonFromMaybe(wrapWidth)}
};
}
TextPositioning TextPositioning::translated(Vec2F translation) const {
return {pos + translation, hAnchor, vAnchor, wrapWidth, charLimit};
}
2023-06-21 09:46:23 +00:00
TextPainter::TextPainter(RendererPtr renderer, TextureGroupPtr textureGroup)
2023-06-20 04:33:09 +00:00
: m_renderer(renderer),
2023-06-21 09:46:23 +00:00
m_fontTextureGroup(textureGroup),
2023-06-20 04:33:09 +00:00
m_fontSize(8),
m_lineSpacing(1.30f),
m_renderSettings({FontMode::Normal, Vec4B::filled(255), "hobo", ""}),
2023-06-20 04:33:09 +00:00
m_splitIgnore(" \t"),
m_splitForce("\n\v"),
2023-06-21 09:46:23 +00:00
m_nonRenderedCharacters("\n\v\r") {
2023-06-21 13:13:37 +00:00
reloadFonts();
m_reloadTracker = make_shared<TrackerListener>();
Root::singleton().registerReloadListener(m_reloadTracker);
2023-06-21 09:46:23 +00:00
}
2023-06-20 04:33:09 +00:00
2023-06-28 10:08:11 +00:00
RectF TextPainter::renderText(StringView s, TextPositioning const& position) {
2023-06-20 04:33:09 +00:00
if (position.charLimit) {
unsigned charLimit = *position.charLimit;
return doRenderText(s, position, true, &charLimit);
} else {
return doRenderText(s, position, true, nullptr);
}
}
2023-06-28 10:08:11 +00:00
RectF TextPainter::renderLine(StringView s, TextPositioning const& position) {
2023-06-20 04:33:09 +00:00
if (position.charLimit) {
unsigned charLimit = *position.charLimit;
return doRenderLine(s, position, true, &charLimit);
} else {
return doRenderLine(s, position, true, nullptr);
}
}
RectF TextPainter::renderGlyph(String::Char c, TextPositioning const& position) {
return doRenderGlyph(c, position, true);
}
2023-06-28 10:08:11 +00:00
RectF TextPainter::determineTextSize(StringView s, TextPositioning const& position) {
2023-06-20 04:33:09 +00:00
return doRenderText(s, position, false, nullptr);
}
2023-06-28 10:08:11 +00:00
RectF TextPainter::determineLineSize(StringView s, TextPositioning const& position) {
2023-06-20 04:33:09 +00:00
return doRenderLine(s, position, false, nullptr);
}
RectF TextPainter::determineGlyphSize(String::Char c, TextPositioning const& position) {
return doRenderGlyph(c, position, false);
}
int TextPainter::glyphWidth(String::Char c) {
return m_fontTextureGroup.glyphWidth(c, m_fontSize);
}
2023-06-28 10:08:11 +00:00
int TextPainter::stringWidth(StringView s) {
if (s.empty())
return 0;
2023-06-21 12:29:40 +00:00
String font = m_renderSettings.font, setFont = font;
m_fontTextureGroup.switchFont(font);
2023-06-28 10:08:11 +00:00
2023-06-20 04:33:09 +00:00
int width = 0;
2023-06-28 10:08:11 +00:00
Text::CommandsCallback commandsCallback = [&](StringView commands) {
commands.forEachSplitView(",", [&](StringView command, size_t, size_t) {
if (command == "reset")
m_fontTextureGroup.switchFont(font = setFont);
else if (command == "set")
setFont = font;
else if (command.beginsWith("font="))
m_fontTextureGroup.switchFont(font = command.substr(5));
});
return true;
};
2023-06-21 12:29:40 +00:00
2023-06-28 10:08:11 +00:00
Text::TextCallback textCallback = [&](StringView text) {
for (String::Char c : text)
2023-06-20 04:33:09 +00:00
width += glyphWidth(c);
2023-06-28 10:08:11 +00:00
return true;
};
Text::processText(s, textCallback, commandsCallback);
2023-06-20 04:33:09 +00:00
return width;
}
2023-06-28 10:08:11 +00:00
void TextPainter::processWrapText(StringView s, Maybe<unsigned> wrapWidth, WrapTextCallback textFunc, WrapCommandsCallback commandsFunc, bool includeCommandSides) {
2023-06-21 12:29:40 +00:00
String font = m_renderSettings.font, setFont = font;
m_fontTextureGroup.switchFont(font);
2023-06-20 04:33:09 +00:00
unsigned linePixelWidth = 0; // How wide is this line so far
2023-06-28 10:08:11 +00:00
int lines = 0;
StringView splitIgnore(m_splitIgnore);
StringView splitForce(m_splitForce);
Text::CommandsCallback commandsCallback = [&](StringView commands) {
StringView inner = commands.utf8().substr(1, commands.utf8Size() - 1);
inner.forEachSplitView(",", [&](StringView command, size_t, size_t) {
if (command == "reset")
m_fontTextureGroup.switchFont(font = setFont);
else if (command == "set")
setFont = font;
else if (command.beginsWith("font="))
m_fontTextureGroup.switchFont(font = command.substr(5));
});
if (commandsFunc)
if (!commandsFunc(includeCommandSides ? commands : inner))
return false;
return true;
};
2023-06-20 04:33:09 +00:00
2023-06-28 10:08:11 +00:00
Text::TextCallback textCallback = [&](StringView text) {
unsigned lineCharSize = 0; // how many characters in this line ?
unsigned lineStart = 0; // Where does this line start ?
unsigned splitPos = 0; // Where did we last see a place to split the string ?
unsigned splitWidth = 0; // How wide was the string there ?
for (auto character : text) {
2023-06-20 04:33:09 +00:00
lineCharSize++; // assume at least one character if we get here.
// is this a linefeed / cr / whatever that forces a line split ?
2023-06-28 10:08:11 +00:00
if (splitForce.find(character) != NPos) {
2023-06-20 04:33:09 +00:00
// knock one off the end because we don't render the CR
2023-06-28 10:08:11 +00:00
if (!textFunc(text.substr(lineStart, lineCharSize - 1), lines++))
return false;
2023-06-20 04:33:09 +00:00
lineStart += lineCharSize; // next line starts after the CR
lineCharSize = 0; // with no characters in it.
linePixelWidth = 0; // No width
splitPos = 0; // and no known splits.
} else {
int charWidth = glyphWidth(character);
// is it a place where we might want to split the line ?
2023-06-28 10:08:11 +00:00
if (splitIgnore.find(character) != NPos) {
2023-06-20 04:33:09 +00:00
splitPos = lineStart + lineCharSize; // this is the character after the space.
splitWidth = linePixelWidth + charWidth; // the width of the string at
// the split point, i.e. after the space.
}
// would the line be too long if we render this next character ?
if (wrapWidth && (linePixelWidth + charWidth) > *wrapWidth) {
// did we find somewhere to split the line ?
if (splitPos) {
2023-06-28 10:08:11 +00:00
if (!textFunc(text.substr(lineStart, (splitPos - lineStart) - 1), lines++))
return false;
2023-06-20 04:33:09 +00:00
unsigned stringEnd = lineStart + lineCharSize;
lineCharSize = stringEnd - splitPos; // next line has the characters after the space.
unsigned stringWidth = (linePixelWidth - splitWidth);
linePixelWidth = stringWidth + charWidth; // and is as wide as the bit after the space.
lineStart = splitPos; // next line starts after the space
splitPos = 0; // split is used up.
} else {
2023-06-28 10:08:11 +00:00
if (!textFunc(text.substr(lineStart, lineCharSize - 1), lines++))
return false;
2023-06-20 04:33:09 +00:00
lineStart += lineCharSize - 1; // skip back by one to include that
2023-06-28 10:08:11 +00:00
// character on the next line.
lineCharSize = 1; // next line has that character in
2023-06-20 04:33:09 +00:00
linePixelWidth = charWidth; // and is as wide as that character
}
} else {
linePixelWidth += charWidth;
}
}
}
2023-06-28 10:08:11 +00:00
// if we hit the end of the string before hitting the end of the line.
if (lineCharSize > 0 && !textFunc(text.substr(lineStart, lineCharSize), lines))
return false;
2023-06-20 04:33:09 +00:00
2023-06-28 10:08:11 +00:00
return true;
};
Text::processText(s, textCallback, commandsCallback, true);
2023-06-20 04:33:09 +00:00
}
2023-06-28 10:08:11 +00:00
List<StringView> TextPainter::wrapTextViews(StringView s, Maybe<unsigned> wrapWidth) {
List<StringView> views = {};
bool active = false;
StringView current;
int lastLine = 0;
auto addText = [&active, &current](StringView text) {
// Merge views if they are adjacent
if (active && current.utf8Ptr() + current.utf8Size() == text.utf8Ptr())
current = StringView(current.utf8Ptr(), current.utf8Size() + text.utf8Size());
else
current = text;
active = true;
};
TextPainter::WrapTextCallback textCallback = [&](StringView text, int line) {
if (lastLine != line) {
views.push_back(current);
lastLine = line;
active = false;
}
addText(text);
return true;
};
TextPainter::WrapCommandsCallback commandsCallback = [&](StringView commands) {
addText(commands);
return true;
};
processWrapText(s, wrapWidth, textCallback, commandsCallback, true);
if (active)
views.push_back(current);
return views;
}
StringList TextPainter::wrapText(StringView s, Maybe<unsigned> wrapWidth) {
StringList result;
String current;
int lastLine = 0;
TextPainter::WrapTextCallback textCallback = [&](StringView text, int line) {
if (lastLine != line) {
result.append(move(current));
lastLine = line;
}
current += text;
return true;
};
TextPainter::WrapCommandsCallback commandsCallback = [&](StringView commands) {
current += commands;
return true;
};
processWrapText(s, wrapWidth, textCallback, commandsCallback, true);
if (!current.empty())
result.append(move(current));
return result;
};
2023-06-20 04:33:09 +00:00
unsigned TextPainter::fontSize() const {
return m_fontSize;
}
void TextPainter::setFontSize(unsigned size) {
m_fontSize = size;
}
void TextPainter::setLineSpacing(float lineSpacing) {
m_lineSpacing = lineSpacing;
}
void TextPainter::setMode(FontMode mode) {
m_renderSettings.mode = mode;
}
void TextPainter::setSplitIgnore(String const& splitIgnore) {
m_splitIgnore = splitIgnore;
}
void TextPainter::setFontColor(Vec4B color) {
m_renderSettings.color = move(color);
}
void TextPainter::setProcessingDirectives(String directives) {
2023-06-21 10:29:23 +00:00
m_renderSettings.directives = move(directives);
2023-06-20 04:33:09 +00:00
}
2023-06-21 09:46:23 +00:00
void TextPainter::setFont(String const& font) {
2023-06-21 12:29:40 +00:00
m_renderSettings.font = font;
2023-06-21 09:46:23 +00:00
}
void TextPainter::addFont(FontPtr const& font, String const& name) {
m_fontTextureGroup.addFont(font, name);
}
2023-06-21 13:13:37 +00:00
void TextPainter::reloadFonts() {
m_fontTextureGroup.clearFonts();
m_fontTextureGroup.cleanup(0);
auto assets = Root::singleton().assets();
auto defaultFont = assets->font("/hobo.ttf");
for (auto& fontPath : assets->scanExtension("ttf")) {
auto font = assets->font(fontPath);
if (font == defaultFont)
continue;
auto fileName = AssetPath::filename(fontPath);
addFont(font->clone(), fileName.substr(0, fileName.findLast(".")));
}
m_fontTextureGroup.addFont(defaultFont->clone(), "hobo", true);
}
2023-06-20 04:33:09 +00:00
void TextPainter::cleanup(int64_t timeout) {
m_fontTextureGroup.cleanup(timeout);
}
2023-06-28 10:08:11 +00:00
void TextPainter::applyCommands(StringView unsplitCommands) {
unsplitCommands.forEachSplitView(",", [&](StringView command, size_t, size_t) {
2023-06-23 09:32:41 +00:00
try {
if (command == "reset") {
m_renderSettings = m_savedRenderSettings;
} else if (command == "set") {
m_savedRenderSettings = m_renderSettings;
} else if (command == "shadow") {
m_renderSettings.mode = (FontMode)((int)m_renderSettings.mode | (int)FontMode::Shadow);
} else if (command == "noshadow") {
m_renderSettings.mode = (FontMode)((int)m_renderSettings.mode & (-1 ^ (int)FontMode::Shadow));
} else if (command.beginsWith("font=")) {
m_renderSettings.font = command.substr(5);
} else if (command.beginsWith("directives=")) {
// Honestly this is really stupid but I just couldn't help myself
// Should probably limit in the future
m_renderSettings.directives = command.substr(11);
} else {
// expects both #... sequences and plain old color names.
2023-06-28 10:08:11 +00:00
Color c = Color(command);
2023-06-23 09:32:41 +00:00
c.setAlphaF(c.alphaF() * ((float)m_savedRenderSettings.color[3]) / 255);
m_renderSettings.color = c.toRgba();
}
} catch (JsonException&) {
} catch (ColorException&) {
}
2023-06-28 10:08:11 +00:00
});
2023-06-23 09:32:41 +00:00
}
2023-06-28 10:08:11 +00:00
RectF TextPainter::doRenderText(StringView s, TextPositioning const& position, bool reallyRender, unsigned* charLimit) {
2023-06-20 04:33:09 +00:00
Vec2F pos = position.pos;
2023-06-28 10:08:11 +00:00
if (s.empty())
return RectF(pos, pos);
List<StringView> lines = wrapTextViews(s, position.wrapWidth);
2023-06-20 04:33:09 +00:00
int height = (lines.size() - 1) * m_lineSpacing * m_fontSize + m_fontSize;
2023-06-23 09:32:41 +00:00
RenderSettings backupRenderSettings = m_renderSettings;
2023-06-20 04:33:09 +00:00
m_savedRenderSettings = m_renderSettings;
if (position.vAnchor == VerticalAnchor::BottomAnchor)
pos[1] += (height - m_fontSize);
else if (position.vAnchor == VerticalAnchor::VMidAnchor)
pos[1] += (height - m_fontSize) / 2;
RectF bounds = RectF::withSize(pos, Vec2F());
2023-06-28 10:08:11 +00:00
for (auto& i : lines) {
2023-06-20 04:33:09 +00:00
bounds.combine(doRenderLine(i, { pos, position.hAnchor, position.vAnchor }, reallyRender, charLimit));
pos[1] -= m_fontSize * m_lineSpacing;
if (charLimit && *charLimit == 0)
break;
}
2023-06-23 09:32:41 +00:00
m_renderSettings = move(backupRenderSettings);
2023-06-20 04:33:09 +00:00
return bounds;
}
2023-06-28 10:08:11 +00:00
RectF TextPainter::doRenderLine(StringView text, TextPositioning const& position, bool reallyRender, unsigned* charLimit) {
2023-06-21 13:13:37 +00:00
if (m_reloadTracker->pullTriggered())
reloadFonts();
2023-06-20 04:33:09 +00:00
TextPositioning pos = position;
2023-06-21 12:29:40 +00:00
2023-06-20 04:33:09 +00:00
if (pos.hAnchor == HorizontalAnchor::RightAnchor) {
2023-06-28 10:08:11 +00:00
StringView trimmedString = charLimit ? text.substr(0, *charLimit) : text;
2023-06-20 04:33:09 +00:00
pos.pos[0] -= stringWidth(trimmedString);
pos.hAnchor = HorizontalAnchor::LeftAnchor;
} else if (pos.hAnchor == HorizontalAnchor::HMidAnchor) {
2023-06-28 10:08:11 +00:00
StringView trimmedString = charLimit ? text.substr(0, *charLimit) : text;
pos.pos[0] -= std::floorf((float)stringWidth(trimmedString) / 2);
2023-06-20 04:33:09 +00:00
pos.hAnchor = HorizontalAnchor::LeftAnchor;
}
bool escape = false;
String escapeCode;
RectF bounds = RectF::withSize(pos.pos, Vec2F());
2023-06-28 10:08:11 +00:00
Text::TextCallback textCallback = [&](StringView text) {
for (String::Char c : text) {
2023-06-20 04:33:09 +00:00
if (charLimit) {
if (*charLimit == 0)
2023-06-28 10:08:11 +00:00
return false;
2023-06-20 04:33:09 +00:00
else
2023-06-28 10:08:11 +00:00
--* charLimit;
2023-06-20 04:33:09 +00:00
}
2023-06-28 10:08:11 +00:00
2023-06-20 04:33:09 +00:00
RectF glyphBounds = doRenderGlyph(c, pos, reallyRender);
bounds.combine(glyphBounds);
pos.pos[0] += glyphBounds.width();
}
2023-06-28 10:08:11 +00:00
return true;
};
Text::CommandsCallback commandsCallback = [&](StringView commands) {
applyCommands(commands);
return true;
};
Text::processText(text, textCallback, commandsCallback);
2023-06-20 04:33:09 +00:00
return bounds;
}
RectF TextPainter::doRenderGlyph(String::Char c, TextPositioning const& position, bool reallyRender) {
if (m_nonRenderedCharacters.find(String(c)) != NPos)
return RectF();
2023-06-21 12:29:40 +00:00
m_fontTextureGroup.switchFont(m_renderSettings.font);
2023-06-20 04:33:09 +00:00
int width = glyphWidth(c);
// Offset left by width if right anchored.
float hOffset = 0;
if (position.hAnchor == HorizontalAnchor::RightAnchor)
hOffset = -width;
else if (position.hAnchor == HorizontalAnchor::HMidAnchor)
2023-06-28 10:08:11 +00:00
hOffset = -std::floorf((float)width / 2);
2023-06-20 04:33:09 +00:00
float vOffset = 0;
if (position.vAnchor == VerticalAnchor::VMidAnchor)
2023-06-28 10:08:11 +00:00
vOffset = -std::floorf((float)m_fontSize / 2);
2023-06-20 04:33:09 +00:00
else if (position.vAnchor == VerticalAnchor::TopAnchor)
vOffset = -(float)m_fontSize;
if (reallyRender) {
if ((int)m_renderSettings.mode & (int)FontMode::Shadow) {
Color shadow = Color::Black;
uint8_t alphaU = m_renderSettings.color[3];
if (alphaU != 255) {
float alpha = byteToFloat(alphaU);
shadow.setAlpha(floatToByte(alpha * (1.5f - 0.5f * alpha)));
}
else
shadow.setAlpha(alphaU);
//Kae: Draw only one shadow glyph instead of stacking two, alpha modified to appear perceptually the same as vanilla
2023-06-21 10:29:23 +00:00
renderGlyph(c, position.pos + Vec2F(hOffset, vOffset - 2), m_fontSize, 1, shadow.toRgba(), m_renderSettings.directives);
2023-06-20 04:33:09 +00:00
}
2023-06-21 10:29:23 +00:00
renderGlyph(c, position.pos + Vec2F(hOffset, vOffset), m_fontSize, 1, m_renderSettings.color, m_renderSettings.directives);
2023-06-20 04:33:09 +00:00
}
return RectF::withSize(position.pos + Vec2F(hOffset, vOffset), {(float)width, (int)m_fontSize});
}
void TextPainter::renderGlyph(String::Char c, Vec2F const& screenPos, unsigned fontSize,
float scale, Vec4B const& color, String const& processingDirectives) {
if (!fontSize)
return;
const FontTextureGroup::GlyphTexture& glyphTexture = m_fontTextureGroup.glyphTexture(c, fontSize, processingDirectives);
Vec2F offset = glyphTexture.processingOffset * (scale * 0.5f); //Kae: Re-center the glyph if the image scale was changed by the directives (it is positioned from the bottom left)
m_renderer->render(renderTexturedRect(glyphTexture.texture, Vec2F(screenPos) + offset, scale, color, 0.0f));
2023-06-20 04:33:09 +00:00
}
}