aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2020-07-20 15:06:37 +0200
committerdec05eba <dec05eba@protonmail.com>2020-07-20 15:06:37 +0200
commit795cc3d873df13bfe2abaa56b17ea247bc892c20 (patch)
tree45f5918e7778fe5793f8636747f2834ba01ef141
parentf52d48906f7fad95353da3cc7ddfe75b12106e4e (diff)
Word-wrap body text
-rw-r--r--include/Body.hpp29
-rw-r--r--include/Text.hpp126
-rw-r--r--include/types.hpp10
-rw-r--r--src/Body.cpp25
-rw-r--r--src/QuickMedia.cpp14
-rw-r--r--src/Text.cpp663
-rw-r--r--src/plugins/Fourchan.cpp3
7 files changed, 847 insertions, 23 deletions
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 <SFML/Graphics/Font.hpp>
#include <SFML/Graphics/Text.hpp>
#include <SFML/Graphics/Texture.hpp>
@@ -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<Text>(*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<Text> title_text;
// Used by image boards for example. The elements are indices to other body items
std::vector<size_t> 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 <SFML/Graphics/VertexArray.hpp>
+#include <SFML/Graphics/Font.hpp>
+#include <SFML/Graphics/RenderTarget.hpp>
+#include <SFML/Window/Event.hpp>
+#include <SFML/System/String.hpp>
+#include <SFML/System/Clock.hpp>
+#include <vector>
+#include "types.hpp"
+#include <assert.h>
+
+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<TextElement> 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 <stdint.h>
+
+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<Text>(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 <SFML/Graphics/RectangleShape.hpp>
+#include <SFML/Window/Clipboard.hpp>
+#include <cmath>
+#include <functional>
+
+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<int>(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<std::mutex> lock(board_list_mutex);
cached_thread_list_items.clear();
for(auto &body_item : body_items) {
- cached_thread_list_items.push_back(std::make_unique<BodyItem>(*body_item));
+ cached_thread_list_items.push_back(std::move(body_item));
}
}