qskinny/src/nodes/QskGraduationRenderer.cpp

549 lines
13 KiB
C++
Raw Normal View History

/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
2023-04-06 09:23:37 +02:00
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#include "QskGraduationRenderer.h"
2023-11-28 10:46:03 +01:00
#include "QskTickmarks.h"
#include "QskSkinlet.h"
#include "QskSGNode.h"
2023-11-28 13:36:47 +01:00
#include "QskAxisScaleNode.h"
#include "QskTextOptions.h"
2022-03-24 11:14:46 +01:00
#include "QskTextColors.h"
#include "QskGraphic.h"
2022-03-24 11:14:46 +01:00
#include "QskColorFilter.h"
#include "QskControl.h"
2022-03-24 11:14:46 +01:00
#include "QskIntervalF.h"
#include "QskFunctions.h"
#include <qstring.h>
#include <qfontmetrics.h>
2023-11-28 13:36:47 +01:00
#include <qquickwindow.h>
namespace
{
class ScaleMap
{
public:
inline ScaleMap( bool isHorizontal, const QTransform& transform )
: t( isHorizontal ? transform.dx() : transform.dy() )
, f( isHorizontal ? transform.m11() : transform.m22() )
{
}
inline qreal map( qreal v ) const { return t + f * v; };
private:
const qreal t;
const qreal f;
};
}
static inline bool qskIsHorizontal( Qt::Edge edge )
{
return edge & ( Qt::TopEdge | Qt::BottomEdge );
}
static QSGNode* qskRemoveTraillingNodes( QSGNode* node, QSGNode* childNode )
{
QskSGNode::removeAllChildNodesFrom( node, childNode );
return nullptr;
}
2023-11-28 13:36:47 +01:00
static inline QTransform qskScaleTransform( Qt::Edge edge,
const QskIntervalF& boundaries, const QskIntervalF& range )
{
2023-11-28 13:36:47 +01:00
using T = QTransform;
2023-11-28 13:36:47 +01:00
if ( qskIsHorizontal( edge ) )
2020-12-05 15:09:31 +01:00
{
2023-11-28 13:36:47 +01:00
auto transform = T::fromTranslate( -boundaries.lowerBound(), 0.0 );
transform *= T::fromScale( range.length() / boundaries.length(), 1.0 );
transform *= T::fromTranslate( range.lowerBound(), 0.0 );
return transform;
}
else
{
auto transform = T::fromTranslate( 0.0, -boundaries.lowerBound() );
transform *= T::fromScale( 1.0, -range.length() / boundaries.length() );
transform *= T::fromTranslate( 0.0, range.upperBound() );
return transform;
}
2023-11-28 13:36:47 +01:00
}
2020-12-05 15:09:31 +01:00
2023-11-28 13:36:47 +01:00
static inline quint8 qskLabelNodeRole( const QVariant& label )
{
if ( !label.isNull() )
2020-12-05 15:09:31 +01:00
{
2023-11-28 13:36:47 +01:00
if ( label.canConvert< QString >() )
return 1;
if ( label.canConvert< QskGraphic >() )
return 2;
}
2023-11-28 13:36:47 +01:00
return QskSGNode::NoRole;
}
class QskGraduationRenderer::PrivateData
2022-03-24 11:14:46 +01:00
{
public:
2023-11-28 13:36:47 +01:00
// Coordinates related to the scales
2022-03-24 11:14:46 +01:00
QskIntervalF boundaries;
2023-11-28 10:46:03 +01:00
QskTickmarks tickmarks;
2022-03-24 11:14:46 +01:00
2023-11-28 13:36:47 +01:00
/*
Item cooordinates. In case of an horizontal scale
position is an y coordinate, while range corresponds to x coordinates
( vertical: v.v )
*/
qreal position = 0.0;
QskIntervalF range;
#if 1
QColor tickColor = Qt::black; // rgb value ???
#endif
2022-03-24 11:14:46 +01:00
qreal tickWidth = 1.0;
2023-11-28 13:36:47 +01:00
qreal tickLength = 10.0;
qreal spacing = 5.0;
2022-03-24 11:14:46 +01:00
QFont font;
QskTextColors textColors;
QskColorFilter colorFilter;
2023-11-28 13:36:47 +01:00
Qt::Edge edge = Qt::BottomEdge;
QskGraduationRenderer::Flags flags = ClampedLabels;
2022-03-24 11:14:46 +01:00
};
QskGraduationRenderer::QskGraduationRenderer()
2022-03-24 11:14:46 +01:00
: m_data( new PrivateData() )
{
}
QskGraduationRenderer::~QskGraduationRenderer()
2022-03-24 11:14:46 +01:00
{
}
void QskGraduationRenderer::setEdge( Qt::Edge edge )
{
2023-11-28 13:36:47 +01:00
m_data->edge = edge;
}
Qt::Edge QskGraduationRenderer::edge() const
2023-04-24 11:55:36 +02:00
{
2023-11-28 13:36:47 +01:00
return m_data->edge;
2023-04-24 11:55:36 +02:00
}
void QskGraduationRenderer::setFlag( Flag flag, bool on )
2023-02-28 11:59:46 +01:00
{
2023-11-28 13:36:47 +01:00
if ( on )
m_data->flags |= flag;
else
m_data->flags &= ~flag;
2023-02-28 11:59:46 +01:00
}
void QskGraduationRenderer::setFlags( Flags flags )
2023-04-24 11:55:36 +02:00
{
2023-11-28 13:36:47 +01:00
m_data->flags = flags;
}
QskGraduationRenderer::Flags QskGraduationRenderer::flags() const
2023-11-28 13:36:47 +01:00
{
return m_data->flags;
2023-04-24 11:55:36 +02:00
}
void QskGraduationRenderer::setBoundaries( qreal lowerBound, qreal upperBound )
2023-04-24 11:55:36 +02:00
{
setBoundaries( QskIntervalF( lowerBound, upperBound ) );
}
void QskGraduationRenderer::setBoundaries( const QskIntervalF& boundaries )
{
2022-03-24 11:14:46 +01:00
m_data->boundaries = boundaries;
}
QskIntervalF QskGraduationRenderer::boundaries() const
2023-04-24 11:55:36 +02:00
{
return m_data->boundaries;
}
qreal QskGraduationRenderer::position() const
2023-11-28 13:36:47 +01:00
{
return m_data->position;
}
void QskGraduationRenderer::setPosition( qreal pos )
2023-11-28 13:36:47 +01:00
{
m_data->position = pos;
}
void QskGraduationRenderer::setRange( qreal from, qreal to )
2023-11-28 13:36:47 +01:00
{
setRange( QskIntervalF( from, to ) );
}
void QskGraduationRenderer::setRange( const QskIntervalF& range )
2023-11-28 13:36:47 +01:00
{
m_data->range = range;
}
QskIntervalF QskGraduationRenderer::range() const
2023-11-28 13:36:47 +01:00
{
return m_data->range;
}
void QskGraduationRenderer::setTickmarks( const QskTickmarks& tickmarks )
{
2022-03-24 11:14:46 +01:00
m_data->tickmarks = tickmarks;
}
const QskTickmarks& QskGraduationRenderer::tickmarks() const
2023-04-24 11:55:36 +02:00
{
return m_data->tickmarks;
}
void QskGraduationRenderer::setSpacing( qreal spacing )
2023-11-28 13:36:47 +01:00
{
m_data->spacing = qMax( spacing, 0.0 );
}
qreal QskGraduationRenderer::spacing() const
2023-11-28 13:36:47 +01:00
{
return m_data->spacing;
}
void QskGraduationRenderer::setTickColor( const QColor& color )
{
2022-03-24 11:14:46 +01:00
m_data->tickColor = color;
}
QColor QskGraduationRenderer::tickColor() const
2023-04-24 11:55:36 +02:00
{
return m_data->tickColor;
}
void QskGraduationRenderer::setTickLength( qreal length )
2023-11-28 13:36:47 +01:00
{
m_data->tickLength = qMax( length, 0.0 );
}
qreal QskGraduationRenderer::tickLength() const
2023-11-28 13:36:47 +01:00
{
return m_data->tickLength;
}
void QskGraduationRenderer::setTickWidth( qreal width )
{
2023-11-28 13:36:47 +01:00
m_data->tickWidth = qMax( width, 0.0 );
}
qreal QskGraduationRenderer::tickWidth() const
2023-04-24 11:55:36 +02:00
{
return m_data->tickWidth;
}
void QskGraduationRenderer::setFont( const QFont& font )
{
2022-03-24 11:14:46 +01:00
m_data->font = font;
}
QFont QskGraduationRenderer::font() const
2023-04-24 11:55:36 +02:00
{
return m_data->font;
}
void QskGraduationRenderer::setTextColors( const QskTextColors& textColors )
{
2022-03-24 11:14:46 +01:00
m_data->textColors = textColors;
}
QskTextColors QskGraduationRenderer::textColors() const
2023-04-24 11:55:36 +02:00
{
return m_data->textColors;
}
void QskGraduationRenderer::setColorFilter( const QskColorFilter& colorFilter )
{
2022-03-24 11:14:46 +01:00
m_data->colorFilter = colorFilter;
}
const QskColorFilter& QskGraduationRenderer::colorFilter() const
2023-04-24 11:55:36 +02:00
{
return m_data->colorFilter;
}
QSGNode* QskGraduationRenderer::updateNode(
2023-11-28 13:36:47 +01:00
const QskSkinnable* skinnable, QSGNode* node )
{
2023-11-28 13:36:47 +01:00
enum Role : quint8 { Ticks = 1, Labels = 2 };
static const QVector< quint8 > roles = { Ticks, Labels };
const auto transform = qskScaleTransform(
m_data->edge, m_data->boundaries, m_data->range );
if ( node == nullptr )
node = new QSGNode();
2023-11-28 13:36:47 +01:00
for ( auto role : roles )
{
2023-11-28 13:36:47 +01:00
auto oldNode = QskSGNode::findChildNode( node, role );
2023-11-28 13:36:47 +01:00
auto newNode = ( role == Ticks )
? updateTicksNode( transform, oldNode )
: updateLabelsNode( skinnable, transform, oldNode );
2023-11-28 13:36:47 +01:00
QskSGNode::replaceChildNode( roles, role, node, oldNode, newNode );
}
return node;
}
QSGNode* QskGraduationRenderer::updateTicksNode(
2023-11-28 13:36:47 +01:00
const QTransform& transform, QSGNode* node ) const
{
2023-11-28 13:36:47 +01:00
QskIntervalF backbone;
if ( m_data->flags & Backbone )
backbone = m_data->boundaries;
2023-11-28 13:36:47 +01:00
const auto orientation = qskIsHorizontal( m_data->edge )
? Qt::Horizontal : Qt::Vertical;
2023-11-28 13:36:47 +01:00
auto alignment = QskAxisScaleNode::Centered;
2020-12-05 15:09:31 +01:00
2023-11-28 13:36:47 +01:00
if ( !( m_data->flags & CenteredTickmarks ) )
{
switch( m_data->edge )
{
case Qt::LeftEdge:
case Qt::TopEdge:
alignment = QskAxisScaleNode::Leading;
break;
case Qt::BottomEdge:
case Qt::RightEdge:
alignment = QskAxisScaleNode::Trailing;
break;
}
}
auto axisNode = QskSGNode::ensureNode< QskAxisScaleNode >( node );
2023-04-04 09:05:16 +02:00
2023-11-28 13:36:47 +01:00
axisNode->setColor( m_data->tickColor );
axisNode->setAxis( orientation, m_data->position, transform );
axisNode->setTickGeometry( alignment, m_data->tickLength, m_data->tickWidth );
axisNode->setPixelAlignment( Qt::Horizontal | Qt::Vertical );
2020-12-05 15:09:31 +01:00
2023-11-28 13:36:47 +01:00
axisNode->update( m_data->tickmarks, backbone );
return axisNode;
}
QSGNode* QskGraduationRenderer::updateLabelsNode( const QskSkinnable* skinnable,
2023-11-28 13:36:47 +01:00
const QTransform& transform, QSGNode* node ) const
{
2022-03-24 11:14:46 +01:00
const auto ticks = m_data->tickmarks.majorTicks();
if ( ticks.isEmpty() )
return nullptr;
if( node == nullptr )
node = new QSGNode;
2022-03-24 11:14:46 +01:00
const QFontMetricsF fm( m_data->font );
auto nextNode = node->firstChild();
2023-11-28 13:36:47 +01:00
QRectF lastRect; // to skip overlapping label
2021-04-19 09:28:19 +02:00
for ( auto tick : ticks )
{
const auto label = labelAt( tick );
2023-11-28 13:36:47 +01:00
const auto role = qskLabelNodeRole( label );
2023-11-28 13:36:47 +01:00
if ( nextNode && QskSGNode::nodeRole( nextNode ) != role )
nextNode = qskRemoveTraillingNodes( node, nextNode );
2023-11-28 13:36:47 +01:00
QSizeF size;
2023-11-28 13:36:47 +01:00
if ( label.canConvert< QString >() )
{
size = qskTextRenderSize( fm, label.toString() );
}
else if ( label.canConvert< QskGraphic >() )
{
const auto graphic = label.value< QskGraphic >();
if ( !graphic.isNull() )
{
2023-11-28 13:36:47 +01:00
size.rheight() = fm.height();
size.rwidth() = graphic.widthForHeight( size.height() );
}
2023-11-28 13:36:47 +01:00
}
2023-11-28 13:36:47 +01:00
if ( size.isEmpty() )
continue;
2023-11-28 13:36:47 +01:00
const auto rect = labelRect( transform, tick, size );
2023-11-28 13:36:47 +01:00
if ( !lastRect.isEmpty() && lastRect.intersects( rect ) )
{
/*
Label do overlap: in case it is the last tick we remove
the precessor - otherwise we simply skip this one
*/
2021-04-19 09:28:19 +02:00
2023-11-28 13:36:47 +01:00
if ( tick != ticks.last() )
continue; // skip this label
2023-11-28 13:36:47 +01:00
if ( auto obsoleteNode = nextNode
? nextNode->previousSibling() : node->lastChild() )
{
2023-11-28 13:36:47 +01:00
node->removeChildNode( obsoleteNode );
if ( obsoleteNode->flags() & QSGNode::OwnedByParent )
delete obsoleteNode;
}
}
2023-11-28 13:36:47 +01:00
nextNode = updateTickLabelNode( skinnable, nextNode, label, rect );
2023-11-28 13:36:47 +01:00
if ( nextNode)
{
lastRect = rect;
2023-11-28 13:36:47 +01:00
if ( nextNode->parent() != node )
{
2023-11-28 13:36:47 +01:00
QskSGNode::setNodeRole( nextNode, role );
node->appendChildNode( nextNode );
}
2023-11-28 13:36:47 +01:00
nextNode = nextNode->nextSibling();
}
}
qskRemoveTraillingNodes( node, nextNode );
return node;
}
QVariant QskGraduationRenderer::labelAt( qreal pos ) const
{
return QString::number( pos, 'g' );
}
2023-11-28 13:36:47 +01:00
// should be cached
QSizeF QskGraduationRenderer::boundingLabelSize() const
{
2023-11-28 13:36:47 +01:00
QSizeF boundingSize( 0.0, 0.0 );
2022-03-24 11:14:46 +01:00
const auto ticks = m_data->tickmarks.majorTicks();
if ( ticks.isEmpty() )
2023-11-28 13:36:47 +01:00
return boundingSize;
2022-03-24 11:14:46 +01:00
const QFontMetricsF fm( m_data->font );
2021-02-01 10:22:54 +01:00
const qreal h = fm.height();
for ( auto tick : ticks )
{
const auto label = labelAt( tick );
if ( label.isNull() )
continue;
if ( label.canConvert< QString >() )
{
2023-11-28 13:36:47 +01:00
boundingSize = boundingSize.expandedTo(
qskTextRenderSize( fm, label.toString() ) );
}
else if ( label.canConvert< QskGraphic >() )
{
const auto graphic = label.value< QskGraphic >();
if ( !graphic.isNull() )
{
2023-11-28 13:36:47 +01:00
const auto w = graphic.widthForHeight( h );
boundingSize.setWidth( qMax( boundingSize.width(), w ) );
}
}
2023-11-28 13:36:47 +01:00
}
return boundingSize;
}
QRectF QskGraduationRenderer::labelRect(
2023-11-28 13:36:47 +01:00
const QTransform& transform, qreal tick, const QSizeF& labelSize ) const
{
const auto isHorizontal = qskIsHorizontal( m_data->edge );
auto offset = m_data->tickLength + m_data->spacing;
if ( m_data->flags & CenteredTickmarks )
offset -= 0.5 * m_data->tickLength;
const bool clampLabels = m_data->flags & ClampedLabels;
const qreal w = labelSize.width();
const qreal h = labelSize.height();
qreal x, y;
const ScaleMap map( isHorizontal, transform );
const auto tickPos = map.map( tick );
qreal min, max;
if ( clampLabels )
{
min = map.map( m_data->boundaries.lowerBound() );
max = map.map( m_data->boundaries.upperBound() );
}
if( isHorizontal )
{
x = tickPos - 0.5 * w;
if ( clampLabels )
x = qBound( min, x, max - w );
y = m_data->position + offset;
}
else
{
const auto tickPos = map.map( tick );
y = tickPos - 0.5 * h;
if ( clampLabels )
y = qBound( max, y, min - h );
x = m_data->position - offset - w;
}
2023-11-28 13:36:47 +01:00
return QRectF( x, y, w, h );
}
QSGNode* QskGraduationRenderer::updateTickLabelNode( const QskSkinnable* skinnable,
2023-11-28 13:36:47 +01:00
QSGNode* node, const QVariant& label, const QRectF& rect ) const
{
if ( label.canConvert< QString >() )
{
return QskSkinlet::updateTextNode( skinnable, node,
rect, Qt::AlignCenter, label.toString(), m_data->font,
QskTextOptions(), m_data->textColors, Qsk::Normal );
}
if ( label.canConvert< QskGraphic >() )
{
const auto alignment = qskIsHorizontal( m_data->edge )
? ( Qt::AlignHCenter | Qt::AlignBottom )
: ( Qt::AlignRight | Qt::AlignVCenter );
return QskSkinlet::updateGraphicNode(
skinnable, node, label.value< QskGraphic >(),
m_data->colorFilter, rect, alignment );
}
2023-11-28 13:36:47 +01:00
return nullptr;
}
2023-11-28 13:36:47 +01:00
#include "moc_QskGraduationRenderer.cpp"