From b3e6ea559dedb1666bc0437a43af2ea047bd3754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Fri, 25 Sep 2026 10:51:55 -0300 Subject: [PATCH] Add pixel-density default and improve chart rendering - Honor EEPP_PIXEL_DENSITY when an app has not specified a scale, without resizing its window. - Scale chart measurements in dp, expose chart styling, and resolve default colors through the UI theme. - Fix gaps at miter joins and round chart text to device pixels. - Use inline storage for common chart axis, series, and label counts. - Add density and rendering regression tests. --- .ecode/project_build.json | 6 + include/eepp/graphics/pixeldensity.hpp | 3 + include/eepp/ui/charts/uichart.hpp | 89 +++++-- include/eepp/ui/uiapplication.hpp | 2 +- include/eepp/ui/uistyle.hpp | 3 + include/eepp/ui/uiwidget.hpp | 3 + include/eepp/window/window.hpp | 6 +- src/eepp/graphics/pixeldensity.cpp | 15 ++ src/eepp/ui/uiapplication.cpp | 5 + src/eepp/ui/uichart.cpp | 239 +++++++++++------- src/eepp/ui/uistyle.cpp | 5 + src/eepp/ui/uiwidget.cpp | 4 + src/eepp/window/backend/SDL3/windowsdl3.cpp | 6 +- src/eepp/window/engine.cpp | 9 +- src/examples/ui_charts/ui_charts.cpp | 8 +- src/tests/unit_tests/charts_ui_tests.cpp | 79 +++++- .../pixel_density_environment_tests.cpp | 46 ++++ src/tools/ecode/ecode.cpp | 22 +- src/tools/eterm/eterm.cpp | 10 +- 19 files changed, 424 insertions(+), 136 deletions(-) create mode 100644 src/tests/unit_tests/pixel_density_environment_tests.cpp diff --git a/.ecode/project_build.json b/.ecode/project_build.json index 535b9a5b1..dda27ffd8 100644 --- a/.ecode/project_build.json +++ b/.ecode/project_build.json @@ -423,6 +423,12 @@ "command": "${project_root}/bin/eproc-debug", "name": "eproc-debug", "working_dir": "${project_root}/bin" + }, + { + "args": "", + "command": "${project_root}/bin/eepp-ui-charts-debug", + "name": "eepp-ui-charts-debug", + "working_dir": "${project_root}/bin" } ], "var": { diff --git a/include/eepp/graphics/pixeldensity.hpp b/include/eepp/graphics/pixeldensity.hpp index 3b5c7d83d..049c35152 100644 --- a/include/eepp/graphics/pixeldensity.hpp +++ b/include/eepp/graphics/pixeldensity.hpp @@ -34,6 +34,9 @@ class EE_API PixelDensity { static const Float& getPixelDensity(); + /** Positive EEPP_PIXEL_DENSITY value, or zero when unset or invalid. */ + static Float getEnvironmentPixelDensity(); + static void setPixelDensity( const Float& pixelDensity ); static void setPixelDensity( const PixelDensitySize& pixelDensity ); diff --git a/include/eepp/ui/charts/uichart.hpp b/include/eepp/ui/charts/uichart.hpp index 877ce561d..af35894ec 100644 --- a/include/eepp/ui/charts/uichart.hpp +++ b/include/eepp/ui/charts/uichart.hpp @@ -15,6 +15,8 @@ class Font; namespace EE { namespace UI { namespace Charts { +using namespace EE::Graphics; + struct PointTooltipContext { size_t index{ 0 }; double x{ 0.0 }; @@ -43,6 +45,35 @@ using PointTooltipProvider = enum class LineJoin : Uint8 { Miter, Bevel }; enum class LineCap : Uint8 { Butt, Square }; +/** Chart measurements are in device-independent pixels. Colors left unset follow the UI theme. */ +struct ChartStyle { + Font* font{ nullptr }; //!< Null uses the current UI theme font. + Float fontSize{ 12.f }; + Float tickLength{ 4.f }; + Float tickLabelGap{ 6.f }; + Float axisLabelGap{ 25.f }; + Float axisLabelInset{ 2.f }; + Float leftMargin{ 50.f }; + Float rightMargin{ 14.f }; + Float topMargin{ 12.f }; + Float topMarginWithLabel{ 26.f }; + Float bottomMargin{ 34.f }; + Float bottomMarginWithLabel{ 52.f }; + Float minimumAxisMargin{ 42.f }; + Float axisLabelPadding{ 12.f }; + Float rightAxisLabelPadding{ 16.f }; + Float xTickSpacing{ 80.f }; + Float yTickSpacing{ 50.f }; + Float hoverDistance{ 10.f }; + Float axisWidth{ 1.f }; + std::optional axisColor; + std::optional tickColor; + std::optional tickLabelColor; + std::optional axisLabelColor; + std::optional hoverColor; + std::optional seriesColor; +}; + class EE_API LineSeries { public: const String& name() const { return mName; } @@ -63,9 +94,12 @@ class EE_API LineSeries { Color color() const { return mColor; } - void setWidth( float width ); + bool hasCustomColor() const { return mColorExplicit; } - float width() const { return mWidth; } + /** Stroke width in device-independent pixels. */ + void setWidth( Float width ); + + Float width() const { return mWidth; } void setJoin( LineJoin join ); @@ -75,9 +109,9 @@ class EE_API LineSeries { LineCap cap() const { return mCap; } - void setMiterLimit( float limit ); + void setMiterLimit( Float limit ); - float miterLimit() const { return mMiterLimit; } + Float miterLimit() const { return mMiterLimit; } Uint64 geometryRevision() const { return mGeometryRevision; } @@ -110,8 +144,9 @@ class EE_API LineSeries { ChartAxis* mXAxis; ChartAxis* mYAxis; Color mColor{ 48, 130, 220 }; - float mWidth{ 2.f }; - float mMiterLimit{ 4.f }; + bool mColorExplicit{ false }; + Float mWidth{ 2.f }; + Float mMiterLimit{ 4.f }; LineJoin mJoin{ LineJoin::Miter }; LineCap mCap{ LineCap::Butt }; bool mVisible{ true }; @@ -131,9 +166,9 @@ class EE_API ChartModel { ChartAxis* yAxis() const { return mAxes[1].get(); } - const std::vector>& axes() const { return mAxes; } + const SmallVector, 4>& axes() const { return mAxes; } - const std::vector>& series() const { return mSeries; } + const SmallVector, 2>& series() const { return mSeries; } Uint64 revision() const { return mRevision.get(); } @@ -141,8 +176,8 @@ class EE_API ChartModel { friend class UIChart; void changed() { mRevision.set( mRevision.get() + 1 ); } - std::vector> mAxes; - std::vector> mSeries; + SmallVector, 4> mAxes; + SmallVector, 2> mSeries; ObservableValue mRevision{ 0 }; }; @@ -171,6 +206,10 @@ class EE_API UIChart : public UIWidget { /** Follow the newest X while preserving a fixed data-space span; zero disables follow mode. */ void setFollowX( double span ); + void setChartStyle( ChartStyle style ); + + const ChartStyle& chartStyle() const { return mChartStyle; } + void draw() override; protected: @@ -212,18 +251,18 @@ class EE_API UIChart : public UIWidget { struct RightAxis { ChartAxis* axis{ nullptr }; SmallVector ticks; - std::vector labels; - float width{ 0.f }; + SmallVector labels; + Float width{ 0.f }; }; Rectf plot; SmallVector xTicks; SmallVector yTicks; - std::vector xLabels; - std::vector yLabels; - std::vector xLabelWidths; - std::vector yLabelWidths; - float xAxisLabelWidth{ 0.f }; + SmallVector xLabels; + SmallVector yLabels; + SmallVector xLabelWidths; + SmallVector yLabelWidths; + Float xAxisLabelWidth{ 0.f }; std::vector rightAxes; Uint64 revision{ 0 }; }; @@ -244,14 +283,16 @@ class EE_API UIChart : public UIWidget { void hideHoverTooltip(); + void updateThemeColors(); + AxisViewport& viewport( const ChartAxis* axis ); const AxisViewport& viewport( const ChartAxis* axis ) const; std::shared_ptr mModel; ObservableValue::Connection mModelConnection; - std::vector mAxes; - std::vector mCaches; + SmallVector mAxes; + SmallVector mCaches; Layout mLayout; Uint64 mViewRevision{ 1 }; Uint64 mLayoutRevision{ 1 }; @@ -267,7 +308,15 @@ class EE_API UIChart : public UIWidget { Uint64 mAutoRevision{ std::numeric_limits::max() }; String mLastXAxisLabel; String mLastYAxisLabel; - Graphics::Font* mLastFont{ nullptr }; + Font* mLastFont{ nullptr }; + ChartStyle mChartStyle; + Color mAxisColor{ 125, 130, 138 }; + Color mTickColor{ 125, 130, 138 }; + Color mTickLabelColor{ 190, 194, 201 }; + Color mAxisLabelColor{ 210, 214, 220 }; + Color mHoverColor{ 160, 165, 173, 140 }; + Color mSeriesColor{ 48, 130, 220 }; + Float mLastDensity{ 0.f }; }; }}} // namespace EE::UI::Charts diff --git a/include/eepp/ui/uiapplication.hpp b/include/eepp/ui/uiapplication.hpp index d0a607f7f..370e236b3 100644 --- a/include/eepp/ui/uiapplication.hpp +++ b/include/eepp/ui/uiapplication.hpp @@ -43,7 +43,7 @@ class EE_API UIApplication { //! By default it will use the current process path as the base path. This will set the //! default working directory. std::optional basePath; - //! Not setting anything will automatically try to detect the main screen pixel density + //! If unset, uses EEPP_PIXEL_DENSITY when valid, otherwise the main display density. std::optional pixelDensity; //! Must be set to true in order to initialize the basic UI resources (font and UI theme). //! Otherwise it will initialize with an empty UI scene node diff --git a/include/eepp/ui/uistyle.hpp b/include/eepp/ui/uistyle.hpp index 2476c0aa9..a6a73723a 100644 --- a/include/eepp/ui/uistyle.hpp +++ b/include/eepp/ui/uistyle.hpp @@ -63,6 +63,9 @@ class EE_API UIStyle : public UIState { CSS::StyleSheetVariable getVariable( const std::string& variable ); + /** Resolve a color variable without copying its stored text. */ + Color getColorVariable( const std::string& variable, Color fallback ); + bool getForceReapplyProperties() const; void setForceReapplyProperties( bool forceReapplyProperties ); diff --git a/include/eepp/ui/uiwidget.hpp b/include/eepp/ui/uiwidget.hpp index 671586ad4..8584b466e 100644 --- a/include/eepp/ui/uiwidget.hpp +++ b/include/eepp/ui/uiwidget.hpp @@ -1063,6 +1063,9 @@ class EE_API UIWidget : public UINode { */ UIStyle* getUIStyle() const; + /** Resolve a CSS color variable, or return fallback when it is absent. */ + Color themeColor( const std::string& variable, Color fallback ) const; + /** * @brief Reloads the widget's style. * diff --git a/include/eepp/window/window.hpp b/include/eepp/window/window.hpp index 5816b386d..6afd62be6 100644 --- a/include/eepp/window/window.hpp +++ b/include/eepp/window/window.hpp @@ -77,7 +77,7 @@ class WindowSettings { inline WindowSettings( Uint32 width, Uint32 height, const std::string& title = std::string(), Uint32 style = WindowStyle::Default, WindowBackend backend = WindowBackend::Default, Uint32 bpp = 32, - const std::string& icon = std::string(), const Float& pixelDensity = 1, + const std::string& icon = std::string(), const Float& pixelDensity = 0, const bool& useScreenKeyboard = EE_SCREEN_KEYBOARD_ENABLED, bool disableHiDPI = false ) : Style( style ), @@ -97,7 +97,7 @@ class WindowSettings { Height( 600 ), BitsPerPixel( 32 ), Backend( WindowBackend::Default ), - PixelDensity( 1 ), + PixelDensity( 0 ), UseScreenKeyboard( EE_SCREEN_KEYBOARD_ENABLED ), DisableHiDPI( false ) {} @@ -108,7 +108,7 @@ class WindowSettings { std::string Icon; std::string Title; WindowBackend Backend{ WindowBackend::Default }; - Float PixelDensity{ 1 }; + Float PixelDensity{ 0 }; //!< Zero leaves UI density to EEPP_PIXEL_DENSITY or display detection. bool UseScreenKeyboard{ EE_SCREEN_KEYBOARD_ENABLED }; bool DisableHiDPI{ false }; }; diff --git a/src/eepp/graphics/pixeldensity.cpp b/src/eepp/graphics/pixeldensity.cpp index c3872925b..df12a8660 100644 --- a/src/eepp/graphics/pixeldensity.cpp +++ b/src/eepp/graphics/pixeldensity.cpp @@ -1,3 +1,6 @@ +#include +#include +#include #include #include @@ -65,6 +68,18 @@ const Float& PixelDensity::getPixelDensity() { return sPixelDensity; } +Float PixelDensity::getEnvironmentPixelDensity() { + const char* value = std::getenv( "EEPP_PIXEL_DENSITY" ); + if ( !value || !*value ) + return 0.f; + char* end = nullptr; + errno = 0; + const Float density = std::strtof( value, &end ); + return errno == 0 && end != value && *end == '\0' && std::isfinite( density ) && density > 0.f + ? density + : 0.f; +} + void PixelDensity::setPixelDensity( const Float& pixelDensity ) { sPixelDensity = pixelDensity; } diff --git a/src/eepp/ui/uiapplication.cpp b/src/eepp/ui/uiapplication.cpp index 8d84a2e2f..900ce95f3 100644 --- a/src/eepp/ui/uiapplication.cpp +++ b/src/eepp/ui/uiapplication.cpp @@ -80,8 +80,13 @@ UIApplication::UIApplication( const WindowSettings& windowSettings, const Settin mDidRun = true; + const Float environmentDensity = PixelDensity::getEnvironmentPixelDensity(); if ( appSettings.pixelDensity && *appSettings.pixelDensity > 0 ) { PixelDensity::setPixelDensity( *appSettings.pixelDensity ); + } else if ( windowSettings.PixelDensity > 0 ) { + PixelDensity::setPixelDensity( windowSettings.PixelDensity ); + } else if ( environmentDensity > 0 ) { + PixelDensity::setPixelDensity( environmentDensity ); } else if ( offscreen ) { PixelDensity::setPixelDensity( 1.f ); } else { diff --git a/src/eepp/ui/uichart.cpp b/src/eepp/ui/uichart.cpp index 2d38201ac..06ee15210 100644 --- a/src/eepp/ui/uichart.cpp +++ b/src/eepp/ui/uichart.cpp @@ -1,11 +1,13 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -51,8 +53,9 @@ std::optional axisExtent( const ChartModel& model, const ChartAxis* a return extent; } -float chartTextWidth( Font* font, const String& label ) { - return font ? Text::getTextWidth( font, 12, label, 0, 4, 0.f, label.getTextHints() ) : 0.f; +Float chartTextWidth( Font* font, const String& label, Uint32 fontSize ) { + return font ? Text::getTextWidth( font, fontSize, label, 0, 4, 0.f, label.getTextHints() ) + : 0.f; } } // namespace @@ -88,14 +91,15 @@ void LineSeries::setVisible( bool visible ) { } void LineSeries::setColor( Color color ) { - if ( mColor != color ) { + if ( mColor != color || !mColorExplicit ) { mColor = color; + mColorExplicit = true; ++mGeometryRevision; changed(); } } -void LineSeries::setWidth( float width ) { +void LineSeries::setWidth( Float width ) { width = std::max( 0.f, width ); if ( mWidth != width ) { mWidth = width; @@ -120,7 +124,7 @@ void LineSeries::setCap( LineCap cap ) { } } -void LineSeries::setMiterLimit( float limit ) { +void LineSeries::setMiterLimit( Float limit ) { limit = std::isfinite( limit ) ? std::max( 1.f, limit ) : 4.f; if ( mMiterLimit != limit ) { mMiterLimit = limit; @@ -302,6 +306,35 @@ void UIChart::setFollowX( double span ) { invalidateDraw(); } +void UIChart::setChartStyle( ChartStyle style ) { + mChartStyle = std::move( style ); + ++mLayoutRevision; + ++mViewRevision; + invalidateDraw(); +} + +void UIChart::updateThemeColors() { + mAxisColor = mChartStyle.axisColor ? *mChartStyle.axisColor + : themeColor( "--tab-line", { 125, 130, 138 } ); + mTickColor = mChartStyle.tickColor.value_or( mAxisColor ); + mTickLabelColor = mChartStyle.tickLabelColor ? *mChartStyle.tickLabelColor + : themeColor( "--font-hint", { 190, 194, 201 } ); + mAxisLabelColor = mChartStyle.axisLabelColor ? *mChartStyle.axisLabelColor + : themeColor( "--font", { 210, 214, 220 } ); + if ( mChartStyle.hoverColor ) { + mHoverColor = *mChartStyle.hoverColor; + } else { + mHoverColor = themeColor( "--primary", { 160, 165, 173 } ); + mHoverColor.a = 140; + } + const Color series = mChartStyle.seriesColor ? *mChartStyle.seriesColor + : themeColor( "--primary", { 48, 130, 220 } ); + if ( mSeriesColor != series ) { + mSeriesColor = series; + ++mViewRevision; + } +} + void UIChart::updateFollow() { if ( mFollowSpan <= 0 ) return; @@ -382,61 +415,75 @@ void UIChart::onSizeChange() { void UIChart::updateLayout() { if ( mLayout.revision == mLayoutRevision ) return; - const float width = getPixelsSize().getWidth(); - const float height = getPixelsSize().getHeight(); - float leftMargin = 50.f; - const float bottomMargin = xAxis()->label().empty() ? 34.f : 52.f; - const float topMargin = yAxis()->label().empty() ? 12.f : 26.f; - Font* font = - getUISceneNode() ? getUISceneNode()->getUIThemeManager()->getDefaultFont() : nullptr; - mLayout.xAxisLabelWidth = chartTextWidth( font, xAxis()->label() ); - float rightMargin = 14.f; + const auto dp = []( Float value ) { return PixelDensity::dpToPx( value ); }; + const Uint32 fontSize = + std::max( 1u, static_cast( std::round( dp( mChartStyle.fontSize ) ) ) ); + const Float width = getPixelsSize().getWidth(); + const Float height = getPixelsSize().getHeight(); + Float leftMargin = dp( mChartStyle.leftMargin ); + const Float bottomMargin = dp( xAxis()->label().empty() ? mChartStyle.bottomMargin + : mChartStyle.bottomMarginWithLabel ); + const Float topMargin = + dp( yAxis()->label().empty() ? mChartStyle.topMargin : mChartStyle.topMarginWithLabel ); + Font* font = mChartStyle.font ? mChartStyle.font + : getUISceneNode() ? getUISceneNode()->getUIThemeManager()->getDefaultFont() + : nullptr; + mLayout.xAxisLabelWidth = chartTextWidth( font, xAxis()->label(), fontSize ); + Float rightMargin = dp( mChartStyle.rightMargin ); mLayout.rightAxes.clear(); for ( const auto& view : mAxes ) { if ( view.axis->position() != AxisPosition::Right ) continue; Layout::RightAxis axisLayout; axisLayout.axis = view.axis; - axisLayout.ticks = linearTicks( view.viewport.range(), - std::max( 1.f, height - topMargin - bottomMargin ), 50.f ); + axisLayout.ticks = + linearTicks( view.viewport.range(), std::max( 1.f, height - topMargin - bottomMargin ), + dp( mChartStyle.yTickSpacing ) ); const double step = axisLayout.ticks.size() > 1 ? axisLayout.ticks[1] - axisLayout.ticks[0] : 1.0; - float labelWidth = 0.f; + Float labelWidth = 0.f; for ( double tick : axisLayout.ticks ) { axisLayout.labels.emplace_back( axisLayout.axis->formatTick( tick, step ) ); - labelWidth = std::max( labelWidth, chartTextWidth( font, axisLayout.labels.back() ) ); + labelWidth = + std::max( labelWidth, chartTextWidth( font, axisLayout.labels.back(), fontSize ) ); } - axisLayout.width = std::max( 42.f, labelWidth + 16.f ); + axisLayout.width = std::max( dp( mChartStyle.minimumAxisMargin ), + labelWidth + dp( mChartStyle.rightAxisLabelPadding ) ); rightMargin += axisLayout.width; mLayout.rightAxes.emplace_back( std::move( axisLayout ) ); } for ( int pass = 0; pass < 2; ++pass ) { - const float plotWidth = std::max( 1.f, width - leftMargin - rightMargin ); - const float plotHeight = std::max( 1.f, height - topMargin - bottomMargin ); + const Float plotWidth = std::max( 1.f, width - leftMargin - rightMargin ); + const Float plotHeight = std::max( 1.f, height - topMargin - bottomMargin ); mLayout.plot = Rectf( leftMargin, topMargin, leftMargin + plotWidth, topMargin + plotHeight ); - mLayout.xTicks = linearTicks( viewport( xAxis() ).range(), plotWidth ); - mLayout.yTicks = linearTicks( viewport( yAxis() ).range(), plotHeight, 50.f ); + mLayout.xTicks = + linearTicks( viewport( xAxis() ).range(), plotWidth, dp( mChartStyle.xTickSpacing ) ); + mLayout.yTicks = + linearTicks( viewport( yAxis() ).range(), plotHeight, dp( mChartStyle.yTickSpacing ) ); mLayout.xLabels.clear(); mLayout.yLabels.clear(); mLayout.xLabelWidths.clear(); mLayout.yLabelWidths.clear(); - float maximumLabelWidth = 0.f; + Float maximumLabelWidth = 0.f; const double xStep = mLayout.xTicks.size() > 1 ? mLayout.xTicks[1] - mLayout.xTicks[0] : 1.0; const double yStep = mLayout.yTicks.size() > 1 ? mLayout.yTicks[1] - mLayout.yTicks[0] : 1.0; for ( double tick : mLayout.xTicks ) { mLayout.xLabels.emplace_back( xAxis()->formatTick( tick, xStep ) ); - mLayout.xLabelWidths.emplace_back( chartTextWidth( font, mLayout.xLabels.back() ) ); + mLayout.xLabelWidths.emplace_back( + chartTextWidth( font, mLayout.xLabels.back(), fontSize ) ); } for ( double tick : mLayout.yTicks ) { mLayout.yLabels.emplace_back( yAxis()->formatTick( tick, yStep ) ); - const float labelWidth = chartTextWidth( font, mLayout.yLabels.back() ); + const Float labelWidth = chartTextWidth( font, mLayout.yLabels.back(), fontSize ); mLayout.yLabelWidths.emplace_back( labelWidth ); maximumLabelWidth = std::max( maximumLabelWidth, labelWidth ); } - const float measuredMargin = std::max( 42.f, maximumLabelWidth + 12.f ); + const Float measuredMargin = + std::max( dp( mChartStyle.minimumAxisMargin ), + maximumLabelWidth + dp( mChartStyle.axisLabelPadding ) ); if ( pass == 0 && std::abs( measuredMargin - leftMargin ) > 2.f ) leftMargin = measuredMargin; else @@ -478,16 +525,17 @@ void UIChart::updateGeometry( SeriesCache& cache, const XYDataRead& read ) { chunk.xs, chunk.ys ); } } - const float halfWidth = cache.series->width() * 0.5f; + const Float halfWidth = PixelDensity::dpToPx( cache.series->width() ) * 0.5f; bool previousValid = false; bool priorSegment = false; Vector2f previous; Vector2f priorDirection; Vector2f priorNormal; + const Color seriesColor = cache.series->hasCustomColor() ? cache.series->color() : mSeriesColor; auto addTriangle = [&]( Vector2f a, Vector2f b, Vector2f c ) { for ( const auto& vertex : { a, b, c } ) { cache.geometry->addVertex( vertex ); - cache.geometry->addColor( cache.series->color() ); + cache.geometry->addColor( seriesColor ); } }; auto addSquareCap = [&]( Vector2f center, Vector2f direction, Vector2f normal, bool start ) { @@ -506,14 +554,14 @@ void UIChart::updateGeometry( SeriesCache& cache, const XYDataRead& read ) { continue; } const Vector2f current( - static_cast( xView.toPixel( point.x, mLayout.plot.Left, mLayout.plot.getWidth(), + static_cast( xView.toPixel( point.x, mLayout.plot.Left, mLayout.plot.getWidth(), false, *cache.series->xAxis() ) ), - static_cast( yView.toPixel( point.y, mLayout.plot.Top, mLayout.plot.getHeight(), + static_cast( yView.toPixel( point.y, mLayout.plot.Top, mLayout.plot.getHeight(), true, *cache.series->yAxis() ) ) ); if ( previousValid ) { - const float dx = current.x - previous.x; - const float dy = current.y - previous.y; - const float length = std::hypot( dx, dy ); + const Float dx = current.x - previous.x; + const Float dy = current.y - previous.y; + const Float length = std::hypot( dx, dy ); if ( length > 0.001f ) { const Vector2f direction( dx / length, dy / length ); const Vector2f normal( -dy / length * halfWidth, dx / length * halfWidth ); @@ -522,15 +570,17 @@ void UIChart::updateGeometry( SeriesCache& cache, const XYDataRead& read ) { addTriangle( previous + normal, previous - normal, current + normal ); addTriangle( current + normal, previous - normal, current - normal ); if ( priorSegment ) { - const float cross = + const Float cross = priorDirection.x * direction.y - priorDirection.y * direction.x; if ( std::abs( cross ) > 0.0001f ) { - const float side = cross > 0 ? -1.f : 1.f; + const Float side = cross > 0 ? -1.f : 1.f; const Vector2f outerA = priorNormal * side; const Vector2f outerB = normal * side; + // The two segment quads leave this wedge empty at every turn. + addTriangle( previous + outerA, previous, previous + outerB ); const Vector2f sum = outerA + outerB; - const float sumLength = std::hypot( sum.x, sum.y ); - const float divisor = sumLength > 0 + const Float sumLength = std::hypot( sum.x, sum.y ); + const Float divisor = sumLength > 0 ? ( sum.x * outerB.x + sum.y * outerB.y ) / ( sumLength * halfWidth ) : 0.f; @@ -539,8 +589,6 @@ void UIChart::updateGeometry( SeriesCache& cache, const XYDataRead& read ) { const Vector2f miter = sum * ( 1.f / sumLength ) * ( halfWidth / divisor ); addTriangle( previous + outerA, previous + miter, previous + outerB ); - } else { - addTriangle( previous + outerA, previous, previous + outerB ); } } } @@ -557,89 +605,109 @@ void UIChart::updateGeometry( SeriesCache& cache, const XYDataRead& read ) { } void UIChart::drawAxes() { + const auto dp = []( Float value ) { return PixelDensity::dpToPx( value ); }; + const Float tickLength = dp( mChartStyle.tickLength ); + const Float tickLabelGap = dp( mChartStyle.tickLabelGap ); Primitives primitive; - primitive.setColor( Color( 125, 130, 138 ) ); + primitive.setLineWidth( dp( mChartStyle.axisWidth ) ); + primitive.setColor( mAxisColor ); const auto& plot = mLayout.plot; const Vector2f origin( mScreenPos.x, mScreenPos.y ); primitive.drawLine( Line2f( { origin.x + plot.Left, origin.y + plot.Bottom }, { origin.x + plot.Right, origin.y + plot.Bottom } ) ); primitive.drawLine( Line2f( { origin.x + plot.Left, origin.y + plot.Top }, { origin.x + plot.Left, origin.y + plot.Bottom } ) ); - Font* font = - getUISceneNode() ? getUISceneNode()->getUIThemeManager()->getDefaultFont() : nullptr; + Font* font = mChartStyle.font ? mChartStyle.font + : getUISceneNode() ? getUISceneNode()->getUIThemeManager()->getDefaultFont() + : nullptr; FontStyleConfig textStyle; textStyle.Font = font; - textStyle.CharacterSize = 12; - textStyle.FontColor = Color( 190, 194, 201 ); + textStyle.CharacterSize = + std::max( 1u, static_cast( std::round( dp( mChartStyle.fontSize ) ) ) ); + textStyle.FontColor = mTickLabelColor; + auto drawText = [&]( const String& label, Vector2f position ) { + Text::draw( label, { std::round( position.x ), std::round( position.y ) }, textStyle, 4, + label.getTextHints() ); + }; + primitive.setColor( mTickColor ); for ( size_t i = 0; i < mLayout.xTicks.size(); ++i ) { - const float x = static_cast( viewport( xAxis() ).toPixel( + const Float x = static_cast( viewport( xAxis() ).toPixel( mLayout.xTicks[i], plot.Left, plot.getWidth(), false, *xAxis() ) ); primitive.drawLine( Line2f( { origin.x + x, origin.y + plot.Bottom }, - { origin.x + x, origin.y + plot.Bottom + 4 } ) ); + { origin.x + x, origin.y + plot.Bottom + tickLength } ) ); if ( font ) { - const float textWidth = mLayout.xLabelWidths[i]; + const Float textWidth = mLayout.xLabelWidths[i]; const String& label = mLayout.xLabels[i]; - Text::draw( label, { origin.x + x - textWidth * 0.5f, origin.y + plot.Bottom + 6 }, - textStyle, 4, label.getTextHints() ); + drawText( label, + { origin.x + x - textWidth * 0.5f, origin.y + plot.Bottom + tickLabelGap } ); } } for ( size_t i = 0; i < mLayout.yTicks.size(); ++i ) { - const float y = static_cast( viewport( yAxis() ).toPixel( + const Float y = static_cast( viewport( yAxis() ).toPixel( mLayout.yTicks[i], plot.Top, plot.getHeight(), true, *yAxis() ) ); - primitive.drawLine( Line2f( { origin.x + plot.Left - 4, origin.y + y }, + primitive.drawLine( Line2f( { origin.x + plot.Left - tickLength, origin.y + y }, { origin.x + plot.Left, origin.y + y } ) ); if ( font ) { - const float textWidth = mLayout.yLabelWidths[i]; + const Float textWidth = mLayout.yLabelWidths[i]; const String& label = mLayout.yLabels[i]; - Text::draw( label, { origin.x + plot.Left - 8 - textWidth, origin.y + y - 7 }, - textStyle, 4, label.getTextHints() ); + drawText( label, { origin.x + plot.Left - tickLength - tickLabelGap - textWidth, + origin.y + y - textStyle.CharacterSize * 0.6f } ); } } - float rightOffset = 0.f; + Float rightOffset = 0.f; for ( const auto& axisLayout : mLayout.rightAxes ) { - const float axisX = plot.Right + rightOffset; + const Float axisX = plot.Right + rightOffset; + primitive.setColor( mAxisColor ); primitive.drawLine( Line2f( { origin.x + axisX, origin.y + plot.Top }, { origin.x + axisX, origin.y + plot.Bottom } ) ); + primitive.setColor( mTickColor ); for ( size_t i = 0; i < axisLayout.ticks.size(); ++i ) { - const float y = - static_cast( viewport( axisLayout.axis ) + const Float y = + static_cast( viewport( axisLayout.axis ) .toPixel( axisLayout.ticks[i], plot.Top, plot.getHeight(), true, *axisLayout.axis ) ); primitive.drawLine( Line2f( { origin.x + axisX, origin.y + y }, - { origin.x + axisX + 4, origin.y + y } ) ); + { origin.x + axisX + tickLength, origin.y + y } ) ); if ( font ) { const String& label = axisLayout.labels[i]; - Text::draw( label, { origin.x + axisX + 7, origin.y + y - 7 }, textStyle, 4, - label.getTextHints() ); + drawText( label, { origin.x + axisX + tickLength + tickLabelGap, + origin.y + y - textStyle.CharacterSize * 0.6f } ); } } rightOffset += axisLayout.width; } if ( font && !xAxis()->label().empty() ) { - textStyle.FontColor = Color( 210, 214, 220 ); + textStyle.FontColor = mAxisLabelColor; const String& label = xAxis()->label(); - Text::draw( label, - { origin.x + plot.Left + ( plot.getWidth() - mLayout.xAxisLabelWidth ) * 0.5f, - origin.y + plot.Bottom + 25 }, - textStyle, 4, label.getTextHints() ); + drawText( label, + { origin.x + plot.Left + ( plot.getWidth() - mLayout.xAxisLabelWidth ) * 0.5f, + origin.y + plot.Bottom + dp( mChartStyle.axisLabelGap ) } ); } if ( font && !yAxis()->label().empty() ) { - textStyle.FontColor = Color( 210, 214, 220 ); + textStyle.FontColor = mAxisLabelColor; const String& label = yAxis()->label(); - Text::draw( label, { origin.x + plot.Left, origin.y + 2 }, textStyle, 4, - label.getTextHints() ); + drawText( label, { origin.x + plot.Left, origin.y + dp( mChartStyle.axisLabelInset ) } ); } } void UIChart::draw() { UIWidget::draw(); - if ( !mVisible || getPixelsSize().getWidth() < 20 || getPixelsSize().getHeight() < 20 ) + if ( !mVisible || getPixelsSize().getWidth() < PixelDensity::dpToPx( 20.f ) || + getPixelsSize().getHeight() < PixelDensity::dpToPx( 20.f ) ) return; + const Float density = PixelDensity::getPixelDensity(); + if ( mLastDensity != density ) { + mLastDensity = density; + ++mLayoutRevision; + ++mViewRevision; + } + updateThemeColors(); synchronize(); updateFollow(); updateAutoRanges(); - Font* font = - getUISceneNode() ? getUISceneNode()->getUIThemeManager()->getDefaultFont() : nullptr; + Font* font = mChartStyle.font ? mChartStyle.font + : getUISceneNode() ? getUISceneNode()->getUIThemeManager()->getDefaultFont() + : nullptr; if ( mLastXAxisLabel != xAxis()->label() || mLastYAxisLabel != yAxis()->label() || mLastFont != font ) { mLastXAxisLabel = xAxis()->label(); @@ -696,7 +764,8 @@ void UIChart::draw() { } if ( mHover ) { Primitives primitive; - primitive.setColor( Color( 160, 165, 173, 140 ) ); + primitive.setColor( mHoverColor ); + primitive.setLineWidth( PixelDensity::dpToPx( mChartStyle.axisWidth ) ); primitive.drawLine( Line2f( { mLastMouse.x, mScreenPos.y + plot.Top }, { mLastMouse.x, mScreenPos.y + plot.Bottom } ) ); } @@ -713,7 +782,7 @@ void UIChart::updateHover( Vector2f screenPosition ) { return; } mHover = true; - float bestDistance = 10.f; + Float bestDistance = PixelDensity::dpToPx( mChartStyle.hoverDistance ); LineSeries* bestSeries = nullptr; ChartPoint bestPoint; size_t bestIndex = 0; @@ -734,23 +803,23 @@ void UIChart::updateHover( Vector2f screenPosition ) { previous = nullptr; continue; } - const Vector2f pixel( static_cast( xView.toPixel( + const Vector2f pixel( static_cast( xView.toPixel( point.x, mLayout.plot.Left, mLayout.plot.getWidth(), false, *series->xAxis() ) ), - static_cast( yView.toPixel( + static_cast( yView.toPixel( point.y, mLayout.plot.Top, mLayout.plot.getHeight(), true, *series->yAxis() ) ) ); if ( previous ) { const Vector2f segment = pixel - previousPixel; - const float lengthSquared = segment.x * segment.x + segment.y * segment.y; - const float t = + const Float lengthSquared = segment.x * segment.x + segment.y * segment.y; + const Float t = lengthSquared > 0.0001f ? std::clamp( ( ( local.x - previousPixel.x ) * segment.x + ( local.y - previousPixel.y ) * segment.y ) / lengthSquared, 0.f, 1.f ) : 0.f; - const float distance = std::hypot( previousPixel.x + t * segment.x - local.x, + const Float distance = std::hypot( previousPixel.x + t * segment.x - local.x, previousPixel.y + t * segment.y - local.y ); if ( distance < bestDistance ) { const size_t candidate = @@ -780,15 +849,15 @@ void UIChart::updateHover( Vector2f screenPosition ) { const ChartPoint point = pointAt( read, candidate ); if ( !std::isfinite( point.x ) || !std::isfinite( point.y ) ) continue; - const float px = static_cast( viewport( series->xAxis() ) + const Float px = static_cast( viewport( series->xAxis() ) .toPixel( point.x, mLayout.plot.Left, mLayout.plot.getWidth(), false, *series->xAxis() ) ); - const float py = static_cast( viewport( series->yAxis() ) + const Float py = static_cast( viewport( series->yAxis() ) .toPixel( point.y, mLayout.plot.Top, mLayout.plot.getHeight(), true, *series->yAxis() ) ); - const float distance = std::hypot( px - local.x, py - local.y ); + const Float distance = std::hypot( px - local.x, py - local.y ); if ( distance < bestDistance ) { bestDistance = distance; bestSeries = series; @@ -882,8 +951,8 @@ Uint32 UIChart::onMouseMove( const Vector2i& position, const Uint32& flags ) { if ( mPanning && ( flags & EE_BUTTON_LMASK ) && mLayout.plot.getWidth() > 0 && mLayout.plot.getHeight() > 0 ) { mFollowSpan = 0.0; - const float dx = mouse.x - mLastMouse.x; - const float dy = mouse.y - mLastMouse.y; + const Float dx = mouse.x - mLastMouse.x; + const Float dy = mouse.y - mLastMouse.y; for ( auto& view : mAxes ) { const bool horizontal = view.axis->position() == AxisPosition::Bottom || view.axis->position() == AxisPosition::Top; diff --git a/src/eepp/ui/uistyle.cpp b/src/eepp/ui/uistyle.cpp index 1b3a31bee..9d42241fd 100644 --- a/src/eepp/ui/uistyle.cpp +++ b/src/eepp/ui/uistyle.cpp @@ -190,6 +190,11 @@ StyleSheetVariable UIStyle::getVariable( const std::string& variable ) { return resolved ? *resolved : StyleSheetVariable(); } +Color UIStyle::getColorVariable( const std::string& variable, Color fallback ) { + const StyleSheetVariable* resolved = getVariableRef( variable ); + return resolved ? Color::fromString( resolved->getValue() ) : fallback; +} + const StyleSheetVariable* UIStyle::getVariableRef( const std::string& variable ) { if ( NULL != mWidget && NULL != mWidget->getUISceneNode() ) resetGlobalDefinition(); diff --git a/src/eepp/ui/uiwidget.cpp b/src/eepp/ui/uiwidget.cpp index 54db7b793..75d82a696 100644 --- a/src/eepp/ui/uiwidget.cpp +++ b/src/eepp/ui/uiwidget.cpp @@ -1460,6 +1460,10 @@ UIStyle* UIWidget::getUIStyle() const { return mStyle; } +Color UIWidget::themeColor( const std::string& variable, Color fallback ) const { + return mStyle ? mStyle->getColorVariable( variable, fallback ) : fallback; +} + void UIWidget::reloadStyle( bool reloadChildren, bool disableAnimations, bool reportStateChange, bool forceReApplyProperties, bool resetPropertyCache ) { createStyle(); diff --git a/src/eepp/window/backend/SDL3/windowsdl3.cpp b/src/eepp/window/backend/SDL3/windowsdl3.cpp index f0335779c..f8f85e3ce 100644 --- a/src/eepp/window/backend/SDL3/windowsdl3.cpp +++ b/src/eepp/window/backend/SDL3/windowsdl3.cpp @@ -134,8 +134,10 @@ bool WindowSDL::create( WindowSettings Settings, ContextSettings Context ) { #if EE_PLATFORM != EE_PLATFORM_MACOS && EE_PLATFORM != EE_PLATFORM_IOS && \ EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN - mWindow.WindowConfig.Width *= mWindow.WindowConfig.PixelDensity; - mWindow.WindowConfig.Height *= mWindow.WindowConfig.PixelDensity; + if ( mWindow.WindowConfig.PixelDensity > 0 ) { + mWindow.WindowConfig.Width *= mWindow.WindowConfig.PixelDensity; + mWindow.WindowConfig.Height *= mWindow.WindowConfig.PixelDensity; + } #endif mSDLWindow = SDL_CreateWindow( mWindow.WindowConfig.Title.c_str(), mWindow.WindowConfig.Width, diff --git a/src/eepp/window/engine.cpp b/src/eepp/window/engine.cpp index a1eb1e5c1..d2a8d482d 100644 --- a/src/eepp/window/engine.cpp +++ b/src/eepp/window/engine.cpp @@ -336,8 +336,13 @@ EE::Window::Window* Engine::createWindow( WindowSettings Settings, ContextSettin mWindows.insert( { mWindow->getWindowID(), mWindow } ); - if ( firstWindow && Settings.PixelDensity > 0 ) - PixelDensity::setPixelDensity( Settings.PixelDensity ); + if ( firstWindow ) { + const Float density = Settings.PixelDensity > 0 + ? Settings.PixelDensity + : PixelDensity::getEnvironmentPixelDensity(); + if ( density > 0 ) + PixelDensity::setPixelDensity( density ); + } return window; } diff --git a/src/examples/ui_charts/ui_charts.cpp b/src/examples/ui_charts/ui_charts.cpp index 834fc4716..c323e6a39 100644 --- a/src/examples/ui_charts/ui_charts.cpp +++ b/src/examples/ui_charts/ui_charts.cpp @@ -81,7 +81,7 @@ EE_MAIN_FUNC int main( int, char** ) { []( double value, double ) { return String::format( "%.1f C", value ); } ); std::vector load; std::vector temperatureX; - std::vector temperatureY; + std::vector temperatureY; load.reserve( 120 ); temperatureX.reserve( 120 ); temperatureY.reserve( 120 ); @@ -90,7 +90,7 @@ EE_MAIN_FUNC int main( int, char** ) { load.push_back( { x, 50.0 + 18.0 * std::sin( x * 0.14 ) + 5.0 * std::sin( x * 0.9 ) } ); temperatureX.push_back( x ); temperatureY.push_back( - static_cast( 37.0 + 0.08 * x + 2.0 * std::sin( x * 0.12 ) ) ); + static_cast( 37.0 + 0.08 * x + 2.0 * std::sin( x * 0.12 ) ) ); } auto* loadSeries = dual->addLineSeries( "Load" ); loadSeries->setColor( Color( 129, 211, 142 ) ); @@ -124,6 +124,10 @@ EE_MAIN_FUNC int main( int, char** ) { dual->fit(); auto* dense = layout->find( "dense" ); + ChartStyle denseStyle = dense->chartStyle(); + denseStyle.hoverColor = Color( 182, 146, 255, 150 ); + denseStyle.tickLabelColor = Color( 202, 193, 227 ); + dense->setChartStyle( denseStyle ); dense->xAxis()->setLabel( "Sample" ); dense->yAxis()->setLabel( "Signal" ); std::vector samples; diff --git a/src/tests/unit_tests/charts_ui_tests.cpp b/src/tests/unit_tests/charts_ui_tests.cpp index 23f847079..a20dbb201 100644 --- a/src/tests/unit_tests/charts_ui_tests.cpp +++ b/src/tests/unit_tests/charts_ui_tests.cpp @@ -1,16 +1,22 @@ #include "utest.h" +#include +#include #include #include #include #include #include #include +#include #include using namespace EE; using namespace EE::Window; using namespace EE::UI; using namespace EE::UI::Charts; +using namespace EE::UI::CSS; +using namespace EE::Graphics; +using namespace EE::System; namespace { @@ -28,10 +34,9 @@ class ChartProbe : public UIChart { } // namespace UTEST( UIChart, buildsAndDrawsMultipleSeries ) { - UIApplication app( WindowSettings{ 480, 320, "Chart Test" }, - UIApplication::Settings( System::Sys::getProcessPath() + ".." + - System::FileSystem::getOSSlash(), - 1.f ) ); + UIApplication app( + WindowSettings{ 480, 320, "Chart Test" }, + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1.f ) ); auto* declarative = app.getUI()->loadLayoutFromString( "" ); ASSERT_TRUE( declarative != nullptr ); @@ -53,10 +58,12 @@ UTEST( UIChart, buildsAndDrawsMultipleSeries ) { second->setCap( LineCap::Square ); second->setJoin( LineJoin::Bevel ); auto* right = chart->model()->addAxis( AxisPosition::Right ); + EXPECT_TRUE( chart->model()->axes().is_small() ); second->setYAxis( right ); second->setPoints( std::vector{ { 0, 2 }, { 1, 1 }, { 2, 3 } } ); chart->fit(); EXPECT_EQ( chart->model()->series().size(), 2u ); + EXPECT_TRUE( chart->model()->series().is_small() ); auto* secondView = UIChart::New(); secondView->setModel( chart->sharedModel() ); secondView->setPixelsSize( 200, 140 ); @@ -78,10 +85,9 @@ UTEST( UIChart, buildsAndDrawsMultipleSeries ) { } UTEST( UIChart, hoverShowsUpdatesAndHidesTooltip ) { - UIApplication app( WindowSettings{ 480, 320, "Chart Hover Test" }, - UIApplication::Settings( System::Sys::getProcessPath() + ".." + - System::FileSystem::getOSSlash(), - 1.f ) ); + UIApplication app( + WindowSettings{ 480, 320, "Chart Hover Test" }, + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1.f ) ); auto* chart = eeNew( ChartProbe, () ); chart->setPixelsSize( 400, 240 ); chart->setParent( app.getUI()->getRoot() ); @@ -115,3 +121,60 @@ UTEST( UIChart, hoverShowsUpdatesAndHidesTooltip ) { EXPECT_TRUE( chart->getTooltipText().empty() ); EXPECT_TRUE( chart->getTooltip()->getText().empty() ); } + +UTEST( UIChart, styleUsesDeviceIndependentMeasurements ) { + const Float previousDensity = PixelDensity::getPixelDensity(); + UIApplication app( + WindowSettings{ 480, 320, "Chart Density Test" }, + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 2.f ) ); + auto* chart = UIChart::New(); + chart->setPixelsSize( 400, 240 ); + chart->setParent( app.getUI()->getRoot() ); + ChartStyle style = chart->chartStyle(); + style.fontSize = 15.f; + style.axisColor = Color( 12, 34, 56 ); + chart->setChartStyle( style ); + chart->getUIStyle()->setStyleSheetVariable( StyleSheetVariable( "--chart-test", "#123456" ) ); + EXPECT_TRUE( chart->themeColor( "--chart-test", Color::Black ) == Color( 18, 52, 86 ) ); + EXPECT_TRUE( chart->themeColor( "--chart-absent", Color::White ) == Color::White ); + chart->addLineSeries( "Dense" )->setPoints( std::vector{ { 0, 0 }, { 1, 1 } } ); + app.getUI()->flushDirtyStyleAndLayout(); + app.getUI()->draw(); + EXPECT_EQ( PixelDensity::getPixelDensity(), 2.f ); + EXPECT_EQ( chart->chartStyle().fontSize, 15.f ); + EXPECT_TRUE( chart->chartStyle().axisColor == Color( 12, 34, 56 ) ); + EXPECT_EQ( app.getWindow()->getWindowInfo()->WindowConfig.Width, 480u ); + PixelDensity::setPixelDensity( previousDensity ); +} + +UTEST( UIChart, miterJoinFillsInnerWedgeAtHighDensity ) { + const Float previousDensity = PixelDensity::getPixelDensity(); + UIApplication app( + WindowSettings{ 220, 220, "Chart Join Test" }, + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 3.f ) ); + auto* chart = UIChart::New(); + chart->setPixelsSize( 200, 200 ); + chart->setParent( app.getUI()->getRoot() ); + ChartStyle style; + style.leftMargin = style.rightMargin = style.topMargin = style.bottomMargin = 0.f; + style.minimumAxisMargin = style.axisLabelPadding = 0.f; + chart->setChartStyle( style ); + chart->xAxis()->setFormatter( []( double, double ) { return String(); } ); + chart->yAxis()->setFormatter( []( double, double ) { return String(); } ); + chart->setAxisRange( chart->xAxis(), { 0, 100 } ); + chart->setAxisRange( chart->yAxis(), { 0, 100 } ); + auto* line = chart->addLineSeries( "Corner" ); + line->setColor( Color( 0, 255, 0 ) ); + line->setWidth( 12.f ); + line->setPoints( std::vector{ { 20, 50 }, { 60, 50 }, { 60, 90 } } ); + app.getUI()->flushDirtyStyleAndLayout(); + app.getWindow()->setClearColor( Color( 20, 20, 20 ) ); + app.getWindow()->clear(); + app.getUI()->draw(); + Image image = app.getWindow()->getFrontBufferImage(); + ASSERT_TRUE( image.getWidth() > 125 && image.getHeight() > 105 ); + const Color wedge = image.getPixel( 125, 105 ); + EXPECT_TRUE_MSG( wedge.g > 180 && wedge.r < 100, + "The inside wedge between adjacent stroke quads must be filled" ); + PixelDensity::setPixelDensity( previousDensity ); +} diff --git a/src/tests/unit_tests/pixel_density_environment_tests.cpp b/src/tests/unit_tests/pixel_density_environment_tests.cpp new file mode 100644 index 000000000..b38f5ce56 --- /dev/null +++ b/src/tests/unit_tests/pixel_density_environment_tests.cpp @@ -0,0 +1,46 @@ +#include "utest.h" +#include +#include +#include +#include +#include +#include + +using namespace EE; +using namespace EE::Graphics; +using namespace EE::System; +using namespace EE::UI; +using namespace EE::Window; + +UTEST( PixelDensity, environmentDefaultIsStrictAndDoesNotScaleWindowSettings ) { + const char* prior = std::getenv( "EEPP_PIXEL_DENSITY" ); + const std::string previous = prior ? prior : ""; + EXPECT_TRUE( Sys::setEnv( "EEPP_PIXEL_DENSITY", "1.75" ) ); + EXPECT_EQ( PixelDensity::getEnvironmentPixelDensity(), 1.75f ); + WindowSettings window( 320, 240, "Density Test" ); + EXPECT_EQ( window.Width, 320u ); + EXPECT_EQ( window.Height, 240u ); + EXPECT_EQ( window.PixelDensity, 0.f ); + EXPECT_TRUE( Sys::setEnv( "EEPP_PIXEL_DENSITY", "1.75oops" ) ); + EXPECT_EQ( PixelDensity::getEnvironmentPixelDensity(), 0.f ); + EXPECT_TRUE( Sys::setEnv( "EEPP_PIXEL_DENSITY", "-2" ) ); + EXPECT_EQ( PixelDensity::getEnvironmentPixelDensity(), 0.f ); + EXPECT_TRUE( Sys::setEnv( "EEPP_PIXEL_DENSITY", "nan" ) ); + EXPECT_EQ( PixelDensity::getEnvironmentPixelDensity(), 0.f ); + EXPECT_TRUE( Sys::setEnv( "EEPP_PIXEL_DENSITY", previous.c_str() ) ); +} + +UTEST( PixelDensity, environmentDefaultAppliesToUIWithoutResizingWindow ) { + const char* prior = std::getenv( "EEPP_PIXEL_DENSITY" ); + const std::string previous = prior ? prior : ""; + const Float priorDensity = PixelDensity::getPixelDensity(); + EXPECT_TRUE( Sys::setEnv( "EEPP_PIXEL_DENSITY", "1.75" ) ); + { + UIApplication app( WindowSettings{ 320, 240, "Environment Density Test" } ); + EXPECT_EQ( PixelDensity::getPixelDensity(), 1.75f ); + EXPECT_EQ( app.getWindow()->getWindowInfo()->WindowConfig.Width, 320u ); + EXPECT_EQ( app.getWindow()->getWindowInfo()->WindowConfig.Height, 240u ); + } + EXPECT_TRUE( Sys::setEnv( "EEPP_PIXEL_DENSITY", previous.c_str() ) ); + PixelDensity::setPixelDensity( priorDensity ); +} diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 98436b2c8..03255fb08 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -4574,6 +4574,7 @@ void App::init( InitParameters& params ) { mPortableMode = params.portable || !params.profile.empty(); mProfilePath = params.profile; mDisplayDPI = currentDisplay->getDPI(); + const Float environmentDensity = PixelDensity::getEnvironmentPixelDensity(); mUseFrameBuffer = params.frameBuffer; mBenchmarkMode = params.benchmarkMode; mDisablePlugins = params.disablePlugins; @@ -4618,19 +4619,18 @@ void App::init( InitParameters& params ) { mDisplayDPI = currentDisplay->getDPI(); #if EE_PLATFORM == EE_PLATFORM_ANDROID - mConfig.windowState.pixelDensity = - params.pidelDensity > 0 - ? params.pidelDensity - : ( mConfig.windowState.pixelDensity > 0 ? mConfig.windowState.pixelDensity - : currentDisplay->getPixelDensity() > 2 ? currentDisplay->getPixelDensity() / 2 - : currentDisplay->getPixelDensity() ); + const Float displayDensity = currentDisplay->getPixelDensity() > 2 + ? currentDisplay->getPixelDensity() / 2 + : currentDisplay->getPixelDensity(); #else - mConfig.windowState.pixelDensity = - params.pidelDensity > 0 - ? params.pidelDensity - : ( mConfig.windowState.pixelDensity > 0 ? mConfig.windowState.pixelDensity - : currentDisplay->getPixelDensity() ); + const Float displayDensity = currentDisplay->getPixelDensity(); #endif + if ( params.pidelDensity > 0 ) { + mConfig.windowState.pixelDensity = params.pidelDensity; + } else if ( mConfig.windowState.pixelDensity <= 0 ) { + mConfig.windowState.pixelDensity = + environmentDensity > 0 ? environmentDensity : displayDensity; + } displayManager->enableScreenSaver(); displayManager->enableMouseFocusClickThrough(); diff --git a/src/tools/eterm/eterm.cpp b/src/tools/eterm/eterm.cpp index 1ef070a40..30c86f663 100644 --- a/src/tools/eterm/eterm.cpp +++ b/src/tools/eterm/eterm.cpp @@ -814,8 +814,14 @@ int App::run( int argc, char* argv[] ) { } UIApplication::Settings appSettings; appSettings.basePath = FileSystem::removeLastFolderFromPath( resPath ); - appSettings.pixelDensity = config->window.pixelDensity > 0 ? config->window.pixelDensity - : currentDisplay->getPixelDensity(); + const Float environmentDensity = PixelDensity::getEnvironmentPixelDensity(); + if ( config->window.pixelDensity > 0 ) { + appSettings.pixelDensity = config->window.pixelDensity; + } else if ( environmentDensity > 0 ) { + appSettings.pixelDensity = environmentDensity; + } else { + appSettings.pixelDensity = currentDisplay->getPixelDensity(); + } appSettings.fontHinting = config->font.hinting; appSettings.fontAntialiasing = config->font.antialiasing; appSettings.baseFont = uiFont.get();