diff --git a/loader/include/Geode/ui/TextArea.hpp b/loader/include/Geode/ui/TextArea.hpp index 9366a1ec2..9298caa7c 100644 --- a/loader/include/Geode/ui/TextArea.hpp +++ b/loader/include/Geode/ui/TextArea.hpp @@ -6,6 +6,8 @@ #include namespace geode { + class SimpleTextAreaImpl; + enum WrappingMode { NO_WRAP, WORD_WRAP, @@ -24,6 +26,7 @@ namespace geode { * * Contact me on Discord (\@smjs) if you have any questions, suggestions or bugs. */ + class GEODE_DLL SimpleTextArea : public cocos2d::CCNode { public: static SimpleTextArea* create(std::string text, std::string font = "chatFont.fnt", float scale = 1.0f); @@ -37,7 +40,7 @@ namespace geode { cocos2d::CCTextAlignment getAlignment(); void setWrappingMode(WrappingMode mode); WrappingMode getWrappingMode(); - void setText(std::string text); + virtual void setText(std::string text); std::string getText(); void setMaxLines(size_t maxLines); size_t getMaxLines(); @@ -50,17 +53,204 @@ namespace geode { std::vector getLines(); float getHeight(); float getLineHeight(); + protected: SimpleTextArea(); ~SimpleTextArea() override; - + + virtual std::unique_ptr createImpl(); + std::unique_ptr m_impl; + + bool init(std::string font, std::string text, float scale, float width, const bool artificialWidth); private: static SimpleTextArea* create(std::string font, std::string text, float scale, float width, const bool artificialWidth); + }; - bool init(std::string font, std::string text, float scale, float width, const bool artificialWidth); + // abb2k wuz here :) + + template + class RichTextKey; + + class RichTextKeyInstanceBase { + public: + virtual ~RichTextKeyInstanceBase() = default; + virtual void applyChangesToSprite(cocos2d::CCFontSprite* spr, int localIndex, int index) = 0; + virtual std::string getKey() const = 0; + virtual bool isCancellation() const = 0; + virtual std::string runStrAddition() = 0; + virtual bool isButton() const = 0; + virtual void callButton(bool keyDown, cocos2d::CCFontSprite* spr, std::set const& word) = 0; + }; + + template + class RichTextKeyInstance final : public RichTextKeyInstanceBase { + public: + RichTextKeyInstance(RichTextKey* key, T data, bool cancellation) + : m_key(std::move(key)), m_value(std::move(data)), m_cancellation(std::move(cancellation)) {} + + RichTextKey* m_key = nullptr; + T m_value; + bool m_cancellation = false; + + void applyChangesToSprite(cocos2d::CCFontSprite* spr, int localIndex, int index) override { + if (m_key->m_applyToSprite != NULL) + m_key->m_applyToSprite(m_value, spr, localIndex, index); + } + + std::string getKey() const override { + return m_key->getKey(); + } + + bool isCancellation() const override { + return m_cancellation; + } + + std::string runStrAddition() override { + if (m_key->m_stringAddition != NULL) + return m_key->m_stringAddition(m_value); + return ""; + } + + bool isButton() const override { + return m_key->m_buttonFunctionallity != NULL; + } + + void callButton(bool keyDown, cocos2d::CCFontSprite* spr, std::set const& word){ + if (m_key->m_buttonFunctionallity != NULL) + m_key->m_buttonFunctionallity(m_value, keyDown, spr, word); + } + }; + + class RichTextKeyBase { + public: + virtual ~RichTextKeyBase() = default; + virtual Result> createInstance(std::string const& value, bool cancellation) = 0; + virtual std::string getKey() const = 0; + }; + + template + class RichTextKey final : public RichTextKeyBase { + public: + /** + @param key The identifier name for this rich text key + @param validCheck Function to validate and parse the value string into type T (if an error is returned the key will not be processed) + @param applyToSprite Function to apply the parsed value to a font sprite (optional) + */ + RichTextKey( + std::string key, + geode::Function(std::string const& value)> validCheck, + geode::Function const& wordClicked)> buttonFunctionallity + ) : m_key(std::move(key)), + m_validCheck(std::move(validCheck)), + m_buttonFunctionallity(std::move(buttonFunctionallity)) {} + /** + @param key The identifier name for this rich text key + @param validCheck Function to validate and parse the value string into type T (if an error is returned the key will not be processed) + @param applyToSprite Function to apply the parsed value to a font sprite (optional) + */ + RichTextKey( + std::string key, + geode::Function(std::string const& value)> validCheck, + geode::Function applyToSprite + ) : m_key(std::move(key)), + m_validCheck(std::move(validCheck)), + m_applyToSprite(std::move(applyToSprite)) {} + /** + @param key The identifier name for this rich text key + @param validCheck Function to validate and parse the value string into type T (if an error is returned the key will not be processed) + @param stringAddition Function to add a new string at the point where the key is (optional) + */ + RichTextKey( + std::string key, + geode::Function(std::string const& value)> validCheck, + geode::Function stringAddition + ) : m_key(std::move(key)), + m_validCheck(std::move(validCheck)), + m_stringAddition(std::move(stringAddition)) {} + /** + @param key The identifier name for this rich text key + @param validCheck Function to validate and parse the value string into type T (if an error is returned the key will not be processed) + @param applyToSprite Function to apply the parsed value to a font sprite (optional) + @param stringAddition Function to add a new string at the point where the key is (optional) + */ + RichTextKey( + std::string key, + geode::Function(std::string const& value)> validCheck, + geode::Function applyToSprite, + geode::Function stringAddition + ) : m_key(std::move(key)), + m_validCheck(std::move(validCheck)), + m_applyToSprite(std::move(applyToSprite)), + m_stringAddition(std::move(stringAddition)) {} + + Result> createInstance(std::string const& value, bool cancellation) override { + if (cancellation){ + if (value == ""){ + return Ok(std::make_shared>( + RichTextKeyInstance(this, T(), true)) + ); + } + else return Err("Cancellation tags cannot have values"); + } + + auto res = m_validCheck(value); + + if (res.isErr()) return Err(res.unwrapErr()); + + return Ok(std::make_shared>( + RichTextKeyInstance(this, res.unwrap(), false)) + ); + } + + std::string getKey() const override { + return m_key; + } + + private: + std::string m_key; + + public: + geode::Function(std::string const& value)> m_validCheck = NULL; + geode::Function m_applyToSprite = NULL; + geode::Function m_stringAddition = NULL; + geode::Function const& wordClicked)> m_buttonFunctionallity = NULL; + }; + + + class GEODE_DLL RichTextArea : public SimpleTextArea, public cocos2d::CCTouchDelegate { + public: + static RichTextArea* create(std::string text, std::string font = "chatFont.fnt", float scale = 1.0f); + static RichTextArea* create(std::string text, std::string font, float scale, float width); + + void setText(std::string text) override; + std::string getRawText(); + + void registerRichTextKey(std::shared_ptr key); + protected: + RichTextArea(); + ~RichTextArea(); + + class RichImpl; + std::unique_ptr createImpl() override; private: - class Impl; - std::unique_ptr m_impl; + static RichTextArea* create(std::string font, std::string text, float scale, float width, const bool artificialWidth); + + bool init(std::string font, std::string text, float scale, float width, const bool artificialWidth); + + bool ccTouchBegan(cocos2d::CCTouch *pTouch, cocos2d::CCEvent *pEvent) override; + + void ccTouchEnded(cocos2d::CCTouch *pTouch, cocos2d::CCEvent *pEvent) override; + void ccTouchCancelled(cocos2d::CCTouch *pTouch, cocos2d::CCEvent *pEvent) override; + + RichImpl* castedImpl(); }; } diff --git a/loader/src/ui/nodes/TextArea.cpp b/loader/src/ui/nodes/TextArea.cpp index 120b0215b..1b0081d26 100644 --- a/loader/src/ui/nodes/TextArea.cpp +++ b/loader/src/ui/nodes/TextArea.cpp @@ -1,45 +1,52 @@ #include +#include +#include -using namespace geode::prelude; -class SimpleTextArea::Impl { +class geode::SimpleTextAreaImpl { public: bool m_shouldUpdate = false; bool m_artificialWidth = false; cocos2d::CCMenu* m_container = nullptr; std::string m_font; std::string m_text; - std::vector m_lines; + std::vector m_lines; cocos2d::ccColor4B m_color = { 0xFF, 0xFF, 0xFF, 0xFF }; cocos2d::CCTextAlignment m_alignment = cocos2d::kCCTextAlignmentLeft; - WrappingMode m_wrappingMode = WORD_WRAP; + geode::WrappingMode m_wrappingMode = geode::WrappingMode::WORD_WRAP; size_t m_maxLines = 0; float m_scale = 1.f; float m_lineHeight = 0.f; float m_linePadding = 0.f; - SimpleTextArea* m_self = nullptr; + geode::SimpleTextArea* m_self = nullptr; + + SimpleTextAreaImpl(geode::SimpleTextArea* self) : m_self(self) {} + + cocos2d::CCLabelBMFont* createLabel(char const* text, float top); - Impl(SimpleTextArea* self) : m_self(self) {} + float calculateOffset(cocos2d::CCLabelBMFont* label); + + virtual void charIteration(geode::FunctionRef overflowHandling); - CCLabelBMFont* createLabel(char const* text, float top); - float calculateOffset(CCLabelBMFont* label); - void charIteration(geode::FunctionRef overflowHandling); void updateLinesNoWrap(); + void updateLinesWordWrap(bool spaceWrap); + void updateLinesCutoffWrap(); + void updateContainer(); }; -CCLabelBMFont* SimpleTextArea::Impl::createLabel(char const* text, float top) { +cocos2d::CCLabelBMFont* geode::SimpleTextAreaImpl::createLabel(char const* text, float top) { if (m_maxLines && m_lines.size() >= m_maxLines) { - CCLabelBMFont* last = m_lines.at(m_maxLines - 1); + cocos2d::CCLabelBMFont* last = m_lines.at(m_maxLines - 1); std::string_view textv = last->getString(); last->setString(fmt::format("{}...", textv.substr(0, textv.size() - 3)).c_str()); return nullptr; } else { - CCLabelBMFont* label = CCLabelBMFont::create(text, m_font.c_str()); + cocos2d::CCLabelBMFont* label = cocos2d::CCLabelBMFont::create(text, m_font.c_str()); label->setScale(m_scale); label->setPosition({ 0, top }); @@ -50,14 +57,14 @@ CCLabelBMFont* SimpleTextArea::Impl::createLabel(char const* text, float top) { } } -float SimpleTextArea::Impl::calculateOffset(CCLabelBMFont* label) { +float geode::SimpleTextAreaImpl::calculateOffset(cocos2d::CCLabelBMFont* label) { return m_linePadding + label->getContentSize().height * m_scale; } -void SimpleTextArea::Impl::charIteration(geode::FunctionRef overflowHandling) { +void geode::SimpleTextAreaImpl::charIteration(geode::FunctionRef overflowHandling) { float top = 0; m_lines.clear(); - CCLabelBMFont* line = createLabel("", top); + cocos2d::CCLabelBMFont* line = createLabel("", top); m_lines = { line }; for (const char c : m_text) { @@ -83,14 +90,14 @@ void SimpleTextArea::Impl::charIteration(geode::FunctionRef/?\\|"); if (delimiters.find(c) == std::string_view::npos) { const std::string& text = line->getString(); const size_t position = text.find_last_of(delimiters) + 1; - CCLabelBMFont* newLine = createLabel((text.substr(position) + c).c_str(), top); + cocos2d::CCLabelBMFont* newLine = createLabel((text.substr(position) + c).c_str(), top); if (newLine != nullptr) { line->setString(text.substr(0, position).c_str()); @@ -122,12 +129,12 @@ void SimpleTextArea::Impl::updateLinesWordWrap(bool spaceWrap) { }); } -void SimpleTextArea::Impl::updateLinesCutoffWrap() { - charIteration([this](CCLabelBMFont* line, char c, float top) { +void geode::SimpleTextAreaImpl::updateLinesCutoffWrap() { + charIteration([this](cocos2d::CCLabelBMFont* line, char c, float top) { const std::string& text = line->getString(); const char back = text.back(); const bool lastIsSpace = back == ' '; - CCLabelBMFont* newLine = createLabel(std::string(!lastIsSpace, back).append(std::string(c != ' ', c)).c_str(), top); + cocos2d::CCLabelBMFont* newLine = createLabel(std::string(!lastIsSpace, back).append(std::string(c != ' ', c)).c_str(), top); if (newLine == nullptr && !lastIsSpace) { if (text[text.size() - 2] == ' ') { @@ -138,21 +145,22 @@ void SimpleTextArea::Impl::updateLinesCutoffWrap() { } return newLine; - }); +}); + } -void SimpleTextArea::Impl::updateContainer() { +void geode::SimpleTextAreaImpl::updateContainer() { switch (m_wrappingMode) { - case NO_WRAP: { + case geode::NO_WRAP: { updateLinesNoWrap(); } break; - case WORD_WRAP: { + case geode::WORD_WRAP: { updateLinesWordWrap(false); } break; - case SPACE_WRAP: { + case geode::SPACE_WRAP: { updateLinesWordWrap(true); } break; - case CUTOFF_WRAP: { + case geode::CUTOFF_WRAP: { updateLinesCutoffWrap(); } break; } @@ -172,19 +180,19 @@ void SimpleTextArea::Impl::updateContainer() { m_container->setContentSize(m_self->getContentSize()); m_container->removeAllChildren(); - for (CCLabelBMFont* line : m_lines) { + for (cocos2d::CCLabelBMFont* line : m_lines) { const float y = height + line->getPositionY(); switch (m_alignment) { - case kCCTextAlignmentLeft: { + case cocos2d::kCCTextAlignmentLeft: { line->setAnchorPoint({ 0, 1 }); line->setPosition({ 0, y }); } break; - case kCCTextAlignmentCenter: { + case cocos2d::kCCTextAlignmentCenter: { line->setAnchorPoint({ 0.5f, 1 }); line->setPosition({ width / 2, y }); } break; - case kCCTextAlignmentRight: { + case cocos2d::kCCTextAlignmentRight: { line->setAnchorPoint({ 1, 1 }); line->setPosition({ width, y }); } break; @@ -194,26 +202,26 @@ void SimpleTextArea::Impl::updateContainer() { } } -SimpleTextArea::SimpleTextArea() : m_impl(std::make_unique(this)) {} -SimpleTextArea::~SimpleTextArea() = default; +geode::SimpleTextArea::SimpleTextArea() = default; +geode::SimpleTextArea::~SimpleTextArea() = default; -SimpleTextArea* SimpleTextArea::create(std::string text, std::string font, float scale) { +geode::SimpleTextArea* geode::SimpleTextArea::create(std::string text, std::string font, float scale) { return SimpleTextArea::create( std::move(font), std::move(text), scale, - CCDirector::sharedDirector()->getWinSize().width / 2, + cocos2d::CCDirector::sharedDirector()->getWinSize().width / 2, false ); } -SimpleTextArea* SimpleTextArea::create(std::string text, std::string font, float scale, float width) { +geode::SimpleTextArea* geode::SimpleTextArea::create(std::string text, std::string font, float scale, float width) { return SimpleTextArea::create( std::move(font), std::move(text), scale, width, true ); } -SimpleTextArea* SimpleTextArea::create(std::string font, std::string text, float scale, float width, bool artificialWidth) { +geode::SimpleTextArea* geode::SimpleTextArea::create(std::string font, std::string text, float scale, float width, bool artificialWidth) { SimpleTextArea* instance = new SimpleTextArea(); - instance->m_impl = std::make_unique(instance); + instance->m_impl = instance->createImpl(); if (instance->init(std::move(font), std::move(text), scale, width, artificialWidth)) { instance->autorelease(); @@ -224,12 +232,12 @@ SimpleTextArea* SimpleTextArea::create(std::string font, std::string text, float return nullptr; } -bool SimpleTextArea::init(std::string font, std::string text, float scale, float width, bool artificialWidth) { +bool geode::SimpleTextArea::init(std::string font, std::string text, float scale, float width, bool artificialWidth) { m_impl->m_font = std::move(font); m_impl->m_text = std::move(text); m_impl->m_scale = scale; m_impl->m_artificialWidth = artificialWidth; - m_impl->m_container = CCMenu::create(); + m_impl->m_container = cocos2d::CCMenu::create(); this->setAnchorPoint({ 0.5f, 0.5f }); m_impl->m_container->setPosition({ 0, 0 }); @@ -241,61 +249,61 @@ bool SimpleTextArea::init(std::string font, std::string text, float scale, float return true; } -void SimpleTextArea::setFont(std::string font) { +void geode::SimpleTextArea::setFont(std::string font) { m_impl->m_font = std::move(font); m_impl->updateContainer(); } -std::string SimpleTextArea::getFont() { +std::string geode::SimpleTextArea::getFont() { return m_impl->m_font; } -void SimpleTextArea::setColor(const ccColor4B& color) { +void geode::SimpleTextArea::setColor(const cocos2d::ccColor4B& color) { m_impl->m_color = color; m_impl->updateContainer(); } -ccColor4B SimpleTextArea::getColor() { +cocos2d::ccColor4B geode::SimpleTextArea::getColor() { return m_impl->m_color; } -void SimpleTextArea::setAlignment(CCTextAlignment alignment) { +void geode::SimpleTextArea::setAlignment(cocos2d::CCTextAlignment alignment) { m_impl->m_alignment = alignment; m_impl->updateContainer(); } -CCTextAlignment SimpleTextArea::getAlignment() { +cocos2d::CCTextAlignment geode::SimpleTextArea::getAlignment() { return m_impl->m_alignment; } -void SimpleTextArea::setWrappingMode(WrappingMode mode) { +void geode::SimpleTextArea::setWrappingMode(WrappingMode mode) { m_impl->m_wrappingMode = mode; m_impl->updateContainer(); } -WrappingMode SimpleTextArea::getWrappingMode() { +geode::WrappingMode geode::SimpleTextArea::getWrappingMode() { return m_impl->m_wrappingMode; } -void SimpleTextArea::setText(std::string text) { +void geode::SimpleTextArea::setText(std::string text) { m_impl->m_text = std::move(text); m_impl->updateContainer(); } -std::string SimpleTextArea::getText() { +std::string geode::SimpleTextArea::getText() { return m_impl->m_text; } -void SimpleTextArea::setMaxLines(size_t maxLines) { +void geode::SimpleTextArea::setMaxLines(size_t maxLines) { m_impl->m_maxLines = maxLines; m_impl->updateContainer(); } -size_t SimpleTextArea::getMaxLines() { +size_t geode::SimpleTextArea::getMaxLines() { return m_impl->m_maxLines; } -void SimpleTextArea::setWidth(float width) { +void geode::SimpleTextArea::setWidth(float width) { m_impl->m_artificialWidth = true; m_impl->updateContainer(); @@ -303,36 +311,462 @@ void SimpleTextArea::setWidth(float width) { m_impl->m_container->setContentSize(this->getContentSize()); } -float SimpleTextArea::getWidth() { +float geode::SimpleTextArea::getWidth() { return m_impl->m_container->getContentSize().width; } -void SimpleTextArea::setScale(float scale) { +void geode::SimpleTextArea::setScale(float scale) { m_impl->m_scale = scale; m_impl->updateContainer(); } -float SimpleTextArea::getScale() { +float geode::SimpleTextArea::getScale() { return m_impl->m_scale; } -void SimpleTextArea::setLinePadding(float padding) { +void geode::SimpleTextArea::setLinePadding(float padding) { m_impl->m_linePadding = padding; m_impl->updateContainer(); } -float SimpleTextArea::getLinePadding() { +float geode::SimpleTextArea::getLinePadding() { return m_impl->m_linePadding; } -std::vector SimpleTextArea::getLines() { +std::vector geode::SimpleTextArea::getLines() { return m_impl->m_lines; } -float SimpleTextArea::getHeight() { +float geode::SimpleTextArea::getHeight() { return m_impl->m_container->getContentSize().height; } -float SimpleTextArea::getLineHeight() { +float geode::SimpleTextArea::getLineHeight() { return m_impl->m_lineHeight; } + +inline std::unique_ptr geode::SimpleTextArea::createImpl() { + return std::make_unique(this); +} + +using namespace geode::prelude; + +class RichTextArea::RichImpl : public SimpleTextAreaImpl { +public: + RichTextArea* m_self = nullptr; + RichImpl(RichTextArea* self) : m_self(self), SimpleTextAreaImpl(self) {} + + std::map> m_richTextKeys; + std::map>> m_richTextInstances; + std::map, std::set> m_charactersForButton{}; + std::shared_ptr m_currentlyHeldButton = nullptr; + + std::string m_rawText; + + std::map m_ogColorForLink{}; + + void charIteration(geode::FunctionRef overflowHandling) override; + void formatRichText(); + + void processLinkClick( + std::string const& link, + bool keyDown, + cocos2d::CCFontSprite* specificSpriteClicked, + std::set const& wordClicked + ); +}; + +RichTextArea* RichTextArea::create(std::string text, std::string font, float scale) { + return RichTextArea::create(std::move(font), std::move(text), scale, CCDirector::sharedDirector()->getWinSize().width / 2, false); +} + +RichTextArea* RichTextArea::create(std::string text, std::string font, float scale, float width) { + return RichTextArea::create(std::move(font), std::move(text), scale, width, true); +} + +RichTextArea* RichTextArea::create(std::string font, std::string text, float scale, float width, bool artificialWidth) { + RichTextArea* instance = new RichTextArea(); + instance->m_impl = instance->createImpl(); + + if (instance->init(std::move(font), std::move(text), scale, width, artificialWidth)) { + instance->autorelease(); + return instance; + } + + delete instance; + return nullptr; +} + +bool RichTextArea::init(std::string font, std::string text, float scale, float width, bool artificialWidth) { + CCTouchDispatcher::get()->addTargetedDelegate(this, 0, true); + + if (!SimpleTextArea::init(font, text, scale, width, artificialWidth)) return false; + + registerRichTextKey(std::make_shared>( + "color", + [](std::string value) -> Result { + auto colorRes = cc3bFromHexString(value); + if (colorRes.isErr()) return Err(colorRes.unwrapErr()); + + return Ok(colorRes.unwrap()); + }, + [](ccColor3B const& value, cocos2d::CCFontSprite* sprite, int localIndex, int charIndex) { + sprite->setColor({ value.r, value.g, value.b }); + } + )); + + registerRichTextKey(std::make_shared>( + "flip", + [](std::string value) -> Result { + if (value == "") return Ok(true); + + if (value != "true" && value != "false") { + return Err("Value must be 'true' or 'false'"); + } + + return Ok(value == "true"); + }, + [](bool const& value, cocos2d::CCFontSprite* sprite, int localIndex, int charIndex) { + sprite->setFlipY(value); + } + )); + + registerRichTextKey(std::make_shared>( + "mirror", + [](std::string value) -> Result { + if (value == "") return Ok(true); + + if (value != "true" && value != "false") { + return Err("Value must be 'true' or 'false'"); + } + + return Ok(value == "true"); + }, + [](bool const& value, cocos2d::CCFontSprite* sprite, int localIndex, int charIndex) { + sprite->setFlipX(value); + } + )); + + registerRichTextKey(std::make_shared>( + "workingTime", + [](std::string value) -> Result { + auto timeRes = geode::utils::numFromString(value); + if (timeRes.isErr()) return Ok(std::time(nullptr)); + + return Ok(timeRes.unwrap()); + }, + [](std::time_t const& value) -> std::string { + return fmt::format("{:%Y-%m-%d %H:%M:%S}", geode::localtime(value)); + } + )); + + registerRichTextKey(std::make_shared>( + "size", + [](std::string value) -> Result { + auto numRes = geode::utils::numFromString(value); + if (numRes.isErr()) return Err("size invalid!"); + + return Ok(numRes.unwrap()); + }, + [](float const& value, cocos2d::CCFontSprite* sprite, int localIndex, int charIndex) { + sprite->setScale(value); + } + )); + + // maybe one day someone will make this work but its fine :( + // registerRichTextKey(std::make_shared>( + // "font", + // [](std::string value) -> Result { + // auto temp = CCLabelBMFont::create("Test", value.c_str()); + // if (temp == nullptr) return Err("Font file isn't valid!"); + + // return Ok(value); + // }, + // [&](std::string const& value, CCFontSprite* const applyToSpr) { + // applyToSpr->retain(); + // applyToSpr->removeFromParent(); + // this->addChild(applyToSpr); + // applyToSpr->release(); + // } + // )); + + registerRichTextKey(std::make_shared>( + "link", + [](std::string value) -> Result { + return Ok(value); + }, + [&](std::string const& value, bool keyDown, cocos2d::CCFontSprite* specificSpriteClicked, std::set const& wordClicked) { + this->castedImpl()->processLinkClick(value, keyDown, specificSpriteClicked, wordClicked); + } + )); + + castedImpl()->m_rawText = m_impl->m_text; + + castedImpl()->formatRichText(); + m_impl->updateContainer(); + + return true; +} + +std::string RichTextArea::getRawText(){ + return castedImpl()->m_rawText; +} + +void RichTextArea::RichImpl::charIteration(geode::FunctionRef overflowHandling) { + float top = 0; + m_lines.clear(); + CCLabelBMFont* line = this->createLabel("", top); + m_lines = { line }; + + std::map> appliedRichTextInstances{}; + + std::map, int> m_charIndexForInstance{}; + + m_charactersForButton.clear(); + + int index = 0; + for (const char& c : m_text) { + if (m_richTextInstances.contains(index)){ + for (const auto& instancePtr : m_richTextInstances[index]) { + if (appliedRichTextInstances.contains(instancePtr->getKey())) { + if (instancePtr->isCancellation()){ + appliedRichTextInstances.erase(instancePtr->getKey()); + m_charIndexForInstance.erase(instancePtr); + } + else{ + appliedRichTextInstances[instancePtr->getKey()] = instancePtr; + m_charIndexForInstance[instancePtr] = 0; + } + } else { + appliedRichTextInstances.insert({instancePtr->getKey(), instancePtr}); + m_charIndexForInstance[instancePtr] = 0; + } + } + } + + index++; + + if (c == '\n') { + line = this->createLabel("", top -= this->calculateOffset(line)); + + if (line == nullptr) { + break; + } else { + m_lines.push_back(line); + } + } else if (m_artificialWidth && line->getContentWidth() * m_scale >= m_self->getWidth()) { + line = overflowHandling(line, c, top -= this->calculateOffset(line)); + + if (line == nullptr) { + break; + } else { + m_lines.push_back(line); + } + } else { + line->setString((std::string(line->getString()) + c).c_str()); + } + + if (line->getChildren()->lastObject() != nullptr){ + for (const auto& [key, instancePtr] : appliedRichTextInstances) { + auto lastChar = static_cast(line->getChildren()->lastObject()); + + instancePtr->applyChangesToSprite(lastChar, m_charIndexForInstance[instancePtr], index); + + if (instancePtr->isButton()){ + if (!instancePtr->isCancellation()){ + if (m_charactersForButton.contains(instancePtr)){ + m_charactersForButton[instancePtr].insert(lastChar); + } + else{ + m_charactersForButton.insert({instancePtr, {lastChar}}); + } + } + } + + } + + for (const auto& [instancePtr, _] : appliedRichTextInstances) { + m_charIndexForInstance[_]++; + } + } + } +} + +void RichTextArea::RichImpl::formatRichText() { + std::regex pattern(R"(<(\/)?([^=<>]+)(?:\s*=\s*([^<>]+))?>)"); + std::smatch match; + + m_richTextInstances.clear(); + + // theres probably a better way to do this lol idk aa + struct MatchInfo { + int position; + ptrdiff_t length; + std::string key; + std::string value; + int overallOffset; + bool cancellation; + }; + std::vector matches; + + auto begin = m_text.cbegin(); + auto end = m_text.cend(); + + int offset = 0; + + while (std::regex_search(begin, end, match, pattern)) { + int matchStartPos = std::distance(m_text.cbegin(), match[0].first); + + std::string value = ""; + if (match.size() >= 4 && match[3].matched) value = match[3]; + + matches.push_back({matchStartPos, match[0].length(), match[2], value, offset, match[1] == '/'}); + offset += match[0].length(); + + begin = match.suffix().first; + } + + std::map>> richTextInstancesBeforeExtraOffset{}; + + for (auto it = matches.rbegin(); it != matches.rend(); ++it) { + const auto& m = *it; + + if (!m_richTextKeys.contains(m.key)) continue; + + auto result = m_richTextKeys[m.key]->createInstance(m.value, m.cancellation); + + if (result.isErr()) continue; + + int effectIndex = m.position - m.overallOffset; + + auto& keyRef = result.unwrap(); + + if (richTextInstancesBeforeExtraOffset.contains(effectIndex)) { + richTextInstancesBeforeExtraOffset[effectIndex].push_back(keyRef); + } else { + richTextInstancesBeforeExtraOffset[effectIndex] = {keyRef}; + } + + m_text.erase(m.position, m.length); + } + + int textAdditionOverallOffset = 0; + int prevExtraOffset = 0; + + for (const auto& [index, keys] : richTextInstancesBeforeExtraOffset) { + for (const auto& keyRef : keys) + { + auto currentAddition = keyRef->runStrAddition(); + if (currentAddition == "") continue; + m_text.insert(index + textAdditionOverallOffset, currentAddition); + textAdditionOverallOffset += currentAddition.length(); + } + + m_richTextInstances[index + prevExtraOffset] = std::move(keys); + + prevExtraOffset = textAdditionOverallOffset; + } +} + +void RichTextArea::RichImpl::processLinkClick( + std::string const& link, + bool keyDown, + cocos2d::CCFontSprite* specificSpriteClicked, + std::set const& wordClicked +){ + if (keyDown){ + for (const auto& linkCharacter : wordClicked) + { + this->m_ogColorForLink.insert({ + linkCharacter, + linkCharacter->getColor() == ccColor3B{255, 255, 255} ? + ccColor3B{m_self->getColor().r, m_self->getColor().g, m_self->getColor().b} : + linkCharacter->getColor() + } + ); + + linkCharacter->setColor({ 78, 78, 255 }); + } + } + else { + for (const auto& linkCharacter : wordClicked) { + if (this->m_ogColorForLink.contains(linkCharacter)) { + linkCharacter->setColor(this->m_ogColorForLink[linkCharacter]); + } + } + + if (specificSpriteClicked != nullptr) + web::openLinkInBrowser(link); + } +} + +void RichTextArea::setText(std::string text) { + m_impl->m_text = std::move(text); + castedImpl()->m_rawText = m_impl->m_text; + castedImpl()->formatRichText(); + m_impl->updateContainer(); +} + +void RichTextArea::registerRichTextKey(std::shared_ptr key){ + if (this->castedImpl()->m_richTextKeys.contains(key->getKey())) return; + + this->castedImpl()->m_richTextKeys.insert({key->getKey(), key}); +} + +bool RichTextArea::ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent){ + for (const auto& [btnKey, textForBtn] : this->castedImpl()->m_charactersForButton) + { + for (const auto& fontSpr : textForBtn) + { + if (fontSpr == nullptr) continue; + + auto touchInSpace = fontSpr->convertTouchToNodeSpace(pTouch); + if (touchInSpace.x > 0 && touchInSpace.y > 0 && touchInSpace.x <= fontSpr->getContentWidth() && touchInSpace.y <= fontSpr->getContentHeight()){ + btnKey->callButton(true, fontSpr, textForBtn); + this->castedImpl()->m_currentlyHeldButton = btnKey; + return true; + } + } + } + + return false; +} + +void RichTextArea::ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent){ + if (this->castedImpl()->m_charactersForButton.contains(this->castedImpl()->m_currentlyHeldButton)){ + auto textForBtn = this->castedImpl()->m_charactersForButton[this->castedImpl()->m_currentlyHeldButton]; + + for (const auto& fontSpr : textForBtn) + { + if (fontSpr == nullptr) continue; + + auto touchInSpace = fontSpr->convertTouchToNodeSpace(pTouch); + if (touchInSpace.x > 0 && touchInSpace.y > 0 && touchInSpace.x <= fontSpr->getContentWidth() && touchInSpace.y <= fontSpr->getContentHeight()){ + this->castedImpl()->m_currentlyHeldButton->callButton(false, fontSpr, textForBtn); + this->castedImpl()->m_currentlyHeldButton = nullptr; + return; + } + } + + this->castedImpl()->m_currentlyHeldButton->callButton(false, nullptr, textForBtn); + } + + this->castedImpl()->m_currentlyHeldButton = nullptr; +} +void RichTextArea::ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent){ + RichTextArea::ccTouchEnded(pTouch, pEvent); +} + +RichTextArea::RichTextArea() : SimpleTextArea() {} + +RichTextArea::~RichTextArea() { + CCTouchDispatcher::get()->removeDelegate(this); +} + +inline std::unique_ptr RichTextArea::createImpl() { + return std::make_unique(this); +} + +RichTextArea::RichImpl* RichTextArea::castedImpl() { + return static_cast(m_impl.get()); +} \ No newline at end of file