#include "../include/Text.hpp" #include "../include/ResourceLoader.hpp" #include "../include/Config.hpp" #include "../include/Theme.hpp" #include "../include/AsyncImageLoader.hpp" #include "../include/StringUtils.hpp" #include "../include/Scale.hpp" #include "../generated/Emoji.hpp" #include #include #include #include #include #include #include #include #include #include #include // TODO: Remove #include // TODO: text editing should take into consideration FORMATTED_TEXT_START/FORMATTED_TEXT_END. // TODO: Add elipses at end when using max_lines. Also allow choosing where the text is cut. // Right now the text is always cut at the end but it should be possible to cut in the middle and start too. 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_MONOSPACE = 1; static const size_t FONT_INDEX_CJK = 2; static const size_t FONT_INDEX_SYMBOLS = 3; static const size_t FONT_INDEX_EMOJI = 4; static const size_t FONT_INDEX_IMAGE = 5; static const uint8_t FORMATTED_TEXT_START = '\x02'; static const uint8_t FORMATTED_TEXT_END = '\x03'; static const mgl::vec2i MAX_IMAGE_SIZE(300, 300); enum class FormattedTextType : uint8_t { TEXT, IMAGE }; Text::Text() : bold_font(false), characterSize(12), maxWidth(0.0f), color(get_theme().text_color), force_color(false), 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), force_color(false), 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)); } // TODO: Validate |str|. Turn |str| into a valid utf-8 string 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; } // static std::string Text::formatted_image(const std::string &url, bool local, mgl::vec2i size, const std::string &alt) { const uint32_t str_size = url.size(); const uint32_t alt_str_size = alt.size(); std::string result; result += FORMATTED_TEXT_START; result += (uint8_t)FormattedTextType::IMAGE; result.append((const char*)&size.x, sizeof(size.x)); result.append((const char*)&size.y, sizeof(size.y)); result.append((const char*)&local, sizeof(local)); result.append((const char*)&str_size, sizeof(str_size)); result.append(url); result.append((const char*)&alt_str_size, sizeof(alt_str_size)); result.append(alt); result += FORMATTED_TEXT_END; return result; } // static std::string Text::formatted_text(const std::string &text, mgl::Color color, uint8_t text_flags) { const uint32_t str_size = text.size(); std::string result; result += FORMATTED_TEXT_START; result += (uint8_t)FormattedTextType::TEXT; result += color.r; result += color.g; result += color.b; result += color.a; result += text_flags; result.append((const char*)&str_size, sizeof(str_size)); result.append(text); result += FORMATTED_TEXT_END; return result; } void Text::insert_text_at_caret_position(const std::string &str) { this->str.insert(caretIndex, str); dirty = true; dirtyText = true; } void Text::set_max_lines(int max_lines) { if(max_lines < 0) max_lines = 0; if(max_lines != this->max_lines) { this->max_lines = max_lines; dirty = true; dirtyText = true; } } // static std::string Text::to_printable_string(const std::string &str) { std::string result; std::vector tmp_text_elements; Text::split_text_by_type(tmp_text_elements, str, 0.0f); for(auto &text_element : tmp_text_elements) { if(text_element.type == TextElement::Type::TEXT) result.append(text_element.text); else if(text_element.type == TextElement::Type::IMAGE) { if(!text_element.alt.empty()) result.append(text_element.alt); else result.append(text_element.url); } } return result; } 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; } static size_t utf8_get_num_codepoints(const char *str, size_t size) { size_t codepoint_index = 0; for(size_t i = 0; i < size;) { unsigned char *cp = (unsigned char*)&str[i]; uint32_t codepoint; size_t clen; if(!mgl::utf8_decode(cp, size - i, &codepoint, &clen)) { codepoint = *cp; clen = 1; } i += clen; ++codepoint_index; } return codepoint_index; } void Text::replace(size_t start_index, size_t length, const std::string &insert_str) { const bool at_end = start_index == str.size(); str.replace(start_index, length, insert_str); dirty = true; dirtyText = true; if(!at_end) { std::vector new_text_elements; Text::split_text_by_type(new_text_elements, insert_str, 0.0f); int caret_advance = 0; for(auto &text_element : new_text_elements) { if(text_element.type == TextElement::Type::IMAGE || text_element.text_type == TextElement::TextType::EMOJI) { caret_advance += 1; } else { caret_advance += utf8_get_num_codepoints(text_element.text.data(), text_element.text.size()); } } caretIndex += caret_advance; dirtyCaret = true; } } int Text::getCaretIndex() const { return caretIndex; } void Text::set_color(mgl::Color color, bool force_color) { if(color != this->color) { this->color = color; dirty = true; } if(force_color != this->force_color) { this->force_color = force_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 } // TODO: Optimize, dont use ostringstream static std::string codepoint_to_hex_str(uint32_t codepoint) { std::ostringstream ss; ss << std::hex << codepoint; return ss.str(); } static size_t find_end_text(const char *str, size_t size) { uint32_t emoji_sequence[32]; size_t emoji_sequence_length = 0; size_t emoji_byte_length = 0; 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 == FORMATTED_TEXT_START || match_emoji_sequence(cp, size - i, emoji_sequence, emoji_sequence_length, emoji_byte_length)) return i; i += clen; } return size; } static size_t parse_formatted_string(const char *str, size_t size, std::string_view &text, mgl::Color &color, uint8_t &flags) { flags = FORMATTED_TEXT_FLAG_NONE; if(size < 5 + sizeof(uint32_t)) return size; color.r = str[0]; color.g = str[1]; color.b = str[2]; color.a = str[3]; flags |= (uint8_t)str[4]; uint32_t text_size; memcpy(&text_size, str + 5, sizeof(text_size)); if(size < 5 + sizeof(uint32_t) + text_size) return size; text = std::string_view(str + 5 + sizeof(uint32_t), text_size); return std::min(5 + sizeof(uint32_t) + text_size + 1, size); // + 1 for FORMATTED_TEXT_END } static size_t parse_formatted_image(const char *str, size_t size, std::string &image_url, bool &image_local, mgl::vec2i &image_size, std::string &alt) { image_url.clear(); image_local = true; image_size = { 0, 0 }; alt.clear(); if(size < sizeof(image_size.x) + sizeof(image_size.y) + sizeof(image_local) + sizeof(uint32_t) + sizeof(uint32_t)) return size; size_t offset = 0; memcpy(&image_size.x, str, sizeof(image_size.x)); offset += sizeof(image_size.x); memcpy(&image_size.y, str + offset, sizeof(image_size.y)); offset += sizeof(image_size.y); memcpy(&image_local, str + offset, sizeof(image_local)); offset += sizeof(image_local); uint32_t text_size; memcpy(&text_size, str + offset, sizeof(text_size)); offset += sizeof(text_size); const size_t image_url_offset = offset; if(size < offset + text_size + sizeof(uint32_t)) return size; offset += text_size; uint32_t alt_size; memcpy(&alt_size, str + offset, sizeof(alt_size)); offset += sizeof(alt_size); const size_t alt_offset = offset; if(size < offset + alt_size) return size; image_url.assign(str + image_url_offset, text_size); alt.assign(str + alt_offset, alt_size); image_size = clamp_to_size(image_size, MAX_IMAGE_SIZE); return std::min(offset + alt_size + 1, size); // + 1 for FORMATTED_TEXT_END } static size_t parse_formatted_text(const char *str, size_t size, TextElement &text_element) { if(size == 0) return 0; FormattedTextType formatted_text_type = (FormattedTextType)*(uint8_t*)&str[0]; switch(formatted_text_type) { case FormattedTextType::TEXT: { text_element.type = TextElement::Type::TEXT; return parse_formatted_string(str + 1, size - 1, text_element.text, text_element.color, text_element.text_flags); } case FormattedTextType::IMAGE: { text_element.type = TextElement::Type::IMAGE; return parse_formatted_image(str + 1, size - 1, text_element.url, text_element.local, text_element.size, text_element.alt); } default: break; } return 0; } // static void Text::split_text_by_type(std::vector &text_elements, std::string_view str, float vspace) { size_t index = 0; size_t size = str.size(); size_t offset; uint32_t emoji_sequence[32]; size_t emoji_sequence_length = 0; size_t emoji_byte_length = 0; std::string emoji_codepoint_combined; 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; } TextElement text_element; if(codepoint == FORMATTED_TEXT_START) { index += 1; offset = parse_formatted_text(str.data() + index, size - index, text_element) + 1; // TODO: if(offset > 0) { text_element.text_type = TextElement::TextType::TEXT; if(text_element.type == TextElement::Type::TEXT) { const std::string_view inside_text = text_element.text; text_element.text = std::string_view(); text_element.text_num_bytes = 0; text_element.type = TextElement::Type::FORMAT_START; text_elements.push_back(text_element); split_text_by_type(text_elements, inside_text, vspace); text_element.type = TextElement::Type::FORMAT_END; text_elements.push_back(std::move(text_element)); } else { text_element.text_num_bytes = 1; if(!text_element.url.empty()) text_elements.push_back(std::move(text_element)); } } } else if(match_emoji_sequence((const unsigned char*)str.data() + index, size - index, emoji_sequence, emoji_sequence_length, emoji_byte_length)) { offset = emoji_byte_length; emoji_codepoint_combined.clear(); for(size_t i = 0; i < emoji_sequence_length; ++i) { if(!emoji_codepoint_combined.empty()) emoji_codepoint_combined += '-'; emoji_codepoint_combined += codepoint_to_hex_str(emoji_sequence[i]); } // Twemoji issue string_replace_all(emoji_codepoint_combined, "-fe0f-20e3", "-20e3"); if(emoji_codepoint_combined == "1f441-fe0f-200d-1f5e8-fe0f") emoji_codepoint_combined = "1f441-200d-1f5e8"; text_element.create_text(std::string_view(str.data() + index, offset)); text_element.text_type = TextElement::TextType::EMOJI; text_element.url = "/usr/share/quickmedia/emoji/" + emoji_codepoint_combined + ".png"; // Some emoji do not work with the -fe0f variant if(emoji_sequence_length > 0 && emoji_sequence[emoji_sequence_length - 1] == 0xfe0f && access(text_element.url.c_str(), F_OK) != 0) { emoji_codepoint_combined.erase(emoji_codepoint_combined.end() - 5, emoji_codepoint_combined.end()); text_element.url = "/usr/share/quickmedia/emoji/" + emoji_codepoint_combined + ".png"; } else if(emoji_sequence_length > 0 && emoji_sequence[emoji_sequence_length - 1] != 0xfe0f && access(text_element.url.c_str(), F_OK) != 0) { emoji_codepoint_combined += "-fe0f"; text_element.url = "/usr/share/quickmedia/emoji/" + emoji_codepoint_combined + ".png"; } text_element.local = true; text_element.size = { (int)vspace, (int)vspace }; text_element.text_num_bytes = emoji_byte_length; text_elements.push_back(std::move(text_element)); } else { offset = find_end_text(str.data() + index, size - index); text_element.create_text(std::string_view(str.data() + index, offset)); text_element.text_type = TextElement::TextType::TEXT; text_element.text_num_bytes = text_element.text.size(); text_elements.push_back(std::move(text_element)); } // We never want an infinite loop even if there is a bug in the above code offset = std::max(offset, (size_t)1); index += offset; } } void Text::split_text_by_type() { 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); textElements.clear(); split_text_by_type(textElements, str, font_get_real_height(latin_font)); } float Text::font_get_real_height(mgl::Font *font) { return font->get_glyph('|').size.y + floor(4.0f * ((float)font->get_character_size() / (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_text_quad_top_side(const VertexRef &vertex_ref) const { return vertices[vertex_ref.vertices_index][vertex_ref.index + 1].position.y; } float Text::get_text_quad_bottom_side(const VertexRef &vertex_ref) const { return vertices[vertex_ref.vertices_index][vertex_ref.index + 4].position.y; } float Text::get_text_quad_height(const VertexRef &vertex_ref) const { return get_text_quad_bottom_side(vertex_ref) - get_text_quad_top_side(vertex_ref); } 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::move_vertex_lines_by_largest_items(int vertices_linear_end) { if(vertices_linear.empty() || vertices_linear_end == 0) return; mgl::Font *latin_font; if(bold_font) latin_font = FontLoader::get_font(FontLoader::FontType::LATIN_BOLD, characterSize * get_config().font.scale.latin_bold); else latin_font = FontLoader::get_font(FontLoader::FontType::LATIN, characterSize * get_config().font.scale.latin); const float vspace = font_get_real_height(latin_font); const float vertex_height = get_text_quad_height(vertices_linear[0]); float vertex_max_height = std::max(vertex_height, vspace); float vertex_second_max_height = vspace; int current_line = vertices_linear[0].line; int current_line_vertices_linear_start = 0; float move_y = 0.0f; for(int i = 0; i < vertices_linear_end; ++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.y += move_y; } if(vertices_linear[i].line != current_line) { const float vertices_move_down_offset = vertex_max_height - vertex_second_max_height; if(vertex_max_height > vspace/* && vertex_max_height - vertex_min_height > 2.0f*/) { for(int j = current_line_vertices_linear_start; j <= i; ++j) { VertexRef &vertex_ref = vertices_linear[j]; mgl::Vertex *vertex = &vertices[vertex_ref.vertices_index][vertex_ref.index]; for(int v = 0; v < 6; ++v) { vertex[v].position.y += vertices_move_down_offset; } } move_y += vertices_move_down_offset; } vertex_max_height = vspace; current_line = vertices_linear[i].line; current_line_vertices_linear_start = i; } const float vertex_height = std::max(get_text_quad_height(vertex_ref), vspace); if(vertex_height > vertex_max_height) { vertex_second_max_height = vertex_max_height; vertex_max_height = vertex_height; } } const float vertices_move_down_offset = vertex_max_height - vertex_second_max_height; if(vertex_max_height > vspace/* && vertex_max_height - vertex_min_height > 2.0f*/) { // TODO: current_line_vertices_linear_start vs vertices_linear_end for(int j = current_line_vertices_linear_start; j < vertices_linear_end; ++j) { VertexRef &vertex_ref = vertices_linear[j]; mgl::Vertex *vertex = &vertices[vertex_ref.vertices_index][vertex_ref.index]; for(int v = 0; v < 6; ++v) { vertex[v].position.y += vertices_move_down_offset; } } } } void Text::updateGeometry(bool update_even_if_not_dirty) { if(dirtyText) { assert(dirty); dirtyText = false; split_text_by_type(); // 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 * get_config().font.scale.latin_bold); else latin_font = FontLoader::get_font(FontLoader::FontType::LATIN, characterSize * get_config().font.scale.latin); const float latin_font_width = latin_font->get_glyph(' ').advance; const float vspace = font_get_real_height(latin_font); const float hspace_latin = latin_font_width + characterSpacing; const float emoji_spacing = std::max(1, int(vspace / 10.0f)); int hspace_monospace = 0; const mgl::Color url_color = get_theme().url_text_color; size_t url_range_index = 0; struct TextFormat { mgl::Color color; uint8_t text_flags = FORMATTED_TEXT_FLAG_NONE; }; std::stack text_format_stack; mgl::vec2f glyphPos; uint32_t prevCodePoint = 0; bool text_contains_images = false; // 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(int textElementIndex = 0; textElementIndex < (int)textElements.size(); ++textElementIndex) { TextElement &textElement = textElements[textElementIndex]; mgl::Color text_element_color = color; bool monospace = false; if(!text_format_stack.empty()) { if((text_format_stack.top().text_flags & FORMATTED_TEXT_FLAG_COLOR) && !force_color) text_element_color = text_format_stack.top().color; if(text_format_stack.top().text_flags & FORMATTED_TEXT_FLAG_CODE) monospace = true; } mgl::Font *ff = latin_font; int vertices_index = FONT_INDEX_LATIN; if(textElement.type == TextElement::Type::FORMAT_START) { text_format_stack.push({ textElement.color, textElement.text_flags }); } else if(textElement.type == TextElement::Type::FORMAT_END) { if(!text_format_stack.empty()) text_format_stack.pop(); } else if(textElement.type == TextElement::Type::IMAGE) { text_contains_images = true; vertices_index = FONT_INDEX_IMAGE; mgl::Color image_color(255, 255, 255, text_element_color.a); int vertexStart = vertices[vertices_index].size(); if(prevCodePoint != 0) glyphPos.x += emoji_spacing + characterSpacing; const float font_height_offset = vspace; mgl::vec2f vertexTopLeft(glyphPos.x, glyphPos.y + font_height_offset - textElement.size.y); mgl::vec2f vertexTopRight(glyphPos.x + textElement.size.x, glyphPos.y + font_height_offset - textElement.size.y); mgl::vec2f vertexBottomLeft(glyphPos.x, glyphPos.y + font_height_offset); mgl::vec2f vertexBottomRight(glyphPos.x + textElement.size.x, glyphPos.y + font_height_offset); vertexTopLeft = vec2f_floor(vertexTopLeft); vertexTopRight = vec2f_floor(vertexTopRight); vertexBottomLeft = vec2f_floor(vertexBottomLeft); vertexBottomRight = vec2f_floor(vertexBottomRight); mgl::vec2f textureTopLeft(0.0f, 0.0f); mgl::vec2f textureTopRight(0.0f + textElement.size.x, 0.0f); mgl::vec2f textureBottomLeft(0.0f, 0.0f + textElement.size.y); mgl::vec2f textureBottomRight(0.0f + textElement.size.x, 0.0f + textElement.size.y); vertices[vertices_index].emplace_back(vertexTopRight, textureTopRight, image_color); vertices[vertices_index].emplace_back(vertexTopLeft, textureTopLeft, image_color); vertices[vertices_index].emplace_back(vertexBottomLeft, textureBottomLeft, image_color); vertices[vertices_index].emplace_back(vertexBottomLeft, textureBottomLeft, image_color); vertices[vertices_index].emplace_back(vertexBottomRight, textureBottomRight, image_color); vertices[vertices_index].emplace_back(vertexTopRight, textureTopRight, image_color); // TODO: Size.x update glyphPos.x += floor(textElement.size.x) + characterSpacing + emoji_spacing; textElement.vertex_ref_index = vertices_linear.size(); vertices_linear.push_back({vertices_index, vertexStart, 0, textElement.text_num_bytes, 'E'}); prevCodePoint = 0; continue; } else if(textElement.text_type == TextElement::TextType::EMOJI) { vertices_index = FONT_INDEX_EMOJI; mgl::Color emoji_color(255, 255, 255, text_element_color.a); int vertexStart = vertices[vertices_index].size(); const mgl::vec2f emoji_size = { vspace, vspace }; if(prevCodePoint != 0) glyphPos.x += emoji_spacing + characterSpacing; const float font_height_offset = floor(vspace * 0.5f); mgl::vec2f vertexTopLeft(glyphPos.x, glyphPos.y + font_height_offset - emoji_size.y * 0.5f); mgl::vec2f vertexTopRight(glyphPos.x + emoji_size.x, glyphPos.y + font_height_offset - emoji_size.y * 0.5f); mgl::vec2f vertexBottomLeft(glyphPos.x, glyphPos.y + font_height_offset + emoji_size.y * 0.5f); mgl::vec2f vertexBottomRight(glyphPos.x + emoji_size.x, glyphPos.y + font_height_offset + emoji_size.y * 0.5f); vertexTopLeft = vec2f_floor(vertexTopLeft); vertexTopRight = vec2f_floor(vertexTopRight); vertexBottomLeft = vec2f_floor(vertexBottomLeft); vertexBottomRight = vec2f_floor(vertexBottomRight); mgl::vec2f textureTopLeft(0.0f, 0.0f); mgl::vec2f textureTopRight(0.0f + emoji_size.x, 0.0f); mgl::vec2f textureBottomLeft(0.0f, 0.0f + emoji_size.y); mgl::vec2f textureBottomRight(0.0f + emoji_size.x, 0.0f + emoji_size.y); 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 += emoji_size.x + characterSpacing + emoji_spacing; textElement.vertex_ref_index = vertices_linear.size(); vertices_linear.push_back({vertices_index, vertexStart, 0, textElement.text_num_bytes, 'E'}); prevCodePoint = 0; 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 = text_element_color; if(!force_color && !monospace) { 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 = text_element_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; } float hspace = hspace_latin; // TODO: CJK monospace if(is_symbol_codepoint(codepoint)) { ff = FontLoader::get_font(FontLoader::FontType::SYMBOLS, characterSize * get_config().font.scale.symbols); vertices_index = FONT_INDEX_SYMBOLS; } else if(is_cjk_codepoint(codepoint)) { ff = FontLoader::get_font(FontLoader::FontType::CJK, characterSize * get_config().font.scale.cjk); vertices_index = FONT_INDEX_CJK; } else if(monospace) { ff = FontLoader::get_font(FontLoader::FontType::LATIN_MONOSPACE, characterSize * get_config().font.scale.latin_monospace); vertices_index = FONT_INDEX_MONOSPACE; if(hspace_monospace == 0) hspace_monospace = ff->get_glyph(' ').advance + characterSpacing; hspace = hspace_monospace; } else { ff = latin_font; vertices_index = FONT_INDEX_LATIN; } // 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, (int)clen, 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, (int)clen, 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, (int)clen, 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, (int)clen, 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; bool cut_lines = false; // TODO: Binary search? int vertices_linear_index = 0; for(vertices_linear_index = 0; vertices_linear_index < (int)vertices_linear.size(); ++vertices_linear_index) { VertexRef &vertex_ref = vertices_linear[vertices_linear_index]; 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 = vertices_linear_index; break; case '\n': text_wrap_offset = 0.0f; last_space_index = -1; ++num_lines; if(max_lines != 0 && num_lines > max_lines) { --num_lines; cut_lines = true; goto vertices_linear_done; } break; default: break; } if(vertex_ref.codepoint == '\n') continue; float vertex_right_side = get_text_quad_right_side(vertex_ref); if(vertex_right_side > maxWidth && maxWidth > WORD_WRAP_MIN_SIZE) { ++num_lines; if(max_lines != 0 && num_lines > max_lines) { --num_lines; cut_lines = true; goto vertices_linear_done; } // TODO: Ignore line wrap on space if(last_space_index != -1 && last_space_index != vertices_linear_index) { float vertex_left_side = get_text_quad_left_side(vertices_linear[last_space_index + 1]); for(int j = last_space_index + 1; j <= vertices_linear_index; ++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; } } vertices_linear_done:; if(text_contains_images) move_vertex_lines_by_largest_items(vertices_linear_index); // TODO: Optimize for(TextElement &textElement : textElements) { if(textElement.text_type == TextElement::TextType::EMOJI || textElement.type == TextElement::Type::IMAGE) { const VertexRef &vertex_ref = vertices_linear[textElement.vertex_ref_index]; const mgl::vec2f top_left_vertex_pos = vertices[vertex_ref.vertices_index][vertex_ref.index + 1].position; textElement.pos = { (int)top_left_vertex_pos.x, (int)top_left_vertex_pos.y }; // Hide images by drawing them far away if we should cut them... if(cut_lines && textElement.vertex_ref_index >= vertices_linear_index) textElement.pos.y = 9999.0f; } } std::array resized_vertices; for(size_t i = 0; i < resized_vertices.size(); ++i) { resized_vertices[i] = false; } if(cut_lines) { for(int i = vertices_linear_index; i < (int)vertices_linear.size(); ++i) { VertexRef &vertex_ref = vertices_linear[i]; if(resized_vertices[vertex_ref.vertices_index]) continue; vertices[vertex_ref.vertices_index].resize(vertex_ref.index); resized_vertices[vertex_ref.vertices_index] = true; } vertices_linear.resize(vertices_linear_index); } boundingBox.size.x = 0.0f; boundingBox.size.y = 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 = std::max(boundingBox.size.y, get_text_quad_bottom_side(vertex_ref)); } //boundingBox.size.y = num_lines * line_height; //boundingBox.size.y = text_offset_y; // TODO: if(vertices_linear.empty()) boundingBox.size.y = line_height; if(editable) 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 * get_config().font.scale.latin_bold); else latin_font = FontLoader::get_font(FontLoader::FontType::LATIN, characterSize * get_config().font.scale.latin); 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; } int Text::getNumLines() const { return num_lines; } // 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(!vertices_linear.empty()) { const float right_pos_last = get_text_quad_right_side(vertices_linear.back()); const float pos_diff = std::abs(caret_offset_x - right_pos_last); if(pos_diff < closest_char) { closest_char = pos_diff; closest_index = vertices_linear.size(); } } 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 str_index = 0; for(size_t i = 0; i < vertices_linear.size(); ++i) { if(i == caret_index) break; auto &vertex = vertices_linear[i]; str_index += vertex.text_num_bytes; } return str_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; // TODO: Dont do this here! it's slow, but it's needed to process the change before the next event updateGeometry(); if(dirtyCaret || caretMoveDirection != CaretMoveDirection::NONE) { updateCaret(); dirtyCaret = false; caretMoveDirection = CaretMoveDirection::NONE; } if(caretIndex > (int)vertices_linear.size()) caretIndex = vertices_linear.size(); 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) { if(caretIndex < 0 || caretIndex > (int)vertices_linear.size()) return; const size_t str_index = get_string_index_from_caret_index(caretIndex); if(str_index > 0 && str_index <= str.size()) { const size_t codepoint_start = str_index - vertices_linear[caretIndex - 1].text_num_bytes; str.erase(codepoint_start, str_index - codepoint_start); --caretIndex; dirty = true; dirtyText = true; dirtyCaret = true; } } else if(event.key.code == mgl::Keyboard::Delete && !caretAtEnd) { if(caretIndex < 0 || caretIndex >= (int)vertices_linear.size()) return; const size_t str_index = get_string_index_from_caret_index(caretIndex); const size_t codepoint_end = str_index + vertices_linear[caretIndex].text_num_bytes; if(str_index < str.size() && codepoint_end <= str.size()) { str.erase(str_index, codepoint_end - str_index); 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_string(); } 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); } std::vector new_text_elements; Text::split_text_by_type(new_text_elements, stringToAdd, 0.0f); for(auto &text_element : new_text_elements) { if(text_element.type == TextElement::Type::IMAGE || text_element.text_type == TextElement::TextType::EMOJI) { caretIndex += 1; } else { caretIndex += utf8_get_num_codepoints(text_element.text.data(), text_element.text.size()); } } 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; float char_scale = 1.0f; FontLoader::FontType latin_font_type; if(bold_font) { latin_font_type = FontLoader::FontType::LATIN_BOLD; char_scale = get_config().font.scale.latin_bold; } else { latin_font_type = FontLoader::FontType::LATIN; char_scale = get_config().font.scale.latin; } mgl::Font *latin_font = FontLoader::get_font(latin_font_type, characterSize * char_scale); const float vspace = font_get_real_height(latin_font); pos.y += floor(vspace*0.25f); // Origin is at bottom left, we want it to be at top left assert(FONT_ARRAY_SIZE == 6); const FontLoader::FontType font_types[] = { latin_font_type, FontLoader::FontType::LATIN_MONOSPACE, FontLoader::FontType::CJK, FontLoader::FontType::SYMBOLS }; const float font_scales[] = { char_scale, get_config().font.scale.latin_monospace, get_config().font.scale.cjk, get_config().font.scale.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 * font_scales[i]); 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]); }*/ // TODO: Use rounded rectangle for fallback image // TODO: Use a new vector with only the image data instead of this. // TODO: Sprite mgl::Sprite sprite; mgl::Rectangle fallback_image(mgl::vec2f(vspace, vspace)); fallback_image.set_color(get_theme().image_loading_background_color); for(const TextElement &textElement : textElements) { if(textElement.text_type == TextElement::TextType::EMOJI) { // TODO: if(textElement.pos.to_vec2f().y + vspace > boundingBox.size.y + 10.0f) { //fprintf(stderr, "bounding box y: %f\n", boundingBox.size.y); continue; } auto emoji_data = AsyncImageLoader::get_instance().get_thumbnail(textElement.url, textElement.local, { (int)vspace, (int)vspace }); if(emoji_data->loading_state == LoadingState::FINISHED_LOADING) { if(!emoji_data->texture.load_from_image(*emoji_data->image)) fprintf(stderr, "Warning: failed to load text emoji: %s\n", textElement.url.c_str()); emoji_data->image.reset(); emoji_data->loading_state = LoadingState::APPLIED_TO_TEXTURE; } if(emoji_data->loading_state == LoadingState::APPLIED_TO_TEXTURE && emoji_data->texture.get_size().x > 0) { sprite.set_texture(&emoji_data->texture); sprite.set_position(pos + textElement.pos.to_vec2f()); //sprite.set_size(textElement.size.to_vec2f()); target.draw(sprite); } else { fallback_image.set_position(pos + textElement.pos.to_vec2f()); target.draw(fallback_image); } } } // TODO: Use a new vector with only the image data instead of this. // TODO: Sprite for(const TextElement &textElement : textElements) { if(textElement.type == TextElement::Type::IMAGE) { auto thumbnail_data = AsyncImageLoader::get_instance().get_thumbnail(textElement.url, textElement.local, textElement.size); if(thumbnail_data->loading_state == LoadingState::FINISHED_LOADING) { if(!thumbnail_data->texture.load_from_image(*thumbnail_data->image)) fprintf(stderr, "Warning: failed to load text image: %s\n", textElement.url.c_str()); thumbnail_data->image.reset(); thumbnail_data->loading_state = LoadingState::APPLIED_TO_TEXTURE; } if(thumbnail_data->loading_state == LoadingState::APPLIED_TO_TEXTURE && thumbnail_data->texture.get_size().x > 0) { // TODO: if(textElement.pos.to_vec2f().y + thumbnail_data->texture.get_size().y > boundingBox.size.y + 10.0f) continue; sprite.set_texture(&thumbnail_data->texture); sprite.set_position(pos + textElement.pos.to_vec2f()); //sprite.set_size(textElement.size.to_vec2f()); target.draw(sprite); } else { fallback_image.set_size(textElement.size.to_vec2f()); fallback_image.set_position(pos + textElement.pos.to_vec2f()); target.draw(fallback_image); } } } if(!editable) return true; pos.y -= floor(vspace*1.25f); 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_color(color); 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; } }