#include "../include/Text.hpp" #include "../include/ColorScheme.hpp" #include "../include/ImagePreview.hpp" #include "../include/StaticImage.hpp" #include "../include/Gif.hpp" #include "../include/WebPagePreview.hpp" #include "../include/ResourceCache.hpp" #include #include #include #include #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), vertices(sf::PrimitiveType::Quads), maxWidth(0.0f), color(sf::Color::White), urlColor(URL_COLOR), dirty(false), dirtyText(false), dirtyCaret(false), plainText(false), editable(false), visible(true), caretMoveDirection(CaretMoveDirection::NONE), lineSpacing(0.0f), characterSpacing(0.0f), caretIndex(0) { } 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), dirtyText(false), dirtyCaret(false), plainText(_plainText), editable(false), visible(true), caretMoveDirection(CaretMoveDirection::NONE), lineSpacing(0.0f), characterSpacing(0.0f), caretIndex(0) { setString(_str); } void Text::setString(const sf::String &str) { if(str != this->str) { this->str = str; dirty = true; dirtyText = true; if(str.getSize() < caretIndex) { caretIndex = str.getSize(); dirtyCaret = true; } } } const sf::String& Text::getString() const { return str; } void Text::setPosition(float x, float y) { position.x = x; position.y = y; } void Text::setPosition(const sf::Vector2f &position) { this->position = position; } sf::Vector2f Text::getPosition() const { return position; } void Text::setMaxWidth(float maxWidth) { if(maxWidth != this->maxWidth) { this->maxWidth = maxWidth; dirty = true; } } float Text::getMaxWidth() const { return maxWidth; } void Text::setCharacterSize(unsigned int characterSize) { if(characterSize != this->characterSize) { this->characterSize = characterSize; dirty = true; } } unsigned int Text::getCharacterSize() const { return characterSize; } const sf::Font* Text::getFont() const { return font; } 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; } } void Text::setCharacterSpacing(float characterSpacing) { if(fabs(characterSpacing - this->characterSpacing) > 0.001f) { this->characterSpacing = characterSpacing; dirty = true; } } void Text::setEditable(bool editable) { if(editable != this->editable) { this->editable = editable; if(!plainText) { dirty = true; dirtyText = true; } dirtyCaret = true; } } float Text::getHeight() const { return boundingBox.height; } 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) { bool foundDot = false; offset = urlStart + urlStr.getSize(); while(offset < textElementStr.size) { if(isspace(textElementStr[offset])) break; else if(textElementStr[offset] == '.') foundDot = true; ++offset; } if(!foundDot) return -1; 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 || editable) 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; // Split emoji for(const TextElement &textElement : textElements) { 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; // Split http newTextElements.clear(); for(const TextElement &textElement : textElements) { 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; // Split https newTextElements.clear(); for(const TextElement &textElement : textElements) { 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 + characterSpacing; float vspace = font->getLineSpacing(characterSize); boundingBox = sf::FloatRect(); sf::Vector2f glyphPos; sf::Uint32 prevCodePoint = 0; size_t lastSpacingWordWrapIndex = -1; float lastSpacingAccumulatedOffset = 0.0f; bool lineHasEmoji = false; 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 += floor(emojiSize - vspace) + lineSpacing; } else { textElement.position.y = glyphPos.y + vspace * 0.5f - emojiSize * 0.5f; lineHasEmoji = true; } glyphPos.x += emojiSize + EMOJI_PADDING + characterSpacing; if(glyphPos.x > maxWidth) { glyphPos.x = 0.0f; glyphPos.y += floor(vspace + lineSpacing); } boundingBox.width = std::max(boundingBox.width, glyphPos.x); continue; } usize vertexOffset = vertices.getVertexCount(); vertices.resize(vertices.getVertexCount() + 4 * (textElement.text.size + 1)); 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; usize vertexStart = vertexOffset + i * 4; switch(codePoint) { case ' ': { vertices[vertexStart + 0] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertexStart + 1] = { sf::Vector2f(glyphPos.x + hspace, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertexStart + 2] = { sf::Vector2f(glyphPos.x + hspace, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; vertices[vertexStart + 3] = { sf::Vector2f(glyphPos.x, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; glyphPos.x += hspace; if(glyphPos.x > maxWidth * 0.5f) { lastSpacingWordWrapIndex = i; lastSpacingAccumulatedOffset = glyphPos.x; } continue; } case '\t': { vertices[vertexStart + 0] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertexStart + 1] = { sf::Vector2f(glyphPos.x + hspace * TAB_WIDTH, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertexStart + 2] = { sf::Vector2f(glyphPos.x + hspace * TAB_WIDTH, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; vertices[vertexStart + 3] = { sf::Vector2f(glyphPos.x, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; glyphPos.x += (hspace * TAB_WIDTH); if(glyphPos.x > maxWidth * 0.5f) { lastSpacingWordWrapIndex = i; lastSpacingAccumulatedOffset = glyphPos.x; } continue; } case '\n': { vertices[vertexStart + 0] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertexStart + 1] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; vertices[vertexStart + 2] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; vertices[vertexStart + 3] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; glyphPos.x = 0.0f; if(lineHasEmoji) { lineHasEmoji = false; glyphPos.y += floor(vspace * EMOJI_SCALE_WITH_TEXT + lineSpacing); } else glyphPos.y += floor(vspace + 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 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 += floor(vspace + lineSpacing); } } glyphPos.x -= lastSpacingAccumulatedOffset; lastSpacingWordWrapIndex = -1; lastSpacingAccumulatedOffset = 0.0f; } else glyphPos.x = 0.0f; if(lineHasEmoji) { lineHasEmoji = false; glyphPos.y += floor(vspace * EMOJI_SCALE_WITH_TEXT + lineSpacing); } else glyphPos.y += floor(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[vertexStart + 0] = { vertexTopLeft, fontColor, textureTopLeft }; vertices[vertexStart + 1] = { vertexTopRight, fontColor, textureTopRight }; vertices[vertexStart + 2] = { vertexBottomRight, fontColor, textureBottomRight }; vertices[vertexStart + 3] = { vertexBottomLeft, fontColor, textureBottomLeft }; glyphPos.x += glyph.advance + characterSpacing; } vertices[vertices.getVertexCount() - 4] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices.getVertexCount() - 3] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices.getVertexCount() - 2] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices.getVertexCount() - 1] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; prevCodePoint = 0; if(textElement.type == TextElement::Type::URL) { glyphPos.y += floor(vspace + lineSpacing); textElement.position.y = glyphPos.y; glyphPos.x = 0.0f; glyphPos.y += floor(vspace * IMAGE_HEIGHT_SCALE) + lineSpacing; } } boundingBox.height = glyphPos.y + lineSpacing; if(lineHasEmoji) boundingBox.height += vspace * EMOJI_SCALE_WITH_TEXT; else boundingBox.height += vspace; usize numVertices = vertices.getVertexCount(); for(usize i = 0; i < numVertices; i += 4) { const sf::Vertex &bottomRight = vertices[i + 2]; boundingBox.width = std::max(boundingBox.width, bottomRight.position.x); } } void Text::updateCaret() { assert(!dirty && !dirtyText); if(textElements.size() == 0) { float vspace = font->getLineSpacing(characterSize); caretIndex = 0; caretPosition = sf::Vector2f(0.0f, -vspace); return; } switch(caretMoveDirection) { case CaretMoveDirection::UP: { caretIndex = getPreviousLineClosestPosition(caretIndex); break; } case CaretMoveDirection::DOWN: { caretIndex = getNextLineClosestPosition(caretIndex); break; } case CaretMoveDirection::HOME: { caretIndex = getStartOfLine(caretIndex); break; } case CaretMoveDirection::END: { caretIndex = getEndOfLine(caretIndex); break; } default: // Ignore... break; } caretIndex = std::min(std::max(0, caretIndex), (int)textElements[0].text.size); usize vertexIndex = caretIndex * 4; if(vertexIndex == 0) { float vspace = font->getLineSpacing(characterSize); caretPosition = sf::Vector2f(0.0f, -vspace); } else { const sf::Vertex &topLeftVertex = vertices[vertexIndex]; caretPosition = topLeftVertex.position; } } bool Text::isCaretAtEnd() const { assert(!dirty && !dirtyText); return textElements[0].text.size == 0 || caretIndex == textElements[0].text.size; } // TODO: This can be optimized by using binary search int Text::getStartOfLine(int startIndex) const { assert(!dirty && !dirtyText); int numVertices = vertices.getVertexCount(); if(numVertices < 4) return 0; usize vertexIndex = startIndex * 4; const sf::Vertex &startTopLeftVertex = vertices[vertexIndex]; int startRow = getRowByPosition(startTopLeftVertex.position); for(int i = startIndex * 4; i > 0; i -= 4) { const sf::Vertex &topLeftVertex = vertices[i]; int row = getRowByPosition(topLeftVertex.position); if(row != startRow) { return std::max(0, i / 4 + 1); } } return 0; } // TODO: This can be optimized by using binary search int Text::getEndOfLine(int startIndex) const { assert(!dirty && !dirtyText); int numVertices = vertices.getVertexCount(); if(numVertices < 4) return 0; usize vertexIndex = startIndex * 4; const sf::Vertex &startTopLeftVertex = vertices[vertexIndex]; int startRow = getRowByPosition(startTopLeftVertex.position); for(int i = startIndex * 4; i < numVertices; i += 4) { const sf::Vertex &topLeftVertex = vertices[i]; int row = getRowByPosition(topLeftVertex.position); if(row != startRow) { return std::max(0, i / 4 - 1); } } return numVertices / 4; } // TODO: This can be optimized by using binary search int Text::getPreviousLineClosestPosition(int startIndex) const { assert(!dirty && !dirtyText); int numVertices = vertices.getVertexCount(); if(numVertices < 4) return 0; usize vertexIndex = startIndex * 4; const sf::Vertex &startTopLeftVertex = vertices[vertexIndex]; int startRow = getRowByPosition(startTopLeftVertex.position); int closestIndex = -1; float closestAbsoluteDiffX = 0.0f; for(int i = startIndex * 4; i >= 0; i -= 4) { const sf::Vertex &topLeftVertex = vertices[i]; int row = getRowByPosition(topLeftVertex.position); float absoluteDiffX = fabs(topLeftVertex.position.x - startTopLeftVertex.position.x); int rowDiff = abs(row - startRow); if(rowDiff > 1) break; if(rowDiff == 1 && (closestIndex == -1 || absoluteDiffX < closestAbsoluteDiffX)) { closestIndex = i; closestAbsoluteDiffX = absoluteDiffX; } } if(closestIndex != -1) return closestIndex / 4; return 0; } // TODO: This can be optimized by using binary search int Text::getNextLineClosestPosition(int startIndex) const { assert(!dirty && !dirtyText); int numVertices = vertices.getVertexCount(); if(numVertices < 4) return 0; usize vertexIndex = startIndex * 4; const sf::Vertex &startTopLeftVertex = vertices[vertexIndex]; int startRow = getRowByPosition(startTopLeftVertex.position); int closestIndex = -1; float closestAbsoluteDiffX = 0.0f; for(int i = startIndex * 4; i < numVertices; i += 4) { const sf::Vertex &topLeftVertex = vertices[i]; int row = getRowByPosition(topLeftVertex.position); float absoluteDiffX = fabs(topLeftVertex.position.x - startTopLeftVertex.position.x); int rowDiff = abs(row - startRow); if(rowDiff > 1) break; if(rowDiff == 1 && (closestIndex == -1 || absoluteDiffX < closestAbsoluteDiffX)) { closestIndex = i; closestAbsoluteDiffX = absoluteDiffX; } } if(closestIndex != -1) return closestIndex / 4; return numVertices / 4; } int Text::getRowByPosition(const sf::Vector2f &position) const { assert(!dirty && !dirtyText); const float vspace = font->getLineSpacing(characterSize); return static_cast(1.0f + position.y / (vspace + lineSpacing)); } void Text::onMouseClick(const sf::Event::MouseButtonEvent &event, Cache *cache) { if(event.button != sf::Mouse::Button::Left) return; float vspace = font->getLineSpacing(characterSize); sf::Vector2f pos = position; pos.y += floor(vspace); // Origin is at bottom left, we want it to be at top left if(pos.y + getHeight() <= 0.0f || pos.y >= renderTargetSize.y) return; for(TextElement &textElement : textElements) { if(textElement.type != TextElement::Type::URL) continue; 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::STATIC_IMAGE: { auto *staticImage = static_cast(contentByUrlResult.staticImage); auto textureSize = staticImage->texture.getSize(); float widthToHeightRatio = (float)textureSize.x / (float)textureSize.y; float imageWidth = fmin(imageHeight * widthToHeightRatio, maxWidth); if(event.x >= pos.x && event.x <= pos.x + imageWidth && event.y >= pos.y && event.y <= pos.y + imageHeight) { ImagePreview::preview(&staticImage->texture, utf8Str); return; } break; } case ContentByUrlResult::CachedType::GIF: { auto textureSize = contentByUrlResult.gif->getSize(); float widthToHeightRatio = (float)textureSize.x / (float)textureSize.y; float imageWidth = fmin(imageHeight * widthToHeightRatio, maxWidth); if(event.x >= pos.x && event.x <= pos.x + imageWidth && event.y >= pos.y && event.y <= pos.y + imageHeight) { ImagePreview::preview(contentByUrlResult.gif, utf8Str); return; } break; } case ContentByUrlResult::CachedType::WEB_PAGE_PREVIEW: { const float previewWidth = fmin(maxWidth, floor(imageHeight * 1.77f)); if(event.x >= pos.x && event.x <= pos.x + previewWidth && event.y >= pos.y && event.y <= pos.y + imageHeight) { // TODO: Implement for other platforms than linux std::string cmd = "xdg-open "; cmd += escapeCommandArg(utf8Str); printf("Clicked on web page preview, opening web page by running command: %s\n", cmd.c_str()); system(cmd.c_str()); return; } break; } } } } } void Text::processEvent(const sf::Event &event, Cache *cache) { if(event.type == sf::Event::MouseButtonReleased) { onMouseClick(event.mouseButton, cache); } if(!editable) return; bool caretAtEnd = textElements.size() == 0 || textElements[0].text.size == 0 || caretIndex == textElements[0].text.size; if(event.type == sf::Event::KeyPressed) { if(event.key.code == sf::Keyboard::Left && caretIndex > 0) { --caretIndex; dirtyCaret = true; } else if(event.key.code == sf::Keyboard::Right && !caretAtEnd) { ++caretIndex; dirtyCaret = true; } else if(event.key.code == sf::Keyboard::BackSpace && caretIndex > 0) { auto strBefore = str.substring(0, caretIndex - 1); auto strAfter = str.substring(caretIndex); --caretIndex; setString(strBefore + strAfter); dirtyCaret = true; } else if(event.key.code == sf::Keyboard::Delete && !caretAtEnd) { auto strBefore = str.substring(0, caretIndex); auto strAfter = str.substring(caretIndex + 1); setString(strBefore + strAfter); } else if(event.key.code == sf::Keyboard::Up) { caretMoveDirection = CaretMoveDirection::UP; } else if(event.key.code == sf::Keyboard::Down) { caretMoveDirection = CaretMoveDirection::DOWN; } else if(event.key.code == sf::Keyboard::Home) { caretMoveDirection = CaretMoveDirection::HOME; } else if(event.key.code == sf::Keyboard::End) { caretMoveDirection = CaretMoveDirection::END; } else if(event.key.code == sf::Keyboard::Return) { if(sf::Keyboard::isKeyPressed(sf::Keyboard::LShift) || sf::Keyboard::isKeyPressed(sf::Keyboard::RShift)) { if(caretAtEnd) str += '\n'; else { auto strBefore = str.substring(0, caretIndex); auto strAfter = str.substring(caretIndex); str = strBefore + '\n' + strAfter; } ++caretIndex; dirty = true; dirtyText = true; dirtyCaret = true; } } } else if(event.type == sf::Event::TextEntered) { if(event.text.unicode == 8 || event.text.unicode == 127) // backspace, del return; sf::String stringToAdd; if(event.text.unicode == 22) // ctrl+v { stringToAdd = sf::Clipboard::getString(); } else if(event.text.unicode >= 32 || event.text.unicode == 9) // 9 == tab stringToAdd = event.text.unicode; else return; if(caretAtEnd) str += stringToAdd; else { auto strBefore = str.substring(0, caretIndex); auto strAfter = str.substring(caretIndex); str = strBefore + stringToAdd + strAfter; } caretIndex += stringToAdd.getSize(); dirty = true; dirtyText = true; dirtyCaret = true; } } bool Text::draw(sf::RenderTarget &target, Cache *cache) { if(dirtyText) { textElements.clear(); stringSplitElements(this->str, 0); dirtyText = false; } if(dirty) { updateGeometry(); dirty = false; } if(dirtyCaret || caretMoveDirection != CaretMoveDirection::NONE) { updateCaret(); dirtyCaret = false; caretMoveDirection = CaretMoveDirection::NONE; } 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; renderTargetSize = target.getSize(); if(pos.y + getHeight() <= 0.0f || pos.y >= renderTargetSize.y) { if(!editable && visible && lastSeenTimer.getElapsedTime().asMilliseconds() > 3000) { visible = false; vertices.resize(0); } return false; } if(!visible) updateGeometry(); states.transform.translate(pos); states.texture = &font->getTexture(characterSize); target.draw(vertices, states); lastSeenTimer.restart(); visible = true; pos.y -= floor(vspace); 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 *gif = static_cast(contentByUrlResult.gif); gif->update(); auto gifSize = gif->getSize(); float widthToHeightRatio = (float)gifSize.x / (float)gifSize.y; sf::Sprite sprite(gif->texture); sprite.setPosition(pos); sprite.setScale(sf::Vector2f(size.x / (float)gifSize.x * widthToHeightRatio, size.y / (float)gifSize.y)); sprite.setColor(sf::Color::White); target.draw(sprite); break; } case ContentByUrlResult::CachedType::STATIC_IMAGE: { auto *staticImage = static_cast(contentByUrlResult.staticImage); auto textureSize = staticImage->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(staticImage->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 *gif = static_cast(contentByUrlResult.gif); gif->update(); auto gifSize = gif->getSize(); float widthToHeightRatio = (float)gifSize.x / (float)gifSize.y; sf::Sprite sprite(gif->texture); sprite.setPosition(pos); sprite.setScale(sf::Vector2f(imageHeight / (float)gifSize.x * widthToHeightRatio, imageHeight / (float)gifSize.y)); sprite.setColor(sf::Color::White); target.draw(sprite); break; } case ContentByUrlResult::CachedType::STATIC_IMAGE: { auto *staticImage = static_cast(contentByUrlResult.staticImage); auto textureSize = staticImage->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(staticImage->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: { auto *webPagePreview = static_cast(contentByUrlResult.webPagePreview); const float previewWidth = fmin(maxWidth, floor(imageHeight * 1.77f)); webPagePreview->title.setCharacterSize(characterSize); webPagePreview->title.setMaxWidth(previewWidth); webPagePreview->title.setPosition(pos); webPagePreview->title.setLineSpacing(0.0f); webPagePreview->title.setFillColor(URL_COLOR); webPagePreview->title.draw(target, cache); pos.y += webPagePreview->title.getHeight(); webPagePreview->description.setCharacterSize(characterSize); webPagePreview->description.setMaxWidth(previewWidth); webPagePreview->description.setPosition(pos); webPagePreview->description.setLineSpacing(0.0f); webPagePreview->description.setFillColor(color); webPagePreview->description.draw(target, cache); break; } } } else { sf::RectangleShape rect(sf::Vector2f(imageHeight, imageHeight)); rect.setFillColor(sf::Color::White); rect.setPosition(pos); target.draw(rect); } } } if(!editable) return true; //float rows = floor(totalHeight / (vspace + lineSpacing)); const float caretRow = getRowByPosition(caretPosition); sf::RectangleShape caretRect(sf::Vector2f(2.0f, floor(vspace))); caretRect.setFillColor(sf::Color::White); caretRect.setPosition(sf::Vector2f(floor(pos.x + caretPosition.x), floor(pos.y + caretRow * (vspace + lineSpacing)))); target.draw(caretRect); return true; } }