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.
This commit is contained in:
Martín Lucas Golini
2026-09-30 12:19:40 -03:00
parent d04a6ad279
commit 2613ec0921
23 changed files with 492 additions and 37 deletions
+2 -2
View File
@@ -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`
+1 -1
View File
@@ -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.
+2
View File
@@ -66,6 +66,8 @@ class EE_API UIMenuBar : public UIWidget {
bool isGlobalMenuBarEnabled() const;
protected:
void positionMenu( UISelectButton* button, UIPopUpMenu* menu );
UIMenuBar();
typedef std::vector<std::pair<UISelectButton*, UIPopUpMenu*>> MenuBarList;
+3
View File
@@ -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).
*
@@ -73,7 +73,7 @@ void StyleSheetPropertyAnimation::tweenProperty(
static_cast<Int32>( 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 ) );
+3 -3
View File
@@ -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 ) {
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+10 -8
View File
@@ -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 ) {
+1 -1
View File
@@ -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 );
+1 -1
View File
@@ -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 ); } );
+5
View File
@@ -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 ) );
+2 -2
View File
@@ -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 );
}
}
+1 -1
View File
@@ -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:
+5 -5
View File
@@ -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() ) {
+2 -2
View File
@@ -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:
+2 -2
View File
@@ -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() );
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+2 -2
View File
@@ -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 )
@@ -0,0 +1,262 @@
#include "utest.hpp"
#include <eepp/graphics/font.hpp>
#include <eepp/graphics/fonttruetype.hpp>
#include <eepp/graphics/ninepatch.hpp>
#include <eepp/graphics/pixeldensity.hpp>
#include <eepp/graphics/texturefactory.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/ui/tools/uidiffview.hpp>
#include <eepp/ui/tools/uifontpickerdialog.hpp>
#include <eepp/ui/tools/uiimageviewer.hpp>
#include <eepp/ui/tools/uimergeview.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uicombobox.hpp>
#include <eepp/ui/uidropdownlist.hpp>
#include <eepp/ui/uilistbox.hpp>
#include <eepp/ui/uiloader.hpp>
#include <eepp/ui/uimessagebox.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uitextedit.hpp>
#include <eepp/ui/uiviewpager.hpp>
#include <eepp/ui/uiwidgettable.hpp>
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<Uint32>( 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 );
}
@@ -13,15 +13,20 @@
#include <eepp/ui/css/stylesheetspecification.hpp>
#include <eepp/ui/tools/htmlformatter.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uicodeeditor.hpp>
#include <eepp/ui/uiconsole.hpp>
#include <eepp/ui/uidropdownlist.hpp>
#include <eepp/ui/uinodedrawable.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uistyle.hpp>
#include <eepp/ui/uitabwidget.hpp>
#include <eepp/ui/uitextedit.hpp>
#include <eepp/ui/uitextinput.hpp>
#include <eepp/ui/uitextspan.hpp>
#include <eepp/ui/uitextview.hpp>
#include <eepp/ui/uithememanager.hpp>
#include <eepp/ui/uitooltip.hpp>
#include <eepp/ui/uiwidget.hpp>
#include <eepp/window/input.hpp>
@@ -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 );
}
+132
View File
@@ -1,14 +1,24 @@
#include "utest.h"
#include <eepp/graphics/ninepatch.hpp>
#include <eepp/graphics/pixeldensity.hpp>
#include <eepp/graphics/rectangledrawable.hpp>
#include <eepp/graphics/texturefactory.hpp>
#include <eepp/graphics/textureregion.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uicheckbox.hpp>
#include <eepp/ui/uimenu.hpp>
#include <eepp/ui/uimenubar.hpp>
#include <eepp/ui/uipopupmenu.hpp>
#include <eepp/ui/uiradiobutton.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uiscrollbar.hpp>
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 );
}