#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, const sf::Font *_cjk_font) : Text("", _font, _cjk_font, 0, 0.0f, false) {} Text::Text(sf::String _str, const sf::Font *_font, const sf::Font *_cjk_font, unsigned int _characterSize, float _maxWidth, bool _plainText) : font(_font), cjk_font(_cjk_font), characterSize(_characterSize), 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) { vertices[0].setPrimitiveType(sf::PrimitiveType::Quads); vertices[1].setPrimitiveType(sf::PrimitiveType::Quads); setString(std::move(_str)); } void Text::setString(sf::String str) { if(str != this->str) { this->str = std::move(str); dirty = true; dirtyText = true; if((int)this->str.getSize() < caretIndex) { caretIndex = this->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::getWidth() const { return boundingBox.width; } float Text::getHeight() const { return boundingBox.height; } // TODO: Is there a more efficient way to do this? maybe japanese characters have a specific bit-pattern? static bool is_japanese_codepoint(sf::Uint32 codepoint) { return (codepoint >= 0x2E80 && codepoint <= 0x2FD5) // Kanji radicals || (codepoint >= 0x3000 && codepoint <= 0x303F) // Punctuation || (codepoint >= 0x3041 && codepoint <= 0x3096) // Hiragana || (codepoint >= 0x30A0 && codepoint <= 0x30FF) // Katakana || (codepoint >= 0x31F0 && codepoint <= 0x31FF) // Miscellaneous symbols and characters 1 || (codepoint >= 0x3220 && codepoint <= 0x3243) // Miscellaneous symbols and characters 2 || (codepoint >= 0x3280 && codepoint <= 0x337F) // Miscellaneous symbols and characters 3 || (codepoint >= 0x3400 && codepoint <= 0x4DB5) // Kanji 1 || (codepoint >= 0x4E00 && codepoint <= 0x9FCB) // Kanji 2 || (codepoint >= 0xF900 && codepoint <= 0xFA6A) // Kanji 3 || (codepoint >= 0xFF01 && codepoint <= 0xFF5E) // Alphanumeric and punctuation (full width) || (codepoint >= 0xFF5F && codepoint <= 0xFF9F); // Katakana and punctuation (half width) } static size_t find_end_of_japanese(const sf::Uint32 *str, size_t size) { for(size_t i = 0; i < size; ++i) { if(!is_japanese_codepoint(str[i])) return i; } return size; } static size_t find_end_of_non_japanese(const sf::Uint32 *str, size_t size) { for(size_t i = 0; i < size; ++i) { if(is_japanese_codepoint(str[i])) return i; } return size; } void Text::splitTextByFont() { textElements.clear(); size_t index = 0; size_t size = str.getSize(); while(index < size) { size_t offset; bool is_japanese = is_japanese_codepoint(str[index]); if(is_japanese) offset = find_end_of_japanese(str.getData() + index + 1, size - index - 1); else offset = find_end_of_non_japanese(str.getData() + index + 1, size - index - 1); textElements.push_back({ StringViewUtf32(str.getData() + index, offset + 1), TextElement::Type::TEXT }); textElements.back().is_japanese = is_japanese; index += 1 + offset; } } // Logic loosely based on https://github.com/SFML/SFML/wiki/Source:-CurvedText void Text::updateGeometry(bool update_even_if_not_dirty) { if(dirtyText) { dirtyText = false; splitTextByFont(); } if(!update_even_if_not_dirty && !dirty) return; vertices[0].clear(); vertices[1].clear(); float hspace = font->getGlyph(' ', characterSize, false).advance + characterSpacing; float vspace = font->getLineSpacing(characterSize); boundingBox = sf::FloatRect(); sf::Vector2f glyphPos; sf::Uint32 prevCodePoint = 0; for(usize textElementIndex = 0; textElementIndex < textElements.size(); ++textElementIndex) { size_t lastSpacingWordWrapIndex = -1; float lastSpacingAccumulatedOffset = 0.0f; TextElement &textElement = textElements[textElementIndex]; const sf::Font *ff = font; size_t vertices_index = 0; if(textElement.is_japanese) { ff = cjk_font; vertices_index = 1; } usize vertexOffset = vertices[vertices_index].getVertexCount(); vertices[vertices_index].resize(vertices[vertices_index].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 = ff->getKerning(prevCodePoint, codePoint, characterSize); prevCodePoint = codePoint; glyphPos.x += kerning; usize vertexStart = vertexOffset + i * 4; switch(codePoint) { case ' ': { vertices[vertices_index][vertexStart + 0] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices_index][vertexStart + 1] = { sf::Vector2f(glyphPos.x + hspace, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices_index][vertexStart + 2] = { sf::Vector2f(glyphPos.x + hspace, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices_index][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[vertices_index][vertexStart + 0] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices_index][vertexStart + 1] = { sf::Vector2f(glyphPos.x + hspace * TAB_WIDTH, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices_index][vertexStart + 2] = { sf::Vector2f(glyphPos.x + hspace * TAB_WIDTH, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices_index][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[vertices_index][vertexStart + 0] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices_index][vertexStart + 1] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices_index][vertexStart + 2] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices_index][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 = ff->getGlyph(codePoint, characterSize, false); // TODO: Fix wrap-around with multiple textElements. Right now it only wrap-arounds within the same textElement, so with mixed latin-japanese it will // wrap at character size rather than at whitespace 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[vertices_index][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[vertices_index][vertexStart + 0] = { vertexTopLeft, fontColor, textureTopLeft }; vertices[vertices_index][vertexStart + 1] = { vertexTopRight, fontColor, textureTopRight }; vertices[vertices_index][vertexStart + 2] = { vertexBottomRight, fontColor, textureBottomRight }; vertices[vertices_index][vertexStart + 3] = { vertexBottomLeft, fontColor, textureBottomLeft }; glyphPos.x += glyph.advance + characterSpacing; } vertices[vertices_index][vertices[vertices_index].getVertexCount() - 4] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices_index][vertices[vertices_index].getVertexCount() - 3] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices_index][vertices[vertices_index].getVertexCount() - 2] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices_index][vertices[vertices_index].getVertexCount() - 1] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; prevCodePoint = 0; } boundingBox.height = glyphPos.y + lineSpacing; boundingBox.height += vspace; for(size_t vertices_index = 0; vertices_index < 2; ++vertices_index) { usize numVertices = vertices[vertices_index].getVertexCount(); for(usize i = 0; i < numVertices; i += 4) { const sf::Vertex &bottomRight = vertices[vertices_index][i + 2]; boundingBox.width = std::max(boundingBox.width, bottomRight.position.x); } } dirty = false; } #if 0 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; } } #endif bool Text::draw(sf::RenderTarget &target) { updateGeometry(); #if 0 if(dirtyCaret || caretMoveDirection != CaretMoveDirection::NONE) { updateCaret(); dirtyCaret = false; caretMoveDirection = CaretMoveDirection::NONE; } #endif 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 // 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[0].resize(0); vertices[1].resize(0); } return false; } if(!visible) { visible = true; updateGeometry(true); } const sf::Font *fonts[] = { font, cjk_font }; for(size_t i = 0; i < 2; ++i) { sf::RenderStates states; states.transform.translate(pos); states.texture = &fonts[i]->getTexture(characterSize); target.draw(vertices[i], states); } lastSeenTimer.restart(); pos.y -= floor(vspace); #if 0 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; #else return true; #endif } }