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( - + - + - + - +