#include "../include/Text.hpp" #include "../include/Cache.hpp" #include "../include/Gif.hpp" #include "../include/WebPagePreview.hpp" #include #include namespace dchat { const float TAB_WIDTH = 4.0f; const float EMOJI_PADDING = 5.0f; const float EMOJI_SCALE_WITH_TEXT = 1.7f; const float EMOJI_SCALE_STANDALONE = 5.0f; const float IMAGE_HEIGHT_SCALE = 15.0f; const sf::Color URL_COLOR(15, 192, 252); Text::Text(const sf::Font *_font) : font(_font), characterSize(0), maxWidth(0.0f), color(sf::Color::White), urlColor(URL_COLOR), 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), urlColor(URL_COLOR), 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(fabs(lineSpacing - this->lineSpacing) > 0.001f) { this->lineSpacing = lineSpacing; dirty = true; } } float Text::getHeight() const { return totalHeight; } size_t stringSplitUrl(const StringViewUtf32 textElementStr, const sf::String &urlStr, size_t offset, std::vector &newTextElements) { size_t stringStart = offset; size_t urlStart = textElementStr.find(StringViewUtf32(urlStr.getData(), urlStr.getSize()), offset); if(urlStart != -1) { offset = urlStart + urlStr.getSize(); while(offset < textElementStr.size) { if(isspace(textElementStr[offset])) break; ++offset; } StringViewUtf32 beforeUrlStr(textElementStr.data + stringStart, urlStart - stringStart); newTextElements.push_back({ beforeUrlStr, TextElement::Type::TEXT }); StringViewUtf32 url(textElementStr.data + urlStart, offset - urlStart); newTextElements.push_back({ url, TextElement::Type::URL }); return offset; } return -1; } void Text::stringSplitElements(sf::String &stringToSplit, usize startIndex) { StringViewUtf32 wholeStr(&stringToSplit[startIndex], stringToSplit.getSize() - startIndex); textElements.push_back({ wholeStr, TextElement::Type::TEXT }); if(plainText) return; const char *httpStrRaw = "http://"; const sf::String httpStr = sf::String::fromUtf8(httpStrRaw, httpStrRaw + 7); const char *httpsStrRaw = "https://"; const sf::String httpsStr = sf::String::fromUtf8(httpsStrRaw, httpsStrRaw + 8); const char *emojiStrRaw = "[emoji]("; const sf::String emojiStr = sf::String::fromUtf8(emojiStrRaw, emojiStrRaw + 8); const char *parentheseStrRaw = ")"; const sf::String parentheseStr = sf::String::fromUtf8(parentheseStrRaw, parentheseStrRaw + 1); static_assert(sizeof(*parentheseStr.getData()) == sizeof(u32), "sf::String size has changed..."); std::vector newTextElements; for(size_t i = 0; i < textElements.size(); ++i) { TextElement textElement = textElements[i]; if(textElement.type != TextElement::Type::TEXT) { newTextElements.push_back(textElement); continue; } size_t offset = 0; while(offset < textElement.text.size) { size_t stringStart = offset; size_t foundStartIndex = textElement.text.find(StringViewUtf32(emojiStr.getData(), emojiStr.getSize()), offset); size_t foundEndIndex = -1; if(foundStartIndex != -1) { offset = foundStartIndex + 8; foundEndIndex = textElement.text.find(StringViewUtf32(parentheseStr.getData(), parentheseStr.getSize()), offset); } if(foundEndIndex != -1) { StringViewUtf32 beforeEmojiStr(textElement.text.data + stringStart, foundStartIndex - stringStart); newTextElements.push_back({ beforeEmojiStr, TextElement::Type::TEXT }); StringViewUtf32 url(textElement.text.data + offset, foundEndIndex - offset); newTextElements.push_back({ url, TextElement::Type::EMOJI }); offset = foundEndIndex + 1; } else { StringViewUtf32 strToEnd(textElement.text.data + stringStart, textElement.text.size - stringStart); newTextElements.push_back({ strToEnd, TextElement::Type::TEXT }); offset = textElement.text.size; } } } textElements = newTextElements; newTextElements.clear(); for(size_t i = 0; i < textElements.size(); ++i) { TextElement textElement = textElements[i]; if(textElement.type != TextElement::Type::TEXT) { newTextElements.push_back(textElement); continue; } size_t offset = 0; while(offset < textElement.text.size) { size_t urlEnd = stringSplitUrl(textElement.text, httpStr, offset, newTextElements); if(urlEnd == -1) { StringViewUtf32 strToEnd(textElement.text.data + offset, textElement.text.size - offset); newTextElements.push_back({ strToEnd, TextElement::Type::TEXT }); offset = textElement.text.size; } else offset = urlEnd; } } textElements = newTextElements; newTextElements.clear(); for(size_t i = 0; i < textElements.size(); ++i) { TextElement textElement = textElements[i]; if(textElement.type != TextElement::Type::TEXT) { newTextElements.push_back(textElement); continue; } size_t offset = 0; while(offset < textElement.text.size) { size_t urlEnd = stringSplitUrl(textElement.text, httpsStr, offset, newTextElements); if(urlEnd == -1) { StringViewUtf32 strToEnd(textElement.text.data + offset, textElement.text.size - offset); newTextElements.push_back({ strToEnd, TextElement::Type::TEXT }); offset = textElement.text.size; } else offset = urlEnd; } } textElements = newTextElements; 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); sf::Color fontColor = (textElement.type == TextElement::Type::TEXT ? color : urlColor); vertices[vertexOffset + i * 4 + 0] = { vertexTopLeft, fontColor, textureTopLeft }; vertices[vertexOffset + i * 4 + 1] = { vertexTopRight, fontColor, textureTopRight }; vertices[vertexOffset + i * 4 + 2] = { vertexBottomRight, fontColor, textureBottomRight }; vertices[vertexOffset + i * 4 + 3] = { vertexBottomLeft, fontColor, textureBottomLeft }; glyphPos.x += glyph.advance; } if(textElement.type != TextElement::Type::TEXT) { prevCodePoint = 0; } if(textElement.type == TextElement::Type::URL) { glyphPos.y += vspace + lineSpacing; textElement.position.y = glyphPos.y; glyphPos.x = 0.0f; glyphPos.y += floor(vspace * IMAGE_HEIGHT_SCALE) + lineSpacing; } } totalHeight = glyphPos.y + vspace + lineSpacing; } 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) { float emojiSize = vspace * (textElement.ownLine ? EMOJI_SCALE_STANDALONE : EMOJI_SCALE_WITH_TEXT); sf::Vector2f pos = position; pos += textElement.position; pos.x = floor(pos.x); pos.y = floor(pos.y); 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 ContentByUrlResult contentByUrlResult = cache.getContentByUrl(utf8Str); if(contentByUrlResult.type == ContentByUrlResult::Type::CACHED) { switch(contentByUrlResult.cachedType) { case ContentByUrlResult::CachedType::GIF: { auto gifSize = contentByUrlResult.gif->getSize(); float widthToHeightRatio = (float)gifSize.x / (float)gifSize.y; contentByUrlResult.gif->setPosition(pos); contentByUrlResult.gif->setScale(sf::Vector2f(size.x / (float)gifSize.x * widthToHeightRatio, size.y / (float)gifSize.y)); contentByUrlResult.gif->draw(target); break; } case ContentByUrlResult::CachedType::TEXTURE: { auto textureSize = contentByUrlResult.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(*contentByUrlResult.texture); sprite.setPosition(pos); sprite.setScale(size.x / (float)textureSize.x * widthToHeightRatio, size.y / (float)textureSize.y); target.draw(sprite); break; } default: // Ignore html in emoji.... break; } } else { sf::RectangleShape rect(size); rect.setFillColor(sf::Color::White); rect.setPosition(pos); target.draw(rect); } } else if(textElement.type == TextElement::Type::URL) { sf::Vector2f pos = position; pos.y += floor(textElement.position.y); float imageHeight = floor(vspace * IMAGE_HEIGHT_SCALE); // 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 ContentByUrlResult contentByUrlResult = cache.getContentByUrl(utf8Str); if(contentByUrlResult.type == ContentByUrlResult::Type::CACHED) { switch(contentByUrlResult.cachedType) { case ContentByUrlResult::CachedType::GIF: { auto gifSize = contentByUrlResult.gif->getSize(); float widthToHeightRatio = (float)gifSize.x / (float)gifSize.y; contentByUrlResult.gif->setPosition(pos); contentByUrlResult.gif->setScale(sf::Vector2f(imageHeight / (float)gifSize.x * widthToHeightRatio, imageHeight / (float)gifSize.y)); contentByUrlResult.gif->draw(target); break; } case ContentByUrlResult::CachedType::TEXTURE: { auto textureSize = contentByUrlResult.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(*contentByUrlResult.texture); sprite.setPosition(pos); sprite.setScale(imageHeight / (float)textureSize.x * widthToHeightRatio, imageHeight / (float)textureSize.y); target.draw(sprite); break; } case ContentByUrlResult::CachedType::WEB_PAGE_PREVIEW: { const float previewWidth = floor(imageHeight * 1.77f); // No need to perform culling here, that is done in @Text draw function contentByUrlResult.webPagePreview->title.setCharacterSize(characterSize); contentByUrlResult.webPagePreview->title.setMaxWidth(previewWidth); contentByUrlResult.webPagePreview->title.setPosition(pos); contentByUrlResult.webPagePreview->title.setLineSpacing(0.0f); contentByUrlResult.webPagePreview->title.setFillColor(URL_COLOR); contentByUrlResult.webPagePreview->title.draw(target, cache); break; } } } else { sf::RectangleShape rect(sf::Vector2f(imageHeight, imageHeight)); rect.setFillColor(sf::Color::White); rect.setPosition(pos); target.draw(rect); } } } } }