aboutsummaryrefslogtreecommitdiff
path: root/src/Text.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/Text.cpp')
-rw-r--r--src/Text.cpp308
1 files changed, 308 insertions, 0 deletions
diff --git a/src/Text.cpp b/src/Text.cpp
new file mode 100644
index 0000000..41eb4e3
--- /dev/null
+++ b/src/Text.cpp
@@ -0,0 +1,308 @@
+#include "../include/Text.hpp"
+#include "../include/Cache.hpp"
+#include "../include/Gif.hpp"
+#include <SFML/Graphics/RectangleShape.hpp>
+
+namespace dchat
+{
+ const float TAB_WIDTH = 4.0f;
+ const float EMOJI_PADDING = 5.0f;
+
+ Text::Text(const sf::Font &_font) :
+ font(_font),
+ characterSize(0),
+ maxWidth(0.0f),
+ color(sf::Color::White),
+ dirty(false),
+ plainText(false),
+ totalHeight(0.0f)
+ {
+
+ }
+
+ Text::Text(const sf::String &_str, const sf::Font &_font, unsigned int _characterSize, float _maxWidth, bool _plainText) :
+ font(_font),
+ characterSize(_characterSize),
+ vertices(sf::PrimitiveType::Quads),
+ maxWidth(_maxWidth),
+ color(sf::Color::White),
+ dirty(true),
+ plainText(_plainText),
+ totalHeight(0.0f)
+ {
+ setString(_str);
+ }
+
+ void Text::setString(const sf::String &str)
+ {
+ if(str != this->str)
+ {
+ this->str = str;
+ dirty = true;
+ stringSplitElements();
+ }
+ }
+
+ void Text::setPosition(float x, float y)
+ {
+ position.x = x;
+ position.y = y;
+ }
+
+ void Text::setPosition(const sf::Vector2f &position)
+ {
+ this->position = position;
+ }
+
+ void Text::setMaxWidth(float maxWidth)
+ {
+ if(maxWidth != this->maxWidth)
+ {
+ this->maxWidth = maxWidth;
+ dirty = true;
+ }
+ }
+
+ void Text::setCharacterSize(unsigned int characterSize)
+ {
+ if(characterSize != this->characterSize)
+ {
+ this->characterSize = characterSize;
+ dirty = true;
+ }
+ }
+
+ void Text::setFillColor(sf::Color color)
+ {
+ if(color != this->color)
+ {
+ this->color = color;
+ dirty = true;
+ }
+ }
+
+ float Text::getHeight() const
+ {
+ return totalHeight;
+ }
+
+ void Text::stringSplitElements()
+ {
+ textElements.clear();
+ if(plainText)
+ {
+ StringViewUtf32 wholeStr(&str[0], str.getSize());
+ textElements.push_back({ wholeStr, TextElement::Type::TEXT });
+ return;
+ }
+
+ size_t offset = 0;
+ while(offset < str.getSize())
+ {
+ size_t stringStart = offset;
+ size_t foundStartIndex = str.find("[emoji](", offset);
+ size_t foundEndIndex = -1;
+ if(foundStartIndex != -1)
+ {
+ offset += (foundStartIndex + 8);
+ foundEndIndex = str.find(")", offset);
+ }
+
+ if(foundEndIndex != -1)
+ {
+ StringViewUtf32 beforeEmojiStr(&str[stringStart], foundStartIndex - stringStart);
+ textElements.push_back({ beforeEmojiStr, TextElement::Type::TEXT });
+
+ StringViewUtf32 url(&str[offset], foundEndIndex - offset);
+ textElements.push_back({ url, TextElement::Type::EMOJI });
+ offset = foundEndIndex + 1;
+ }
+ else
+ {
+ StringViewUtf32 strToEnd(&str[stringStart], str.getSize() - stringStart);
+ textElements.push_back({ strToEnd, TextElement::Type::TEXT });
+ offset = str.getSize();
+ }
+ }
+ }
+
+ // Logic loosely based on https://github.com/SFML/SFML/wiki/Source:-CurvedText
+ void Text::updateGeometry()
+ {
+ vertices.clear();
+ float hspace = font.getGlyph(' ', characterSize, false).advance;
+ float vspace = font.getLineSpacing(characterSize);
+ float emojiSize = vspace * 1.0;
+
+ sf::Vector2f glyphPos;
+ sf::Uint32 prevCodePoint = 0;
+ size_t lastSpacingWordWrapIndex = -1;
+ float lastSpacingAccumulatedOffset = 0.0f;
+ for(TextElement &textElement : textElements)
+ {
+ if(textElement.type == TextElement::Type::EMOJI)
+ {
+ glyphPos.x += EMOJI_PADDING;
+ textElement.position.x = glyphPos.x;
+ textElement.position.y = glyphPos.y + vspace * 0.5f - emojiSize * 0.5f;
+ glyphPos.x += emojiSize + EMOJI_PADDING;
+ if(glyphPos.x > maxWidth)
+ {
+ glyphPos.x = 0.0f;
+ glyphPos.y += vspace;
+ }
+ continue;
+ }
+
+ usize vertexOffset = vertices.getVertexCount();
+ vertices.resize(vertices.getVertexCount() + (4 * textElement.text.size));
+ textElement.position = glyphPos;
+ for(size_t i = 0; i < textElement.text.size; ++i)
+ {
+ sf::Uint32 codePoint = textElement.text[i];
+ float kerning = font.getKerning(prevCodePoint, codePoint, characterSize);
+ prevCodePoint = codePoint;
+ glyphPos.x += kerning;
+
+ switch(codePoint)
+ {
+ case ' ':
+ {
+ glyphPos.x += hspace;
+ if(glyphPos.x > maxWidth * 0.5f)
+ {
+ lastSpacingWordWrapIndex = i;
+ lastSpacingAccumulatedOffset = glyphPos.x;
+ }
+ continue;
+ }
+ case '\t':
+ {
+ glyphPos.x += (hspace * TAB_WIDTH);
+ if(glyphPos.x > maxWidth * 0.5f)
+ {
+ lastSpacingWordWrapIndex = i;
+ lastSpacingAccumulatedOffset = glyphPos.x;
+ }
+ continue;
+ }
+ case '\n':
+ {
+ glyphPos.x = 0.0f;
+ glyphPos.y += vspace;
+ continue;
+ }
+ case '\v':
+ {
+ glyphPos.y += (vspace * TAB_WIDTH);
+ continue;
+ }
+ }
+
+ const sf::Glyph &glyph = font.getGlyph(codePoint, characterSize, false);
+ if(glyphPos.x + glyph.advance > maxWidth)
+ {
+ // If there was a space in the text and text width is too long, then we need to word wrap at space index instead,
+ // which means we need to change the position of all vertices after the space to the current vertex
+ //printf("last spacing word wrap index: %zu\n", lastSpacingWordWrapIndex);
+ if(lastSpacingWordWrapIndex != -1)
+ {
+ for(size_t j = lastSpacingWordWrapIndex; j < i; ++j)
+ {
+ for(size_t k = 0; k < 4; ++k)
+ {
+ sf::Vector2f &vertexPos = vertices[vertexOffset + j * 4 + k].position;
+ vertexPos.x -= lastSpacingAccumulatedOffset;
+ vertexPos.y += vspace;
+ }
+ }
+
+ glyphPos.x -= lastSpacingAccumulatedOffset;
+ lastSpacingWordWrapIndex = -1;
+ lastSpacingAccumulatedOffset = 0.0f;
+ }
+ else
+ glyphPos.x = 0.0f;
+
+ glyphPos.y += vspace;
+ }
+
+ sf::Vector2f vertexTopLeft(glyphPos.x + glyph.bounds.left, glyphPos.y + glyph.bounds.top);
+ sf::Vector2f vertexTopRight(glyphPos.x + glyph.bounds.left + glyph.bounds.width, glyphPos.y + glyph.bounds.top);
+ sf::Vector2f vertexBottomLeft(glyphPos.x + glyph.bounds.left, glyphPos.y + glyph.bounds.top + glyph.bounds.height);
+ sf::Vector2f vertexBottomRight(glyphPos.x + glyph.bounds.left + glyph.bounds.width, glyphPos.y + glyph.bounds.top + glyph.bounds.height);
+
+ sf::Vector2f textureTopLeft(glyph.textureRect.left, glyph.textureRect.top);
+ sf::Vector2f textureTopRight(glyph.textureRect.left + glyph.textureRect.width, glyph.textureRect.top);
+ sf::Vector2f textureBottomLeft(glyph.textureRect.left, glyph.textureRect.top + glyph.textureRect.height);
+ sf::Vector2f textureBottomRight(glyph.textureRect.left + glyph.textureRect.width, glyph.textureRect.top + glyph.textureRect.height);
+
+ vertices[vertexOffset + i * 4 + 0] = { vertexTopLeft, color, textureTopLeft };
+ vertices[vertexOffset + i * 4 + 1] = { vertexTopRight, color, textureTopRight };
+ vertices[vertexOffset + i * 4 + 2] = { vertexBottomRight, color, textureBottomRight };
+ vertices[vertexOffset + i * 4 + 3] = { vertexBottomLeft, color, textureBottomLeft };
+
+ glyphPos.x += glyph.advance;
+ }
+ }
+ totalHeight = glyphPos.y + vspace;
+ }
+
+ void Text::draw(sf::RenderTarget &target, Cache &cache)
+ {
+ if(dirty)
+ {
+ updateGeometry();
+ dirty = false;
+ }
+
+ float vspace = font.getLineSpacing(characterSize);
+ float emojiSize = vspace * 1.0;
+
+ sf::RenderStates states;
+ sf::Vector2f pos = position;
+ pos.y += vspace; // Origin is at bottom left, we want it to be at top left
+ states.transform.translate(pos);
+ states.texture = &font.getTexture(characterSize);
+ target.draw(vertices, states);
+
+ for(TextElement &textElement : textElements)
+ {
+ if(textElement.type == TextElement::Type::EMOJI)
+ {
+ sf::Vector2f pos = position;
+ pos += textElement.position;
+ sf::Vector2f size(emojiSize, emojiSize);
+
+ // TODO: Optimize this (add unordered_map that takes StringViewUtf32 as key)
+ auto u8Str = sf::String::fromUtf32(textElement.text.data, textElement.text.data + textElement.text.size).toUtf8();
+ const std::string &utf8Str = *(std::basic_string<char>*)&u8Str;
+ const ImageByUrlResult imageByUrlResult = cache.getImageByUrl(utf8Str);
+ if(imageByUrlResult.type == ImageByUrlResult::Type::CACHED)
+ {
+ if(imageByUrlResult.isGif)
+ {
+ imageByUrlResult.gif->setPosition(pos);
+ imageByUrlResult.gif->setSize(size);
+ imageByUrlResult.gif->draw(target);
+ }
+ else
+ {
+ // TODO: Store this sprite somewhere, might not be efficient to create a new sprite object every frame
+ sf::Sprite sprite(*imageByUrlResult.texture);
+ sprite.setPosition(pos);
+ sprite.setScale(size.x / (float)imageByUrlResult.texture->getSize().x, size.y / (float)imageByUrlResult.texture->getSize().y);
+ target.draw(sprite);
+ }
+ }
+ else
+ {
+ sf::RectangleShape rect(size);
+ rect.setFillColor(sf::Color::White);
+ rect.setPosition(pos);
+ target.draw(rect);
+ }
+ }
+ }
+ }
+}