From 795cc3d873df13bfe2abaa56b17ea247bc892c20 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 20 Jul 2020 15:06:37 +0200 Subject: Word-wrap body text --- src/Text.cpp | 663 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 663 insertions(+) create mode 100644 src/Text.cpp (limited to 'src/Text.cpp') diff --git a/src/Text.cpp b/src/Text.cpp new file mode 100644 index 0000000..da21b1d --- /dev/null +++ b/src/Text.cpp @@ -0,0 +1,663 @@ +#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(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(!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 != -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 == 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 == 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) + { + if(dirtyText) + { + textElements.clear(); + StringViewUtf32 wholeStr(this->str.getData(), this->str.getSize()); + textElements.push_back({ wholeStr, TextElement::Type::TEXT }); + dirtyText = false; + } + + 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; + } +} -- cgit v1.2.3