diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,10 +24,16 @@ scenepositionattached.cpp mnemonicattached.cpp wheelhandler.cpp + shadowedrectangle.cpp + scenegraph/shadowedrectanglenode.cpp + scenegraph/shadowedrectanglematerial.cpp + scenegraph/shadowedborderrectanglematerial.cpp ${kirigami_QM_LOADER} ${KIRIGAMI_STATIC_FILES} ) +qt5_add_resources(SHADERS scenegraph/shaders.qrc) + add_subdirectory(libkirigami) if(STATIC_LIBRARY) @@ -55,7 +61,7 @@ endif(STATIC_LIBRARY) -add_library(kirigamiplugin ${kirigami_SRCS} ${RESOURCES}) +add_library(kirigamiplugin ${kirigami_SRCS} ${RESOURCES} ${SHADERS}) if(STATIC_LIBRARY) SET_TARGET_PROPERTIES(kirigamiplugin PROPERTIES diff --git a/src/kirigamiplugin.cpp b/src/kirigamiplugin.cpp --- a/src/kirigamiplugin.cpp +++ b/src/kirigamiplugin.cpp @@ -17,6 +17,7 @@ #include "pagepool.h" #include "scenepositionattached.h" #include "wheelhandler.h" +#include "shadowedrectangle.h" #include #include @@ -235,6 +236,11 @@ //TODO: remove qmlRegisterType(componentUrl(QStringLiteral("SwipeListItem2.qml")), uri, 2, 11, "SwipeListItem2"); + // 2.12 + qmlRegisterType(uri, 2, 12, "ShadowedRectangle"); + qmlRegisterUncreatableType(uri, 2, 12, "BorderGroup", QStringLiteral("Used as grouped property")); + qmlRegisterUncreatableType(uri, 2, 12, "ShadowGroup", QStringLiteral("Used as grouped property")); + qmlProtectModule(uri, 2); } diff --git a/src/scenegraph/header_desktop.glsl b/src/scenegraph/header_desktop.glsl new file mode 100644 --- /dev/null +++ b/src/scenegraph/header_desktop.glsl @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// This file contains common directives needed for the shaders to work. +// It is included as the very first bit in the shader. +// Important: If a specific GLSL version is needed, it should be set in this +// file. + +// This file is intended for desktop OpenGL version 2.1 or greater. + +#version 120 + +#ifndef lowp + #define lowp +#endif + +#ifndef mediump + #define mediump +#endif + +#ifndef highp + #define highp mediump +#endif diff --git a/src/scenegraph/header_desktop_core.glsl b/src/scenegraph/header_desktop_core.glsl new file mode 100644 --- /dev/null +++ b/src/scenegraph/header_desktop_core.glsl @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// This file contains common directives needed for the shaders to work. +// It is included as the very first bit in the shader. +// Important: If a specific GLSL version is needed, it should be set in this +// file. + +// This file is intended for desktop OpenGL version 4.5 or greater. + +#version 450 diff --git a/src/scenegraph/header_es.glsl b/src/scenegraph/header_es.glsl new file mode 100644 --- /dev/null +++ b/src/scenegraph/header_es.glsl @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// This file contains common directives needed for the shaders to work. +// It is included as the very first bit in the shader. +// Important: If a specific GLSL version is needed, it should be set in this +// file. + +// This file is intended for OpenGLES version 2.0 or greater. + +#version 100 +#extension GL_OES_standard_derivatives : enable + diff --git a/src/scenegraph/shaders.qrc b/src/scenegraph/shaders.qrc new file mode 100644 --- /dev/null +++ b/src/scenegraph/shaders.qrc @@ -0,0 +1,15 @@ + + + + header_es.glsl + header_desktop.glsl + header_desktop_core.glsl + shadowedrectangle.vert + shadowedrectangle_core.vert + shadowedrectangle.frag + shadowedrectangle_core.frag + shadowedborderrectangle.frag + shadowedborderrectangle_core.frag + + + diff --git a/src/scenegraph/shadowedborderrectangle.frag b/src/scenegraph/shadowedborderrectangle.frag new file mode 100644 --- /dev/null +++ b/src/scenegraph/shadowedborderrectangle.frag @@ -0,0 +1,94 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#line 7 + +// This is based on the 2D SDF functions provided by Inigo Quilez: +// https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm + +// This shader renders a rectangle with rounded corners and a shadow below it. +// In addition it renders a border around it. + +uniform lowp float opacity; +uniform lowp float size; +uniform lowp float radius; +uniform lowp vec4 color; +uniform lowp vec4 shadowColor; +uniform lowp vec2 offset; +uniform lowp vec2 aspect; +uniform lowp float borderWidth; +uniform lowp vec4 borderColor; + +varying lowp vec2 uv; + +const lowp float minimum_shadow_radius = 0.05; +const lowp float smoothing = 0.001; + +// Calculate the distance to a rectangle with rounded corners. +// \param point The point to calculate the distance of. +// \param rect The rectangle to calculate the distance of. +// \param translation The amount of translation to apply to the rectangle. +// \param radius A vec4 with the radius of each corner. Order is top right, bottom right, top left, bottom left. +lowp float sdf_rounded_rectangle(in lowp vec2 point, in lowp vec2 rect, in lowp vec2 translation, in lowp vec4 radius) +{ + radius.xy = (point.x > 0.0) ? radius.xy : radius.zw; + radius.x = (point.y > 0.0) ? radius.x : radius.y; + lowp vec2 d = abs(point - translation) - rect + radius.x; + return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - radius.x; +} + +// Render an sdf value into a color. +lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor, in lowp float sdfAlpha) +{ + lowp float g = fwidth(sdf); + return mix(sourceColor, sdfColor, sdfAlpha * (1.0 - smoothstep(smoothing - g, smoothing + g, sdf))); +} + +void main() +{ + // Scaling factor that is the inverse of the amount of scaling applied to the geometry. + lowp float inverse_scale = 1.0 / (1.0 + size + length(offset) * 2.0 + borderWidth * 2.0); + + // Correction factor to round the corners of a larger shadow. + // We want to account for size in regards to shadow radius, so that a larger shadow is + // more rounded, but only if we are not already rounding the corners due to corner radius. + lowp float size_factor = 0.5 * (minimum_shadow_radius / max(radius, minimum_shadow_radius)); + + lowp float shadow_radius = radius + size * size_factor; + + lowp vec4 col = vec4(0.0); + + // Calculate the shadow's distance field. + lowp float shadow = sdf_rounded_rectangle(uv, (aspect + borderWidth) * inverse_scale, offset * inverse_scale, vec4(shadow_radius * inverse_scale)); + // Render it, interpolating the color over the distance. + col = mix(col, shadowColor * sign(size), shadowColor.a * (1.0 - smoothstep(-size * 0.5, size * 0.5, shadow))); + + // Scale corrected corner radius + lowp vec4 corner_radius = vec4(radius * inverse_scale); + + // Calculate the outer rectangle distance field. + lowp float outer_rect = sdf_rounded_rectangle(uv, (aspect + borderWidth) * inverse_scale, vec2(0.0), corner_radius); + + // First, remove anything that was rendered by the shadow if it is inside the rectangle. + // This allows us to use colors with alpha without rendering artifacts. + col = sdf_render(outer_rect, col, vec4(0.0), 1.0); + + // Then, render it again but this time with the proper color and properly alpha blended. + col = sdf_render(outer_rect, col, borderColor, borderColor.a); + + // Calculate the inner rectangle distance field. + // This uses a reduced corner radius because the inner corners need to be smaller than the outer corners. + lowp vec4 inner_radius = vec4((radius - borderWidth) * inverse_scale); + lowp float inner_rect = sdf_rounded_rectangle(uv, (aspect - borderWidth) * inverse_scale, vec2(0.0), inner_radius); + + // Like above, but this time cut out the inner rectangle. + col = sdf_render(inner_rect, col, vec4(0.0), 1.0); + + // Finally, render the inner rectangle. + col = sdf_render(inner_rect, col, color, color.a); + + gl_FragColor = col * opacity; +} diff --git a/src/scenegraph/shadowedborderrectangle_core.frag b/src/scenegraph/shadowedborderrectangle_core.frag new file mode 100644 --- /dev/null +++ b/src/scenegraph/shadowedborderrectangle_core.frag @@ -0,0 +1,94 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#line 7 + +// This is based on the 2D SDF functions provided by Inigo Quilez: +// https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm + +// This shader renders a rectangle with rounded corners and a shadow below it. +// In addition it renders a border around it. + +uniform lowp float opacity; +uniform lowp float size; +uniform lowp float radius; +uniform lowp vec4 color; +uniform lowp vec4 shadowColor; +uniform lowp vec2 offset; +uniform lowp vec2 aspect; +uniform lowp float borderWidth; +uniform lowp vec4 borderColor; + +in lowp vec2 uv; + +const lowp float minimum_shadow_radius = 0.05; +const lowp float smoothing = 0.001; + +// Calculate the distance to a rectangle with rounded corners. +// \param point The point to calculate the distance of. +// \param rect The rectangle to calculate the distance of. +// \param translation The amount of translation to apply to the rectangle. +// \param radius A vec4 with the radius of each corner. Order is top right, bottom right, top left, bottom left. +lowp float sdf_rounded_rectangle(in lowp vec2 point, in lowp vec2 rect, in lowp vec2 translation, in lowp vec4 radius) +{ + radius.xy = (point.x > 0.0) ? radius.xy : radius.zw; + radius.x = (point.y > 0.0) ? radius.x : radius.y; + lowp vec2 d = abs(point - translation) - rect + radius.x; + return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - radius.x; +} + +// Render an sdf value into a color. +lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor, in lowp float sdfAlpha) +{ + lowp float g = fwidth(sdf); + return mix(sourceColor, sdfColor, sdfAlpha * (1.0 - smoothstep(smoothing - g, smoothing + g, sdf))); +} + +void main() +{ + // Scaling factor that is the inverse of the amount of scaling applied to the geometry. + lowp float inverse_scale = 1.0 / (1.0 + size + length(offset) * 2.0 + borderWidth * 2.0); + + // Correction factor to round the corners of a larger shadow. + // We want to account for size in regards to shadow radius, so that a larger shadow is + // more rounded, but only if we are not already rounding the corners due to corner radius. + lowp float size_factor = 0.5 * (minimum_shadow_radius / max(radius, minimum_shadow_radius)); + + lowp float shadow_radius = radius + size * size_factor; + + lowp vec4 col = vec4(0.0); + + // Calculate the shadow's distance field. + lowp float shadow = sdf_rounded_rectangle(uv, (aspect + borderWidth) * inverse_scale, offset * inverse_scale, vec4(shadow_radius * inverse_scale)); + // Render it, interpolating the color over the distance. + col = mix(col, shadowColor * sign(size), shadowColor.a * (1.0 - smoothstep(-size * 0.5, size * 0.5, shadow))); + + // Scale corrected corner radius + lowp vec4 corner_radius = vec4(radius * inverse_scale); + + // Calculate the outer rectangle distance field. + lowp float outer_rect = sdf_rounded_rectangle(uv, (aspect + borderWidth) * inverse_scale, vec2(0.0), corner_radius); + + // First, remove anything that was rendered by the shadow if it is inside the rectangle. + // This allows us to use colors with alpha without rendering artifacts. + col = sdf_render(outer_rect, col, vec4(0.0), 1.0); + + // Then, render it again but this time with the proper color and properly alpha blended. + col = sdf_render(outer_rect, col, borderColor, borderColor.a); + + // Calculate the inner rectangle distance field. + // This uses a reduced corner radius because the inner corners need to be smaller than the outer corners. + lowp vec4 inner_radius = vec4((radius - borderWidth) * inverse_scale); + lowp float inner_rect = sdf_rounded_rectangle(uv, (aspect - borderWidth) * inverse_scale, vec2(0.0), inner_radius); + + // Like above, but this time cut out the inner rectangle. + col = sdf_render(inner_rect, col, vec4(0.0), 1.0); + + // Finally, render the inner rectangle. + col = sdf_render(inner_rect, col, color, color.a); + + gl_FragColor = col * opacity; +} diff --git a/src/scenegraph/shadowedborderrectanglematerial.h b/src/scenegraph/shadowedborderrectanglematerial.h new file mode 100644 --- /dev/null +++ b/src/scenegraph/shadowedborderrectanglematerial.h @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include "shadowedrectanglematerial.h" + +/** + * A material rendering a rectangle with a shadow and a border. + * + * This material uses a distance field shader to render a rectangle with a + * shadow below it, optionally with rounded corners and a border. + */ +class ShadowedBorderRectangleMaterial : public ShadowedRectangleMaterial +{ +public: + ShadowedBorderRectangleMaterial(); + + QSGMaterialShader* createShader() const override; + QSGMaterialType* type() const override; + int compare(const QSGMaterial* other) const override; + + float borderWidth = 0.0; + QColor borderColor = Qt::black; + + static QSGMaterialType staticType; +}; + +class ShadowedBorderRectangleShader : public ShadowedRectangleShader +{ +public: + ShadowedBorderRectangleShader(); + + void initialize() override; + void updateState(const QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; + +private: + int m_borderWidthLocation = -1; + int m_borderColorLocation = -1; +}; diff --git a/src/scenegraph/shadowedborderrectanglematerial.cpp b/src/scenegraph/shadowedborderrectanglematerial.cpp new file mode 100644 --- /dev/null +++ b/src/scenegraph/shadowedborderrectanglematerial.cpp @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "shadowedborderrectanglematerial.h" + +#include + +QSGMaterialType ShadowedBorderRectangleMaterial::staticType; + +ShadowedBorderRectangleMaterial::ShadowedBorderRectangleMaterial() +{ + setFlag(QSGMaterial::Blending, true); +} + +QSGMaterialShader* ShadowedBorderRectangleMaterial::createShader() const +{ + return new ShadowedBorderRectangleShader{}; +} + +QSGMaterialType* ShadowedBorderRectangleMaterial::type() const +{ + return &staticType; +} + +int ShadowedBorderRectangleMaterial::compare(const QSGMaterial *other) const +{ + auto material = static_cast(other); + + auto result = ShadowedRectangleMaterial::compare(other); + if (result == 0 + && material->borderColor == borderColor + && qFuzzyCompare(material->borderWidth, borderWidth) + ) { + return 0; + } + + return result; +} + +ShadowedBorderRectangleShader::ShadowedBorderRectangleShader() +{ + auto header = QOpenGLContext::currentContext()->isOpenGLES() ? QStringLiteral("header_es.glsl") : QStringLiteral("header_desktop.glsl"); + + auto shaderRoot = QStringLiteral(":/org/kde/kirigami/shaders/"); + + setShaderSourceFiles(QOpenGLShader::Fragment, { + shaderRoot + header, + shaderRoot + QStringLiteral("shadowedborderrectangle.frag") + }); +} + +void ShadowedBorderRectangleShader::initialize() +{ + ShadowedRectangleShader::initialize(); + m_borderWidthLocation = program()->uniformLocation("borderWidth"); + m_borderColorLocation = program()->uniformLocation("borderColor"); +} + +void ShadowedBorderRectangleShader::updateState(const QSGMaterialShader::RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) +{ + ShadowedRectangleShader::updateState(state, newMaterial, oldMaterial); + + auto p = program(); + + if (!oldMaterial || newMaterial->compare(oldMaterial) != 0 || state.isCachedMaterialDataDirty()) { + auto material = static_cast(newMaterial); + p->setUniformValue(m_borderWidthLocation, material->borderWidth); + p->setUniformValue(m_borderColorLocation, material->borderColor); + } +} diff --git a/src/scenegraph/shadowedrectangle.frag b/src/scenegraph/shadowedrectangle.frag new file mode 100644 --- /dev/null +++ b/src/scenegraph/shadowedrectangle.frag @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// This is based on the 2D SDF functions provided by Inigo Quilez: +// https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm + +// This shader renders a rectangle with rounded corners and a shadow below it. + +uniform lowp float opacity; +uniform lowp float size; +uniform lowp float radius; +uniform lowp vec4 color; +uniform lowp vec4 shadowColor; +uniform lowp vec2 offset; +uniform lowp vec2 aspect; + +varying lowp vec2 uv; + +const lowp float minimum_shadow_radius = 0.05; + +// Calculate the distance to a rectangle with rounded corners. +// \param point The point to calculate the distance of. +// \param rect The rectangle to calculate the distance of. +// \param translation The amount of translation to apply to the rectangle. +// \param radius A vec4 with the radius of each corner. Order is top right, bottom right, top left, bottom left. +lowp float sdf_rounded_rectangle(in lowp vec2 point, in lowp vec2 rect, in lowp vec2 translation, in lowp vec4 radius) +{ + radius.xy = (point.x > 0.0) ? radius.xy : radius.zw; + radius.x = (point.y > 0.0) ? radius.x : radius.y; + lowp vec2 d = abs(point - translation) - rect + radius.x; + return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - radius.x; +} + +void main() +{ + // Scaling factor that is the inverse of the amount of scaling applied to the geometry. + lowp float inverse_scale = 1.0 / (1.0 + size + length(offset) * 2.0); + + // Correction factor to round the corners of a larger shadow. + // We want to account for size in regards to shadow radius, so that a larger shadow is + // more rounded, but only if we are not already rounding the corners due to corner radius. + lowp float size_factor = 0.5 * (minimum_shadow_radius / max(radius, minimum_shadow_radius)); + + lowp float shadowRadius = radius + size * size_factor; + + lowp vec4 col = vec4(0.0); + + // Calculate the shadow's distance field. + lowp float shadow = sdf_rounded_rectangle(uv, aspect * inverse_scale, offset * inverse_scale, vec4(shadowRadius * inverse_scale)); + // Render it, interpolating the color over the distance. + col = mix(col, shadowColor * sign(size), shadowColor.a * (1.0 - smoothstep(-size * 0.5, size * 0.5, shadow))); + + // Calculate the main rectangle distance field. + lowp float rect = sdf_rounded_rectangle(uv, aspect * inverse_scale, vec2(0.0), vec4(radius * inverse_scale)); + + lowp float g = fwidth(rect); + + // First, remove anything that was rendered by the shadow if it is inside the rectangle. + // This allows us to use colors with alpha without rendering artifacts. + col = mix(col, vec4(0.0), 1.0 - smoothstep(0.001 - g, 0.001 + g, rect)); + + // Then, render it again but this time with the proper color and properly alpha blended. + col = mix(col, color, color.a * (1.0 - smoothstep(0.001 - g, 0.001 + g, rect))); + + gl_FragColor = col * opacity; +} diff --git a/src/scenegraph/shadowedrectangle.vert b/src/scenegraph/shadowedrectangle.vert new file mode 100644 --- /dev/null +++ b/src/scenegraph/shadowedrectangle.vert @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +uniform highp mat4 matrix; +uniform lowp vec2 aspect; +uniform lowp vec2 offset; + +attribute highp vec4 in_vertex; +attribute mediump vec2 in_uv; + +varying mediump vec2 uv; + +void main() { + uv = (-1.0 + 2.0 * in_uv) * aspect; + gl_Position = matrix * in_vertex; +} diff --git a/src/scenegraph/shadowedrectangle_core.frag b/src/scenegraph/shadowedrectangle_core.frag new file mode 100644 --- /dev/null +++ b/src/scenegraph/shadowedrectangle_core.frag @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// This is based on the 2D SDF functions provided by Inigo Quilez: +// https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm + +// This shader renders a rectangle with rounded corners and a shadow below it. + +uniform lowp float opacity; +uniform lowp float size; +uniform lowp float radius; +uniform lowp vec4 color; +uniform lowp vec4 shadowColor; +uniform lowp vec2 offset; +uniform lowp vec2 aspect; + +in lowp vec2 uv; + +const lowp float minimum_shadow_radius = 0.05; + +// Calculate the distance to a rectangle with rounded corners. +// \param point The point to calculate the distance of. +// \param rect The rectangle to calculate the distance of. +// \param translation The amount of translation to apply to the rectangle. +// \param radius A vec4 with the radius of each corner. Order is top right, bottom right, top left, bottom left. +lowp float sdf_rounded_rectangle(in lowp vec2 point, in lowp vec2 rect, in lowp vec2 translation, in lowp vec4 radius) +{ + radius.xy = (point.x > 0.0) ? radius.xy : radius.zw; + radius.x = (point.y > 0.0) ? radius.x : radius.y; + lowp vec2 d = abs(point - translation) - rect + radius.x; + return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - radius.x; +} + +void main() +{ + // Scaling factor that is the inverse of the amount of scaling applied to the geometry. + lowp float inverse_scale = 1.0 / (1.0 + size + length(offset) * 2.0); + + // Correction factor to round the corners of a larger shadow. + // We want to account for size in regards to shadow radius, so that a larger shadow is + // more rounded, but only if we are not already rounding the corners due to corner radius. + lowp float size_factor = 0.5 * (minimum_shadow_radius / max(radius, minimum_shadow_radius)); + + lowp float shadowRadius = radius + size * size_factor; + + lowp vec4 col = vec4(0.0); + + // Calculate the shadow's distance field. + lowp float shadow = sdf_rounded_rectangle(uv, aspect * inverse_scale, offset * inverse_scale, vec4(shadowRadius * inverse_scale)); + // Render it, interpolating the color over the distance. + col = mix(col, shadowColor * sign(size), shadowColor.a * (1.0 - smoothstep(-size * 0.5, size * 0.5, shadow))); + + // Calculate the main rectangle distance field. + lowp float rect = sdf_rounded_rectangle(uv, aspect * inverse_scale, vec2(0.0), vec4(radius * inverse_scale)); + + lowp float g = fwidth(rect); + + // First, remove anything that was rendered by the shadow if it is inside the rectangle. + // This allows us to use colors with alpha without rendering artifacts. + col = mix(col, vec4(0.0), 1.0 - smoothstep(0.001 - g, 0.001 + g, rect)); + + // Then, render it again but this time with the proper color and properly alpha blended. + col = mix(col, color, color.a * (1.0 - smoothstep(0.001 - g, 0.001 + g, rect))); + + gl_FragColor = col * opacity; +} diff --git a/src/scenegraph/shadowedrectangle_core.vert b/src/scenegraph/shadowedrectangle_core.vert new file mode 100644 --- /dev/null +++ b/src/scenegraph/shadowedrectangle_core.vert @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +uniform mat4 matrix; +uniform vec2 aspect; +uniform vec2 offset; + +in vec4 in_vertex; +in vec2 in_uv; + +out vec2 uv; + +void main() { + uv = (-1.0 + 2.0 * in_uv) * aspect; + gl_Position = matrix * in_vertex; +} diff --git a/src/scenegraph/shadowedrectanglematerial.h b/src/scenegraph/shadowedrectanglematerial.h new file mode 100644 --- /dev/null +++ b/src/scenegraph/shadowedrectanglematerial.h @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include + +/** + * A material rendering a rectangle with a shadow. + * + * This material uses a distance field shader to render a rectangle with a + * shadow below it, optionally with rounded corners. + */ +class ShadowedRectangleMaterial : public QSGMaterial +{ +public: + ShadowedRectangleMaterial(); + + QSGMaterialShader* createShader() const override; + QSGMaterialType* type() const override; + int compare(const QSGMaterial* other) const override; + + QVector2D aspect = QVector2D{1.0, 1.0}; + float size = 0.0; + float radius = 0.0; + QColor color = Qt::white; + QColor shadowColor = Qt::black; + QVector2D offset; + + static QSGMaterialType staticType; +}; + +class ShadowedRectangleShader : public QSGMaterialShader +{ +public: + ShadowedRectangleShader(); + + char const *const *attributeNames() const override; + + void initialize() override; + void updateState(const QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; + +private: + int m_matrixLocation = -1; + int m_opacityLocation = -1; + int m_aspectLocation = -1; + int m_sizeLocation = -1; + int m_radiusLocation = -1; + int m_colorLocation = -1; + int m_shadowColorLocation = -1; + int m_offsetLocation = -1; +}; diff --git a/src/scenegraph/shadowedrectanglematerial.cpp b/src/scenegraph/shadowedrectanglematerial.cpp new file mode 100644 --- /dev/null +++ b/src/scenegraph/shadowedrectanglematerial.cpp @@ -0,0 +1,101 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "shadowedrectanglematerial.h" + +#include + +QSGMaterialType ShadowedRectangleMaterial::staticType; + +ShadowedRectangleMaterial::ShadowedRectangleMaterial() +{ + setFlag(QSGMaterial::Blending, true); +} + +QSGMaterialShader* ShadowedRectangleMaterial::createShader() const +{ + return new ShadowedRectangleShader{}; +} + +QSGMaterialType* ShadowedRectangleMaterial::type() const +{ + return &staticType; +} + +int ShadowedRectangleMaterial::compare(const QSGMaterial *other) const +{ + auto material = static_cast(other); + + if (material->color == color + && material->shadowColor == shadowColor + && material->offset == offset + && material->aspect == aspect + && qFuzzyCompare(material->size, size) + && qFuzzyCompare(material->radius, radius)) { + return 0; + } + + return QSGMaterial::compare(other); +} + +ShadowedRectangleShader::ShadowedRectangleShader() +{ + auto header = QOpenGLContext::currentContext()->isOpenGLES() ? QStringLiteral("header_es.glsl") : QStringLiteral("header_desktop.glsl"); + + auto shaderRoot = QStringLiteral(":/org/kde/kirigami/shaders/"); + + setShaderSourceFiles(QOpenGLShader::Vertex, { + shaderRoot + header, + shaderRoot + QStringLiteral("shadowedrectangle.vert") + }); + + setShaderSourceFiles(QOpenGLShader::Fragment, { + shaderRoot + header, + shaderRoot + QStringLiteral("shadowedrectangle.frag") + }); +} + +const char *const * ShadowedRectangleShader::attributeNames() const +{ + static char const *const names[] = {"in_vertex", "in_uv", nullptr}; + return names; +} + +void ShadowedRectangleShader::initialize() +{ + QSGMaterialShader::initialize(); + m_matrixLocation = program()->uniformLocation("matrix"); + m_aspectLocation = program()->uniformLocation("aspect"); + m_opacityLocation = program()->uniformLocation("opacity"); + m_sizeLocation = program()->uniformLocation("size"); + m_radiusLocation = program()->uniformLocation("radius"); + m_colorLocation = program()->uniformLocation("color"); + m_shadowColorLocation = program()->uniformLocation("shadowColor"); + m_offsetLocation = program()->uniformLocation("offset"); +} + +void ShadowedRectangleShader::updateState(const QSGMaterialShader::RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) +{ + auto p = program(); + + if (state.isMatrixDirty()) { + p->setUniformValue(m_matrixLocation, state.combinedMatrix()); + } + + if (state.isOpacityDirty()) { + p->setUniformValue(m_opacityLocation, state.opacity()); + } + + if (!oldMaterial || newMaterial->compare(oldMaterial) != 0 || state.isCachedMaterialDataDirty()) { + auto material = static_cast(newMaterial); + p->setUniformValue(m_aspectLocation, material->aspect); + p->setUniformValue(m_sizeLocation, material->size); + p->setUniformValue(m_radiusLocation, material->radius); + p->setUniformValue(m_colorLocation, material->color); + p->setUniformValue(m_shadowColorLocation, material->shadowColor); + p->setUniformValue(m_offsetLocation, material->offset); + } +} diff --git a/src/scenegraph/shadowedrectanglenode.h b/src/scenegraph/shadowedrectanglenode.h new file mode 100644 --- /dev/null +++ b/src/scenegraph/shadowedrectanglenode.h @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include + +class ShadowedRectangleMaterial; + +/** + * Scene graph node for a shadowed rectangle. + * + * This node will set up the geometry and materials for a shadowed rectangle, + * optionally with rounded corners. + * + * \note You must call updateGeometry() after setting properties of this node, + * otherwise the node's state will not correctly reflect all the properties. + * + * \sa ShadowedRectangle + */ +class ShadowedRectangleNode : public QSGGeometryNode +{ +public: + ShadowedRectangleNode(); + + /** + * Set the width of the border. + * + * Note that this will switch between a material with or without border. + * This means this needs to be called before any other setters. + */ + void setBorderWidth(qreal width); + + void setRect(const QRectF &rect); + void setSize(qreal size); + void setRadius(qreal radius); + void setColor(const QColor &color); + void setShadowColor(const QColor &color); + void setOffset(const QVector2D &offset); + void setBorderColor(const QColor &color); + + /** + * Update the geometry for this node. + * + * This is done as an explicit step to avoid the geometry being recreated + * multiple times while updating properties. + */ + void updateGeometry(); + +private: + QSGGeometry *m_geometry; + ShadowedRectangleMaterial *m_material; + + QRectF m_rect; + qreal m_size = 0.0; + qreal m_radius = 0.0; + QVector2D m_offset = QVector2D{0.0, 0.0}; + QVector2D m_aspect = QVector2D{1.0, 1.0}; + qreal m_borderWidth = 0.0; + QColor m_borderColor; +}; diff --git a/src/scenegraph/shadowedrectanglenode.cpp b/src/scenegraph/shadowedrectanglenode.cpp new file mode 100644 --- /dev/null +++ b/src/scenegraph/shadowedrectanglenode.cpp @@ -0,0 +1,175 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "shadowedrectanglenode.h" +#include "shadowedrectanglematerial.h" +#include "shadowedborderrectanglematerial.h" + +QColor premultiply(const QColor &color) +{ + return QColor::fromRgbF( + color.redF() * color.alphaF(), + color.greenF() * color.greenF(), + color.blueF() * color.blueF(), + color.alphaF() + ); +} + +ShadowedRectangleNode::ShadowedRectangleNode() +{ + m_geometry = new QSGGeometry{QSGGeometry::defaultAttributes_TexturedPoint2D(), 4}; + setGeometry(m_geometry); + + m_material = new ShadowedRectangleMaterial{}; + setMaterial(m_material); + + setFlags(QSGNode::OwnsGeometry | QSGNode::OwnsMaterial); +} + +void ShadowedRectangleNode::setRect(const QRectF& rect) +{ + if (rect == m_rect) { + return; + } + + m_rect = rect; + + QVector2D newAspect{1.0, 1.0}; + if (m_rect.width() >= m_rect.height()) { + newAspect.setX(m_rect.width() / m_rect.height()); + } else { + newAspect.setY(m_rect.height() / m_rect.width()); + } + + if (m_material->aspect != newAspect) { + m_material->aspect = newAspect; + markDirty(QSGNode::DirtyMaterial); + m_aspect = newAspect; + } +} + +void ShadowedRectangleNode::setSize(qreal size) +{ + auto minDimension = std::min(m_rect.width(), m_rect.height()); + float uniformSize = (size / minDimension) * 2.0; + + if (!qFuzzyCompare(m_material->size, uniformSize)) { + m_material->size = uniformSize; + markDirty(QSGNode::DirtyMaterial); + m_size = size; + } +} + +void ShadowedRectangleNode::setRadius(qreal radius) +{ + auto minDimension = std::min(m_rect.width(), m_rect.height()); + float uniformRadius = radius * 2.0 / minDimension; + + if (!qFuzzyCompare(m_material->radius, uniformRadius)) { + m_material->radius = std::min(uniformRadius, 1.0f); + markDirty(QSGNode::DirtyMaterial); + m_radius = radius; + } +} + +void ShadowedRectangleNode::setColor(const QColor &color) +{ + auto premultiplied = premultiply(color); + if (m_material->color != premultiplied) { + m_material->color = premultiplied; + markDirty(QSGNode::DirtyMaterial); + } +} + +void ShadowedRectangleNode::setShadowColor(const QColor& color) +{ + auto premultiplied = premultiply(color); + if (m_material->shadowColor != premultiplied) { + m_material->shadowColor = premultiplied; + markDirty(QSGNode::DirtyMaterial); + } +} + +void ShadowedRectangleNode::setOffset(const QVector2D& offset) +{ + auto minDimension = std::min(m_rect.width(), m_rect.height()); + auto uniformOffset = offset / minDimension; + + if (m_material->offset != uniformOffset) { + m_material->offset = uniformOffset; + markDirty(QSGNode::DirtyMaterial); + m_offset = offset; + } +} + +void ShadowedRectangleNode::setBorderWidth(qreal width) +{ + // We can achieve more performant shaders by splitting the two into separate + // shaders. This requires separating the materials as well. So when + // borderWidth is increased to something where the border should be visible, + // switch to the with-border material. Otherwise use the no-border version. + + if (qFuzzyIsNull(width)) { + if (m_material->type() == &ShadowedBorderRectangleMaterial::staticType) { + auto newMaterial = new ShadowedRectangleMaterial(); + setMaterial(newMaterial); + m_material = newMaterial; + m_borderWidth = width; + m_rect = QRectF{}; + markDirty(QSGNode::DirtyMaterial); + } + return; + } else { + if (m_material->type() == &ShadowedRectangleMaterial::staticType) { + auto newMaterial = new ShadowedBorderRectangleMaterial(); + setMaterial(newMaterial); + m_material = newMaterial; + m_rect = QRectF{}; + markDirty(QSGNode::DirtyMaterial); + } + } + + auto minDimension = std::min(m_rect.width(), m_rect.height()); + float uniformBorderWidth = width / minDimension; + + auto borderMaterial = static_cast(m_material); + if (!qFuzzyCompare(borderMaterial->borderWidth, uniformBorderWidth)) { + borderMaterial->borderWidth = uniformBorderWidth; + markDirty(QSGNode::DirtyMaterial); + m_borderWidth = width; + } +} + +void ShadowedRectangleNode::setBorderColor(const QColor& color) +{ + if (m_material->type() != &ShadowedBorderRectangleMaterial::staticType) { + return; + } + + auto borderMaterial = static_cast(m_material); + auto premultiplied = premultiply(color); + if (borderMaterial->borderColor != premultiplied) { + borderMaterial->borderColor = premultiplied; + markDirty(QSGNode::DirtyMaterial); + } +} + +void ShadowedRectangleNode::updateGeometry() +{ + auto rect = m_rect.adjusted(-m_size * m_aspect.x(), -m_size * m_aspect.y(), + m_size * m_aspect.x(), m_size * m_aspect.y()); + + auto offsetLength = m_offset.length(); + + rect = rect.adjusted(-offsetLength * m_aspect.x(), -offsetLength * m_aspect.y(), + offsetLength * m_aspect.x(), offsetLength * m_aspect.y()); + + rect = rect.adjusted(-m_borderWidth * m_aspect.x(), -m_borderWidth * m_aspect.y(), + m_borderWidth * m_aspect.x(), m_borderWidth * m_aspect.y()); + + QSGGeometry::updateTexturedRectGeometry(m_geometry, rect, QRectF{0.0, 0.0, 1.0, 1.0}); + markDirty(QSGNode::DirtyGeometry); +} diff --git a/src/shadowedrectangle.h b/src/shadowedrectangle.h new file mode 100644 --- /dev/null +++ b/src/shadowedrectangle.h @@ -0,0 +1,164 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include + +/** + * Grouped property for rectangle border. + */ +class BorderGroup : public QObject +{ + Q_OBJECT + /** + * The width of the border in pixels. + * + * Default is 0. + */ + Q_PROPERTY(qreal width READ width WRITE setWidth NOTIFY changed) + /** + * The color of the border. + * + * Full RGBA colors are supported. The default is fully opaque black. + */ + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY changed) + +public: + explicit BorderGroup(QObject *parent = nullptr); + + qreal width() const; + void setWidth(qreal newWidth); + + QColor color() const; + void setColor(const QColor &newColor); + + Q_SIGNAL void changed(); + +private: + qreal m_width = 0.0; + QColor m_color = Qt::black; +}; + +/** + * Grouped property for rectangle shadow. + */ +class ShadowGroup : public QObject +{ + Q_OBJECT + /** + * The size of the shadow. + * + * This is the approximate size of the shadow in pixels. However, due to falloff + * the actual shadow size can differ. The default is 0, which means no shadow will + * be rendered. + */ + Q_PROPERTY(qreal size READ size WRITE setSize NOTIFY changed) + /** + * Offset of the shadow on the X axis. + * + * In pixels. The default is 0. + */ + Q_PROPERTY(qreal xOffset READ xOffset WRITE setXOffset NOTIFY changed) + /** + * Offset of the shadow on the Y axis. + * + * In pixels. The default is 0. + */ + Q_PROPERTY(qreal yOffset READ yOffset WRITE setYOffset NOTIFY changed) + /** + * The color of the shadow. + * + * Full RGBA colors are supported. The default is fully opaque black. + */ + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY changed) + +public: + explicit ShadowGroup(QObject *parent = nullptr); + + qreal size() const; + void setSize(qreal newSize); + + qreal xOffset() const; + void setXOffset(qreal newXOffset); + + qreal yOffset() const; + void setYOffset(qreal newYOffset); + + QColor color() const; + void setColor(const QColor &newShadowColor); + + Q_SIGNAL void changed(); + +private: + qreal m_size = 0.0; + qreal m_xOffset = 0.0; + qreal m_yOffset = 0.0; + QColor m_color = Qt::black; +}; + +/** + * A rectangle with a shadow. + * + * This item will render a rectangle, with a shadow below it. The rendering is done + * using distance fields, which provide greatly improved performance. The shadow is + * rendered outside of the item's bounds, so the item's width and height are the + * rectangle's width and height. + */ +class ShadowedRectangle : public QQuickItem +{ + Q_OBJECT + /** + * Corner radius of the rectangle. + * + * This is the amount of rounding to apply to the rectangle's corners, in pixels. + * The default is 0. + */ + Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged) + /** + * The color of the rectangle. + * + * Full RGBA colors are supported. The default is fully opaque white. + */ + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) + /** + * Border properties. + * + * \sa BorderGroup + */ + Q_PROPERTY(BorderGroup *border READ border CONSTANT) + /** + * Shadow properties. + * + * \sa ShadowGroup + */ + Q_PROPERTY(ShadowGroup *shadow READ shadow CONSTANT) + +public: + ShadowedRectangle(QQuickItem *parent = nullptr); + ~ShadowedRectangle() override; + + BorderGroup *border() const; + ShadowGroup *shadow() const; + + qreal radius() const; + void setRadius(qreal newRadius); + Q_SIGNAL void radiusChanged(); + + QColor color() const; + void setColor(const QColor &newColor); + Q_SIGNAL void colorChanged(); + +protected: + QSGNode *updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data) override; + +private: + const std::unique_ptr m_border; + const std::unique_ptr m_shadow; + qreal m_radius = 0.0; + QColor m_color = Qt::white; +}; diff --git a/src/shadowedrectangle.cpp b/src/shadowedrectangle.cpp new file mode 100644 --- /dev/null +++ b/src/shadowedrectangle.cpp @@ -0,0 +1,186 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "shadowedrectangle.h" + +#include "scenegraph/shadowedrectanglenode.h" + +BorderGroup::BorderGroup(QObject* parent) + : QObject(parent) +{ +} + +qreal BorderGroup::width() const +{ + return m_width; +} + +void BorderGroup::setWidth(qreal newWidth) +{ + if (newWidth == m_width) { + return; + } + + m_width = newWidth; + Q_EMIT changed(); +} + +QColor BorderGroup::color() const +{ + return m_color; +} + +void BorderGroup::setColor(const QColor & newColor) +{ + if (newColor == m_color) { + return; + } + + m_color = newColor; + Q_EMIT changed(); +} + +ShadowGroup::ShadowGroup(QObject *parent) + : QObject(parent) +{ +} + +qreal ShadowGroup::size() const +{ + return m_size; +} + +void ShadowGroup::setSize(qreal newSize) +{ + if (newSize == m_size) { + return; + } + + m_size = newSize; + Q_EMIT changed(); +} + +qreal ShadowGroup::xOffset() const +{ + return m_xOffset; +} + +void ShadowGroup::setXOffset(qreal newXOffset) +{ + if (newXOffset == m_xOffset) { + return; + } + + m_xOffset = newXOffset; + Q_EMIT changed(); +} + +qreal ShadowGroup::yOffset() const +{ + return m_yOffset; +} + +void ShadowGroup::setYOffset(qreal newYOffset) +{ + if (newYOffset == m_yOffset) { + return; + } + + m_yOffset = newYOffset; + Q_EMIT changed(); +} + +QColor ShadowGroup::color() const +{ + return m_color; +} + +void ShadowGroup::setColor(const QColor & newColor) +{ + if (newColor == m_color) { + return; + } + + m_color = newColor; + Q_EMIT changed(); +} + +ShadowedRectangle::ShadowedRectangle(QQuickItem *parentItem) + : QQuickItem(parentItem), m_border(new BorderGroup), m_shadow(new ShadowGroup) +{ + setFlag(QQuickItem::ItemHasContents, true); + + connect(m_border.get(), &BorderGroup::changed, this, &ShadowedRectangle::update); + connect(m_shadow.get(), &ShadowGroup::changed, this, &ShadowedRectangle::update); +} + +ShadowedRectangle::~ShadowedRectangle() +{ +} + +BorderGroup *ShadowedRectangle::border() const +{ + return m_border.get(); +} + +ShadowGroup *ShadowedRectangle::shadow() const +{ + return m_shadow.get(); +} + +qreal ShadowedRectangle::radius() const +{ + return m_radius; +} + +void ShadowedRectangle::setRadius(qreal newRadius) +{ + if (newRadius == m_radius) { + return; + } + + m_radius = newRadius; + update(); + Q_EMIT radiusChanged(); +} + +QColor ShadowedRectangle::color() const +{ + return m_color; +} + +void ShadowedRectangle::setColor(const QColor & newColor) +{ + if (newColor == m_color) { + return; + } + + m_color = newColor; + update(); + Q_EMIT colorChanged(); +} + +QSGNode *ShadowedRectangle::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data) +{ + Q_UNUSED(data); + + if (!node) { + node = new ShadowedRectangleNode; + } + + auto elevatedNode = static_cast(node); + elevatedNode->setBorderWidth(m_border->width()); + elevatedNode->setRect(boundingRect()); + elevatedNode->setSize(m_shadow->size()); + elevatedNode->setRadius(m_radius); + elevatedNode->setOffset(QVector2D{float(m_shadow->xOffset()), float(m_shadow->yOffset())}); + elevatedNode->setColor(m_color); + elevatedNode->setShadowColor(m_shadow->color()); + elevatedNode->setBorderColor(m_border->color()); + elevatedNode->updateGeometry(); + + return elevatedNode; +} diff --git a/tests/ShadowedRectangleTest.qml b/tests/ShadowedRectangleTest.qml new file mode 100644 --- /dev/null +++ b/tests/ShadowedRectangleTest.qml @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: 2020 Arjen Hiemstra + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +import org.kde.kirigami 2.12 as Kirigami + +Kirigami.ApplicationWindow { + id: window + + width: 500 + height: 500 + + pageStack.initialPage: Kirigami.Page { + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + Column { + anchors.centerIn: parent + + Kirigami.ShadowedRectangle { + width: 400 + height: 300 + + color: Kirigami.Theme.highlightColor + + radius: radiusSlider.value + + shadow.size: sizeSlider.value + shadow.xOffset: xOffsetSlider.value + shadow.yOffset: yOffsetSlider.value + + border.width: borderWidthSlider.value + border.color: Kirigami.Theme.textColor + } + + Item { width: 1; height: Kirigami.Units.gridUnit } + + Slider { + id: sizeSlider + + from: 0 + to: 100 + } + + Slider { + id: radiusSlider + + from: 0 + to: 200 + } + + Slider { + id: xOffsetSlider + + from: -100 + to: 100 + } + + Slider { + id: yOffsetSlider + + from: -100 + to: 100 + } + + Slider { + id: borderWidthSlider + + from: 0 + to: 50 + } + } + } +}