From cc5ebf21e747802e60941083da0e0d7d1bfbb2aa Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 2 Sep 2021 19:57:34 +0300 Subject: [PATCH] Add style::colorizer for theme palette changing. --- CMakeLists.txt | 2 + ui/style/style_core_palette.cpp | 23 +++- ui/style/style_core_palette.h | 4 + ui/style/style_palette_colorizer.cpp | 198 +++++++++++++++++++++++++++ ui/style/style_palette_colorizer.h | 53 +++++++ 5 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 ui/style/style_palette_colorizer.cpp create mode 100644 ui/style/style_palette_colorizer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 91e2e54..9e88ffd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -137,6 +137,8 @@ PRIVATE ui/style/style_core_scale.h ui/style/style_core_types.cpp ui/style/style_core_types.h + ui/style/style_palette_colorizer.cpp + ui/style/style_palette_colorizer.h ui/text/text.cpp ui/text/text.h ui/text/text_block.cpp diff --git a/ui/style/style_core_palette.cpp b/ui/style/style_core_palette.cpp index 7677d7c..39b036f 100644 --- a/ui/style/style_core_palette.cpp +++ b/ui/style/style_core_palette.cpp @@ -6,6 +6,8 @@ // #include "ui/style/style_core_palette.h" +#include "ui/style/style_palette_colorizer.h" + namespace style { int palette::indexOfColor(style::color c) const { @@ -22,13 +24,18 @@ color palette::colorAtIndex(int index) const { return _colors[index]; } -void palette::finalize() { +void palette::finalize(const colorizer &with) { if (_ready) return; _ready = true; + _colorizer = with ? &with : nullptr; palette_data::finalize(*this); } +void palette::finalize() { + finalize(colorizer()); +} + palette &palette::operator=(const palette &other) { auto wasReady = _ready; for (int i = 0; i != kCount; ++i) { @@ -124,7 +131,19 @@ void palette::compute(int index, int fallbackIndex, TempColorData value) { _status[index] = Status::Loaded; new (data(index)) internal::ColorData(*data(fallbackIndex)); } else { - _status[index] = Status::Created; + if (_colorizer && *_colorizer) { + colorize( + //(index + // ? style::main_palette::data()[index - 1].name + // : qstr("transparent")), + value.r, + value.g, + value.b, + *_colorizer); + _status[index] = Status::Loaded; + } else { + _status[index] = Status::Created; + } new (data(index)) internal::ColorData(value.r, value.g, value.b, value.a); } } diff --git a/ui/style/style_core_palette.h b/ui/style/style_core_palette.h index e6d3f31..39c2e1b 100644 --- a/ui/style/style_core_palette.h +++ b/ui/style/style_core_palette.h @@ -11,6 +11,8 @@ namespace style { +struct colorizer; + class palette : public palette_data { public: palette() = default; @@ -34,6 +36,7 @@ public: void reset(); // Created not inited, should be finalized before usage. + void finalize(const colorizer &with); void finalize(); int indexOfColor(color c) const; @@ -47,6 +50,7 @@ private: void compute(int index, int fallbackIndex, TempColorData value); void setData(int index, const internal::ColorData &value); + const colorizer *_colorizer = nullptr; bool _ready = false; }; diff --git a/ui/style/style_palette_colorizer.cpp b/ui/style/style_palette_colorizer.cpp new file mode 100644 index 0000000..fc7a7bc --- /dev/null +++ b/ui/style/style_palette_colorizer.cpp @@ -0,0 +1,198 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#include "ui/style/style_palette_colorizer.h" + +namespace style { +namespace { + +constexpr auto kEnoughLightnessForContrast = 64; + +void FillColorizeResult(uchar &r, uchar &g, uchar &b, const QColor &color) { + auto nowR = 0; + auto nowG = 0; + auto nowB = 0; + color.getRgb(&nowR, &nowG, &nowB); + r = uchar(nowR); + g = uchar(nowG); + b = uchar(nowB); +} + +[[nodiscard]] std::optional colorize( + const colorizer::Color &color, + const colorizer &with) { + const auto changeColor = std::abs(color.hue - with.was.hue) + < with.hueThreshold; + if (!changeColor) { + return std::nullopt; + } + const auto nowHue = color.hue + (with.now.hue - with.was.hue); + const auto nowSaturation = ((color.saturation > with.was.saturation) + && (with.now.saturation > with.was.saturation)) + ? (((with.now.saturation * (255 - with.was.saturation)) + + ((color.saturation - with.was.saturation) + * (255 - with.now.saturation))) + / (255 - with.was.saturation)) + : ((color.saturation != with.was.saturation) + && (with.was.saturation != 0)) + ? ((color.saturation * with.now.saturation) + / with.was.saturation) + : with.now.saturation; + const auto nowValue = (color.value > with.was.value) + ? (((with.now.value * (255 - with.was.value)) + + ((color.value - with.was.value) + * (255 - with.now.value))) + / (255 - with.was.value)) + : (color.value < with.was.value) + ? ((color.value * with.now.value) + / with.was.value) + : with.now.value; + return colorizer::Color{ + ((nowHue + 360) % 360), + nowSaturation, + nowValue + }; +} + +} // namespace + +QColor ColorFromHex(std::string_view hex) { + Expects(hex.size() == 6); + + const auto component = [](char a, char b) { + const auto convert = [](char ch) { + Expects((ch >= '0' && ch <= '9') + || (ch >= 'A' && ch <= 'F') + || (ch >= 'a' && ch <= 'f')); + + return (ch >= '0' && ch <= '9') + ? int(ch - '0') + : int(ch - ((ch >= 'A' && ch <= 'F') ? 'A' : 'a') + 10); + }; + return convert(a) * 16 + convert(b); + }; + + return QColor( + component(hex[0], hex[1]), + component(hex[2], hex[3]), + component(hex[4], hex[5])); +}; + +void colorize(uchar &r, uchar &g, uchar &b, const colorizer &with) { + const auto changed = colorize(QColor(int(r), int(g), int(b)), with); + if (changed) { + FillColorizeResult(r, g, b, *changed); + } +} + +void colorize( + QLatin1String name, + uchar &r, + uchar &g, + uchar &b, + const colorizer &with) { + if (with.ignoreKeys.contains(name)) { + return; + } + + const auto i = with.keepContrast.find(name); + if (i == end(with.keepContrast)) { + colorize(r, g, b, with); + return; + } + const auto check = i->second.first; + const auto rgb = QColor(int(r), int(g), int(b)); + const auto changed = colorize(rgb, with); + const auto checked = colorize(check, with).value_or(check); + const auto lightness = [](QColor hsv) { + return hsv.value() - (hsv.value() * hsv.saturation()) / 511; + }; + const auto changedLightness = lightness(changed.value_or(rgb).toHsv()); + const auto checkedLightness = lightness( + QColor::fromHsv(checked.hue, checked.saturation, checked.value)); + const auto delta = std::abs(changedLightness - checkedLightness); + if (delta >= kEnoughLightnessForContrast) { + if (changed) { + FillColorizeResult(r, g, b, *changed); + } + return; + } + const auto replace = i->second.second; + const auto result = colorize(replace, with).value_or(replace); + FillColorizeResult( + r, + g, + b, + QColor::fromHsv(result.hue, result.saturation, result.value)); +} + +void colorize(uint32 &pixel, const colorizer &with) { + const auto chars = reinterpret_cast(&pixel); + colorize(chars[2], chars[1], chars[0], with); +} + +void colorize(QImage &image, const colorizer &with) { + image = std::move(image).convertToFormat(QImage::Format_ARGB32); + const auto bytes = image.bits(); + const auto bytesPerLine = image.bytesPerLine(); + for (auto line = 0; line != image.height(); ++line) { + const auto ints = reinterpret_cast( + bytes + line * bytesPerLine); + const auto end = ints + image.width(); + for (auto p = ints; p != end; ++p) { + colorize(*p, with); + } + } +} + +std::optional colorize(const QColor &color, const colorizer &with) { + auto hue = 0; + auto saturation = 0; + auto lightness = 0; + color.getHsv(&hue, &saturation, &lightness); + const auto result = colorize( + colorizer::Color{ hue, saturation, lightness }, + with); + if (!result) { + return std::nullopt; + } + const auto &fields = *result; + return QColor::fromHsv(fields.hue, fields.saturation, fields.value); +} + +QByteArray colorize( + QLatin1String hexColor, + const style::colorizer &with) { + Expects(hexColor.size() == 7 || hexColor.size() == 9); + + auto color = ColorFromHex(std::string_view(hexColor.data() + 1, 6)); + const auto changed = colorize(color, with).value_or(color).toRgb(); + + auto result = QByteArray(); + result.reserve(hexColor.size()); + result.append(hexColor.data()[0]); + const auto addHex = [&](int code) { + if (code >= 0 && code < 10) { + result.append('0' + code); + } else if (code >= 10 && code < 16) { + result.append('a' + (code - 10)); + } + }; + const auto addValue = [&](int code) { + addHex(code / 16); + addHex(code % 16); + }; + addValue(changed.red()); + addValue(changed.green()); + addValue(changed.blue()); + if (hexColor.size() == 9) { + result.append(hexColor.data()[7]); + result.append(hexColor.data()[8]); + } + return result; +} + +} // namespace style diff --git a/ui/style/style_palette_colorizer.h b/ui/style/style_palette_colorizer.h new file mode 100644 index 0000000..d1dafe9 --- /dev/null +++ b/ui/style/style_palette_colorizer.h @@ -0,0 +1,53 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +namespace style { + +struct colorizer { + struct Color { + int hue = 0; + int saturation = 0; + int value = 0; + }; + int hueThreshold = 0; + int lightnessMin = 0; + int lightnessMax = 255; + Color was; + Color now; + base::flat_set ignoreKeys; + base::flat_map> keepContrast; + + explicit operator bool() const { + return (hueThreshold > 0); + } +}; + +[[nodiscard]] QColor ColorFromHex(std::string_view hex); + +void colorize( + uchar &r, + uchar &g, + uchar &b, + const colorizer &with); +void colorize( + QLatin1String name, + uchar &r, + uchar &g, + uchar &b, + const colorizer &with); +void colorize(QImage &image, const colorizer &with); + +[[nodiscard]] std::optional colorize( + const QColor &color, + const colorizer &with); + +[[nodiscard]] QByteArray colorize( + QLatin1String hexColor, + const colorizer &with); + +} // namespace style