From 2613ec0921e628786f4dcbe8db2b1fa379b0ae47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Wed, 30 Sep 2026 12:19:40 -0300 Subject: [PATCH] Fix remaining DPI inconsistencies and expand regression coverage Correct checkbox and radio autosizing, submenu padding, scrollbar skin sizing, and nested menu-bar popup coordinates. Preserve resolved stroke widths through CSS serialization and transitions, including HTML's special px scaling. Add high-density regressions for these fixes and the preceding DPI cleanup commits, covering padding and margin setters, widget defaults, dialog dimensions, table padding, editor spacing, and diff gutters. --- .agents/rules/build-project.md | 4 +- .agents/rules/unit-tests.md | 2 +- include/eepp/ui/uimenubar.hpp | 2 + include/eepp/ui/uinode.hpp | 3 + .../ui/css/stylesheetpropertyanimation.cpp | 6 +- src/eepp/ui/uicheckbox.cpp | 6 +- src/eepp/ui/uicodeeditor.cpp | 2 +- src/eepp/ui/uiconsole.cpp | 2 +- src/eepp/ui/uimenubar.cpp | 18 +- src/eepp/ui/uimenusubmenu.cpp | 2 +- src/eepp/ui/uimessagebox.cpp | 2 +- src/eepp/ui/uinode.cpp | 5 + src/eepp/ui/uiradiobutton.cpp | 4 +- src/eepp/ui/uirichtext.cpp | 2 +- src/eepp/ui/uiscrollbar.cpp | 10 +- src/eepp/ui/uitextedit.cpp | 4 +- src/eepp/ui/uitextinput.cpp | 4 +- src/eepp/ui/uitextspan.cpp | 2 +- src/eepp/ui/uitextview.cpp | 2 +- src/eepp/ui/uitooltip.cpp | 4 +- .../unit_tests/ui_pixel_density_tests.cpp | 262 ++++++++++++++++++ .../unit_tests/uicss_inheritance_tests.cpp | 49 ++++ src/tests/unit_tests/uimenu_tests.cpp | 132 +++++++++ 23 files changed, 492 insertions(+), 37 deletions(-) create mode 100644 src/tests/unit_tests/ui_pixel_density_tests.cpp diff --git a/.agents/rules/build-project.md b/.agents/rules/build-project.md index 4a53fb463..296ba55de 100644 --- a/.agents/rules/build-project.md +++ b/.agents/rules/build-project.md @@ -27,9 +27,9 @@ arguments. Do not add sanitizer, linker, framework, generator, architecture, or outside the scripts unless `.ecode/project_build.json` itself requires them. Do not fall back to a hand-written Premake or direct Make command. -## Release Performance Builds (Linux) +## Release Builds (Linux) -For performance investigations, use the `eepp-linux-ninja` configuration from +For release builds, use the `eepp-linux-ninja` configuration from `.ecode/project_build.json`. At the time of writing, its commands are: `premake5 --disable-static-build --with-debug-symbols --with-backend=SDL3 ninja` diff --git a/.agents/rules/unit-tests.md b/.agents/rules/unit-tests.md index 24823eac5..3d1c9fb87 100644 --- a/.agents/rules/unit-tests.md +++ b/.agents/rules/unit-tests.md @@ -7,7 +7,7 @@ The test binary manages its own current working directory, so you can execute it The required default workflow is: -1. Build the release unit-test target. +1. Build the release unit-test target (use ninja build). 2. Run `bin/unit_tests/eepp-unit_tests` directly, without Xvfb. 3. If sandbox restrictions prevent access to the host display, retry that same direct command with elevated permissions. diff --git a/include/eepp/ui/uimenubar.hpp b/include/eepp/ui/uimenubar.hpp index 42c16b9aa..244e78a12 100644 --- a/include/eepp/ui/uimenubar.hpp +++ b/include/eepp/ui/uimenubar.hpp @@ -66,6 +66,8 @@ class EE_API UIMenuBar : public UIWidget { bool isGlobalMenuBarEnabled() const; protected: + void positionMenu( UISelectButton* button, UIPopUpMenu* menu ); + UIMenuBar(); typedef std::vector> MenuBarList; diff --git a/include/eepp/ui/uinode.hpp b/include/eepp/ui/uinode.hpp index 1cf64a17b..bc39c3a02 100644 --- a/include/eepp/ui/uinode.hpp +++ b/include/eepp/ui/uinode.hpp @@ -1139,6 +1139,9 @@ class EE_API UINode : public Node { virtual Float convertLength( const CSS::StyleSheetLength& length, const Float& containerLength ) const; + /** Serializes a resolved pixel length for reapplication, including HTML px scaling. */ + std::string pixelsLengthToString( Float pixels ) const; + /** * @brief Converts a CSS length to density-independent pixels (dp). * diff --git a/src/eepp/ui/css/stylesheetpropertyanimation.cpp b/src/eepp/ui/css/stylesheetpropertyanimation.cpp index 9947b595c..b48727aa9 100644 --- a/src/eepp/ui/css/stylesheetpropertyanimation.cpp +++ b/src/eepp/ui/css/stylesheetpropertyanimation.cpp @@ -73,7 +73,7 @@ void StyleSheetPropertyAnimation::tweenProperty( static_cast( startColor.a + ( endColor.a - startColor.a ) * progress ), 255 ) ); widget->applyProperty( - StyleSheetProperty( property, resColor.toHexString(), propertyIndex ) ); + StyleSheetProperty( property, resColor.toHexString(), propertyIndex ) ); break; } case PropertyType::NumberLength: { @@ -83,8 +83,8 @@ void StyleSheetPropertyAnimation::tweenProperty( Float end = widget->convertLength( endValue, containerLength ); Float value = easingFn( timingFunction, timingFunctionParameters, normalizedProgress, start, end - start, 1.f ); - widget->applyProperty( - StyleSheetProperty( property, String::fromFloat( value, "px" ), propertyIndex ) ); + widget->applyProperty( StyleSheetProperty( + property, widget->pixelsLengthToString( value ), propertyIndex ) ); if ( isDone ) { widget->applyProperty( StyleSheetProperty( property, endValue, propertyIndex ) ); diff --git a/src/eepp/ui/uicheckbox.cpp b/src/eepp/ui/uicheckbox.cpp index 58125d1f5..fd9870644 100644 --- a/src/eepp/ui/uicheckbox.cpp +++ b/src/eepp/ui/uicheckbox.cpp @@ -84,9 +84,9 @@ void UICheckBox::onThemeLoaded() { void UICheckBox::onAutoSize() { if ( mFlags & UI_AUTO_SIZE ) { if ( getSize().getWidth() == 0 ) { - setInternalPixelsWidth( (int)mTextCache.getTextWidth() + - mActiveButton->getPixelsSize().getWidth() + mTextSeparation + - mPaddingPx.Left + mPaddingPx.Right ); + setInternalPixelsWidth( + (int)mTextCache.getTextWidth() + mActiveButton->getPixelsSize().getWidth() + + PixelDensity::dpToPx( mTextSeparation ) + mPaddingPx.Left + mPaddingPx.Right ); } if ( getSize().getHeight() == 0 ) { diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index 3dd97b941..4191bc5e2 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -3274,7 +3274,7 @@ std::string UICodeEditor::getPropertyString( const PropertyDefinition* propertyD case PropertyId::FontWeight: return Text::fontWeightToString( mFontStyleConfig.Weight ); case PropertyId::TextStrokeWidth: - return String::fromFloat( getOutlineThickness(), "px" ); + return pixelsLengthToString( getOutlineThickness() ); case PropertyId::TextStrokeColor: return getOutlineColor().toHexString(); case PropertyId::TextSelection: diff --git a/src/eepp/ui/uiconsole.cpp b/src/eepp/ui/uiconsole.cpp index 211ba5865..eaf3195e8 100644 --- a/src/eepp/ui/uiconsole.cpp +++ b/src/eepp/ui/uiconsole.cpp @@ -281,7 +281,7 @@ std::string UIConsole::getPropertyString( const PropertyDefinition* propertyDef, case PropertyId::FontWeight: return Text::fontWeightToString( getFontStyleConfig().Weight ); case PropertyId::TextStrokeWidth: - return String::fromFloat( getFontOutlineThickness(), "px" ); + return pixelsLengthToString( getFontOutlineThickness() ); case PropertyId::TextStrokeColor: return getFontOutlineColor().toHexString(); default: diff --git a/src/eepp/ui/uimenubar.cpp b/src/eepp/ui/uimenubar.cpp index bbe16bf1d..097a28eb9 100644 --- a/src/eepp/ui/uimenubar.cpp +++ b/src/eepp/ui/uimenubar.cpp @@ -46,19 +46,25 @@ void UIMenuBar::setCurrentMenu( UIPopUpMenu* currentMenu ) { mWaitingUp = nullptr; } +void UIMenuBar::positionMenu( UISelectButton* button, UIPopUpMenu* menu ) { + Vector2f pos( 0, button->getSize().getHeight() ); + button->nodeToWorld( pos ); + menu->setParent( getWindowContainer() ); + menu->getParent()->worldToNode( pos ); + menu->setPosition( pos ); +} + void UIMenuBar::showMenu( const Uint32& index ) { eeASSERT( index < mButtons.size() ); auto but = mButtons[index]; auto tbut = but.first; auto tpop = but.second; - Vector2f pos( tbut->getPosition().x, tbut->getPosition().y + tbut->getSize().getHeight() ); - tpop->setPosition( pos ); + positionMenu( tbut, tpop ); if ( !tpop->isVisible() ) { mCurrentMenu = tpop; tbut->select(); - tpop->setParent( getWindowContainer() ); tpop->show(); mWaitingUp = tpop; } else if ( mCurrentMenu != tpop || mWaitingUp == nullptr ) { @@ -321,15 +327,12 @@ Uint32 UIMenuBar::onMessage( const NodeMessage* msg ) { if ( tpop == nullptr ) return 1; - Vector2f pos( tbut->getPosition().x, - tbut->getPosition().y + tbut->getSize().getHeight() ); - tpop->setPosition( pos ); + positionMenu( tbut, tpop ); if ( msg->getMsg() == NodeMessage::MouseOver ) { if ( nullptr != mCurrentMenu && mCurrentMenu != tpop ) { mCurrentMenu = tpop; tbut->select(); - tpop->setParent( getWindowContainer() ); tpop->show(); } } else { @@ -338,7 +341,6 @@ Uint32 UIMenuBar::onMessage( const NodeMessage* msg ) { if ( !tpop->isVisible() ) { mCurrentMenu = tpop; tbut->select(); - tpop->setParent( getWindowContainer() ); tpop->show(); mWaitingUp = tpop; } else if ( mCurrentMenu != tpop || mWaitingUp == nullptr ) { diff --git a/src/eepp/ui/uimenusubmenu.cpp b/src/eepp/ui/uimenusubmenu.cpp index 8b9108e43..a2ce4e9e4 100644 --- a/src/eepp/ui/uimenusubmenu.cpp +++ b/src/eepp/ui/uimenusubmenu.cpp @@ -104,7 +104,7 @@ void UIMenuSubMenu::showSubMenu() { mSubMenu->setParent( menu->getParent() ); Vector2f pos = getPixelsPosition(); nodeToWorldTranslation( pos ); - pos.x += mSize.getWidth() + menu->getPadding().Right; + pos.x += mSize.getWidth() + menu->getPixelsPadding().Right; UIMenu::findBestMenuPos( pos, mSubMenu, menu, this ); mSubMenu->getParent()->worldToNode( pos ); mSubMenu->setPosition( pos ); diff --git a/src/eepp/ui/uimessagebox.cpp b/src/eepp/ui/uimessagebox.cpp index d7c740442..b962867b9 100644 --- a/src/eepp/ui/uimessagebox.cpp +++ b/src/eepp/ui/uimessagebox.cpp @@ -64,7 +64,7 @@ UIMessageBox::UIMessageBox( const Type& type, const String& message, const Uint3 mTextEdit = UITextEdit::New(); mTextEdit->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ) ->setLayoutMargin( Rectf( 0, 4, 0, 4 ) ) - ->setSize( Vector2f{ 400, 100 } ) + ->setSize( Vector2f{ 600, 200 } ) ->setParent( vlay ); mTextEdit->getDocument().setCommand( "complete-edit", [this] { sendCommonEvent( Event::OnConfirm ); } ); diff --git a/src/eepp/ui/uinode.cpp b/src/eepp/ui/uinode.cpp index da0a8e548..8c5260189 100644 --- a/src/eepp/ui/uinode.cpp +++ b/src/eepp/ui/uinode.cpp @@ -1848,6 +1848,11 @@ Float UINode::convertLength( const CSS::StyleSheetLength& length, return ret; } +std::string UINode::pixelsLengthToString( Float pixels ) const { + return mFlags & UI_HTML_ELEMENT ? String::fromFloat( PixelDensity::pxToDp( pixels ), "dp" ) + : String::fromFloat( pixels, "px" ); +} + Float UINode::convertLengthAsDp( const StyleSheetLength& length, const Float& containerLength ) const { return PixelDensity::pxToDp( convertLength( length, containerLength ) ); diff --git a/src/eepp/ui/uiradiobutton.cpp b/src/eepp/ui/uiradiobutton.cpp index 0b9120c56..a056425bb 100644 --- a/src/eepp/ui/uiradiobutton.cpp +++ b/src/eepp/ui/uiradiobutton.cpp @@ -87,8 +87,8 @@ void UIRadioButton::onAutoSize() { } if ( getSize().getHeight() == 0 ) { - setInternalHeight( mActiveButton->getSize().getHeight() + mPaddingPx.Top + - mPaddingPx.Bottom ); + setInternalHeight( mActiveButton->getSize().getHeight() + mPadding.Top + + mPadding.Bottom ); } } diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 5caac79d6..7ddc61f8d 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -681,7 +681,7 @@ std::string UIRichText::getPropertyString( const PropertyDefinition* propertyDef return String::fromFloat( getFontShadowOffset().x ) + " " + String::fromFloat( getFontShadowOffset().y ); case PropertyId::TextStrokeWidth: - return String::fromFloat( getOutlineThickness(), "px" ); + return pixelsLengthToString( getOutlineThickness() ); case PropertyId::TextStrokeColor: return getOutlineColor().toHexString(); case PropertyId::SelectionColor: diff --git a/src/eepp/ui/uiscrollbar.cpp b/src/eepp/ui/uiscrollbar.cpp index 4dc6fec5d..41084e6ab 100644 --- a/src/eepp/ui/uiscrollbar.cpp +++ b/src/eepp/ui/uiscrollbar.cpp @@ -128,9 +128,9 @@ void UIScrollBar::onAutoSize() { UISkin* tSkin = mSlider->getBackSlider()->getSkin(); if ( NULL != tSkin ) { - size = tSkin->getSize(); + size = PixelDensity::pxToDp( tSkin->getPixelsSize() ); - setMinSize( PixelDensity::pxToDp( size ) ); + setMinSize( size ); if ( mFlags & UI_AUTO_SIZE ) { if ( mSlider->isVertical() ) { @@ -145,9 +145,9 @@ void UIScrollBar::onAutoSize() { tSkin = mSlider->getSliderButton()->getSkin(); if ( NULL != tSkin ) { - size = tSkin->getSize(); + size = PixelDensity::pxToDp( tSkin->getPixelsSize() ); - setMinSize( PixelDensity::pxToDp( size ) ); + setMinSize( size ); if ( mFlags & UI_AUTO_SIZE ) { if ( mSlider->isVertical() ) { @@ -160,7 +160,7 @@ void UIScrollBar::onAutoSize() { } if ( mWidthPolicy == SizePolicy::WrapContent || mHeightPolicy == SizePolicy::WrapContent ) { - size = PixelDensity::dpToPx( mSlider->getSize() ) + mPaddingPx; + size = mSlider->getPixelsSize() + mPaddingPx; if ( mScrollBarStyle == TwoButtons ) { if ( mSlider->isVertical() ) { diff --git a/src/eepp/ui/uitextedit.cpp b/src/eepp/ui/uitextedit.cpp index 060fc23d2..382b8038f 100644 --- a/src/eepp/ui/uitextedit.cpp +++ b/src/eepp/ui/uitextedit.cpp @@ -262,7 +262,7 @@ bool UITextEdit::applyProperty( const StyleSheetProperty& attribute ) { setHintFontStyle( attribute.asFontStyle() ); break; case PropertyId::HintStrokeWidth: - setHintOutlineThickness( PixelDensity::dpToPx( attribute.asDpDimension() ) ); + setHintOutlineThickness( lengthFromValue( attribute ) ); break; case PropertyId::HintStrokeColor: setHintOutlineColor( attribute.asColor() ); @@ -301,7 +301,7 @@ std::string UITextEdit::getPropertyString( const PropertyDefinition* propertyDef case PropertyId::HintFontStyle: return Text::styleFlagToString( getHintFontStyle() ); case PropertyId::HintStrokeWidth: - return String::fromFloat( getHintOutlineThickness(), "px" ); + return pixelsLengthToString( getHintOutlineThickness() ); case PropertyId::HintStrokeColor: return getHintOutlineColor().toHexString(); case PropertyId::HintDisplay: diff --git a/src/eepp/ui/uitextinput.cpp b/src/eepp/ui/uitextinput.cpp index 7c8a50117..2c9df8a27 100644 --- a/src/eepp/ui/uitextinput.cpp +++ b/src/eepp/ui/uitextinput.cpp @@ -602,7 +602,7 @@ std::string UITextInput::getPropertyString( const PropertyDefinition* propertyDe case PropertyId::HintFontStyle: return Text::styleFlagToString( getHintFontStyle() ); case PropertyId::HintStrokeWidth: - return String::fromFloat( getHintOutlineThickness(), "px" ); + return pixelsLengthToString( getHintOutlineThickness() ); case PropertyId::HintStrokeColor: return getHintOutlineColor().toHexString(); case PropertyId::HintDisplay: @@ -682,7 +682,7 @@ bool UITextInput::applyProperty( const StyleSheetProperty& attribute ) { setHintFontStyle( attribute.asFontStyle() ); break; case PropertyId::HintStrokeWidth: - setHintOutlineThickness( PixelDensity::dpToPx( attribute.asDpDimension() ) ); + setHintOutlineThickness( lengthFromValue( attribute ) ); break; case PropertyId::HintStrokeColor: setHintOutlineColor( attribute.asColor() ); diff --git a/src/eepp/ui/uitextspan.cpp b/src/eepp/ui/uitextspan.cpp index ff78d8276..adb1cbdcd 100644 --- a/src/eepp/ui/uitextspan.cpp +++ b/src/eepp/ui/uitextspan.cpp @@ -226,7 +226,7 @@ std::string UITextSpan::getPropertyString( const PropertyDefinition* propertyDef return String::fromFloat( getFontShadowOffset().x ) + " " + String::fromFloat( getFontShadowOffset().y ); case PropertyId::TextStrokeWidth: - return String::fromFloat( getOutlineThickness(), "px" ); + return pixelsLengthToString( getOutlineThickness() ); case PropertyId::TextStrokeColor: return getOutlineColor().toHexString(); case PropertyId::TextDecoration: diff --git a/src/eepp/ui/uitextview.cpp b/src/eepp/ui/uitextview.cpp index 26b643115..442065618 100644 --- a/src/eepp/ui/uitextview.cpp +++ b/src/eepp/ui/uitextview.cpp @@ -917,7 +917,7 @@ std::string UITextView::getPropertyString( const PropertyDefinition* propertyDef case PropertyId::FontWeight: return Text::fontWeightToString( mFontStyleConfig.Weight ); case PropertyId::TextStrokeWidth: - return String::fromFloat( getOutlineThickness(), "px" ); + return pixelsLengthToString( getOutlineThickness() ); case PropertyId::TextStrokeColor: return getOutlineColor().toHexString(); case PropertyId::Wordwrap: diff --git a/src/eepp/ui/uitooltip.cpp b/src/eepp/ui/uitooltip.cpp index e9c69d6f3..fe4fb535f 100644 --- a/src/eepp/ui/uitooltip.cpp +++ b/src/eepp/ui/uitooltip.cpp @@ -513,7 +513,7 @@ std::string UITooltip::getPropertyString( const PropertyDefinition* propertyDef, case PropertyId::FontWeight: return Text::fontWeightToString( mStyleConfig.Weight ); case PropertyId::TextStrokeWidth: - return String::fromFloat( getOutlineThickness(), "px" ); + return pixelsLengthToString( getOutlineThickness() ); case PropertyId::TextStrokeColor: return getOutlineColor().toHexString(); case PropertyId::Wordwrap: @@ -652,7 +652,7 @@ bool UITooltip::applyProperty( const StyleSheetProperty& attribute ) { break; case PropertyId::TextStrokeWidth: if ( !mUsingCustomStyling ) - setOutlineThickness( PixelDensity::dpToPx( attribute.asDpDimension() ) ); + setOutlineThickness( lengthFromValue( attribute ) ); break; case PropertyId::TextStrokeColor: if ( !mUsingCustomStyling ) diff --git a/src/tests/unit_tests/ui_pixel_density_tests.cpp b/src/tests/unit_tests/ui_pixel_density_tests.cpp new file mode 100644 index 000000000..dd1108032 --- /dev/null +++ b/src/tests/unit_tests/ui_pixel_density_tests.cpp @@ -0,0 +1,262 @@ +#include "utest.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace EE; +using namespace EE::Graphics; +using namespace EE::System; +using namespace EE::UI; +using namespace EE::UI::Tools; + +namespace { + +struct RestoreDensity { + Float previous{ PixelDensity::getPixelDensity() }; + + ~RestoreDensity() { PixelDensity::setPixelDensity( previous ); } +}; + +struct DensityApplication { + RestoreDensity density; + UIApplication app{ + WindowSettings( 1200, 1000, "Pixel Density Regression", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 2.f ) }; +}; + +class PixelSetterWidget : public UIWidget { + public: + PixelSetterWidget() : UIWidget( "pixel-setter-test" ) {} + + int paddingChanges{ 0 }; + int marginChanges{ 0 }; + + protected: + void onPaddingChange() { + ++paddingChanges; + UIWidget::onPaddingChange(); + } + + void onMarginChange() { + ++marginChanges; + UIWidget::onMarginChange(); + } +}; + +class DensityListBox : public UIListBox { + public: + void recalculateRowHeight() { + mRowHeight = 0; + setRowHeight(); + } +}; + +class DensityWidgetTable : public UIWidgetTable { + public: + void refreshSkinPadding() { + autoPadding(); + containerResize(); + } +}; + +} // namespace + +UTEST( PixelDensityRegression, PixelPaddingUpdatesAndRepeatedValues ) { + DensityApplication fixture; + auto* widget = eeNew( PixelSetterWidget, () ); + widget->setParent( fixture.app.getUI() ); + const Rectf logical( 4, 6, 8, 10 ); + widget->setPadding( logical ); + int changes = widget->paddingChanges; + // A pixel value equal to the previous DP value must still update the widget. + widget->setPaddingPixels( logical ); + EXPECT_TRUE( widget->getPixelsPadding() == logical ); + EXPECT_TRUE( widget->getPadding() == Rectf( 2, 3, 4, 5 ) ); + EXPECT_EQ( widget->paddingChanges, changes + 1 ); + widget->setPaddingPixels( logical ); + EXPECT_EQ( widget->paddingChanges, changes + 1 ); + + using Setter = UIWidget* ( UIWidget::* )( const Float& ); + Setter setters[] = { &UIWidget::setPaddingPixelsLeft, &UIWidget::setPaddingPixelsTop, + &UIWidget::setPaddingPixelsRight, &UIWidget::setPaddingPixelsBottom }; + for ( auto setter : setters ) { + widget->setPadding( Rectf( 4, 4, 4, 4 ) ); + changes = widget->paddingChanges; + ( widget->*setter )( 4 ); + EXPECT_EQ( widget->paddingChanges, changes + 1 ); + ( widget->*setter )( 4 ); + EXPECT_EQ( widget->paddingChanges, changes + 1 ); + ( widget->*setter )( 5 ); + const Rectf pixels = widget->getPixelsPadding(); + const Rectf dp = widget->getPadding(); + EXPECT_TRUE( dp == PixelDensity::pxToDp( pixels ).ceil() ); + EXPECT_EQ( pixels.Left + pixels.Top + pixels.Right + pixels.Bottom, 29.f ); + } +} + +UTEST( PixelDensityRegression, PixelMarginsUpdateAndRepeatedValues ) { + DensityApplication fixture; + auto* widget = eeNew( PixelSetterWidget, () ); + widget->setParent( fixture.app.getUI() ); + const Rectf logical( 4, 6, 8, 10 ); + widget->setLayoutMargin( logical ); + int changes = widget->marginChanges; + widget->setLayoutPixelsMargin( logical ); + EXPECT_TRUE( widget->getLayoutPixelsMargin() == logical ); + EXPECT_TRUE( widget->getLayoutMargin() == Rectf( 2, 3, 4, 5 ) ); + EXPECT_EQ( widget->marginChanges, changes + 1 ); + widget->setLayoutPixelsMargin( logical ); + EXPECT_EQ( widget->marginChanges, changes + 1 ); + + using Setter = UIWidget* ( UIWidget::* )( const Float& ); + Setter setters[] = { &UIWidget::setLayoutPixelsMarginLeft, &UIWidget::setLayoutPixelsMarginTop, + &UIWidget::setLayoutPixelsMarginRight, + &UIWidget::setLayoutPixelsMarginBottom }; + for ( auto setter : setters ) { + widget->setLayoutMargin( Rectf( 4, 4, 4, 4 ) ); + changes = widget->marginChanges; + ( widget->*setter )( 4 ); + EXPECT_EQ( widget->marginChanges, changes + 1 ); + ( widget->*setter )( 4 ); + EXPECT_EQ( widget->marginChanges, changes + 1 ); + ( widget->*setter )( 5 ); + const Rectf pixels = widget->getLayoutPixelsMargin(); + EXPECT_TRUE( widget->getLayoutMargin() == PixelDensity::pxToDp( pixels ).ceil() ); + EXPECT_EQ( pixels.Left + pixels.Top + pixels.Right + pixels.Bottom, 29.f ); + } +} + +UTEST( PixelDensityRegression, LoaderPagerAndMergeToolbarUseLogicalDefaults ) { + DensityApplication fixture; + auto* loader = UILoader::New(); + loader->setParent( fixture.app.getUI() ); + EXPECT_EQ( loader->getOutlineThickness(), 8.f ); + auto* viewer = UIImageViewer::New(); + viewer->setParent( fixture.app.getUI() ); + EXPECT_EQ( viewer->getLoader()->getOutlineThickness(), 6.f ); + EXPECT_EQ( viewer->getLoader()->getPixelsSize().x, 128.f ); + auto* pager = UIViewPager::New(); + pager->setParent( fixture.app.getUI() ); + EXPECT_EQ( pager->getDragResistance(), 8.f ); + auto* merge = UIMergeView::New(); + merge->setParent( fixture.app.getUI() ); + auto* button = merge->addToolbarAction( "dpi-test", "Test", {}, "", [] {} ); + EXPECT_EQ( button->getLayoutMargin().Right, 4.f ); + EXPECT_EQ( button->getLayoutPixelsMargin().Right, 8.f ); +} + +UTEST( PixelDensityRegression, ListBoxAutomaticRowHeightAddsLogicalPadding ) { + DensityApplication fixture; + auto* list = eeNew( DensityListBox, () ); + list->setParent( fixture.app.getUI() ); + list->getReferenceItem()->setFontSize( 16 ); + list->recalculateRowHeight(); + const auto& style = list->getReferenceItem()->getFontStyleConfig(); + const Uint32 fontHeight = + style.getFont() ? style.getFont()->getFontHeight( style.getFontCharacterSize() ) : 12; + EXPECT_EQ( list->getRowHeight(), static_cast( fontHeight / 2.f + 4 ) ); +} + +UTEST( PixelDensityRegression, MessageBoxControlsUseLogicalDimensions ) { + DensityApplication fixture; + auto* editBox = UIMessageBox::New( UIMessageBox::TEXT_EDIT, "Edit" ); + EXPECT_EQ( editBox->getTextEdit()->getSize().x, 400.f ); + EXPECT_EQ( editBox->getTextEdit()->getSize().y, 100.f ); + EXPECT_EQ( editBox->getTextEdit()->getPixelsSize().x, 800.f ); + auto* dropdownBox = UIMessageBox::New( UIMessageBox::DROPDOWNLIST, "Choose" ); + EXPECT_EQ( dropdownBox->getDropDownList()->getSize().x, 200.f ); + EXPECT_EQ( dropdownBox->getDropDownList()->getPixelsSize().x, 400.f ); + auto* comboBox = UIMessageBox::New( UIMessageBox::COMBOBOX, "Choose" ); + EXPECT_EQ( comboBox->getComboBox()->getSize().x, 200.f ); + EXPECT_EQ( comboBox->getComboBox()->getPixelsSize().x, 400.f ); +} + +UTEST( PixelDensityRegression, FontPickerUsesLogicalSceneInset ) { + DensityApplication fixture; + fixture.app.getUI()->setSize( 600, 450 ); + const Sizef scene = fixture.app.getUI()->getSize(); + auto* dialog = UIFontPickerDialog::New(); + + EXPECT_EQ( dialog->getMinWindowSize().x, scene.x - 32.f ); + EXPECT_EQ( dialog->getMinWindowSize().y, scene.y - 32.f ); +} + +UTEST( PixelDensityRegression, EditorLineNumberPaddingScalesExactlyOnce ) { + DensityApplication fixture; + auto* editor = UICodeEditor::New(); + editor->setParent( fixture.app.getUI() ); + editor->setLineNumberPadding( 3, 5 ); + EXPECT_EQ( editor->getLineNumberPaddingLeft(), 6.f ); + EXPECT_EQ( editor->getLineNumberPaddingRight(), 10.f ); + editor->setLineNumberPadding( 3, 5 ); + EXPECT_EQ( editor->getLineNumberPaddingLeft(), 6.f ); + EXPECT_EQ( editor->getLineNumberPaddingRight(), 10.f ); + EXPECT_EQ( editor->getLineNumberWidth(), + eeceil( editor->getLineNumberDigits() * editor->getGlyphWidth() + 16.f ) ); +} + +UTEST( PixelDensityRegression, DiffGuttersUsePhysicalGlyphWidthAndGrowWithLineNumbers ) { + DensityApplication fixture; + auto* diff = UIDiffView::New(); + diff->setParent( fixture.app.getUI() ); + for ( bool large : { false, true, false } ) { + diff->loadFromPatch( large + ? "--- a/test\n+++ b/test\n@@ -100000,1 +100000,1 @@\n-old\n+new\n" + : "--- a/test\n+++ b/test\n@@ -1,1 +1,1 @@\n-old\n+new\n" ); + const int digits = large ? 6 : 5; + EXPECT_EQ( diff->getEditor()->getPluginsGutterSpace(), + diff->getEditor()->getGlyphWidth() * ( digits * 2 + 1 ) ); + diff->setViewMode( UIDiffView::ViewMode::SideBySide ); + for ( auto* editor : { diff->getLeftEditor(), diff->getRightEditor() } ) { + EXPECT_EQ( editor->getPluginsGutterSpace(), editor->getGlyphWidth() * digits ); + editor->setFontSize( 18 ); + EXPECT_EQ( editor->getPluginsGutterSpace(), editor->getGlyphWidth() * digits ); + } + diff->setViewMode( UIDiffView::ViewMode::Unified ); + } +} + +UTEST( PixelDensityRegression, TableContainerPaddingAndOriginStayLogical ) { + DensityApplication fixture; + auto* table = eeNew( DensityWidgetTable, () ); + table->setParent( fixture.app.getUI() ); + table->unsetFlags( UI_AUTO_PADDING ); + table->setSize( 200, 100 ); + table->setPadding( Rectf( 3, 4, 5, 6 ) ); + EXPECT_TRUE( table->getContainerPadding() == Rectf( 3, 4, 5, 6 ) ); + EXPECT_EQ( table->getContainer()->getPosition().x, 3.f ); + EXPECT_EQ( table->getContainer()->getPosition().y, 4.f ); + EXPECT_EQ( table->getContainer()->getPixelsPosition().x, 6.f ); + EXPECT_EQ( table->getContainer()->getPixelsPosition().y, 8.f ); + unsigned char pixels[20 * 30 * 4] = {}; + auto texture = TextureFactory::instance()->loadFromPixels( pixels, 20, 30, 4 ); + auto skin = UISkin::New( "table-dpi-test" ); + skin->setStateDrawable( UIState::StateFlagNormal, NinePatch::New( texture, 2, 4, 6, 8, 2.f ) ); + table->setSkin( skin.get() ); + table->setFlags( UI_AUTO_PADDING ); + table->refreshSkinPadding(); + const Rectf expected = PixelDensity::pxToDp( skin->getBorderSize() ) + table->getPadding(); + EXPECT_TRUE( table->getContainerPadding() == expected ); + EXPECT_EQ( table->getContainer()->getPosition().x, expected.Left ); + EXPECT_EQ( table->getContainer()->getPosition().y, expected.Top ); +} diff --git a/src/tests/unit_tests/uicss_inheritance_tests.cpp b/src/tests/unit_tests/uicss_inheritance_tests.cpp index 8231932c4..a623b224b 100644 --- a/src/tests/unit_tests/uicss_inheritance_tests.cpp +++ b/src/tests/unit_tests/uicss_inheritance_tests.cpp @@ -13,15 +13,20 @@ #include #include #include +#include +#include #include #include #include #include #include #include +#include +#include #include #include #include +#include #include #include @@ -2101,3 +2106,47 @@ UTEST( CSSFunctions, NotAFunctionReturnsDefault ) { EXPECT_EQ( StyleSheetLength::Unit::Px, len.getUnit() ); EXPECT_EQ( 0, len.getValue() ); } + +UTEST( CSSUnits, StrokeRoundTripAndTransitionAtHighDensity ) { + UIApplication app( WindowSettings( 800, 600, "eepp - CSS Units Test", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + UIApplication::Settings( System::Sys::getProcessPath() + ".." + + System::FileSystem::getOSSlash(), + 1 ) ); + const Float previousDensity = PixelDensity::getPixelDensity(); + PixelDensity::setPixelDensity( 2.f ); + auto* definition = + StyleSheetSpecification::instance()->getProperty( PropertyId::TextStrokeWidth ); + UIWidget* widgets[] = { UITextView::New(), UITextSpan::New(), UIRichText::New(), + UITooltip::New(), UIConsole::New(), UICodeEditor::New(), + UITextInput::New(), UITextEdit::New() }; + for ( auto* widget : widgets ) { + widget->setParent( app.getUI() ); + definition = StyleSheetSpecification::instance()->getProperty( + widget->isType( UI_TYPE_TEXTINPUT ) || widget->isType( UI_TYPE_TEXTEDIT ) + ? PropertyId::HintStrokeWidth + : PropertyId::TextStrokeWidth ); + for ( bool html : { false, true } ) { + if ( html ) + widget->setFlags( UI_HTML_ELEMENT ); + else + widget->unsetFlags( UI_HTML_ELEMENT ); + for ( const char* source : { "2px", "2dp" } ) { + widget->applyProperty( StyleSheetProperty( definition, source ) ); + const Float expected = html || std::string_view( source ) == "2dp" ? 4.f : 2.f; + std::string serialized = widget->getPropertyString( definition ); + EXPECT_EQ( widget->lengthFromValue( serialized, PropertyRelativeTarget::None, 0 ), + expected ); + widget->applyProperty( StyleSheetProperty( definition, serialized ) ); + EXPECT_TRUE( widget->getPropertyString( definition ) == serialized ); + StyleSheetPropertyAnimation::tweenProperty( widget, 0.5f, definition, serialized, + "6dp", Ease::Interpolation::Linear, {}, + 0, false ); + EXPECT_EQ( widget->lengthFromValue( widget->getPropertyString( definition ), + PropertyRelativeTarget::None, 0 ), + ( expected + 12.f ) / 2.f ); + } + } + } + PixelDensity::setPixelDensity( previousDensity ); +} diff --git a/src/tests/unit_tests/uimenu_tests.cpp b/src/tests/unit_tests/uimenu_tests.cpp index ca8e1e586..6c08e7373 100644 --- a/src/tests/unit_tests/uimenu_tests.cpp +++ b/src/tests/unit_tests/uimenu_tests.cpp @@ -1,14 +1,24 @@ #include "utest.h" +#include +#include +#include +#include +#include #include #include #include +#include #include +#include #include +#include #include +#include using namespace EE; using namespace EE::System; +using namespace EE::Graphics; using namespace EE::UI; using namespace EE::Window; @@ -85,3 +95,125 @@ UTEST( UIMenu, SemanticActivationLifecycleAndRoles ) { subMenu->showSubMenu(); EXPECT_EQ( subMenuShowStep, 2 ); } + +UTEST( UIMenu, NestedMenuBarAndSubmenuAtHighDensity ) { + UIApplication app( + WindowSettings( 800, 600, "eepp - Menu DPI Test", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + const Float previousDensity = PixelDensity::getPixelDensity(); + PixelDensity::setPixelDensity( 2.f ); + auto* container = UIWidget::New(); + container->setParent( app.getUI() ); + container->setPosition( 30, 40 ); + auto* bar = UIMenuBar::New(); + bar->setParent( container ); + bar->setPosition( 10, 15 ); + bar->setSize( 300, 24 ); + auto* popup = UIPopUpMenu::New(); + popup->add( "Item" ); + bar->addMenuButton( "File", popup ); + auto* button = bar->getButton( 0 ); + Vector2f expected( 0, button->getSize().getHeight() ); + button->nodeToWorld( expected ); + bar->showMenu( 0 ); + Vector2f actual( 0, 0 ); + popup->nodeToWorld( actual ); + EXPECT_EQ( actual.x, expected.x ); + EXPECT_EQ( actual.y, expected.y ); + popup->setPadding( Rectf( 0, 0, 3, 0 ) ); + auto* child = UIPopUpMenu::New(); + child->add( "Child" ); + auto* item = popup->addSubMenu( "Submenu", {}, child ); + // The placement policy aligns the submenu to the parent menu's right edge. + Vector2f subExpected( popup->getSize().getWidth(), 0 ); + popup->nodeToWorld( subExpected ); + item->showSubMenu(); + Vector2f subActual( 0, 0 ); + child->nodeToWorld( subActual ); + EXPECT_EQ( subActual.x, subExpected.x ); + PixelDensity::setPixelDensity( previousDensity ); +} + +namespace { + +class DensityTestCheckBox : public UICheckBox { + public: + using UICheckBox::onAutoSize; +}; + +class DensityTestRadioButton : public UIRadioButton { + public: + using UIRadioButton::onAutoSize; +}; + +class DensityTestScrollBar : public UIScrollBar { + public: + DensityTestScrollBar( UIOrientation orientation ) : UIScrollBar( "scrollbar", orientation ) {} + + using UIScrollBar::onAutoSize; +}; + +} // namespace + +UTEST( UIUnits, AutoSizeAtHighDensity ) { + UIApplication app( + WindowSettings( 800, 600, "eepp - Sizing DPI Test", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + const Float previousDensity = PixelDensity::getPixelDensity(); + PixelDensity::setPixelDensity( 2.f ); + auto* checkbox = eeNew( DensityTestCheckBox, () ); + checkbox->setParent( app.getUI() ); + checkbox->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + checkbox->setFlags( UI_AUTO_SIZE ); + checkbox->setTextSeparation( 8 ); + checkbox->setPadding( Rectf( 3, 4, 5, 6 ) ); + checkbox->getCheckedButton()->setSize( 10, 12 ); + checkbox->setSize( 0, 0 ); + checkbox->onAutoSize(); + EXPECT_EQ( checkbox->getPixelsSize().x, (int)checkbox->getTextWidth() + 20 + 16 + + checkbox->getPixelsPadding().Left + + checkbox->getPixelsPadding().Right ); + auto* radio = eeNew( DensityTestRadioButton, () ); + radio->setParent( app.getUI() ); + radio->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + radio->setFlags( UI_AUTO_SIZE ); + radio->setPadding( Rectf( 3, 4, 5, 6 ) ); + radio->getActiveButton()->setSize( 10, 12 ); + radio->setMinSize( Sizef::Zero ); + radio->setSize( 0, 0 ); + radio->onAutoSize(); + EXPECT_EQ( radio->getSize().y, 12 + radio->getPadding().Top + radio->getPadding().Bottom ); + + auto region = TextureRegion::New(); + region->setDestSize( Sizef( 20, 30 ) ); + region->setPixelDensity( 2.f ); + unsigned char texturePixels[20 * 30 * 4] = {}; + auto texture = TextureFactory::instance()->loadFromPixels( texturePixels, 20, 30, 4 ); + auto ninePatch = NinePatch::New( texture, 2, 2, 2, 2, 2.f ); + DrawablePtr drawables[] = { DrawablePtr( RectangleDrawable::New( {}, Sizef( 20, 30 ) ) ), + region, ninePatch }; + for ( auto& drawable : drawables ) { + auto skin = UISkin::New( "dpi-test" ); + skin->setStateDrawable( UIState::StateFlagNormal, drawable ); + for ( auto orientation : { UIOrientation::Vertical, UIOrientation::Horizontal } ) { + auto* scrollbar = eeNew( DensityTestScrollBar, ( orientation ) ); + scrollbar->setParent( app.getUI() ); + scrollbar->setSize( 100, 100 ); + scrollbar->getSlider()->getBackSlider()->setSkin( skin.get() ); + scrollbar->onAutoSize(); + const Sizef pixels = skin->getPixelsSize(); + EXPECT_EQ( scrollbar->getCurMinSize().x, pixels.x / 2.f ); + EXPECT_EQ( scrollbar->getCurMinSize().y, pixels.y / 2.f ); + if ( orientation == UIOrientation::Vertical ) { + EXPECT_EQ( scrollbar->getPixelsSize().x, pixels.x ); + EXPECT_EQ( scrollbar->getSlider()->getPixelsSize().x, pixels.x ); + } else { + EXPECT_EQ( scrollbar->getPixelsSize().y, pixels.y ); + EXPECT_EQ( scrollbar->getSlider()->getPixelsSize().y, pixels.y ); + } + } + } + PixelDensity::setPixelDensity( previousDensity ); +}