From 7a2cb2c4b81a8a0696d3a11ce8781542f181bb12 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 6 May 2018 17:15:51 +0200 Subject: Make dchat Text editable Not finished yet. Currently text can be entered, removed and you can move caret using arrow keys (up, down, left, right), home and end. Need to implement text selection and remove focus from chatbar when editing message board text. Chatbar should be replaced with dchat Text for proper multiline editable text. --- src/Cache.cpp | 42 +++++- src/Channel.cpp | 1 + src/ChannelSidePanel.cpp | 4 +- src/MessageBoard.cpp | 5 + src/Text.cpp | 325 +++++++++++++++++++++++++++++++++++++++++++---- src/UsersSidePanel.cpp | 4 +- 6 files changed, 346 insertions(+), 35 deletions(-) (limited to 'src') diff --git a/src/Cache.cpp b/src/Cache.cpp index cba346b..c795595 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -196,22 +196,32 @@ namespace dchat int exitStatus; if(it->process->try_get_exit_status(exitStatus)) { + ContentByUrlResult contentByUrlResult; bool failed = exitStatus != 0; - if(!failed) + if(failed) + { + contentByUrlResult = { (sf::Texture*)nullptr, ContentByUrlResult::Type::FAILED_DOWNLOAD }; + } + else { boost::filesystem::path filepath = getImagesDir(); odhtdb::Hash urlHash(it->url.data(), it->url.size()); filepath /= urlHash.toString(); - ContentByUrlResult contentByUrlResult = loadImageFromFile(filepath); - imageDownloadMutex.lock(); - contentUrlCache[it->url] = contentByUrlResult; - imageDownloadMutex.unlock(); + contentByUrlResult = loadImageFromFile(filepath); if(contentByUrlResult.type == ContentByUrlResult::Type::CACHED) { printf("Download content from url: %s\n", it->url.c_str()); } } + + imageDownloadMutex.lock(); + contentUrlCache[it->url] = contentByUrlResult; + boost::filesystem::path downloadingFilepath = getImagesDir() / ".downloading"; + // Intentionally ignore failure, program should not crash if we fail to remove these files... + boost::system::error_code err; + boost::filesystem::remove(downloadingFilepath, err); + imageDownloadMutex.unlock(); it = imageDownloadProcesses.erase(it); } else @@ -243,6 +253,18 @@ namespace dchat downloadWaitThread.join(); } + void replaceFileIgnoreError(const boost::filesystem::path &path) + { + try + { + fileReplace(path, StringView()); + } + catch(FileException &e) + { + fprintf(stderr, "Failed to replace file: %s, reason: %s\n", path.string().c_str(), e.what()); + } + } + const ContentByUrlResult Cache::getContentByUrl(const string &url, int downloadLimitBytes) { lock_guard lock(imageDownloadMutex); @@ -255,6 +277,15 @@ namespace dchat odhtdb::Hash urlHash(url.data(), url.size()); filepath /= urlHash.toString(); + boost::filesystem::path downloadingFilepath = getImagesDir() / ".downloading"; + if(boost::filesystem::exists(downloadingFilepath)) + { + // Intentionally ignore failure, program should not crash if we fail to remove these files... + boost::system::error_code err; + boost::filesystem::remove(filepath, err); + boost::filesystem::remove(downloadingFilepath, err); + } + ContentByUrlResult contentByUrlResult = loadImageFromFile(filepath); if(contentByUrlResult.type == ContentByUrlResult::Type::CACHED) { @@ -268,6 +299,7 @@ namespace dchat return contentByUrlResult; } + replaceFileIgnoreError(downloadingFilepath); ContentByUrlResult result((sf::Texture*)nullptr, ContentByUrlResult::Type::DOWNLOADING); contentUrlCache[url] = result; diff --git a/src/Channel.cpp b/src/Channel.cpp index f8e79c6..2bd1b0b 100644 --- a/src/Channel.cpp +++ b/src/Channel.cpp @@ -31,6 +31,7 @@ namespace dchat { Message *message = new Message(&systemUser, u8"pepedab https://discordemoji.com/assets/emoji/PepeDab.gif coggers https://discordemoji.com/assets/emoji/COGGERS.gif check out this url http://www.grandtournation.com/6808/start-date-of-the-grand-tour-season-3-confirmed-mark-your-calendars/ owo"); + message->text.setEditable(true); messageBoard.addMessage(message); } diff --git a/src/ChannelSidePanel.cpp b/src/ChannelSidePanel.cpp index 8d0b714..89a550a 100644 --- a/src/ChannelSidePanel.cpp +++ b/src/ChannelSidePanel.cpp @@ -33,7 +33,7 @@ namespace dchat { float posY = ChannelTopPanel::getHeight(); auto windowSize = window.getSize(); - sf::RectangleShape rect(sf::Vector2f(getWidth(), windowSize.y)); + sf::RectangleShape rect(sf::Vector2f(getWidth(), windowSize.y - ChannelTopPanel::getHeight())); rect.setPosition(0.0f, posY); rect.setFillColor(ColorScheme::getPanelColor()); window.draw(rect); @@ -68,6 +68,6 @@ namespace dchat float ChannelSidePanel::getWidth() { - return WIDTH * Settings::getScaling(); + return floor(WIDTH * Settings::getScaling()); } } diff --git a/src/MessageBoard.cpp b/src/MessageBoard.cpp index cee3e50..d73dc84 100644 --- a/src/MessageBoard.cpp +++ b/src/MessageBoard.cpp @@ -238,6 +238,11 @@ namespace dchat selectingTextStart.x = mousePos.x; selectingTextStart.y = mousePos.y; } + + for(Message *message : messages) + { + message->text.processEvent(event); + } } void MessageBoard::draw(sf::RenderWindow &window, Cache &cache) diff --git a/src/Text.cpp b/src/Text.cpp index bc23235..be147db 100644 --- a/src/Text.cpp +++ b/src/Text.cpp @@ -22,8 +22,13 @@ namespace dchat color(sf::Color::White), urlColor(URL_COLOR), dirty(false), + dirtyText(false), + dirtyCaret(false), plainText(false), - totalHeight(0.0f) + editable(false), + caretMoveDirection(CaretMoveDirection::NONE), + totalHeight(0.0f), + caretIndex(0) { } @@ -36,32 +41,28 @@ namespace dchat color(sf::Color::White), urlColor(URL_COLOR), dirty(true), + dirtyText(false), + dirtyCaret(false), plainText(_plainText), + editable(false), + caretMoveDirection(CaretMoveDirection::NONE), totalHeight(0.0f), - lineSpacing(0.0f) + lineSpacing(0.0f), + caretIndex(0) { setString(_str); } + void Text::setString(const sf::String &str) { if(str != this->str) { this->str = str; dirty = true; - textElements.clear(); - stringSplitElements(this->str, 0); + dirtyText = true; } } - void Text::appendStringNewLine(const sf::String &str) - { - usize prevSize = this->str.getSize(); - this->str += '\n'; - this->str += str; - dirty = true; - stringSplitElements(this->str, prevSize); - } - void Text::setPosition(float x, float y) { position.x = x; @@ -109,6 +110,20 @@ namespace dchat } } + 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 totalHeight; @@ -142,7 +157,7 @@ namespace dchat { StringViewUtf32 wholeStr(&stringToSplit[startIndex], stringToSplit.getSize() - startIndex); textElements.push_back({ wholeStr, TextElement::Type::TEXT }); - if(plainText) + if(plainText || editable) return; const char *httpStrRaw = "http://"; @@ -159,9 +174,9 @@ namespace dchat static_assert(sizeof(*parentheseStr.getData()) == sizeof(u32), "sf::String size has changed..."); std::vector newTextElements; - for(size_t i = 0; i < textElements.size(); ++i) + // Split emoji + for(const TextElement &textElement : textElements) { - TextElement textElement = textElements[i]; if(textElement.type != TextElement::Type::TEXT) { newTextElements.push_back(textElement); @@ -199,10 +214,10 @@ namespace dchat } textElements = newTextElements; + // Split http newTextElements.clear(); - for(size_t i = 0; i < textElements.size(); ++i) + for(const TextElement &textElement : textElements) { - TextElement textElement = textElements[i]; if(textElement.type != TextElement::Type::TEXT) { newTextElements.push_back(textElement); @@ -225,10 +240,10 @@ namespace dchat } textElements = newTextElements; + // Split https newTextElements.clear(); - for(size_t i = 0; i < textElements.size(); ++i) + for(const TextElement &textElement : textElements) { - TextElement textElement = textElements[i]; if(textElement.type != TextElement::Type::TEXT) { newTextElements.push_back(textElement); @@ -333,10 +348,13 @@ namespace dchat prevCodePoint = codePoint; glyphPos.x += kerning; + usize vertexStart = vertexOffset + i * 4; + switch(codePoint) { case ' ': { + vertices[vertexStart].position = sf::Vector2f(glyphPos.x, glyphPos.y - vspace); glyphPos.x += hspace; if(glyphPos.x > maxWidth * 0.5f) { @@ -347,6 +365,7 @@ namespace dchat } case '\t': { + vertices[vertexStart].position = sf::Vector2f(glyphPos.x, glyphPos.y - vspace); glyphPos.x += (hspace * TAB_WIDTH); if(glyphPos.x > maxWidth * 0.5f) { @@ -357,12 +376,14 @@ namespace dchat } case '\n': { + vertices[vertexStart].position = sf::Vector2f(glyphPos.x, glyphPos.y - vspace); glyphPos.x = 0.0f; glyphPos.y += vspace + lineSpacing; continue; } case '\v': { + vertices[vertexStart].position = sf::Vector2f(glyphPos.x, glyphPos.y - vspace); glyphPos.y += (vspace * TAB_WIDTH) + lineSpacing; continue; } @@ -408,10 +429,10 @@ namespace dchat sf::Color fontColor = (textElement.type == TextElement::Type::TEXT ? color : urlColor); - vertices[vertexOffset + i * 4 + 0] = { vertexTopLeft, fontColor, textureTopLeft }; - vertices[vertexOffset + i * 4 + 1] = { vertexTopRight, fontColor, textureTopRight }; - vertices[vertexOffset + i * 4 + 2] = { vertexBottomRight, fontColor, textureBottomRight }; - vertices[vertexOffset + i * 4 + 3] = { vertexBottomLeft, fontColor, textureBottomLeft }; + 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; } @@ -433,14 +454,256 @@ namespace dchat totalHeight = glyphPos.y + vspace + lineSpacing; } + void Text::updateCaret() + { + assert(!dirty && !dirtyText); + 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; + } + + usize vertexIndex = caretIndex * 4; + 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 step algorithm (jump to middle of vertices list and check if it's at next row, if not then divide by 2 again, and do this recursively) + int Text::getStartOfLine(int startIndex) const + { + assert(!dirty && !dirtyText); + int numVertices = vertices.getVertexCount(); + if(numVertices < 4) return 0; + + usize vertexIndex = caretIndex * 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 step algorithm (jump to middle of vertices list and check if it's at next row, if not then divide by 2 again, and do this recursively) + int Text::getEndOfLine(int startIndex) const + { + assert(!dirty && !dirtyText); + int numVertices = vertices.getVertexCount(); + if(numVertices < 4) return 0; + + usize vertexIndex = caretIndex * 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 step algorithm (jump to middle of vertices list and check if it's at next row, if not then divide by 2 again, and do this recursively) + int Text::getPreviousLineClosestPosition(int startIndex) const + { + assert(!dirty && !dirtyText); + int numVertices = vertices.getVertexCount(); + if(numVertices < 4) return 0; + + usize vertexIndex = caretIndex * 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 step algorithm (jump to middle of vertices list and check if it's at next row, if not then divide by 2 again, and do this recursively) + int Text::getNextLineClosestPosition(int startIndex) const + { + assert(!dirty && !dirtyText); + int numVertices = vertices.getVertexCount(); + if(numVertices < 4) return 0; + + usize vertexIndex = caretIndex * 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; + + 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 && !isCaretAtEnd()) + { + ++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); + setString(strBefore + strAfter); + --caretIndex; + dirtyCaret = true; + } + else if(event.key.code == sf::Keyboard::Delete && !isCaretAtEnd()) + { + 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.type == sf::Event::TextEntered) + { + if(event.text.unicode == 8 || event.text.unicode == 127) // backspace, del + return; + + if(isCaretAtEnd()) + str += event.text.unicode; + else + { + auto strBefore = str.substring(0, caretIndex); + auto strAfter = str.substring(caretIndex); + str = strBefore + event.text.unicode + strAfter; + } + + ++caretIndex; + dirty = true; + dirtyText = true; + dirtyCaret = true; + } + } + void Text::draw(sf::RenderTarget &target, Cache &cache) { + if(dirtyText) + { + textElements.clear(); + stringSplitElements(this->str, 0); + dirtyText = false; + } + if(dirty) { updateGeometry(); dirty = false; } + if(dirtyCaret || caretMoveDirection != CaretMoveDirection::NONE) + { + updateCaret(); + dirtyCaret = false; + caretMoveDirection = CaretMoveDirection::NONE; + } + float vspace = font->getLineSpacing(characterSize); sf::RenderStates states; @@ -458,6 +721,7 @@ namespace dchat states.texture = &font->getTexture(characterSize); target.draw(vertices, states); + pos.y -= floor(vspace); for(TextElement &textElement : textElements) { if(textElement.type == TextElement::Type::EMOJI) @@ -551,9 +815,8 @@ namespace dchat } case ContentByUrlResult::CachedType::WEB_PAGE_PREVIEW: { - const float previewWidth = floor(imageHeight * 1.77f); + const float previewWidth = fmin(maxWidth, floor(imageHeight * 1.77f)); - // No need to perform culling here, that is done in @Text draw function contentByUrlResult.webPagePreview->title.setCharacterSize(characterSize); contentByUrlResult.webPagePreview->title.setMaxWidth(previewWidth); contentByUrlResult.webPagePreview->title.setPosition(pos); @@ -573,5 +836,15 @@ namespace dchat } } } + + if(!editable) return; + + //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); } } diff --git a/src/UsersSidePanel.cpp b/src/UsersSidePanel.cpp index f665d1f..9c04062 100644 --- a/src/UsersSidePanel.cpp +++ b/src/UsersSidePanel.cpp @@ -20,7 +20,7 @@ namespace dchat { float posY = ChannelTopPanel::getHeight(); auto windowSize = window.getSize(); - sf::RectangleShape rect(sf::Vector2f(getWidth(), windowSize.y)); + sf::RectangleShape rect(sf::Vector2f(getWidth(), windowSize.y - ChannelTopPanel::getHeight())); rect.setFillColor(ColorScheme::getPanelColor()); rect.setPosition(windowSize.x - getWidth(), posY); window.draw(rect); @@ -45,6 +45,6 @@ namespace dchat float UsersSidePanel::getWidth() { - return WIDTH * Settings::getScaling(); + return floor(WIDTH * Settings::getScaling()); } } -- cgit v1.2.3