From 795cc3d873df13bfe2abaa56b17ea247bc892c20 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 20 Jul 2020 15:06:37 +0200 Subject: Word-wrap body text --- include/Body.hpp | 29 ++- include/Text.hpp | 126 +++++++++ include/types.hpp | 10 + src/Body.cpp | 25 +- src/QuickMedia.cpp | 14 +- src/Text.cpp | 663 +++++++++++++++++++++++++++++++++++++++++++++++ src/plugins/Fourchan.cpp | 3 +- 7 files changed, 847 insertions(+), 23 deletions(-) create mode 100644 include/Text.hpp create mode 100644 include/types.hpp create mode 100644 src/Text.cpp diff --git a/include/Body.hpp b/include/Body.hpp index 4c7044d..86b6984 100644 --- a/include/Body.hpp +++ b/include/Body.hpp @@ -1,5 +1,6 @@ #pragma once +#include "Text.hpp" #include #include #include @@ -12,18 +13,29 @@ namespace QuickMedia { class BodyItem { public: - BodyItem(std::string _title): visible(true), num_lines(1) { + BodyItem(std::string _title): visible(true), dirty(true) { set_title(std::move(_title)); } + BodyItem(const BodyItem &other) { + title = other.title; + url = other.url; + thumbnail_url = other.thumbnail_url; + attached_content_url = other.attached_content_url; + author = other.author; + visible = other.visible; + dirty = other.dirty; + if(other.title_text) + title_text = std::make_unique(*other.title_text); + else + title_text = nullptr; + replies = other.replies; + post_number = other.post_number; + } + void set_title(std::string new_title) { title = std::move(new_title); - // TODO: Optimize this - num_lines = 1; - for(char c : title) { - if(c == '\n') - ++num_lines; - } + dirty = true; } std::string title; @@ -32,9 +44,10 @@ namespace QuickMedia { std::string attached_content_url; std::string author; bool visible; + bool dirty; + std::unique_ptr title_text; // Used by image boards for example. The elements are indices to other body items std::vector replies; - int num_lines; std::string post_number; }; diff --git a/include/Text.hpp b/include/Text.hpp new file mode 100644 index 0000000..8b6c0b9 --- /dev/null +++ b/include/Text.hpp @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "types.hpp" +#include + +namespace QuickMedia +{ + struct StringViewUtf32 { + const u32 *data; + size_t size; + + StringViewUtf32() : data(nullptr), size(0) {} + StringViewUtf32(const u32 *data, usize size) : data(data), size(size) {} + + size_t find(const StringViewUtf32 &other, size_t offset = 0) const; + + u32 operator [] (usize index) const { + assert(index < size); + return data[index]; + } + }; + + struct TextElement + { + enum class Type + { + TEXT + }; + + TextElement() {} + TextElement(const StringViewUtf32 &_text, Type _type) : text(_text), type(_type), ownLine(false) {} + + StringViewUtf32 text; + sf::Vector2f position; + Type type; + bool ownLine; // Currently only used for emoji, to make emoji bigger when it's the only thing on a line + }; + + class Text + { + public: + Text(const sf::Font *font); + Text(const sf::String &str, const sf::Font *font, unsigned int characterSize, float maxWidth, bool plainText = true); + + void setString(const sf::String &str); + const sf::String& getString() const; + + void setPosition(float x, float y); + void setPosition(const sf::Vector2f &position); + sf::Vector2f getPosition() const; + + void setMaxWidth(float maxWidth); + float getMaxWidth() const; + + void setCharacterSize(unsigned int characterSize); + unsigned int getCharacterSize() const; + + const sf::Font* getFont() const; + + void setFillColor(sf::Color color); + void setLineSpacing(float lineSpacing); + void setCharacterSpacing(float characterSpacing); + void setEditable(bool editable); + + // Warning: won't update until @draw is called + float getHeight() const; + + void processEvent(const sf::Event &event); + + // Performs culling. @updateGeometry is called even if text is not visible if text is dirty, because updateGeometry might change the dimension of the text and make is visible. + // Returns true if text was drawn on screen (if text is within window borders) + bool draw(sf::RenderTarget &target); + + void updateGeometry(bool update_even_if_not_dirty = false); + private: + enum class CaretMoveDirection : u8 + { + NONE, + UP, + DOWN, + HOME, + END + }; + + void updateCaret(); + bool isCaretAtEnd() const; + int getStartOfLine(int startIndex) const; + int getEndOfLine(int startIndex) const; + int getRowByPosition(const sf::Vector2f &position) const; + + int getPreviousLineClosestPosition(int startIndex) const; + int getNextLineClosestPosition(int startIndex) const; + private: + sf::String str; + const sf::Font *font; + unsigned int characterSize; + sf::VertexArray vertices; + float maxWidth; + sf::Vector2f position; + sf::Color color; + sf::Color urlColor; + bool dirty; + bool dirtyText; + bool dirtyCaret; + bool plainText; + bool editable; + bool visible; + CaretMoveDirection caretMoveDirection; + sf::FloatRect boundingBox; + float lineSpacing; + float characterSpacing; + std::vector textElements; + + int caretIndex; + sf::Vector2f caretPosition; + sf::Clock lastSeenTimer; + sf::Vector2u renderTargetSize; + }; +} diff --git a/include/types.hpp b/include/types.hpp new file mode 100644 index 0000000..dc2a016 --- /dev/null +++ b/include/types.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +typedef intptr_t isize; +typedef uintptr_t usize; \ No newline at end of file diff --git a/src/Body.cpp b/src/Body.cpp index da7152d..6081028 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -133,7 +133,6 @@ namespace QuickMedia { // TODO: Load thumbnails with more than one thread. // TODO: Show chapters (rows) that have been read differently to make it easier to see what hasn't been read yet. void Body::draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size, const Json::Value &content_progress) { - const float font_height = title_text.getCharacterSize() + title_text.getLineSpacing() + 4.0f; const float image_max_height = 100.0f; const float spacing_y = 15.0f; const float padding_x = 10.0f; @@ -155,7 +154,6 @@ namespace QuickMedia { sf::RectangleShape selected_border; selected_border.setFillColor(sf::Color(0, 85, 119)); - const float selected_border_width = 5.0f; int num_items = items.size(); if(num_items == 0) @@ -169,6 +167,15 @@ namespace QuickMedia { for(auto &body_item : items) { // Intentionally create the item with the key item->thumbnail_url if it doesn't exist item_thumbnail_textures[body_item->thumbnail_url].referenced = true; + + if(body_item->dirty) { + body_item->dirty = false; + if(body_item->title_text) + body_item->title_text->setString(body_item->title); + else + body_item->title_text = std::make_unique(body_item->title, title_text.getFont(), 14, size.x - 50 - image_padding_x * 2.0f); + //body_item->title_text->updateGeometry(); + } } // Find the starting row that can be drawn to make selected row visible as well @@ -178,7 +185,7 @@ namespace QuickMedia { for(; first_visible_item >= 0; --first_visible_item) { auto &item = items[first_visible_item]; if(item->visible) { - float item_height = font_height * item->num_lines; + float item_height = item->title_text->getHeight(); if(!item->author.empty()) { item_height += author_text.getCharacterSize() + 2.0f; } @@ -212,7 +219,7 @@ namespace QuickMedia { if(!item->visible) continue; - float item_height = font_height * item->num_lines; + float item_height = item->title_text->getHeight(); if(!item->author.empty()) { item_height += author_text.getCharacterSize() + 2.0f; } @@ -297,9 +304,13 @@ namespace QuickMedia { item_pos.y += author_text.getCharacterSize() + 2.0f; } - title_text.setString(item->title); - title_text.setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y)); - window.draw(title_text); + //title_text.setString(item->title); + //title_text.setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y)); + //window.draw(title_text); + item->title_text->setString(item->title); + item->title_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 4.0f)); + item->title_text->setMaxWidth(size.x - text_offset_x - image_padding_x * 2.0f); + item->title_text->draw(window); // TODO: Do the same for non-manga content const Json::Value &item_progress = content_progress[item->title]; diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index eefc45a..7bb537b 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -703,13 +703,9 @@ namespace QuickMedia { window.clear(back_color); { - tab_spacing_rect.setPosition(0.0f, search_bar->getBottomWithoutShadow()); - tab_spacing_rect.setSize(sf::Vector2f(window_size.x, tab_spacer_height)); - window.draw(tab_spacing_rect); - - tab_drop_shadow.setSize(sf::Vector2f(window_size.x, 5.0f)); - tab_drop_shadow.setPosition(0.0f, std::floor(search_bar->getBottomWithoutShadow() + tab_height)); - window.draw(tab_drop_shadow); + //tab_spacing_rect.setPosition(0.0f, search_bar->getBottomWithoutShadow()); + //tab_spacing_rect.setSize(sf::Vector2f(window_size.x, tab_spacer_height)); + //window.draw(tab_spacing_rect); const float width_per_tab = window_size.x / tabs.size(); const float tab_y = tab_spacer_height + std::floor(search_bar->getBottomWithoutShadow() + tab_height * 0.5f - (tab_text_size + 5.0f) * 0.5f); @@ -730,6 +726,10 @@ namespace QuickMedia { window.draw(*tab.text); ++i; } + + tab_drop_shadow.setSize(sf::Vector2f(window_size.x, 5.0f)); + tab_drop_shadow.setPosition(0.0f, std::floor(search_bar->getBottomWithoutShadow() + tab_height)); + window.draw(tab_drop_shadow); } search_bar->draw(window, false); window.display(); diff --git a/src/Text.cpp b/src/Text.cpp new file mode 100644 index 0000000..da21b1d --- /dev/null +++ b/src/Text.cpp @@ -0,0 +1,663 @@ +#include "../include/Text.hpp" +#include +#include +#include +#include + +namespace QuickMedia +{ + const float TAB_WIDTH = 4.0f; + + const sf::Color URL_COLOR(15, 192, 252); + + size_t StringViewUtf32::find(const StringViewUtf32 &other, size_t offset) const { + if(offset >= size) + return -1; + + auto it = std::search(data + offset, data + size - offset, std::boyer_moore_searcher(other.data, other.data + other.size)); + if(it != data + size) + return it - data; + + return -1; + } + + Text::Text(const sf::Font *_font) : + font(_font), + characterSize(0), + vertices(sf::PrimitiveType::Quads), + maxWidth(0.0f), + color(sf::Color::White), + urlColor(URL_COLOR), + dirty(false), + dirtyText(false), + dirtyCaret(false), + plainText(false), + editable(false), + visible(true), + caretMoveDirection(CaretMoveDirection::NONE), + lineSpacing(0.0f), + characterSpacing(0.0f), + caretIndex(0) + { + + } + + Text::Text(const sf::String &_str, const sf::Font *_font, unsigned int _characterSize, float _maxWidth, bool _plainText) : + font(_font), + characterSize(_characterSize), + vertices(sf::PrimitiveType::Quads), + maxWidth(_maxWidth), + color(sf::Color::White), + urlColor(URL_COLOR), + dirty(true), + dirtyText(false), + dirtyCaret(false), + plainText(_plainText), + editable(false), + visible(true), + caretMoveDirection(CaretMoveDirection::NONE), + lineSpacing(0.0f), + characterSpacing(0.0f), + caretIndex(0) + { + setString(_str); + } + + void Text::setString(const sf::String &str) + { + if(str != this->str) + { + this->str = str; + dirty = true; + dirtyText = true; + if(str.getSize() < caretIndex) + { + caretIndex = str.getSize(); + dirtyCaret = true; + } + } + } + + const sf::String& Text::getString() const + { + return str; + } + + void Text::setPosition(float x, float y) + { + position.x = x; + position.y = y; + } + + void Text::setPosition(const sf::Vector2f &position) + { + this->position = position; + } + + sf::Vector2f Text::getPosition() const + { + return position; + } + + void Text::setMaxWidth(float maxWidth) + { + if(std::abs(maxWidth - this->maxWidth) > 1.0f) + { + this->maxWidth = maxWidth; + dirty = true; + } + } + + float Text::getMaxWidth() const + { + return maxWidth; + } + + void Text::setCharacterSize(unsigned int characterSize) + { + if(characterSize != this->characterSize) + { + this->characterSize = characterSize; + dirty = true; + } + } + + unsigned int Text::getCharacterSize() const + { + return characterSize; + } + + const sf::Font* Text::getFont() const + { + return font; + } + + void Text::setFillColor(sf::Color color) + { + if(color != this->color) + { + this->color = color; + dirty = true; + } + } + + void Text::setLineSpacing(float lineSpacing) + { + if(fabs(lineSpacing - this->lineSpacing) > 0.001f) + { + this->lineSpacing = lineSpacing; + dirty = true; + } + } + + void Text::setCharacterSpacing(float characterSpacing) + { + if(fabs(characterSpacing - this->characterSpacing) > 0.001f) + { + this->characterSpacing = characterSpacing; + dirty = true; + } + } + + void Text::setEditable(bool editable) + { + if(editable != this->editable) + { + this->editable = editable; + if(!plainText) + { + dirty = true; + dirtyText = true; + } + dirtyCaret = true; + } + } + + float Text::getHeight() const + { + return boundingBox.height; + } + + // Logic loosely based on https://github.com/SFML/SFML/wiki/Source:-CurvedText + void Text::updateGeometry(bool update_even_if_not_dirty) + { + if(!update_even_if_not_dirty && !dirty) + return; + + vertices.clear(); + float hspace = font->getGlyph(' ', characterSize, false).advance + characterSpacing; + float vspace = font->getLineSpacing(characterSize); + + boundingBox = sf::FloatRect(); + + sf::Vector2f glyphPos; + sf::Uint32 prevCodePoint = 0; + size_t lastSpacingWordWrapIndex = -1; + float lastSpacingAccumulatedOffset = 0.0f; + for(usize textElementIndex = 0; textElementIndex < textElements.size(); ++textElementIndex) + { + TextElement &textElement = textElements[textElementIndex]; + + usize vertexOffset = vertices.getVertexCount(); + vertices.resize(vertices.getVertexCount() + 4 * (textElement.text.size + 1)); + textElement.position = glyphPos; + for(size_t i = 0; i < textElement.text.size; ++i) + { + sf::Uint32 codePoint = textElement.text[i]; + float kerning = font->getKerning(prevCodePoint, codePoint, characterSize); + prevCodePoint = codePoint; + glyphPos.x += kerning; + + usize vertexStart = vertexOffset + i * 4; + + switch(codePoint) + { + case ' ': + { + vertices[vertexStart + 0] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + vertices[vertexStart + 1] = { sf::Vector2f(glyphPos.x + hspace, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + vertices[vertexStart + 2] = { sf::Vector2f(glyphPos.x + hspace, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; + vertices[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; + } + continue; + } + case '\t': + { + vertices[vertexStart + 0] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + vertices[vertexStart + 1] = { sf::Vector2f(glyphPos.x + hspace * TAB_WIDTH, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + vertices[vertexStart + 2] = { sf::Vector2f(glyphPos.x + hspace * TAB_WIDTH, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; + vertices[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; + } + continue; + } + case '\n': + { + vertices[vertexStart + 0] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + vertices[vertexStart + 1] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; + vertices[vertexStart + 2] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; + vertices[vertexStart + 3] = { sf::Vector2f(0.0f, glyphPos.y), sf::Color::Transparent, sf::Vector2f() }; + glyphPos.x = 0.0f; + glyphPos.y += floor(vspace + lineSpacing); + continue; + } + } + + const sf::Glyph &glyph = font->getGlyph(codePoint, characterSize, false); + 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 != -1) + { + for(size_t j = lastSpacingWordWrapIndex; j < i; ++j) + { + for(size_t k = 0; k < 4; ++k) + { + sf::Vector2f &vertexPos = vertices[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); + sf::Vector2f vertexBottomLeft(glyphPos.x + glyph.bounds.left, glyphPos.y + glyph.bounds.top + glyph.bounds.height); + sf::Vector2f vertexBottomRight(glyphPos.x + glyph.bounds.left + glyph.bounds.width, glyphPos.y + glyph.bounds.top + glyph.bounds.height); + + sf::Vector2f textureTopLeft(glyph.textureRect.left, glyph.textureRect.top); + sf::Vector2f textureTopRight(glyph.textureRect.left + glyph.textureRect.width, glyph.textureRect.top); + sf::Vector2f textureBottomLeft(glyph.textureRect.left, glyph.textureRect.top + glyph.textureRect.height); + sf::Vector2f textureBottomRight(glyph.textureRect.left + glyph.textureRect.width, glyph.textureRect.top + glyph.textureRect.height); + + sf::Color fontColor = (textElement.type == TextElement::Type::TEXT ? color : urlColor); + + vertices[vertexStart + 0] = { vertexTopLeft, fontColor, textureTopLeft }; + vertices[vertexStart + 1] = { vertexTopRight, fontColor, textureTopRight }; + vertices[vertexStart + 2] = { vertexBottomRight, fontColor, textureBottomRight }; + vertices[vertexStart + 3] = { vertexBottomLeft, fontColor, textureBottomLeft }; + + glyphPos.x += glyph.advance + characterSpacing; + } + + vertices[vertices.getVertexCount() - 4] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + vertices[vertices.getVertexCount() - 3] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + vertices[vertices.getVertexCount() - 2] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + vertices[vertices.getVertexCount() - 1] = { sf::Vector2f(glyphPos.x, glyphPos.y - vspace), sf::Color::Transparent, sf::Vector2f() }; + + prevCodePoint = 0; + } + + boundingBox.height = glyphPos.y + lineSpacing; + boundingBox.height += vspace; + + usize numVertices = vertices.getVertexCount(); + for(usize i = 0; i < numVertices; i += 4) + { + const sf::Vertex &bottomRight = vertices[i + 2]; + boundingBox.width = std::max(boundingBox.width, bottomRight.position.x); + } + + dirty = false; + } + + void Text::updateCaret() + { + assert(!dirty && !dirtyText); + if(textElements.size() == 0) + { + float vspace = font->getLineSpacing(characterSize); + caretIndex = 0; + caretPosition = sf::Vector2f(0.0f, -vspace); + return; + } + + switch(caretMoveDirection) + { + case CaretMoveDirection::UP: + { + caretIndex = getPreviousLineClosestPosition(caretIndex); + break; + } + case CaretMoveDirection::DOWN: + { + caretIndex = getNextLineClosestPosition(caretIndex); + break; + } + case CaretMoveDirection::HOME: + { + caretIndex = getStartOfLine(caretIndex); + break; + } + case CaretMoveDirection::END: + { + caretIndex = getEndOfLine(caretIndex); + break; + } + default: + // 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 == textElements[0].text.size; + } + + // 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); + } + } + return 0; + } + + // TODO: This can be optimized by using binary search + 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); + } + } + return numVertices / 4; + } + + // 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; + } + } + + if(closestIndex != -1) + return closestIndex / 4; + + return 0; + } + + // TODO: This can be optimized by using binary search + 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; + } + } + + 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(1.0f + position.y / (vspace + lineSpacing)); + } + + void Text::processEvent(const sf::Event &event) + { + if(!editable) return; + + bool caretAtEnd = textElements.size() == 0 || textElements[0].text.size == 0 || caretIndex == textElements[0].text.size; + + if(event.type == sf::Event::KeyPressed) + { + if(event.key.code == sf::Keyboard::Left && caretIndex > 0) + { + --caretIndex; + dirtyCaret = true; + } + else if(event.key.code == sf::Keyboard::Right && !caretAtEnd) + { + ++caretIndex; + dirtyCaret = true; + } + else if(event.key.code == sf::Keyboard::BackSpace && caretIndex > 0) + { + auto strBefore = str.substring(0, caretIndex - 1); + auto strAfter = str.substring(caretIndex); + --caretIndex; + setString(strBefore + strAfter); + dirtyCaret = true; + } + else if(event.key.code == sf::Keyboard::Delete && !caretAtEnd) + { + auto strBefore = str.substring(0, caretIndex); + auto strAfter = str.substring(caretIndex + 1); + setString(strBefore + strAfter); + } + else if(event.key.code == sf::Keyboard::Up) + { + caretMoveDirection = CaretMoveDirection::UP; + } + else if(event.key.code == sf::Keyboard::Down) + { + caretMoveDirection = CaretMoveDirection::DOWN; + } + else if(event.key.code == sf::Keyboard::Home) + { + caretMoveDirection = CaretMoveDirection::HOME; + } + else if(event.key.code == sf::Keyboard::End) + { + caretMoveDirection = CaretMoveDirection::END; + } + else if(event.key.code == sf::Keyboard::Return) + { + if(sf::Keyboard::isKeyPressed(sf::Keyboard::LShift) || sf::Keyboard::isKeyPressed(sf::Keyboard::RShift)) + { + if(caretAtEnd) + str += '\n'; + else + { + auto strBefore = str.substring(0, caretIndex); + auto strAfter = str.substring(caretIndex); + str = strBefore + '\n' + strAfter; + } + + ++caretIndex; + dirty = true; + dirtyText = true; + dirtyCaret = true; + } + } + } + else if(event.type == sf::Event::TextEntered) + { + if(event.text.unicode == 8 || event.text.unicode == 127) // backspace, del + return; + + sf::String stringToAdd; + if(event.text.unicode == 22) // ctrl+v + { + stringToAdd = sf::Clipboard::getString(); + } + else if(event.text.unicode >= 32 || event.text.unicode == 9) // 9 == tab + stringToAdd = event.text.unicode; + else + return; + + if(caretAtEnd) + str += stringToAdd; + else + { + auto strBefore = str.substring(0, caretIndex); + auto strAfter = str.substring(caretIndex); + str = strBefore + stringToAdd + strAfter; + } + + caretIndex += stringToAdd.getSize(); + dirty = true; + dirtyText = true; + dirtyCaret = true; + } + } + + bool Text::draw(sf::RenderTarget &target) + { + if(dirtyText) + { + textElements.clear(); + StringViewUtf32 wholeStr(this->str.getData(), this->str.getSize()); + textElements.push_back({ wholeStr, TextElement::Type::TEXT }); + dirtyText = false; + } + + updateGeometry(); + + if(dirtyCaret || caretMoveDirection != CaretMoveDirection::NONE) + { + updateCaret(); + dirtyCaret = false; + caretMoveDirection = CaretMoveDirection::NONE; + } + + float vspace = font->getLineSpacing(characterSize); + + sf::RenderStates states; + 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); + //sf::FloatRect textRect(pos.x, pos.y, maxWidth, ) + //colRect.contains() + //if(pos.x + maxWidth <= 0.0f || pos.x >= maxWidth || pos.y + totalHeight <= 0.0f || pos.y >= target.getSize().y) return; + renderTargetSize = target.getSize(); + if(pos.y + getHeight() <= 0.0f || pos.y >= renderTargetSize.y) + { + if(!editable && visible && lastSeenTimer.getElapsedTime().asMilliseconds() > 3000) + { + visible = false; + vertices.resize(0); + } + return false; + } + + if(!visible) + updateGeometry(true); + + states.transform.translate(pos); + states.texture = &font->getTexture(characterSize); + target.draw(vertices, states); + lastSeenTimer.restart(); + visible = true; + + pos.y -= floor(vspace); + + 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)))); + target.draw(caretRect); + return true; + } +} diff --git a/src/plugins/Fourchan.cpp b/src/plugins/Fourchan.cpp index 22a3faf..42bb54e 100644 --- a/src/plugins/Fourchan.cpp +++ b/src/plugins/Fourchan.cpp @@ -291,6 +291,7 @@ namespace QuickMedia { comment_text.back() = ' '; html_unescape_sequences(comment_text); // TODO: Do the same when wrapping is implemented + // TODO: Remove this int num_lines = 0; for(size_t i = 0; i < comment_text.size(); ++i) { if(comment_text[i] == '\n') { @@ -348,7 +349,7 @@ namespace QuickMedia { std::lock_guard lock(board_list_mutex); cached_thread_list_items.clear(); for(auto &body_item : body_items) { - cached_thread_list_items.push_back(std::make_unique(*body_item)); + cached_thread_list_items.push_back(std::move(body_item)); } } -- cgit v1.2.3