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,112 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include + +/** + * Utilities for UI colors. + */ +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,70 @@ +/* + * 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) { + return QColor::Invalid; + } + + QEventLoop loop; + connect(response.data(), &QQuickItemGrabResult::ready, &loop, &QEventLoop::quit); + loop.exec(); + + auto image = response->image(); + const auto imageData = [image]() { + QList data; + for (int i = 0; i < image.width(); i++) { + for (int ii = 0; ii < image.height(); ii++) { + auto pixel = image.pixel(i, ii); + if (pixel != 0) { + data << pixel; + } + } + } + return data; + }(); + const auto skipFactor = qMax(1, qFloor(imageData.length() / maxPixels)); + + int r = 0, g = 0, b = 0, a = 0, c = 0; + for (int i = 0; i < imageData.length(); i++) { + if ((skipFactor != 1) && (i % skipFactor == 0)) { + continue; + } + c++; + r += qRed(imageData[i]); + g += qGreen(imageData[i]); + b += qBlue(imageData[i]); + a += qAlpha(imageData[i]); + } + + if (c == 0) { + return QColor::Invalid; + } + + r = r / c; + g = g / c; + b = b / c; + a = a / c; + + return QColor::fromRgb(r, g, b, a); +} + +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); }