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.
This commit is contained in:
RH 2024-02-15 11:33:38 +11:00 committed by GitHub
parent 2ca2c41515
commit 8c23d32235
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 254 additions and 75 deletions

View File

@ -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<TagBehavior> 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, RichElement*> {
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<RichElementNewLine*>(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<RichElement*>(_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<RichElementNewLine*>(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()

View File

@ -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<std::pair<ValueMap, RichElement*>(const ValueMap& tagAttrValueMap)> VisitEnterHandler;
typedef std::function<RichElement*()> 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<Node*>& row, float rowWidth);
float stripTrailingWhitespace(const Vector<Node*>& row);
@ -657,5 +645,3 @@ protected:
// end of ui group
/// @}
NS_AX_END
#endif /* defined(__UIRichText__) */

View File

@ -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 face="fonts/arial.ttf">h1. HEADING</h1><h2 face="fonts/Marker Felt.ttf">h2. HEADING</h2><h3 face="fonts/American Typewriter.ttf">h3. HEADING</h3><h4>h4. HEADING</h4><h5>h5. HEADING</h5><h6>h6. HEADING</h6>)");
_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(
"<p><b>Paragraph1: </b>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.</p><p><b>Paragraph2: </b>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.</p>");
_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;
}

View File

@ -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__) */