mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-05-28 17:16:29 +03:00
RichText class now can hold any Drawable.
Added UIRichText with spans (UITextSpan). Still a WIP but core is working.
This commit is contained in:
@@ -357,6 +357,12 @@
|
||||
"command": "${project_root}/bin/eepp-ui-application-hello-world",
|
||||
"name": "eepp-ui-application-hello-world-debug",
|
||||
"working_dir": "${project_root}/bin"
|
||||
},
|
||||
{
|
||||
"args": "",
|
||||
"command": "${project_root}/bin/eepp-ui-richtext-debug",
|
||||
"name": "eepp-ui-richtext-debug",
|
||||
"working_dir": "${project_root}/bin"
|
||||
}
|
||||
],
|
||||
"var": {
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -80,3 +80,4 @@ ecode.dmg
|
||||
/projects/android-project/app/.classpath
|
||||
/projects/android-project/.project
|
||||
/projects/android-project/app/.project
|
||||
/design/
|
||||
|
||||
@@ -55,14 +55,38 @@ class EE_API RichText : public Drawable {
|
||||
/** @brief Sets the text alignment (Left, Center, Right). */
|
||||
void setAlign( Uint32 align );
|
||||
|
||||
/** @return The text alignment. */
|
||||
Uint32 getAlign() const { return mAlign; }
|
||||
|
||||
/** @brief Sets the maximum width for wrapping. If 0, wrapping is disabled. */
|
||||
void setMaxWidth( Float width );
|
||||
|
||||
/** @return The maximum width for wrapping. */
|
||||
Float getMaxWidth() const { return mMaxWidth; }
|
||||
|
||||
/** @return The list of text spans. */
|
||||
std::vector<std::shared_ptr<Text>>& getSpans() { return mSpans; }
|
||||
enum class BlockType { Text, Drawable, CustomSize };
|
||||
|
||||
struct Block {
|
||||
BlockType type{ BlockType::Text };
|
||||
std::shared_ptr<Text> text;
|
||||
std::shared_ptr<Drawable> drawable;
|
||||
Sizef customSize;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Adds a drawable (e.g., an image) into the text flow.
|
||||
* @param drawable The drawable to add.
|
||||
*/
|
||||
void addDrawable( std::shared_ptr<Drawable> drawable );
|
||||
|
||||
/**
|
||||
* @brief Adds a custom size spacer into the text flow.
|
||||
* @param size The physical dimensions of the spacer.
|
||||
*/
|
||||
void addCustomSize( const Sizef& size );
|
||||
|
||||
/** @return The list of blocks. */
|
||||
const std::vector<Block>& getBlocks() { return mBlocks; }
|
||||
|
||||
virtual void draw( const Float& X, const Float& Y, const Vector2f& scale = Vector2f::One,
|
||||
const Float& rotation = 0, BlendMode effect = BlendMode::Alpha(),
|
||||
@@ -86,8 +110,9 @@ class EE_API RichText : public Drawable {
|
||||
|
||||
/** @brief Structure representing a rendered span within a line. */
|
||||
struct RenderSpan {
|
||||
std::shared_ptr<Text> text;
|
||||
Block block;
|
||||
Vector2f position; // Local position relative to RichText origin
|
||||
Sizef size;
|
||||
};
|
||||
|
||||
/** @brief Structure representing a rendered paragraph (line). */
|
||||
@@ -103,7 +128,7 @@ class EE_API RichText : public Drawable {
|
||||
const std::vector<RenderParagraph>& getLines() const { return mLines; }
|
||||
|
||||
protected:
|
||||
std::vector<std::shared_ptr<Text>> mSpans;
|
||||
std::vector<Block> mBlocks;
|
||||
std::vector<RenderParagraph> mLines;
|
||||
FontStyleConfig mDefaultStyle;
|
||||
Uint32 mAlign{ TEXT_ALIGN_LEFT };
|
||||
|
||||
@@ -43,6 +43,7 @@ enum UIFlag : Uint32 {
|
||||
UI_SCROLLABLE = ( 1 << 27 ),
|
||||
UI_HIGHLIGHT = ( 1 << 28 ),
|
||||
UI_PARENT_ATTRIBUTE_CHANGED = ( 1 << 29 ),
|
||||
UI_LOADS_ITS_CHILDREN = ( 1 << 30 ),
|
||||
};
|
||||
|
||||
enum UINodeType {
|
||||
@@ -107,6 +108,8 @@ enum UINodeType {
|
||||
UI_TYPE_IMAGE_VIEWER,
|
||||
UI_TYPE_AUDIO_PLAYER,
|
||||
UI_TYPE_NODELINK,
|
||||
UI_TYPE_TEXTSPAN,
|
||||
UI_TYPE_RICHTEXT,
|
||||
UI_TYPE_MODULES = 10000,
|
||||
UI_TYPE_TERMINAL = 10001,
|
||||
UI_TYPE_USER = 200000,
|
||||
|
||||
89
include/eepp/ui/uirichtext.hpp
Normal file
89
include/eepp/ui/uirichtext.hpp
Normal file
@@ -0,0 +1,89 @@
|
||||
#ifndef EE_UI_UIRICHTEXT_HPP
|
||||
#define EE_UI_UIRICHTEXT_HPP
|
||||
|
||||
#include <eepp/graphics/richtext.hpp>
|
||||
#include <eepp/ui/uilayout.hpp>
|
||||
|
||||
namespace EE { namespace UI {
|
||||
|
||||
class EE_API UIRichText : public UILayout {
|
||||
public:
|
||||
static UIRichText* New();
|
||||
|
||||
static UIRichText* NewWithTag( const std::string& tag );
|
||||
|
||||
explicit UIRichText( const std::string& tag = "richtext" );
|
||||
|
||||
virtual ~UIRichText();
|
||||
|
||||
virtual Uint32 getType() const;
|
||||
|
||||
virtual bool isType( const Uint32& type ) const;
|
||||
|
||||
virtual void draw();
|
||||
|
||||
virtual void loadFromXmlNode( const pugi::xml_node& node );
|
||||
|
||||
virtual bool applyProperty( const StyleSheetProperty& attribute );
|
||||
|
||||
virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
|
||||
const Uint32& propertyIndex = 0 ) const;
|
||||
|
||||
virtual std::vector<PropertyId> getPropertiesImplemented() const;
|
||||
|
||||
Graphics::RichText* getRichText();
|
||||
|
||||
Graphics::Font* getFont() const;
|
||||
|
||||
UIRichText* setFont( Graphics::Font* font );
|
||||
|
||||
Uint32 getFontSize() const;
|
||||
|
||||
UIRichText* setFontSize( const Uint32& characterSize );
|
||||
|
||||
const Uint32& getFontStyle() const;
|
||||
|
||||
UIRichText* setFontStyle( const Uint32& fontStyle );
|
||||
|
||||
const Color& getFontColor() const;
|
||||
|
||||
UIRichText* setFontColor( const Color& color );
|
||||
|
||||
const Color& getFontShadowColor() const;
|
||||
|
||||
UIRichText* setFontShadowColor( const Color& color );
|
||||
|
||||
const Vector2f& getFontShadowOffset() const;
|
||||
|
||||
UIRichText* setFontShadowOffset( const Vector2f& offset );
|
||||
|
||||
const Float& getOutlineThickness() const;
|
||||
|
||||
UIRichText* setOutlineThickness( const Float& outlineThickness );
|
||||
|
||||
const Color& getOutlineColor() const;
|
||||
|
||||
UIRichText* setOutlineColor( const Color& outlineColor );
|
||||
|
||||
Uint32 getTextAlign() const;
|
||||
|
||||
UIRichText* setTextAlign( const Uint32& align );
|
||||
|
||||
protected:
|
||||
Graphics::RichText* mRichText;
|
||||
|
||||
virtual void onSizeChange();
|
||||
virtual void onPaddingChange();
|
||||
virtual void onLayoutUpdate();
|
||||
virtual void onChildCountChange( Node* child, const bool& removed );
|
||||
virtual void onFontChanged();
|
||||
virtual void onFontStyleChanged();
|
||||
virtual void onAlphaChange();
|
||||
|
||||
void rebuildRichText();
|
||||
void positionChildren();
|
||||
};
|
||||
|
||||
}} // namespace EE::UI
|
||||
|
||||
#endif
|
||||
86
include/eepp/ui/uitextspan.hpp
Normal file
86
include/eepp/ui/uitextspan.hpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#ifndef EE_UI_UITEXTSPAN_HPP
|
||||
#define EE_UI_UITEXTSPAN_HPP
|
||||
|
||||
#include <eepp/ui/uifontstyleconfig.hpp>
|
||||
#include <eepp/ui/uiwidget.hpp>
|
||||
|
||||
namespace EE { namespace UI {
|
||||
|
||||
class EE_API UITextSpan : public UIWidget {
|
||||
public:
|
||||
static UITextSpan* New();
|
||||
|
||||
static UITextSpan* NewWithTag( const std::string& tag );
|
||||
|
||||
virtual ~UITextSpan();
|
||||
|
||||
virtual Uint32 getType() const;
|
||||
|
||||
virtual bool isType( const Uint32& type ) const;
|
||||
|
||||
virtual void draw();
|
||||
|
||||
virtual bool applyProperty( const StyleSheetProperty& attribute );
|
||||
|
||||
virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
|
||||
const Uint32& propertyIndex = 0 ) const;
|
||||
|
||||
virtual std::vector<PropertyId> getPropertiesImplemented() const;
|
||||
|
||||
const String& getText() const;
|
||||
|
||||
UITextSpan* setText( const String& text );
|
||||
|
||||
const UIFontStyleConfig& getFontStyleConfig() const;
|
||||
|
||||
virtual void loadFromXmlNode( const pugi::xml_node& node );
|
||||
|
||||
void setFontStyleConfig( const UIFontStyleConfig& fontStyleConfig );
|
||||
|
||||
Graphics::Font* getFont() const;
|
||||
|
||||
UITextSpan* setFont( Graphics::Font* font );
|
||||
|
||||
Uint32 getFontSize() const;
|
||||
|
||||
UITextSpan* setFontSize( const Uint32& characterSize );
|
||||
|
||||
const Uint32& getFontStyle() const;
|
||||
|
||||
UITextSpan* setFontStyle( const Uint32& fontStyle );
|
||||
|
||||
const Float& getOutlineThickness() const;
|
||||
|
||||
UITextSpan* setOutlineThickness( const Float& outlineThickness );
|
||||
|
||||
const Color& getOutlineColor() const;
|
||||
|
||||
UITextSpan* setOutlineColor( const Color& outlineColor );
|
||||
|
||||
const Color& getFontColor() const;
|
||||
|
||||
UITextSpan* setFontColor( const Color& color );
|
||||
|
||||
const Color& getFontShadowColor() const;
|
||||
|
||||
UITextSpan* setFontShadowColor( const Color& color );
|
||||
|
||||
const Vector2f& getFontShadowOffset() const;
|
||||
|
||||
UITextSpan* setFontShadowOffset( const Vector2f& offset );
|
||||
|
||||
protected:
|
||||
String mText;
|
||||
UIFontStyleConfig mFontStyleConfig;
|
||||
|
||||
explicit UITextSpan( const std::string& tag = "span" );
|
||||
|
||||
virtual void onAlphaChange();
|
||||
virtual void onFontChanged();
|
||||
virtual void onFontStyleChanged();
|
||||
virtual void onTextChanged();
|
||||
};
|
||||
|
||||
}} // namespace EE::UI
|
||||
|
||||
#endif
|
||||
@@ -496,6 +496,11 @@ class EE_API UIWidget : public UINode {
|
||||
*/
|
||||
virtual void loadFromXmlNode( const pugi::xml_node& node );
|
||||
|
||||
/**
|
||||
* @brief Boolean that indicates if the widget is in charge of loading its children nodes
|
||||
*/
|
||||
bool loadsItsChildren() const;
|
||||
|
||||
/**
|
||||
* @brief Notifies that layout attributes have changed.
|
||||
*
|
||||
|
||||
@@ -1574,6 +1574,12 @@ solution "eepp"
|
||||
files { "src/examples/ui_application_hello_world/*.cpp" }
|
||||
build_link_configuration( "eepp-ui-application-hello-world", true )
|
||||
|
||||
project "eepp-ui-richtext"
|
||||
set_kind()
|
||||
language "C++"
|
||||
files { "src/examples/ui_richtext/*.cpp" }
|
||||
build_link_configuration( "eepp-ui-richtext", true )
|
||||
|
||||
project "eepp-richtext"
|
||||
set_kind()
|
||||
language "C++"
|
||||
|
||||
@@ -1458,6 +1458,12 @@ workspace "eepp"
|
||||
files { "src/examples/ui_application_hello_world/*.cpp" }
|
||||
build_link_configuration( "eepp-ui-application-hello-world", true )
|
||||
|
||||
project "eepp-ui-richtext"
|
||||
set_kind()
|
||||
language "C++"
|
||||
files { "src/examples/ui_richtext/*.cpp" }
|
||||
build_link_configuration( "eepp-ui-richtext", true )
|
||||
|
||||
project "eepp-richtext"
|
||||
set_kind()
|
||||
language "C++"
|
||||
|
||||
@@ -42,13 +42,20 @@ void RichText::draw( const Float& X, const Float& Y, const Vector2f& scale, cons
|
||||
|
||||
Vector2f pos = span.position;
|
||||
|
||||
if ( rotation == 0 && scale == Vector2f::One ) {
|
||||
span.text->draw( std::trunc( X + pos.x ), std::trunc( Y + line.y + pos.y ),
|
||||
Vector2f::One, 0, effect );
|
||||
} else {
|
||||
span.text->draw( std::trunc( X + pos.x * scale.x ),
|
||||
std::trunc( Y + ( line.y + pos.y ) * scale.y ), scale, rotation,
|
||||
effect, rotationCenter, scaleCenter );
|
||||
if ( span.block.type == BlockType::Text ) {
|
||||
if ( rotation == 0 && scale == Vector2f::One ) {
|
||||
span.block.text->draw( std::trunc( X + pos.x ),
|
||||
std::trunc( Y + line.y + pos.y ), Vector2f::One, 0,
|
||||
effect );
|
||||
} else {
|
||||
span.block.text->draw( std::trunc( X + pos.x * scale.x ),
|
||||
std::trunc( Y + ( line.y + pos.y ) * scale.y ), scale,
|
||||
rotation, effect, rotationCenter, scaleCenter );
|
||||
}
|
||||
} else if ( span.block.type == BlockType::Drawable && span.block.drawable ) {
|
||||
span.block.drawable->draw(
|
||||
Vector2f( std::trunc( X + pos.x ), std::trunc( Y + line.y + pos.y ) ),
|
||||
span.size );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +72,28 @@ void RichText::addSpan( const String& text, const FontStyleConfig& style ) {
|
||||
auto span = std::make_shared<Text>();
|
||||
span->setString( text );
|
||||
span->setStyleConfig( style );
|
||||
mSpans.push_back( span );
|
||||
Block block;
|
||||
block.type = BlockType::Text;
|
||||
block.text = span;
|
||||
mBlocks.push_back( block );
|
||||
mNeedsLayoutUpdate = true;
|
||||
}
|
||||
|
||||
void RichText::addDrawable( std::shared_ptr<Drawable> drawable ) {
|
||||
if ( !drawable )
|
||||
return;
|
||||
Block block;
|
||||
block.type = BlockType::Drawable;
|
||||
block.drawable = drawable;
|
||||
mBlocks.push_back( block );
|
||||
mNeedsLayoutUpdate = true;
|
||||
}
|
||||
|
||||
void RichText::addCustomSize( const Sizef& size ) {
|
||||
Block block;
|
||||
block.type = BlockType::CustomSize;
|
||||
block.customSize = size;
|
||||
mBlocks.push_back( block );
|
||||
mNeedsLayoutUpdate = true;
|
||||
}
|
||||
|
||||
@@ -85,7 +113,7 @@ void RichText::addSpan( const String& text, Font* font, Uint32 characterSize, Co
|
||||
}
|
||||
|
||||
void RichText::clear() {
|
||||
mSpans.clear();
|
||||
mBlocks.clear();
|
||||
mLines.clear();
|
||||
mNeedsLayoutUpdate = true;
|
||||
}
|
||||
@@ -111,8 +139,10 @@ void RichText::setMaxWidth( Float width ) {
|
||||
|
||||
void RichText::invalidate() {
|
||||
mNeedsLayoutUpdate = true;
|
||||
for ( auto& span : mSpans ) {
|
||||
span->invalidate();
|
||||
for ( auto& block : mBlocks ) {
|
||||
if ( block.type == BlockType::Text && block.text ) {
|
||||
block.text->invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,62 +156,96 @@ void RichText::updateLayout() {
|
||||
Float curX = 0;
|
||||
Float maxWidth = 0;
|
||||
|
||||
for ( auto& span : mSpans ) {
|
||||
if ( span->getString().empty() )
|
||||
continue;
|
||||
for ( auto& block : mBlocks ) {
|
||||
if ( block.type == BlockType::Text ) {
|
||||
auto& span = block.text;
|
||||
if ( !span || span->getString().empty() )
|
||||
continue;
|
||||
|
||||
auto& fontStyle = span->getFontStyleConfig();
|
||||
if ( !fontStyle.Font )
|
||||
continue;
|
||||
auto& fontStyle = span->getFontStyleConfig();
|
||||
if ( !fontStyle.Font )
|
||||
continue;
|
||||
|
||||
Uint32 textHints = span->getTextHints();
|
||||
Uint32 textHints = span->getTextHints();
|
||||
|
||||
LineWrapInfoEx wrapInfo = LineWrap::computeLineBreaksEx(
|
||||
span->getString(), fontStyle, mMaxWidth > 0 ? mMaxWidth : 1e9f,
|
||||
mMaxWidth > 0 ? LineWrapMode::Word : LineWrapMode::NoWrap, false, 4, 0.f, textHints,
|
||||
false, curX );
|
||||
LineWrapInfoEx wrapInfo = LineWrap::computeLineBreaksEx(
|
||||
span->getString(), fontStyle, mMaxWidth > 0 ? mMaxWidth : 1e9f,
|
||||
mMaxWidth > 0 ? LineWrapMode::Word : LineWrapMode::NoWrap, false, 4, 0.f, textHints,
|
||||
false, curX );
|
||||
|
||||
// Make sure we have the end of the string as a "wrap" point for the loop
|
||||
if ( wrapInfo.wraps.empty() || wrapInfo.wraps.back() != (Float)span->getString().size() )
|
||||
wrapInfo.wraps.push_back( span->getString().size() );
|
||||
// Make sure we have the end of the string as a "wrap" point for the loop
|
||||
if ( wrapInfo.wraps.empty() ||
|
||||
wrapInfo.wraps.back() != (Float)span->getString().size() )
|
||||
wrapInfo.wraps.push_back( span->getString().size() );
|
||||
|
||||
for ( size_t i = 0; i < wrapInfo.wraps.size() - 1; ++i ) {
|
||||
size_t startIdx = wrapInfo.wraps[i];
|
||||
size_t endIdx = wrapInfo.wraps[i + 1];
|
||||
bool isNewline = ( endIdx - startIdx == 1 && span->getString()[startIdx] == '\n' );
|
||||
for ( size_t i = 0; i < wrapInfo.wraps.size() - 1; ++i ) {
|
||||
size_t startIdx = wrapInfo.wraps[i];
|
||||
size_t endIdx = wrapInfo.wraps[i + 1];
|
||||
bool isNewline = ( endIdx - startIdx == 1 && span->getString()[startIdx] == '\n' );
|
||||
|
||||
if ( !isNewline ) {
|
||||
std::shared_ptr<Text> renderSpanText = std::make_shared<Text>();
|
||||
renderSpanText->setString(
|
||||
span->getString().substr( startIdx, endIdx - startIdx ) );
|
||||
renderSpanText->setStyleConfig( fontStyle );
|
||||
if ( !isNewline ) {
|
||||
std::shared_ptr<Text> renderSpanText = std::make_shared<Text>();
|
||||
renderSpanText->setString(
|
||||
span->getString().substr( startIdx, endIdx - startIdx ) );
|
||||
renderSpanText->setStyleConfig( fontStyle );
|
||||
|
||||
RenderSpan renderSpan;
|
||||
renderSpan.text = renderSpanText;
|
||||
renderSpan.position = { curX, 0 }; // Y adjusted later
|
||||
Block newBlock;
|
||||
newBlock.type = BlockType::Text;
|
||||
newBlock.text = renderSpanText;
|
||||
|
||||
RenderParagraph& currentLine = mLines.back();
|
||||
currentLine.spans.push_back( renderSpan );
|
||||
RenderSpan renderSpan;
|
||||
renderSpan.block = newBlock;
|
||||
renderSpan.position = { curX, 0 }; // Y adjusted later
|
||||
|
||||
Float ascent = fontStyle.Font->getAscent( fontStyle.CharacterSize );
|
||||
Float height = fontStyle.Font->getLineSpacing( fontStyle.CharacterSize );
|
||||
RenderParagraph& currentLine = mLines.back();
|
||||
currentLine.spans.push_back( renderSpan );
|
||||
|
||||
currentLine.maxAscent = std::max( currentLine.maxAscent, ascent );
|
||||
currentLine.height = std::max( currentLine.height, height );
|
||||
Float ascent = fontStyle.Font->getAscent( fontStyle.CharacterSize );
|
||||
Float height = fontStyle.Font->getLineSpacing( fontStyle.CharacterSize );
|
||||
|
||||
Float spanWidth = renderSpan.text->getTextWidth();
|
||||
curX += spanWidth;
|
||||
currentLine.width += spanWidth;
|
||||
currentLine.maxAscent = std::max( currentLine.maxAscent, ascent );
|
||||
currentLine.height = std::max( currentLine.height, height );
|
||||
|
||||
Float spanWidth = renderSpan.block.text->getTextWidth();
|
||||
renderSpan.size = Sizef( spanWidth, height );
|
||||
curX += spanWidth;
|
||||
currentLine.width += spanWidth;
|
||||
}
|
||||
|
||||
// If it's a newline, or if it's not the very last segment (which means it wrapped),
|
||||
// start a new line. Exception: If the last segment was just a newline, we already
|
||||
// handled it.
|
||||
if ( i < wrapInfo.wraps.size() - 2 || isNewline ) {
|
||||
maxWidth = std::max( maxWidth, curX );
|
||||
mLines.push_back( RenderParagraph() );
|
||||
curX = 0;
|
||||
}
|
||||
}
|
||||
} else if ( block.type == BlockType::Drawable || block.type == BlockType::CustomSize ) {
|
||||
Sizef blockSize = block.type == BlockType::Drawable
|
||||
? ( block.drawable ? block.drawable->getPixelsSize() : Sizef() )
|
||||
: block.customSize;
|
||||
|
||||
// If it's a newline, or if it's not the very last segment (which means it wrapped),
|
||||
// start a new line. Exception: If the last segment was just a newline, we already
|
||||
// handled it.
|
||||
if ( i < wrapInfo.wraps.size() - 2 || isNewline ) {
|
||||
// Wrap if needed
|
||||
if ( mMaxWidth > 0 && curX + blockSize.getWidth() > mMaxWidth && curX > 0 ) {
|
||||
maxWidth = std::max( maxWidth, curX );
|
||||
mLines.push_back( RenderParagraph() );
|
||||
curX = 0;
|
||||
}
|
||||
|
||||
RenderSpan renderSpan;
|
||||
renderSpan.block = block;
|
||||
renderSpan.position = { curX, 0 };
|
||||
renderSpan.size = blockSize;
|
||||
|
||||
RenderParagraph& currentLine = mLines.back();
|
||||
currentLine.spans.push_back( renderSpan );
|
||||
|
||||
currentLine.maxAscent = std::max( currentLine.maxAscent, blockSize.getHeight() );
|
||||
currentLine.height = std::max( currentLine.height, blockSize.getHeight() );
|
||||
|
||||
curX += blockSize.getWidth();
|
||||
currentLine.width += blockSize.getWidth();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,10 +270,19 @@ void RichText::updateLayout() {
|
||||
}
|
||||
|
||||
for ( auto& span : line.spans ) {
|
||||
Float ascent = span.text->getFont()->getAscent( span.text->getCharacterSize() );
|
||||
Float offsetY = line.maxAscent - ascent;
|
||||
span.position.x += xOffset;
|
||||
span.position.y = offsetY;
|
||||
if ( span.block.type == BlockType::Text ) {
|
||||
Float ascent =
|
||||
span.block.text->getFont()->getAscent( span.block.text->getCharacterSize() );
|
||||
Float offsetY = line.maxAscent - ascent;
|
||||
span.position.x += xOffset;
|
||||
span.position.y = offsetY;
|
||||
} else {
|
||||
Float offsetY = line.height - span.size.getHeight();
|
||||
if ( offsetY < 0 )
|
||||
offsetY = 0;
|
||||
span.position.x += xOffset;
|
||||
span.position.y = offsetY;
|
||||
}
|
||||
}
|
||||
|
||||
curY += line.height;
|
||||
|
||||
461
src/eepp/ui/uirichtext.cpp
Normal file
461
src/eepp/ui/uirichtext.cpp
Normal file
@@ -0,0 +1,461 @@
|
||||
#include <eepp/graphics/fontmanager.hpp>
|
||||
#include <eepp/graphics/text.hpp>
|
||||
#include <eepp/ui/css/propertydefinition.hpp>
|
||||
#include <eepp/ui/uirichtext.hpp>
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
#include <eepp/ui/uitextspan.hpp>
|
||||
#include <eepp/ui/uithememanager.hpp>
|
||||
#include <eepp/ui/uiwidgetcreator.hpp>
|
||||
|
||||
#define PUGIXML_HEADER_ONLY
|
||||
#include <pugixml/pugixml.hpp>
|
||||
|
||||
namespace EE { namespace UI {
|
||||
|
||||
UIRichText* UIRichText::New() {
|
||||
return eeNew( UIRichText, () );
|
||||
}
|
||||
|
||||
UIRichText* UIRichText::NewWithTag( const std::string& tag ) {
|
||||
return eeNew( UIRichText, ( tag ) );
|
||||
}
|
||||
|
||||
UIRichText::UIRichText( const std::string& tag ) :
|
||||
UILayout( tag ), mRichText( Graphics::RichText::New() ) {
|
||||
mFlags |= UI_LOADS_ITS_CHILDREN;
|
||||
|
||||
UITheme* theme = getUISceneNode()->getUIThemeManager()->getDefaultTheme();
|
||||
|
||||
if ( NULL != theme && NULL != theme->getDefaultFont() ) {
|
||||
mRichText->getFontStyleConfig().Font = theme->getDefaultFont();
|
||||
} else if ( NULL != getUISceneNode()->getUIThemeManager()->getDefaultFont() ) {
|
||||
mRichText->getFontStyleConfig().Font =
|
||||
getUISceneNode()->getUIThemeManager()->getDefaultFont();
|
||||
}
|
||||
|
||||
if ( NULL != theme ) {
|
||||
mRichText->getFontStyleConfig().CharacterSize = theme->getDefaultFontSize();
|
||||
} else {
|
||||
mRichText->getFontStyleConfig().CharacterSize =
|
||||
getUISceneNode()->getUIThemeManager()->getDefaultFontSize();
|
||||
}
|
||||
}
|
||||
|
||||
UIRichText::~UIRichText() {
|
||||
eeDelete( mRichText );
|
||||
}
|
||||
|
||||
Uint32 UIRichText::getType() const {
|
||||
return UI_TYPE_RICHTEXT;
|
||||
}
|
||||
|
||||
bool UIRichText::isType( const Uint32& type ) const {
|
||||
return UIRichText::getType() == type ? true : UILayout::isType( type );
|
||||
}
|
||||
|
||||
Graphics::RichText* UIRichText::getRichText() {
|
||||
return mRichText;
|
||||
}
|
||||
|
||||
void UIRichText::draw() {
|
||||
if ( mVisible && 0.f != mAlpha ) {
|
||||
UIWidget::draw();
|
||||
|
||||
if ( mRichText->getSize().getWidth() > 0.f ) {
|
||||
if ( isClipped() ) {
|
||||
clipSmartEnable( mScreenPos.x + mPaddingPx.Left, mScreenPos.y + mPaddingPx.Top,
|
||||
mSize.getWidth() - mPaddingPx.Left - mPaddingPx.Right,
|
||||
mSize.getHeight() - mPaddingPx.Top - mPaddingPx.Bottom );
|
||||
}
|
||||
|
||||
mRichText->draw( std::trunc( mScreenPos.x ) + (int)mPaddingPx.Left,
|
||||
std::trunc( mScreenPos.y ) + (int)mPaddingPx.Top, Vector2f::One, 0.f,
|
||||
getBlendMode() );
|
||||
|
||||
if ( isClipped() )
|
||||
clipSmartDisable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool UIRichText::applyProperty( const StyleSheetProperty& attribute ) {
|
||||
if ( !checkPropertyDefinition( attribute ) )
|
||||
return false;
|
||||
|
||||
switch ( attribute.getPropertyDefinition()->getPropertyId() ) {
|
||||
case PropertyId::FontFamily: {
|
||||
Graphics::Font* font =
|
||||
Graphics::FontManager::instance()->getByName( attribute.value() );
|
||||
if ( NULL != font && font->loaded() ) {
|
||||
setFont( font );
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PropertyId::FontSize:
|
||||
setFontSize( lengthFromValue( attribute ) );
|
||||
break;
|
||||
case PropertyId::FontStyle:
|
||||
setFontStyle( attribute.asFontStyle() );
|
||||
break;
|
||||
case PropertyId::Color:
|
||||
setFontColor( attribute.asColor() );
|
||||
break;
|
||||
case PropertyId::TextShadowColor:
|
||||
setFontShadowColor( attribute.asColor() );
|
||||
break;
|
||||
case PropertyId::TextShadowOffset:
|
||||
setFontShadowOffset( attribute.asVector2f() );
|
||||
break;
|
||||
case PropertyId::TextStrokeWidth:
|
||||
setOutlineThickness( lengthFromValue( attribute ) );
|
||||
break;
|
||||
case PropertyId::TextStrokeColor:
|
||||
setOutlineColor( attribute.asColor() );
|
||||
break;
|
||||
case PropertyId::TextAlign: {
|
||||
std::string align = String::toLower( attribute.value() );
|
||||
if ( align == "center" )
|
||||
setTextAlign( TEXT_ALIGN_CENTER );
|
||||
else if ( align == "left" )
|
||||
setTextAlign( TEXT_ALIGN_LEFT );
|
||||
else if ( align == "right" )
|
||||
setTextAlign( TEXT_ALIGN_RIGHT );
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return UILayout::applyProperty( attribute );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string UIRichText::getPropertyString( const PropertyDefinition* propertyDef,
|
||||
const Uint32& propertyIndex ) const {
|
||||
if ( NULL == propertyDef )
|
||||
return "";
|
||||
|
||||
switch ( propertyDef->getPropertyId() ) {
|
||||
case PropertyId::FontFamily:
|
||||
return NULL != getFont() ? getFont()->getName() : "";
|
||||
case PropertyId::FontSize:
|
||||
return String::format( "%dpx", getFontSize() );
|
||||
case PropertyId::FontStyle:
|
||||
return Graphics::Text::styleFlagToString( getFontStyle() );
|
||||
case PropertyId::Color:
|
||||
return getFontColor().toHexString();
|
||||
case PropertyId::TextShadowColor:
|
||||
return getFontShadowColor().toHexString();
|
||||
case PropertyId::TextShadowOffset:
|
||||
return String::fromFloat( getFontShadowOffset().x ) + " " +
|
||||
String::fromFloat( getFontShadowOffset().y );
|
||||
case PropertyId::TextStrokeWidth:
|
||||
return String::fromFloat( PixelDensity::dpToPx( getOutlineThickness() ), "px" );
|
||||
case PropertyId::TextStrokeColor:
|
||||
return getOutlineColor().toHexString();
|
||||
case PropertyId::TextAlign:
|
||||
return getTextAlign() == TEXT_ALIGN_CENTER
|
||||
? "center"
|
||||
: ( getTextAlign() == TEXT_ALIGN_RIGHT ? "right" : "left" );
|
||||
default:
|
||||
return UILayout::getPropertyString( propertyDef, propertyIndex );
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<PropertyId> UIRichText::getPropertiesImplemented() const {
|
||||
auto props = UILayout::getPropertiesImplemented();
|
||||
auto local = {
|
||||
PropertyId::FontFamily, PropertyId::FontSize, PropertyId::FontStyle,
|
||||
PropertyId::Color, PropertyId::TextShadowColor, PropertyId::TextShadowOffset,
|
||||
PropertyId::TextStrokeWidth, PropertyId::TextStrokeColor, PropertyId::TextAlign };
|
||||
props.insert( props.end(), local.begin(), local.end() );
|
||||
return props;
|
||||
}
|
||||
|
||||
Graphics::Font* UIRichText::getFont() const {
|
||||
return mRichText->getFontStyleConfig().Font;
|
||||
}
|
||||
|
||||
UIRichText* UIRichText::setFont( Graphics::Font* font ) {
|
||||
if ( NULL != font && mRichText->getFontStyleConfig().Font != font ) {
|
||||
mRichText->getFontStyleConfig().Font = font;
|
||||
mRichText->invalidate();
|
||||
setLayoutDirty();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
Uint32 UIRichText::getFontSize() const {
|
||||
return mRichText->getFontStyleConfig().CharacterSize;
|
||||
}
|
||||
|
||||
UIRichText* UIRichText::setFontSize( const Uint32& characterSize ) {
|
||||
if ( mRichText->getFontStyleConfig().CharacterSize != characterSize ) {
|
||||
mRichText->getFontStyleConfig().CharacterSize = characterSize;
|
||||
mRichText->invalidate();
|
||||
setLayoutDirty();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
const Uint32& UIRichText::getFontStyle() const {
|
||||
return mRichText->getFontStyleConfig().Style;
|
||||
}
|
||||
|
||||
UIRichText* UIRichText::setFontStyle( const Uint32& fontStyle ) {
|
||||
if ( mRichText->getFontStyleConfig().Style != fontStyle ) {
|
||||
mRichText->getFontStyleConfig().Style = fontStyle;
|
||||
mRichText->invalidate();
|
||||
setLayoutDirty();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
const Color& UIRichText::getFontColor() const {
|
||||
return mRichText->getFontStyleConfig().FontColor;
|
||||
}
|
||||
|
||||
UIRichText* UIRichText::setFontColor( const Color& color ) {
|
||||
if ( mRichText->getFontStyleConfig().FontColor != color ) {
|
||||
mRichText->getFontStyleConfig().FontColor = color;
|
||||
mRichText->invalidate();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
const Color& UIRichText::getFontShadowColor() const {
|
||||
return mRichText->getFontStyleConfig().ShadowColor;
|
||||
}
|
||||
|
||||
UIRichText* UIRichText::setFontShadowColor( const Color& color ) {
|
||||
if ( mRichText->getFontStyleConfig().ShadowColor != color ) {
|
||||
mRichText->getFontStyleConfig().ShadowColor = color;
|
||||
if ( mRichText->getFontStyleConfig().ShadowColor != Color::Transparent )
|
||||
mRichText->getFontStyleConfig().Style |= Graphics::Text::Shadow;
|
||||
else
|
||||
mRichText->getFontStyleConfig().Style &= ~Graphics::Text::Shadow;
|
||||
mRichText->invalidate();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
const Vector2f& UIRichText::getFontShadowOffset() const {
|
||||
return mRichText->getFontStyleConfig().ShadowOffset;
|
||||
}
|
||||
|
||||
UIRichText* UIRichText::setFontShadowOffset( const Vector2f& offset ) {
|
||||
if ( mRichText->getFontStyleConfig().ShadowOffset != offset ) {
|
||||
mRichText->getFontStyleConfig().ShadowOffset = offset;
|
||||
mRichText->invalidate();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
const Float& UIRichText::getOutlineThickness() const {
|
||||
return mRichText->getFontStyleConfig().OutlineThickness;
|
||||
}
|
||||
|
||||
UIRichText* UIRichText::setOutlineThickness( const Float& outlineThickness ) {
|
||||
if ( mRichText->getFontStyleConfig().OutlineThickness != outlineThickness ) {
|
||||
mRichText->getFontStyleConfig().OutlineThickness = outlineThickness;
|
||||
mRichText->invalidate();
|
||||
setLayoutDirty();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
const Color& UIRichText::getOutlineColor() const {
|
||||
return mRichText->getFontStyleConfig().OutlineColor;
|
||||
}
|
||||
|
||||
UIRichText* UIRichText::setOutlineColor( const Color& outlineColor ) {
|
||||
if ( mRichText->getFontStyleConfig().OutlineColor != outlineColor ) {
|
||||
mRichText->getFontStyleConfig().OutlineColor = outlineColor;
|
||||
mRichText->invalidate();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
Uint32 UIRichText::getTextAlign() const {
|
||||
return mRichText->getAlign();
|
||||
}
|
||||
|
||||
UIRichText* UIRichText::setTextAlign( const Uint32& align ) {
|
||||
if ( mRichText->getAlign() != align ) {
|
||||
mRichText->setAlign( align );
|
||||
setLayoutDirty();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
void UIRichText::loadFromXmlNode( const pugi::xml_node& node ) {
|
||||
beginAttributesTransaction();
|
||||
|
||||
UIWidget::loadFromXmlNode( node );
|
||||
|
||||
auto collapseXmlWhitespace = []( const String& text ) -> String {
|
||||
String res;
|
||||
res.reserve( text.size() );
|
||||
bool inSpace = false;
|
||||
for ( size_t i = 0; i < text.size(); ++i ) {
|
||||
if ( text[i] == ' ' || text[i] == '\t' || text[i] == '\n' || text[i] == '\r' ||
|
||||
text[i] == '\v' ) {
|
||||
if ( !inSpace ) {
|
||||
res += ' ';
|
||||
inSpace = true;
|
||||
}
|
||||
} else {
|
||||
res += text[i];
|
||||
inSpace = false;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
for ( pugi::xml_node child = node.first_child(); child; child = child.next_sibling() ) {
|
||||
if ( child.type() == pugi::node_element ) {
|
||||
if ( String::iequals( child.name(), "span" ) ||
|
||||
String::iequals( child.name(), "textspan" ) ) {
|
||||
UITextSpan* span = UITextSpan::New();
|
||||
span->setParent( this );
|
||||
span->loadFromXmlNode( child );
|
||||
} else {
|
||||
// Let parent logic load standard child widget
|
||||
UIWidget* widget = UIWidgetCreator::createFromName( child.name() );
|
||||
if ( widget ) {
|
||||
widget->setParent( this );
|
||||
widget->loadFromXmlNode( child );
|
||||
}
|
||||
}
|
||||
} else if ( child.type() == pugi::node_pcdata ) {
|
||||
String text = collapseXmlWhitespace( getTranslatorString( child.value() ) );
|
||||
if ( !text.empty() ) {
|
||||
UITextSpan* span = UITextSpan::New();
|
||||
span->setParent( this );
|
||||
span->setText( text );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endAttributesTransaction();
|
||||
setLayoutDirty();
|
||||
}
|
||||
|
||||
void UIRichText::onSizeChange() {
|
||||
UILayout::onSizeChange();
|
||||
setLayoutDirty(); // Re-wrap if size changes
|
||||
}
|
||||
|
||||
void UIRichText::onPaddingChange() {
|
||||
UILayout::onPaddingChange();
|
||||
setLayoutDirty();
|
||||
}
|
||||
|
||||
void UIRichText::onChildCountChange( Node* child, const bool& removed ) {
|
||||
UILayout::onChildCountChange( child, removed );
|
||||
setLayoutDirty();
|
||||
}
|
||||
|
||||
void UIRichText::onFontChanged() {
|
||||
setLayoutDirty();
|
||||
}
|
||||
|
||||
void UIRichText::onFontStyleChanged() {
|
||||
setLayoutDirty();
|
||||
}
|
||||
|
||||
void UIRichText::onAlphaChange() {
|
||||
UILayout::onAlphaChange();
|
||||
}
|
||||
|
||||
void UIRichText::rebuildRichText() {
|
||||
mRichText->clear();
|
||||
|
||||
// Calculate maximum layout width for the RichText block
|
||||
Float maxWidth = mSize.getWidth() - mPaddingPx.Left - mPaddingPx.Right;
|
||||
if ( maxWidth < 0 )
|
||||
maxWidth = 0;
|
||||
if ( mWidthPolicy == SizePolicy::WrapContent ) {
|
||||
mRichText->setMaxWidth( 0.f ); // Let it grow unbounded to query text bounds later
|
||||
} else {
|
||||
mRichText->setMaxWidth( maxWidth );
|
||||
}
|
||||
|
||||
Node* child = mChild;
|
||||
while ( NULL != child ) {
|
||||
if ( child->isWidget() ) {
|
||||
UIWidget* widget = static_cast<UIWidget*>( child );
|
||||
if ( widget->isType( UI_TYPE_TEXTSPAN ) ) {
|
||||
UITextSpan* span = static_cast<UITextSpan*>( widget );
|
||||
mRichText->addSpan( span->getText(), span->getFontStyleConfig() );
|
||||
} else {
|
||||
mRichText->addCustomSize( widget->getPixelsSize() );
|
||||
}
|
||||
}
|
||||
child = child->getNextNode();
|
||||
}
|
||||
}
|
||||
|
||||
void UIRichText::positionChildren() {
|
||||
const auto& lines = mRichText->getLines();
|
||||
|
||||
Node* child = mChild;
|
||||
|
||||
size_t currentLine = 0;
|
||||
size_t currentSpan = 0;
|
||||
|
||||
// Helper to find the next RenderSpan of type CustomSize
|
||||
auto getNextCustomSpan = [&]() -> const Graphics::RichText::RenderSpan* {
|
||||
while ( currentLine < lines.size() ) {
|
||||
const auto& line = lines[currentLine];
|
||||
while ( currentSpan < line.spans.size() ) {
|
||||
const auto& span = line.spans[currentSpan];
|
||||
currentSpan++;
|
||||
if ( span.block.type == Graphics::RichText::BlockType::CustomSize ) {
|
||||
return &span;
|
||||
}
|
||||
}
|
||||
currentSpan = 0;
|
||||
currentLine++;
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
while ( NULL != child ) {
|
||||
if ( child->isWidget() ) {
|
||||
UIWidget* widget = static_cast<UIWidget*>( child );
|
||||
if ( !widget->isType( UI_TYPE_TEXTSPAN ) ) {
|
||||
const auto* span = getNextCustomSpan();
|
||||
if ( span ) {
|
||||
size_t lineIdx = currentSpan > 0 ? currentLine : currentLine - 1;
|
||||
Float lineY = lines[lineIdx].y;
|
||||
|
||||
widget->setPixelsPosition( mPaddingPx.Left + span->position.x,
|
||||
mPaddingPx.Top + lineY + span->position.y );
|
||||
}
|
||||
}
|
||||
}
|
||||
child = child->getNextNode();
|
||||
}
|
||||
}
|
||||
|
||||
void UIRichText::onLayoutUpdate() {
|
||||
rebuildRichText();
|
||||
|
||||
mRichText->getSize(); // Forces an updateLayout internally
|
||||
|
||||
positionChildren();
|
||||
|
||||
// Resize logic
|
||||
if ( mWidthPolicy == SizePolicy::WrapContent ) {
|
||||
setInternalPixelsWidth( mRichText->getSize().getWidth() + mPaddingPx.Left +
|
||||
mPaddingPx.Right );
|
||||
}
|
||||
if ( mHeightPolicy == SizePolicy::WrapContent ) {
|
||||
setInternalPixelsHeight( mRichText->getSize().getHeight() + mPaddingPx.Top +
|
||||
mPaddingPx.Bottom );
|
||||
}
|
||||
|
||||
UILayout::onLayoutUpdate();
|
||||
}
|
||||
|
||||
}} // namespace EE::UI
|
||||
@@ -292,7 +292,7 @@ std::vector<UIWidget*> UISceneNode::loadNode( pugi::xml_node node, Node* parent,
|
||||
clock.getElapsedTime().asMilliseconds(), std::string( name ) ) );
|
||||
}
|
||||
|
||||
if ( widget.first_child() ) {
|
||||
if ( widget.first_child() && !uiwidget->loadsItsChildren() ) {
|
||||
loadNode( widget.first_child(), uiwidget, marker );
|
||||
}
|
||||
|
||||
|
||||
299
src/eepp/ui/uitextspan.cpp
Normal file
299
src/eepp/ui/uitextspan.cpp
Normal file
@@ -0,0 +1,299 @@
|
||||
#include <eepp/graphics/fontmanager.hpp>
|
||||
#include <eepp/graphics/text.hpp>
|
||||
#include <eepp/ui/css/propertydefinition.hpp>
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
#include <eepp/ui/uitextspan.hpp>
|
||||
#include <eepp/ui/uithememanager.hpp>
|
||||
#define PUGIXML_HEADER_ONLY
|
||||
#include <pugixml/pugixml.hpp>
|
||||
|
||||
namespace EE { namespace UI {
|
||||
|
||||
UITextSpan* UITextSpan::New() {
|
||||
return eeNew( UITextSpan, () );
|
||||
}
|
||||
|
||||
UITextSpan* UITextSpan::NewWithTag( const std::string& tag ) {
|
||||
return eeNew( UITextSpan, ( tag ) );
|
||||
}
|
||||
|
||||
UITextSpan::UITextSpan( const std::string& tag ) : UIWidget( tag ) {
|
||||
mFlags |= UI_VALIGN_CENTER | UI_HALIGN_LEFT | UI_LOADS_ITS_CHILDREN;
|
||||
|
||||
UITheme* theme = getUISceneNode()->getUIThemeManager()->getDefaultTheme();
|
||||
|
||||
if ( NULL != theme && NULL != theme->getDefaultFont() ) {
|
||||
mFontStyleConfig.Font = theme->getDefaultFont();
|
||||
} else if ( NULL != getUISceneNode()->getUIThemeManager()->getDefaultFont() ) {
|
||||
mFontStyleConfig.Font = getUISceneNode()->getUIThemeManager()->getDefaultFont();
|
||||
}
|
||||
|
||||
if ( NULL != theme ) {
|
||||
mFontStyleConfig.CharacterSize = theme->getDefaultFontSize();
|
||||
} else {
|
||||
mFontStyleConfig.CharacterSize =
|
||||
getUISceneNode()->getUIThemeManager()->getDefaultFontSize();
|
||||
}
|
||||
}
|
||||
|
||||
UITextSpan::~UITextSpan() {}
|
||||
|
||||
Uint32 UITextSpan::getType() const {
|
||||
return UI_TYPE_TEXTSPAN;
|
||||
}
|
||||
|
||||
bool UITextSpan::isType( const Uint32& type ) const {
|
||||
return UITextSpan::getType() == type ? true : UIWidget::isType( type );
|
||||
}
|
||||
|
||||
void UITextSpan::draw() {
|
||||
// Skip native generic rendering because it will be drawn by UIRichText
|
||||
}
|
||||
|
||||
bool UITextSpan::applyProperty( const StyleSheetProperty& attribute ) {
|
||||
if ( !checkPropertyDefinition( attribute ) )
|
||||
return false;
|
||||
|
||||
switch ( attribute.getPropertyDefinition()->getPropertyId() ) {
|
||||
case PropertyId::Text:
|
||||
setText( getTranslatorString( attribute.value() ) );
|
||||
break;
|
||||
case PropertyId::Color:
|
||||
setFontColor( attribute.asColor() );
|
||||
break;
|
||||
case PropertyId::TextShadowColor:
|
||||
setFontShadowColor( attribute.asColor() );
|
||||
break;
|
||||
case PropertyId::TextShadowOffset:
|
||||
setFontShadowOffset( attribute.asVector2f() );
|
||||
break;
|
||||
case PropertyId::FontFamily: {
|
||||
Graphics::Font* font =
|
||||
Graphics::FontManager::instance()->getByName( attribute.value() );
|
||||
if ( NULL != font && font->loaded() ) {
|
||||
setFont( font );
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PropertyId::FontSize:
|
||||
setFontSize( lengthFromValue( attribute ) );
|
||||
break;
|
||||
case PropertyId::FontStyle:
|
||||
setFontStyle( attribute.asFontStyle() );
|
||||
break;
|
||||
case PropertyId::TextStrokeWidth:
|
||||
setOutlineThickness( lengthFromValue( attribute ) );
|
||||
break;
|
||||
case PropertyId::TextStrokeColor:
|
||||
setOutlineColor( attribute.asColor() );
|
||||
break;
|
||||
default:
|
||||
return UIWidget::applyProperty( attribute );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string UITextSpan::getPropertyString( const PropertyDefinition* propertyDef,
|
||||
const Uint32& propertyIndex ) const {
|
||||
if ( NULL == propertyDef )
|
||||
return "";
|
||||
|
||||
switch ( propertyDef->getPropertyId() ) {
|
||||
case PropertyId::Text:
|
||||
return getText().toUtf8();
|
||||
case PropertyId::FontFamily:
|
||||
return NULL != getFont() ? getFont()->getName() : "";
|
||||
case PropertyId::FontSize:
|
||||
return String::format( "%dpx", getFontSize() );
|
||||
case PropertyId::FontStyle:
|
||||
return Graphics::Text::styleFlagToString( getFontStyle() );
|
||||
case PropertyId::Color:
|
||||
return getFontColor().toHexString();
|
||||
case PropertyId::TextShadowColor:
|
||||
return getFontShadowColor().toHexString();
|
||||
case PropertyId::TextShadowOffset:
|
||||
return String::fromFloat( getFontShadowOffset().x ) + " " +
|
||||
String::fromFloat( getFontShadowOffset().y );
|
||||
case PropertyId::TextStrokeWidth:
|
||||
return String::fromFloat( PixelDensity::dpToPx( getOutlineThickness() ), "px" );
|
||||
case PropertyId::TextStrokeColor:
|
||||
return getOutlineColor().toHexString();
|
||||
default:
|
||||
return UIWidget::getPropertyString( propertyDef, propertyIndex );
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<PropertyId> UITextSpan::getPropertiesImplemented() const {
|
||||
auto props = UIWidget::getPropertiesImplemented();
|
||||
auto local = { PropertyId::Text, PropertyId::FontFamily,
|
||||
PropertyId::FontSize, PropertyId::FontStyle,
|
||||
PropertyId::Color, PropertyId::TextShadowColor,
|
||||
PropertyId::TextShadowOffset, PropertyId::TextStrokeWidth,
|
||||
PropertyId::TextStrokeColor };
|
||||
props.insert( props.end(), local.begin(), local.end() );
|
||||
return props;
|
||||
}
|
||||
|
||||
const String& UITextSpan::getText() const {
|
||||
return mText;
|
||||
}
|
||||
|
||||
UITextSpan* UITextSpan::setText( const String& text ) {
|
||||
if ( mText != text ) {
|
||||
mText = text;
|
||||
onTextChanged();
|
||||
notifyLayoutAttrChange();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
const UIFontStyleConfig& UITextSpan::getFontStyleConfig() const {
|
||||
return mFontStyleConfig;
|
||||
}
|
||||
|
||||
void UITextSpan::setFontStyleConfig( const UIFontStyleConfig& fontStyleConfig ) {
|
||||
mFontStyleConfig = fontStyleConfig;
|
||||
onFontStyleChanged();
|
||||
onFontChanged();
|
||||
notifyLayoutAttrChange();
|
||||
}
|
||||
|
||||
Graphics::Font* UITextSpan::getFont() const {
|
||||
return mFontStyleConfig.getFont();
|
||||
}
|
||||
|
||||
UITextSpan* UITextSpan::setFont( Graphics::Font* font ) {
|
||||
if ( mFontStyleConfig.Font != font ) {
|
||||
mFontStyleConfig.Font = font;
|
||||
onFontChanged();
|
||||
notifyLayoutAttrChange();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
Uint32 UITextSpan::getFontSize() const {
|
||||
return mFontStyleConfig.getFontCharacterSize();
|
||||
}
|
||||
|
||||
UITextSpan* UITextSpan::setFontSize( const Uint32& characterSize ) {
|
||||
if ( mFontStyleConfig.CharacterSize != characterSize ) {
|
||||
mFontStyleConfig.CharacterSize = characterSize;
|
||||
onFontStyleChanged();
|
||||
notifyLayoutAttrChange();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
const Uint32& UITextSpan::getFontStyle() const {
|
||||
return mFontStyleConfig.getFontStyle();
|
||||
}
|
||||
|
||||
UITextSpan* UITextSpan::setFontStyle( const Uint32& fontStyle ) {
|
||||
if ( mFontStyleConfig.Style != fontStyle ) {
|
||||
mFontStyleConfig.Style = fontStyle;
|
||||
onFontStyleChanged();
|
||||
notifyLayoutAttrChange();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
const Float& UITextSpan::getOutlineThickness() const {
|
||||
return mFontStyleConfig.getOutlineThickness();
|
||||
}
|
||||
|
||||
UITextSpan* UITextSpan::setOutlineThickness( const Float& outlineThickness ) {
|
||||
if ( mFontStyleConfig.OutlineThickness != outlineThickness ) {
|
||||
mFontStyleConfig.OutlineThickness = outlineThickness;
|
||||
onFontStyleChanged();
|
||||
notifyLayoutAttrChange();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
const Color& UITextSpan::getOutlineColor() const {
|
||||
return mFontStyleConfig.getOutlineColor();
|
||||
}
|
||||
|
||||
UITextSpan* UITextSpan::setOutlineColor( const Color& outlineColor ) {
|
||||
if ( mFontStyleConfig.OutlineColor != outlineColor ) {
|
||||
mFontStyleConfig.OutlineColor = outlineColor;
|
||||
onFontStyleChanged();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
const Color& UITextSpan::getFontColor() const {
|
||||
return mFontStyleConfig.getFontColor();
|
||||
}
|
||||
|
||||
UITextSpan* UITextSpan::setFontColor( const Color& color ) {
|
||||
if ( mFontStyleConfig.FontColor != color ) {
|
||||
mFontStyleConfig.FontColor = color;
|
||||
onFontStyleChanged();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
const Color& UITextSpan::getFontShadowColor() const {
|
||||
return mFontStyleConfig.getFontShadowColor();
|
||||
}
|
||||
|
||||
UITextSpan* UITextSpan::setFontShadowColor( const Color& color ) {
|
||||
if ( mFontStyleConfig.ShadowColor != color ) {
|
||||
mFontStyleConfig.ShadowColor = color;
|
||||
if ( color != Color::Transparent )
|
||||
mFontStyleConfig.Style |= Graphics::Text::Shadow;
|
||||
else
|
||||
mFontStyleConfig.Style &= ~Graphics::Text::Shadow;
|
||||
onFontStyleChanged();
|
||||
notifyLayoutAttrChange();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
const Vector2f& UITextSpan::getFontShadowOffset() const {
|
||||
return mFontStyleConfig.getFontShadowOffset();
|
||||
}
|
||||
|
||||
UITextSpan* UITextSpan::setFontShadowOffset( const Vector2f& offset ) {
|
||||
if ( mFontStyleConfig.ShadowOffset != offset ) {
|
||||
mFontStyleConfig.ShadowOffset = offset;
|
||||
onFontStyleChanged();
|
||||
notifyLayoutAttrChange();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
void UITextSpan::onAlphaChange() {
|
||||
UIWidget::onAlphaChange();
|
||||
notifyLayoutAttrChange();
|
||||
}
|
||||
|
||||
void UITextSpan::onFontChanged() {
|
||||
sendCommonEvent( Event::OnFontChanged );
|
||||
}
|
||||
|
||||
void UITextSpan::onFontStyleChanged() {
|
||||
sendCommonEvent( Event::OnFontStyleChanged );
|
||||
}
|
||||
|
||||
void UITextSpan::onTextChanged() {
|
||||
sendCommonEvent( Event::OnTextChanged );
|
||||
sendCommonEvent( Event::OnValueChange );
|
||||
}
|
||||
|
||||
void UITextSpan::loadFromXmlNode( const pugi::xml_node& node ) {
|
||||
beginAttributesTransaction();
|
||||
|
||||
UIWidget::loadFromXmlNode( node );
|
||||
|
||||
for ( pugi::xml_node child = node.first_child(); child; child = child.next_sibling() ) {
|
||||
if ( child.type() == pugi::node_pcdata )
|
||||
mText += getTranslatorString( child.value() );
|
||||
}
|
||||
|
||||
endAttributesTransaction();
|
||||
}
|
||||
|
||||
}} // namespace EE::UI
|
||||
@@ -2024,6 +2024,10 @@ void UIWidget::loadFromXmlNode( const pugi::xml_node& node ) {
|
||||
endAttributesTransaction();
|
||||
}
|
||||
|
||||
bool UIWidget::loadsItsChildren() const {
|
||||
return ( mFlags & UI_LOADS_ITS_CHILDREN ) != 0;
|
||||
}
|
||||
|
||||
std::string UIWidget::getLayoutWidthPolicyString() const {
|
||||
SizePolicy rules = getLayoutWidthPolicy();
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
#include <eepp/ui/uiwidgettable.hpp>
|
||||
#include <eepp/ui/uiwidgettablerow.hpp>
|
||||
#include <eepp/ui/uiwindow.hpp>
|
||||
#include <eepp/ui/uirichtext.hpp>
|
||||
#include <eepp/ui/uitextspan.hpp>
|
||||
|
||||
namespace EE { namespace UI {
|
||||
|
||||
@@ -106,6 +108,8 @@ void UIWidgetCreator::createBaseWidgetList() {
|
||||
registeredWidget["nodelink"] = UINodeLink::New;
|
||||
registeredWidget["textureviewer"] = Tools::UITextureViewer::New;
|
||||
registeredWidget["imageviewer"] = Tools::UIImageViewer::New;
|
||||
registeredWidget["richtext"] = UIRichText::New;
|
||||
registeredWidget["textspan"] = UITextSpan::New;
|
||||
|
||||
registeredWidget["hbox"] = UILinearLayout::NewHorizontal;
|
||||
registeredWidget["vbox"] = UILinearLayout::NewVertical;
|
||||
@@ -122,6 +126,8 @@ void UIWidgetCreator::createBaseWidgetList() {
|
||||
registeredWidget["tooltip"] = UITooltip::New;
|
||||
registeredWidget["tv"] = UITextView::New;
|
||||
registeredWidget["a"] = UIAnchor::New;
|
||||
registeredWidget["span"] = UITextSpan::New;
|
||||
registeredWidget["p"] = UIRichText::New;
|
||||
|
||||
sBaseListCreated = true;
|
||||
}
|
||||
|
||||
28
src/examples/ui_richtext/ui_richtext.cpp
Normal file
28
src/examples/ui_richtext/ui_richtext.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#include <eepp/ee.hpp>
|
||||
|
||||
EE_MAIN_FUNC int main( int, char** ) {
|
||||
UIApplication app( { 800, 600, "eepp - UIRichText Example" } );
|
||||
app.getUI()->loadLayoutFromString( R"xml(
|
||||
<LinearLayout layout_width="match_parent"
|
||||
layout_height="match_parent"
|
||||
orientation="vertical">
|
||||
<RichText id="rich_text"
|
||||
layout_width="match_parent"
|
||||
layout_height="wrap_content"
|
||||
font-size="24dp"
|
||||
font-color="white">Welcome to the <span color="#FFD700" font-style="bold">UIRichText</span> example!
|
||||
This component supports <span color="#00FF00" font-style="italic">styled text</span>,
|
||||
<span color="#00BFFF" font-style="shadow">shadows</span>,
|
||||
and <span color="#FF4500" text-stroke-width="1dp" text-stroke-color="black">outlines</span> using <span font-family="monospace" color="#A9A9A9">HTML-like tags</span>.
|
||||
</RichText>
|
||||
<Image src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAqElEQVR4nO1VWw6AIAwD46G4/5e30g8Tg3t0BYkmhn7hWFu3BUhp4vfIZN6+WeQywsCU5m2QQSjN2CxD1EG+bdCqDlhrSLtqF3wvLmBUUBPqzjJrbebOYBSkQV/3gUJQQZ3NrDXkOXheQbpP5fUZTIPQQNxZuUSXpUoQn1+0SB99r4gz7l0trgHQwhGb6G3g/+LTGl40wce7lIEpZEh0v8mhDTmGiY9xADIWNJt0vYyHAAAAAElFTkSuQmCC" margin="4dp" layout-gravity="center_horizontal" />
|
||||
<RichText id="rich_text"
|
||||
layout_width="match_parent"
|
||||
layout_height="wrap_content"
|
||||
font-size="24dp"
|
||||
font-color="white">We can also mix <span color="#FFD700" font-style="bold">contents</span> with more <span color="#00FF00" font-style="italic">text</span>!
|
||||
</RichText>
|
||||
</LinearLayout>
|
||||
)xml" );
|
||||
return app.run();
|
||||
}
|
||||
@@ -8,6 +8,10 @@
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <eepp/system/scopedop.hpp>
|
||||
#include <eepp/system/sys.hpp>
|
||||
#include <eepp/ui/uirichtext.hpp>
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
#include <eepp/ui/uitextspan.hpp>
|
||||
#include <eepp/ui/uithememanager.hpp>
|
||||
#include <eepp/window/engine.hpp>
|
||||
|
||||
using namespace EE;
|
||||
@@ -235,3 +239,52 @@ UTEST( RichText, RichTextTest ) {
|
||||
runTest();
|
||||
}
|
||||
}
|
||||
|
||||
UTEST( UIRichText, IntegrationAndLayoutVerification ) {
|
||||
Engine::instance()->createWindow( WindowSettings( 800, 600, "RichText Test",
|
||||
WindowStyle::Default, WindowBackend::Default,
|
||||
32, {}, 1, false, true ) );
|
||||
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
||||
|
||||
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
|
||||
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
||||
|
||||
ASSERT_TRUE( font->loaded() );
|
||||
FontFamily::loadFromRegular( font );
|
||||
|
||||
UI::UISceneNode* sceneNode = UI::UISceneNode::New();
|
||||
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
||||
themeManager->setDefaultFont( font );
|
||||
|
||||
String xml = R"xml(
|
||||
<RichText id="rt" layout_width="300dp" layout_height="wrap_content">Hello <span color="#FF0000">Red</span><Widget id="placeholder" layout_width="50dp" layout_height="50dp"/>World</RichText>
|
||||
)xml";
|
||||
|
||||
sceneNode->loadLayoutFromString( xml );
|
||||
|
||||
UI::UIRichText* rt = sceneNode->find<UI::UIRichText>( "rt" );
|
||||
ASSERT_TRUE( rt != nullptr );
|
||||
|
||||
// force layout
|
||||
sceneNode->update( Time::Zero );
|
||||
|
||||
auto* graphicsRt = rt->getRichText();
|
||||
const auto& blocks = graphicsRt->getBlocks();
|
||||
|
||||
ASSERT_EQ( blocks.size(), (size_t)4 );
|
||||
EXPECT_EQ( blocks[1].type, Graphics::RichText::BlockType::Text );
|
||||
EXPECT_TRUE( blocks[1].text->getFillColor() == Color::fromString( "#FF0000" ) );
|
||||
|
||||
EXPECT_EQ( blocks[2].type, Graphics::RichText::BlockType::CustomSize );
|
||||
EXPECT_EQ( blocks[2].customSize.getWidth(), PixelDensity::dpToPx( 50 ) );
|
||||
|
||||
UI::UIWidget* placeholder = rt->find<UI::UIWidget>( "placeholder" );
|
||||
ASSERT_TRUE( placeholder != nullptr );
|
||||
|
||||
Vector2f pos = placeholder->getPixelsPosition();
|
||||
Float expectedX = blocks[0].text->getTextWidth() + blocks[1].text->getTextWidth();
|
||||
EXPECT_NEAR( pos.x, expectedX, 2.0f );
|
||||
|
||||
eeDelete( sceneNode );
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user