#include "../include/Text.hpp" #include "../include/ResourceLoader.hpp" #include "../include/Utils.hpp" #include "../generated/Emoji.hpp" #include #include #include #include #include #include #include namespace QuickMedia { static const float TAB_WIDTH = 4.0f; static const float WORD_WRAP_MIN_SIZE = 80.0f; static const sf::Color URL_COLOR(35, 140, 245); 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(bool bold_font) : Text("", bold_font, 0, 0.0f) {} Text::Text(sf::String _str, bool bold_font, unsigned int characterSize, float maxWidth, bool highlight_urls) : bold_font(bold_font), characterSize(characterSize), maxWidth(maxWidth), color(sf::Color::White), dirty(true), dirtyText(false), dirtyCaret(false), editable(false), highlight_urls(highlight_urls), caretMoveDirection(CaretMoveDirection::NONE), num_lines(1), lineSpacing(0.0f), characterSpacing(0.0f), caretIndex(0), caret_offset_x(0.0f) { vertices[0].setPrimitiveType(sf::PrimitiveType::Triangles); vertices[1].setPrimitiveType(sf::PrimitiveType::Triangles); vertices[2].setPrimitiveType(sf::PrimitiveType::Triangles); 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::appendText(sf::String str) { this->str += std::move(str); dirty = true; dirtyText = true; } 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; } // TODO: Instead of setting text to dirty, iterate vertices and change their positions void Text::setMaxWidth(float maxWidth) { if(std::abs(maxWidth - this->maxWidth) > 1.0f) { this->maxWidth = maxWidth; if(num_lines > 1 || maxWidth < boundingBox.width) { dirty = true; dirtyCaret = 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; } 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; dirtyCaret = true; if(editable) dirty = true; else vertices_linear.clear(); } } bool Text::isEditable() const { return editable; } void Text::moveCaretToEnd() { caretIndex = vertices_linear.size(); } 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 bool is_korean_codepoint(sf::Uint32 codepoint) { return codepoint >= 0xAC00 && codepoint <= 0xD7A3; } // TODO: Is there a more efficient way to do this? maybe chinese characters have a specific bit-pattern? static bool is_chinese_codepoint(sf::Uint32 codepoint) { return (codepoint >= 0x4E00 && codepoint <= 0x9FFF) // CJK Unified Ideographs || (codepoint >= 0x3400 && codepoint <= 0x4DBF) // CJK Unified Ideographs Extension A || (codepoint >= 0x20000 && codepoint <= 0x2A6DF) // CJK Unified Ideographs Extension B || (codepoint >= 0x2A700 && codepoint <= 0x2B73F) // CJK Unified Ideographs Extension C || (codepoint >= 0x2B740 && codepoint <= 0x2B81F) // CJK Unified Ideographs Extension D || (codepoint >= 0x2B820 && codepoint <= 0x2CEAF) // CJK Unified Ideographs Extension E || (codepoint >= 0xF900 && codepoint <= 0xFAFF) // CJK Compatibility Ideographs || (codepoint >= 0x2F800 && codepoint <= 0x2FA1F); // CJK Compatibility Ideographs Supplement } // TODO: Merge chinese, japanese and korean codepoints into one function since they share ranges static bool is_cjk_codepoint(sf::Uint32 codepoint) { return is_chinese_codepoint(codepoint) || is_japanese_codepoint(codepoint) || is_korean_codepoint(codepoint); } static size_t find_end_of_cjk(const sf::Uint32 *str, size_t size) { for(size_t i = 0; i < size; ++i) { if(!is_cjk_codepoint(str[i])) return i; } return size; } static size_t find_end_of_emoji(const sf::Uint32 *str, size_t size) { for(size_t i = 0; i < size; ++i) { if(!codepoint_is_emoji(str[i])) return i; } return size; } static size_t find_end_of_non_cjk_and_non_emoji(const sf::Uint32 *str, size_t size) { for(size_t i = 0; i < size; ++i) { if(is_cjk_codepoint(str[i]) || codepoint_is_emoji(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; TextElement::TextType text_type = TextElement::TextType::LATIN; if(is_cjk_codepoint(str[index])) { text_type = TextElement::TextType::CJK; offset = find_end_of_cjk(str.getData() + index + 1, size - index - 1); } else if(codepoint_is_emoji(str[index])) { text_type = TextElement::TextType::EMOJI; offset = find_end_of_emoji(str.getData() + index + 1, size - index - 1); } else { offset = find_end_of_non_cjk_and_non_emoji(str.getData() + index + 1, size - index - 1); } textElements.push_back({ StringViewUtf32(str.getData() + index, offset + 1), TextElement::Type::TEXT }); textElements.back().text_type = text_type; index += 1 + offset; } } float Text::get_text_quad_left_side(const VertexRef &vertex_ref) const { return vertices[vertex_ref.vertices_index][vertex_ref.index + 1].position.x; } float Text::get_text_quad_right_side(const VertexRef &vertex_ref) const { return vertices[vertex_ref.vertices_index][vertex_ref.index + 5].position.x; } float Text::get_caret_offset_by_caret_index(int index) const { const int num_vertices = vertices_linear.size(); if(num_vertices == 0) return 0.0f; else if(index < num_vertices) return get_text_quad_left_side(vertices_linear[index]); else return get_text_quad_right_side(vertices_linear[num_vertices - 1]); } VertexRef& Text::get_vertex_ref_clamp(int index) { const int num_vertices = vertices_linear.size(); assert(num_vertices > 0); if(index == num_vertices) return vertices_linear.back(); else return vertices_linear[index]; } int Text::get_vertex_line(int index) const { const int num_vertices = vertices_linear.size(); if(num_vertices == 0) { return 0; } else if(index < num_vertices) { return vertices_linear[index].line; } else { if(vertices_linear.back().codepoint == '\n') return vertices_linear.back().line + 1; else return vertices_linear.back().line; } } sf::Uint32 Text::get_vertex_codepoint(int index) const { const int num_vertices = vertices_linear.size(); if(num_vertices == 0) { return 0; } else if(index < num_vertices) { return vertices_linear[index].codepoint; } else { return vertices_linear.back().codepoint; } } void Text::updateGeometry(bool update_even_if_not_dirty) { if(dirtyText) { assert(dirty); dirtyText = false; splitTextByFont(); // TODO: Optimize if(highlight_urls) { auto u8 = str.toUtf8(); std::string *u8_str = (std::string*)&u8; url_ranges = extract_urls(*u8_str); convert_utf8_to_utf32_ranges(*u8_str, url_ranges); } else { url_ranges.clear(); } dirty = true; } if(!update_even_if_not_dirty && !dirty) return; dirty = false; vertices_linear.clear(); vertices[0].clear(); vertices[1].clear(); vertices[2].clear(); boundingBox = sf::FloatRect(); sf::Font *latin_font; if(bold_font) latin_font = FontLoader::get_font(FontLoader::FontType::LATIN_BOLD); else latin_font = FontLoader::get_font(FontLoader::FontType::LATIN); float latin_font_height = latin_font->getGlyph(' ', characterSize, false).advance; float hspace = latin_font_height + characterSpacing; float vspace = latin_font->getLineSpacing(characterSize); // TODO: What about japanese font??? size_t url_range_index = 0; sf::Vector2f glyphPos; sf::Uint32 prevCodePoint = 0; // TODO: Only do this if dirtyText for(usize textElementIndex = 0; textElementIndex < textElements.size(); ++textElementIndex) { TextElement &textElement = textElements[textElementIndex]; const sf::Font *ff = latin_font; int vertices_index = 0; if(textElement.text_type == TextElement::TextType::CJK) { ff = FontLoader::get_font(FontLoader::FontType::CJK); vertices_index = 1; } else if(textElement.text_type == TextElement::TextType::EMOJI) { vertices_index = 2; textElement.position = glyphPos; sf::Color emoji_color(255, 255, 255, color.a); for(size_t i = 0; i < textElement.text.size; ++i) { sf::Uint32 codePoint = textElement.text[i]; int vertexStart = vertices[vertices_index].getVertexCount(); EmojiRectangle emoji_rec = emoji_get_extents(codePoint); const float font_height_offset = -latin_font_height * 1.2f; sf::Vector2f vertexTopLeft(glyphPos.x, glyphPos.y + font_height_offset - std::floor(emoji_rec.height * get_ui_scale()) * 0.5f); sf::Vector2f vertexTopRight(glyphPos.x + std::floor(emoji_rec.width * get_ui_scale()), glyphPos.y + font_height_offset - std::floor(emoji_rec.height * get_ui_scale()) * 0.5f); sf::Vector2f vertexBottomLeft(glyphPos.x, glyphPos.y + font_height_offset + emoji_rec.height * get_ui_scale() * 0.5f); sf::Vector2f vertexBottomRight(glyphPos.x + std::floor(emoji_rec.width * get_ui_scale()), glyphPos.y + font_height_offset + std::floor(emoji_rec.height * get_ui_scale()) * 0.5f); sf::Vector2f textureTopLeft(emoji_rec.x, emoji_rec.y); sf::Vector2f textureTopRight(emoji_rec.x + emoji_rec.width, emoji_rec.y); sf::Vector2f textureBottomLeft(emoji_rec.x, emoji_rec.y + emoji_rec.height); sf::Vector2f textureBottomRight(emoji_rec.x + emoji_rec.width, emoji_rec.y + emoji_rec.height); vertices[vertices_index].append({ vertexTopRight, emoji_color, textureTopRight }); vertices[vertices_index].append({ vertexTopLeft, emoji_color, textureTopLeft }); vertices[vertices_index].append({ vertexBottomLeft, emoji_color, textureBottomLeft }); vertices[vertices_index].append({ vertexBottomLeft, emoji_color, textureBottomLeft }); vertices[vertices_index].append({ vertexBottomRight, emoji_color, textureBottomRight }); vertices[vertices_index].append({ vertexTopRight, emoji_color, textureTopRight }); glyphPos.x += std::floor(emoji_rec.width * get_ui_scale()) + characterSpacing; vertices_linear.push_back({vertices_index, vertexStart, 0, codePoint}); } continue; } //vertices[vertices_index].resize(vertices[vertices_index].getVertexCount() + 4 * textElement.text.size); // TODO: Precalculate textElement.position = glyphPos; for(size_t i = 0; i < textElement.text.size; ++i) { sf::Color text_color = color; if(url_range_index < url_ranges.size()) { size_t string_offset = (textElement.text.data + i) - str.getData(); if(string_offset >= url_ranges[url_range_index].start && string_offset < url_ranges[url_range_index].start + url_ranges[url_range_index].length) { text_color = URL_COLOR; text_color.a = color.a; if(string_offset + 1 == url_ranges[url_range_index].start + url_ranges[url_range_index].length) ++url_range_index; } } sf::Uint32 codePoint = textElement.text[i]; // TODO: Make this work when combining multiple different fonts (for example latin and japanese). // For japanese we could use a hack, because all japanese characters are monospace (exception being half-width characters). float kerning = ff->getKerning(prevCodePoint, codePoint, characterSize); prevCodePoint = codePoint; glyphPos.x += kerning; int vertexStart = vertices[vertices_index].getVertexCount(); switch(codePoint) { case ' ': { sf::Vector2f vertexTopLeft(glyphPos.x, glyphPos.y - vspace); sf::Vector2f vertexTopRight(glyphPos.x + hspace, glyphPos.y - vspace); sf::Vector2f vertexBottomLeft(glyphPos.x, glyphPos.y); sf::Vector2f vertexBottomRight(glyphPos.x + hspace, glyphPos.y); vertices[vertices_index].append({ vertexTopRight, sf::Color::Transparent, sf::Vector2f() }); vertices[vertices_index].append({ vertexTopLeft, sf::Color::Transparent, sf::Vector2f() }); vertices[vertices_index].append({ vertexBottomLeft, sf::Color::Transparent, sf::Vector2f() }); vertices[vertices_index].append({ vertexBottomLeft, sf::Color::Transparent, sf::Vector2f() }); vertices[vertices_index].append({ vertexBottomRight, sf::Color::Transparent, sf::Vector2f() }); vertices[vertices_index].append({ vertexTopRight, sf::Color::Transparent, sf::Vector2f() }); glyphPos.x += hspace; vertices_linear.push_back({vertices_index, vertexStart, 0, codePoint}); continue; } case '\t': { const float char_width = hspace * TAB_WIDTH; sf::Vector2f vertexTopLeft(glyphPos.x, glyphPos.y - vspace); sf::Vector2f vertexTopRight(glyphPos.x + char_width, glyphPos.y - vspace); sf::Vector2f vertexBottomLeft(glyphPos.x, glyphPos.y); sf::Vector2f vertexBottomRight(glyphPos.x + char_width, glyphPos.y); vertices[vertices_index].append({ vertexTopRight, sf::Color::Transparent, sf::Vector2f() }); vertices[vertices_index].append({ vertexTopLeft, sf::Color::Transparent, sf::Vector2f() }); vertices[vertices_index].append({ vertexBottomLeft, sf::Color::Transparent, sf::Vector2f() }); vertices[vertices_index].append({ vertexBottomLeft, sf::Color::Transparent, sf::Vector2f() }); vertices[vertices_index].append({ vertexBottomRight, sf::Color::Transparent, sf::Vector2f() }); vertices[vertices_index].append({ vertexTopRight, sf::Color::Transparent, sf::Vector2f() }); glyphPos.x += char_width; vertices_linear.push_back({vertices_index, vertexStart, 0, codePoint}); continue; } case '\n': { sf::Vector2f vertexTopLeft(glyphPos.x, glyphPos.y - vspace); sf::Vector2f vertexTopRight(glyphPos.x, glyphPos.y - vspace); sf::Vector2f vertexBottomLeft(glyphPos.x, glyphPos.y); sf::Vector2f vertexBottomRight(glyphPos.x, glyphPos.y); vertices[vertices_index].append({ vertexTopRight, sf::Color::Transparent, sf::Vector2f() }); vertices[vertices_index].append({ vertexTopLeft, sf::Color::Transparent, sf::Vector2f() }); vertices[vertices_index].append({ vertexBottomLeft, sf::Color::Transparent, sf::Vector2f() }); vertices[vertices_index].append({ vertexBottomLeft, sf::Color::Transparent, sf::Vector2f() }); vertices[vertices_index].append({ vertexBottomRight, sf::Color::Transparent, sf::Vector2f() }); vertices[vertices_index].append({ vertexTopRight, sf::Color::Transparent, sf::Vector2f() }); glyphPos.x = 0.0f; glyphPos.y += floor(vspace + lineSpacing); vertices_linear.push_back({vertices_index, vertexStart, 0, codePoint}); continue; } } const sf::Glyph &glyph = ff->getGlyph(codePoint, characterSize, false); 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); vertices[vertices_index].append({ vertexTopRight, text_color, textureTopRight }); vertices[vertices_index].append({ vertexTopLeft, text_color, textureTopLeft }); vertices[vertices_index].append({ vertexBottomLeft, text_color, textureBottomLeft }); vertices[vertices_index].append({ vertexBottomLeft, text_color, textureBottomLeft }); vertices[vertices_index].append({ vertexBottomRight, text_color, textureBottomRight }); vertices[vertices_index].append({ vertexTopRight, text_color, textureTopRight }); glyphPos.x += glyph.advance + characterSpacing; vertices_linear.push_back({vertices_index, vertexStart, 0, codePoint}); } //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; } const float line_height = floor(vspace + lineSpacing); float text_wrap_offset = 0.0f; float text_offset_y = 0.0f; int last_space_index = -1; num_lines = 1; // TODO: Binary search? for(int i = 0; i < (int)vertices_linear.size(); ++i) { VertexRef &vertex_ref = vertices_linear[i]; sf::Vertex *vertex = &vertices[vertex_ref.vertices_index][vertex_ref.index]; for(int v = 0; v < 6; ++v) { vertex[v].position.x -= text_wrap_offset; vertex[v].position.y += text_offset_y; } vertex_ref.line = num_lines - 1; switch(vertex_ref.codepoint) { case ' ': case '\t': last_space_index = i; break; case '\n': text_wrap_offset = 0.0f; last_space_index = -1; ++num_lines; break; default: break; } float vertex_right_side = get_text_quad_right_side(vertex_ref); if(vertex_right_side > maxWidth && maxWidth > WORD_WRAP_MIN_SIZE) { ++num_lines; // TODO: Ignore line wrap on space if(last_space_index != -1 && last_space_index != i) { float vertex_left_side = get_text_quad_left_side(vertices_linear[last_space_index + 1]); for(int j = last_space_index + 1; j <= i; ++j) { VertexRef &vertex_ref_wrap = vertices_linear[j]; sf::Vertex *vertex = &vertices[vertex_ref_wrap.vertices_index][vertex_ref_wrap.index]; for(int v = 0; v < 6; ++v) { vertex[v].position.x -= vertex_left_side; vertex[v].position.y += line_height; } vertex_ref_wrap.line = num_lines - 1; } last_space_index = -1; text_wrap_offset += vertex_left_side; } else { float vertex_left_side = get_text_quad_left_side(vertex_ref); for(int v = 0; v < 6; ++v) { vertex[v].position.x -= vertex_left_side; vertex[v].position.y += line_height; } text_wrap_offset += vertex_left_side; vertex_ref.line = num_lines - 1; } text_offset_y += line_height; } } boundingBox.width = 0.0f; for(VertexRef &vertex_ref : vertices_linear) { boundingBox.width = std::max(boundingBox.width, get_text_quad_right_side(vertex_ref)); } boundingBox.height = num_lines * line_height; //url_ranges.clear(); if(!editable) vertices_linear.clear(); } void Text::updateCaret() { assert(!dirty && !dirtyText); sf::Font *latin_font; if(bold_font) latin_font = FontLoader::get_font(FontLoader::FontType::LATIN_BOLD); else latin_font = FontLoader::get_font(FontLoader::FontType::LATIN); if(vertices_linear.empty()) { caretIndex = 0; caretPosition = sf::Vector2f(0.0f, floor(latin_font->getLineSpacing(characterSize))); caret_offset_x = 0.0f; return; } switch(caretMoveDirection) { case CaretMoveDirection::NONE: { caret_offset_x = get_caret_offset_by_caret_index(caretIndex); break; } case CaretMoveDirection::UP: { caretIndex = getPreviousLineClosestPosition(caretIndex); break; } case CaretMoveDirection::DOWN: { caretIndex = getNextLineClosestPosition(caretIndex); break; } case CaretMoveDirection::HOME: { caretIndex = getStartOfLine(caretIndex); caret_offset_x = get_caret_offset_by_caret_index(caretIndex); break; } case CaretMoveDirection::END: { caretIndex = getEndOfLine(caretIndex); caret_offset_x = get_caret_offset_by_caret_index(caretIndex); break; } case CaretMoveDirection::LEFT: { caretIndex = std::max(0, caretIndex - 1); caret_offset_x = get_caret_offset_by_caret_index(caretIndex); break; } case CaretMoveDirection::RIGHT: { caretIndex = std::min((int)vertices_linear.size(), caretIndex + 1); caret_offset_x = get_caret_offset_by_caret_index(caretIndex); break; } case CaretMoveDirection::LEFT_WORD: { caretIndex = getStartOfWord(caretIndex); caret_offset_x = get_caret_offset_by_caret_index(caretIndex); break; } case CaretMoveDirection::RIGHT_WORD: { caretIndex = getEndOfWord(caretIndex); caret_offset_x = get_caret_offset_by_caret_index(caretIndex); break; } } if(caretIndex == (int)vertices_linear.size()) { VertexRef &last_vertex = get_vertex_ref_clamp(caretIndex); if(last_vertex.codepoint == '\n') caretPosition.x = 0.0f; else caretPosition.x = get_text_quad_right_side(last_vertex); caretPosition.y = (1 + get_vertex_line(caretIndex)) * floor(latin_font->getLineSpacing(characterSize) + lineSpacing); } else { caretPosition.x = get_caret_offset_by_caret_index(caretIndex); caretPosition.y = (1 + get_vertex_line(caretIndex)) * floor(latin_font->getLineSpacing(characterSize) + lineSpacing); } } // TODO: This can be optimized by using binary search int Text::getStartOfLine(int startIndex) const { assert(!dirty && !dirtyText); int start_line = get_vertex_line(startIndex); for(int i = startIndex - 1; i >= 0; --i) { if(get_vertex_line(i) != start_line) { return i + 1; } } return 0; } // TODO: This can be optimized by using binary search int Text::getEndOfLine(int startIndex) const { assert(!dirty && !dirtyText); const int num_vertices = vertices_linear.size(); int start_line = get_vertex_line(startIndex); for(int i = startIndex + 1; i < num_vertices; ++i) { if(get_vertex_line(i) != start_line) { return i - 1; } } return num_vertices; } static bool is_special_character(sf::Uint32 codepoint) { return (codepoint <= 47) || (codepoint >= 58 && codepoint <= 64) || (codepoint >= 91 && codepoint <= 96) || (codepoint >= 123 && codepoint <= 127); } int Text::getStartOfWord(int startIndex) const { assert(!dirty && !dirtyText); bool start_is_special_char = is_special_character(get_vertex_codepoint(startIndex - 1)); for(int i = startIndex - 1; i >= 0; --i) { bool is_special_char = is_special_character(vertices_linear[i].codepoint); if(is_special_char != start_is_special_char) return i + 1; } return 0; } int Text::getEndOfWord(int startIndex) const { assert(!dirty && !dirtyText); const int num_vertices = vertices_linear.size(); bool start_is_special_char = is_special_character(get_vertex_codepoint(startIndex)); for(int i = startIndex + 1; i < num_vertices; ++i) { bool is_special_char = is_special_character(vertices_linear[i].codepoint); if(is_special_char != start_is_special_char) return i; } return num_vertices; } // TODO: This can be optimized by using binary search int Text::getPreviousLineClosestPosition(int startIndex) const { assert(!dirty && !dirtyText); const int start_line = get_vertex_line(startIndex); float closest_char = 999999.9f; int closest_index = -1; for(int i = getStartOfLine(startIndex) - 1; i >= 0 && get_vertex_line(i) == start_line - 1; --i) { const float left_pos = get_text_quad_left_side(vertices_linear[i]); const float pos_diff = std::abs(caret_offset_x - left_pos); if(pos_diff < closest_char) { closest_char = pos_diff; closest_index = i; } } if(closest_index != -1) return closest_index; return startIndex; } // TODO: This can be optimized by using binary search int Text::getNextLineClosestPosition(int startIndex) const { assert(!dirty && !dirtyText); const int num_vertices = vertices_linear.size(); const int start_line = get_vertex_line(startIndex); float closest_char = 999999.9f; int closest_index = -1; for(int i = getEndOfLine(startIndex) + 1; i < num_vertices && get_vertex_line(i) == start_line + 1; ++i) { const float left_pos = get_text_quad_left_side(vertices_linear[i]); const float pos_diff = std::abs(caret_offset_x - left_pos); if(pos_diff < closest_char) { closest_char = pos_diff; closest_index = i; } } if(closest_index != -1) return closest_index; return startIndex; } // TODO: Optimize text editing by only processing the changed parts in updateGeometry. // TODO: Split text into lines and add to vertices list so the lines that are cut off are not visible. This is good when using the text and as text input // where there are a max number of rows shown at a time. void Text::processEvent(const sf::Event &event) { if(!editable) return; bool caretAtEnd = caretIndex == (int)vertices_linear.size(); if(event.type == sf::Event::KeyPressed) { if(event.key.code == sf::Keyboard::Left && caretIndex > 0) { if(event.key.control) caretMoveDirection = CaretMoveDirection::LEFT_WORD; else caretMoveDirection = CaretMoveDirection::LEFT; dirtyCaret = true; } else if(event.key.code == sf::Keyboard::Right && !caretAtEnd) { if(event.key.control) caretMoveDirection = CaretMoveDirection::RIGHT_WORD; else caretMoveDirection = CaretMoveDirection::RIGHT; 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::Enter) { if(event.key.shift) { 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(editable && (dirtyCaret || caretMoveDirection != CaretMoveDirection::NONE)) { updateCaret(); dirtyCaret = false; caretMoveDirection = CaretMoveDirection::NONE; } sf::Vector2f pos = position; FontLoader::FontType latin_font_type; if(bold_font) latin_font_type = FontLoader::FontType::LATIN_BOLD; else latin_font_type = FontLoader::FontType::LATIN; sf::Font *latin_font = FontLoader::get_font(latin_font_type); const float vspace = latin_font->getLineSpacing(characterSize); pos.y += floor(vspace); // Origin is at bottom left, we want it to be at top left const FontLoader::FontType font_types[] = { latin_font_type, FontLoader::FontType::CJK }; for(size_t i = 0; i < 2; ++i) { if(vertices[i].getVertexCount() == 0) continue; sf::Font *font = FontLoader::get_font(font_types[i]); sf::RenderStates states; states.transform.translate(pos); states.texture = &font->getTexture(characterSize); target.draw(vertices[i], states); } if(vertices[2].getVertexCount() > 0) { sf::RenderStates states; states.transform.translate(pos); states.texture = TextureLoader::get_texture("images/emoji.png"); target.draw(vertices[2], states); } if(!editable) return true; pos.y -= floor(vspace * 2.0f); const float caret_margin = std::floor(2.0f * get_ui_scale()); sf::RectangleShape caretRect(sf::Vector2f(std::floor(2.0f * get_ui_scale()), floor(vspace - caret_margin * 2.0f))); caretRect.setPosition(floor(pos.x + caretPosition.x), floor(pos.y + caretPosition.y + caret_margin + std::floor(4.0f * get_ui_scale()))); target.draw(caretRect); return true; } }