532 lines
16 KiB
C++
532 lines
16 KiB
C++
/******************************************************************************
|
|
* QSkinny - Copyright (C) 2016 Uwe Rathmann
|
|
* This file may be used under the terms of the QSkinny License, Version 1.0
|
|
*****************************************************************************/
|
|
|
|
#include "QskBoxMaterial.h"
|
|
#include "QskBoxOptions.h"
|
|
|
|
#include <QSGMaterialShader>
|
|
#include <QSGTexture>
|
|
#include <QSGGeometryNode>
|
|
|
|
#include <QGuiApplication>
|
|
#include <QImage>
|
|
#include <QOpenGLTexture>
|
|
#include <QOpenGLContext>
|
|
#include <QOpenGLFunctions>
|
|
#include <QPainter>
|
|
#include <QtMath>
|
|
|
|
#include <unordered_map>
|
|
|
|
#define SHADER_S(x) #x
|
|
#define SHADER(x) SHADER_S(x)
|
|
|
|
static qreal qskDevicePixelRatio = 0;
|
|
|
|
class QskBoxMaterial::TextureData
|
|
{
|
|
public:
|
|
TextureData() :
|
|
referenceCount( 1 )
|
|
{
|
|
}
|
|
|
|
QSizeF size;
|
|
QRectF coordinates;
|
|
QRect bounds;
|
|
qint64 referenceCount;
|
|
};
|
|
|
|
namespace
|
|
{
|
|
class TextureCache : public std::unordered_map< uint, QskBoxMaterial::TextureData* >
|
|
{
|
|
public:
|
|
iterator findByData( QskBoxMaterial::TextureData* data )
|
|
{
|
|
auto compare =
|
|
[ data ] ( const std::pair< uint, QskBoxMaterial::TextureData* >& item )
|
|
{
|
|
return item.second == data;
|
|
};
|
|
|
|
return std::find_if( begin(), end(), compare );
|
|
}
|
|
};
|
|
|
|
static TextureCache qskTextureCache;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
class TextureAtlas
|
|
{
|
|
public:
|
|
TextureAtlas() :
|
|
m_texture( QOpenGLTexture::Target2D )
|
|
{
|
|
auto context = QOpenGLContext::currentContext();
|
|
Q_ASSERT_X( context, "QskSymbolMaterial",
|
|
"Did you access qskAtlas from the wrong thread? It should be only "
|
|
"called from the Scene Graph thread." );
|
|
|
|
QObject::connect( context, &QOpenGLContext::aboutToBeDestroyed,
|
|
&TextureAtlas::cleanup );
|
|
|
|
GLint size;
|
|
context->functions()->glGetIntegerv( GL_MAX_TEXTURE_SIZE, &size );
|
|
m_texture.setSize( size, size );
|
|
m_texture.setAutoMipMapGenerationEnabled( false );
|
|
m_texture.setFormat( QOpenGLTexture::RGBA8_UNorm );
|
|
m_texture.setMinMagFilters( QOpenGLTexture::Nearest, QOpenGLTexture::Nearest );
|
|
m_texture.setWrapMode( QOpenGLTexture::ClampToEdge );
|
|
m_texture.allocateStorage( QOpenGLTexture::RGBA, QOpenGLTexture::UInt8 );
|
|
}
|
|
|
|
void insert( QskBoxMaterial::TextureData* data );
|
|
void remove( const QskBoxMaterial::TextureData* data );
|
|
|
|
inline void bindTexture()
|
|
{
|
|
m_texture.bind();
|
|
}
|
|
|
|
inline void releaseTexture()
|
|
{
|
|
m_texture.release();
|
|
}
|
|
|
|
static void cleanup();
|
|
|
|
private:
|
|
QOpenGLTexture m_texture;
|
|
QRegion m_region;
|
|
};
|
|
|
|
Q_GLOBAL_STATIC( TextureAtlas, qskAtlas )
|
|
|
|
void TextureAtlas::cleanup()
|
|
{
|
|
if ( qskAtlas.exists() && !qskAtlas.isDestroyed() )
|
|
qskAtlas->m_texture.destroy();
|
|
}
|
|
|
|
void TextureAtlas::insert( QskBoxMaterial::TextureData* data )
|
|
{
|
|
const qreal width = data->size.width() * qskDevicePixelRatio;
|
|
const qreal height = data->size.height() * qskDevicePixelRatio;
|
|
|
|
const int bucketWidth = int( std::ceil( width ) );
|
|
const int bucketHeight = int( std::ceil( height ) );
|
|
|
|
const int boundsWidth = m_texture.width();
|
|
const int boundsHeight = m_texture.height();
|
|
|
|
if ( bucketWidth > boundsWidth || bucketHeight > boundsHeight )
|
|
{
|
|
qWarning() << "Requested texture is larger than maximum size";
|
|
return;
|
|
}
|
|
|
|
const auto freeRegion = QRegion( 0, 0, boundsWidth, boundsHeight ).subtracted( m_region );
|
|
for ( const auto& rect : freeRegion.rects() )
|
|
{
|
|
const QRect bounds( rect.x(), rect.y(), bucketWidth, bucketHeight );
|
|
if ( rect.contains( bounds ) )
|
|
{
|
|
m_region += bounds;
|
|
|
|
data->bounds = bounds;
|
|
data->coordinates = QRectF(
|
|
qreal( bounds.x() ) / boundsWidth,
|
|
qreal( bounds.y() ) / boundsHeight,
|
|
width / boundsWidth,
|
|
height / boundsHeight );
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
qWarning() << "Texture atlas is full. Check that the application is "
|
|
"releasing unused texture data.";
|
|
}
|
|
|
|
void TextureAtlas::remove( const QskBoxMaterial::TextureData* data )
|
|
{
|
|
m_region -= data->bounds;
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
static QPainterPath qskBorderPath( QMarginsF borders, QMarginsF marginsExtra,
|
|
const QskBoxOptions& options, bool inner )
|
|
{
|
|
qreal topLeftX = options.radius.topLeftX;
|
|
qreal topRightX = options.radius.topRightX;
|
|
qreal bottomRightX = options.radius.bottomRightX;
|
|
qreal bottomLeftX = options.radius.bottomLeftX;
|
|
qreal topLeftY = options.radius.topLeftY;
|
|
qreal topRightY = options.radius.topRightY;
|
|
qreal bottomRightY = options.radius.bottomRightY;
|
|
qreal bottomLeftY = options.radius.bottomLeftY;
|
|
|
|
const auto spacingX = marginsExtra.left() + marginsExtra.right();
|
|
const auto spacingY = marginsExtra.top() + marginsExtra.bottom();
|
|
|
|
auto width = borders.left() + std::max( topLeftX, bottomLeftX )
|
|
+ spacingX + borders.right() + std::max( topRightX, bottomRightX );
|
|
|
|
auto height = borders.top() + std::max( topLeftY, topRightY )
|
|
+ spacingY + borders.bottom() + std::max( bottomLeftY, bottomRightY );
|
|
|
|
if ( inner )
|
|
{
|
|
topLeftX = std::max( topLeftX - borders.left(), 0.0 );
|
|
topLeftY = std::max( topLeftY - borders.top(), 0.0 );
|
|
topRightX = std::max( topRightX - borders.right(), 0.0 );
|
|
topRightY = std::max( topRightY - borders.top(), 0.0 );
|
|
bottomLeftX = std::max( bottomLeftX - borders.left(), 0.0 );
|
|
bottomLeftY = std::max( bottomLeftY - borders.bottom(), 0.0 );
|
|
bottomRightX = std::max( bottomRightX - borders.right(), 0.0 );
|
|
bottomRightY = std::max( bottomRightY - borders.bottom(), 0.0 );
|
|
}
|
|
else
|
|
{
|
|
borders = QMarginsF();
|
|
}
|
|
|
|
QPainterPath path;
|
|
|
|
path.moveTo( borders.left(), borders.top() + topLeftY );
|
|
|
|
path.arcTo( borders.left(), borders.top(), topLeftX * 2, topLeftY * 2, 180, -90 );
|
|
path.lineTo( width - topRightX - borders.right(), path.currentPosition().y() );
|
|
|
|
path.arcTo( path.currentPosition().x() - topRightX, path.currentPosition().y(),
|
|
topRightX * 2, topRightY * 2, 90, -90 );
|
|
path.lineTo( path.currentPosition().x(), height - bottomRightY - borders.bottom() );
|
|
|
|
path.arcTo( path.currentPosition().x() - bottomRightX * 2,
|
|
path.currentPosition().y() - bottomRightY,
|
|
bottomRightX * 2, bottomRightY * 2, 0, -90 );
|
|
path.lineTo( borders.left() + bottomLeftX, path.currentPosition().y() );
|
|
|
|
path.arcTo( path.currentPosition().x() - bottomLeftX,
|
|
path.currentPosition().y() - bottomLeftY * 2,
|
|
bottomLeftX * 2, bottomLeftY * 2, -90, -90 );
|
|
path.closeSubpath();
|
|
|
|
return path;
|
|
}
|
|
|
|
static inline void swizzle( const QImage& inputImage, QImage& outputImage, int bit )
|
|
{
|
|
Q_ASSERT( inputImage.size() == outputImage.size() );
|
|
for ( int i = 0; i < inputImage.height(); ++i )
|
|
{
|
|
auto input = inputImage.scanLine(i);
|
|
auto output = outputImage.scanLine(i);
|
|
for ( int j = 0; j < inputImage.width(); ++j )
|
|
output[j * 4 + bit] = input[j];
|
|
}
|
|
}
|
|
|
|
QImage qskCreateImage( const QSize& size, const QVector< QPainterPath >& paths )
|
|
{
|
|
// Image to contain all layers
|
|
QImage textureImage( size, QImage::Format_RGBA8888 );
|
|
textureImage.fill( Qt::black );
|
|
|
|
// Image to render single channels
|
|
QImage channelImage( size, QImage::Format_Alpha8 );
|
|
channelImage.setDevicePixelRatio( qskDevicePixelRatio );
|
|
|
|
int currentColor = 0;
|
|
for ( int i = 0; i < paths.size(); i++ )
|
|
{
|
|
channelImage.fill( Qt::transparent );
|
|
|
|
QPainter painter( &channelImage );
|
|
|
|
if ( size.width() > qskDevicePixelRatio
|
|
|| size.height() > qskDevicePixelRatio )
|
|
{
|
|
painter.setRenderHint( QPainter::Antialiasing );
|
|
}
|
|
|
|
QBrush brush( Qt::black );
|
|
if ( i == 2 )
|
|
{
|
|
static QBrush shadowBrush;
|
|
if ( shadowBrush.style() == Qt::NoBrush )
|
|
{
|
|
QRadialGradient gradient( 0.5, 0.5, 0.5 );
|
|
gradient.setCoordinateMode( QGradient::ObjectBoundingMode );
|
|
gradient.setStops( { { 0.0, Qt::black }, { 1.0, Qt::transparent } } );
|
|
shadowBrush = gradient;
|
|
}
|
|
|
|
brush = shadowBrush;
|
|
}
|
|
|
|
painter.fillPath( paths[i], brush );
|
|
swizzle( channelImage, textureImage, currentColor++ );
|
|
|
|
Q_ASSERT(currentColor < 4);
|
|
}
|
|
|
|
return textureImage;
|
|
}
|
|
|
|
static QVector< QPainterPath > qskCreateTexturePaths(
|
|
const QskBoxOptions& options )
|
|
{
|
|
QVector< QPainterPath > paths;
|
|
|
|
const QMarginsF allMargins = options.unitedMargins();
|
|
const QMarginsF padding = options.padding();
|
|
|
|
const QMarginsF extra = allMargins - options.borders - options.shadows - padding;
|
|
|
|
const auto width = allMargins.left() + allMargins.right();
|
|
const auto height = allMargins.top() + allMargins.bottom();
|
|
|
|
const QSize size( std::ceil( width ), std::ceil( height ) );
|
|
const auto translateX = options.shadows.left() + ( size.width() - width ) * 0.5f;
|
|
const auto translateY = options.shadows.top() + ( size.height() - height ) * 0.5f;
|
|
|
|
{
|
|
auto outerPath = qskBorderPath( options.borders, extra, options, false );
|
|
outerPath = outerPath.translated( translateX, translateY );
|
|
|
|
paths.append( outerPath );
|
|
}
|
|
|
|
if ( !options.borders.isNull() )
|
|
{
|
|
auto innerPath = qskBorderPath( options.borders, extra, options, true );
|
|
innerPath = innerPath.translated( translateX, translateY );
|
|
|
|
paths.append( innerPath );
|
|
}
|
|
else
|
|
{
|
|
paths.prepend( QPainterPath() ); // make outerPath the innerPath
|
|
}
|
|
|
|
if ( !options.shadows.isNull() )
|
|
{
|
|
auto shadowPath = qskBorderPath(
|
|
options.borders + options.shadows, extra, options, false );
|
|
|
|
shadowPath = shadowPath.translated( translateX - options.shadows.left(),
|
|
translateY - options.shadows.top() );
|
|
|
|
paths.append( shadowPath );
|
|
}
|
|
|
|
return paths;
|
|
}
|
|
}
|
|
|
|
class QskBoxMaterialShader final : public QSGMaterialShader
|
|
{
|
|
public:
|
|
virtual const char* const* attributeNames() const override final
|
|
{
|
|
static char const* const attributeNames[] =
|
|
{
|
|
"position", "texture", "background", "foreground", nullptr
|
|
};
|
|
return attributeNames;
|
|
}
|
|
|
|
virtual void initialize() override final
|
|
{
|
|
qt_Matrix = program()->uniformLocation( "qt_Matrix" );
|
|
qt_Opacity = program()->uniformLocation( "qt_Opacity" );
|
|
}
|
|
|
|
virtual void updateState( const RenderState& state,
|
|
QSGMaterial* next, QSGMaterial* prev ) override final
|
|
{
|
|
Q_UNUSED( next );
|
|
if ( !prev )
|
|
qskAtlas->bindTexture();
|
|
|
|
if ( state.isOpacityDirty() )
|
|
{
|
|
program()->setUniformValue( qt_Opacity, state.opacity() );
|
|
}
|
|
|
|
if ( state.isMatrixDirty() )
|
|
{
|
|
program()->setUniformValue( qt_Matrix, state.combinedMatrix() );
|
|
}
|
|
}
|
|
|
|
virtual const char* vertexShader() const override final
|
|
{
|
|
return SHADER(
|
|
|
|
uniform highp mat4 qt_Matrix;
|
|
attribute highp vec4 position;
|
|
attribute highp vec2 texture;
|
|
attribute highp vec4 background;
|
|
attribute highp vec4 foreground;
|
|
varying highp vec2 texCoord;
|
|
varying lowp vec4 color0;
|
|
varying lowp vec4 color1;
|
|
varying lowp vec4 color2;
|
|
|
|
void main()
|
|
{
|
|
texCoord = texture;
|
|
color0 = foreground.bgra;
|
|
color1 = background.bgra;
|
|
color2 = foreground.bgra;
|
|
gl_Position = qt_Matrix * position;
|
|
}
|
|
);
|
|
}
|
|
|
|
virtual const char* fragmentShader() const override final
|
|
{
|
|
return SHADER(
|
|
|
|
varying lowp vec4 color0;
|
|
varying lowp vec4 color1;
|
|
varying lowp vec4 color2;
|
|
uniform lowp float qt_Opacity;
|
|
uniform sampler2D texture;
|
|
varying highp vec2 texCoord;
|
|
|
|
void main()
|
|
{
|
|
highp vec4 texel = texture2D( texture, texCoord );
|
|
highp vec4 effect = color2 * texel.r;
|
|
highp vec4 bg = color0 * texel.b;
|
|
highp vec4 fg = color1 * texel.g;
|
|
fg = fg + bg * ( 1.0 - fg.a );
|
|
gl_FragColor = ( fg + effect * ( 1.0 - fg.a ) ) * qt_Opacity;
|
|
}
|
|
);
|
|
}
|
|
|
|
private:
|
|
int qt_Matrix;
|
|
int qt_Opacity;
|
|
};
|
|
|
|
QskBoxMaterial::QskBoxMaterial() :
|
|
m_data( nullptr )
|
|
{
|
|
if ( qskDevicePixelRatio == 0 )
|
|
qskDevicePixelRatio = qGuiApp->devicePixelRatio();
|
|
|
|
setFlag( Blending );
|
|
}
|
|
|
|
QskBoxMaterial::~QskBoxMaterial()
|
|
{
|
|
releaseTexture();
|
|
}
|
|
|
|
bool QskBoxMaterial::isValid() const
|
|
{
|
|
return m_data != nullptr;
|
|
}
|
|
|
|
void QskBoxMaterial::setBoxOptions( const QskBoxOptions& options )
|
|
{
|
|
const auto key = options.metricsHash();
|
|
|
|
auto it = qskTextureCache.find( key );
|
|
if ( it != qskTextureCache.cend() )
|
|
{
|
|
if ( m_data == it->second )
|
|
return;
|
|
|
|
// De-ref and possibly release texture
|
|
releaseTexture();
|
|
|
|
m_data = it->second;
|
|
++m_data->referenceCount;
|
|
}
|
|
else
|
|
{
|
|
it = qskTextureCache.emplace( key, new TextureData ).first;
|
|
m_data = it->second;
|
|
|
|
const QVector< QPainterPath > paths = qskCreateTexturePaths( options );
|
|
|
|
const QMarginsF borders = options.unitedMargins();
|
|
|
|
m_data->size = QSizeF( borders.left() + borders.right(),
|
|
borders.top() + borders.bottom() );
|
|
|
|
qskAtlas->insert( m_data );
|
|
|
|
const QImage textureImage = qskCreateImage( m_data->bounds.size(), paths );
|
|
|
|
qskAtlas->bindTexture();
|
|
|
|
auto gl = QOpenGLContext::currentContext()->functions();
|
|
|
|
gl->glTexSubImage2D( GL_TEXTURE_2D, 0,
|
|
m_data->bounds.x(), m_data->bounds.y(),
|
|
m_data->bounds.width(), m_data->bounds.height(),
|
|
QOpenGLTexture::BGRA, GL_UNSIGNED_BYTE, textureImage.constBits() );
|
|
|
|
qskAtlas->releaseTexture();
|
|
}
|
|
}
|
|
|
|
QSizeF QskBoxMaterial::textureSize() const
|
|
{
|
|
return m_data ? m_data->size : QSizeF();
|
|
}
|
|
|
|
QRectF QskBoxMaterial::textureCoordinates() const
|
|
{
|
|
return m_data ? m_data->coordinates : QRectF();
|
|
}
|
|
|
|
int QskBoxMaterial::compare( const QSGMaterial* other ) const
|
|
{
|
|
if ( this == other )
|
|
return 0;
|
|
|
|
auto material = static_cast< decltype( this ) >( other );
|
|
if ( textureCoordinates() == material->textureCoordinates() )
|
|
return 0;
|
|
|
|
return QSGMaterial::compare( other );
|
|
}
|
|
|
|
void QskBoxMaterial::releaseTexture()
|
|
{
|
|
if ( m_data == nullptr || --m_data->referenceCount > 0 )
|
|
return;
|
|
|
|
qskTextureCache.erase( qskTextureCache.findByData( m_data ) );
|
|
qskAtlas->remove( m_data );
|
|
|
|
delete m_data;
|
|
m_data = nullptr;
|
|
}
|
|
|
|
QSGMaterialShader* QskBoxMaterial::createShader() const
|
|
{
|
|
return new QskBoxMaterialShader;
|
|
}
|
|
|
|
QSGMaterialType* QskBoxMaterial::type() const
|
|
{
|
|
static QSGMaterialType materialType;
|
|
return &materialType;
|
|
}
|