#include "../include/Text.hpp" #include "../include/Cache.hpp" #include "../include/Gif.hpp" #include #include namespace dchat { const float TAB_WIDTH = 4.0f; const float EMOJI_PADDING = 5.0f; const float EMOJI_SCALE_WITH_TEXT = 1.0f; const float EMOJI_SCALE_STANDALONE = 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), lineSpacing(0.0f) { setString(_str); } void Text::setString(const sf::String &str) { if(str != this->str) { this->str = str; dirty = true; textElements.clear(); stringSplitElements(this->str, 0); } } void Text::appendStringNewLine(const sf::String &str) { usize prevSize = this->str.getSize(); this->str += '\n'; this->str += str; dirty = true; stringSplitElements(this->str, prevSize); } 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; } } void Text::setLineSpacing(float lineSpacing) { if(lineSpacing != this->lineSpacing) { this->lineSpacing = lineSpacing; dirty = true; } } float Text::getHeight() const { return totalHeight; } void Text::stringSplitElements(sf::String &stringToSplit, usize startIndex) { if(plainText) { StringViewUtf32 wholeStr(&stringToSplit[startIndex], stringToSplit.getSize() - startIndex); textElements.push_back({ wholeStr, TextElement::Type::TEXT }); return; } size_t offset = startIndex; while(offset < stringToSplit.getSize()) { size_t stringStart = offset; size_t foundStartIndex = stringToSplit.find("[emoji](", offset); size_t foundEndIndex = -1; if(foundStartIndex != -1) { offset += (foundStartIndex + 8); foundEndIndex = stringToSplit.find(")", offset); } if(foundEndIndex != -1) { StringViewUtf32 beforeEmojiStr(&stringToSplit[stringStart], foundStartIndex - stringStart); textElements.push_back({ beforeEmojiStr, TextElement::Type::TEXT }); StringViewUtf32 url(&stringToSplit[offset], foundEndIndex - offset); textElements.push_back({ url, TextElement::Type::EMOJI }); offset = foundEndIndex + 1; } else { StringViewUtf32 strToEnd(&stringToSplit[stringStart], stringToSplit.getSize() - stringStart); textElements.push_back({ strToEnd, TextElement::Type::TEXT }); offset = stringToSplit.getSize(); } } for(std::vector::iterator it = textElements.begin(); it != textElements.end();) { if(it->text.size == 0) it = textElements.erase(it); else ++it; } } // 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); sf::Vector2f glyphPos; sf::Uint32 prevCodePoint = 0; size_t lastSpacingWordWrapIndex = -1; float lastSpacingAccumulatedOffset = 0.0f; for(usize textElementIndex = 0; textElementIndex < textElements.size(); ++textElementIndex) { TextElement &textElement = textElements[textElementIndex]; if(textElement.type == TextElement::Type::EMOJI) { bool ownLineLeft = false; if(textElementIndex == 0) ownLineLeft = true; else { TextElement &prevElement = textElements[textElementIndex - 1]; if(prevElement.text[prevElement.text.size - 1] == '\n') ownLineLeft = true; } bool ownLineRight = false; if(textElementIndex == textElements.size() - 1) ownLineRight = true; else { TextElement &nextElement = textElements[textElementIndex + 1]; if(nextElement.text[0] == '\n') ownLineRight = true; } if(ownLineLeft && ownLineRight) textElement.ownLine = true; float emojiSize = vspace * (textElement.ownLine ? EMOJI_SCALE_STANDALONE : EMOJI_SCALE_WITH_TEXT); glyphPos.x += EMOJI_PADDING; textElement.position.x = glyphPos.x; if(textElement.ownLine) { textElement.position.y = glyphPos.y; // TODO: Find a better way to do this, @totalHeight is wrong because we add emojiSize and then vspace glyphPos.y += emojiSize - vspace; } else { 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 + lineSpacing; } 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 + lineSpacing; continue; } case '\v': { glyphPos.y += (vspace * TAB_WIDTH) + lineSpacing; 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 + lineSpacing; } } glyphPos.x -= lastSpacingAccumulatedOffset; lastSpacingWordWrapIndex = -1; lastSpacingAccumulatedOffset = 0.0f; } else glyphPos.x = 0.0f; glyphPos.y += vspace + lineSpacing; } 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 + lineSpacing + vspace; } void Text::draw(sf::RenderTarget &target, Cache &cache) { if(dirty) { updateGeometry(); dirty = false; } float vspace = font->getLineSpacing(characterSize); sf::RenderStates states; sf::Vector2f pos = position; pos.y += floor(vspace); // Origin is at bottom left, we want it to be at top left // TODO: Do not use maxWidth here. Max width might be set to 99999 and actual text width might be 200. Text width should be calculated instead //sf::FloatRect targetRect(0.0f, 0.0f, maxWidth, target.getSize().y); //sf::FloatRect textRect(pos.x, pos.y, maxWidth, ) //colRect.contains() //if(pos.x + maxWidth <= 0.0f || pos.x >= maxWidth || pos.y + totalHeight <= 0.0f || pos.y >= target.getSize().y) return; if(pos.y + totalHeight <= 0.0f || pos.y >= target.getSize().y) return; 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; pos.x = floor(pos.x); pos.y = floor(pos.y); float emojiSize = vspace * (textElement.ownLine ? EMOJI_SCALE_STANDALONE : EMOJI_SCALE_WITH_TEXT); 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*)&u8Str; const ImageByUrlResult imageByUrlResult = cache.getImageByUrl(utf8Str); if(imageByUrlResult.type == ImageByUrlResult::Type::CACHED) { if(imageByUrlResult.isGif) { auto gifSize = imageByUrlResult.gif->getSize(); float widthToHeightRatio = (float)gifSize.x / (float)gifSize.y; imageByUrlResult.gif->setPosition(pos); imageByUrlResult.gif->setScale(sf::Vector2f(size.x / (float)gifSize.x * widthToHeightRatio, size.y / (float)gifSize.y)); imageByUrlResult.gif->draw(target); } else { auto textureSize = imageByUrlResult.texture->getSize(); float widthToHeightRatio = (float)textureSize.x / (float)textureSize.y; // 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)textureSize.x * widthToHeightRatio, size.y / (float)textureSize.y); target.draw(sprite); } } else { sf::RectangleShape rect(size); rect.setFillColor(sf::Color::White); rect.setPosition(pos); target.draw(rect); } } } } }