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/Body.cpp | 2 +- src/BodyItem.cpp | 3 + src/Config.cpp | 1 + src/QuickMedia.cpp | 41 ++++--- src/ResourceLoader.cpp | 21 +++- src/Text.cpp | 309 +++++++++++++++++++++++++++---------------------- src/plugins/Matrix.cpp | 134 +++++++++++++++++++-- 7 files changed, 343 insertions(+), 168 deletions(-) (limited to 'src') diff --git a/src/Body.cpp b/src/Body.cpp index 5de0c56..32d5dd5 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -883,7 +883,7 @@ namespace QuickMedia { } else { body_item->description_text = std::make_unique(body_item->get_description(), false, std::floor(get_config().body.description_font_size * get_config().scale * get_config().font_scale), width, true); } - body_item->description_text->set_color(body_item->get_description_color()); + body_item->description_text->set_color(body_item->get_description_color(), body_item->force_description_color); body_item->description_text->updateGeometry(); } diff --git a/src/BodyItem.cpp b/src/BodyItem.cpp index dfd6302..64d18d4 100644 --- a/src/BodyItem.cpp +++ b/src/BodyItem.cpp @@ -72,6 +72,9 @@ namespace QuickMedia { title_color = other.title_color; author_color = other.author_color; description_color = other.description_color; + force_title_color = other.force_title_color; + force_description_color = other.force_description_color; + force_author_color = other.force_author_color; extra = other.extra; keep_alive_frames = other.keep_alive_frames; selectable = other.selectable; diff --git a/src/Config.cpp b/src/Config.cpp index 4ce65b5..6a73dcf 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -293,6 +293,7 @@ namespace QuickMedia { if(font_json.isObject()) { get_json_value_path(font_json, "latin", config->font.latin); get_json_value_path(font_json, "latin_bold", config->font.latin_bold); + get_json_value_path(font_json, "latin_monospace", config->font.latin_monospace); get_json_value_path(font_json, "cjk", config->font.cjk); get_json_value_path(font_json, "symbols", config->font.symbols); } diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index bbff6c6..80ae146 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -5308,7 +5308,7 @@ namespace QuickMedia { body_item->embedded_item->embedded_item = nullptr; body_item->embedded_item->reactions.clear(); if(message->user->user_id != my_user_id && ((related_body_item->userdata && static_cast(related_body_item->userdata)->user.get() == me) || message_contains_user_mention(body_item->get_description(), my_display_name) || message_contains_user_mention(body_item->get_description(), my_user_id))) - body_item->set_description_color(get_theme().attention_alert_text_color); + body_item->set_description_color(get_theme().attention_alert_text_color, true); else body_item->set_description_color(get_theme().text_color); body_item->embedded_item_status = FetchStatus::FINISHED_LOADING; @@ -5322,7 +5322,7 @@ namespace QuickMedia { static std::shared_ptr message_to_body_item(RoomData *room, Message *message, const std::string &my_display_name, const std::string &my_user_id) { auto body_item = BodyItem::create(""); body_item->set_author(extract_first_line_remove_newline_elipses(room->get_user_display_name(message->user), AUTHOR_MAX_LENGTH)); - body_item->set_description(strip(message_get_body_remove_formatting(message))); + body_item->set_description(strip(formatted_text_to_qm_text(message->body.c_str(), message->body.size()))); body_item->set_timestamp(message->timestamp); if(!message->thumbnail_url.empty()) { body_item->thumbnail_url = message->thumbnail_url; @@ -5353,7 +5353,7 @@ namespace QuickMedia { body_item->thumbnail_url.clear(); } if(message->user->user_id != my_user_id && (message_contains_user_mention(body_item->get_description(), my_display_name) || message_contains_user_mention(body_item->get_description(), my_user_id))) - body_item->set_description_color(get_theme().attention_alert_text_color); + body_item->set_description_color(get_theme().attention_alert_text_color, true); return body_item; } @@ -5652,9 +5652,11 @@ namespace QuickMedia { // TODO: Properly check reply message objects for mention of user instead of message data, but only when synapse fixes that notifications // are not triggered by reply to a message with our display name/user id. Message *reply_to_message = static_cast(body_item->userdata); - body_item->set_description(strip(message_get_body_remove_formatting(message.get()))); + std::string qm_formatted_text = formatted_text_to_qm_text(reply_to_message->body.c_str(), reply_to_message->body.size()); + + body_item->set_description(std::move(qm_formatted_text)); if(message->user != me && (message_contains_user_mention(reply_to_message->body, my_display_name) || message_contains_user_mention(reply_to_message->body, me->user_id))) - body_item->set_description_color(get_theme().attention_alert_text_color); + body_item->set_description_color(get_theme().attention_alert_text_color, true); else body_item->set_description_color(get_theme().text_color); message->replaces = reply_to_message; @@ -5691,9 +5693,11 @@ namespace QuickMedia { // TODO: Properly check reply message objects for mention of user instead of message data, but only when synapse fixes that notifications // are not triggered by reply to a message with our display name/user id. Message *reply_to_message = static_cast(body_item->userdata); - body_item->set_description(strip(message_get_body_remove_formatting(message.get()))); + std::string qm_formatted_text = formatted_text_to_qm_text(reply_to_message->body.c_str(), reply_to_message->body.size()); + + body_item->set_description(std::move(qm_formatted_text)); if(message->user != me && (message_contains_user_mention(reply_to_message->body, my_display_name) || message_contains_user_mention(reply_to_message->body, me->user_id))) - body_item->set_description_color(get_theme().attention_alert_text_color); + body_item->set_description_color(get_theme().attention_alert_text_color, true); else body_item->set_description_color(get_theme().text_color); message->replaces = reply_to_message; @@ -6114,9 +6118,9 @@ namespace QuickMedia { auto message = std::make_shared(); message->user = matrix->get_me(current_room); if(msgtype == "m.emote") - message->body = "*" + current_room->get_user_display_name(me) + "* " + text; + message->body = "*" + current_room->get_user_display_name(me) + "* " + matrix->body_to_formatted_body(current_room, text); else - message->body = text; + message->body = matrix->body_to_formatted_body(current_room, text); message->type = MessageType::TEXT; message->timestamp = time(NULL) * 1000; @@ -6196,8 +6200,11 @@ namespace QuickMedia { size_t body_item_index = 0; auto body_item = find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->get_items().data(), tabs[MESSAGES_TAB_INDEX].body->get_items().size(), message->related_event_id, &body_item_index); if(body_item) { + const std::string formatted_text = matrix->body_to_formatted_body(current_room, text); + std::string qm_formatted_text = formatted_text_to_qm_text(formatted_text.c_str(), formatted_text.size()); + auto body_item_shared_ptr = tabs[MESSAGES_TAB_INDEX].body->get_item_by_index(body_item_index); - body_item_shared_ptr->set_description(text); + body_item_shared_ptr->set_description(std::move(qm_formatted_text)); body_item_shared_ptr->set_description_color(get_theme().provisional_message_color); auto edit_body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); @@ -6292,7 +6299,7 @@ namespace QuickMedia { *body_item = *related_body_item; body_item->reactions.clear(); if(message_contains_user_mention(related_body_item->get_description(), current_room->get_user_display_name(me)) || message_contains_user_mention(related_body_item->get_description(), me->user_id)) - body_item->set_description_color(get_theme().attention_alert_text_color); + body_item->set_description_color(get_theme().attention_alert_text_color, true); else body_item->set_description_color(get_theme().text_color); event_data->status = FetchStatus::FINISHED_LOADING; @@ -6928,8 +6935,10 @@ namespace QuickMedia { if(event.key.control && event.key.code == mgl::Keyboard::C) { BodyItem *selected = tabs[selected_tab].body->get_selected(); - if(selected) - set_clipboard(selected->get_description()); + if(selected) { + const std::string body_text_unformatted = Text::to_printable_string(selected->get_description()); + set_clipboard(body_text_unformatted); + } } if(selected_tab == MESSAGES_TAB_INDEX) { @@ -7058,10 +7067,12 @@ namespace QuickMedia { // TODO: Show inline notification show_notification("QuickMedia", "You can't edit a message that was posted by somebody else"); } else { + const std::string body_text_unformatted = Text::to_printable_string(selected->get_description()); + chat_state = ChatState::EDITING; currently_operating_on_item = selected; chat_input.set_editable(true); - chat_input.set_text(selected->get_description()); // TODO: Description? it may change in the future, in which case this should be edited + chat_input.set_text(std::move(body_text_unformatted)); // TODO: Description? it may change in the future, in which case this should be edited chat_input.move_caret_to_end(); replying_to_text.set_string("Editing message:"); } @@ -7480,7 +7491,7 @@ namespace QuickMedia { fetch_body_item->embedded_item = message_to_body_item(current_room, fetch_message_result.message.get(), current_room->get_user_display_name(me), me->user_id); fetch_body_item->embedded_item_status = FetchStatus::FINISHED_LOADING; if(fetch_message_result.message->user == me) - fetch_body_item->set_description_color(get_theme().attention_alert_text_color); + fetch_body_item->set_description_color(get_theme().attention_alert_text_color, true); } else { fetch_body_item->embedded_item_status = FetchStatus::FAILED_TO_LOAD; } diff --git a/src/ResourceLoader.cpp b/src/ResourceLoader.cpp index e086ee7..86687d2 100644 --- a/src/ResourceLoader.cpp +++ b/src/ResourceLoader.cpp @@ -12,9 +12,9 @@ #include static std::string resource_root; -static std::array, 4> font_file_cache; +static std::array, 5> font_file_cache; // font_cache[(unsigned int)font_type][character_size] -static std::array>, 4> font_cache; +static std::array>, 5> font_cache; static std::unordered_map> texture_cache; namespace QuickMedia { @@ -100,6 +100,23 @@ namespace QuickMedia::FontLoader { } break; } + case FontType::LATIN_MONOSPACE: { + const char *args[] = { "fc-match", "monospace:lang=en", "file", nullptr }; + if(get_config().use_system_fonts && exec_program(args, accumulate_string, &output) == 0 && output.size() > 6) { + Path path = strip(output.substr(6)); + noto_directories.push_back(path.parent().data); + font_file_name = path.filename(); + } else if(!get_config().font.latin_monospace.empty() && find_font(get_config().font.latin_monospace, found_font_filepath)) { + const Path font_path = found_font_filepath; + noto_directories.push_back(font_path.parent().data); + font_file_name = font_path.filename(); + } else { + noto_directories.push_back("/usr/share/fonts/noto"); + noto_directories.push_back("/usr/share/fonts/truetype/noto"); + font_file_name = "NotoSansMono-Regular.ttf"; + } + break; + } case FontType::CJK: { const char *args[] = { "fc-match", "sans:lang=ja", "file", nullptr }; if(get_config().use_system_fonts && exec_program(args, accumulate_string, &output) == 0 && output.size() > 6) { 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); } } } diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 0fcc1c3..e4a7bd1 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -18,6 +18,7 @@ #include #include #include "../../include/QuickMedia.hpp" +#include // TODO: Use string assign with string length instead of assigning to c string (which calls strlen) // Show images/videos inline. @@ -598,15 +599,15 @@ namespace QuickMedia { } static std::string message_to_room_description_text(Message *message) { - std::string body = strip(message->body); + std::string body = strip(formatted_text_to_qm_text(message->body.c_str(), message->body.size())); if(message->type == MessageType::REACTION) - return "Reacted with: " + extract_first_line_remove_newline_elipses(body, 150); + return "Reacted with: " + body; else if(message->related_event_type == RelatedEventType::REPLY) - return extract_first_line_remove_newline_elipses(remove_reply_formatting(body), 150); + return body; else if(message->related_event_type == RelatedEventType::EDIT) - return "Edited: " + extract_first_line_remove_newline_elipses(remove_reply_formatting(body), 150); + return "Edited: " + body; else - return extract_first_line_remove_newline_elipses(body, 150); + return body; } void MatrixQuickMedia::update_room_description(RoomData *room, const Messages &new_messages, bool is_initial_sync, bool sync_is_cache) { @@ -672,13 +673,13 @@ namespace QuickMedia { if(!room_desc.empty()) room_desc += '\n'; room_desc += "** " + std::to_string(unread_notification_count) + " unread mention(s) **"; // TODO: Better notification? - room->body_item->set_description_color(get_theme().attention_alert_text_color); + room->body_item->set_description_color(get_theme().attention_alert_text_color, true); } else { room->body_item->set_description_color(get_theme().faded_text_color); } room->body_item->set_description(std::move(room_desc)); if(set_room_as_unread) - room->body_item->set_title_color(get_theme().attention_alert_text_color); + room->body_item->set_title_color(get_theme().attention_alert_text_color, true); room->last_message_read = false; rooms_page->move_room_to_top(room); @@ -1085,8 +1086,8 @@ namespace QuickMedia { body_item->url = notification.event_id; if(!notification.read) { - body_item->set_author_color(get_theme().attention_alert_text_color); - body_item->set_description_color(get_theme().attention_alert_text_color); + body_item->set_author_color(get_theme().attention_alert_text_color, true); + body_item->set_description_color(get_theme().attention_alert_text_color, true); } body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; @@ -2232,6 +2233,115 @@ namespace QuickMedia { return result; } + // Returns -1 if its not a hex value + static int get_hex_value(char c) { + if(c >= '0' && c <= '9') + return c - '0'; + else if(c >= 'a' && c <= 'f') + return 10 + (c - 'a'); + else if(c >= 'A' && c <= 'F') + return 10 + (c - 'A'); + else + return -1; + } + + // Parses hex colors in the format #RRGGBB(AA) + static bool parse_hex_set_color(const char *str, int size, mgl::Color &color) { + if(size == 0) + return false; + + // #RRGGBB(AA), case insensitive hex + if(str[0] != '#') + return false; + + if(size - 1 != 6 && size - 1 != 8) + return false; + + mgl::Color new_color; + for(int i = 1; i < size; i += 2) { + const int c1 = get_hex_value(str[i + 0]); + const int c2 = get_hex_value(str[i + 1]); + if(c1 == -1 || c2 == -1) + return false; + (&new_color.r)[(i - 1)/2] = (c1 << 4) | c2; + } + color = new_color; + return true; + } + + struct FormattedTextParseUserdata { + std::string result; + int mx_reply_depth = 0; + bool inside_font_tag = false; + bool font_tag_has_custom_color = false; + bool inside_code_tag = false; + mgl::Color font_color = mgl::Color(255, 255, 255, 255); + }; + + // TODO: Full proper parsing with tag depth + static int formattext_text_parser_callback(HtmlParser *html_parser, HtmlParseType parse_type, void *userdata) { + FormattedTextParseUserdata &parse_userdata = *(FormattedTextParseUserdata*)userdata; + switch(parse_type) { + case HTML_PARSE_TAG_START: { + if(html_parser->tag_name.size == 2 && memcmp(html_parser->tag_name.data, "br", 2) == 0) + parse_userdata.result += '\n'; + else if(html_parser->tag_name.size == 4 && memcmp(html_parser->tag_name.data, "font", 4) == 0) + parse_userdata.inside_font_tag = true; + else if(html_parser->tag_name.size == 8 && memcmp(html_parser->tag_name.data, "mx-reply", 8) == 0) + ++parse_userdata.mx_reply_depth; + else if(html_parser->tag_name.size == 4 && memcmp(html_parser->tag_name.data, "code", 4) == 0) + parse_userdata.inside_code_tag = true; + break; + } + case HTML_PARSE_TAG_END: { + /*if(html_parser->tag_name.size == 2 && memcmp(html_parser->tag_name.data, "br", 2) == 0) { + parse_userdata.result += '\n'; + } else */if(html_parser->tag_name.size == 4 && memcmp(html_parser->tag_name.data, "font", 4) == 0) { + parse_userdata.inside_font_tag = false; + parse_userdata.font_tag_has_custom_color = false; + } else if(html_parser->tag_name.size == 8 && memcmp(html_parser->tag_name.data, "mx-reply", 8) == 0) { + parse_userdata.mx_reply_depth = std::max(0, parse_userdata.mx_reply_depth - 1); + } else if(html_parser->tag_name.size == 4 && memcmp(html_parser->tag_name.data, "code", 4) == 0) { + parse_userdata.inside_code_tag = false; + } + break; + } + case HTML_PARSE_ATTRIBUTE: { + if(parse_userdata.inside_font_tag && html_parser->attribute_key.size == 5 && memcmp(html_parser->attribute_key.data, "color", 5) == 0) { + if(parse_hex_set_color(html_parser->attribute_value.data, html_parser->attribute_value.size, parse_userdata.font_color)) + parse_userdata.font_tag_has_custom_color = true; + } + break; + } + case HTML_PARSE_TEXT: + case HTML_PARSE_JAVASCRIPT_CODE: { + if(parse_userdata.mx_reply_depth == 0) { + std::string text_to_add(html_parser->text.data, html_parser->text.size); + html_unescape_sequences(text_to_add); + + uint8_t formatted_text_flags = FORMATTED_TEXT_FLAG_NONE; + if(parse_userdata.font_tag_has_custom_color) + formatted_text_flags |= FORMATTED_TEXT_FLAG_COLOR; + if(parse_userdata.inside_code_tag) + formatted_text_flags |= FORMATTED_TEXT_FLAG_MONOSPACE; + + if(formatted_text_flags != FORMATTED_TEXT_FLAG_NONE) + parse_userdata.result += Text::formatted_text(text_to_add, parse_userdata.font_color, formatted_text_flags); + else + parse_userdata.result += std::move(text_to_add); + } + break; + } + } + return 0; + } + + std::string formatted_text_to_qm_text(const char *str, size_t size) { + FormattedTextParseUserdata parse_userdata; + html_parser_parse(str, size, formattext_text_parser_callback, &parse_userdata); + return std::move(parse_userdata.result); + } + std::shared_ptr Matrix::parse_message_event(const rapidjson::Value &event_item_json, RoomData *room_data) { if(!event_item_json.IsObject()) return nullptr; @@ -2612,6 +2722,10 @@ namespace QuickMedia { if(!body_json.IsString()) return nullptr; + const rapidjson::Value *formatted_body_json = &GetMember(*content_json, "formatted_body"); + if(!formatted_body_json->IsString()) + formatted_body_json = &body_json; + auto message = std::make_shared(); std::string prefix; @@ -2678,7 +2792,7 @@ namespace QuickMedia { message->user = user; message->event_id = event_id_str; - message->body = prefix + body_json.GetString(); + message->body = prefix + std::string(formatted_body_json->GetString(), formatted_body_json->GetStringLength()); message->related_event_id = std::move(related_event_id); message->related_event_type = related_event_type; message->timestamp = timestamp; -- cgit v1.2.3