From 047d55f263ce0dfa1b2b32e3ee172c4efff6dfe1 Mon Sep 17 00:00:00 2001 From: SMJSGaming Date: Sun, 1 Sep 2024 18:22:58 +0200 Subject: [PATCH] Added a text area remake to replace the old and unreliable SimpleTextArea --- loader/include/Geode/UI.hpp | 3 +- .../ui/{TextArea.hpp => SimpleTextArea.hpp} | 2 +- loader/include/Geode/ui/TextAreaV2.hpp | 241 +++++++++++ loader/src/ui/mods/ModsLayer.hpp | 2 +- loader/src/ui/mods/list/ModDeveloperItem.cpp | 1 - loader/src/ui/mods/list/ModList.cpp | 4 +- loader/src/ui/mods/list/ModList.hpp | 4 +- loader/src/ui/mods/list/ModProblemItem.cpp | 10 +- .../{TextArea.cpp => SimpleTextArea.cpp} | 2 +- loader/src/ui/nodes/TextAreaV2.cpp | 377 ++++++++++++++++++ 10 files changed, 632 insertions(+), 14 deletions(-) rename loader/include/Geode/ui/{TextArea.hpp => SimpleTextArea.hpp} (96%) create mode 100644 loader/include/Geode/ui/TextAreaV2.hpp rename loader/src/ui/nodes/{TextArea.cpp => SimpleTextArea.cpp} (99%) create mode 100644 loader/src/ui/nodes/TextAreaV2.cpp diff --git a/loader/include/Geode/UI.hpp b/loader/include/Geode/UI.hpp index cd76a8d2..0794c8ce 100644 --- a/loader/include/Geode/UI.hpp +++ b/loader/include/Geode/UI.hpp @@ -18,5 +18,6 @@ #include "ui/ScrollLayer.hpp" #include "ui/SelectList.hpp" #include "ui/Scrollbar.hpp" -#include "ui/TextArea.hpp" +#include "ui/TextAreaV2.hpp" +#include "ui/SimpleTextArea.hpp" #include "ui/TextRenderer.hpp" diff --git a/loader/include/Geode/ui/TextArea.hpp b/loader/include/Geode/ui/SimpleTextArea.hpp similarity index 96% rename from loader/include/Geode/ui/TextArea.hpp rename to loader/include/Geode/ui/SimpleTextArea.hpp index 05b1f6da..8d564e3f 100644 --- a/loader/include/Geode/ui/TextArea.hpp +++ b/loader/include/Geode/ui/SimpleTextArea.hpp @@ -22,7 +22,7 @@ namespace geode { * * Contact me on Discord (\@smjs) if you have any questions, suggestions or bugs. */ - class GEODE_DLL SimpleTextArea : public cocos2d::CCNode { + class GEODE_DLL [[deprecated("Use TextArea instead")]] SimpleTextArea : public cocos2d::CCNode { public: static SimpleTextArea* create(const std::string& text, const std::string& font = "chatFont.fnt", const float scale = 1); static SimpleTextArea* create(const std::string& text, const std::string& font, const float scale, const float width); diff --git a/loader/include/Geode/ui/TextAreaV2.hpp b/loader/include/Geode/ui/TextAreaV2.hpp new file mode 100644 index 00000000..a9372e54 --- /dev/null +++ b/loader/include/Geode/ui/TextAreaV2.hpp @@ -0,0 +1,241 @@ +#pragma once + +#include +#include "PaddingNode.hpp" + +namespace geode { + class GEODE_DLL TextAreaV2 : public PaddingNode { + public: + enum class WrappingMode { + // Doesn't wrap the text and completely ignores bounds + NoWrap, + // Wraps the text on the last special character (spaces included) before the width is exceeded + WordWrap, + // Wraps the text on the last space before the width is exceeded + SpaceWrap, + // Wraps the text on the exact character that exceeds the width + CutoffWrap + }; + + enum class Alignment { + // Aligns the text to the left + Left, + // Aligns the text in the center + Center, + // Aligns the text to the right + Right + }; + + struct Line { + std::string text; + std::string overflow; + cocos2d::CCLabelBMFont* label; + int lineNumber; + float currentHeight; + bool isLastLine; + }; + + static TextAreaV2* create(const std::string& text, const std::string& font, const float scale = 1, const float width = -1, const float height = -1, const bool deferUpdates = false); + + /** + * Sets the font of the text area + */ + void setFont(const std::string& font); + /** + * Gets the font of the text area + */ + std::string getFont() const; + /** + * Sets the text of the text area + */ + void setText(const std::string& text); + /** + * Gets the text of the text area + */ + std::string getText() const; + /** + * Sets the color of the text in the text area + */ + void setTextColor(const cocos2d::ccColor4B& color); + /** + * Gets the color of the text in the text area + */ + cocos2d::ccColor4B getTextColor() const; + /** + * Sets horizontal the alignment of the text in the text area + */ + void setAlignment(const Alignment alignment); + /** + * Gets the horizontal alignment of the text in the text area + */ + Alignment getAlignment() const; + /** + * Sets the wrapping mode of the text in the text area + */ + void setWrappingMode(const WrappingMode mode); + /** + * Gets the wrapping mode of the text in the text area + */ + WrappingMode getWrappingMode() const; + /** + * Sets the maximum number of lines in the text area + * + * @note If this is set to a value smaller than 0, it will stop keeping track of the lines and leave either all the lines or as many as the max height allows + */ + void setMaxLines(const int maxLines); + /** + * Gets the maximum number of lines in the text area + */ + int getMaxLines() const; + /** + * Sets the scale of the text in the text area + */ + void setTextScale(const float scale); + /** + * Gets the scale of the text in the text area + */ + float getTextScale() const; + /** + * Sets the padding between lines in the text area + */ + void setLinePadding(const float padding); + /** + * Gets the padding between lines in the text area + */ + float getLinePadding() const; + /** + * Sets a minimum bound for the width of the text area + * + * @note If this is set to a value smaller than 0, it will stop rescaling the container and leave it at the max width + */ + void setMinWidth(const float width); + /** + * Resets the minimum width to prevent it from rescaling the container + */ + void resetMinWidth(); + /** + * Gets the min width of the text area + */ + float getMinWidth() const; + /** + * Sets the max width of the text area to define the wrapping bounds + * + * @note If this is set to a value smaller than 0, it will stop rescaling the container and leave it as big as the text or the min width + */ + void setMaxWidth(const float width); + /** + * Resets the maximum width to prevent it from rescaling the container + */ + void resetMaxWidth(); + /** + * Gets the max width of the text area + */ + float getMaxWidth() const; + /** + * Sets a minimum bound for the height of the text area + * + * @note If this is set to a value smaller than 0, it will stop rescaling the container and leave it at the max height + */ + void setMinHeight(const float height); + /** + * Resets the minimum height to prevent it from rescaling the container + */ + void resetMinHeight(); + /** + * Gets the min height of the text area + */ + float getMinHeight() const; + /** + * Sets the max height of the text area to define the maximum number of lines through a height limit + * + * @note If this is set to a value smaller than 0, it will stop rescaling the container and leave it as big as the lines or the min height + */ + void setMaxHeight(const float height); + /** + * Resets the maximum height to prevent it from rescaling the container + */ + void resetMaxHeight(); + /** + * Gets the max height of the text area + */ + float getMaxHeight() const; + /** + * Resets the minimum bounds for the width and height + */ + void resetMinBounds(); + /** + * Resets the maximum bounds for the width and height + */ + void resetMaxBounds(); + /** + * Sets whether the text should be hyphenated + */ + void setHyphenate(const bool hyphenate); + /** + * Gets whether the text should be hyphenated + */ + bool getHyphenate() const; + /** + * Sets whether the text area should show an ellipsis when the text overflows + * + * @note This will only show an ellipsis if the last line doesn't end with an ellipsis already + */ + void setEllipsis(const bool ellipsis); + /** + * Gets whether the text area should show an ellipsis when the text overflows + */ + bool getEllipsis() const; + /** + * Sets whether the text area should defer updates until the next frame + * + * @warning If set to true, all nodes won't immediately update and will only update on the next frame. This can cause side effects if not accounted for + */ + void setDeferUpdates(const bool defer); + /** + * Gets whether the text area should defer updates until the next frame + */ + bool getDeferUpdates() const; + /** + * Gets the height of a line including padding + */ + float getLineHeight() const; + /** + * Gets the labels of the text area + */ + std::vector getLines() const; + protected: + std::string m_font; + std::string m_text; + cocos2d::ccColor4B m_textColor; + Alignment m_alignment; + WrappingMode m_wrappingMode; + int m_maxLines; + float m_textScale; + float m_linePadding; + float m_minWidth; + float m_maxWidth; + float m_minHeight; + float m_maxHeight; + bool m_hyphenate; + bool m_ellipsis; + bool m_deferUpdates; + bool m_deferred; + std::vector m_lines; + + TextAreaV2(const std::string& text, const std::string& font, const float scale, const float width, const float height, const bool deferUpdates); + bool init() override; + void update(); + void updatePadding() override; + void updateContainer(const float dt = 0); + void updateLineAlignment(cocos2d::CCLabelBMFont* line); + void addEllipsis(Line& line); + bool isWidthOverflowing(const cocos2d::CCLabelBMFont* line); + size_t getOverflowAmount(cocos2d::CCLabelBMFont* line, const size_t lineSize); + Line createLine(const std::string& text, Line& previousLine); + std::vector createLines(); + std::vector createNotWrap(const Line& reference); + std::vector createCutoffWrap(const std::string& text, Line& reference); + std::vector createDelimitedWrap(const std::string& text, Line& reference, const std::string& delimiters); + std::vector wrapper(const std::string& text, Line& reference, const std::function(Line& currentLine)>& onOverflow); + }; +} \ No newline at end of file diff --git a/loader/src/ui/mods/ModsLayer.hpp b/loader/src/ui/mods/ModsLayer.hpp index 7e7de630..da860786 100644 --- a/loader/src/ui/mods/ModsLayer.hpp +++ b/loader/src/ui/mods/ModsLayer.hpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include #include diff --git a/loader/src/ui/mods/list/ModDeveloperItem.cpp b/loader/src/ui/mods/list/ModDeveloperItem.cpp index 5d490810..3cc0cc69 100644 --- a/loader/src/ui/mods/list/ModDeveloperItem.cpp +++ b/loader/src/ui/mods/list/ModDeveloperItem.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include diff --git a/loader/src/ui/mods/list/ModList.cpp b/loader/src/ui/mods/list/ModList.cpp index 8ec95174..3f7153da 100644 --- a/loader/src/ui/mods/list/ModList.cpp +++ b/loader/src/ui/mods/list/ModList.cpp @@ -323,9 +323,9 @@ bool ModList::init(ModListSource* src, CCSize const& size) { m_statusDetailsBtn->setID("status-details-button"); m_statusContainer->addChild(m_statusDetailsBtn); - m_statusDetails = SimpleTextArea::create("", "chatFont.fnt", .6f); + m_statusDetails = TextAreaV2::create("", "chatFont.fnt", .6f); m_statusDetails->setID("status-details-input"); - m_statusDetails->setAlignment(kCCTextAlignmentCenter); + m_statusDetails->setAlignment(TextAreaV2::Alignment::Center); m_statusContainer->addChild(m_statusDetails); m_statusLoadingCircle = createLoadingCircle(50); diff --git a/loader/src/ui/mods/list/ModList.hpp b/loader/src/ui/mods/list/ModList.hpp index 21b72dcc..380f6814 100644 --- a/loader/src/ui/mods/list/ModList.hpp +++ b/loader/src/ui/mods/list/ModList.hpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include #include @@ -26,7 +26,7 @@ protected: ScrollLayer* m_list; CCMenu* m_statusContainer; CCLabelBMFont* m_statusTitle; - SimpleTextArea* m_statusDetails; + TextAreaV2* m_statusDetails; CCMenuItemSpriteExtra* m_statusDetailsBtn; CCNode* m_statusLoadingCircle; Slider* m_statusLoadingBar; diff --git a/loader/src/ui/mods/list/ModProblemItem.cpp b/loader/src/ui/mods/list/ModProblemItem.cpp index a1522ef5..1f809be7 100644 --- a/loader/src/ui/mods/list/ModProblemItem.cpp +++ b/loader/src/ui/mods/list/ModProblemItem.cpp @@ -11,7 +11,7 @@ #include #include #include -#include +#include #include #include #include @@ -57,15 +57,15 @@ bool ModProblemItem::init(Mod* source, LoadProblem problem, CCSize const& size) CCPoint { 10.0f, 0.0f } ); - auto label = SimpleTextArea::create( + auto label = TextAreaV2::create( message.c_str(), "bigFont.fnt" ); - label->setWrappingMode(WrappingMode::SPACE_WRAP); + label->setWrappingMode(TextAreaV2::WrappingMode::SpaceWrap); label->setAnchorPoint({ 0.0f, 0.5f }); label->setMaxLines(4); if (this->showFixButton() || this->showInfoButton()) { - label->setWidth(size.width * 0.7f); + label->setMaxWidth(size.width * 0.7f); auto helpMenu = CCMenu::create(); helpMenu->setAnchorPoint({ 1.0f, 0.5f }); @@ -92,7 +92,7 @@ bool ModProblemItem::init(Mod* source, LoadProblem problem, CCSize const& size) // Left + Right + Space between constexpr float paddings = 30.0f; float calc = size.width - paddings - icon->getScaledContentWidth(); - label->setWidth(calc); + label->setMaxWidth(calc); } label->setScale(0.4f); this->addChildAtPosition( diff --git a/loader/src/ui/nodes/TextArea.cpp b/loader/src/ui/nodes/SimpleTextArea.cpp similarity index 99% rename from loader/src/ui/nodes/TextArea.cpp rename to loader/src/ui/nodes/SimpleTextArea.cpp index c46384ff..2d85fb06 100644 --- a/loader/src/ui/nodes/TextArea.cpp +++ b/loader/src/ui/nodes/SimpleTextArea.cpp @@ -1,4 +1,4 @@ -#include +#include using namespace geode::prelude; diff --git a/loader/src/ui/nodes/TextAreaV2.cpp b/loader/src/ui/nodes/TextAreaV2.cpp new file mode 100644 index 00000000..0d5d5fb1 --- /dev/null +++ b/loader/src/ui/nodes/TextAreaV2.cpp @@ -0,0 +1,377 @@ +#include +#include + +using namespace geode::prelude; + +#define IMPL_GETTER(type, name, methodName) \ + type geode::TextAreaV2::get##methodName() const { return m_##name; } +#define IMPL_GETTER_SETTER(type, paramType, name, methodName) \ + void geode::TextAreaV2::set##methodName(const paramType name) { m_##name = name; this->update(); } \ + IMPL_GETTER(type, name, methodName) + +geode::TextAreaV2* geode::TextAreaV2::create(const std::string& text, const std::string& font, const float scale, const float width, const float height, const bool deferUpdates) { + TextAreaV2* area = new TextAreaV2(text, font, scale, width, height, deferUpdates); + + if (area && area->init()) { + area->autorelease(); + + return area; + } else { + CC_SAFE_DELETE(area); + + return nullptr; + } +} + +geode::TextAreaV2::TextAreaV2(const std::string& text, const std::string& font, const float scale, const float width, const float height, const bool deferUpdates) : + PaddingNode(CCNode::create()), + m_text(text), + m_font(font), + m_textColor({ 255, 255, 255, 255 }), + m_alignment(Alignment::Left), + m_wrappingMode(WrappingMode::WordWrap), + m_maxLines(-1), + m_textScale(scale), + m_linePadding(0), + m_minWidth(-1), + m_maxWidth(width), + m_minHeight(-1), + m_maxHeight(height), + m_hyphenate(true), + m_ellipsis(true), + m_deferUpdates(deferUpdates), + m_deferred(false) { } + +bool geode::TextAreaV2::init() { + if (!PaddingNode::init()) { + return false; + } + + this->update(); + + return true; +} + +IMPL_GETTER_SETTER(std::string, std::string&, font, Font) +IMPL_GETTER_SETTER(std::string, std::string&, text, Text) +IMPL_GETTER_SETTER(ccColor4B, ccColor4B&, textColor, TextColor) +IMPL_GETTER_SETTER(geode::TextAreaV2::Alignment, Alignment, alignment, Alignment) +IMPL_GETTER_SETTER(geode::TextAreaV2::WrappingMode, WrappingMode, wrappingMode, WrappingMode) +IMPL_GETTER_SETTER(int, int, maxLines, MaxLines) +IMPL_GETTER_SETTER(float, float, textScale, TextScale) +IMPL_GETTER_SETTER(float, float, linePadding, LinePadding) +IMPL_GETTER_SETTER(float, float, minWidth, MinWidth) +IMPL_GETTER_SETTER(float, float, maxWidth, MaxWidth) +IMPL_GETTER_SETTER(float, float, minHeight, MinHeight) +IMPL_GETTER_SETTER(float, float, maxHeight, MaxHeight) +IMPL_GETTER_SETTER(bool, bool, hyphenate, Hyphenate) +IMPL_GETTER_SETTER(bool, bool, ellipsis, Ellipsis) +IMPL_GETTER(bool, deferUpdates, DeferUpdates) +IMPL_GETTER(std::vector, lines, Lines) + +void geode::TextAreaV2::resetMinWidth() { + this->setMinWidth(-1); +} + +void geode::TextAreaV2::resetMaxWidth() { + this->setMaxWidth(-1); +} + +void geode::TextAreaV2::resetMinHeight() { + this->setMinHeight(-1); +} + +void geode::TextAreaV2::resetMaxHeight() { + this->setMaxHeight(-1); +} + +void geode::TextAreaV2::resetMinBounds() { + this->resetMinWidth(); + this->resetMinHeight(); +} + +void geode::TextAreaV2::resetMaxBounds() { + this->resetMaxWidth(); + this->resetMaxHeight(); +} + +void geode::TextAreaV2::setDeferUpdates(const bool defer) { + m_deferUpdates = defer; + + // If already deferred, update the container immediately and unschedule the deferred update + if (m_deferred) { + this->unschedule(schedule_selector(TextAreaV2::updateContainer)); + + this->updateContainer(); + } +} + +float geode::TextAreaV2::getLineHeight() const { + return m_lines.empty() ? 0 : m_lines.front().label->getScaledContentHeight() + m_linePadding; +} + +void geode::TextAreaV2::update() { + if (!m_deferred) { + if (m_deferUpdates) { + m_deferred = true; + + this->scheduleOnce(schedule_selector(TextAreaV2::updateContainer), 0); + } else { + this->updateContainer(); + } + } +} + +void geode::TextAreaV2::updatePadding() { + this->update(); +} + +void geode::TextAreaV2::updateContainer(const float dt) { + PaddingNode::updatePadding(); + m_container->removeAllChildren(); + + m_deferred = false; + m_lines = this->createLines(); + + const bool upperBoundWidth = m_maxWidth >= 0; + const bool upperBoundHeight = m_maxHeight >= 0; + const bool artificialWidth = m_minWidth >= 0 || upperBoundWidth; + const bool artificialHeight = m_minHeight >= 0 || upperBoundHeight; + const bool inheritedNodeSize = this->getContentSize() == this->getPaddedContainerSize(); + float height = m_lines.empty() ? 0 : m_lines.back().currentHeight; + float width = 0; + + // First determine the container size before manipulating the node anchors and true positions + for (const Line& line : m_lines) { + width = std::max(width, line.label->getScaledContentWidth()); + } + + if (artificialWidth) { + width = std::max(m_minWidth, width); + + m_container->setContentWidth(upperBoundWidth ? std::min(m_maxWidth, width) : width); + } else { + m_container->setContentWidth(width); + } + + if (artificialHeight) { + height = std::max(m_minHeight, height); + + m_container->setContentHeight(upperBoundHeight ? std::min(m_maxHeight, height) : height); + } else { + m_container->setContentHeight(height); + } + + if (inheritedNodeSize) { + this->setContentSize(this->getPaddedContainerSize()); + } + + for (const Line& line : m_lines) { + this->updateLineAlignment(line.label); + + // Correct the Y position to be relative to the container height + line.label->setPositionY(m_container->getContentHeight() - line.currentHeight + line.label->getScaledContentHeight()); + m_container->addChild(line.label); + } +} + +void geode::TextAreaV2::updateLineAlignment(CCLabelBMFont* line) { + switch (m_alignment) { + case Alignment::Left: + line->setAnchorPoint({ 0, 1 }); + line->setPositionX(0); + break; + case Alignment::Center: + line->setAnchorPoint({ 0.5f, 1 }); + line->setPositionX(m_container->getContentWidth() / 2); + break; + case Alignment::Right: + line->setAnchorPoint({ 1, 1 }); + line->setPositionX(m_container->getContentWidth()); + break; + } +} + +void geode::TextAreaV2::addEllipsis(Line& line) { + if (!m_ellipsis || line.lineNumber == -1) { + return; + } + + line.text = line.text.find_first_not_of(' ') == std::string::npos ? + "..." : + line.text + std::string("...").substr(std::min(3, line.text.size() - line.text.find_last_not_of('.') - 1)); + + line.label->setString(line.text.c_str()); + + if (this->isWidthOverflowing(line.label)) { + const size_t lineSize = line.text.size(); + const size_t overflow = this->getOverflowAmount(line.label, lineSize + 3); + + line.overflow = line.text.substr(lineSize - overflow - 3, overflow) + line.overflow; + + line.label->setString((line.text = line.text.substr(0, lineSize - overflow - 3) + "...").c_str()); + } +} + +bool geode::TextAreaV2::isWidthOverflowing(const CCLabelBMFont* line) { + return m_wrappingMode != WrappingMode::NoWrap && m_maxWidth >= 0 && line->getScaledContentWidth() + this->getTotalPaddingX() > m_maxWidth; +} + +size_t geode::TextAreaV2::getOverflowAmount(CCLabelBMFont* line, const size_t lineSize) { + for (size_t overflow = 1; overflow < lineSize; overflow++) { + CCNode* character = cocos::getChild(line, lineSize - overflow); + + if ((character->getPositionX() - character->getContentWidth() / 2) * m_textScale <= m_maxWidth) { + return overflow; + } + } + + return 0; +} + +geode::TextAreaV2::Line geode::TextAreaV2::createLine(const std::string& text, Line& previousLine) { + CCLabelBMFont* line = CCLabelBMFont::create(text.c_str(), m_font.c_str()); + + line->setScale(m_textScale); + line->setColor({ m_textColor.r, m_textColor.g, m_textColor.b }); + line->setOpacity(m_textColor.a); + + const size_t lineSize = text.size(); + const float currentHeight = m_linePadding + previousLine.currentHeight + line->getScaledContentHeight(); + const size_t overflow = this->isWidthOverflowing(line) ? this->getOverflowAmount(line, lineSize) : 0; + const size_t overflowStart = lineSize - overflow; + const std::string finalText = text.substr(0, overflowStart); + const bool exceededLines = (m_maxLines != -1 && previousLine.lineNumber + 1 >= m_maxLines) || + (m_maxHeight != -1 && currentHeight + this->getTotalPaddingY() > m_maxHeight); + + if (overflow > 0) { + line->setString(finalText.c_str()); + } + + previousLine.isLastLine = exceededLines; + + return { + .text = finalText, + .overflow = text.substr(overflowStart, overflow), + .label = line, + .lineNumber = previousLine.lineNumber + 1, + .currentHeight = currentHeight, + .isLastLine = false + }; +} + +std::vector geode::TextAreaV2::createLines() { + Line placeholderLine = { + .lineNumber = -1, + .currentHeight = -m_linePadding + }; + + switch (m_wrappingMode) { + case WrappingMode::NoWrap: return this->createNotWrap(placeholderLine); + case WrappingMode::WordWrap: return this->createDelimitedWrap(m_text, placeholderLine, " `~!@#$%^&*()-_=+[{}];:'\",<.>/?\\|"); + case WrappingMode::SpaceWrap: return this->createDelimitedWrap(m_text, placeholderLine, " "); + case WrappingMode::CutoffWrap: return this->createCutoffWrap(m_text, placeholderLine); + } +} + +std::vector geode::TextAreaV2::createNotWrap(const Line& reference) { + Line previousLine = reference; + std::stringstream stream(m_text); + std::vector lines; + std::string line; + + while (std::getline(stream, line)) { + const Line currentLine = this->createLine(line, previousLine); + + if (previousLine.isLastLine) { + this->addEllipsis(previousLine); + + break; + } else { + lines.push_back(previousLine = currentLine); + } + } + + return lines; +} + +std::vector geode::TextAreaV2::createCutoffWrap(const std::string& text, Line& reference) { + return this->wrapper(text, reference, [this](Line& currentLine) { + if (currentLine.text.empty()) return std::vector(); + + if (m_hyphenate && currentLine.text.back() != '-') { + currentLine.overflow = currentLine.text.back() + currentLine.overflow; + currentLine.text.pop_back(); + currentLine.text = currentLine.text.substr(0, currentLine.text.find_last_not_of(' ') + 1) + "-"; + + currentLine.label->setString(currentLine.text.c_str()); + } + + return this->createCutoffWrap(currentLine.overflow.erase(0, currentLine.overflow.find_first_not_of(' ')), currentLine); + }); +} + +std::vector geode::TextAreaV2::createDelimitedWrap(const std::string& text, Line& reference, const std::string& delimiters) { + return this->wrapper(text, reference, [this, delimiters](Line& currentLine) { + if (currentLine.text.empty()) return std::vector(); + + const size_t lineSize = currentLine.text.size(); + size_t additionalOverflow = 0; + + while (additionalOverflow < lineSize && delimiters.find(currentLine.text[lineSize - additionalOverflow - 1]) == std::string::npos) { + additionalOverflow++; + } + + if (additionalOverflow == lineSize) additionalOverflow = 0; + + currentLine.overflow = currentLine.text.substr(lineSize - additionalOverflow) + currentLine.overflow; + currentLine.text = currentLine.text.substr(0, lineSize - additionalOverflow); + + if (m_hyphenate && currentLine.text.back() != '-') { + if (additionalOverflow == 0) { + currentLine.overflow = currentLine.text.back() + currentLine.overflow; + currentLine.text.pop_back(); + } + + currentLine.text = currentLine.text.substr(0, currentLine.text.find_last_not_of(' ') + 1) + "-"; + } + + currentLine.label->setString(currentLine.text.c_str()); + + return this->createDelimitedWrap(currentLine.overflow.erase(0, currentLine.overflow.find_first_not_of(' ')), currentLine, delimiters); + }); +} + +std::vector geode::TextAreaV2::wrapper(const std::string& text, Line& reference, const std::function(Line& currentLine)>& onOverflow) { + Line previousLine = reference; + std::stringstream stream(text); + std::vector lines; + std::string line; + + while (std::getline(stream, line)) { + Line currentLine = this->createLine(line, previousLine); + + if (previousLine.isLastLine) { + if (previousLine.lineNumber == reference.lineNumber) { + reference.isLastLine = true; + + this->addEllipsis(reference); + } else { + this->addEllipsis(previousLine); + } + + break; + } else if (currentLine.overflow.empty()) { + lines.push_back(previousLine = currentLine); + } else { + const std::vector overflowedLines = onOverflow(currentLine); + + lines.push_back(currentLine); + lines.insert(lines.end(), overflowedLines.begin(), overflowedLines.end()); + + if ((previousLine = lines.back()).isLastLine) break; + } + } + + return lines; +} \ No newline at end of file