2023-06-20 04:33:09 +00:00
|
|
|
#include "StarTextPainter.hpp"
|
|
|
|
#include "StarJsonExtra.hpp"
|
|
|
|
#include <regex>
|
|
|
|
|
|
|
|
namespace Star {
|
|
|
|
|
|
|
|
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),
|
2024-04-21 20:07:59 +00:00
|
|
|
m_defaultRenderSettings(),
|
|
|
|
m_renderSettings(),
|
|
|
|
m_savedRenderSettings() {
|
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) {
|
2024-04-21 20:07:59 +00:00
|
|
|
RectF rect;
|
2023-06-20 04:33:09 +00:00
|
|
|
if (position.charLimit) {
|
|
|
|
unsigned charLimit = *position.charLimit;
|
2024-04-21 20:07:59 +00:00
|
|
|
rect = doRenderText(s, position, true, &charLimit);
|
2023-06-20 04:33:09 +00:00
|
|
|
} else {
|
2024-04-21 20:07:59 +00:00
|
|
|
rect = doRenderText(s, position, true, nullptr);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
2024-04-21 20:07:59 +00:00
|
|
|
renderPrimitives();
|
|
|
|
return rect;
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
2023-06-28 10:08:11 +00:00
|
|
|
RectF TextPainter::renderLine(StringView s, TextPositioning const& position) {
|
2024-04-21 20:07:59 +00:00
|
|
|
RectF rect;
|
2023-06-20 04:33:09 +00:00
|
|
|
if (position.charLimit) {
|
|
|
|
unsigned charLimit = *position.charLimit;
|
2024-04-21 20:07:59 +00:00
|
|
|
rect = doRenderLine(s, position, true, &charLimit);
|
2023-06-20 04:33:09 +00:00
|
|
|
} else {
|
2024-04-21 20:07:59 +00:00
|
|
|
rect = doRenderLine(s, position, true, nullptr);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
2024-04-21 20:07:59 +00:00
|
|
|
renderPrimitives();
|
|
|
|
return rect;
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
RectF TextPainter::renderGlyph(String::Char c, TextPositioning const& position) {
|
2024-04-21 20:07:59 +00:00
|
|
|
auto rect = doRenderGlyph(c, position, true);
|
|
|
|
renderPrimitives();
|
|
|
|
return rect;
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
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) {
|
2024-04-21 20:07:59 +00:00
|
|
|
return m_fontTextureGroup.glyphWidth(c, m_renderSettings.fontSize);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
2024-04-21 20:07:59 +00:00
|
|
|
int TextPainter::stringWidth(StringView s, unsigned charLimit) {
|
2023-06-28 10:08:11 +00:00
|
|
|
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
|
|
|
|
|
|
|
Text::CommandsCallback commandsCallback = [&](StringView commands) {
|
|
|
|
commands.forEachSplitView(",", [&](StringView command, size_t, size_t) {
|
2024-04-21 20:07:59 +00:00
|
|
|
if (command == "reset") {
|
2023-06-28 10:08:11 +00:00
|
|
|
m_fontTextureGroup.switchFont(font = setFont);
|
2024-04-21 20:07:59 +00:00
|
|
|
} else if (command == "set") {
|
2023-06-28 10:08:11 +00:00
|
|
|
setFont = font;
|
2024-04-21 20:07:59 +00:00
|
|
|
} else if (command.beginsWith("font=")) {
|
2023-06-28 10:08:11 +00:00
|
|
|
m_fontTextureGroup.switchFont(font = command.substr(5));
|
2024-04-21 20:07:59 +00:00
|
|
|
}
|
2023-06-28 10:08:11 +00:00
|
|
|
});
|
|
|
|
return true;
|
|
|
|
};
|
2023-06-21 12:29:40 +00:00
|
|
|
|
2024-04-21 20:07:59 +00:00
|
|
|
int width = 0;
|
2023-06-28 10:08:11 +00:00
|
|
|
Text::TextCallback textCallback = [&](StringView text) {
|
2024-04-21 20:07:59 +00:00
|
|
|
for (String::Char c : text) {
|
2023-06-20 04:33:09 +00:00
|
|
|
width += glyphWidth(c);
|
2024-04-21 20:07:59 +00:00
|
|
|
if (charLimit && --charLimit == 0)
|
|
|
|
return false;
|
|
|
|
}
|
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-07-01 14:52:36 +00:00
|
|
|
bool TextPainter::processWrapText(StringView text, unsigned* wrapWidth, WrapTextCallback textFunc) {
|
2023-06-21 12:29:40 +00:00
|
|
|
String font = m_renderSettings.font, setFont = font;
|
|
|
|
m_fontTextureGroup.switchFont(font);
|
2024-04-21 20:07:59 +00:00
|
|
|
unsigned lines = 0;
|
2023-06-28 10:08:11 +00:00
|
|
|
|
2023-07-01 13:20:25 +00:00
|
|
|
auto it = text.begin();
|
|
|
|
auto end = text.end();
|
2023-06-28 10:08:11 +00:00
|
|
|
|
2023-07-01 13:20:25 +00:00
|
|
|
unsigned linePixelWidth = 0; // How wide is this line so far
|
|
|
|
unsigned lineCharSize = 0; // how many characters in this line ?
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-07-01 13:20:25 +00:00
|
|
|
auto escIt = end;
|
|
|
|
unsigned splitWidth = 0; // How wide was the string there ?
|
|
|
|
|
|
|
|
auto lineStartIt = it; // Where does this line start ?
|
2024-04-04 04:09:40 +00:00
|
|
|
auto splitIt = end; // != end if we last saw a place to split the string
|
2023-07-01 13:20:25 +00:00
|
|
|
|
|
|
|
auto slice = [](StringView::const_iterator a, StringView::const_iterator b) -> StringView {
|
|
|
|
const char* aPtr = &*a.base();
|
|
|
|
return StringView(aPtr, &*b.base() - aPtr);
|
2023-07-01 04:01:27 +00:00
|
|
|
};
|
|
|
|
|
2023-07-01 13:20:25 +00:00
|
|
|
while (it != end) {
|
|
|
|
auto character = *it;
|
|
|
|
|
|
|
|
if (Text::isEscapeCode(character))
|
|
|
|
escIt = it;
|
|
|
|
|
|
|
|
if (escIt != end) {
|
|
|
|
if (character == Text::EndEsc) {
|
2024-04-21 20:07:59 +00:00
|
|
|
StringView inner = slice(++escIt, it);
|
2023-07-01 13:20:25 +00:00
|
|
|
inner.forEachSplitView(",", [&](StringView command, size_t, size_t) {
|
2024-04-21 20:07:59 +00:00
|
|
|
if (command == "reset") {
|
2023-07-01 13:20:25 +00:00
|
|
|
m_fontTextureGroup.switchFont(font = setFont);
|
2024-04-21 20:07:59 +00:00
|
|
|
} else if (command == "set") {
|
2023-07-01 13:20:25 +00:00
|
|
|
setFont = font;
|
2024-04-21 20:07:59 +00:00
|
|
|
} else if (command.beginsWith("font=")) {
|
2023-07-01 13:20:25 +00:00
|
|
|
m_fontTextureGroup.switchFont(font = command.substr(5));
|
2024-04-21 20:07:59 +00:00
|
|
|
}
|
2023-07-01 13:20:25 +00:00
|
|
|
});
|
|
|
|
escIt = end;
|
|
|
|
}
|
|
|
|
lineCharSize++;
|
|
|
|
} else {
|
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-07-01 14:36:32 +00:00
|
|
|
if (character == '\n' || character == '\v') {
|
2023-06-20 04:33:09 +00:00
|
|
|
// knock one off the end because we don't render the CR
|
2023-07-01 13:20:25 +00:00
|
|
|
if (!textFunc(slice(lineStartIt, it), lines++))
|
2023-06-28 10:08:11 +00:00
|
|
|
return false;
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-07-01 13:20:25 +00:00
|
|
|
lineStartIt = it;
|
2023-07-01 14:36:32 +00:00
|
|
|
++lineStartIt;
|
2024-04-03 01:19:55 +00:00
|
|
|
// next line starts after the CR with no characters in it and no known splits.
|
|
|
|
lineCharSize = linePixelWidth = 0;
|
2024-04-15 01:10:33 +00:00
|
|
|
splitIt = end;
|
2023-06-20 04:33:09 +00:00
|
|
|
} else {
|
|
|
|
int charWidth = glyphWidth(character);
|
|
|
|
// is it a place where we might want to split the line ?
|
2023-07-01 14:36:32 +00:00
|
|
|
if (character == ' ' || character == '\t') {
|
2023-07-01 13:20:25 +00:00
|
|
|
splitIt = it;
|
2024-04-04 04:09:40 +00:00
|
|
|
splitWidth = linePixelWidth + charWidth; // the width of the string at
|
2023-06-20 04:33:09 +00:00
|
|
|
// 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 ?
|
2024-04-04 04:09:40 +00:00
|
|
|
if (splitIt != end) {
|
2023-07-01 13:20:25 +00:00
|
|
|
if (!textFunc(slice(lineStartIt, splitIt), lines++))
|
2023-06-28 10:08:11 +00:00
|
|
|
return false;
|
2024-04-03 01:19:55 +00:00
|
|
|
unsigned stringWidth = linePixelWidth - splitWidth;
|
2023-06-20 04:33:09 +00:00
|
|
|
linePixelWidth = stringWidth + charWidth; // and is as wide as the bit after the space.
|
2024-04-04 04:09:40 +00:00
|
|
|
lineStartIt = ++splitIt;
|
|
|
|
splitIt = end;
|
2023-06-20 04:33:09 +00:00
|
|
|
} else {
|
2023-07-01 13:20:25 +00:00
|
|
|
if (!textFunc(slice(lineStartIt, it), lines++))
|
2023-06-28 10:08:11 +00:00
|
|
|
return false;
|
2024-04-03 01:19:55 +00:00
|
|
|
lineStartIt = it; // include that 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-07-01 14:36:32 +00:00
|
|
|
++it;
|
2023-06-28 10:08:11 +00:00
|
|
|
};
|
|
|
|
|
2023-07-01 04:01:27 +00:00
|
|
|
// if we hit the end of the string before hitting the end of the line.
|
|
|
|
if (lineCharSize > 0)
|
2023-07-01 13:20:25 +00:00
|
|
|
return textFunc(slice(lineStartIt, end), lines);
|
|
|
|
|
|
|
|
return 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 = {};
|
2024-04-21 20:07:59 +00:00
|
|
|
auto last = views.end();
|
|
|
|
unsigned curLine = 0;
|
|
|
|
TextPainter::WrapTextCallback textCallback = [&](StringView text, unsigned line) {
|
|
|
|
if (line == curLine && last != views.end() && last->end() == text.begin()) {
|
|
|
|
*last = StringView(last->utf8Ptr(), last->utf8Size() + text.utf8Size());
|
|
|
|
} else {
|
|
|
|
last = views.insert(views.end(), text);
|
|
|
|
curLine = line;
|
2023-06-28 10:08:11 +00:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
2023-07-01 14:52:36 +00:00
|
|
|
processWrapText(s, wrapWidth.ptr(), textCallback);
|
2023-06-28 10:08:11 +00:00
|
|
|
|
|
|
|
return views;
|
|
|
|
}
|
|
|
|
|
|
|
|
StringList TextPainter::wrapText(StringView s, Maybe<unsigned> wrapWidth) {
|
|
|
|
StringList result;
|
|
|
|
|
|
|
|
String current;
|
|
|
|
int lastLine = 0;
|
2024-04-21 20:07:59 +00:00
|
|
|
TextPainter::WrapTextCallback textCallback = [&](StringView text, unsigned line) {
|
2023-06-28 10:08:11 +00:00
|
|
|
if (lastLine != line) {
|
2024-02-19 15:55:19 +00:00
|
|
|
result.append(std::move(current));
|
2023-06-28 10:08:11 +00:00
|
|
|
lastLine = line;
|
|
|
|
}
|
|
|
|
current += text;
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
2023-07-01 14:52:36 +00:00
|
|
|
processWrapText(s, wrapWidth.ptr(), textCallback);
|
2023-06-28 10:08:11 +00:00
|
|
|
|
|
|
|
if (!current.empty())
|
2024-02-19 15:55:19 +00:00
|
|
|
result.append(std::move(current));
|
2023-06-28 10:08:11 +00:00
|
|
|
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
unsigned TextPainter::fontSize() const {
|
2024-04-21 20:07:59 +00:00
|
|
|
return m_renderSettings.fontSize;
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void TextPainter::setFontSize(unsigned size) {
|
2024-04-21 20:07:59 +00:00
|
|
|
m_renderSettings.fontSize = size;
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void TextPainter::setLineSpacing(float lineSpacing) {
|
2024-04-21 20:07:59 +00:00
|
|
|
m_renderSettings.lineSpacing = lineSpacing;
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void TextPainter::setMode(FontMode mode) {
|
2024-04-21 20:07:59 +00:00
|
|
|
m_renderSettings.shadow = fontModeToColor(mode).toRgba();
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void TextPainter::setFontColor(Vec4B color) {
|
2024-02-19 15:55:19 +00:00
|
|
|
m_renderSettings.color = std::move(color);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
2024-04-21 20:07:59 +00:00
|
|
|
void TextPainter::setProcessingDirectives(StringView directives, bool back) {
|
|
|
|
Directives& target = back ? m_renderSettings.backDirectives : m_renderSettings.directives;
|
|
|
|
modifyDirectives(target = String(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
|
|
|
}
|
|
|
|
|
2024-04-21 20:07:59 +00:00
|
|
|
TextStyle& TextPainter::setTextStyle(TextStyle const& textStyle) {
|
|
|
|
TextStyle& style = m_renderSettings = textStyle;
|
|
|
|
modifyDirectives(style.directives);
|
|
|
|
modifyDirectives(style.backDirectives);
|
|
|
|
return style;
|
|
|
|
}
|
|
|
|
|
|
|
|
void TextPainter::clearTextStyle() {
|
|
|
|
m_renderSettings = m_defaultRenderSettings;
|
|
|
|
}
|
|
|
|
|
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();
|
2023-08-04 13:47:52 +00:00
|
|
|
String defaultName = "hobo";
|
|
|
|
auto defaultFont = loadFont("/hobo.ttf", defaultName);
|
2024-03-25 01:49:18 +00:00
|
|
|
auto loadFontsByExtension = [&](String const& ext) {
|
|
|
|
for (auto& fontPath : assets->scanExtension(ext)) {
|
|
|
|
auto font = assets->font(fontPath);
|
|
|
|
if (font == defaultFont)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
auto name = AssetPath::filename(fontPath);
|
|
|
|
name = name.substr(0, name.findLast("."));
|
|
|
|
addFont(loadFont(fontPath, name), name);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
loadFontsByExtension("ttf");
|
|
|
|
loadFontsByExtension("woff2");
|
2023-08-04 13:47:52 +00:00
|
|
|
m_fontTextureGroup.addFont(defaultFont, defaultName, true);
|
2024-04-21 22:17:10 +00:00
|
|
|
m_fontTextureGroup.setFallbackFont("unifont");
|
2023-06-21 13:13:37 +00:00
|
|
|
}
|
|
|
|
|
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;
|
2024-04-21 20:07:59 +00:00
|
|
|
} else if (command.beginsWith("shadow")) {
|
|
|
|
if (command.utf8Size() == 6)
|
|
|
|
m_renderSettings.shadow = Color::Black.toRgba();
|
|
|
|
else if (command[6] == '=')
|
|
|
|
m_renderSettings.shadow = Color(command.substr(7)).toRgba();
|
2023-06-23 09:32:41 +00:00
|
|
|
} else if (command == "noshadow") {
|
2024-04-21 20:07:59 +00:00
|
|
|
m_renderSettings.shadow = Color::Clear.toRgba();
|
2023-06-23 09:32:41 +00:00
|
|
|
} else if (command.beginsWith("font=")) {
|
|
|
|
m_renderSettings.font = command.substr(5);
|
|
|
|
} else if (command.beginsWith("directives=")) {
|
2023-07-03 20:01:29 +00:00
|
|
|
setProcessingDirectives(command.substr(11));
|
2024-04-21 20:07:59 +00:00
|
|
|
} else if (command.beginsWith("backdirectives=")) {
|
|
|
|
setProcessingDirectives(command.substr(15), true);
|
2023-06-23 09:32:41 +00:00
|
|
|
} 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
|
|
|
}
|
|
|
|
|
2024-04-21 20:07:59 +00:00
|
|
|
void TextPainter::modifyDirectives(Directives& directives) {
|
|
|
|
if (directives) {
|
|
|
|
directives.loadOperations();
|
|
|
|
for (auto& entry : directives->entries) {
|
|
|
|
if (auto border = entry.operation.ptr<BorderImageOperation>())
|
|
|
|
border->includeTransparent = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
2024-04-21 20:07:59 +00:00
|
|
|
TextStyle backup = m_savedRenderSettings = m_renderSettings;
|
|
|
|
int height = (lines.size() - 1) * backup.lineSpacing * backup.fontSize + backup.fontSize;
|
2023-06-20 04:33:09 +00:00
|
|
|
if (position.vAnchor == VerticalAnchor::BottomAnchor)
|
2024-04-21 20:07:59 +00:00
|
|
|
pos[1] += (height - backup.fontSize);
|
2023-06-20 04:33:09 +00:00
|
|
|
else if (position.vAnchor == VerticalAnchor::VMidAnchor)
|
2024-04-21 20:07:59 +00:00
|
|
|
pos[1] += (height - backup.fontSize) / 2;
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
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));
|
2024-04-21 20:07:59 +00:00
|
|
|
pos[1] -= m_renderSettings.fontSize * m_renderSettings.lineSpacing;
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
if (charLimit && *charLimit == 0)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2024-04-21 20:07:59 +00:00
|
|
|
m_renderSettings = std::move(backup);
|
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;
|
2023-06-28 16:42:05 +00:00
|
|
|
pos.pos[0] -= floor((float)stringWidth(trimmedString) / 2);
|
2023-06-20 04:33:09 +00:00
|
|
|
pos.hAnchor = HorizontalAnchor::LeftAnchor;
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2024-04-21 20:07:59 +00:00
|
|
|
--*charLimit;
|
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;
|
|
|
|
};
|
|
|
|
|
2023-06-28 21:05:01 +00:00
|
|
|
m_fontTextureGroup.switchFont(m_renderSettings.font);
|
2023-06-28 10:08:11 +00:00
|
|
|
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) {
|
2023-07-01 14:36:32 +00:00
|
|
|
if (c == '\n' || c == '\v' || c == '\r')
|
2023-06-20 04:33:09 +00:00
|
|
|
return RectF();
|
2023-06-29 18:34:10 +00:00
|
|
|
m_fontTextureGroup.switchFont(m_renderSettings.font);
|
2023-06-28 21:05:01 +00:00
|
|
|
|
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 16:42:05 +00:00
|
|
|
hOffset = -floor((float)width / 2);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
float vOffset = 0;
|
|
|
|
if (position.vAnchor == VerticalAnchor::VMidAnchor)
|
2024-04-21 20:07:59 +00:00
|
|
|
vOffset = -floor((float)m_renderSettings.fontSize / 2);
|
2023-06-20 04:33:09 +00:00
|
|
|
else if (position.vAnchor == VerticalAnchor::TopAnchor)
|
2024-04-21 20:07:59 +00:00
|
|
|
vOffset = -(float)m_renderSettings.fontSize;
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-07-03 20:01:29 +00:00
|
|
|
Directives* directives = m_renderSettings.directives ? &m_renderSettings.directives : nullptr;
|
|
|
|
|
2024-04-21 20:07:59 +00:00
|
|
|
Vec2F pos = position.pos + Vec2F(hOffset, vOffset);
|
2023-06-20 04:33:09 +00:00
|
|
|
if (reallyRender) {
|
2024-04-21 20:07:59 +00:00
|
|
|
bool hasShadow = m_renderSettings.shadow[3] > 0;
|
|
|
|
bool hasBackDirectives = m_renderSettings.backDirectives;
|
|
|
|
if (hasShadow) {
|
|
|
|
//Kae: unlike vanilla we draw only one shadow glyph instead of two, so i'm tweaking the alpha here
|
|
|
|
Vec4B shadow = m_renderSettings.shadow;
|
|
|
|
uint8_t alphaU = m_renderSettings.color[3] * byteToFloat(shadow[3]);
|
2023-06-20 14:59:41 +00:00
|
|
|
if (alphaU != 255) {
|
|
|
|
float alpha = byteToFloat(alphaU);
|
2024-04-21 20:07:59 +00:00
|
|
|
shadow[3] = floatToByte(alpha * (1.5f - 0.5f * alpha));
|
2023-06-20 14:59:41 +00:00
|
|
|
}
|
|
|
|
else
|
2024-04-21 20:07:59 +00:00
|
|
|
shadow[3] = alphaU;
|
2023-06-20 14:59:41 +00:00
|
|
|
|
2024-04-21 20:07:59 +00:00
|
|
|
Directives const* shadowDirectives = hasBackDirectives ? &m_renderSettings.backDirectives : directives;
|
|
|
|
renderGlyph(c, pos + Vec2F(0, -2), m_shadowPrimitives, m_renderSettings.fontSize, 1, shadow, shadowDirectives);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
2024-04-21 20:07:59 +00:00
|
|
|
if (hasBackDirectives)
|
|
|
|
renderGlyph(c, pos, m_backPrimitives, m_renderSettings.fontSize, 1, m_renderSettings.color, &m_renderSettings.backDirectives);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2024-04-21 20:07:59 +00:00
|
|
|
auto& output = (hasShadow || hasBackDirectives) ? m_frontPrimitives : m_renderer->immediatePrimitives();
|
|
|
|
renderGlyph(c, pos, output, m_renderSettings.fontSize, 1, m_renderSettings.color, directives);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
2024-04-21 20:07:59 +00:00
|
|
|
return RectF::withSize(pos, {(float)width, (int)m_renderSettings.fontSize});
|
|
|
|
}
|
|
|
|
|
|
|
|
void TextPainter::renderPrimitives() {
|
|
|
|
auto& destination = m_renderer->immediatePrimitives();
|
|
|
|
std::move(std::begin(m_shadowPrimitives), std::end(m_shadowPrimitives), std::back_inserter(destination));
|
|
|
|
m_shadowPrimitives.clear();
|
|
|
|
std::move(std::begin(m_backPrimitives), std::end(m_backPrimitives), std::back_inserter(destination));
|
|
|
|
m_backPrimitives.clear();
|
|
|
|
std::move(std::begin(m_frontPrimitives), std::end(m_frontPrimitives), std::back_inserter(destination));
|
|
|
|
m_frontPrimitives.clear();
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
2024-04-21 20:07:59 +00:00
|
|
|
void TextPainter::renderGlyph(String::Char c, Vec2F const& screenPos, List<RenderPrimitive>& out, unsigned fontSize,
|
2023-07-03 20:01:29 +00:00
|
|
|
float scale, Vec4B const& color, Directives const* processingDirectives) {
|
2023-06-20 04:33:09 +00:00
|
|
|
if (!fontSize)
|
|
|
|
return;
|
|
|
|
|
2023-06-20 14:59:41 +00:00
|
|
|
const FontTextureGroup::GlyphTexture& glyphTexture = m_fontTextureGroup.glyphTexture(c, fontSize, processingDirectives);
|
2023-07-03 04:21:51 +00:00
|
|
|
Vec2F offset = glyphTexture.offset * scale;
|
2024-04-21 20:07:59 +00:00
|
|
|
out.emplace_back(std::in_place_type_t<RenderQuad>(), glyphTexture.texture, Vec2F::round(screenPos + offset), scale, color, 0.0f);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
2023-08-04 13:47:52 +00:00
|
|
|
FontPtr TextPainter::loadFont(String const& fontPath, Maybe<String> fontName) {
|
|
|
|
if (!fontName) {
|
|
|
|
auto name = AssetPath::filename(fontPath);
|
|
|
|
fontName.emplace(name.substr(0, name.findLast(".")));
|
|
|
|
}
|
|
|
|
|
|
|
|
auto assets = Root::singleton().assets();
|
|
|
|
|
|
|
|
auto font = assets->font(fontPath)->clone();
|
|
|
|
if (auto fontConfig = assets->json("/interface.config:font").opt(*fontName)) {
|
|
|
|
font->setAlphaThreshold(fontConfig->getUInt("alphaThreshold", 0));
|
|
|
|
}
|
|
|
|
return font;
|
|
|
|
}
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|