#include "../include/Text.hpp" #include #include #include #include namespace QuickMedia { const float TAB_WIDTH = 4.0f; const sf::Color URL_COLOR(15, 192, 252); size_t StringViewUtf32::find(const StringViewUtf32 &other, size_t offset) const { if(offset >= size) return -1; auto it = std::search(data + offset, data + size - offset, std::boyer_moore_searcher(other.data, other.data + other.size)); if(it != data + size) return it - data; return -1; } 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((int)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(std::abs(maxWidth - this->maxWidth) > 1.0f) { 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; } // Logic loosely based on https://github.com/SFML/SFML/wiki/Source:-CurvedText void Text::updateGeometry(bool update_even_if_not_dirty) { if(dirtyText) { textElements.clear(); StringViewUtf32 wholeStr(this->str.getData(), this->str.getSize()); textElements.push_back({ wholeStr, TextElement::Type::TEXT }); dirtyText = false; } if(!update_even_if_not_dirty && !dirty) return; 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; for(usize textElementIndex = 0; textElementIndex < textElements.size(); ++textElementIndex) { TextElement &textElement = textElements[textElementIndex]; 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; 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 != (size_t)-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; 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; } boundingBox.height = glyphPos.y + lineSpacing; 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); } dirty = false; } 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 == (int)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::processEvent(const sf::Event &event) { if(!editable) return; bool caretAtEnd = textElements.size() == 0 || textElements[0].text.size == 0 || caretIndex == (int)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) { updateGeometry(); 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(true); states.transform.translate(pos); states.texture = &font->getTexture(characterSize); target.draw(vertices, states); lastSeenTimer.restart(); visible = true; pos.y -= floor(vspace); 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; } }