Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/rendering/StarTextPainter.cpp
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
committerKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
commit6352e8e3196f78388b6c771073f9e03eaa612673 (patch)
treee23772f79a7fbc41bc9108951e9e136857484bf4 /source/rendering/StarTextPainter.cpp
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/rendering/StarTextPainter.cpp')
-rw-r--r--source/rendering/StarTextPainter.cpp402
1 files changed, 402 insertions, 0 deletions
diff --git a/source/rendering/StarTextPainter.cpp b/source/rendering/StarTextPainter.cpp
new file mode 100644
index 0000000..0ef374d
--- /dev/null
+++ b/source/rendering/StarTextPainter.cpp
@@ -0,0 +1,402 @@
+#include "StarTextPainter.hpp"
+#include "StarJsonExtra.hpp"
+
+#include <regex>
+
+namespace Star {
+
+namespace Text {
+ String stripEscapeCodes(String const& s) {
+ String regex = strf("\\%s[^;]*%s", CmdEsc, EndEsc);
+ return std::regex_replace(s.utf8(), std::regex(regex.utf8()), "");
+ }
+
+ String preprocessEscapeCodes(String const& s) {
+ bool escape = false;
+ auto result = s.utf8();
+
+ size_t escapeStartIdx = 0;
+ for (size_t i = 0; i < result.size(); i++) {
+ auto& c = result[i];
+ if (c == CmdEsc || c == StartEsc) {
+ 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};
+}
+
+TextPainter::TextPainter(FontPtr font, RendererPtr renderer, TextureGroupPtr textureGroup)
+ : m_renderer(renderer),
+ m_fontTextureGroup(move(font), textureGroup),
+ m_fontSize(8),
+ m_lineSpacing(1.30f),
+ m_renderSettings({FontMode::Normal, Vec4B::filled(255)}),
+ m_splitIgnore(" \t"),
+ m_splitForce("\n\v"),
+ m_nonRenderedCharacters("\n\v\r") {}
+
+RectF TextPainter::renderText(String const& s, TextPositioning const& position) {
+ if (position.charLimit) {
+ unsigned charLimit = *position.charLimit;
+ return doRenderText(s, position, true, &charLimit);
+ } else {
+ return doRenderText(s, position, true, nullptr);
+ }
+}
+
+RectF TextPainter::renderLine(String const& s, TextPositioning const& position) {
+ 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);
+}
+
+RectF TextPainter::determineTextSize(String const& s, TextPositioning const& position) {
+ return doRenderText(s, position, false, nullptr);
+}
+
+RectF TextPainter::determineLineSize(String const& s, TextPositioning const& position) {
+ 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);
+}
+
+int TextPainter::stringWidth(String const& s) {
+ int width = 0;
+ bool escape = false;
+
+ for (String::Char c : Text::preprocessEscapeCodes(s)) {
+ if (c == Text::StartEsc)
+ escape = true;
+ if (!escape)
+ width += glyphWidth(c);
+ if (c == Text::EndEsc)
+ escape = false;
+ }
+
+ return width;
+}
+
+StringList TextPainter::wrapText(String const& s, Maybe<unsigned> wrapWidth) {
+ String text = Text::preprocessEscapeCodes(s);
+
+ unsigned lineStart = 0; // Where does this line start ?
+ unsigned lineCharSize = 0; // how many characters in this line ?
+ unsigned linePixelWidth = 0; // How wide is this line so far
+
+ bool inEscapeSequence = false;
+
+ unsigned splitPos = 0; // Where did we last see a place to split the string ?
+ unsigned splitWidth = 0; // How wide was the string there ?
+
+ StringList lines; // list of renderable string lines
+
+ // loop through every character in the string
+ for (auto character : text) {
+ // this up here to deal with the (common) occurance that the first charcter
+ // is an escape initiator
+ if (character == Text::StartEsc)
+ inEscapeSequence = true;
+
+ if (inEscapeSequence) {
+ lineCharSize++; // just jump straight to the next character, we don't care what it is.
+ if (character == Text::EndEsc)
+ inEscapeSequence = false;
+ } else {
+ lineCharSize++; // assume at least one character if we get here.
+
+ // is this a linefeed / cr / whatever that forces a line split ?
+ if (m_splitForce.find(String(character)) != NPos) {
+ // knock one off the end because we don't render the CR
+ lines.push_back(text.substr(lineStart, lineCharSize - 1));
+
+ 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 ?
+ if (m_splitIgnore.find(String(character)) != NPos) {
+ 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) {
+ lines.push_back(text.substr(lineStart, (splitPos - lineStart) - 1));
+
+ 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 {
+ // don't draw the last character that puts us over the edge
+ lines.push_back(text.substr(lineStart, lineCharSize - 1));
+
+ lineStart += lineCharSize - 1; // skip back by one to include that
+ // character on the next line.
+ lineCharSize = 1; // next line has that character in
+ linePixelWidth = charWidth; // and is as wide as that character
+ }
+ } else {
+ linePixelWidth += charWidth;
+ }
+ }
+ }
+ }
+
+ // if we hit the end of the string before hitting the end of the line.
+ if (lineCharSize > 0)
+ lines.push_back(text.substr(lineStart, lineCharSize));
+
+ return lines;
+}
+
+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) {
+ m_processingDirectives = move(directives);
+}
+
+void TextPainter::cleanup(int64_t timeout) {
+ m_fontTextureGroup.cleanup(timeout);
+}
+
+RectF TextPainter::doRenderText(String const& s, TextPositioning const& position, bool reallyRender, unsigned* charLimit) {
+ Vec2F pos = position.pos;
+ StringList lines = wrapText(s, position.wrapWidth);
+
+ int height = (lines.size() - 1) * m_lineSpacing * m_fontSize + m_fontSize;
+
+ auto savedRenderSettings = m_renderSettings;
+ 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());
+ for (auto i : lines) {
+ bounds.combine(doRenderLine(i, { pos, position.hAnchor, position.vAnchor }, reallyRender, charLimit));
+ pos[1] -= m_fontSize * m_lineSpacing;
+
+ if (charLimit && *charLimit == 0)
+ break;
+ }
+
+ m_renderSettings = savedRenderSettings;
+
+ return bounds;
+}
+
+RectF TextPainter::doRenderLine(String const& s, TextPositioning const& position, bool reallyRender, unsigned* charLimit) {
+ String text = s;
+ TextPositioning pos = position;
+
+ if (pos.hAnchor == HorizontalAnchor::RightAnchor) {
+ auto trimmedString = s;
+ if (charLimit)
+ trimmedString = s.slice(0, *charLimit);
+ pos.pos[0] -= stringWidth(trimmedString);
+ pos.hAnchor = HorizontalAnchor::LeftAnchor;
+ } else if (pos.hAnchor == HorizontalAnchor::HMidAnchor) {
+ auto trimmedString = s;
+ if (charLimit)
+ trimmedString = s.slice(0, *charLimit);
+ unsigned width = stringWidth(trimmedString);
+ pos.pos[0] -= std::floor(width / 2);
+ pos.hAnchor = HorizontalAnchor::LeftAnchor;
+ }
+
+ bool escape = false;
+ String escapeCode;
+ RectF bounds = RectF::withSize(pos.pos, Vec2F());
+ for (String::Char c : text) {
+ if (c == Text::StartEsc)
+ escape = true;
+
+ if (!escape) {
+ if (charLimit) {
+ if (*charLimit == 0)
+ break;
+ else
+ --*charLimit;
+ }
+ RectF glyphBounds = doRenderGlyph(c, pos, reallyRender);
+ bounds.combine(glyphBounds);
+ pos.pos[0] += glyphBounds.width();
+ } else if (c == Text::EndEsc) {
+ auto commands = escapeCode.split(',');
+ for (auto command : commands) {
+ 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 {
+ // expects both #... sequences and plain old color names.
+ Color c = jsonToColor(command);
+ c.setAlphaF(c.alphaF() * ((float)m_savedRenderSettings.color[3]) / 255);
+ m_renderSettings.color = c.toRgba();
+ }
+ } catch (JsonException&) {
+ } catch (ColorException&) {
+ }
+ }
+ escape = false;
+ escapeCode = "";
+ }
+ if (escape && (c != Text::StartEsc))
+ escapeCode.append(c);
+ }
+
+ return bounds;
+}
+
+RectF TextPainter::doRenderGlyph(String::Char c, TextPositioning const& position, bool reallyRender) {
+ if (m_nonRenderedCharacters.find(String(c)) != NPos)
+ return RectF();
+ 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)
+ hOffset = -std::floor(width / 2);
+
+ float vOffset = 0;
+ if (position.vAnchor == VerticalAnchor::VMidAnchor)
+ vOffset = -std::floor((float)m_fontSize / 2);
+ else if (position.vAnchor == VerticalAnchor::TopAnchor)
+ vOffset = -(float)m_fontSize;
+
+ if (reallyRender) {
+ if ((int)m_renderSettings.mode & (int)FontMode::Shadow) {
+ Color shadow = Color::Black;
+ shadow.setAlpha(m_renderSettings.color[3]);
+ renderGlyph(c, position.pos + Vec2F(hOffset, vOffset - 2), m_fontSize, 1, shadow.toRgba(), m_processingDirectives);
+ renderGlyph(c, position.pos + Vec2F(hOffset, vOffset - 1), m_fontSize, 1, shadow.toRgba(), m_processingDirectives);
+ }
+
+ renderGlyph(c, position.pos + Vec2F(hOffset, vOffset), m_fontSize, 1, m_renderSettings.color, m_processingDirectives);
+ }
+
+ 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;
+
+ auto texture = m_fontTextureGroup.glyphTexture(c, fontSize, processingDirectives);
+ m_renderer->render(renderTexturedRect(move(texture), Vec2F(screenPos), scale, color, 0.0f));
+}
+
+}