diff --git a/bin/assets/plugins/aiassistant.json b/bin/assets/plugins/aiassistant.json
index 0f3df03cb..cd1e05654 100644
--- a/bin/assets/plugins/aiassistant.json
+++ b/bin/assets/plugins/aiassistant.json
@@ -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",
diff --git a/bin/assets/ui/breeze.css b/bin/assets/ui/breeze.css
index dc302051a..770c764de 100644
--- a/bin/assets/ui/breeze.css
+++ b/bin/assets/ui/breeze.css
@@ -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,");
+ 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,
diff --git a/bin/unit_tests/assets/fontrendering/eepp-ui-richtext.webp b/bin/unit_tests/assets/fontrendering/eepp-ui-richtext.webp
new file mode 100644
index 000000000..db5a01cb3
Binary files /dev/null and b/bin/unit_tests/assets/fontrendering/eepp-ui-richtext.webp differ
diff --git a/bin/unit_tests/assets/fontrendering/eepp-uirichtext.webp b/bin/unit_tests/assets/fontrendering/eepp-uirichtext.webp
deleted file mode 100644
index 2399970d4..000000000
Binary files a/bin/unit_tests/assets/fontrendering/eepp-uirichtext.webp and /dev/null differ
diff --git a/include/eepp/ui/uilinearlayout.hpp b/include/eepp/ui/uilinearlayout.hpp
index bd6697d57..0e803d1d6 100644
--- a/include/eepp/ui/uilinearlayout.hpp
+++ b/include/eepp/ui/uilinearlayout.hpp
@@ -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;
diff --git a/include/eepp/ui/uirichtext.hpp b/include/eepp/ui/uirichtext.hpp
index dc031618c..2b4f28efd 100644
--- a/include/eepp/ui/uirichtext.hpp
+++ b/include/eepp/ui/uirichtext.hpp
@@ -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();
diff --git a/include/eepp/ui/uiscrollbar.hpp b/include/eepp/ui/uiscrollbar.hpp
index 4b384f56d..61584ce02 100644
--- a/include/eepp/ui/uiscrollbar.hpp
+++ b/include/eepp/ui/uiscrollbar.hpp
@@ -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();
diff --git a/include/eepp/ui/uitextspan.hpp b/include/eepp/ui/uitextspan.hpp
index d33b06b6f..1feafbbd5 100644
--- a/include/eepp/ui/uitextspan.hpp
+++ b/include/eepp/ui/uitextspan.hpp
@@ -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;
diff --git a/include/eepp/ui/uitextview.hpp b/include/eepp/ui/uitextview.hpp
index a02f70a60..6276ececd 100644
--- a/include/eepp/ui/uitextview.hpp
+++ b/include/eepp/ui/uitextview.hpp
@@ -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;
diff --git a/src/eepp/ui/uiimage.cpp b/src/eepp/ui/uiimage.cpp
index 3b973cd44..7cfab3793 100644
--- a/src/eepp/ui/uiimage.cpp
+++ b/src/eepp/ui/uiimage.cpp
@@ -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;
}
diff --git a/src/eepp/ui/uilinearlayout.cpp b/src/eepp/ui/uilinearlayout.cpp
index 79f49e743..b17ea7885 100644
--- a/src/eepp/ui/uilinearlayout.cpp
+++ b/src/eepp/ui/uilinearlayout.cpp
@@ -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() :
UILayout( "linearlayout" ), mOrientation( UIOrientation::Vertical ) {
mFlags |= UI_OWNS_CHILDREN_POSITION;
diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp
index dea8dc1e2..7b2a66d9a 100644
--- a/src/eepp/ui/uirichtext.cpp
+++ b/src/eepp/ui/uirichtext.cpp
@@ -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( 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( 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
diff --git a/src/eepp/ui/uiscrollbar.cpp b/src/eepp/ui/uiscrollbar.cpp
index 9c6182af4..5c9fd7a73 100644
--- a/src/eepp/ui/uiscrollbar.cpp
+++ b/src/eepp/ui/uiscrollbar.cpp
@@ -121,6 +121,9 @@ void UIScrollBar::setTheme( UITheme* Theme ) {
}
void UIScrollBar::onAutoSize() {
+ if ( mSlider == nullptr )
+ return;
+
Sizef size;
UISkin* tSkin = mSlider->getBackSlider()->getSkin();
diff --git a/src/eepp/ui/uitextview.cpp b/src/eepp/ui/uitextview.cpp
index 52d75a643..e828907ca 100644
--- a/src/eepp/ui/uitextview.cpp
+++ b/src/eepp/ui/uitextview.cpp
@@ -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() )
diff --git a/src/eepp/ui/uiwidgetcreator.cpp b/src/eepp/ui/uiwidgetcreator.cpp
index af920c069..1eb5b2ad3 100644
--- a/src/eepp/ui/uiwidgetcreator.cpp
+++ b/src/eepp/ui/uiwidgetcreator.cpp
@@ -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;
}
diff --git a/src/examples/ui_richtext/ui_richtext.cpp b/src/examples/ui_richtext/ui_richtext.cpp
index 7e880c4fd..4afc5cf8d 100644
--- a/src/examples/ui_richtext/ui_richtext.cpp
+++ b/src/examples/ui_richtext/ui_richtext.cpp
@@ -3,20 +3,29 @@
EE_MAIN_FUNC int main( int, char** ) {
UIApplication app( { 800, 600, "eepp - UIRichText Example" } );
app.getUI()->loadLayoutFromString( R"xml(
-
+
+
+
Welcome to the UIRichText example!
+ color="white">Welcome to the UIRichText example!
This component supports styled text,
shadows,
and outlines using HTML-like tags.
We can also mix contents with more text!
+ color="#fefefe">We can also mix contents with more text!
-
+
+
+
)xml" );
+
+ app.getUI()->on( Event::KeyUp, [&app]( const Event* event ) {
+ if ( event->asKeyEvent()->getKeyCode() == KEY_F11 ) {
+ UIWidgetInspector::create( app.getUI() );
+ }
+ } );
+
return app.run();
}
diff --git a/src/tests/unit_tests/richtext.cpp b/src/tests/unit_tests/richtext.cpp
index 3e6f2451d..f2dffa560 100644
--- a/src/tests/unit_tests/richtext.cpp
+++ b/src/tests/unit_tests/richtext.cpp
@@ -354,14 +354,14 @@ UTEST( UIRichText, RichTextTest ) {
layout_height="match_parent"
orientation="vertical">
Welcome to the UIRichText example!
+ color="white">Welcome to the UIRichText example!
This component supports styled text,
shadows,
and outlines using HTML-like tags.
We can also mix contents with more text!
+ color="#efefef">We can also mix contents with more text!
)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" );
diff --git a/src/tools/ecode/uiwelcomescreen.cpp b/src/tools/ecode/uiwelcomescreen.cpp
index 1d85ccc2c..b7e357522 100644
--- a/src/tools/ecode/uiwelcomescreen.cpp
+++ b/src/tools/ecode/uiwelcomescreen.cpp
@@ -169,18 +169,18 @@ static const auto LAYOUT = R"xml(
-
+
-
+
-
+
-
+