#include "../include/Text.hpp" #include "../include/ResourceLoader.hpp" #include "../include/Config.hpp" #include "../include/Theme.hpp" #include "../generated/Emoji.hpp" #include #include #include #include #include #include namespace QuickMedia { static float floor(float v) { return (int)v; } static float fabs(float v) { return v >= 0.0 ? v : -v; } static const float TAB_WIDTH = 4.0f; static const float WORD_WRAP_MIN_SIZE = 80.0f; static const size_t FONT_INDEX_LATIN = 0; static const size_t FONT_INDEX_CJK = 1; static const size_t FONT_INDEX_SYMBOLS = 2; static const size_t FONT_INDEX_EMOJI = 3; static const size_t FONT_ARRAY_SIZE = 4; Text::Text() : bold_font(false), characterSize(12), maxWidth(0.0f), color(get_theme().text_color), dirty(true), dirtyText(true), dirtyCaret(true), editable(false), highlight_urls(false), caretMoveDirection(CaretMoveDirection::NONE), num_lines(1), lineSpacing(0.0f), characterSpacing(0.0f), caretIndex(0), caret_offset_x(0.0f) { } Text::Text(std::string _str, bool bold_font, unsigned int characterSize, float maxWidth, bool highlight_urls) : bold_font(bold_font), characterSize(characterSize), maxWidth(maxWidth), color(get_theme().text_color), dirty(true), dirtyText(true), dirtyCaret(true), 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) { setString(std::move(_str)); } Text::Text(const Text &other) : Text(other.str, other.bold_font, other.characterSize, other.maxWidth, other.highlight_urls) { } Text& Text::operator=(const Text &other) { str = other.str; bold_font = other.bold_font; characterSize = other.characterSize; maxWidth = other.maxWidth; highlight_urls = other.highlight_urls; caretIndex = other.caretIndex; position = other.position; return *this; } void Text::setString(std::string str) { //if(str != this->str) //{ size_t prev_str_size = this->str.size(); this->str = std::move(str); dirty = true; dirtyText = true; if((int)this->str.size() < caretIndex || prev_str_size == 0) { caretIndex = this->str.size(); dirtyCaret = true; } // } } const std::string& Text::getString() const { return str; } void Text::appendText(const std::string &str) { this->str += str; dirty = true; dirtyText = true; } void Text::insert_text_at_caret_position(const std::string &str) { this->str.insert(caretIndex, str); dirty = true; dirtyText = true; } void Text::set_position(float x, float y) { position.x = x; position.y = y; } void Text::set_position(const mgl::vec2f &position) { this->position = position; } mgl::vec2f Text::get_position() 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.size.x) { 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::get_character_size() const { return characterSize; } void Text::replace(size_t start_index, size_t length, const std::string &insert_str) { int string_diff = (int)insert_str.size() - (int)length; str.replace(start_index, length, insert_str); dirty = true; dirtyText = true; if(caretIndex >= (int)start_index) { caretIndex += string_diff; dirtyCaret = true; } } int Text::getCaretIndex() const { return caretIndex; } void Text::set_color(mgl::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.size.x; } float Text::getHeight() const { return boundingBox.size.y; } // TODO: Is there a more efficient way to do this? maybe japanese characters have a specific bit-pattern? static bool is_japanese_codepoint(uint32_t 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(uint32_t 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(uint32_t 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(uint32_t codepoint) { return is_chinese_codepoint(codepoint) || is_japanese_codepoint(codepoint) || is_korean_codepoint(codepoint); } static bool is_symbol_codepoint(uint32_t codepoint) { // TODO: Add the remaining NotoSansSymbols2-Regular.ttf codepoints as well. // See codepoint ranges with: fc-query --format='%{charset}\n' /usr/share/fonts/noto/NotoSansSymbols2-Regular.ttf. return codepoint >= 0x2800 && codepoint <= 0x28FF; // Braille } static size_t find_end_of_cjk(const char *str, size_t size) { for(size_t i = 0; i < size;) { const unsigned char *cp = (const unsigned char*)&str[i]; uint32_t codepoint; size_t clen; if(!mgl::utf8_decode(cp, size - i, &codepoint, &clen)) { codepoint = *cp; clen = 1; } if(!is_cjk_codepoint(codepoint)) return i; i += clen; } return size; } static size_t find_end_of_emoji(const char *str, size_t size) { for(size_t i = 0; i < size;) { const unsigned char *cp = (const unsigned char*)&str[i]; uint32_t codepoint; size_t clen; if(!mgl::utf8_decode(cp, size - i, &codepoint, &clen)) { codepoint = *cp; clen = 1; } if(!codepoint_is_emoji(codepoint)) return i; i += clen; } return size; } static size_t find_end_of_symbol(const char *str, size_t size) { for(size_t i = 0; i < size;) { const unsigned char *cp = (const unsigned char*)&str[i]; uint32_t codepoint; size_t clen; if(!mgl::utf8_decode(cp, size - i, &codepoint, &clen)) { codepoint = *cp; clen = 1; } if(!is_symbol_codepoint(codepoint)) return i; i += clen; } return size; } static size_t find_end_latin(const char *str, size_t size) { for(size_t i = 0; i < size;) { const unsigned char *cp = (const unsigned char*)&str[i]; uint32_t codepoint; size_t clen; if(!mgl::utf8_decode(cp, size - i, &codepoint, &clen)) { codepoint = *cp; clen = 1; } if(is_cjk_codepoint(codepoint) || codepoint_is_emoji(codepoint) || is_symbol_codepoint(codepoint)) return i; i += clen; } return size; } void Text::splitTextByFont() { textElements.clear(); size_t index = 0; size_t size = str.size(); while(index < size) { const unsigned char *cp = (const unsigned char*)&str[index]; uint32_t codepoint; size_t clen; if(!mgl::utf8_decode(cp, size - index, &codepoint, &clen)) { codepoint = *cp; clen = 1; } size_t offset; TextElement::TextType text_type = TextElement::TextType::LATIN; if(is_symbol_codepoint(codepoint)) { text_type = TextElement::TextType::SYMBOL; offset = find_end_of_symbol(str.data() + index, size - index); } else if(is_cjk_codepoint(codepoint)) { text_type = TextElement::TextType::CJK; offset = find_end_of_cjk(str.data() + index, size - index); } else if(codepoint_is_emoji(codepoint)) { text_type = TextElement::TextType::EMOJI; offset = find_end_of_emoji(str.data() + index, size - index); } else { offset = find_end_latin(str.data() + index, size - index); } textElements.push_back({ std::string_view(str.data() + index, offset), TextElement::Type::TEXT }); textElements.back().text_type = text_type; index += offset; } } float Text::font_get_real_height(mgl::Font *font) { return font->get_glyph('|').size.y + floor(4.0f * ((float)characterSize / (float)14.0f)); } 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; } } uint32_t 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; } } static mgl::vec2f vec2f_floor(mgl::vec2f value) { return mgl::vec2f((int)value.x, (int)value.y); } void Text::updateGeometry(bool update_even_if_not_dirty) { if(dirtyText) { assert(dirty); dirtyText = false; splitTextByFont(); // TODO: Optimize if(highlight_urls) { url_ranges = extract_urls(str); } else { url_ranges.clear(); } } if(!update_even_if_not_dirty && !dirty) return; dirty = false; vertices_linear.clear(); for(size_t i = 0; i < FONT_ARRAY_SIZE; ++i) { vertices[i].clear(); vertices[i].shrink_to_fit(); } boundingBox = mgl::FloatRect(mgl::vec2f(0.0f, 0.0f), mgl::vec2f(0.0f, 0.0f)); mgl::Font *latin_font; if(bold_font) latin_font = FontLoader::get_font(FontLoader::FontType::LATIN_BOLD, characterSize); else latin_font = FontLoader::get_font(FontLoader::FontType::LATIN, characterSize); const float latin_font_width = latin_font->get_glyph(' ').advance; const float hspace = latin_font_width + characterSpacing; const float vspace = font_get_real_height(latin_font); const float emoji_scale = vspace / 20.0f; const mgl::Color url_color = get_theme().url_text_color; size_t url_range_index = 0; mgl::vec2f glyphPos; uint32_t prevCodePoint = 0; // TODO: Only do this if dirtyText (then the Text object shouldn't be reset in Body. There should be a cleanup function in text instead) for(usize textElementIndex = 0; textElementIndex < textElements.size(); ++textElementIndex) { TextElement &textElement = textElements[textElementIndex]; mgl::Font *ff = latin_font; int vertices_index = FONT_INDEX_LATIN; prevCodePoint = 0; if(textElement.text_type == TextElement::TextType::CJK) { ff = FontLoader::get_font(FontLoader::FontType::CJK, characterSize); vertices_index = FONT_INDEX_CJK; } else if(textElement.text_type == TextElement::TextType::SYMBOL) { ff = FontLoader::get_font(FontLoader::FontType::SYMBOLS, characterSize); vertices_index = FONT_INDEX_SYMBOLS; } else if(textElement.text_type == TextElement::TextType::EMOJI) { vertices_index = FONT_INDEX_EMOJI; mgl::Color emoji_color(255, 255, 255, color.a); for(size_t i = 0; i < textElement.text.size();) { const unsigned char *cp = (const unsigned char*)&textElement.text[i]; uint32_t codepoint; size_t clen; if(!mgl::utf8_decode(cp, textElement.text.size() - i, &codepoint, &clen)) { codepoint = *cp; clen = 1; } int vertexStart = vertices[vertices_index].size(); EmojiRectangle emoji_rec = emoji_get_extents(codepoint); const float font_height_offset = floor(-vspace * 0.2f); mgl::vec2f vertexTopLeft(glyphPos.x, glyphPos.y + font_height_offset - floor(emoji_rec.height * emoji_scale) * 0.5f); mgl::vec2f vertexTopRight(glyphPos.x + floor(emoji_rec.width * emoji_scale), glyphPos.y + font_height_offset - floor(emoji_rec.height * emoji_scale) * 0.5f); mgl::vec2f vertexBottomLeft(glyphPos.x, glyphPos.y + font_height_offset + emoji_rec.height * emoji_scale * 0.5f); mgl::vec2f vertexBottomRight(glyphPos.x + floor(emoji_rec.width * emoji_scale), glyphPos.y + font_height_offset + floor(emoji_rec.height * emoji_scale) * 0.5f); vertexTopLeft = vec2f_floor(vertexTopLeft); vertexTopRight = vec2f_floor(vertexTopRight); vertexBottomLeft = vec2f_floor(vertexBottomLeft); vertexBottomRight = vec2f_floor(vertexBottomRight); mgl::vec2f textureTopLeft(emoji_rec.x, emoji_rec.y); mgl::vec2f textureTopRight(emoji_rec.x + emoji_rec.width, emoji_rec.y); mgl::vec2f textureBottomLeft(emoji_rec.x, emoji_rec.y + emoji_rec.height); mgl::vec2f textureBottomRight(emoji_rec.x + emoji_rec.width, emoji_rec.y + emoji_rec.height); vertices[vertices_index].emplace_back(vertexTopRight, textureTopRight, emoji_color); vertices[vertices_index].emplace_back(vertexTopLeft, textureTopLeft, emoji_color); vertices[vertices_index].emplace_back(vertexBottomLeft, textureBottomLeft, emoji_color); vertices[vertices_index].emplace_back(vertexBottomLeft, textureBottomLeft, emoji_color); vertices[vertices_index].emplace_back(vertexBottomRight, textureBottomRight, emoji_color); vertices[vertices_index].emplace_back(vertexTopRight, textureTopRight, emoji_color); glyphPos.x += floor(emoji_rec.width * emoji_scale) + characterSpacing; vertices_linear.push_back({vertices_index, vertexStart, 0, codepoint}); i += clen; } continue; } //vertices[vertices_index].resize(vertices[vertices_index].size() + 4 * textElement.text.size); // TODO: Precalculate for(size_t i = 0; i < textElement.text.size();) { mgl::Color text_color = color; if(url_range_index < url_ranges.size()) { size_t string_offset = (textElement.text.data() + i) - str.data(); 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; } } const unsigned char *cp = (const unsigned char*)&textElement.text[i]; uint32_t codepoint; size_t clen; if(!mgl::utf8_decode(cp, textElement.text.size() - i, &codepoint, &clen)) { codepoint = *cp; clen = 1; } // 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->get_kerning(prevCodePoint, codepoint); prevCodePoint = codepoint; glyphPos.x += kerning; int vertexStart = vertices[vertices_index].size(); i += clen; switch(codepoint) { case ' ': { mgl::vec2f vertexTopLeft(glyphPos.x, glyphPos.y - vspace); mgl::vec2f vertexTopRight(glyphPos.x + hspace, glyphPos.y - vspace); mgl::vec2f vertexBottomLeft(glyphPos.x, glyphPos.y); mgl::vec2f vertexBottomRight(glyphPos.x + hspace, glyphPos.y); vertexTopLeft = vec2f_floor(vertexTopLeft); vertexTopRight = vec2f_floor(vertexTopRight); vertexBottomLeft = vec2f_floor(vertexBottomLeft); vertexBottomRight = vec2f_floor(vertexBottomRight); vertices[vertices_index].emplace_back(vertexTopRight, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); vertices[vertices_index].emplace_back(vertexTopLeft, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); vertices[vertices_index].emplace_back(vertexBottomLeft, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); vertices[vertices_index].emplace_back(vertexBottomLeft, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); vertices[vertices_index].emplace_back(vertexBottomRight, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); vertices[vertices_index].emplace_back(vertexTopRight, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); glyphPos.x += hspace; vertices_linear.push_back({vertices_index, vertexStart, 0, codepoint}); break; } case '\t': { const float char_width = hspace * TAB_WIDTH; mgl::vec2f vertexTopLeft(glyphPos.x, glyphPos.y - vspace); mgl::vec2f vertexTopRight(glyphPos.x + char_width, glyphPos.y - vspace); mgl::vec2f vertexBottomLeft(glyphPos.x, glyphPos.y); mgl::vec2f vertexBottomRight(glyphPos.x + char_width, glyphPos.y); vertexTopLeft = vec2f_floor(vertexTopLeft); vertexTopRight = vec2f_floor(vertexTopRight); vertexBottomLeft = vec2f_floor(vertexBottomLeft); vertexBottomRight = vec2f_floor(vertexBottomRight); vertices[vertices_index].emplace_back(vertexTopRight, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); vertices[vertices_index].emplace_back(vertexTopLeft, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); vertices[vertices_index].emplace_back(vertexBottomLeft, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); vertices[vertices_index].emplace_back(vertexBottomLeft, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); vertices[vertices_index].emplace_back(vertexBottomRight, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); vertices[vertices_index].emplace_back(vertexTopRight, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); glyphPos.x += char_width; vertices_linear.push_back({vertices_index, vertexStart, 0, codepoint}); break; } case '\n': { mgl::vec2f vertexTopLeft(glyphPos.x, glyphPos.y - vspace); mgl::vec2f vertexTopRight(glyphPos.x, glyphPos.y - vspace); mgl::vec2f vertexBottomLeft(glyphPos.x, glyphPos.y); mgl::vec2f vertexBottomRight(glyphPos.x, glyphPos.y); vertexTopLeft = vec2f_floor(vertexTopLeft); vertexTopRight = vec2f_floor(vertexTopRight); vertexBottomLeft = vec2f_floor(vertexBottomLeft); vertexBottomRight = vec2f_floor(vertexBottomRight); vertices[vertices_index].emplace_back(vertexTopRight, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); vertices[vertices_index].emplace_back(vertexTopLeft, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); vertices[vertices_index].emplace_back(vertexBottomLeft, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); vertices[vertices_index].emplace_back(vertexBottomLeft, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); vertices[vertices_index].emplace_back(vertexBottomRight, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); vertices[vertices_index].emplace_back(vertexTopRight, mgl::vec2f(), mgl::Color(0, 0, 0, 0)); glyphPos.x = 0.0f; glyphPos.y += floor(vspace + lineSpacing); vertices_linear.push_back({vertices_index, vertexStart, 0, codepoint}); break; } default: { mgl::FontGlyph glyph = ff->get_glyph(codepoint); mgl::vec2f vertexTopLeft(glyphPos.x + glyph.position.x, glyphPos.y + glyph.position.y); mgl::vec2f vertexTopRight(glyphPos.x + glyph.position.x + glyph.size.x, glyphPos.y + glyph.position.y); mgl::vec2f vertexBottomLeft(glyphPos.x + glyph.position.x, glyphPos.y + glyph.position.y + glyph.size.y); mgl::vec2f vertexBottomRight(glyphPos.x + glyph.position.x + glyph.size.x, glyphPos.y + glyph.position.y + glyph.size.y); vertexTopLeft = vec2f_floor(vertexTopLeft); vertexTopRight = vec2f_floor(vertexTopRight); vertexBottomLeft = vec2f_floor(vertexBottomLeft); vertexBottomRight = vec2f_floor(vertexBottomRight); mgl::vec2f textureTopLeft(glyph.texture_position.x, glyph.texture_position.y); mgl::vec2f textureTopRight(glyph.texture_position.x + glyph.texture_size.x, glyph.texture_position.y); mgl::vec2f textureBottomLeft(glyph.texture_position.x, glyph.texture_position.y + glyph.texture_size.y); mgl::vec2f textureBottomRight(glyph.texture_position.x + glyph.texture_size.x, glyph.texture_position.y + glyph.texture_size.y); vertices[vertices_index].emplace_back(vertexTopRight, textureTopRight, text_color); vertices[vertices_index].emplace_back(vertexTopLeft, textureTopLeft, text_color); vertices[vertices_index].emplace_back(vertexBottomLeft, textureBottomLeft, text_color); vertices[vertices_index].emplace_back(vertexBottomLeft, textureBottomLeft, text_color); vertices[vertices_index].emplace_back(vertexBottomRight, textureBottomRight, text_color); vertices[vertices_index].emplace_back(vertexTopRight, textureTopRight, text_color); glyphPos.x += glyph.advance + characterSpacing; vertices_linear.push_back({vertices_index, vertexStart, 0, codepoint}); break; } } } } 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]; mgl::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]; mgl::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.size.x = 0.0f; for(VertexRef &vertex_ref : vertices_linear) { boundingBox.size.x = std::max(boundingBox.size.x, get_text_quad_right_side(vertex_ref)); } boundingBox.size.y = num_lines * line_height; // TODO: Clear |vertices| somehow even with editable text for(size_t i = 0; i < FONT_ARRAY_SIZE; ++i) { // TODO: Use VertexBuffer::Dynamic for editable text? vertex_buffers[i].update(vertices[i].data(), vertices[i].size(), mgl::PrimitiveType::Triangles, mgl::VertexBuffer::Static); } //url_ranges.clear(); if(!editable) { for(size_t i = 0; i < FONT_ARRAY_SIZE; ++i) { vertices[i].clear(); vertices[i].shrink_to_fit(); } vertices_linear.clear(); vertices_linear.shrink_to_fit(); } } void Text::updateCaret() { assert(!dirty && !dirtyText); mgl::Font *latin_font; if(bold_font) latin_font = FontLoader::get_font(FontLoader::FontType::LATIN_BOLD, characterSize); else latin_font = FontLoader::get_font(FontLoader::FontType::LATIN, characterSize); const float vspace = font_get_real_height(latin_font); if(vertices_linear.empty()) { caretIndex = 0; caretPosition = mgl::vec2f(0.0f, floor(vspace)); 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(vspace + lineSpacing); } else { caretPosition.x = get_caret_offset_by_caret_index(caretIndex); caretPosition.y = (1 + get_vertex_line(caretIndex)) * floor(vspace + 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(uint32_t 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 size_t Text::get_string_index_from_caret_index(size_t caret_index) const { size_t codepoint_index = 0; for(size_t i = 0; i < str.size();) { if(codepoint_index == caret_index) return i; unsigned char *cp = (unsigned char*)&str[i]; uint32_t codepoint; size_t clen; if(!mgl::utf8_decode(cp, str.size() - i, &codepoint, &clen)) { codepoint = *cp; clen = 1; } i += clen; ++codepoint_index; } return str.size(); } static size_t utf8_get_length(const std::string &str) { size_t codepoint_index = 0; for(size_t i = 0; i < str.size();) { unsigned char *cp = (unsigned char*)&str[i]; uint32_t codepoint; size_t clen; if(!mgl::utf8_decode(cp, str.size() - i, &codepoint, &clen)) { codepoint = *cp; clen = 1; } i += clen; ++codepoint_index; } return codepoint_index; } // 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(mgl::Window &window, const mgl::Event &event) { if(!editable) return; bool caretAtEnd = caretIndex == (int)vertices_linear.size(); if(event.type == mgl::Event::KeyPressed) { if(event.key.code == mgl::Keyboard::Left && caretIndex > 0) { if(event.key.control) caretMoveDirection = CaretMoveDirection::LEFT_WORD; else caretMoveDirection = CaretMoveDirection::LEFT; dirtyCaret = true; } else if(event.key.code == mgl::Keyboard::Right && !caretAtEnd) { if(event.key.control) caretMoveDirection = CaretMoveDirection::RIGHT_WORD; else caretMoveDirection = CaretMoveDirection::RIGHT; dirtyCaret = true; } else if(event.key.code == mgl::Keyboard::Backspace && caretIndex > 0) { const size_t str_index = get_string_index_from_caret_index(caretIndex); if(str_index > 0) { const size_t codepoint_start = mgl::utf8_get_start_of_codepoint((const unsigned char*)str.c_str(), str.size(), str_index - 1); str.erase(codepoint_start, str_index - codepoint_start); --caretIndex; dirty = true; dirtyText = true; dirtyCaret = true; } } else if(event.key.code == mgl::Keyboard::Delete && !caretAtEnd) { const size_t str_index = get_string_index_from_caret_index(caretIndex); uint32_t decoded_codepoint = 0; size_t decoded_length = 0; mgl::utf8_decode((const unsigned char*)str.c_str() + str_index, str.size() - str_index, &decoded_codepoint, &decoded_length); str.erase(str_index, decoded_length); dirty = true; dirtyText = true; } else if(event.key.code == mgl::Keyboard::D && event.key.control) { setString(""); } else if(event.key.code == mgl::Keyboard::Up) { caretMoveDirection = CaretMoveDirection::UP; } else if(event.key.code == mgl::Keyboard::Down) { caretMoveDirection = CaretMoveDirection::DOWN; } else if(event.key.code == mgl::Keyboard::Home) { caretMoveDirection = CaretMoveDirection::HOME; } else if(event.key.code == mgl::Keyboard::End) { caretMoveDirection = CaretMoveDirection::END; } else if(event.key.code == mgl::Keyboard::Enter) { if(event.key.shift && !single_line_edit) { if(caretAtEnd) { str += '\n'; } else { const size_t str_index = get_string_index_from_caret_index(caretIndex); str.insert(str_index, 1, '\n'); } ++caretIndex; dirty = true; dirtyText = true; dirtyCaret = true; } } } else if(event.type == mgl::Event::TextEntered) { if(event.text.codepoint == 8 || event.text.codepoint == 127) // backspace, del return; std::string stringToAdd; if(event.text.codepoint == 22) // ctrl+v { stringToAdd = window.get_clipboard(); } else if(event.text.codepoint >= 32 || (event.text.codepoint == '\t' && !single_line_edit)) stringToAdd.assign(event.text.str, event.text.size); else return; if(caretAtEnd) { str += stringToAdd; } else { const size_t str_index = get_string_index_from_caret_index(caretIndex); str.insert(str_index, stringToAdd); } caretIndex += utf8_get_length(stringToAdd); dirty = true; dirtyText = true; dirtyCaret = true; } } bool Text::draw(mgl::Window &target) { updateGeometry(); if(editable && (dirtyCaret || caretMoveDirection != CaretMoveDirection::NONE)) { updateCaret(); dirtyCaret = false; caretMoveDirection = CaretMoveDirection::NONE; } mgl::vec2f pos = position; FontLoader::FontType latin_font_type; if(bold_font) latin_font_type = FontLoader::FontType::LATIN_BOLD; else latin_font_type = FontLoader::FontType::LATIN; mgl::Font *latin_font = FontLoader::get_font(latin_font_type, characterSize); const float vspace = font_get_real_height(latin_font); 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, FontLoader::FontType::SYMBOLS }; for(size_t i = 0; i < FONT_INDEX_EMOJI; ++i) { if(vertex_buffers[i].size() == 0) continue; mgl::Font *font = FontLoader::get_font(font_types[i], characterSize); if(!font) continue; mgl::Texture font_texture = font->get_texture(); vertex_buffers[i].set_texture(&font_texture); vertex_buffers[i].set_position(pos); target.draw(vertex_buffers[i]); } if(vertex_buffers[FONT_INDEX_EMOJI].size() > 0) { vertex_buffers[FONT_INDEX_EMOJI].set_texture(TextureLoader::get_texture("images/emoji.png", true)); vertex_buffers[FONT_INDEX_EMOJI].set_position(pos); target.draw(vertex_buffers[FONT_INDEX_EMOJI]); } if(!editable) return true; pos.y -= floor(vspace * 2.0f); const float caret_margin = floor(2.0f * get_config().scale); mgl::Rectangle caretRect(mgl::vec2f(0.0f, 0.0f), mgl::vec2f(floor(2.0f * get_config().scale), floor(vspace - caret_margin * 2.0f))); caretRect.set_position(mgl::vec2f( floor(pos.x + caretPosition.x), floor(pos.y + caretPosition.y + caret_margin + floor(4.0f * get_config().scale)) )); target.draw(caretRect); return true; } }