296 lines
8.9 KiB
C++
296 lines
8.9 KiB
C++
/******************************************************************************
|
|
* QSkinny - Copyright (C) The authors
|
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
*****************************************************************************/
|
|
|
|
#include "CircularChartSkinlet.h"
|
|
#include "CircularChart.h"
|
|
#include "ChartSample.h"
|
|
|
|
#include <QskIntervalF.h>
|
|
#include <QskArcMetrics.h>
|
|
#include <QskArcRenderNode.h>
|
|
|
|
#include <qmath.h>
|
|
|
|
namespace
|
|
{
|
|
inline QskArcMetrics segmentMetrics(
|
|
const QskSkinnable* skinnable, int index )
|
|
{
|
|
auto metrics = skinnable->arcMetricsHint( CircularChart::Arc );
|
|
|
|
const auto chart = static_cast< const CircularChart* >( skinnable );
|
|
|
|
const auto span = chart->stackedInterval( index );
|
|
const auto total = chart->effectiveTotalValue();
|
|
|
|
const auto a1 = metrics.angleAtRatio( span.lowerBound() / total );
|
|
const auto a2 = metrics.angleAtRatio( span.upperBound() / total );
|
|
|
|
metrics.setStartAngle( a1 );
|
|
metrics.setSpanAngle( a2 - a1 );
|
|
|
|
return metrics;
|
|
}
|
|
|
|
QRectF closedArcRect( const QskSkinnable* skinnable )
|
|
{
|
|
using Q = CircularChart;
|
|
|
|
const auto chart = static_cast< const CircularChart* >( skinnable );
|
|
const auto r = chart->subControlContentsRect( Q::Arc );
|
|
|
|
const auto metrics = skinnable->arcMetricsHint( Q::Arc );
|
|
if ( metrics.isClosed() )
|
|
return r;
|
|
|
|
const auto size = skinnable->strutSizeHint( CircularChart::Arc );
|
|
|
|
auto pos = r.topLeft();
|
|
|
|
{
|
|
const QRectF ellipseRect( 0.0, 0.0, size.width(), size.height() );
|
|
pos -= metrics.boundingRect( ellipseRect ).topLeft();
|
|
}
|
|
|
|
return QRectF( pos.x(), pos.y(), size.width(), size.height() );
|
|
}
|
|
|
|
static inline QRectF textRect( qreal x, qreal y )
|
|
{
|
|
const qreal sz = 1000.0; // something big enough to avoid wrapping
|
|
return QRectF( x - 0.5 * sz, y - 0.5 * sz, sz, sz );
|
|
}
|
|
}
|
|
|
|
class CircularChartSkinlet::PrivateData
|
|
{
|
|
public:
|
|
// caching the geometry of the rect for drawing all arc segments
|
|
mutable QRectF closedArcRect;
|
|
};
|
|
|
|
CircularChartSkinlet::CircularChartSkinlet( QskSkin* skin )
|
|
: Inherited( skin )
|
|
, m_data( new PrivateData() )
|
|
{
|
|
setNodeRoles( { PanelRole, ArcRole, SegmentRole, SegmentLabelRole } );
|
|
}
|
|
|
|
CircularChartSkinlet::~CircularChartSkinlet() = default;
|
|
|
|
void CircularChartSkinlet::updateNode(
|
|
QskSkinnable* skinnable, QSGNode* node ) const
|
|
{
|
|
m_data->closedArcRect = ::closedArcRect( skinnable );
|
|
Inherited::updateNode( skinnable, node );
|
|
}
|
|
|
|
QRectF CircularChartSkinlet::subControlRect( const QskSkinnable* skinnable,
|
|
const QRectF& contentsRect, QskAspect::Subcontrol subControl ) const
|
|
{
|
|
using Q = CircularChart;
|
|
|
|
if ( subControl == Q::Panel )
|
|
return contentsRect;
|
|
|
|
if ( subControl == Q::Arc )
|
|
{
|
|
const auto chart = static_cast< const CircularChart* >( skinnable );
|
|
return chart->subControlContentsRect( Q::Panel );
|
|
}
|
|
|
|
return Inherited::subControlRect( skinnable, contentsRect, subControl );
|
|
}
|
|
|
|
QSGNode* CircularChartSkinlet::updateSubNode(
|
|
const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const
|
|
{
|
|
using Q = CircularChart;
|
|
|
|
switch ( nodeRole )
|
|
{
|
|
case PanelRole:
|
|
return updateBoxNode( skinnable, node, Q::Panel );
|
|
|
|
case ArcRole:
|
|
{
|
|
const auto aspect = Q::Arc | QskAspect::Border;
|
|
|
|
const auto borderColor = skinnable->color( aspect );
|
|
const auto borderWidth = skinnable->metric( aspect );
|
|
|
|
return updateArcSegmentNode( skinnable, node, borderWidth, borderColor,
|
|
skinnable->gradientHint( Q::Arc ), skinnable->arcMetricsHint( Q::Arc ) );
|
|
}
|
|
|
|
case SegmentRole:
|
|
return updateSeriesNode( skinnable, Q::Segment, node );
|
|
|
|
case SegmentLabelRole:
|
|
return updateSeriesNode( skinnable, Q::SegmentLabel, node );
|
|
}
|
|
|
|
return Inherited::updateSubNode( skinnable, nodeRole, node );
|
|
}
|
|
|
|
int CircularChartSkinlet::sampleCount(
|
|
const QskSkinnable* skinnable, QskAspect::Subcontrol subControl ) const
|
|
{
|
|
using Q = CircularChart;
|
|
|
|
if ( subControl == Q::Segment || subControl == Q::SegmentLabel )
|
|
{
|
|
const auto chart = static_cast< const CircularChart* >( skinnable );
|
|
return chart->series().count();
|
|
}
|
|
|
|
return Inherited::sampleCount( skinnable, subControl );
|
|
}
|
|
|
|
QRectF CircularChartSkinlet::sampleRect( const QskSkinnable* skinnable,
|
|
const QRectF& contentsRect, QskAspect::Subcontrol subControl, int index ) const
|
|
{
|
|
// See https://github.com/uwerat/qskinny/issues/307
|
|
return Inherited::sampleRect( skinnable, contentsRect, subControl, index );
|
|
}
|
|
|
|
int CircularChartSkinlet::sampleIndexAt(
|
|
const QskSkinnable* skinnable, const QRectF& contentsRect,
|
|
QskAspect::Subcontrol subControl, const QPointF& pos ) const
|
|
{
|
|
// See https://github.com/uwerat/qskinny/issues/307
|
|
return Inherited::sampleIndexAt( skinnable, contentsRect, subControl, pos );
|
|
}
|
|
|
|
QSGNode* CircularChartSkinlet::updateSampleNode( const QskSkinnable* skinnable,
|
|
QskAspect::Subcontrol subControl, int index, QSGNode* node ) const
|
|
{
|
|
using Q = CircularChart;
|
|
|
|
const auto chart = static_cast< const CircularChart* >( skinnable );
|
|
|
|
if ( subControl == Q::Segment )
|
|
{
|
|
const auto aspect = Q::Segment | QskAspect::Border;
|
|
|
|
const auto borderColor = skinnable->color( aspect );
|
|
const auto borderWidth = skinnable->metric( aspect );
|
|
|
|
return updateArcSegmentNode( skinnable, node, borderWidth, borderColor,
|
|
chart->gradientAt( index ), ::segmentMetrics( skinnable, index ) );
|
|
}
|
|
|
|
if ( subControl == Q::SegmentLabel )
|
|
{
|
|
return updateArcLabelNode( skinnable, node,
|
|
chart->labelAt( index ), ::segmentMetrics( skinnable, index ) );
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
QSGNode* CircularChartSkinlet::updateArcSegmentNode( const QskSkinnable*,
|
|
QSGNode* node, qreal borderWidth, const QColor& borderColor,
|
|
const QskGradient& gradient, const QskArcMetrics& metrics ) const
|
|
{
|
|
auto arcNode = static_cast< QskArcRenderNode* >( node );
|
|
if ( arcNode == nullptr )
|
|
arcNode = new QskArcRenderNode();
|
|
|
|
arcNode->updateArc( m_data->closedArcRect, metrics, true,
|
|
borderWidth, borderColor, gradient );
|
|
|
|
return arcNode;
|
|
}
|
|
|
|
QSGNode* CircularChartSkinlet::updateArcLabelNode(
|
|
const QskSkinnable* skinnable, QSGNode* node,
|
|
const QString& text, const QskArcMetrics& metrics ) const
|
|
{
|
|
/*
|
|
possible improvements:
|
|
|
|
- rotating the labels
|
|
- elide/wrap modes
|
|
*/
|
|
const auto& r = m_data->closedArcRect;
|
|
const auto sz = qMin( r.width(), r.height() );
|
|
|
|
if ( text.isEmpty() || sz <= 0.0 )
|
|
return nullptr;
|
|
|
|
const auto m = metrics.toAbsolute( 0.5 * sz );
|
|
|
|
const auto pos = 0.5; // [0.0, 1.0] -> distance from the inner border
|
|
const auto dt = pos * ( 1.0 - m.thickness() / sz );
|
|
|
|
const qreal radians = qDegreesToRadians( m.startAngle() + 0.5 * m.spanAngle() );
|
|
const auto c = r.center();
|
|
|
|
const qreal x = c.x () + dt * r.width() * qFastCos( radians );
|
|
const qreal y = c.y() - dt * r.height() * qFastSin( radians );
|
|
|
|
return updateTextNode( skinnable, node,
|
|
textRect( x, y ), Qt::AlignCenter, text, CircularChart::SegmentLabel );
|
|
}
|
|
|
|
QSizeF CircularChartSkinlet::sizeHint( const QskSkinnable* skinnable,
|
|
Qt::SizeHint which, const QSizeF& constraint ) const
|
|
{
|
|
using Q = CircularChart;
|
|
|
|
if ( which != Qt::PreferredSize )
|
|
return QSizeF();
|
|
|
|
const auto sz = skinnable->strutSizeHint( CircularChart::Arc );
|
|
|
|
auto metrics = skinnable->arcMetricsHint( Q::Arc );
|
|
|
|
QSizeF hint;
|
|
|
|
if ( metrics.isClosed() )
|
|
{
|
|
// We only support constrained hints for closed arcs
|
|
|
|
if ( constraint.width() >= 0.0 || constraint.height() >= 0.0 )
|
|
{
|
|
const qreal ratio = sz.isEmpty() ? 1.0 : sz.width() / sz.height();
|
|
|
|
if ( constraint.width() >= 0.0 )
|
|
{
|
|
hint = skinnable->innerBoxSize(
|
|
Q::Panel, QSizeF( constraint.width(), constraint.width() ) );
|
|
|
|
hint.setHeight( hint.width() / ratio );
|
|
hint = skinnable->outerBoxSize( Q::Panel, hint );
|
|
|
|
return QSizeF( -1.0, hint.height() );
|
|
}
|
|
else
|
|
{
|
|
hint = skinnable->innerBoxSize(
|
|
Q::Panel, QSizeF( constraint.height(), constraint.height() ) );
|
|
|
|
hint.setWidth( hint.height() * ratio );
|
|
hint = skinnable->outerBoxSize( Q::Panel, hint );
|
|
|
|
return QSizeF( hint.width(), -1.0 );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( constraint.width() >= 0.0 || constraint.height() >= 0.0 )
|
|
return QSizeF();
|
|
|
|
metrics = metrics.toAbsolute( 0.5 * sz.width(), 0.5 * sz.height() );
|
|
|
|
hint = metrics.boundingSize( sz );
|
|
hint = skinnable->outerBoxSize( Q::Panel, hint );
|
|
|
|
return hint;
|
|
}
|
|
|
|
#include "moc_CircularChartSkinlet.cpp"
|