qskinny/playground/charts/CircularChartSkinlet.cpp
2024-09-23 16:04:09 +02:00

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"