From 978d15db78d1231a70dffd525e0d6c14f179cb68 Mon Sep 17 00:00:00 2001 From: RH Date: Sun, 18 Feb 2024 23:09:54 +1100 Subject: [PATCH] RichText and ScrollView enhancements (#1696) * Add id tag to several RichText elements to allow locating the nodes in RichText Add font related styling to paragraph tags * Move functionality out of ListView and into ScrollView to allow scrolling to a specific child node within a ScrollView * Add function to allow finding protected child node by name * Example of anchor tags to local RichText content in a ScrollView * Fix string storage type --- core/2d/ProtectedNode.cpp | 11 ++ core/2d/ProtectedNode.h | 9 + core/ui/UIListView.cpp | 44 +---- core/ui/UIListView.h | 92 ++++----- core/ui/UIRichText.cpp | 174 ++++++++++++++++-- core/ui/UIRichText.h | 44 +++-- core/ui/UIScrollView.cpp | 63 ++++++- core/ui/UIScrollView.h | 113 ++++++++---- .../UIRichTextTest/UIRichTextTest.cpp | 128 +++++++++++++ .../UIRichTextTest/UIRichTextTest.h | 13 ++ 10 files changed, 528 insertions(+), 163 deletions(-) diff --git a/core/2d/ProtectedNode.cpp b/core/2d/ProtectedNode.cpp index 64b987b19f..60e3ee0e3c 100644 --- a/core/2d/ProtectedNode.cpp +++ b/core/2d/ProtectedNode.cpp @@ -128,6 +128,17 @@ Node* ProtectedNode::getProtectedChildByTag(int tag) return nullptr; } +Node* ProtectedNode::getProtectedChildByName(std::string_view name) +{ + // AXASSERT(!name.empty(), "Invalid name"); + for (auto&& child : _protectedChildren) + { + if (child && child->getName() == name) + return child; + } + return nullptr; +} + /* "remove" logic MUST only be on this method * If a class want's to extend the 'removeChild' behavior it only needs * to override this method diff --git a/core/2d/ProtectedNode.h b/core/2d/ProtectedNode.h index d3be277abf..78f3c58892 100644 --- a/core/2d/ProtectedNode.h +++ b/core/2d/ProtectedNode.h @@ -94,6 +94,15 @@ public: */ virtual Node* getProtectedChildByTag(int tag); + /** + * Gets a child from the container with its name. + * + * @param name An identifier to find the child node. + * + * @return a Node object whose name equals to the input parameter. + */ + virtual Node* getProtectedChildByName(std::string_view name); + ////// REMOVES ////// /** diff --git a/core/ui/UIListView.cpp b/core/ui/UIListView.cpp index f968efc27f..2c107dc13e 100644 --- a/core/ui/UIListView.cpp +++ b/core/ui/UIListView.cpp @@ -1,6 +1,7 @@ /**************************************************************************** Copyright (c) 2013-2016 Chukong Technologies Inc. Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd. +Copyright (c) 2019-present Axmol Engine contributors (see AUTHORS.md). https://axmolengine.github.io/ @@ -28,8 +29,6 @@ THE SOFTWARE. NS_AX_BEGIN -static const float DEFAULT_TIME_IN_SEC_FOR_SCROLL_TO_ITEM = 1.0f; - namespace ui { @@ -45,7 +44,6 @@ ListView::ListView() , _topPadding(0.0f) , _rightPadding(0.0f) , _bottomPadding(0.0f) - , _scrollTime(DEFAULT_TIME_IN_SEC_FOR_SCROLL_TO_ITEM) , _curSelectedIndex(-1) , _innerContainerDoLayoutDirty(true) , _eventCallback(nullptr) @@ -523,17 +521,6 @@ float ListView::getBottomPadding() const return _bottomPadding; } -void ListView::setScrollDuration(float time) -{ - if (time >= 0) - _scrollTime = time; -} - -float ListView::getScrollDuration() const -{ - return _scrollTime; -} - void ListView::setDirection(Direction dir) { switch (dir) @@ -642,13 +629,6 @@ void ListView::interceptTouchEvent(TouchEventType event, Widget* sender, Touch* } } -static Vec2 calculateItemPositionWithAnchor(Widget* item, const Vec2& itemAnchorPoint) -{ - Vec2 origin(item->getLeftBoundary(), item->getBottomBoundary()); - Vec2 size = item->getContentSize(); - return origin + Vec2(size.width * itemAnchorPoint.x, size.height * itemAnchorPoint.y); -} - static Widget* findClosestItem(const Vec2& targetPosition, const Vector& items, const Vec2& itemAnchorPoint, @@ -676,7 +656,7 @@ static Widget* findClosestItem(const Vec2& targetPosition, // Binary search ssize_t midIndex = (firstIndex + lastIndex) / 2; - Vec2 itemPosition = calculateItemPositionWithAnchor(items.at(midIndex), itemAnchorPoint); + Vec2 itemPosition = ListView::calculateItemPositionWithAnchor(items.at(midIndex), itemAnchorPoint); float distanceFromMid = (targetPosition - itemPosition).length(); if (distanceFromFirst <= distanceFromLast) { @@ -830,17 +810,6 @@ void ListView::jumpToPercentBothDirection(const Vec2& percent) ScrollView::jumpToPercentBothDirection(percent); } -Vec2 ListView::calculateItemDestination(const Vec2& positionRatioInView, Widget* item, const Vec2& itemAnchorPoint) -{ - const Vec2& contentSize = getContentSize(); - Vec2 positionInView; - positionInView.x += contentSize.width * positionRatioInView.x; - positionInView.y += contentSize.height * positionRatioInView.y; - - Vec2 itemPosition = calculateItemPositionWithAnchor(item, itemAnchorPoint); - return -(itemPosition - positionInView); -} - void ListView::jumpToItem(ssize_t itemIndex, const Vec2& positionRatioInView, const Vec2& itemAnchorPoint) { Widget* item = getItem(itemIndex); @@ -875,8 +844,7 @@ void ListView::scrollToItem(ssize_t itemIndex, { return; } - Vec2 destination = calculateItemDestination(positionRatioInView, item, itemAnchorPoint); - startAutoScrollToDestination(destination, timeInSec, true); + ScrollView::scrollToItem(item, positionRatioInView, itemAnchorPoint, timeInSec); } ssize_t ListView::getCurSelectedIndex() const @@ -920,12 +888,12 @@ void ListView::copyClonedWidgetChildren(Widget* model) } } -void ListView::copySpecialProperties(Widget* widget) +void ListView::copySpecialProperties(Widget* model) { - ListView* listViewEx = dynamic_cast(widget); + ListView* listViewEx = dynamic_cast(model); if (listViewEx) { - ScrollView::copySpecialProperties(widget); + ScrollView::copySpecialProperties(model); setItemModel(listViewEx->_model); setItemsMargin(listViewEx->_itemsMargin); setGravity(listViewEx->_gravity); diff --git a/core/ui/UIListView.h b/core/ui/UIListView.h index ba20f8a137..edaae387ad 100644 --- a/core/ui/UIListView.h +++ b/core/ui/UIListView.h @@ -1,6 +1,7 @@ /**************************************************************************** Copyright (c) 2013-2016 Chukong Technologies Inc. Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd. +Copyright (c) 2019-present Axmol Engine contributors (see AUTHORS.md). https://axmolengine.github.io/ @@ -106,7 +107,7 @@ public: * @js NA * @lua NA */ - virtual ~ListView(); + ~ListView() override; /** * Create an empty ListView. @@ -288,31 +289,16 @@ public: */ float getBottomPadding() const; - /** - * Set the time in seconds to scroll between items. - * Subsequent calls of function 'scrollToItem', will take 'time' seconds for scrolling. - * @param time The seconds needed to scroll between two items. 'time' must be >= 0 - * @see scrollToItem(ssize_t, const Vec2&, const Vec2&) - */ - void setScrollDuration(float time); - - /** - * Get the time in seconds to scroll between items. - * @return The time in seconds to scroll between items - * @see setScrollDuration(float) - */ - float getScrollDuration() const; - // override methods - virtual void doLayout() override; - virtual void requestDoLayout() override; - virtual void addChild(Node* child) override; - virtual void addChild(Node* child, int localZOrder) override; - virtual void addChild(Node* child, int zOrder, int tag) override; - virtual void addChild(Node* child, int zOrder, std::string_view name) override; - virtual void removeAllChildren() override; - virtual void removeAllChildrenWithCleanup(bool cleanup) override; - virtual void removeChild(Node* child, bool cleanup = true) override; + void doLayout() override; + void requestDoLayout() override; + void addChild(Node* child) override; + void addChild(Node* child, int localZOrder) override; + void addChild(Node* child, int zOrder, int tag) override; + void addChild(Node* child, int zOrder, std::string_view name) override; + void removeAllChildren() override; + void removeAllChildrenWithCleanup(bool cleanup) override; + void removeChild(Node* child, bool cleanup = true) override; /** * @brief Query the closest item to a specific position in inner container. @@ -367,17 +353,17 @@ public: /** * Override functions */ - virtual void jumpToBottom() override; - virtual void jumpToTop() override; - virtual void jumpToLeft() override; - virtual void jumpToRight() override; - virtual void jumpToTopLeft() override; - virtual void jumpToTopRight() override; - virtual void jumpToBottomLeft() override; - virtual void jumpToBottomRight() override; - virtual void jumpToPercentVertical(float percent) override; - virtual void jumpToPercentHorizontal(float percent) override; - virtual void jumpToPercentBothDirection(const Vec2& percent) override; + void jumpToBottom() override; + void jumpToTop() override; + void jumpToLeft() override; + void jumpToRight() override; + void jumpToTopLeft() override; + void jumpToTopRight() override; + void jumpToBottomLeft() override; + void jumpToBottomRight() override; + void jumpToPercentVertical(float percent) override; + void jumpToPercentHorizontal(float percent) override; + void jumpToPercentBothDirection(const Vec2& percent) override; /** * @brief Jump to specific item @@ -391,15 +377,20 @@ public: * @brief Scroll to specific item * @param positionRatioInView Specifies the position with ratio in list view's content size. * @param itemAnchorPoint Specifies an anchor point of each item for position to calculate distance. - * @param timeInSec Scroll time */ void scrollToItem(ssize_t itemIndex, const Vec2& positionRatioInView, const Vec2& itemAnchorPoint); + + /** + * @brief Scroll to specific item + * @param positionRatioInView Specifies the position with ratio in list view's content size. + * @param itemAnchorPoint Specifies an anchor point of each item for position to calculate distance. + * @param timeInSec Scroll time + */ void scrollToItem(ssize_t itemIndex, const Vec2& positionRatioInView, const Vec2& itemAnchorPoint, float timeInSec); /** * @brief Query current selected widget's index. * - * @return An index of a selected item. */ ssize_t getCurSelectedIndex() const; @@ -423,14 +414,14 @@ public: * Direction Direction::VERTICAL means vertical scroll, Direction::HORIZONTAL means horizontal scroll. * @param dir Set the list view's scroll direction. */ - virtual void setDirection(Direction dir) override; + void setDirection(Direction dir) override; - virtual std::string getDescription() const override; + std::string getDescription() const override; - virtual bool init() override; + bool init() override; protected: - virtual void handleReleaseLogic(Touch* touch) override; + void handleReleaseLogic(Touch* touch) override; virtual void onItemListChanged(); @@ -439,19 +430,18 @@ protected: void remedyVerticalLayoutParameter(LinearLayoutParameter* layoutParameter, ssize_t itemIndex); void remedyHorizontalLayoutParameter(LinearLayoutParameter* layoutParameter, ssize_t itemIndex); - virtual void onSizeChanged() override; - virtual Widget* createCloneInstance() override; - virtual void copySpecialProperties(Widget* model) override; - virtual void copyClonedWidgetChildren(Widget* model) override; + void onSizeChanged() override; + Widget* createCloneInstance() override; + void copySpecialProperties(Widget* model) override; + void copyClonedWidgetChildren(Widget* model) override; void selectedItemEvent(TouchEventType event); - virtual void interceptTouchEvent(Widget::TouchEventType event, Widget* sender, Touch* touch) override; + void interceptTouchEvent(Widget::TouchEventType event, Widget* sender, Touch* touch) override; - virtual Vec2 getHowMuchOutOfBoundary(const Vec2& addition = Vec2::ZERO) override; + Vec2 getHowMuchOutOfBoundary(const Vec2& addition = Vec2::ZERO) override; - virtual void startAttenuatingAutoScroll(const Vec2& deltaMove, const Vec2& initialVelocity) override; + void startAttenuatingAutoScroll(const Vec2& deltaMove, const Vec2& initialVelocity) override; void startMagneticScroll(); - Vec2 calculateItemDestination(const Vec2& positionRatioInView, Widget* item, const Vec2& itemAnchorPoint); protected: Widget* _model; @@ -470,8 +460,6 @@ protected: float _rightPadding; float _bottomPadding; - float _scrollTime; - ssize_t _curSelectedIndex; bool _innerContainerDoLayoutDirty; diff --git a/core/ui/UIRichText.cpp b/core/ui/UIRichText.cpp index deb7bc7456..25fa08ab28 100644 --- a/core/ui/UIRichText.cpp +++ b/core/ui/UIRichText.cpp @@ -151,11 +151,12 @@ RichElementText* RichElementText::create(int tag, const Color3B& shadowColor, const Vec2& shadowOffset, int shadowBlurRadius, - const Color3B& glowColor) + const Color3B& glowColor, + std::string_view id) { RichElementText* element = new RichElementText(); if (element->init(tag, color, opacity, text, fontName, fontSize, flags, url, outlineColor, outlineSize, shadowColor, - shadowOffset, shadowBlurRadius, glowColor)) + shadowOffset, shadowBlurRadius, glowColor, id)) { element->autorelease(); return element; @@ -177,7 +178,8 @@ bool RichElementText::init(int tag, const Color3B& shadowColor, const Vec2& shadowOffset, int shadowBlurRadius, - const Color3B& glowColor) + const Color3B& glowColor, + std::string_view id) { if (RichElement::init(tag, color, opacity)) { @@ -192,6 +194,7 @@ bool RichElementText::init(int tag, _shadowOffset = shadowOffset; _shadowBlurRadius = shadowBlurRadius; _glowColor = glowColor; + _id = id; return true; } return false; @@ -202,10 +205,11 @@ RichElementImage* RichElementImage::create(int tag, uint8_t opacity, std::string_view filePath, std::string_view url, - Widget::TextureResType texType) + Widget::TextureResType texType, + std::string_view id) { RichElementImage* element = new RichElementImage(); - if (element->init(tag, color, opacity, filePath, url, texType)) + if (element->init(tag, color, opacity, filePath, url, texType, id)) { element->autorelease(); return element; @@ -219,7 +223,8 @@ bool RichElementImage::init(int tag, uint8_t opacity, std::string_view filePath, std::string_view url, - Widget::TextureResType texType) + Widget::TextureResType texType, + std::string_view id) { if (RichElement::init(tag, color, opacity)) { @@ -230,6 +235,7 @@ bool RichElementImage::init(int tag, _scaleY = 1.0f; _url = url; _textureType = texType; + _id = id; return true; } return false; @@ -250,13 +256,19 @@ void RichElementImage::setUrl(std::string_view url) _url = url; } +void RichElementImage::setId(std::string_view id) +{ + _id = id; +} + RichElementCustomNode* RichElementCustomNode::create(int tag, const Color3B& color, uint8_t opacity, - ax::Node* customNode) + ax::Node* customNode, + std::string_view id) { RichElementCustomNode* element = new RichElementCustomNode(); - if (element->init(tag, color, opacity, customNode)) + if (element->init(tag, color, opacity, customNode, id)) { element->autorelease(); return element; @@ -265,11 +277,16 @@ RichElementCustomNode* RichElementCustomNode::create(int tag, return nullptr; } -bool RichElementCustomNode::init(int tag, const Color3B& color, uint8_t opacity, ax::Node* customNode) +bool RichElementCustomNode::init(int tag, + const Color3B& color, + uint8_t opacity, + ax::Node* customNode, + std::string_view id) { if (RichElement::init(tag, color, opacity)) { _customNode = customNode; + _id = id; _customNode->retain(); return true; } @@ -339,6 +356,7 @@ public: Vec2 shadowOffset; /*!< shadow effect offset value */ int shadowBlurRadius; /*!< the shadow effect blur radius */ Color3B glowColor; /*!< the glow effect color value */ + std::string name; /*!< the name of this element */ void setColor(const Color3B& acolor) { @@ -398,6 +416,8 @@ public: std::tuple getGlow() const; + std::string getName() const; + void startElement(void* ctx, const char* name, const char** atts) override; void endElement(void* ctx, const char* name) override; @@ -569,6 +589,11 @@ MyXMLVisitor::MyXMLVisitor(RichText* richText) : _fontElements(20), _richText(ri elementImg->setScaleX(scaleX); elementImg->setScaleY(scaleY); + + if (tagAttrValueMap.find("id") != tagAttrValueMap.end()) + { + elementImg->setId(tagAttrValueMap.at("id").asString()); + } } return make_pair(ValueMap(), elementImg); }); @@ -581,6 +606,12 @@ MyXMLVisitor::MyXMLVisitor(RichText* richText) : _fontElements(20), _richText(ri { attrValueMap[RichText::KEY_URL] = tagAttrValueMap.at("href").asString(); } + + if (tagAttrValueMap.find("id") != tagAttrValueMap.end()) + { + attrValueMap[RichText::KEY_ID] = tagAttrValueMap.at("id").asString(); + } + return make_pair(attrValueMap, nullptr); }); @@ -589,8 +620,33 @@ 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); }); + MyXMLVisitor::setTagDescription( + "p", true, + [](const ValueMap& tagAttrValueMap) -> std::pair { + ValueMap attrValueMap; + if (tagAttrValueMap.find("size") != tagAttrValueMap.end()) + { + attrValueMap[RichText::KEY_FONT_SIZE] = tagAttrValueMap.at("size").asString(); + } + + 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(); + } + + if (tagAttrValueMap.find("id") != tagAttrValueMap.end()) + { + attrValueMap[RichText::KEY_ID] = tagAttrValueMap.at("id").asString(); + } + + return make_pair(attrValueMap, nullptr); + }, + [] { return RichElementNewLine::create(0, 2, Color3B::WHITE, 255); }); constexpr auto headerTagEnterHandler = [](const ValueMap& tagAttrValueMap, float defaultFontSize) -> std::pair { @@ -616,6 +672,11 @@ MyXMLVisitor::MyXMLVisitor(RichText* richText) : _fontElements(20), _richText(ri attrValueMap[RichText::KEY_TEXT_BOLD] = true; + if (tagAttrValueMap.find("id") != tagAttrValueMap.end()) + { + attrValueMap[RichText::KEY_ID] = tagAttrValueMap.at("id").asString(); + } + return make_pair(attrValueMap, nullptr); }; @@ -817,6 +878,16 @@ std::tuple MyXMLVisitor::getGlow() const return std::make_tuple(false, Color3B::WHITE); } +std::string MyXMLVisitor::getName() const +{ + for (auto i = _fontElements.rbegin(), iRend = _fontElements.rend(); i != iRend; ++i) + { + if (!i->name.empty()) + return i->name; + } + return ""; +} + void MyXMLVisitor::startElement(void* /*ctx*/, const char* elementName, const char** atts) { auto it = _tagTables.find(elementName); @@ -960,7 +1031,49 @@ void MyXMLVisitor::startElement(void* /*ctx*/, const char* elementName, const ch } } } - + if (attrValueMap.find(RichText::KEY_URL) != attrValueMap.end()) + { + attributes.url = attrValueMap.at(RichText::KEY_URL).asString(); + attributes.setColor(_richText->getAnchorFontColor3B()); + if (_richText->isAnchorTextBoldEnabled()) + { + attributes.bold = true; + } + if (_richText->isAnchorTextItalicEnabled()) + { + attributes.italics = true; + } + if (_richText->isAnchorTextUnderlineEnabled()) + { + attributes.line = StyleLine::UNDERLINE; + } + if (_richText->isAnchorTextDelEnabled()) + { + attributes.line = StyleLine::STRIKETHROUGH; + } + if (_richText->isAnchorTextOutlineEnabled()) + { + attributes.effect = StyleEffect::OUTLINE; + attributes.outlineColor = _richText->getAnchorTextOutlineColor3B(); + attributes.outlineSize = _richText->getAnchorTextOutlineSize(); + } + if (_richText->isAnchorTextShadowEnabled()) + { + attributes.effect = StyleEffect::SHADOW; + attributes.shadowColor = _richText->getAnchorTextShadowColor3B(); + attributes.shadowOffset = _richText->getAnchorTextShadowOffset(); + attributes.shadowBlurRadius = _richText->getAnchorTextShadowBlurRadius(); + } + if (_richText->isAnchorTextGlowEnabled()) + { + attributes.effect = StyleEffect::GLOW; + attributes.glowColor = _richText->getAnchorTextGlowColor3B(); + } + } + if (attrValueMap.find(RichText::KEY_ID) != attrValueMap.end()) + { + attributes.name = attrValueMap.at(RichText::KEY_ID).asString(); + } pushBackFontElement(attributes); } if (richElement) @@ -1017,6 +1130,7 @@ void MyXMLVisitor::textHandler(void* /*ctx*/, const char* str, size_t len) auto outline = getOutline(); auto shadow = getShadow(); auto glow = getGlow(); + auto name = getName(); uint32_t flags = 0; if (italics) @@ -1038,7 +1152,7 @@ void MyXMLVisitor::textHandler(void* /*ctx*/, const char* str, size_t len) auto element = RichElementText::create(0, color, 255, text, face, fontSize, flags, url, std::get<1>(outline), std::get<2>(outline), std::get<1>(shadow), std::get<2>(shadow), - std::get<3>(shadow), std::get<1>(glow)); + std::get<3>(shadow), std::get<1>(glow), name); _richText->pushBackElement(element); } @@ -1125,6 +1239,7 @@ const std::string RichText::KEY_ANCHOR_TEXT_SHADOW_OFFSET_WIDTH("KEY_ANCHOR_TEXT const std::string RichText::KEY_ANCHOR_TEXT_SHADOW_OFFSET_HEIGHT("KEY_ANCHOR_TEXT_SHADOW_OFFSET_HEIGHT"); const std::string RichText::KEY_ANCHOR_TEXT_SHADOW_BLUR_RADIUS("KEY_ANCHOR_TEXT_SHADOW_BLUR_RADIUS"); const std::string RichText::KEY_ANCHOR_TEXT_GLOW_COLOR("KEY_ANCHOR_TEXT_GLOW_COLOR"); +const std::string RichText::KEY_ID("KEY_ID"); RichText::RichText() : _formatTextDirty(true), _leftSpaceWidth(0.0f) { @@ -1702,6 +1817,9 @@ void RichText::formatText(bool force) label->enableGlow(Color4B(elmtText->_glowColor)); } label->setTextColor(Color4B(elmtText->_color)); + + label->setName(elmtText->_id); + elementRenderer = label; break; } @@ -1732,6 +1850,7 @@ void RichText::formatText(bool force) UrlTouchListenerComponent::create(elementRenderer, elmtImage->_url, std::bind(&RichText::openUrl, this, std::placeholders::_1))); elementRenderer->setColor(element->_color); + elementRenderer->setName(elmtImage->_id); } break; } @@ -1774,7 +1893,7 @@ void RichText::formatText(bool force) handleTextRenderer(elmtText->_text, elmtText->_fontName, elmtText->_fontSize, elmtText->_color, elmtText->_opacity, elmtText->_flags, elmtText->_url, elmtText->_outlineColor, elmtText->_outlineSize, elmtText->_shadowColor, elmtText->_shadowOffset, - elmtText->_shadowBlurRadius, elmtText->_glowColor); + elmtText->_shadowBlurRadius, elmtText->_glowColor, elmtText->_id); break; } case RichElement::Type::IMAGE: @@ -1782,13 +1901,13 @@ void RichText::formatText(bool force) RichElementImage* elmtImage = static_cast(element); handleImageRenderer(elmtImage->_filePath, elmtImage->_textureType, elmtImage->_color, elmtImage->_opacity, elmtImage->_width, elmtImage->_height, elmtImage->_url, - elmtImage->_scaleX, elmtImage->_scaleY); + elmtImage->_scaleX, elmtImage->_scaleY, elmtImage->_id); break; } case RichElement::Type::CUSTOM: { RichElementCustomNode* elmtCustom = static_cast(element); - handleCustomRenderer(elmtCustom->_customNode); + handleCustomRenderer(elmtCustom->_customNode, elmtCustom->_id); break; } case RichElement::Type::NEWLINE: @@ -1972,7 +2091,8 @@ void RichText::handleTextRenderer(std::string_view text, const Color3B& shadowColor, const Vec2& shadowOffset, int shadowBlurRadius, - const Color3B& glowColor) + const Color3B& glowColor, + std::string_view id) { bool fileExist = FileUtils::getInstance()->isFileExist(fontName); RichText::WrapMode wrapMode = static_cast(_defaults.at(KEY_WRAP_MODE).asInt()); @@ -1982,6 +2102,7 @@ void RichText::handleTextRenderer(std::string_view text, ss << text; std::string currentText; size_t realLines = 0; + auto isFirstLabel = true; while (std::getline(ss, currentText, '\n')) { if (realLines > 0) @@ -2026,6 +2147,12 @@ void RichText::handleTextRenderer(std::string_view text, textRenderer->setTextColor(Color4B(color)); textRenderer->setOpacity(opacity); + if (isFirstLabel && !id.empty()) + { + textRenderer->setName(id); + isFirstLabel = false; + } + // textRendererWidth will get 0.0f, when we've got glError: 0x0501 in Label::getContentSize // It happens when currentText is very very long so that can't generate a texture const float textRendererWidth = textRenderer->getContentSize().width; @@ -2094,7 +2221,8 @@ void RichText::handleImageRenderer(std::string_view filePath, int height, std::string_view url, float scaleX, - float scaleY) + float scaleY, + std::string_view id) { Sprite* imageRenderer; if (textureType == Widget::TextureResType::LOCAL) @@ -2110,6 +2238,8 @@ void RichText::handleImageRenderer(std::string_view filePath, if (height != -1) imageRenderer->setScaleY(height / currentSize.height); + imageRenderer->setName(id); + imageRenderer->setScaleX(imageRenderer->getScaleX() * scaleX); imageRenderer->setScaleY(imageRenderer->getScaleY() * scaleY); @@ -2122,9 +2252,15 @@ void RichText::handleImageRenderer(std::string_view filePath, } } -void RichText::handleCustomRenderer(ax::Node* renderer) +void RichText::handleCustomRenderer(ax::Node* renderer, std::string_view id) { Vec2 imgSize = renderer->getContentSize(); + + if (!id.empty()) + { + renderer->setName(id); + } + _leftSpaceWidth -= imgSize.width; if (_leftSpaceWidth < 0.0f) { diff --git a/core/ui/UIRichText.h b/core/ui/UIRichText.h index 13ceb786ad..497dac7009 100644 --- a/core/ui/UIRichText.h +++ b/core/ui/UIRichText.h @@ -121,6 +121,7 @@ public: * @param shadowOffset shadow effect offset value * @param shadowBlurRadius the shadow effect blur radius * @param glowColor glow color + * @param id ID of element * @return True if initialize success, false otherwise. */ bool init(int tag, @@ -136,7 +137,8 @@ public: const Color3B& shadowColor = Color3B::BLACK, const Vec2& shadowOffset = Vec2(2.0, -2.0), int shadowBlurRadius = 0, - const Color3B& glowColor = Color3B::WHITE); + const Color3B& glowColor = Color3B::WHITE, + std::string_view id = ""sv); /** * @brief Create a RichElementText with various arguments. @@ -155,6 +157,7 @@ public: * @param shadowOffset shadow effect offset value * @param shadowBlurRadius the shadow effect blur radius * @param glowColor glow color + * @param id ID of element * @return RichElementText instance. */ static RichElementText* create(int tag, @@ -170,7 +173,8 @@ public: const Color3B& shadowColor = Color3B::BLACK, const Vec2& shadowOffset = Vec2(2.0, -2.0), int shadowBlurRadius = 0, - const Color3B& glowColor = Color3B::WHITE); + const Color3B& glowColor = Color3B::WHITE, + std::string_view id = ""sv); protected: std::string _text; @@ -184,6 +188,7 @@ protected: Vec2 _shadowOffset; /*!< shadow effect offset value */ int _shadowBlurRadius; /*!< the shadow effect blur radius */ Color3B _glowColor; /*!< attributes of glow tag */ + std::string _id; /*!< ID of this text field */ friend class RichText; }; @@ -210,6 +215,7 @@ public: * @param filePath A image file name. * @param url uniform resource locator * @param texType texture type, may be a valid file path, or a sprite frame name + * @param id ID of element * @return True if initialize success, false otherwise. */ bool init(int tag, @@ -217,7 +223,8 @@ public: uint8_t opacity, std::string_view filePath, std::string_view url = "", - Widget::TextureResType texType = Widget::TextureResType::LOCAL); + Widget::TextureResType texType = Widget::TextureResType::LOCAL, + std::string_view id = ""sv); /** * @brief Create a RichElementImage with various arguments. @@ -228,6 +235,7 @@ public: * @param filePath A image file name. * @param url uniform resource locator * @param texType texture type, may be a valid file path, or a sprite frame name + * @param id ID of element * @return A RichElementImage instance. */ static RichElementImage* create(int tag, @@ -235,7 +243,8 @@ public: uint8_t opacity, std::string_view filePath, std::string_view url = "", - Widget::TextureResType texType = Widget::TextureResType::LOCAL); + Widget::TextureResType texType = Widget::TextureResType::LOCAL, + std::string_view id = ""sv); void setWidth(int width); void setHeight(int height); @@ -243,6 +252,7 @@ public: void setScaleX(float scaleX) { _scaleX = scaleX; } void setScaleY(float scaleY) { _scaleY = scaleY; } void setUrl(std::string_view url); + void setId(std::string_view id); protected: std::string _filePath; @@ -254,6 +264,7 @@ protected: float _scaleX; float _scaleY; std::string _url; /*!< attributes of anchor tag */ + std::string _id; /*!< attributes of anchor tag */ }; /** @@ -283,9 +294,10 @@ public: * @param color A color in Color3B. * @param opacity A opacity in GLubyte. * @param customNode A custom node pointer. + * @param id ID of element * @return True if initialize success, false otherwise. */ - bool init(int tag, const Color3B& color, uint8_t opacity, Node* customNode); + bool init(int tag, const Color3B& color, uint8_t opacity, Node* customNode, std::string_view id = ""sv); /** * @brief Create a RichElementCustomNode with various arguments. @@ -294,12 +306,19 @@ public: * @param color A color in Color3B. * @param opacity A opacity in GLubyte. * @param customNode A custom node pointer. + * @param id ID of element * @return A RichElementCustomNode instance. */ - static RichElementCustomNode* create(int tag, const Color3B& color, uint8_t opacity, Node* customNode); + static RichElementCustomNode* create(int tag, + const Color3B& color, + uint8_t opacity, + Node* customNode, + std::string_view id = ""sv); protected: Node* _customNode{}; + std::string _id; + friend class RichText; }; @@ -394,7 +413,7 @@ public: 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 */ static const std::string KEY_HORIZONTAL_ALIGNMENT; /*!< key of left, right, or center */ - static const std::string KEY_VERTICAL_ALIGNMENT; /*!< key of left, right, or center */ + static const std::string KEY_VERTICAL_ALIGNMENT; /*!< key of left, right, or center */ static const std::string KEY_FONT_COLOR_STRING; /*!< key of font color */ static const std::string KEY_FONT_SIZE; /*!< key of font size */ static const std::string KEY_FONT_SMALL; /*!< key of font size small */ @@ -431,6 +450,7 @@ public: static const std::string KEY_ANCHOR_TEXT_SHADOW_OFFSET_HEIGHT; /*!< key of shadow offset (height) of anchor tag */ static const std::string KEY_ANCHOR_TEXT_SHADOW_BLUR_RADIUS; /*!< key of shadow blur radius of anchor tag */ static const std::string KEY_ANCHOR_TEXT_GLOW_COLOR; /*!< key of glow color of anchor tag */ + static const std::string KEY_ID; /*!< key of id */ /** * @brief Default constructor. @@ -611,7 +631,8 @@ protected: const Color3B& shadowColor = Color3B::BLACK, const Vec2& shadowOffset = Vec2(2.0, -2.0), int shadowBlurRadius = 0, - const Color3B& glowColor = Color3B::WHITE); + const Color3B& glowColor = Color3B::WHITE, + std::string_view id = ""sv); void handleImageRenderer(std::string_view filePath, Widget::TextureResType textureType, const Color3B& color, @@ -619,9 +640,10 @@ protected: int width, int height, std::string_view url, - float scaleX = 1.f, - float scaleY = 1.f); - void handleCustomRenderer(Node* renderer); + float scaleX = 1.f, + float scaleY = 1.f, + std::string_view id = ""sv); + void handleCustomRenderer(Node* renderer, std::string_view id = ""sv); void formatRenderers(); void addNewLine(int quantity = 1); void doHorizontalAlignment(const Vector& row, float rowWidth); diff --git a/core/ui/UIScrollView.cpp b/core/ui/UIScrollView.cpp index 9cc8c3b9c3..46d2e66c0b 100644 --- a/core/ui/UIScrollView.cpp +++ b/core/ui/UIScrollView.cpp @@ -1,6 +1,7 @@ /**************************************************************************** Copyright (c) 2013-2016 Chukong Technologies Inc. Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd. +Copyright (c) 2019-present Axmol Engine contributors (see AUTHORS.md). https://axmolengine.github.io/ @@ -32,19 +33,23 @@ THE SOFTWARE. #include "2d/Camera.h" NS_AX_BEGIN -static const int NUMBER_OF_GATHERED_TOUCHES_FOR_MOVE_SPEED = 5; -static const float OUT_OF_BOUNDARY_BREAKING_FACTOR = 0.05f; -static const float BOUNCE_BACK_DURATION = 1.0f; +namespace +{ +constexpr float DEFAULT_TIME_IN_SEC_FOR_SCROLL_TO_ITEM = 1.0f; +constexpr int NUMBER_OF_GATHERED_TOUCHES_FOR_MOVE_SPEED = 5; +constexpr float OUT_OF_BOUNDARY_BREAKING_FACTOR = 0.05f; +constexpr float BOUNCE_BACK_DURATION = 1.0f; -#define MOVE_INCH 7.0f / 160.0f - -static float convertDistanceFromPointToInch(const Vec2& dis) +float convertDistanceFromPointToInch(const Vec2& dis) { auto glView = Director::getInstance()->getGLView(); int dpi = Device::getDPI(); float distance = Vec2(dis.x * glView->getScaleX() / dpi, dis.y * glView->getScaleY() / dpi).getLength(); return distance; } +} // namespace + +#define MOVE_INCH (7.0f / 160.0f) namespace ui { @@ -78,6 +83,7 @@ ScrollView::ScrollView() , _horizontalScrollBar(nullptr) , _scrollViewEventListener(nullptr) , _eventCallback(nullptr) + , _scrollTime(DEFAULT_TIME_IN_SEC_FOR_SCROLL_TO_ITEM) { setTouchEnabled(true); _propagateTouchEvents = false; @@ -1587,6 +1593,51 @@ Widget* ScrollView::findNextFocusedWidget(ax::ui::Widget::FocusDirection directi return Widget::findNextFocusedWidget(direction, current); } } + +void ScrollView::setScrollDuration(float time) +{ + if (time >= 0) + _scrollTime = time; +} + +float ScrollView::getScrollDuration() const +{ + return _scrollTime; +} + +Vec2 ScrollView::calculateItemPositionWithAnchor(const Node* node, const Vec2& itemAnchorPoint) +{ + auto boundingBox = node->getBoundingBox(); + Vec2 origin(boundingBox.origin.x, boundingBox.origin.y); + Vec2 size = node->getContentSize(); + return origin + Vec2(size.width * itemAnchorPoint.x, size.height * itemAnchorPoint.y); +} + +Vec2 ScrollView::calculateItemDestination(const Vec2& positionRatioInView, const Node* item, const Vec2& itemAnchorPoint) +{ + const Vec2& contentSize = getContentSize(); + Vec2 positionInView; + positionInView.x += contentSize.width * positionRatioInView.x; + positionInView.y += contentSize.height * positionRatioInView.y; + + Vec2 itemPosition = calculateItemPositionWithAnchor(item, itemAnchorPoint); + return -(itemPosition - positionInView); +} + +void ScrollView::scrollToItem(Node* item, const Vec2& positionRatioInView, const Vec2& itemAnchorPoint) +{ + scrollToItem(item, positionRatioInView, itemAnchorPoint, _scrollTime); +} + +void ScrollView::scrollToItem(Node* item, const Vec2& positionRatioInView, const Vec2& itemAnchorPoint, float timeInSec) +{ + if (item == nullptr) + { + return; + } + auto destination = calculateItemDestination(positionRatioInView, item, itemAnchorPoint); + startAutoScrollToDestination(destination, timeInSec, true); +} } // namespace ui NS_AX_END diff --git a/core/ui/UIScrollView.h b/core/ui/UIScrollView.h index e724f06463..413e4c39bc 100644 --- a/core/ui/UIScrollView.h +++ b/core/ui/UIScrollView.h @@ -1,6 +1,7 @@ /**************************************************************************** Copyright (c) 2013-2016 Chukong Technologies Inc. Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd. +Copyright (c) 2019-present Axmol Engine contributors (see AUTHORS.md). https://axmolengine.github.io/ @@ -101,7 +102,7 @@ public: * @js NA * @lua NA */ - virtual ~ScrollView(); + ~ScrollView() override; /** * Create an empty ScrollView. @@ -340,24 +341,24 @@ public: virtual void addEventListener(const ccScrollViewCallback& callback); // override functions - virtual void addChild(Node* child) override; - virtual void addChild(Node* child, int localZOrder) override; - virtual void addChild(Node* child, int localZOrder, int tag) override; - virtual void addChild(Node* child, int localZOrder, std::string_view name) override; - virtual void removeAllChildren() override; - virtual void removeAllChildrenWithCleanup(bool cleanup) override; - virtual void removeChild(Node* child, bool cleanup = true) override; - virtual Vector& getChildren() override; - virtual const Vector& getChildren() const override; - virtual ssize_t getChildrenCount() const override; - virtual Node* getChildByTag(int tag) const override; - virtual Node* getChildByName(std::string_view name) const override; + void addChild(Node* child) override; + void addChild(Node* child, int localZOrder) override; + void addChild(Node* child, int localZOrder, int tag) override; + void addChild(Node* child, int localZOrder, std::string_view name) override; + void removeAllChildren() override; + void removeAllChildrenWithCleanup(bool cleanup) override; + void removeChild(Node* child, bool cleanup = true) override; + Vector& getChildren() override; + const Vector& getChildren() const override; + ssize_t getChildrenCount() const override; + Node* getChildByTag(int tag) const override; + Node* getChildByName(std::string_view name) const override; // touch event callback - virtual bool onTouchBegan(Touch* touch, Event* unusedEvent) override; - virtual void onTouchMoved(Touch* touch, Event* unusedEvent) override; - virtual void onTouchEnded(Touch* touch, Event* unusedEvent) override; - virtual void onTouchCancelled(Touch* touch, Event* unusedEvent) override; - virtual void update(float dt) override; + bool onTouchBegan(Touch* touch, Event* unusedEvent) override; + void onTouchMoved(Touch* touch, Event* unusedEvent) override; + void onTouchEnded(Touch* touch, Event* unusedEvent) override; + void onTouchCancelled(Touch* touch, Event* unusedEvent) override; + void update(float dt) override; /** * @brief Toggle bounce enabled when scroll to the edge. @@ -453,7 +454,7 @@ public: /** * @brief Set the scroll bar's color * - * @param the scroll bar's color + * @param color the scroll bar's color */ void setScrollBarColor(const Color3B& color); @@ -467,7 +468,7 @@ public: /** * @brief Set the scroll bar's opacity * - * @param the scroll bar's opacity + * @param opacity the scroll bar's opacity */ void setScrollBarOpacity(uint8_t opacity); @@ -481,7 +482,7 @@ public: /** * @brief Set scroll bar auto hide state * - * @param scroll bar auto hide state + * @param autoHideEnabled scroll bar auto hide state */ void setScrollBarAutoHideEnabled(bool autoHideEnabled); @@ -495,7 +496,7 @@ public: /** * @brief Set scroll bar auto hide time * - * @param scroll bar auto hide time + * @param autoHideTime bar auto hide time */ void setScrollBarAutoHideTime(float autoHideTime); @@ -509,7 +510,7 @@ public: /** * @brief Set the touch total time threshold * - * @param the touch total time threshold + * @param touchTotalTimeThreshold the touch total time threshold */ void setTouchTotalTimeThreshold(float touchTotalTimeThreshold); @@ -526,7 +527,7 @@ public: * @see `Layout::Type` * @param type Layout type enum. */ - virtual void setLayoutType(Type type) override; + void setLayoutType(Type type) override; /** * Get the layout type for scrollview. @@ -534,22 +535,22 @@ public: * @see `Layout::Type` * @return LayoutType */ - virtual Type getLayoutType() const override; + Type getLayoutType() const override; /** * Return the "class name" of widget. */ - virtual std::string getDescription() const override; + std::string getDescription() const override; /** * @lua NA */ - virtual void onEnter() override; + void onEnter() override; /** * @lua NA */ - virtual void onExit() override; + void onExit() override; /** * When a widget is in a layout, you could call this method to get the next focused widget within a specified @@ -558,7 +559,22 @@ public: *@param current the current focused widget *@return the next focused widget in a layout */ - virtual Widget* findNextFocusedWidget(FocusDirection direction, Widget* current) override; + Widget* findNextFocusedWidget(FocusDirection direction, Widget* current) override; + + /** + * Set the time in seconds to scroll between items. + * Subsequent calls of function 'scrollToItem', will take 'time' seconds for scrolling. + * @param time The seconds needed to scroll between two items. 'time' must be >= 0 + * @see scrollToItem(ssize_t, const Vec2&, const Vec2&) + */ + void setScrollDuration(float time); + + /** + * Get the time in seconds to scroll between items. + * @return The time in seconds to scroll between items + * @see setScrollDuration(float) + */ + float getScrollDuration() const; /** * @return Whether the user is currently dragging the ScrollView to scroll it @@ -569,7 +585,26 @@ public: */ bool isAutoScrolling() const { return _autoScrolling; } - virtual bool init() override; + bool init() override; + + /** + * @brief Scroll to specific item + * @param item Item to scroll to + * @param positionRatioInView Specifies the position with ratio in list view's content size. + * @param itemAnchorPoint Specifies an anchor point of each item for position to calculate distance. + */ + void scrollToItem(Node* item, const Vec2& positionRatioInView, const Vec2& itemAnchorPoint); + + /** + * @brief Scroll to specific item + * @param item Item to scroll to + * @param positionRatioInView Specifies the position with ratio in list view's content size. + * @param itemAnchorPoint Specifies an anchor point of each item for position to calculate distance. + * @param timeInSec Scroll time + */ + void scrollToItem(Node* item, const Vec2& positionRatioInView, const Vec2& itemAnchorPoint, float timeInSec); + + static Vec2 calculateItemPositionWithAnchor(const Node* node, const Vec2& itemAnchorPoint); protected: enum class MoveDirection @@ -580,14 +615,14 @@ protected: RIGHT, }; - virtual void initRenderer() override; + void initRenderer() override; - virtual void onSizeChanged() override; - virtual void doLayout() override; + void onSizeChanged() override; + void doLayout() override; - virtual Widget* createCloneInstance() override; - virtual void copySpecialProperties(Widget* model) override; - virtual void copyClonedWidgetChildren(Widget* model) override; + Widget* createCloneInstance() override; + void copySpecialProperties(Widget* model) override; + void copyClonedWidgetChildren(Widget* model) override; virtual void initScrollBar(); virtual void removeScrollBar(); @@ -622,7 +657,7 @@ protected: virtual void handleMoveLogic(Touch* touch); virtual void handleReleaseLogic(Touch* touch); - virtual void interceptTouchEvent(Widget::TouchEventType event, Widget* sender, Touch* touch) override; + void interceptTouchEvent(Widget::TouchEventType event, Widget* sender, Touch* touch) override; void processScrollEvent(MoveDirection dir, bool bounce); void processScrollingEvent(); @@ -631,6 +666,8 @@ protected: void updateScrollBar(const Vec2& outOfBoundary); + Vec2 calculateItemDestination(const Vec2& positionRatioInView, const Node* item, const Vec2& itemAnchorPoint); + protected: virtual float getAutoScrollStopEpsilon() const; bool fltEqualZero(const Vec2& point) const; @@ -678,6 +715,8 @@ protected: Ref* _scrollViewEventListener; ccScrollViewCallback _eventCallback; + + float _scrollTime; }; } // namespace ui diff --git a/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.cpp b/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.cpp index fbd2438ace..970083b627 100644 --- a/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.cpp +++ b/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.cpp @@ -53,6 +53,7 @@ UIRichTextTests::UIRichTextTests() ADD_TEST_CASE(UIRichTextNewline); ADD_TEST_CASE(UIRichTextHeaders); ADD_TEST_CASE(UIRichTextParagraph); + ADD_TEST_CASE(UIRichTextScrollTo); } // @@ -1053,3 +1054,130 @@ bool UIRichTextParagraph::init() } return false; } + +bool UIRichTextScrollTo::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, 150); +#endif + + // ScrollView + _scrollView = ScrollView::create(); + _scrollView->setContentSize(_defaultContentSize); + _scrollView->setInnerContainerSize(_defaultContentSize); + _scrollView->setBounceEnabled(true); + _scrollView->setDirection(ScrollView::Direction::VERTICAL); + _scrollView->setLayoutType(Layout::Type::VERTICAL); + _scrollView->setScrollBarEnabled(true); + _scrollView->setScrollBarWidth(4); + _scrollView->setScrollBarPositionFromCorner(Vec2(2, 2)); + _scrollView->setScrollBarColor(Color3B::WHITE); + _scrollView->setScrollBarAutoHideEnabled(false); + _scrollView->setAnchorPoint(Vec2::ANCHOR_MIDDLE); + _scrollView->setPosition(_widget->getContentSize() / 2); + _scrollView->setLocalZOrder(10); + _widget->addChild(_scrollView); + + ValueMap valMap; + valMap[RichText::KEY_ANCHOR_FONT_COLOR_STRING] = "#00ffdd"; + + // RichText + _richText = RichText::createWithXML( + R"(

Quick Links

+Jump To Fancy Header
+Jump To Second Paragraph
+Jump To Web Search
+
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et +dolore magna aliqua. +Purus faucibus ornare suspendisse sed nisi. Viverra aliquet eget sit amet tellus cras adipiscing. +Ut tellus elementum sagittis vitae. +Risus feugiat in ante metus dictum at. Semper eget duis at tellus at. Iaculis eu non diam phasellus vestibulum +lorem sed risus. +Sed vulputate odio ut enim. Morbi tristique senectus et netus et malesuada fames.

+

Fancy Header

+

This is the second paragraph! Cras sed felis eget velit aliquet sagittis id consectetur purus. +Turpis nunc eget lorem dolor sed viverra ipsum nunc. +Ultrices tincidunt arcu non sodales neque sodales ut etiam sit. Risus feugiat in ante metus dictum at tempor. +Id neque aliquam vestibulum morbi blandit cursus risus.

+

Tortor condimentum lacinia quis vel eros donec ac. Molestie ac feugiat sed lectus. +Aliquam id diam maecenas ultricies mi eget mauris. +Ullamcorper malesuada proin libero nunc consequat interdum varius. +Sollicitudin nibh sit amet commodo nulla facilisi nullam vehicula ipsum. +Diam quam nulla porttitor massa id neque aliquam vestibulum morbi. Sed velit dignissim sodales ut. +Morbi leo urna molestie at elementum eu facilisis. +Cursus metus aliquam eleifend mi in. Euismod lacinia at quis risus sed vulputate odio. +Sit amet mattis vulputate enim nulla aliquet porttitor lacus luctus.

+Google!)", + valMap); + _richText->ignoreContentAdaptWithSize(false); + _richText->setContentSize(Size(_defaultContentSize.width, 0)); + _richText->setAnchorPoint(Vec2::ANCHOR_MIDDLE_TOP); + _richText->setOpenUrlHandler([this](std::string_view url) { + // Check if the href starts with a "#" character + if (url.starts_with('#')) + { + auto* node = _richText->getProtectedChildByName(url.substr(1)); + if (node) + { + // Scroll to the location of that node, and the reason it works is because + // the ScrollView inner container is the same height as the RichText + _scrollView->scrollToItem(node, Vec2::ANCHOR_MIDDLE_TOP, Vec2::ANCHOR_MIDDLE_TOP); + } + } + else if (!url.empty()) + { + Application::getInstance()->openURL(url); + } + }); + + const auto contentSize = _scrollView->getInnerContainerSize(); + _richText->setPosition(Vec2(contentSize.width / 2, contentSize.height)); + _richText->setLocalZOrder(10); + + _richText->formatText(); + _scrollView->addChild(_richText); + + updateScrollViewSize(); + + // test remove all children, this call won't effect the test + _richText->removeAllChildren(); + + return true; + } + return false; +} + +void UIRichTextScrollTo::updateScrollViewSize() +{ + auto newHeight = 0.f; + auto&& children = _scrollView->getChildren(); + for (auto&& child : children) + { + auto&& contentSize = child->getContentSize(); + newHeight += contentSize.height; + if (const auto* widget = dynamic_cast(child)) + { + if (const auto* layoutParam = widget->getLayoutParameter()) + { + auto&& margin = layoutParam->getMargin(); + newHeight += margin.top + margin.bottom; + } + } + } + _scrollView->setInnerContainerSize(Size(_scrollView->getInnerContainerSize().width, newHeight)); + _scrollView->scrollToTop(0.f, false); +} diff --git a/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.h b/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.h index f8a5476b3e..a2a9c063ce 100644 --- a/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.h +++ b/tests/cpp-tests/Source/UITest/CocoStudioGUITest/UIRichTextTest/UIRichTextTest.h @@ -220,4 +220,17 @@ public: bool init() override; }; +class UIRichTextScrollTo : public UIRichTextTestBase +{ +public: + CREATE_FUNC(UIRichTextScrollTo); + + bool init() override; + +protected: + void updateScrollViewSize(); + + ax::ui::ScrollView* _scrollView; +}; + #endif /* defined(__TestCpp__UIRichTextTest__) */