RichText class now can hold any Drawable.

Added UIRichText with spans (UITextSpan). Still a WIP but core is working.
This commit is contained in:
Martín Lucas Golini
2026-03-02 00:20:24 -03:00
parent 0a1c0de152
commit fc0f33201c
17 changed files with 1210 additions and 59 deletions

View File

@@ -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
View File

@@ -80,3 +80,4 @@ ecode.dmg
/projects/android-project/app/.classpath
/projects/android-project/.project
/projects/android-project/app/.project
/design/

View File

@@ -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 };

View File

@@ -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,

View 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

View 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

View File

@@ -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.
*

View File

@@ -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++"

View File

@@ -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++"

View File

@@ -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
View 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

View File

@@ -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
View 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

View File

@@ -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();

View File

@@ -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;
}

View 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();
}

View File

@@ -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();
}