mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-25 23:57:53 +03:00
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.
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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<Color> axisColor;
|
||||
std::optional<Color> tickColor;
|
||||
std::optional<Color> tickLabelColor;
|
||||
std::optional<Color> axisLabelColor;
|
||||
std::optional<Color> hoverColor;
|
||||
std::optional<Color> 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<std::unique_ptr<ChartAxis>>& axes() const { return mAxes; }
|
||||
const SmallVector<std::unique_ptr<ChartAxis>, 4>& axes() const { return mAxes; }
|
||||
|
||||
const std::vector<std::unique_ptr<LineSeries>>& series() const { return mSeries; }
|
||||
const SmallVector<std::unique_ptr<LineSeries>, 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<std::unique_ptr<ChartAxis>> mAxes;
|
||||
std::vector<std::unique_ptr<LineSeries>> mSeries;
|
||||
SmallVector<std::unique_ptr<ChartAxis>, 4> mAxes;
|
||||
SmallVector<std::unique_ptr<LineSeries>, 2> mSeries;
|
||||
ObservableValue<Uint64> 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<double, 16> ticks;
|
||||
std::vector<String> labels;
|
||||
float width{ 0.f };
|
||||
SmallVector<String, 8> labels;
|
||||
Float width{ 0.f };
|
||||
};
|
||||
|
||||
Rectf plot;
|
||||
SmallVector<double, 16> xTicks;
|
||||
SmallVector<double, 16> yTicks;
|
||||
std::vector<String> xLabels;
|
||||
std::vector<String> yLabels;
|
||||
std::vector<float> xLabelWidths;
|
||||
std::vector<float> yLabelWidths;
|
||||
float xAxisLabelWidth{ 0.f };
|
||||
SmallVector<String, 8> xLabels;
|
||||
SmallVector<String, 8> yLabels;
|
||||
SmallVector<Float, 8> xLabelWidths;
|
||||
SmallVector<Float, 8> yLabelWidths;
|
||||
Float xAxisLabelWidth{ 0.f };
|
||||
std::vector<RightAxis> 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<ChartModel> mModel;
|
||||
ObservableValue<Uint64>::Connection mModelConnection;
|
||||
std::vector<ViewAxis> mAxes;
|
||||
std::vector<SeriesCache> mCaches;
|
||||
SmallVector<ViewAxis, 4> mAxes;
|
||||
SmallVector<SeriesCache, 2> mCaches;
|
||||
Layout mLayout;
|
||||
Uint64 mViewRevision{ 1 };
|
||||
Uint64 mLayoutRevision{ 1 };
|
||||
@@ -267,7 +308,15 @@ class EE_API UIChart : public UIWidget {
|
||||
Uint64 mAutoRevision{ std::numeric_limits<Uint64>::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
|
||||
|
||||
@@ -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<std::string> 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<Float> 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
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <eepp/core/string.hpp>
|
||||
#include <eepp/graphics/pixeldensity.hpp>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+154
-85
@@ -1,11 +1,13 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <eepp/graphics/pixeldensity.hpp>
|
||||
#include <eepp/graphics/primitives.hpp>
|
||||
#include <eepp/graphics/renderer/renderer.hpp>
|
||||
#include <eepp/graphics/text.hpp>
|
||||
#include <eepp/ui/charts/uichart.hpp>
|
||||
#include <eepp/ui/uieventdispatcher.hpp>
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
#include <eepp/ui/uistyle.hpp>
|
||||
#include <eepp/ui/uithememanager.hpp>
|
||||
#include <eepp/ui/uitooltip.hpp>
|
||||
#include <limits>
|
||||
@@ -51,8 +53,9 @@ std::optional<DataRange> 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<Uint32>( 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<float>( xView.toPixel( point.x, mLayout.plot.Left, mLayout.plot.getWidth(),
|
||||
static_cast<Float>( xView.toPixel( point.x, mLayout.plot.Left, mLayout.plot.getWidth(),
|
||||
false, *cache.series->xAxis() ) ),
|
||||
static_cast<float>( yView.toPixel( point.y, mLayout.plot.Top, mLayout.plot.getHeight(),
|
||||
static_cast<Float>( 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<Uint32>( 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<float>( viewport( xAxis() ).toPixel(
|
||||
const Float x = static_cast<Float>( 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<float>( viewport( yAxis() ).toPixel(
|
||||
const Float y = static_cast<Float>( 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<float>( viewport( axisLayout.axis )
|
||||
const Float y =
|
||||
static_cast<Float>( 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<float>( xView.toPixel(
|
||||
const Vector2f pixel( static_cast<Float>( xView.toPixel(
|
||||
point.x, mLayout.plot.Left, mLayout.plot.getWidth(),
|
||||
false, *series->xAxis() ) ),
|
||||
static_cast<float>( yView.toPixel(
|
||||
static_cast<Float>( 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<float>( viewport( series->xAxis() )
|
||||
const Float px = static_cast<Float>( viewport( series->xAxis() )
|
||||
.toPixel( point.x, mLayout.plot.Left,
|
||||
mLayout.plot.getWidth(), false,
|
||||
*series->xAxis() ) );
|
||||
const float py = static_cast<float>( viewport( series->yAxis() )
|
||||
const Float py = static_cast<Float>( 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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ EE_MAIN_FUNC int main( int, char** ) {
|
||||
[]( double value, double ) { return String::format( "%.1f C", value ); } );
|
||||
std::vector<ChartPoint> load;
|
||||
std::vector<double> temperatureX;
|
||||
std::vector<float> temperatureY;
|
||||
std::vector<Float> 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<float>( 37.0 + 0.08 * x + 2.0 * std::sin( x * 0.12 ) ) );
|
||||
static_cast<Float>( 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<UIChart>( "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<ChartPoint> samples;
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
#include "utest.h"
|
||||
#include <eepp/graphics/image.hpp>
|
||||
#include <eepp/graphics/pixeldensity.hpp>
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <eepp/system/sys.hpp>
|
||||
#include <eepp/ui/charts/uichart.hpp>
|
||||
#include <eepp/ui/uiapplication.hpp>
|
||||
#include <eepp/ui/uiroot.hpp>
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
#include <eepp/ui/uistyle.hpp>
|
||||
#include <eepp/ui/uitooltip.hpp>
|
||||
|
||||
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(
|
||||
"<Chart id=\"declarative_chart\" layout_width=\"20dp\" layout_height=\"20dp\" />" );
|
||||
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<ChartPoint>{ { 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<ChartPoint>{ { 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<ChartPoint>{ { 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 );
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#include "utest.h"
|
||||
#include <cstdlib>
|
||||
#include <eepp/graphics/pixeldensity.hpp>
|
||||
#include <eepp/system/sys.hpp>
|
||||
#include <eepp/ui/uiapplication.hpp>
|
||||
#include <eepp/window/window.hpp>
|
||||
#include <string>
|
||||
|
||||
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 );
|
||||
}
|
||||
+11
-11
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user