diff options
Diffstat (limited to 'src/Text.cpp')
-rw-r--r-- | src/Text.cpp | 308 |
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); + } + } + } + } +} |