From 8c23d322352fbc86b26054168b80c751bfd03fb3 Mon Sep 17 00:00:00 2001 From: RH Date: Thu, 15 Feb 2024 11:33:38 +1100 Subject: [PATCH] RichText enhancements (#1683) * Add paragraph tag support Add HTML header tags support Add multi-new line rich text element Add support for end of tag action General code clean up * Remove redundant additions * Fix compilation issue on platforms other than Windows * Remove redundant constructors and destructors. Initialize member variables. Remove explicit inline specifier since all member class functions are implicitly inlined. --- core/ui/UIRichText.cpp | 129 +++++++++++++++--- core/ui/UIRichText.h | 100 ++++++-------- .../UIRichTextTest/UIRichTextTest.cpp | 83 +++++++++++ .../UIRichTextTest/UIRichTextTest.h | 17 +++ 4 files changed, 254 insertions(+), 75 deletions(-) diff --git a/core/ui/UIRichText.cpp b/core/ui/UIRichText.cpp index a38f9cae93..deb7bc7456 100644 --- a/core/ui/UIRichText.cpp +++ b/core/ui/UIRichText.cpp @@ -288,6 +288,18 @@ RichElementNewLine* RichElementNewLine::create(int tag, const Color3B& color, ui return nullptr; } +RichElementNewLine* RichElementNewLine::create(int tag, int quantity, const Color3B& color, uint8_t opacity) +{ + RichElementNewLine* element = new RichElementNewLine(quantity); + if (element->init(tag, color, opacity)) + { + element->autorelease(); + return element; + } + AX_SAFE_DELETE(element); + return nullptr; +} + /** @brief parse a XML. */ class MyXMLVisitor : public SAXDelegator { @@ -340,6 +352,8 @@ public: , italics(false) , line(StyleLine::NONE) , effect(StyleEffect::NONE) + , outlineSize(0) + , shadowBlurRadius(0) {} }; @@ -352,6 +366,7 @@ private: { bool isFontElement; RichText::VisitEnterHandler handleVisitEnter; + RichText::VisitExitHandler handleVisitExit; }; typedef hlookup::string_map TagTables; @@ -359,7 +374,7 @@ private: public: explicit MyXMLVisitor(RichText* richText); - virtual ~MyXMLVisitor(); + ~MyXMLVisitor() override; Color3B getColor() const; @@ -397,7 +412,8 @@ public: static void setTagDescription(std::string_view tag, bool isFontElement, - RichText::VisitEnterHandler&& handleVisitEnter); + RichText::VisitEnterHandler&& handleVisitEnter, + RichText::VisitExitHandler&& handleVisitExit = nullptr); static void removeTagDescription(std::string_view tag); @@ -573,6 +589,66 @@ MyXMLVisitor::MyXMLVisitor(RichText* richText) : _fontElements(20), _richText(ri return make_pair(ValueMap(), richElement); }); + MyXMLVisitor::setTagDescription("p", false, nullptr, + [] { return RichElementNewLine::create(0, 2, Color3B::WHITE, 255); }); + + constexpr auto headerTagEnterHandler = [](const ValueMap& tagAttrValueMap, + float defaultFontSize) -> std::pair { + ValueMap attrValueMap; + if (tagAttrValueMap.find("size") != tagAttrValueMap.end()) + { + attrValueMap[RichText::KEY_FONT_SIZE] = tagAttrValueMap.at("size").asString(); + } + else + { + attrValueMap[RichText::KEY_FONT_SIZE] = defaultFontSize; + } + + if (tagAttrValueMap.find("color") != tagAttrValueMap.end()) + { + attrValueMap[RichText::KEY_FONT_COLOR_STRING] = tagAttrValueMap.at("color").asString(); + } + + if (tagAttrValueMap.find("face") != tagAttrValueMap.end()) + { + attrValueMap[RichText::KEY_FONT_FACE] = tagAttrValueMap.at("face").asString(); + } + + attrValueMap[RichText::KEY_TEXT_BOLD] = true; + + return make_pair(attrValueMap, nullptr); + }; + + MyXMLVisitor::setTagDescription( + "h1", true, + [headerTagEnterHandler](const ValueMap& tagAttrValueMap) { return headerTagEnterHandler(tagAttrValueMap, 34); }, + [] { return RichElementNewLine::create(0, 2, Color3B::WHITE, 255); }); + + MyXMLVisitor::setTagDescription( + "h2", true, + [headerTagEnterHandler](const ValueMap& tagAttrValueMap) { return headerTagEnterHandler(tagAttrValueMap, 30); }, + [] { return RichElementNewLine::create(0, 2, Color3B::WHITE, 255); }); + + MyXMLVisitor::setTagDescription( + "h3", true, + [headerTagEnterHandler](const ValueMap& tagAttrValueMap) { return headerTagEnterHandler(tagAttrValueMap, 24); }, + [] { return RichElementNewLine::create(0, 2, Color3B::WHITE, 255); }); + + MyXMLVisitor::setTagDescription( + "h4", true, + [headerTagEnterHandler](const ValueMap& tagAttrValueMap) { return headerTagEnterHandler(tagAttrValueMap, 20); }, + [] { return RichElementNewLine::create(0, 2, Color3B::WHITE, 255); }); + + MyXMLVisitor::setTagDescription( + "h5", true, + [headerTagEnterHandler](const ValueMap& tagAttrValueMap) { return headerTagEnterHandler(tagAttrValueMap, 18); }, + [] { return RichElementNewLine::create(0, 2, Color3B::WHITE, 255); }); + + MyXMLVisitor::setTagDescription( + "h6", true, + [headerTagEnterHandler](const ValueMap& tagAttrValueMap) { return headerTagEnterHandler(tagAttrValueMap, 16); }, + [] { return RichElementNewLine::create(0, 2, Color3B::WHITE, 255); }); + MyXMLVisitor::setTagDescription("outline", true, [](const ValueMap& tagAttrValueMap) { // supported attributes: // color, size @@ -915,6 +991,15 @@ void MyXMLVisitor::endElement(void* /*ctx*/, const char* elementName) { popBackFontElement(); } + + if (tagBehavior.handleVisitExit != nullptr) + { + auto* richElement = tagBehavior.handleVisitExit(); + if (richElement) + { + pushBackElement(richElement); + } + } } } @@ -974,14 +1059,12 @@ void MyXMLVisitor::pushBackElement(RichElement* element) void MyXMLVisitor::setTagDescription(std::string_view tag, bool isFontElement, - RichText::VisitEnterHandler&& handleVisitEnter) + RichText::VisitEnterHandler&& handleVisitEnter, + RichText::VisitExitHandler&& handleVisitExit) { - hlookup::set_item( - MyXMLVisitor::_tagTables, tag, - TagBehavior{ - isFontElement, - std::move( - handleVisitEnter)}); // MyXMLVisitor::_tagTables[tag] = {isFontElement, std::move(handleVisitEnter)}; + // MyXMLVisitor::_tagTables[tag] = {isFontElement, std::move(handleVisitEnter), std::move(handleVisitExit)}; + hlookup::set_item(MyXMLVisitor::_tagTables, tag, + TagBehavior{isFontElement, std::move(handleVisitEnter), std::move(handleVisitExit)}); } void MyXMLVisitor::removeTagDescription(std::string_view tag) @@ -1536,9 +1619,11 @@ std::string RichText::stringWithColor4B(const ax::Color4B& color4b) return std::string(buf, 9); } -void RichText::setTagDescription(std::string_view tag, bool isFontElement, VisitEnterHandler handleVisitEnter) +void RichText::setTagDescription(std::string_view tag, + bool isFontElement, + VisitEnterHandler handleVisitEnter, VisitExitHandler handleVisitExit) { - MyXMLVisitor::setTagDescription(tag, isFontElement, std::move(handleVisitEnter)); + MyXMLVisitor::setTagDescription(tag, isFontElement, std::move(handleVisitEnter), std::move(handleVisitExit)); } void RichText::removeTagDescription(std::string_view tag) @@ -1659,7 +1744,9 @@ void RichText::formatText(bool force) } case RichElement::Type::NEWLINE: { - addNewLine(); + auto* newLineMulti = static_cast(element); + + addNewLine(newLineMulti->_quantity); break; } default: @@ -1678,7 +1765,7 @@ void RichText::formatText(bool force) addNewLine(); for (ssize_t i = 0, size = _richElements.size(); i < size; ++i) { - RichElement* element = static_cast(_richElements.at(i)); + RichElement* element = _richElements.at(i); switch (element->_type) { case RichElement::Type::TEXT: @@ -1706,7 +1793,9 @@ void RichText::formatText(bool force) } case RichElement::Type::NEWLINE: { - addNewLine(); + auto* newLineMulti = static_cast(element); + + addNewLine(newLineMulti->_quantity); break; } default: @@ -2049,11 +2138,15 @@ void RichText::handleCustomRenderer(ax::Node* renderer) } } -void RichText::addNewLine() +void RichText::addNewLine(int quantity) { - _leftSpaceWidth = _customSize.width; - _elementRenders.emplace_back(); - _lineHeights.emplace_back(); + do + { + _leftSpaceWidth = _customSize.width; + _elementRenders.emplace_back(); + _lineHeights.emplace_back(); + } + while (--quantity > 0); } void RichText::formatRenderers() diff --git a/core/ui/UIRichText.h b/core/ui/UIRichText.h index 077675c10e..13ceb786ad 100644 --- a/core/ui/UIRichText.h +++ b/core/ui/UIRichText.h @@ -1,6 +1,7 @@ /**************************************************************************** Copyright (c) 2013 cocos2d-x.org Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd. + Copyright (c) 2019-present Axmol Engine contributors (see AUTHORS.md). https://axmolengine.github.io/ @@ -22,9 +23,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ****************************************************************************/ - -#ifndef __UIRICHTEXT_H__ -#define __UIRICHTEXT_H__ +#pragma once #include "ui/UIWidget.h" #include "ui/GUIExport.h" @@ -59,20 +58,6 @@ public: NEWLINE /*!< RichElementNewLine */ }; - /** - * @brief Default constructor. - * @js ctor - * @lua new - */ - RichElement(){}; - - /** - * @brief Default destructor. - * @js NA - * @lua NA - */ - virtual ~RichElement(){}; - /** * @brief Initialize a rich element with different arguments. * @@ -87,10 +72,10 @@ public: void setColor(const Color3B& color); protected: - Type _type; /*!< Rich element type. */ - int _tag; /*!< A integer tag value. */ - Color3B _color; /*!< A color in `Color3B`. */ - uint8_t _opacity; /*!< A opacity value in `GLubyte`. */ + Type _type{}; /*!< Rich element type. */ + int _tag{}; /*!< A integer tag value. */ + Color3B _color{}; /*!< A color in `Color3B`. */ + uint8_t _opacity{}; /*!< A opacity value in `GLubyte`. */ friend class RichText; }; @@ -105,7 +90,7 @@ public: * @js ctor * @lua new */ - RichElementText() { _type = Type::TEXT; }; + RichElementText() : _fontSize(0), _flags(0), _outlineSize(0), _shadowBlurRadius(0) { _type = Type::TEXT; } enum { @@ -119,13 +104,6 @@ public: GLOW_FLAG = 1 << 7 /*!< glow effect */ }; - /** - *@brief Default destructor. - * @js NA - * @lua NA - */ - virtual ~RichElementText(){}; - /** * @brief Initialize a RichElementText with various arguments. * @@ -221,14 +199,7 @@ public: * @lua new * */ - RichElementImage() { _type = Type::IMAGE; }; - - /** - * @brief Default destructor. - * @js NA - * @lua NA - */ - virtual ~RichElementImage(){}; + RichElementImage(): _textureType(), _width(0), _height(0), _scaleX(0), _scaleY(0) { _type = Type::IMAGE; } /** * @brief Initialize a RichElementImage with various arguments. @@ -268,9 +239,9 @@ public: void setWidth(int width); void setHeight(int height); - inline void setScale(float scale) { _scaleX = _scaleY = scale; } - inline void setScaleX(float scaleX) { _scaleX = scaleX; } - inline void setScaleY(float scaleY) { _scaleY = scaleY; } + void setScale(float scale) { _scaleX = _scaleY = scale; } + void setScaleX(float scaleX) { _scaleX = scaleX; } + void setScaleY(float scaleY) { _scaleY = scaleY; } void setUrl(std::string_view url); protected: @@ -296,14 +267,14 @@ public: * @js ctor * @lua new */ - RichElementCustomNode() { _type = Type::CUSTOM; }; + RichElementCustomNode() { _type = Type::CUSTOM; } /** * @brief Default destructor. * @js NA * @lua NA */ - virtual ~RichElementCustomNode() { AX_SAFE_RELEASE(_customNode); }; + ~RichElementCustomNode() override { AX_SAFE_RELEASE(_customNode); } /** * @brief Initialize a RichElementCustomNode with various arguments. @@ -328,7 +299,7 @@ public: static RichElementCustomNode* create(int tag, const Color3B& color, uint8_t opacity, Node* customNode); protected: - Node* _customNode; + Node* _customNode{}; friend class RichText; }; @@ -344,14 +315,14 @@ public: * @lua new * */ - RichElementNewLine() { _type = Type::NEWLINE; }; + RichElementNewLine(int quantity = 1) : _quantity(quantity) { _type = Type::NEWLINE; } /** * @brief Default destructor. * @js NA * @lua NA */ - virtual ~RichElementNewLine(){}; + ~RichElementNewLine() override = default; /** * @brief Create a RichElementNewLine with various arguments. @@ -363,8 +334,20 @@ public: */ static RichElementNewLine* create(int tag, const Color3B& color, uint8_t opacity); + /** + * @brief Create a RichElementNewLine with various arguments. + * + * @param tag A integer tag value. + * @param quantity Number of new lines to add + * @param color A color in Color3B. + * @param opacity A opacity in GLubyte. + * @return A RichElementNewLine instance. + */ + static RichElementNewLine* create(int tag, int quantity, const Color3B& color, uint8_t opacity); + protected: friend class RichText; + int _quantity; }; /** @@ -406,6 +389,7 @@ public: * @result text attributes and RichElement */ typedef std::function(const ValueMap& tagAttrValueMap)> VisitEnterHandler; + typedef std::function VisitExitHandler; static const std::string KEY_VERTICAL_SPACE; /*!< key of vertical space */ static const std::string KEY_WRAP_MODE; /*!< key of per word, or per char */ @@ -460,7 +444,7 @@ public: * @js NA * @lua NA */ - virtual ~RichText(); + ~RichText() override; /** * @brief Create a empty RichText. @@ -522,8 +506,8 @@ public: void formatText(bool force = false); // override functions. - virtual void ignoreContentAdaptWithSize(bool ignore) override; - virtual std::string getDescription() const override; + void ignoreContentAdaptWithSize(bool ignore) override; + std::string getDescription() const override; void setWrapMode(WrapMode wrapMode); /*!< sets the wrapping mode: WRAP_PER_CHAR or WRAP_PER_WORD */ WrapMode getWrapMode() const; /*!< returns the current wrapping mode */ @@ -540,7 +524,7 @@ public: float getFontSize(); /*!< return the current font size */ void setFontFace(std::string_view face); /*!< Set the font face. @param face the font face. */ std::string getFontFace(); /*!< return the current font face */ - void setAnchorFontColor(std::string_view color); /*!< Set the font color of a-tag. @param face the font color. */ + void setAnchorFontColor(std::string_view color); /*!< Set the font color of a-tag. @param color the font color. */ std::string getAnchorFontColor(); /*!< return the current font color of a-tag */ ax::Color3B getAnchorFontColor3B(); /*!< return the current font color of a-tag */ void setAnchorTextBold(bool enable); /*!< enable bold text of a-tag */ @@ -579,9 +563,13 @@ public: * @brief add a callback to own tag. * @param tag tag's name * @param isFontElement use attributes of text tag - * @param handleVisitEnter callback + * @param handleVisitEnter callback at opening tag + * @param handleVisitExit callback at closing tag */ - static void setTagDescription(std::string_view tag, bool isFontElement, VisitEnterHandler handleVisitEnter); + static void setTagDescription(std::string_view tag, + bool isFontElement, + VisitEnterHandler handleVisitEnter, + VisitExitHandler handleVisitExit = nullptr); /** * @brief remove a callback to own tag. @@ -598,7 +586,7 @@ public: */ void setOpenUrlHandler(const OpenUrlHandler& handleOpenUrl); - virtual bool init() override; + bool init() override; bool initWithXML(std::string_view xml, const ValueMap& defaults = ValueMap(), @@ -607,9 +595,9 @@ public: bool setString(std::string_view text); protected: - virtual void adaptRenderers() override; + void adaptRenderers() override; - virtual void initRenderer() override; + void initRenderer() override; void pushToContainer(Node* renderer); void handleTextRenderer(std::string_view text, std::string_view fontName, @@ -635,7 +623,7 @@ protected: float scaleY = 1.f); void handleCustomRenderer(Node* renderer); void formatRenderers(); - void addNewLine(); + void addNewLine(int quantity = 1); void doHorizontalAlignment(const Vector& row, float rowWidth); float stripTrailingWhitespace(const Vector& row); @@ -657,5 +645,3 @@ protected: // end of ui group /// @} NS_AX_END - -#endif /* defined(__UIRichText__) */ diff --git a/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.cpp b/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.cpp index 78c0f87eda..fbd2438ace 100644 --- a/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.cpp +++ b/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.cpp @@ -1,5 +1,6 @@ /**************************************************************************** Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd. +Copyright (c) 2019-present Axmol Engine contributors (see AUTHORS.md). https://axmolengine.github.io/ @@ -50,6 +51,8 @@ UIRichTextTests::UIRichTextTests() ADD_TEST_CASE(UIRichTextXMLExtend); ADD_TEST_CASE(UIRichTextXMLSpace); ADD_TEST_CASE(UIRichTextNewline); + ADD_TEST_CASE(UIRichTextHeaders); + ADD_TEST_CASE(UIRichTextParagraph); } // @@ -970,3 +973,83 @@ bool UIRichTextNewline::init() } return false; } + +bool UIRichTextHeaders::init() +{ + if (UIRichTextTestBase::init()) + { + auto& widgetSize = _widget->getContentSize(); + + // Add the alert + Text* alert = Text::create("Header Tags", "fonts/Marker Felt.ttf", 30); + alert->setColor(Color3B(159, 168, 176)); + alert->setPosition( + Vec2(widgetSize.width / 2.0f, widgetSize.height / 2.0f - alert->getContentSize().height * 3.125)); + _widget->addChild(alert); + + createButtonPanel(); + +#ifdef AX_PLATFORM_PC + _defaultContentSize = Size(290, 290); +#endif + + // RichText + _richText = RichText::createWithXML( + R"(

h1. HEADING

h2. HEADING

h3. HEADING

h4. HEADING

h5. HEADING
h6. HEADING
)"); + _richText->ignoreContentAdaptWithSize(false); + _richText->setContentSize(_defaultContentSize); + + _richText->setPosition(Vec2(widgetSize.width / 2, widgetSize.height / 2)); + _richText->setLocalZOrder(10); + + _widget->addChild(_richText); + + // test remove all children, this call won't effect the test + _richText->removeAllChildren(); + + return true; + } + return false; +} + +bool UIRichTextParagraph::init() +{ + if (UIRichTextTestBase::init()) + { + auto& widgetSize = _widget->getContentSize(); + + // Add the alert + Text* alert = Text::create("Paragraph Tag", "fonts/Marker Felt.ttf", 30); + alert->setColor(Color3B(159, 168, 176)); + alert->setPosition( + Vec2(widgetSize.width / 2.0f, widgetSize.height / 2.0f - alert->getContentSize().height * 3.125)); + _widget->addChild(alert); + + createButtonPanel(); + +#ifdef AX_PLATFORM_PC + _defaultContentSize = Size(290, 290); +#endif + + // RichText + _richText = RichText::createWithXML( + "

Paragraph1: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et " + "dolore magna aliqua. Feugiat scelerisque varius morbi enim nunc. Dis parturient montes nascetur ridiculus " + "mus mauris vitae ultricies.

Paragraph2: Lectus urna duis convallis convallis tellus id interdum velit. " + "Convallis a cras semper auctor neque vitae tempus quam pellentesque. Congue quisque egestas diam in arcu " + "cursus euismod quis.

"); + _richText->ignoreContentAdaptWithSize(false); + _richText->setContentSize(_defaultContentSize); + + _richText->setPosition(Vec2(widgetSize.width / 2, widgetSize.height / 2)); + _richText->setLocalZOrder(10); + + _widget->addChild(_richText); + + // test remove all children, this call won't effect the test + _richText->removeAllChildren(); + + return true; + } + return false; +} diff --git a/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.h b/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.h index bad76295cc..f8a5476b3e 100644 --- a/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.h +++ b/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.h @@ -1,5 +1,6 @@ /**************************************************************************** Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd. + Copyright (c) 2019-present Axmol Engine contributors (see AUTHORS.md). https://axmolengine.github.io/ @@ -203,4 +204,20 @@ public: bool init() override; }; +class UIRichTextHeaders : public UIRichTextTestBase +{ +public: + CREATE_FUNC(UIRichTextHeaders); + + bool init() override; +}; + +class UIRichTextParagraph : public UIRichTextTestBase +{ +public: + CREATE_FUNC(UIRichTextParagraph); + + bool init() override; +}; + #endif /* defined(__TestCpp__UIRichTextTest__) */