diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -25,6 +25,7 @@ mnemonicattached.cpp wheelhandler.cpp shadowedrectangle.cpp + colorutils.cpp scenegraph/shadowedrectanglenode.cpp scenegraph/shadowedrectanglematerial.cpp scenegraph/shadowedborderrectanglematerial.cpp diff --git a/src/colorutils.h b/src/colorutils.h new file mode 100644 --- /dev/null +++ b/src/colorutils.h @@ -0,0 +1,113 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include + +/** + * Utilities for processing items to obtain colors and information useful for + * UIs that need to adjust to variable elements. + */ +class ColorUtils : public QObject +{ + Q_OBJECT +public: + /** + * Describes the contrast of an item. + */ + enum Brightness { + Dark, /**< The item is dark and requires a light foreground color to achieve readable contrast. */ + Light, /**< The item is light and requires a dark foreground color to achieve readable contrast. */ + }; + Q_ENUM(Brightness) + + explicit ColorUtils(QObject* parent = nullptr); + + /** + * Averages the colors of an item. + * + * @note + * This function renders the item to an offscreen buffer and copies it from + * GPU memory to CPU memory, which can be costly. Avoid using this for items + * that are costly to render. + * + * @code{.qml} + * import QtQuick 2.0 + * import org.kde.kirigami 2.12 as Kirigami + * + * Column { + * Row { + * id: colorRow + * Rectangle { + * color: "red" + * height: 50 + * width: 50 + * } + * Rectangle { + * color: "blue" + * height: 50 + * width: 50 + * } + * } + * Rectangle { + * color: Kirigami.ColorUtils.averageColorForItem(colorRow) + * height: 50 + * width: 100 + * } + * } + * @endcode + * + * @since 5.69 + * @since org.kde.kirigami 2.12 + */ + Q_INVOKABLE QColor averageColorForItem(QQuickItem *item, int maxPixels = 65536); + + /** + * Returns whether an item is bright or dark. + * + * @note + * This function renders the item to an offscreen buffer and copies it from + * GPU memory to CPU memory, which can be costly. Avoid using this for items + * that are costly to render. + * + * @code{.qml} + * import QtQuick 2.0 + * import org.kde.kirigami 2.12 as Kirigami + * + * Column { + * Kirigami.Heading { + * text: { + * if (Kirigami.ColorUtils.brightnessForItem(colorRow) == Kirigami.ColorUtils.Light) { + * return "The item is light" + * } else { + * return "The item is dark" + * } + * } + * } + * Row { + * id: colorRow + * Rectangle { + * color: "black" + * height: 50 + * width: 50 + * } + * Rectangle { + * color: "grey" + * height: 50 + * width: 50 + * } + * } + * } + * @endcode + * + * @since 5.69 + * @since org.kde.kirigami 2.12 + */ + Q_INVOKABLE Brightness brightnessForItem(QQuickItem *item); +}; diff --git a/src/colorutils.cpp b/src/colorutils.cpp new file mode 100644 --- /dev/null +++ b/src/colorutils.cpp @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "colorutils.h" + +#include +#include + +ColorUtils::ColorUtils(QObject *parent) : QObject(parent) {} + +QColor ColorUtils::averageColorForItem(QQuickItem *item, int maxPixels) +{ + const auto response = item->grabToImage(QSize(item->width(), item->height())); + if (!response) { // Response will be a null pointer when something goes wrong, + // so we return an invalid colour. + return QColor::Invalid; + } + + // This setup allows us to await QQuickItemGrabResult::ready. The image will + // be invalid before the result is ready. + QEventLoop loop; + connect(response.data(), &QQuickItemGrabResult::ready, &loop, &QEventLoop::quit); + loop.exec(); + + auto image = response->image(); + // We use this lambda setup to allow a const imageData, which is easier + // for compilers to optimize due to lack of mutability. + const auto imageData = [image]() { + QList data; + // Iterate through every pixel in the list, flattening the 2D image into + // a 1D array for easier processing later on. + for (int widthIndex = 0; widthIndex < image.width(); widthIndex++) { + for (int heightIndex = 0; heightIndex < image.height(); heightIndex++) { + auto pixel = image.pixel(widthIndex, heightIndex); + // Only add pixels that aren't transparent in order to get a more + // usable color for usage in UI. + if (pixel != 0) { + data << pixel; + } + } + } + return data; + }(); + + // Compute our skip factor by dividing the amount of pixels by the maximum allowed + // amount of pixels. When amount > max, skip factor can be greater than one, causing + // pixels to be skipped for performance reasons. + const auto skipFactor = qMax(1, qFloor(imageData.length() / maxPixels)); + + int red = 0, green = 0, blue = 0, alpha = 0, colorCount = 0; + for (int pixelIndex = 0; pixelIndex < imageData.length(); pixelIndex++) { + // Since any number modulo one is zero, we don't want to attempt + // skipping when the skipFactor is one. If it's not one, we + // check if the current index modulo the skip factor is equivalent to + // zero. It'll be zero every n items, where n is the skip factor. + if ((skipFactor != 1) && (pixelIndex % skipFactor == 0)) { + continue; + } + colorCount++; + red += qRed(imageData[pixelIndex]); + green += qGreen(imageData[pixelIndex]); + blue += qBlue(imageData[pixelIndex]); + alpha += qAlpha(imageData[pixelIndex]); + } + + // If we somehow got here without any colours, we want to + // return an invalid colour instead of causing a divide by zero error. + if (colorCount == 0) { + return QColor::Invalid; + } + + // Average the colours. + red = red / colorCount; + green = green / colorCount; + blue = blue / colorCount; + alpha = alpha / colorCount; + + return QColor::fromRgb(red, green, blue, alpha); +} + +ColorUtils::Brightness ColorUtils::brightnessForItem(QQuickItem *item) { + auto color = averageColorForItem(item); + + // These are the luma coefficients from Rec. 709. + auto luma = (0.299*color.red() + 0.587*color.green() + 0.114*color.blue())/255; + return luma > 0.5 ? ColorUtils::Brightness::Light : ColorUtils::Brightness::Dark; +} diff --git a/src/kirigamiplugin.cpp b/src/kirigamiplugin.cpp --- a/src/kirigamiplugin.cpp +++ b/src/kirigamiplugin.cpp @@ -18,6 +18,7 @@ #include "scenepositionattached.h" #include "wheelhandler.h" #include "shadowedrectangle.h" +#include "colorutils.h" #include #include @@ -240,6 +241,7 @@ 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")); + qmlRegisterSingletonType(uri, 2, 12, "ColorUtils", [](QQmlEngine*, QJSEngine*) { return new ColorUtils; }); qmlProtectModule(uri, 2); }