From 8025d1075db0779bde635148f6e38303eb29d6c8 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 6 Nov 2022 13:54:02 +0100 Subject: Formatted text with color in matrix, monospace for codeblocks --- src/Text.cpp | 309 ++++++++++++++++++++++++++++++++--------------------------- 1 file changed, 169 insertions(+), 140 deletions(-) (limited to 'src/Text.cpp') diff --git a/src/Text.cpp b/src/Text.cpp index 7692eef..e6fe90c 100644 --- a/src/Text.cpp +++ b/src/Text.cpp @@ -6,6 +6,7 @@ #include "../include/StringUtils.hpp" #include "../generated/Emoji.hpp" #include +#include #include #include #include @@ -16,6 +17,8 @@ // TODO: Remove #include +// TODO: text editing should take into consideration FORMATTED_TEXT_START/FORMATTED_TEXT_END. + namespace QuickMedia { static float floor(float v) { @@ -30,11 +33,11 @@ namespace QuickMedia 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_INDEX_IMAGE = 4; - static const size_t FONT_ARRAY_SIZE = 5; + 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'; @@ -49,6 +52,7 @@ namespace QuickMedia characterSize(12), maxWidth(0.0f), color(get_theme().text_color), + force_color(false), dirty(true), dirtyText(true), dirtyCaret(true), @@ -69,6 +73,7 @@ namespace QuickMedia characterSize(characterSize), maxWidth(maxWidth), color(get_theme().text_color), + force_color(false), dirty(true), dirtyText(true), dirtyCaret(true), @@ -83,21 +88,6 @@ namespace QuickMedia { 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) { @@ -126,25 +116,24 @@ namespace QuickMedia dirtyText = true; } - void Text::append_image(const std::string &url, bool local, mgl::vec2i size) { - str += Text::formatted_image(url, local, size); - } - // static std::string Text::formatted_image(const std::string &url, bool local, mgl::vec2i size) { + const uint32_t str_size = url.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 += FORMATTED_TEXT_END; return result; } // static - std::string Text::formatted_text(const std::string &text, mgl::Color color, bool bold) { + 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; @@ -152,7 +141,8 @@ namespace QuickMedia result += color.g; result += color.b; result += color.a; - result += (uint8_t)bold; + result += text_flags; + result.append((const char*)&str_size, sizeof(str_size)); result.append(text); result += FORMATTED_TEXT_END; return result; @@ -163,6 +153,18 @@ namespace QuickMedia 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); + } + return result; + } void Text::set_position(float x, float y) { @@ -227,13 +229,17 @@ namespace QuickMedia return caretIndex; } - void Text::set_color(mgl::Color color) + void Text::set_color(mgl::Color color, bool force_color) { - if(color != this->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) @@ -328,24 +334,6 @@ namespace QuickMedia 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; - } - // TODO: Optimize, dont use ostringstream static std::string codepoint_to_hex_str(uint32_t codepoint) { std::ostringstream ss; @@ -353,7 +341,11 @@ namespace QuickMedia return ss.str(); } - static size_t find_end_of_symbol(const char *str, size_t size) { + 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; @@ -363,60 +355,64 @@ namespace QuickMedia clen = 1; } - if(!is_symbol_codepoint(codepoint)) + 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 find_end_latin(const char *str, size_t size) { - uint32_t emoji_sequence[32]; - size_t emoji_sequence_length = 0; - size_t emoji_byte_length = 0; + 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; - 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(size < 5 + sizeof(uint32_t)) + return size; - if(is_cjk_codepoint(codepoint) || is_symbol_codepoint(codepoint) || codepoint == FORMATTED_TEXT_START || match_emoji_sequence(cp, size - i, emoji_sequence, emoji_sequence_length, emoji_byte_length)) - return i; + color.r = str[0]; + color.g = str[1]; + color.b = str[2]; + color.a = str[3]; + flags |= (uint8_t)str[4]; - i += clen; - } + uint32_t text_size; + memcpy(&text_size, str + 5, sizeof(text_size)); - return 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 } -// TODO: -#if 0 static size_t parse_formatted_image(const char *str, size_t size, std::string &image_url, bool &image_local, mgl::vec2i &image_size) { image_url.clear(); image_local = true; image_size = { 0, 0 }; - for(size_t i = 0; i < size; ++i) { - const char *cp = &str[i]; - if(*cp == FORMATTED_TEXT_END) { - const size_t image_len = i; - if(image_len < sizeof(image_size.x) + sizeof(image_size.y) + sizeof(image_local)) - return size; - - memcpy(&image_size.x, str, sizeof(image_size.x)); - memcpy(&image_size.y, str + sizeof(image_size.x), sizeof(image_size.y)); - memcpy(&image_local, str + sizeof(image_size.x) + sizeof(image_size.y), sizeof(image_local)); - const size_t image_url_index = sizeof(image_size.x) + sizeof(image_size.y) + sizeof(image_local); - image_url.assign(str + image_url_index, image_len - image_url_index); - return i + 1; - } - } - return size; + if(size < sizeof(image_size.x) + sizeof(image_size.y) + sizeof(image_local) + 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); + + if(size < offset + text_size) + return size; + + image_url.assign(str + offset, text_size); + return std::min(offset + text_size + 1, size); // + 1 for FORMATTED_TEXT_END } static size_t parse_formatted_text(const char *str, size_t size, TextElement &text_element) { @@ -427,8 +423,7 @@ namespace QuickMedia switch(formatted_text_type) { case FormattedTextType::TEXT: { text_element.type = TextElement::Type::TEXT; - // TODO: - //return parse_formatted_text(str + 1, size - 1, text_element) + 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; @@ -439,17 +434,9 @@ namespace QuickMedia } return 0; } -#endif - void Text::split_text_by_type(std::vector &text_elements, const std::string &str) { - text_elements.clear(); - 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); + // 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(); @@ -470,31 +457,27 @@ namespace QuickMedia TextElement text_element; - if(is_symbol_codepoint(codepoint)) { - offset = find_end_of_symbol(str.data() + index, size - index); - - text_element.create_text(std::string_view(str.data() + index, offset)); - text_element.text_type = TextElement::TextType::SYMBOL; - text_element.text_num_bytes = text_element.text.size(); - text_elements.push_back(std::move(text_element)); - } else if(is_cjk_codepoint(codepoint)) { - offset = find_end_of_cjk(str.data() + index, size - index); - - text_element.create_text(std::string_view(str.data() + index, offset)); - text_element.text_type = TextElement::TextType::CJK; - text_element.text_num_bytes = text_element.text.size(); - text_elements.push_back(std::move(text_element)); - } else if(codepoint == FORMATTED_TEXT_START) { - // TODO: - offset = 1; - #if 0 - text_element.type = TextElement::Type::IMAGE; - + if(codepoint == FORMATTED_TEXT_START) { index += 1; - offset = parse_formatted_text(str.data() + index, size - index, text_element); - text_element.text_num_bytes = ... // TODO - text_elements.push_back(std::move(text_element)); - #endif + 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("", 0); + 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; + 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; @@ -510,19 +493,18 @@ namespace QuickMedia if(emoji_codepoint_combined == "1f441-fe0f-200d-1f5e8-fe0f") emoji_codepoint_combined = "1f441-200d-1f5e8"; - text_element.create_text("E"); + text_element.create_text(std::string_view(str.data() + index, offset)); text_element.text_type = TextElement::TextType::EMOJI; - text_element.text = "E"; 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_latin(str.data() + index, size - index); + 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::LATIN; + text_element.text_type = TextElement::TextType::TEXT; text_element.text_num_bytes = text_element.text.size(); text_elements.push_back(std::move(text_element)); } @@ -533,6 +515,16 @@ namespace QuickMedia } } + 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)characterSize / (float)14.0f)); } @@ -614,7 +606,7 @@ namespace QuickMedia if(dirtyText) { assert(dirty); dirtyText = false; - split_text_by_type(textElements, str); + split_text_by_type(); // TODO: Optimize if(highlight_urls) { url_ranges = extract_urls(str); @@ -646,8 +638,14 @@ namespace QuickMedia const float emoji_spacing = 2.0f; 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; @@ -656,11 +654,25 @@ namespace QuickMedia { 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_MONOSPACE) + monospace = true; + } + mgl::Font *ff = latin_font; int vertices_index = FONT_INDEX_LATIN; - if(textElement.type == TextElement::Type::IMAGE) { + 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) { vertices_index = FONT_INDEX_IMAGE; - mgl::Color image_color(255, 255, 255, color.a); + mgl::Color image_color(255, 255, 255, text_element_color.a); int vertexStart = vertices[vertices_index].size(); if(prevCodePoint != 0) @@ -696,15 +708,9 @@ namespace QuickMedia prevCodePoint = 0; continue; - } else 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); + mgl::Color emoji_color(255, 255, 255, text_element_color.a); int vertexStart = vertices[vertices_index].size(); const mgl::vec2f emoji_size = { vspace, vspace }; @@ -746,12 +752,12 @@ namespace QuickMedia //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; + mgl::Color text_color = text_element_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; + 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; } @@ -764,6 +770,22 @@ namespace QuickMedia codepoint = *cp; clen = 1; } + + // TODO: CJK monospace + if(is_symbol_codepoint(codepoint)) { + ff = FontLoader::get_font(FontLoader::FontType::SYMBOLS, characterSize); + vertices_index = FONT_INDEX_SYMBOLS; + } else if(is_cjk_codepoint(codepoint)) { + ff = FontLoader::get_font(FontLoader::FontType::CJK, characterSize); + vertices_index = FONT_INDEX_CJK; + } else if(monospace) { + ff = FontLoader::get_font(FontLoader::FontType::LATIN_MONOSPACE, characterSize); + vertices_index = FONT_INDEX_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); @@ -1277,7 +1299,7 @@ namespace QuickMedia } std::vector new_text_elements; - split_text_by_type(new_text_elements, stringToAdd); + 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; @@ -1322,7 +1344,8 @@ namespace QuickMedia 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 - const FontLoader::FontType font_types[] = { latin_font_type, FontLoader::FontType::CJK, FontLoader::FontType::SYMBOLS }; + assert(FONT_ARRAY_SIZE == 6); + const FontLoader::FontType font_types[] = { latin_font_type, FontLoader::FontType::LATIN_MONOSPACE, FontLoader::FontType::CJK, FontLoader::FontType::SYMBOLS }; for(size_t i = 0; i < FONT_INDEX_EMOJI; ++i) { if(vertex_buffers[i].size() == 0) continue; @@ -1346,6 +1369,9 @@ namespace QuickMedia // TODO: Use a new vector with only the image data instead of this. // TODO: Sprite mgl::Sprite sprite; + mgl::Rectangle fallback_emoji(mgl::vec2f(vspace, vspace)); + fallback_emoji.set_color(get_theme().shade_color); + for(const TextElement &textElement : textElements) { if(textElement.text_type == TextElement::TextType::EMOJI) { auto emoji_data = AsyncImageLoader::get_instance().get_thumbnail(textElement.url, textElement.local, { (int)vspace, (int)vspace }); @@ -1356,11 +1382,14 @@ namespace QuickMedia emoji_data->loading_state = LoadingState::APPLIED_TO_TEXTURE; } - if(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_emoji.set_position(pos + textElement.pos.to_vec2f()); + target.draw(fallback_emoji); } } } -- cgit v1.2.3