diff options
author | dec05eba <dec05eba@protonmail.com> | 2020-09-28 13:32:34 +0200 |
---|---|---|
committer | dec05eba <dec05eba@protonmail.com> | 2020-09-28 13:32:34 +0200 |
commit | 4277763df5c1dac8ff389d3bfd138f03acc7f1e2 (patch) | |
tree | b0c3fc77ea601f105308d42840adb1ce2050b414 /src | |
parent | a16cfdc4f6cd14d576760c100a817c08f45be394 (diff) |
Implement text editing with navigation and multilingual fonts
Diffstat (limited to 'src')
-rw-r--r-- | src/Entry.cpp | 72 | ||||
-rw-r--r-- | src/ImageViewer.cpp | 1 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 95 | ||||
-rw-r--r-- | src/SearchBar.cpp | 3 | ||||
-rw-r--r-- | src/StringUtils.cpp | 10 | ||||
-rw-r--r-- | src/Text.cpp | 418 | ||||
-rw-r--r-- | src/plugins/Matrix.cpp | 6 | ||||
-rw-r--r-- | src/plugins/Plugin.cpp | 4 |
8 files changed, 368 insertions, 241 deletions
diff --git a/src/Entry.cpp b/src/Entry.cpp new file mode 100644 index 0000000..977feab --- /dev/null +++ b/src/Entry.cpp @@ -0,0 +1,72 @@ +#include "../include/Entry.hpp" +#include <SFML/Graphics/RenderWindow.hpp> +#include <SFML/Graphics/RectangleShape.hpp> +#include <SFML/Window/Event.hpp> +#include <cmath> + +const float background_margin_horizontal = 15.0f; +const float padding_vertical = 5.0f; +const float background_margin_vertical = 4.0f; + +namespace QuickMedia { + Entry::Entry(const std::string &placeholder_text, sf::Font *font, sf::Font *cjk_font) : + on_submit_callback(nullptr), + text("", font, cjk_font, 18, 0.0f), + width(0.0f), + background(sf::Vector2f(1.0f, 1.0f), 10.0f, 10), + placeholder(placeholder_text, *font, 18) + { + text.setEditable(true); + background.setFillColor(sf::Color(55, 60, 68)); + placeholder.setFillColor(sf::Color(255, 255, 255, 100)); + } + + void Entry::process_event(sf::Event &event) { + text.processEvent(event); + if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Enter && !event.key.shift) { + if(on_submit_callback) { + bool clear_text = on_submit_callback(text.getString()); + if(clear_text) + text.setString(""); + } + } + } + + // TODO: Set the max number of visible lines and use glScissor to cut off the lines outsides + // (and also split text into lines to not draw them at all once they are not inside the scissor box) + void Entry::draw(sf::RenderWindow &window) { + background.setSize(sf::Vector2f(width, get_height())); + window.draw(background); + if(text.getString().isEmpty() && !text.isEditable()) { + window.draw(placeholder); + //sf::Vector2f placeholder_pos = placeholder.getPosition(); + //const float caret_margin = 2.0f; + //const float vspace = placeholder.getFont()->getLineSpacing(18); + //sf::RectangleShape caret_rect(sf::Vector2f(2.0f, floor(vspace - caret_margin * 2.0f))); + //caret_rect.setPosition(floor(placeholder_pos.x), floor(placeholder_pos.y + caret_margin)); + //window.draw(caret_rect); + } else { + text.draw(window); + } + } + + void Entry::set_editable(bool editable) { + text.setEditable(editable); + } + + void Entry::set_position(const sf::Vector2f &pos) { + background.setPosition(pos); + text.setPosition(pos + sf::Vector2f(background_margin_horizontal, background_margin_vertical)); + placeholder.setPosition(pos + sf::Vector2f(background_margin_horizontal, background_margin_vertical + 7.0f)); + } + + void Entry::set_max_width(float width) { + this->width = width; + text.setMaxWidth(this->width - background_margin_horizontal * 2.0f); + } + + float Entry::get_height() { + text.updateGeometry(); + return std::floor(text.getHeight() + background_margin_vertical * 2.0f + padding_vertical *2.0f); + } +}
\ No newline at end of file diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index c45e35d..c929086 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -4,6 +4,7 @@ #include "../plugins/Manga.hpp" #include <cmath> #include <SFML/Window/Event.hpp> +#include <SFML/Graphics/RenderWindow.hpp> #include <SFML/Graphics/RectangleShape.hpp> namespace QuickMedia { diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index a0d8508..a854613 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -18,6 +18,7 @@ #include "../include/ImageViewer.hpp" #include "../include/ImageUtils.hpp" #include "../include/base64_url.hpp" +#include "../include/Entry.hpp" #include <SFML/Graphics/RectangleShape.hpp> #include <SFML/Window/Clipboard.hpp> @@ -3107,6 +3108,7 @@ namespace QuickMedia { } else if(navigation_stage == NavigationStage::POSTING_COMMENT) { // TODO: Show "Posting..." when posting comment } else if(navigation_stage == NavigationStage::VIEWING_ATTACHED_IMAGE) { + // TODO: Use image instead of data with string. texture->loadFromMemory creates a temporary image anyways that parses the string. std::string image_data; if(downloading_image && load_image_future.valid() && load_image_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { downloading_image = false; @@ -3341,22 +3343,16 @@ namespace QuickMedia { // get_all_room_messages is not needed here because its done in the loop, where the initial timeout is 0ms - SearchBar chat_input(*font, &plugin_logo, "Send a message..."); - chat_input.set_background_color(sf::Color::Transparent); - chat_input.padding_vertical = 10.0f; - - // TODO: Scroll to bottom when receiving new messages, but only if we are already at the bottom? - - // TODO: Filer for rooms and settings - chat_input.onTextUpdateCallback = nullptr; - Page new_page = Page::CHAT; + bool typing_message = false; - // TODO: Show post message immediately, instead of waiting for sync. Otherwise it can take a while until we receive the message, - // which happens when uploading an image. - chat_input.onTextSubmitCallback = [matrix, &tabs, &selected_tab, ¤t_room_id, &new_page](const std::string &text) -> bool { + sf::Sprite logo_sprite(plugin_logo); + + Entry chat_input("Press ctrl+c to begin writing a message...", font.get(), cjk_font.get()); + chat_input.set_editable(false); + chat_input.on_submit_callback = [matrix, &chat_input, &tabs, &selected_tab, ¤t_room_id, &new_page, &typing_message](const sf::String &text) { if(tabs[selected_tab].type == ChatTabType::MESSAGES) { - if(text.empty()) + if(text.isEmpty()) return false; if(text[0] == '/') { @@ -3364,9 +3360,13 @@ namespace QuickMedia { strip(command); if(command == "/upload") { new_page = Page::FILE_MANAGER; + chat_input.set_editable(false); + typing_message = false; return true; } else if(command == "/logout") { new_page = Page::CHAT_LOGIN; + chat_input.set_editable(false); + typing_message = false; return true; } else { fprintf(stderr, "Error: invalid command: %s, expected /upload\n", command.c_str()); @@ -3375,11 +3375,14 @@ namespace QuickMedia { } // TODO: Make asynchronous - if(matrix->post_message(current_room_id, text) != PluginResult::OK) { + if(matrix->post_message(current_room_id, text) == PluginResult::OK) { + chat_input.set_editable(false); + typing_message = false; + return true; + } else { show_notification("QuickMedia", "Failed to post matrix message", Urgency::CRITICAL); return false; } - return true; } return false; }; @@ -3413,6 +3416,9 @@ namespace QuickMedia { sf::RectangleShape more_messages_below_rect; more_messages_below_rect.setFillColor(sf::Color(128, 50, 50)); + sf::RectangleShape chat_input_shade; + chat_input_shade.setFillColor(sf::Color(33, 38, 44)); + sf::Clock start_typing_timer; const double typing_timeout_seconds = 3.0; bool typing = false; @@ -3430,13 +3436,15 @@ namespace QuickMedia { sf::Clock frame_timer; + float prev_chat_height = chat_input.get_height(); + while (current_page == Page::CHAT) { sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds(); while (window.pollEvent(event)) { base_event_handler(event, Page::EXIT, false, false, false); if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) { redraw = true; - } else if(event.type == sf::Event::KeyPressed) { + } else if(event.type == sf::Event::KeyPressed && !typing_message) { if(event.key.code == sf::Keyboard::Up || event.key.code == sf::Keyboard::PageUp || event.key.code == sf::Keyboard::Home) { bool hit_top = false; switch(event.key.code) { @@ -3511,9 +3519,14 @@ namespace QuickMedia { } } - if(tabs[selected_tab].type == ChatTabType::MESSAGES) { + if(!typing_message && tabs[selected_tab].type == ChatTabType::MESSAGES && event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::C && event.key.control) { + chat_input.set_editable(true); + typing_message = true; + } + + if(typing_message && tabs[selected_tab].type == ChatTabType::MESSAGES) { if(event.type == sf::Event::TextEntered) { - chat_input.onTextEntered(event.text.unicode); + //chat_input.onTextEntered(event.text.unicode); // TODO: Also show typing event when ctrl+v pasting? if(event.text.unicode != 13) { // Return key start_typing_timer.restart(); @@ -3523,8 +3536,12 @@ namespace QuickMedia { } typing = true; } + } else if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Escape) { + chat_input.set_editable(false); + typing_message = false; } - chat_input.on_event(event); + //chat_input.on_event(event); + chat_input.process_event(event); } else if(tabs[selected_tab].type == ChatTabType::ROOMS && event.type == sf::Event::KeyReleased && event.key.code == sf::Keyboard::Enter) { BodyItem *selected_item = tabs[selected_tab].body->get_selected(); if(selected_item) { @@ -3607,10 +3624,22 @@ namespace QuickMedia { const float tab_shade_height = tab_spacer_height + std::floor(tab_vertical_offset) + tab_height + 10.0f; + const float chat_height = chat_input.get_height(); + if(std::abs(chat_height - prev_chat_height) > 1.0f) { + prev_chat_height = chat_height; + redraw = true; + } if(redraw) { + const float logo_padding_x = 15.0f; + const float chat_input_padding_x = 15.0f; + const float chat_input_padding_y = 15.0f; + + //chat_height += 10.0f; redraw = false; - chat_input.onWindowResize(window_size); - chat_input.set_vertical_position(window_size.y - chat_input.getBottomWithoutShadow()); + chat_input.set_max_width(window_size.x - (logo_padding_x + plugin_logo.getSize().x + chat_input_padding_x * 2.0f)); + chat_input.set_position(sf::Vector2f(logo_padding_x + plugin_logo.getSize().x + chat_input_padding_x, window_size.y - chat_height - chat_input_padding_y)); + //chat_input.onWindowResize(window_size); + //chat_input.set_vertical_position(window_size.y - chat_input.getBottomWithoutShadow()); float body_padding_horizontal = 25.0f; float body_padding_vertical = 5.0f; @@ -3620,15 +3649,17 @@ namespace QuickMedia { body_padding_horizontal = 0.0f; } - float input_bottom = chat_input.getBottomWithoutShadow(); - if(tabs[selected_tab].type != ChatTabType::MESSAGES) - input_bottom = 0.0f; + chat_input_shade.setSize(sf::Vector2f(window_size.x, chat_input.get_height() + chat_input_padding_y * 2.0f)); + chat_input_shade.setPosition(0.0f, window_size.y - chat_input_shade.getSize().y); + body_pos = sf::Vector2f(body_padding_horizontal, body_padding_vertical + tab_shade_height); - body_size = sf::Vector2f(body_width, window_size.y - input_bottom - body_padding_vertical - tab_shade_height); + body_size = sf::Vector2f(body_width, window_size.y - chat_input_shade.getSize().y - body_padding_vertical + tab_shade_height); //get_body_dimensions(window_size, &chat_input, body_pos, body_size, true); more_messages_below_rect.setSize(sf::Vector2f(window_size.x, gradient_height)); - more_messages_below_rect.setPosition(0.0f, std::floor(window_size.y - chat_input.getBottomWithoutShadow() - gradient_height)); + more_messages_below_rect.setPosition(0.0f, std::floor(window_size.y - chat_input_shade.getSize().y - gradient_height)); + + logo_sprite.setPosition(logo_padding_x, window_size.y - chat_input_shade.getSize().y * 0.5f - plugin_logo.getSize().y * 0.5f); } if(!sync_running && sync_timer.getElapsedTime().asMilliseconds() >= sync_min_time_ms) { @@ -3681,16 +3712,12 @@ namespace QuickMedia { fetching_previous_messages_running = false; } - chat_input.update(); + //chat_input.update(); window.clear(back_color); - if(tabs[selected_tab].type == ChatTabType::MESSAGES) - chat_input.draw(window, false); - const float width_per_tab = window_size.x / tabs.size(); tab_background.setSize(sf::Vector2f(std::floor(width_per_tab - tab_margin_x * 2.0f), tab_height)); - tabs[selected_tab].body->draw(window, body_pos, body_size); const float tab_y = tab_spacer_height + std::floor(tab_vertical_offset + tab_height * 0.5f - (tab_text_size + 5.0f) * 0.5f); @@ -3739,6 +3766,12 @@ namespace QuickMedia { window.draw(more_messages_below_rect); } + if(tabs[selected_tab].type == ChatTabType::MESSAGES) { + window.draw(chat_input_shade); + chat_input.draw(window); //chat_input.draw(window, false); + window.draw(logo_sprite); + } + window.display(); } diff --git a/src/SearchBar.cpp b/src/SearchBar.cpp index 419ca38..fa81c61 100644 --- a/src/SearchBar.cpp +++ b/src/SearchBar.cpp @@ -2,9 +2,12 @@ #include "../include/Scale.hpp" #include <SFML/Window/Event.hpp> #include <SFML/Window/Clipboard.hpp> +#include <SFML/Graphics/RenderWindow.hpp> #include <cmath> #include <assert.h> +// TODO: Use a seperate placeholder sf::Text instead of switching the text to placeholder text.... + const sf::Color text_placeholder_color(255, 255, 255, 100); const sf::Color front_color(55, 60, 68); const float background_margin_horizontal = 15.0f; diff --git a/src/StringUtils.cpp b/src/StringUtils.cpp index f255971..111822e 100644 --- a/src/StringUtils.cpp +++ b/src/StringUtils.cpp @@ -4,10 +4,10 @@ namespace QuickMedia { void string_split(const std::string &str, char delimiter, StringSplitCallback callback_func) { size_t index = 0; - while(true) { + while(index < str.size()) { size_t new_index = str.find(delimiter, index); if(new_index == std::string::npos) - break; + new_index = str.size(); if(!callback_func(str.data() + index, new_index - index)) break; @@ -19,11 +19,12 @@ namespace QuickMedia { size_t string_replace_all(std::string &str, char old_char, const std::string &new_str) { size_t num_replaced_substrings = 0; size_t index = 0; - while(true) { + while(index < str.size()) { index = str.find(old_char, index); if(index == std::string::npos) break; str.replace(index, 1, new_str); + index += new_str.size(); ++num_replaced_substrings; } return num_replaced_substrings; @@ -32,11 +33,12 @@ namespace QuickMedia { size_t string_replace_all(std::string &str, const std::string &old_str, const std::string &new_str) { size_t num_replaced_substrings = 0; size_t index = 0; - while(true) { + while(index < str.size()) { index = str.find(old_str, index); if(index == std::string::npos) break; str.replace(index, old_str.size(), new_str); + index += new_str.size(); ++num_replaced_substrings; } return num_replaced_substrings; diff --git a/src/Text.cpp b/src/Text.cpp index 8f58e3d..3962374 100644 --- a/src/Text.cpp +++ b/src/Text.cpp @@ -1,6 +1,9 @@ #include "../include/Text.hpp" #include <SFML/Graphics/RectangleShape.hpp> #include <SFML/Window/Clipboard.hpp> +#include <SFML/Window/Event.hpp> +#include <SFML/Graphics/RenderTarget.hpp> +#include <SFML/Graphics/Font.hpp> #include <cmath> #include <functional> @@ -48,8 +51,8 @@ namespace QuickMedia void Text::setString(sf::String str) { - if(str != this->str) - { + //if(str != this->str) + //{ this->str = std::move(str); dirty = true; dirtyText = true; @@ -58,7 +61,7 @@ namespace QuickMedia caretIndex = this->str.getSize(); dirtyCaret = true; } - } + // } } const sf::String& Text::getString() const @@ -156,6 +159,10 @@ namespace QuickMedia } } + bool Text::isEditable() const { + return editable; + } + float Text::getWidth() const { return boundingBox.width; @@ -214,10 +221,18 @@ namespace QuickMedia index += 1 + offset; } } + + float Text::get_text_quad_left_side(const VertexRef &vertex_ref) const { + return vertices[vertex_ref.vertices_index][vertex_ref.index].position.x; + } + + float Text::get_text_quad_right_side(const VertexRef &vertex_ref) const { + return vertices[vertex_ref.vertices_index][vertex_ref.index + 1].position.x; + } - // Logic loosely based on https://github.com/SFML/SFML/wiki/Source:-CurvedText void Text::updateGeometry(bool update_even_if_not_dirty) { if(dirtyText) { + assert(dirty); dirtyText = false; splitTextByFont(); } @@ -225,10 +240,11 @@ namespace QuickMedia if(!update_even_if_not_dirty && !dirty) return; + vertices_linear.clear(); vertices[0].clear(); vertices[1].clear(); float hspace = font->getGlyph(' ', characterSize, false).advance + characterSpacing; - float vspace = font->getLineSpacing(characterSize); + float vspace = font->getLineSpacing(characterSize); // TODO: What about japanese font??? boundingBox = sf::FloatRect(); @@ -236,27 +252,27 @@ namespace QuickMedia sf::Uint32 prevCodePoint = 0; for(usize textElementIndex = 0; textElementIndex < textElements.size(); ++textElementIndex) { - size_t lastSpacingWordWrapIndex = -1; - float lastSpacingAccumulatedOffset = 0.0f; TextElement &textElement = textElements[textElementIndex]; const sf::Font *ff = font; - size_t vertices_index = 0; + int vertices_index = 0; if(textElement.is_japanese) { ff = cjk_font; vertices_index = 1; } usize vertexOffset = vertices[vertices_index].getVertexCount(); - vertices[vertices_index].resize(vertices[vertices_index].getVertexCount() + 4 * (textElement.text.size + 1)); + vertices[vertices_index].resize(vertices[vertices_index].getVertexCount() + 4 * textElement.text.size); // TODO: Precalculate textElement.position = glyphPos; for(size_t i = 0; i < textElement.text.size; ++i) { sf::Uint32 codePoint = textElement.text[i]; + // 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->getKerning(prevCodePoint, codePoint, characterSize); prevCodePoint = codePoint; glyphPos.x += kerning; - usize vertexStart = vertexOffset + i * 4; + int vertexStart = vertexOffset + i * 4; switch(codePoint) { @@ -267,67 +283,34 @@ namespace QuickMedia vertices[vertices_index][vertexStart + 2] = { sf::Vector2f(glyphPos.x + hspace, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices_index][vertexStart + 3] = { sf::Vector2f(glyphPos.x, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; glyphPos.x += hspace; - if(glyphPos.x > maxWidth * 0.5f) - { - lastSpacingWordWrapIndex = i; - lastSpacingAccumulatedOffset = glyphPos.x; - } + vertices_linear.push_back({vertices_index, vertexStart, 0, codePoint}); continue; } case '\t': { + const float char_width = hspace * TAB_WIDTH; vertices[vertices_index][vertexStart + 0] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; - vertices[vertices_index][vertexStart + 1] = { sf::Vector2f(glyphPos.x + hspace * TAB_WIDTH, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; - vertices[vertices_index][vertexStart + 2] = { sf::Vector2f(glyphPos.x + hspace * TAB_WIDTH, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; + vertices[vertices_index][vertexStart + 1] = { sf::Vector2f(glyphPos.x + char_width, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + vertices[vertices_index][vertexStart + 2] = { sf::Vector2f(glyphPos.x + char_width, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; vertices[vertices_index][vertexStart + 3] = { sf::Vector2f(glyphPos.x, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; - glyphPos.x += (hspace * TAB_WIDTH); - if(glyphPos.x > maxWidth * 0.5f) - { - lastSpacingWordWrapIndex = i; - lastSpacingAccumulatedOffset = glyphPos.x; - } + glyphPos.x += char_width; + vertices_linear.push_back({vertices_index, vertexStart, 0, codePoint}); continue; } case '\n': { - vertices[vertices_index][vertexStart + 0] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; - vertices[vertices_index][vertexStart + 1] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; - vertices[vertices_index][vertexStart + 2] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; - vertices[vertices_index][vertexStart + 3] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; glyphPos.x = 0.0f; glyphPos.y += floor(vspace + lineSpacing); + vertices[vertices_index][vertexStart + 0] = { sf::Vector2f(0.0f, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + vertices[vertices_index][vertexStart + 1] = { sf::Vector2f(0.0f, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + vertices[vertices_index][vertexStart + 2] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; + vertices[vertices_index][vertexStart + 3] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; + vertices_linear.push_back({vertices_index, vertexStart, 0, codePoint}); continue; } } const sf::Glyph &glyph = ff->getGlyph(codePoint, characterSize, false); - // TODO: Fix wrap-around with multiple textElements. Right now it only wrap-arounds within the same textElement, so with mixed latin-japanese it will - // wrap at character size rather than at whitespace - if(glyphPos.x + glyph.advance > maxWidth) - { - // If there was a space in the text and text width is too long, then we need to word wrap at space index instead, - // which means we need to change the position of all vertices after the space to the current vertex - if(lastSpacingWordWrapIndex != (size_t)-1) - { - for(size_t j = lastSpacingWordWrapIndex; j < i; ++j) - { - for(size_t k = 0; k < 4; ++k) - { - sf::Vector2f &vertexPos = vertices[vertices_index][vertexOffset + j * 4 + k].position; - vertexPos.x -= lastSpacingAccumulatedOffset; - vertexPos.y += floor(vspace + lineSpacing); - } - } - - glyphPos.x -= lastSpacingAccumulatedOffset; - lastSpacingWordWrapIndex = -1; - lastSpacingAccumulatedOffset = 0.0f; - } - else - glyphPos.x = 0.0f; - - glyphPos.y += floor(vspace + lineSpacing); - } sf::Vector2f vertexTopLeft(glyphPos.x + glyph.bounds.left, glyphPos.y + glyph.bounds.top); sf::Vector2f vertexTopRight(glyphPos.x + glyph.bounds.left + glyph.bounds.width, glyphPos.y + glyph.bounds.top); @@ -347,43 +330,110 @@ namespace QuickMedia vertices[vertices_index][vertexStart + 3] = { vertexBottomLeft, fontColor, textureBottomLeft }; glyphPos.x += glyph.advance + characterSpacing; + vertices_linear.push_back({vertices_index, vertexStart, 0, codePoint}); } - vertices[vertices_index][vertices[vertices_index].getVertexCount() - 4] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; - vertices[vertices_index][vertices[vertices_index].getVertexCount() - 3] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; - vertices[vertices_index][vertices[vertices_index].getVertexCount() - 2] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; - vertices[vertices_index][vertices[vertices_index].getVertexCount() - 1] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + //vertices[vertices_index][vertices[vertices_index].getVertexCount() - 4] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + //vertices[vertices_index][vertices[vertices_index].getVertexCount() - 3] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + //vertices[vertices_index][vertices[vertices_index].getVertexCount() - 2] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + //vertices[vertices_index][vertices[vertices_index].getVertexCount() - 1] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; prevCodePoint = 0; } - - boundingBox.height = glyphPos.y + lineSpacing; - boundingBox.height += vspace; - for(size_t vertices_index = 0; vertices_index < 2; ++vertices_index) { - usize numVertices = vertices[vertices_index].getVertexCount(); - for(usize i = 0; i < numVertices; i += 4) - { - const sf::Vertex &bottomRight = vertices[vertices_index][i + 2]; - boundingBox.width = std::max(boundingBox.width, bottomRight.position.x); + const float line_height = floor(vspace + lineSpacing); + float text_wrap_offset = 0.0f; + float text_offset_y = 0.0f; + int last_space_index = -1; + int num_lines = 1; + // TODO: Binary search? + for(int i = 0; i < (int)vertices_linear.size(); ++i) { + VertexRef &vertex_ref = vertices_linear[i]; + switch(vertex_ref.codepoint) { + case ' ': + case '\t': + last_space_index = i; + break; + case '\n': + text_wrap_offset = 0.0f; + last_space_index = -1; + ++num_lines; + break; + default: + break; + } + + sf::Vertex *vertex = &vertices[vertex_ref.vertices_index][vertex_ref.index]; + vertex[0].position.x -= text_wrap_offset; + vertex[1].position.x -= text_wrap_offset; + vertex[2].position.x -= text_wrap_offset; + vertex[3].position.x -= text_wrap_offset; + + vertex[0].position.y += text_offset_y; + vertex[1].position.y += text_offset_y; + vertex[2].position.y += text_offset_y; + vertex[3].position.y += text_offset_y; + vertex_ref.line = num_lines - 1; + + float vertex_right_side = get_text_quad_right_side(vertex_ref); + if(vertex_right_side > maxWidth) { + ++num_lines; + // TODO: Ignore line wrap on space + if(last_space_index != -1 && last_space_index != i) { + float vertex_left_side = get_text_quad_left_side(vertices_linear[last_space_index + 1]); + for(int j = last_space_index + 1; j <= i; ++j) { + VertexRef &vertex_ref_wrap = vertices_linear[j]; + sf::Vertex *vertex = &vertices[vertex_ref_wrap.vertices_index][vertex_ref_wrap.index]; + vertex[0].position.x -= vertex_left_side; + vertex[1].position.x -= vertex_left_side; + vertex[2].position.x -= vertex_left_side; + vertex[3].position.x -= vertex_left_side; + + vertex[0].position.y += line_height; + vertex[1].position.y += line_height; + vertex[2].position.y += line_height; + vertex[3].position.y += line_height; + + vertex_ref_wrap.line = num_lines - 1; + } + last_space_index = -1; + text_wrap_offset += vertex_left_side; + } else { + float vertex_left_side = get_text_quad_left_side(vertex_ref); + vertex[0].position.x -= vertex_left_side; + vertex[1].position.x -= vertex_left_side; + vertex[2].position.x -= vertex_left_side; + vertex[3].position.x -= vertex_left_side; + + vertex[0].position.y += line_height; + vertex[1].position.y += line_height; + vertex[2].position.y += line_height; + vertex[3].position.y += line_height; + + text_wrap_offset += vertex_left_side; + vertex_ref.line = num_lines - 1; + } + text_offset_y += line_height; } } + boundingBox.width = 0.0f; + for(VertexRef &vertex_ref : vertices_linear) { + boundingBox.width = std::max(boundingBox.width, get_text_quad_right_side(vertex_ref)); + } + boundingBox.height = num_lines * line_height; dirty = false; } - -#if 0 + + // TODO: Fix caret up/down navigation! its broken because of newlines void Text::updateCaret() { assert(!dirty && !dirtyText); - if(textElements.size() == 0) - { - float vspace = font->getLineSpacing(characterSize); + if(vertices_linear.empty()) { caretIndex = 0; - caretPosition = sf::Vector2f(0.0f, -vspace); + caretPosition = sf::Vector2f(0.0f, floor(font->getLineSpacing(characterSize))); return; } - switch(caretMoveDirection) { @@ -407,49 +457,41 @@ namespace QuickMedia caretIndex = getEndOfLine(caretIndex); break; } - default: + case CaretMoveDirection::NONE: // Ignore... break; } - - caretIndex = std::min(std::max(0, caretIndex), (int)textElements[0].text.size); - - usize vertexIndex = caretIndex * 4; - if(vertexIndex == 0) - { - float vspace = font->getLineSpacing(characterSize); - caretPosition = sf::Vector2f(0.0f, -vspace); - } - else - { - const sf::Vertex &topLeftVertex = vertices[vertexIndex]; - caretPosition = topLeftVertex.position; - } - } - bool Text::isCaretAtEnd() const - { - assert(!dirty && !dirtyText); - return textElements[0].text.size == 0 || caretIndex == (int)textElements[0].text.size; + if(caretIndex == (int)vertices_linear.size()) { + caretPosition.x = get_text_quad_right_side(vertices_linear.back()); + caretPosition.y = (1 + vertices_linear.back().line) * floor(font->getLineSpacing(characterSize) + lineSpacing); + } else if(caretIndex == 0) { + caretPosition = sf::Vector2f(0.0f, floor(font->getLineSpacing(characterSize))); + } else { + if(vertices_linear[caretIndex].codepoint == '\n') { + caretPosition.x = get_text_quad_right_side(vertices_linear[caretIndex - 1]); + caretPosition.y = (1 + vertices_linear[caretIndex - 1].line) * floor(font->getLineSpacing(characterSize) + lineSpacing); + } else { + caretPosition.x = get_text_quad_left_side(vertices_linear[caretIndex]); + caretPosition.y = (1 + vertices_linear[caretIndex].line) * floor(font->getLineSpacing(characterSize) + lineSpacing); + } + } } // TODO: This can be optimized by using binary search int Text::getStartOfLine(int startIndex) const { assert(!dirty && !dirtyText); - int numVertices = vertices.getVertexCount(); - if(numVertices < 4) return 0; - - usize vertexIndex = startIndex * 4; - const sf::Vertex &startTopLeftVertex = vertices[vertexIndex]; - int startRow = getRowByPosition(startTopLeftVertex.position); - for(int i = startIndex * 4; i > 0; i -= 4) - { - const sf::Vertex &topLeftVertex = vertices[i]; - int row = getRowByPosition(topLeftVertex.position); - if(row != startRow) - { - return std::max(0, i / 4 + 1); + const int num_vertices = vertices_linear.size(); + const int start_index_wrap = startIndex < num_vertices ? startIndex : num_vertices - 1; + int start_line = vertices_linear[start_index_wrap].line; + if(vertices_linear[start_index_wrap].codepoint == '\n') + --start_line; + for(int i = startIndex - 1; i >= 0; --i) { + if(vertices_linear[i].line != start_line) { + if(i + 2 <= num_vertices && vertices_linear[i + 1].codepoint == '\n') + return i + 2; + return i + 1; } } return 0; @@ -459,55 +501,46 @@ namespace QuickMedia int Text::getEndOfLine(int startIndex) const { assert(!dirty && !dirtyText); - int numVertices = vertices.getVertexCount(); - if(numVertices < 4) return 0; - - usize vertexIndex = startIndex * 4; - const sf::Vertex &startTopLeftVertex = vertices[vertexIndex]; - int startRow = getRowByPosition(startTopLeftVertex.position); - for(int i = startIndex * 4; i < numVertices; i += 4) - { - const sf::Vertex &topLeftVertex = vertices[i]; - int row = getRowByPosition(topLeftVertex.position); - if(row != startRow) - { - return std::max(0, i / 4 - 1); + const int num_vertices = vertices_linear.size(); + const int start_index_wrap = startIndex < num_vertices ? startIndex : num_vertices - 1; + int start_line = vertices_linear[start_index_wrap].line; + if(vertices_linear[start_index_wrap].codepoint == '\n') + return startIndex; + for(int i = startIndex + 1; i < (int)vertices_linear.size(); ++i) { + if(vertices_linear[i].line != start_line) { + if(vertices_linear[i].codepoint == '\n') + return i; + return i - 1; } } - return numVertices / 4; + return (int)vertices_linear.size(); } // TODO: This can be optimized by using binary search int Text::getPreviousLineClosestPosition(int startIndex) const { assert(!dirty && !dirtyText); - int numVertices = vertices.getVertexCount(); - if(numVertices < 4) return 0; - - usize vertexIndex = startIndex * 4; - const sf::Vertex &startTopLeftVertex = vertices[vertexIndex]; - int startRow = getRowByPosition(startTopLeftVertex.position); - int closestIndex = -1; - float closestAbsoluteDiffX = 0.0f; - for(int i = startIndex * 4; i >= 0; i -= 4) - { - const sf::Vertex &topLeftVertex = vertices[i]; - int row = getRowByPosition(topLeftVertex.position); - float absoluteDiffX = fabs(topLeftVertex.position.x - startTopLeftVertex.position.x); - int rowDiff = abs(row - startRow); - if(rowDiff > 1) - break; - - if(rowDiff == 1 && (closestIndex == -1 || absoluteDiffX < closestAbsoluteDiffX)) - { - closestIndex = i; - closestAbsoluteDiffX = absoluteDiffX; - } + const int num_vertices = vertices_linear.size(); + float start_left_pos; + if(startIndex == num_vertices) { + start_left_pos = get_text_quad_right_side(vertices_linear.back()); + if(vertices_linear.back().codepoint == '\n') + return getStartOfLine(startIndex - 1); + } else { + start_left_pos = get_text_quad_left_side(vertices_linear[startIndex]); + if(vertices_linear[startIndex].codepoint == '\n') + return getStartOfLine(startIndex - 1); + } + float closest_char = 999999.9f; + for(int i = getStartOfLine(startIndex) - 1; i >= 0; --i) { + //if(vertices_linear[i].codepoint == '\n') + // continue; + const float left_pos = get_text_quad_left_side(vertices_linear[i]); + const float pos_diff = std::abs(start_left_pos - left_pos); + if(pos_diff > closest_char) + return i + 1; + closest_char = pos_diff; } - - if(closestIndex != -1) - return closestIndex / 4; - return 0; } @@ -515,43 +548,31 @@ namespace QuickMedia int Text::getNextLineClosestPosition(int startIndex) const { assert(!dirty && !dirtyText); - int numVertices = vertices.getVertexCount(); - if(numVertices < 4) return 0; - - usize vertexIndex = startIndex * 4; - const sf::Vertex &startTopLeftVertex = vertices[vertexIndex]; - int startRow = getRowByPosition(startTopLeftVertex.position); - int closestIndex = -1; - float closestAbsoluteDiffX = 0.0f; - for(int i = startIndex * 4; i < numVertices; i += 4) - { - const sf::Vertex &topLeftVertex = vertices[i]; - int row = getRowByPosition(topLeftVertex.position); - float absoluteDiffX = fabs(topLeftVertex.position.x - startTopLeftVertex.position.x); - int rowDiff = abs(row - startRow); - if(rowDiff > 1) - break; - - if(rowDiff == 1 && (closestIndex == -1 || absoluteDiffX < closestAbsoluteDiffX)) - { - closestIndex = i; - closestAbsoluteDiffX = absoluteDiffX; - } + const int num_vertices = vertices_linear.size(); + float start_left_pos; + if(startIndex == num_vertices) { + return startIndex; + } else { + start_left_pos = get_text_quad_left_side(vertices_linear[startIndex]); + if(vertices_linear[startIndex].codepoint == '\n') + return startIndex + 1; } - - if(closestIndex != -1) - return closestIndex / 4; - - return numVertices / 4; - } - - int Text::getRowByPosition(const sf::Vector2f &position) const - { - assert(!dirty && !dirtyText); - const float vspace = font->getLineSpacing(characterSize); - return static_cast<int>(1.0f + position.y / (vspace + lineSpacing)); + float closest_char = 999999.9f; + for(int i = getEndOfLine(startIndex) + 1; i < (int)vertices_linear.size(); ++i) { + //if(vertices_linear[i].codepoint == '\n') + // continue; + const float left_pos = get_text_quad_left_side(vertices_linear[i]); + const float pos_diff = std::abs(start_left_pos - left_pos); + if(pos_diff > closest_char) + return i - 1; + closest_char = pos_diff; + } + return (int)vertices_linear.size(); } - + + // TODO: Optimize text editing by only processing the changed parts in updateGeometry. + // TODO: Split text into lines and add to vertices list so the lines that are cut off are not visible. This is good when using the text and as text input + // where there are a max number of rows shown at a time. void Text::processEvent(const sf::Event &event) { if(!editable) return; @@ -602,7 +623,7 @@ namespace QuickMedia } else if(event.key.code == sf::Keyboard::Return) { - if(sf::Keyboard::isKeyPressed(sf::Keyboard::LShift) || sf::Keyboard::isKeyPressed(sf::Keyboard::RShift)) + if(event.key.shift) { if(caretAtEnd) str += '\n'; @@ -650,24 +671,19 @@ namespace QuickMedia dirtyCaret = true; } } -#endif + bool Text::draw(sf::RenderTarget &target) { updateGeometry(); - -#if 0 + if(dirtyCaret || caretMoveDirection != CaretMoveDirection::NONE) { updateCaret(); dirtyCaret = false; caretMoveDirection = CaretMoveDirection::NONE; } -#endif - - float vspace = font->getLineSpacing(characterSize); sf::Vector2f pos = position; - pos.y += floor(vspace); // Origin is at bottom left, we want it to be at top left // TODO: Do not use maxWidth here. Max width might be set to 99999 and actual text width might be 200. Text width should be calculated instead //sf::FloatRect targetRect(0.0f, 0.0f, maxWidth, target.getSize().y); @@ -685,6 +701,9 @@ namespace QuickMedia } return false; } + + const float vspace = font->getLineSpacing(characterSize); + pos.y += floor(vspace); // Origin is at bottom left, we want it to be at top left if(!visible) { visible = true; @@ -700,21 +719,14 @@ namespace QuickMedia } lastSeenTimer.restart(); - pos.y -= floor(vspace); - -#if 0 if(!editable) return true; - - //float rows = floor(totalHeight / (vspace + lineSpacing)); - const float caretRow = getRowByPosition(caretPosition); - - sf::RectangleShape caretRect(sf::Vector2f(2.0f, floor(vspace))); - caretRect.setFillColor(sf::Color::White); - caretRect.setPosition(sf::Vector2f(floor(pos.x + caretPosition.x), floor(pos.y + caretRow * (vspace + lineSpacing)))); + pos.y -= floor(vspace * 2.0f); + + const float caret_margin = 2.0f; + + sf::RectangleShape caretRect(sf::Vector2f(2.0f, floor(vspace - caret_margin * 2.0f))); + caretRect.setPosition(floor(pos.x + caretPosition.x), floor(pos.y + caretPosition.y + caret_margin + 4.0f)); target.draw(caretRect); return true; -#else - return true; -#endif } } diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index d364b16..513a9fb 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -666,7 +666,10 @@ namespace QuickMedia { std::string formatted_body; bool contains_formatted_text = false; if(msgtype == MessageType::TEXT) { - string_split(body, '\n', [&formatted_body, &contains_formatted_text](const char *str, size_t size){ + int line = 0; + string_split(body, '\n', [&formatted_body, &contains_formatted_text, &line](const char *str, size_t size){ + if(line > 0) + formatted_body += "<br/>"; if(size > 0 && str[0] == '>') { std::string line(str, size); html_escape_sequences(line); @@ -677,6 +680,7 @@ namespace QuickMedia { } else { formatted_body.append(str, size); } + ++line; return true; }); } diff --git a/src/plugins/Plugin.cpp b/src/plugins/Plugin.cpp index f23175c..20c4b0a 100644 --- a/src/plugins/Plugin.cpp +++ b/src/plugins/Plugin.cpp @@ -35,11 +35,11 @@ namespace QuickMedia { void html_escape_sequences(std::string &str) { const std::array<HtmlEscapeSequence, 6> escape_sequences = { + HtmlEscapeSequence { '&', "&" }, // This should be first, to not accidentally replace a new sequence caused by replacing this HtmlEscapeSequence { '"', """ }, HtmlEscapeSequence { '\'', "'" }, HtmlEscapeSequence { '<', "<" }, - HtmlEscapeSequence { '>', ">" }, - HtmlEscapeSequence { '&', "&" } // This should be last, to not accidentally replace a new sequence caused by replacing this + HtmlEscapeSequence { '>', ">" } }; for(const HtmlEscapeSequence &escape_sequence : escape_sequences) { |