Improved RichText support, added more support for basic HTML elements.

This commit is contained in:
Martín Lucas Golini
2026-03-04 01:04:34 -03:00
parent 7ba21c8a0d
commit 0c1bbb6959
18 changed files with 215 additions and 41 deletions

View File

@@ -112,6 +112,11 @@
"max_tokens": 1000000,
"cheapest": true
},
{
"name": "gemini-3.1-flash-lite-preview",
"display_name": "Gemini 3.1 Flash Lite Preview",
"max_tokens": 1000000
},
{
"name": "gemini-2.5-pro",
"display_name": "Gemini 2.5 Pro",

View File

@@ -52,6 +52,93 @@
droppable-hovering-color: #FFFFFF20;
}
b,
strong {
font-style: bold;
}
u {
text-decoration: underline;
}
s {
text-decoration: strikethrough;
}
i,
em {
font-style: italic;
}
h1 {
font-size: 32dp;
margin: 0.67em 0;
}
h2 {
font-size: 24dp;
margin: 0.83em 0;
}
h3 {
font-size: 18dp;
margin: 1.00em 0;
}
h4 {
font-size: 16dp;
margin: 1.33em 0;
}
h5 {
font-size: 13dp;
margin: 1.67em 0;
}
h6 {
font-size: 11dp;
margin: 1.67em 0;
}
code {
font-family: monospace;
}
p, ol, ul, pre {
margin: 1em 0;
}
li {
padding-left: 2em;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><circle cx='50' cy='50' r='40' fill='currentColor'/></svg>");
background-tint: var(--font);
background-position: 0.6em 0.5em;
background-size: 0.8em 0.8em;
}
a {
color: var(--primary);
selection-color: var(--font-selected-pressed);
selection-back-color: var(--primary);
cursor: arrow;
text-decoration: none;
gravity: bottom;
}
a:hover {
color: var(--font-highlight);
cursor: hand;
text-decoration: underline;
}
img {
scale-type: fit-inside;
layout-width: match_parent;
layout-height: wrap_content;
max-height: 100vh;
clip: true;
}
pushbutton,
selectbutton,
tableview::cell,

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -15,6 +15,8 @@ class EE_API UILinearLayout : public UILayout {
static UILinearLayout* NewHorizontal();
static UILinearLayout* NewVerticalWidthMatchParent();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;

View File

@@ -83,12 +83,15 @@ class EE_API UIRichText : public UILayout {
UIRichText* setTextAlign( const Uint32& align );
virtual void updateLayout();
protected:
RichText mRichText;
virtual Uint32 onMessage( const NodeMessage* Msg );
virtual void onSizeChange();
virtual void onPaddingChange();
virtual void onLayoutUpdate();
virtual void onChildCountChange( Node* child, const bool& removed );
virtual void onFontChanged();
virtual void onFontStyleChanged();

View File

@@ -84,9 +84,9 @@ class EE_API UIScrollBar : public UIWidget {
protected:
ScrollBarType mScrollBarStyle;
UISlider* mSlider;
UIWidget* mBtnUp;
UIWidget* mBtnDown;
UISlider* mSlider{ nullptr };
UIWidget* mBtnUp{ nullptr };
UIWidget* mBtnDown{ nullptr };
virtual void onSizeChange();

View File

@@ -26,6 +26,8 @@ class EE_API UITextSpan : public UIWidget {
static UITextSpan* NewMark() { return NewWithTag( "mark" ); }
static UITextSpan* NewCode() { return NewWithTag( "code" ); }
virtual ~UITextSpan();
virtual Uint32 getType() const;

View File

@@ -200,6 +200,8 @@ class EE_API UIAnchor : public UITextView {
public:
static UIAnchor* New();
static UIAnchor* NewA();
virtual bool applyProperty( const StyleSheetProperty& attribute );
virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
@@ -212,7 +214,7 @@ class EE_API UIAnchor : public UITextView {
const std::string& getHref() const;
protected:
UIAnchor();
UIAnchor( const std::string& tag = "anchor" );
std::string mHref;

View File

@@ -93,13 +93,15 @@ void UIImage::onAutoSize() {
Sizef size( getPixelsSize() );
if ( mWidthPolicy == SizePolicy::WrapContent )
if ( mWidthPolicy == SizePolicy::WrapContent ) {
size.x =
( (int)mDrawable->getPixelsSize().getWidth() + mPaddingPx.Left + mPaddingPx.Right );
}
if ( mHeightPolicy == SizePolicy::WrapContent )
if ( mHeightPolicy == SizePolicy::WrapContent ) {
size.y = ( (int)mDrawable->getPixelsSize().getHeight() + mPaddingPx.Top +
mPaddingPx.Bottom );
}
setPixelsSize( size );
}
@@ -210,7 +212,15 @@ void UIImage::safeDeleteDrawable() {
void UIImage::onDrawableResourceEvent( DrawableResource::Event event, DrawableResource* ) {
if ( event == DrawableResource::Change ) {
invalidateDraw();
runOnMainThread( [this] {
auto s = mSize;
onAutoSize();
calcDestSize();
if ( mSize != s ) {
invalidateDraw();
notifyLayoutAttrChangeParent();
}
} );
} else if ( event == DrawableResource::Unload ) {
mDrawable = NULL;
}

View File

@@ -21,6 +21,12 @@ UILinearLayout* UILinearLayout::NewHorizontal() {
return ( eeNew( UILinearLayout, () ) )->setOrientation( UIOrientation::Horizontal );
}
UILinearLayout* UILinearLayout::NewVerticalWidthMatchParent() {
return ( eeNew( UILinearLayout, () ) )
->setLayoutWidthPolicy( SizePolicy::MatchParent )
->asType<UILinearLayout>();
}
UILinearLayout::UILinearLayout() :
UILayout( "linearlayout" ), mOrientation( UIOrientation::Vertical ) {
mFlags |= UI_OWNS_CHILDREN_POSITION;

View File

@@ -176,7 +176,8 @@ UIRichText* UIRichText::setFont( Graphics::Font* font ) {
if ( NULL != font && mRichText.getFontStyleConfig().Font != font ) {
mRichText.getFontStyleConfig().Font = font;
mRichText.invalidate();
setLayoutDirty();
notifyLayoutAttrChange();
notifyLayoutAttrChangeParent();
updateDefaultSpansStyle();
}
return this;
@@ -190,7 +191,9 @@ UIRichText* UIRichText::setFontSize( const Uint32& characterSize ) {
if ( mRichText.getFontStyleConfig().CharacterSize != characterSize ) {
mRichText.getFontStyleConfig().CharacterSize = characterSize;
mRichText.invalidate();
setLayoutDirty();
notifyLayoutAttrChange();
notifyLayoutAttrChangeParent();
updateDefaultSpansStyle();
}
return this;
@@ -204,7 +207,9 @@ UIRichText* UIRichText::setFontStyle( const Uint32& fontStyle ) {
if ( mRichText.getFontStyleConfig().Style != fontStyle ) {
mRichText.getFontStyleConfig().Style = fontStyle;
mRichText.invalidate();
setLayoutDirty();
notifyLayoutAttrChange();
notifyLayoutAttrChangeParent();
updateDefaultSpansStyle();
}
return this;
@@ -261,7 +266,9 @@ UIRichText* UIRichText::setOutlineThickness( const Float& outlineThickness ) {
if ( mRichText.getFontStyleConfig().OutlineThickness != outlineThickness ) {
mRichText.getFontStyleConfig().OutlineThickness = outlineThickness;
mRichText.invalidate();
setLayoutDirty();
notifyLayoutAttrChange();
notifyLayoutAttrChangeParent();
updateDefaultSpansStyle();
}
return this;
@@ -287,7 +294,9 @@ Uint32 UIRichText::getTextAlign() const {
UIRichText* UIRichText::setTextAlign( const Uint32& align ) {
if ( mRichText.getAlign() != align ) {
mRichText.setAlign( align );
setLayoutDirty();
notifyLayoutAttrChange();
notifyLayoutAttrChangeParent();
}
return this;
}
@@ -343,17 +352,18 @@ void UIRichText::loadFromXmlNode( const pugi::xml_node& node ) {
}
endAttributesTransaction();
setLayoutDirty();
}
void UIRichText::onSizeChange() {
UILayout::onSizeChange();
setLayoutDirty(); // Re-wrap if size changes
notifyLayoutAttrChange();
notifyLayoutAttrChangeParent();
}
void UIRichText::onPaddingChange() {
UILayout::onPaddingChange();
setLayoutDirty();
notifyLayoutAttrChange();
notifyLayoutAttrChangeParent();
}
void UIRichText::onChildCountChange( Node* child, const bool& removed ) {
@@ -361,15 +371,19 @@ void UIRichText::onChildCountChange( Node* child, const bool& removed ) {
if ( !removed && child->isWidget() && child->isType( UI_TYPE_TEXTSPAN ) ) {
static_cast<UITextSpan*>( child )->setInheritedStyle( mRichText.getFontStyleConfig() );
}
setLayoutDirty();
notifyLayoutAttrChange();
notifyLayoutAttrChangeParent();
}
void UIRichText::onFontChanged() {
setLayoutDirty();
notifyLayoutAttrChange();
notifyLayoutAttrChangeParent();
}
void UIRichText::onFontStyleChanged() {
setLayoutDirty();
notifyLayoutAttrChange();
notifyLayoutAttrChangeParent();
}
void UIRichText::onAlphaChange() {
@@ -397,6 +411,11 @@ void UIRichText::rebuildRichText() {
UITextSpan* span = static_cast<UITextSpan*>( widget );
mRichText.addSpan( span->getText(), span->getFontStyleConfig() );
} else {
if ( mSize.getWidth() != 0 &&
widget->getLayoutWidthPolicy() == SizePolicy::MatchParent ) {
widget->setPixelsSize( mSize.getWidth(), widget->getPixelsSize().getHeight() );
}
mRichText.addCustomSize( widget->getPixelsSize() );
}
}
@@ -457,14 +476,17 @@ void UIRichText::updateDefaultSpansStyle() {
}
}
void UIRichText::onLayoutUpdate() {
void UIRichText::updateLayout() {
if ( mPacking )
return;
mPacking = true;
rebuildRichText();
mRichText.getSize(); // Forces an updateLayout internally
positionChildren();
// Resize logic
if ( mWidthPolicy == SizePolicy::WrapContent ) {
setInternalPixelsWidth( mRichText.getSize().getWidth() + mPaddingPx.Left +
mPaddingPx.Right );
@@ -474,7 +496,19 @@ void UIRichText::onLayoutUpdate() {
mPaddingPx.Bottom );
}
UILayout::onLayoutUpdate();
mPacking = false;
mDirtyLayout = false;
}
Uint32 UIRichText::onMessage( const NodeMessage* Msg ) {
switch ( Msg->getMsg() ) {
case NodeMessage::LayoutAttributeChange: {
tryUpdateLayout();
return 1;
}
}
return 0;
}
}} // namespace EE::UI

View File

@@ -121,6 +121,9 @@ void UIScrollBar::setTheme( UITheme* Theme ) {
}
void UIScrollBar::onAutoSize() {
if ( mSlider == nullptr )
return;
Sizef size;
UISkin* tSkin = mSlider->getBackSlider()->getSkin();

View File

@@ -406,8 +406,9 @@ void UITextView::alignFix() {
break;
}
case UI_VALIGN_BOTTOM:
mRealAlignOffset.y = ( (Float)mSize.y - mPaddingPx.Top - mPaddingPx.Bottom -
(Float)mTextCache.getTextHeight() );
mRealAlignOffset.y =
( (Float)mSize.y - mPaddingPx.Top - mPaddingPx.Bottom -
(Float)mTextCache.getFont()->getAscent( mTextCache.getCharacterSize() ) );
break;
case UI_VALIGN_TOP:
mRealAlignOffset.y = 0;
@@ -950,7 +951,11 @@ UIAnchor* UIAnchor::New() {
return eeNew( UIAnchor, () );
}
UIAnchor::UIAnchor() : UITextView( "anchor" ) {
UIAnchor* UIAnchor::NewA() {
return eeNew( UIAnchor, ( "a" ) );
}
UIAnchor::UIAnchor( const std::string& tag ) : UITextView( tag ) {
onClick(
[this]( const MouseEvent* ) {
if ( !mHref.empty() )

View File

@@ -125,13 +125,17 @@ void UIWidgetCreator::createBaseWidgetList() {
registeredWidget["rlay"] = UIRelativeLayout::New;
registeredWidget["tooltip"] = UITooltip::New;
registeredWidget["tv"] = UITextView::New;
registeredWidget["a"] = UIAnchor::New;
// HTML elements
registeredWidget["a"] = UIAnchor::NewA;
registeredWidget["span"] = UITextSpan::New;
registeredWidget["em"] = UITextSpan::NewEmphasis;
registeredWidget["b"] = UITextSpan::NewBold;
registeredWidget["strong"] = UITextSpan::NewBold;
registeredWidget["i"] = UITextSpan::NewItalics;
registeredWidget["u"] = UITextSpan::NewUnderline;
registeredWidget["s"] = UITextSpan::NewStrikethrough;
registeredWidget["code"] = UITextSpan::NewCode;
registeredWidget["mark"] = UITextSpan::NewMark;
registeredWidget["div"] = UIRichText::New;
registeredWidget["p"] = UIRichText::NewParagraph;
@@ -141,9 +145,11 @@ void UIWidgetCreator::createBaseWidgetList() {
registeredWidget["h4"] = UIRichText::NewH4;
registeredWidget["h5"] = UIRichText::NewH5;
registeredWidget["h6"] = UIRichText::NewH6;
registeredWidget["ul"] = UILinearLayout::NewVertical;
registeredWidget["ol"] = UILinearLayout::NewVertical;
registeredWidget["ul"] = UILinearLayout::NewVerticalWidthMatchParent;
registeredWidget["ol"] = UILinearLayout::NewVerticalWidthMatchParent;
registeredWidget["li"] = UIRichText::NewListItem;
registeredWidget["pre"] = UITextSpan::New;
registeredWidget["img"] = [] { return UIImage::NewWithTag( "img" ); };
sBaseListCreated = true;
}

View File

@@ -3,20 +3,29 @@
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">
<vbox layout_width="match_parent" layout_height="match_parent">
<ScrollView lw="mp" lh="mp">
<vbox layout_width="match_parent" layout_height="wrap_content" padding="8dp">
<RichText font-size="12dp"
font-color="#cecece">Welcome to the <span color="#FFD700" font-style="bold">UIRichText</span> example!
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="file://assets/icon/ee.png" margin="4dp" layout-gravity="center_horizontal" />
<RichText font-size="12dp"
font-color="#ccc">We can also mix <span color="#FFD700" font-style="bold">contents</span> with more <span color="#00FF00" font-style="italic">text</span>!
color="#fefefe">We can also mix <span color="#FFD700" font-style="bold">contents</span> with more <span color="#00FF00" font-style="italic">text</span>!
</RichText>
</LinearLayout>
</vbox>
</ScrollView>
</vbox>
)xml" );
app.getUI()->on( Event::KeyUp, [&app]( const Event* event ) {
if ( event->asKeyEvent()->getKeyCode() == KEY_F11 ) {
UIWidgetInspector::create( app.getUI() );
}
} );
return app.run();
}

View File

@@ -354,14 +354,14 @@ UTEST( UIRichText, RichTextTest ) {
layout_height="match_parent"
orientation="vertical">
<RichText font-size="12dp"
font-color="#cecece">Welcome to the <span color="#FFD700" font-style="bold">UIRichText</span> example!
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="file://assets/icon/ee.png" margin="4dp" layout-gravity="center_horizontal" />
<RichText font-size="12dp"
font-color="#ccc">We can also mix <span color="#FFD700" font-style="bold">contents</span> with more <span color="#00FF00" font-style="italic">text</span>!
color="#efefef">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" );
@@ -370,7 +370,7 @@ UTEST( UIRichText, RichTextTest ) {
SceneManager::instance()->draw();
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
compareImages( utest_state, utest_result, app.getWindow(), "eepp-uirichtext" );
compareImages( utest_state, utest_result, app.getWindow(), "eepp-ui-richtext" );
};
UTEST_PRINT_STEP( "Text Shaper disabled" );

View File

@@ -169,18 +169,18 @@ static const auto LAYOUT = R"xml(
<vbox lw="wc" lh="wc" lg="center">
<hbox>
<tv text='@string(the_ecode_nbsp, "The ecode ")' />
<a id="home_doc" text="@string(documentation, documentation)" href="https://github.com/SpartanJ/ecode" />
<Anchor id="home_doc" text="@string(documentation, documentation)" href="https://github.com/SpartanJ/ecode" />
</hbox>
<hbox>
<tv text='@string(the_ecode_nbsp, "The ecode ")' />
<a id="home_forum" text="@string(forum, forum)" href="https://github.com/SpartanJ/ecode/discussions" />
<Anchor id="home_forum" text="@string(forum, forum)" href="https://github.com/SpartanJ/ecode/discussions" />
</hbox>
<hbox>
<tv text='@string(the_ecode_nbsp, "The ecode ")' />
<a id="home_issues" text="@string(issues, issues)" href="https://github.com/SpartanJ/ecode/issues" />
<Anchor id="home_issues" text="@string(issues, issues)" href="https://github.com/SpartanJ/ecode/issues" />
</hbox>
<hbox>
<a id="check-for-updates" text="@string(check_for_updates, Check for Updates)" />
<Anchor id="check-for-updates" text="@string(check_for_updates, Check for Updates)" />
</hbox>
</vbox>
</vbox>