aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2022-11-06 13:54:02 +0100
committerdec05eba <dec05eba@protonmail.com>2022-11-07 14:24:32 +0100
commit8025d1075db0779bde635148f6e38303eb29d6c8 (patch)
tree64dce4cc00fa55edba0ab7d2522e13473e6ef3c4 /src
parentf8b3a9d055bfc0e4bb9e9a570ccc8853ec38a225 (diff)
Formatted text with color in matrix, monospace for codeblocks
Diffstat (limited to 'src')
-rw-r--r--src/Body.cpp2
-rw-r--r--src/BodyItem.cpp3
-rw-r--r--src/Config.cpp1
-rw-r--r--src/QuickMedia.cpp41
-rw-r--r--src/ResourceLoader.cpp21
-rw-r--r--src/Text.cpp309
-rw-r--r--src/plugins/Matrix.cpp134
7 files changed, 343 insertions, 168 deletions
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<Text>(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<Message*>(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<BodyItem> 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<Message*>(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<Message*>(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>();
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 <assert.h>
static std::string resource_root;
-static std::array<std::unique_ptr<mgl::MemoryMappedFile>, 4> font_file_cache;
+static std::array<std::unique_ptr<mgl::MemoryMappedFile>, 5> font_file_cache;
// font_cache[(unsigned int)font_type][character_size]
-static std::array<std::unordered_map<unsigned int, std::unique_ptr<mgl::Font>>, 4> font_cache;
+static std::array<std::unordered_map<unsigned int, std::unique_ptr<mgl::Font>>, 5> font_cache;
static std::unordered_map<std::string, std::unique_ptr<mgl::Texture>> 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 <string.h>
+#include <stack>
#include <mglpp/graphics/Rectangle.hpp>
#include <mglpp/window/Event.hpp>
#include <mglpp/window/Window.hpp>
@@ -16,6 +17,8 @@
// TODO: Remove
#include <sstream>
+// 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<TextElement> 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<TextElement> &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<TextElement> &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<TextFormat> 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<TextElement> 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 <unistd.h>
#include <malloc.h>
#include "../../include/QuickMedia.hpp"
+#include <HtmlParser.h>
// 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<Message> 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<Message>();
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;