From e38fbf814b1bcc132e1bc33fef3f31223b8983bc Mon Sep 17 00:00:00 2001 From: AlexeyZavar Date: Sun, 5 Oct 2025 01:51:09 +0300 Subject: [PATCH] feat: redesign now playing widget --- README.md | 1 + Telegram/CMakeLists.txt | 11 + Telegram/Resources/icons/ayu/nocover.svg | 5 + Telegram/Resources/qrc/ayu/ayu.qrc | 1 + .../ayu/ui/components/now_playing.cpp | 295 +++++++++ .../ayu/ui/components/now_playing.h | 40 ++ .../ayu/ui/utils/ayu_profile_values.cpp | 2 +- .../ayu/ui/utils/color_cut_quantizer.cpp | 340 ++++++++++ .../ayu/ui/utils/color_cut_quantizer.h | 90 +++ .../SourceFiles/ayu/ui/utils/color_utils.cpp | 230 +++++++ .../SourceFiles/ayu/ui/utils/color_utils.h | 45 ++ Telegram/SourceFiles/ayu/ui/utils/palette.cpp | 610 ++++++++++++++++++ Telegram/SourceFiles/ayu/ui/utils/palette.h | 183 ++++++ Telegram/SourceFiles/info/info.style | 2 +- .../info/profile/info_profile_cover.cpp | 17 +- .../info/profile/info_profile_cover.h | 4 +- .../info/profile/info_profile_music_button.h | 5 + Telegram/cmake/td_ui.cmake | 2 - 18 files changed, 1871 insertions(+), 12 deletions(-) create mode 100644 Telegram/Resources/icons/ayu/nocover.svg create mode 100644 Telegram/SourceFiles/ayu/ui/components/now_playing.cpp create mode 100644 Telegram/SourceFiles/ayu/ui/components/now_playing.h create mode 100644 Telegram/SourceFiles/ayu/ui/utils/color_cut_quantizer.cpp create mode 100644 Telegram/SourceFiles/ayu/ui/utils/color_cut_quantizer.h create mode 100644 Telegram/SourceFiles/ayu/ui/utils/color_utils.cpp create mode 100644 Telegram/SourceFiles/ayu/ui/utils/color_utils.h create mode 100644 Telegram/SourceFiles/ayu/ui/utils/palette.cpp create mode 100644 Telegram/SourceFiles/ayu/ui/utils/palette.h diff --git a/README.md b/README.md index 215ffbc117..cbd7db9703 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ Enjoy using **AyuGram**? Consider sending us a tip! - [JSON for Modern C++](https://github.com/nlohmann/json) - [SQLite](https://github.com/sqlite/sqlite) - [sqlite_orm](https://github.com/fnc12/sqlite_orm) +- [androidx sources](https://github.com/androidx/androidx) ### Icons diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 86e9320108..920cfc9c6b 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -133,6 +133,12 @@ set(ayugram_files ayu/ui/ayu_logo.h ayu/ui/utils/ayu_profile_values.cpp ayu/ui/utils/ayu_profile_values.h + ayu/ui/utils/color_cut_quantizer.cpp + ayu/ui/utils/color_cut_quantizer.h + ayu/ui/utils/color_utils.cpp + ayu/ui/utils/color_utils.h + ayu/ui/utils/palette.cpp + ayu/ui/utils/palette.h ayu/ui/settings/settings_appearance.cpp ayu/ui/settings/settings_appearance.h ayu/ui/settings/settings_ayu_utils.cpp @@ -183,6 +189,8 @@ set(ayugram_files ayu/ui/components/image_view.h ayu/ui/components/icon_picker.cpp ayu/ui/components/icon_picker.h + ayu/ui/components/now_playing.cpp + ayu/ui/components/now_playing.h ayu/libs/json.hpp ayu/libs/json_ext.hpp ayu/libs/sqlite/sqlite3.c @@ -227,6 +235,9 @@ set(ayugram_files ayu/data/entities.h ayu/data/ayu_database.cpp ayu/data/ayu_database.h + + info/profile/info_profile_music_button.cpp + info/profile/info_profile_music_button.h ) target_precompile_headers(Telegram PRIVATE $<$:${src_loc}/stdafx.h>) diff --git a/Telegram/Resources/icons/ayu/nocover.svg b/Telegram/Resources/icons/ayu/nocover.svg new file mode 100644 index 0000000000..53027c9c21 --- /dev/null +++ b/Telegram/Resources/icons/ayu/nocover.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Telegram/Resources/qrc/ayu/ayu.qrc b/Telegram/Resources/qrc/ayu/ayu.qrc index 57b4c4ad2e..1bb327c6d8 100644 --- a/Telegram/Resources/qrc/ayu/ayu.qrc +++ b/Telegram/Resources/qrc/ayu/ayu.qrc @@ -52,6 +52,7 @@ ../../art/ayu/yaplus/app_preview.png ../../art/ayu/yaplus/app_macos.png ../../art/ayu/yaplus/app_icon.ico + ../../icons/ayu/nocover.svg ../../icons/ayu/donates/boosty.svg ../../icons/ayu/donates/ton.svg ../../icons/ayu/donates/bitcoin.svg diff --git a/Telegram/SourceFiles/ayu/ui/components/now_playing.cpp b/Telegram/SourceFiles/ayu/ui/components/now_playing.cpp new file mode 100644 index 0000000000..6ec6b999f7 --- /dev/null +++ b/Telegram/SourceFiles/ayu/ui/components/now_playing.cpp @@ -0,0 +1,295 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#include "now_playing.h" + +#include "info/profile/info_profile_music_button.h" + +#include + +#include "ayu/ui/utils/color_utils.h" +#include "ayu/ui/utils/palette.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_session.h" +#include "main/main_session.h" +#include "styles/palette.h" +#include "styles/style_info.h" +#include "ui/painter.h" +#include "ui/image/image.h" +#include "ui/widgets/labels.h" +#include "window/themes/window_theme.h" + +class Image; + +namespace Info::Profile { + +namespace { + +QRgb AdjustHsl(QRgb color, float luminance, float saturation = -1.0f) { + auto hsl = Ayu::Ui::ColorUtils::colorToHSL(color); + + if (saturation > 0.0f) { + hsl[1] = std::min(hsl[1] * saturation, 1.0f); + } + + hsl[2] = std::min(hsl[2] * luminance, 1.0f); + + return Ayu::Ui::ColorUtils::HSLToRGB(hsl); +} + +QRgb BlendARGB(QRgb color1, QRgb color2, float ratio) { + const auto inverseRatio = 1.0f - ratio; + const auto r = static_cast(qRed(color1) * inverseRatio + qRed(color2) * ratio); + const auto g = static_cast(qGreen(color1) * inverseRatio + qGreen(color2) * ratio); + const auto b = static_cast(qBlue(color1) * inverseRatio + qBlue(color2) * ratio); + const auto a = static_cast(qAlpha(color1) * inverseRatio + qAlpha(color2) * ratio); + return qRgba(r, g, b, a); +} + +QColor GetNoCoverBgColor() { + return Window::Theme::IsNightMode() ? st::windowBgOver->c.darker(170) : st::windowBgOver->c; +} + +struct Cover +{ + QPixmap pixToDraw; + QPixmap pixToBg; + bool noCover; +}; + +QPixmap MakeNoCoverImage(const QSize &size) { + static QPixmap result; + static auto resultTheme = Window::Theme::Background()->id(); + if (!result.isNull() && result.size() == size && resultTheme == Window::Theme::Background()->id()) { + return result; + } + resultTheme = Window::Theme::Background()->id(); + + auto image = QImage(size, QImage::Format_ARGB32); + { + auto p = Painter(&image); + auto hq = PainterHighQualityEnabler(p); + + const auto bgColor = Window::Theme::IsNightMode() + ? st::windowBoldFg->c.darker() + : st::windowBoldFg->c.lighter(); + image.fill(bgColor); + + auto svgIcon = QSvgRenderer(u":/gui/icons/ayu/nocover.svg"_q); + p.setPen(st::windowBoldFg->p); + svgIcon.render(&p, QRect(0, 0, size.width(), size.height())); + } + const auto img = Image(std::move(image)); + result = img.pix(size, Images::PrepareArgs{.options = Images::Option::RoundSmall}); + return result; +} + +} // namespace + +Cover GetCurrentCover( + const std::shared_ptr &dataMedia, + const QSize &size) { + if (!dataMedia) { + return { + .pixToDraw = MakeNoCoverImage(size), + .pixToBg = MakeNoCoverImage(size), + .noCover = true + }; + } + + auto cover = QPixmap(); + const auto scaled = [&](not_null image) + { + const auto aspectRatio = Qt::KeepAspectRatioByExpanding; + return image->size().scaled(size, aspectRatio); + }; + const auto args = Images::PrepareArgs{ + .options = Images::Option::RoundSmall, + .outer = size, + }; + if (const auto normal = dataMedia->thumbnail()) { + return { + .pixToDraw = normal->pixSingle(scaled(normal), args), + .pixToBg = normal->pix(), + .noCover = false + }; + } /*else if (const auto blurred = dataMedia->thumbnailInline()) { + return { + .pix = blurred->pixSingle(scaled(blurred), args.blurred()), + .noCover = false + }; + }*/ + + return { + .pixToDraw = MakeNoCoverImage(size), + .pixToBg = MakeNoCoverImage(size), + .noCover = true + }; +} + +QRgb ExtractColorFromCover(const QPixmap &cover) { + const auto palette = Ayu::Ui::Palette::from(cover).generate(); + + const auto *swatch = palette.darkVibrantSwatch(); + if (!swatch) { + swatch = palette.mutedSwatch(); + } + if (!swatch) { + swatch = palette.darkMutedSwatch(); + } + if (!swatch) { + swatch = palette.dominantSwatch(); + } + + if (!swatch) { + return GetNoCoverBgColor().rgb(); + } + + auto extractedColor = swatch->rgb(); + + const auto whiteColor = qRgb(255, 255, 255); + const auto contrast = Ayu::Ui::ColorUtils::calculateContrast(whiteColor, extractedColor); + + auto adjustedColor = extractedColor; + if (contrast > 15.0f) { + adjustedColor = AdjustHsl(extractedColor, 2.0f); + } else if (static_cast(contrast) < 10) { + adjustedColor = AdjustHsl(extractedColor, 0.5f); + } + + if (Ayu::Ui::ColorUtils::calculateContrast(whiteColor, adjustedColor) < 3.0f) { + adjustedColor = BlendARGB(adjustedColor, qRgb(0, 0, 0), 0.3f); + } + + return adjustedColor; +} + +AyuMusicButton::AyuMusicButton( + QWidget *parent, + MusicButtonData data, + Fn handler) + : RippleButton(parent, st::infoMusicButtonRipple) + , _performer(std::make_unique( + this, + data.performer, + st::infoMusicButtonPerformer)) + , _title(std::make_unique( + this, + data.title, + st::infoMusicButtonTitle)) + , _mediaView(data.mediaView) { + rpl::combine( + _title->naturalWidthValue(), + _performer->naturalWidthValue() + ) | rpl::start_with_next([=] + { + resizeToWidth(widthNoMargins()); + }, + lifetime()); + + _title->setAttribute(Qt::WA_TransparentForMouseEvents); + _performer->setAttribute(Qt::WA_TransparentForMouseEvents); + + if (_mediaView) { + _mediaView->owner()->owner().session().downloaderTaskFinished() | rpl::filter([=] + { + return _mediaView->loaded(); + }) | rpl::take(1) | rpl::start_with_next( + [=]() + { + resizeToWidth(widthNoMargins()); + update(); + _title->update(); + _performer->update(); + }, + lifetime()); + } + + setClickedCallback(std::move(handler)); +} + +AyuMusicButton::~AyuMusicButton() = default; + +void AyuMusicButton::updateData(MusicButtonData data) { + _performer->setText(data.performer); + _title->setText(data.title); + _mediaView = data.mediaView; + + if (_mediaView) { + _mediaView->owner()->owner().session().downloaderTaskFinished() | rpl::filter([=] + { + return _mediaView->loaded(); + }) | rpl::take(1) | rpl::start_with_next( + [=]() + { + resizeToWidth(widthNoMargins()); + update(); + _title->update(); + _performer->update(); + }, + lifetime()); + } + + resizeToWidth(widthNoMargins()); +} + +void AyuMusicButton::paintEvent(QPaintEvent *e) { + auto p = Painter(this); + + const auto &font = st::infoMusicButtonTitle.style.font; + const auto skip = st::normalFont->spacew / 2; + const auto size = font->height + skip + font->height; + + const auto cover = GetCurrentCover(_mediaView, QSize(size, size)); + QColor bgColor; + if (cover.noCover) { + bgColor = GetNoCoverBgColor(); + } else { + bgColor = QColor::fromRgb(ExtractColorFromCover(cover.pixToBg)); + } + p.fillRect(e->rect(), bgColor); + if (cover.noCover) { + paintRipple(p, QPoint()); + } + + if (!cover.pixToDraw.isNull()) { + if (!cover.noCover) { + _title->setTextColorOverride(Qt::white); + _performer->setTextColorOverride(Qt::lightGray); + } else { + _title->setTextColorOverride(std::nullopt); + _performer->setTextColorOverride(std::nullopt); + } + + auto hq = PainterHighQualityEnabler(p); + const auto coverRect = QRect(st::infoMusicButtonPadding.left(), st::infoMusicButtonPadding.top(), size, size); + p.drawPixmap(coverRect.topLeft(), cover.pixToDraw); + } else { + _title->setTextColorOverride(std::nullopt); + _performer->setTextColorOverride(std::nullopt); + } +} + +int AyuMusicButton::resizeGetHeight(int newWidth) { + const auto padding = st::infoMusicButtonPadding; + const auto &font = st::infoMusicButtonTitle.style.font; + + const auto top = padding.top(); + const auto skip = st::normalFont->spacew / 2; + + const auto coverSize = font->height + skip + font->height; + + const auto available = newWidth - padding.left() - padding.right() - coverSize; + _title->resizeToNaturalWidth(available); + _title->moveToLeft(st::infoMusicButtonPadding.left() + padding.left() + coverSize, top); + _performer->resizeToNaturalWidth(available); + _performer->moveToLeft(st::infoMusicButtonPadding.left() + padding.left() + coverSize, top + font->height + skip); + + return padding.top() + font->height + skip + font->height + padding.bottom(); +} + +} // namespace Info::Profile diff --git a/Telegram/SourceFiles/ayu/ui/components/now_playing.h b/Telegram/SourceFiles/ayu/ui/components/now_playing.h new file mode 100644 index 0000000000..d46431d555 --- /dev/null +++ b/Telegram/SourceFiles/ayu/ui/components/now_playing.h @@ -0,0 +1,40 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#pragma once + +#include "ui/widgets/buttons.h" + +namespace Ui { +class FlatLabel; +} // namespace Ui + +namespace Data { +class DocumentMedia; +} + +namespace Info::Profile { + +struct MusicButtonData; + +class AyuMusicButton final : public Ui::RippleButton { +public: + AyuMusicButton(QWidget *parent, MusicButtonData data, Fn handler); + ~AyuMusicButton(); + + void updateData(MusicButtonData data); + +private: + void paintEvent(QPaintEvent *e) override; + int resizeGetHeight(int newWidth) override; + + std::unique_ptr _performer; + std::unique_ptr _title; + std::shared_ptr _mediaView; + +}; + +} // namespace Info::Profile diff --git a/Telegram/SourceFiles/ayu/ui/utils/ayu_profile_values.cpp b/Telegram/SourceFiles/ayu/ui/utils/ayu_profile_values.cpp index 7b53fb9296..2f68aede5b 100644 --- a/Telegram/SourceFiles/ayu/ui/utils/ayu_profile_values.cpp +++ b/Telegram/SourceFiles/ayu/ui/utils/ayu_profile_values.cpp @@ -37,4 +37,4 @@ rpl::producer IDValue(not_null peer) { rpl::producer IDValue(MsgId topicRootId) { return rpl::single(IDString(topicRootId)) | Ui::Text::ToWithEntities(); -} \ No newline at end of file +} diff --git a/Telegram/SourceFiles/ayu/ui/utils/color_cut_quantizer.cpp b/Telegram/SourceFiles/ayu/ui/utils/color_cut_quantizer.cpp new file mode 100644 index 0000000000..81ac68a163 --- /dev/null +++ b/Telegram/SourceFiles/ayu/ui/utils/color_cut_quantizer.cpp @@ -0,0 +1,340 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +// +// Code is based on https://github.com/androidx/androidx/blob/androidx-main/palette/palette/src/main/java/androidx/palette/graphics/ColorCutQuantizer.java +#include "color_cut_quantizer.h" +#include "color_utils.h" + +#include +#include + +namespace Ayu::Ui { + +ColorCutQuantizer::ColorCutQuantizer( + const std::vector &pixels, + int maxColors, + const std::vector &filters) + : _histogram(1 << (QUANTIZE_WORD_WIDTH * 3), 0) + , _filters(filters) { + std::vector quantizedPixels(pixels.size()); + + for (size_t i = 0; i < pixels.size(); ++i) { + const auto quantizedColor = quantizeFromRgb888(pixels[i]); + quantizedPixels[i] = quantizedColor; + _histogram[quantizedColor]++; + } + + int distinctColorCount = 0; + for (size_t color = 0; color < _histogram.size(); ++color) { + if (_histogram[color] > 0 && shouldIgnoreColor(color)) { + _histogram[color] = 0; + } + if (_histogram[color] > 0) { + distinctColorCount++; + } + } + + _colors.reserve(distinctColorCount); + for (size_t color = 0; color < _histogram.size(); ++color) { + if (_histogram[color] > 0) { + _colors.push_back(color); + } + } + + if (distinctColorCount <= maxColors) { + _quantizedColors.reserve(_colors.size()); + for (const auto color : _colors) { + _quantizedColors.emplace_back(approximateToRgb888(color), _histogram[color]); + } + } else { + _quantizedColors = quantizePixels(maxColors); + } +} + +std::vector ColorCutQuantizer::quantizedColors() const { + return _quantizedColors; +} + +std::vector ColorCutQuantizer::quantizePixels(int maxColors) { + using VboxComparator = std::function; + VboxComparator comparator = [](const Vbox &lhs, const Vbox &rhs) + { + return lhs.volume() < rhs.volume(); + }; + + std::priority_queue, VboxComparator> pq(comparator); + + pq.emplace(0, _colors.size() - 1, this); + + splitBoxes(pq, maxColors); + + std::vector vboxes; + while (!pq.empty()) { + vboxes.push_back(pq.top()); + pq.pop(); + } + + return generateAverageColors(vboxes); +} + +void ColorCutQuantizer::splitBoxes( + std::priority_queue, std::function> &queue, + int maxSize) { + while (static_cast(queue.size()) < maxSize) { + if (queue.empty()) { + return; + } + + auto vbox = queue.top(); + queue.pop(); + + if (vbox.canSplit()) { + queue.push(vbox.splitBox()); + queue.push(vbox); + } else { + queue.push(vbox); + return; + } + } +} + +std::vector ColorCutQuantizer::generateAverageColors(const std::vector &vboxes) { + std::vector colors; + colors.reserve(vboxes.size()); + + for (const auto &vbox : vboxes) { + auto swatch = vbox.averageColor(); + if (!shouldIgnoreColor(swatch)) { + colors.push_back(swatch); + } + } + + return colors; +} + +bool ColorCutQuantizer::shouldIgnoreColor(int color565) const { + const auto rgb = approximateToRgb888(color565); + _tempHsl = ColorUtils::colorToHSL(rgb); + return shouldIgnoreColor(rgb, _tempHsl); +} + +bool ColorCutQuantizer::shouldIgnoreColor(const Swatch &swatch) const { + return shouldIgnoreColor(swatch.rgb(), swatch.hsl()); +} + +bool ColorCutQuantizer::shouldIgnoreColor(QRgb rgb, const std::array &hsl) const { + if (!_filters.empty()) { + for (const auto filter : _filters) { + if (!(*filter)(rgb, hsl)) { + return true; + } + } + } + return false; +} + +int ColorCutQuantizer::quantizeFromRgb888(QRgb color) { + const auto r = modifyWordWidth(qRed(color), 8, QUANTIZE_WORD_WIDTH); + const auto g = modifyWordWidth(qGreen(color), 8, QUANTIZE_WORD_WIDTH); + const auto b = modifyWordWidth(qBlue(color), 8, QUANTIZE_WORD_WIDTH); + return r << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH) | g << QUANTIZE_WORD_WIDTH | b; +} + +int ColorCutQuantizer::approximateToRgb888(int r, int g, int b) { + return qRgb( + modifyWordWidth(r, QUANTIZE_WORD_WIDTH, 8), + modifyWordWidth(g, QUANTIZE_WORD_WIDTH, 8), + modifyWordWidth(b, QUANTIZE_WORD_WIDTH, 8)); +} + +int ColorCutQuantizer::approximateToRgb888(int color) { + return approximateToRgb888(quantizedRed(color), quantizedGreen(color), quantizedBlue(color)); +} + +int ColorCutQuantizer::quantizedRed(int color) { + return (color >> (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH)) & QUANTIZE_WORD_MASK; +} + +int ColorCutQuantizer::quantizedGreen(int color) { + return (color >> QUANTIZE_WORD_WIDTH) & QUANTIZE_WORD_MASK; +} + +int ColorCutQuantizer::quantizedBlue(int color) { + return color & QUANTIZE_WORD_MASK; +} + +int ColorCutQuantizer::modifyWordWidth(int value, int currentWidth, int targetWidth) { + int newValue; + if (targetWidth > currentWidth) { + newValue = value << (targetWidth - currentWidth); + } else { + newValue = value >> (currentWidth - targetWidth); + } + return newValue & ((1 << targetWidth) - 1); +} + +void ColorCutQuantizer::modifySignificantOctet( + std::vector &colors, + int dimension, + int lower, + int upper) { + switch (dimension) { + case COMPONENT_RED: break; + case COMPONENT_GREEN: for (int i = lower; i <= upper; ++i) { + const auto color = colors[i]; + colors[i] = quantizedGreen(color) << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH) + | quantizedRed(color) << QUANTIZE_WORD_WIDTH + | quantizedBlue(color); + } + break; + case COMPONENT_BLUE: for (int i = lower; i <= upper; ++i) { + const auto color = colors[i]; + colors[i] = quantizedBlue(color) << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH) + | quantizedGreen(color) << QUANTIZE_WORD_WIDTH + | quantizedRed(color); + } + break; + default: break; + } +} + +ColorCutQuantizer::Vbox::Vbox(int lowerIndex, int upperIndex, ColorCutQuantizer *quantizer) + : _lowerIndex(lowerIndex) + , _upperIndex(upperIndex) + , _population(0) + , _minRed(0), _maxRed(0) + , _minGreen(0), _maxGreen(0) + , _minBlue(0), _maxBlue(0) + , _quantizer(quantizer) { + fitBox(); +} + +int ColorCutQuantizer::Vbox::volume() const { + return (_maxRed - _minRed + 1) * (_maxGreen - _minGreen + 1) * (_maxBlue - _minBlue + 1); +} + +bool ColorCutQuantizer::Vbox::canSplit() const { + return colorCount() > 1; +} + +int ColorCutQuantizer::Vbox::colorCount() const { + return 1 + _upperIndex - _lowerIndex; +} + +void ColorCutQuantizer::Vbox::fitBox() { + const auto &colors = _quantizer->_colors; + const auto &hist = _quantizer->_histogram; + + int minRed = std::numeric_limits::max(); + int minGreen = std::numeric_limits::max(); + int minBlue = std::numeric_limits::max(); + int maxRed = std::numeric_limits::min(); + int maxGreen = std::numeric_limits::min(); + int maxBlue = std::numeric_limits::min(); + int count = 0; + + for (int i = _lowerIndex; i <= _upperIndex; ++i) { + const auto color = colors[i]; + count += hist[color]; + + const auto r = quantizedRed(color); + const auto g = quantizedGreen(color); + const auto b = quantizedBlue(color); + + maxRed = std::max(maxRed, r); + minRed = std::min(minRed, r); + maxGreen = std::max(maxGreen, g); + minGreen = std::min(minGreen, g); + maxBlue = std::max(maxBlue, b); + minBlue = std::min(minBlue, b); + } + + _minRed = minRed; + _maxRed = maxRed; + _minGreen = minGreen; + _maxGreen = maxGreen; + _minBlue = minBlue; + _maxBlue = maxBlue; + _population = count; +} + +ColorCutQuantizer::Vbox ColorCutQuantizer::Vbox::splitBox() { + if (!canSplit()) { + throw std::runtime_error("Cannot split a box with only 1 color"); + } + + const auto splitPoint = findSplitPoint(); + auto newBox = Vbox(splitPoint + 1, _upperIndex, _quantizer); + + _upperIndex = splitPoint; + fitBox(); + + return newBox; +} + +int ColorCutQuantizer::Vbox::longestColorDimension() const { + const auto redLength = _maxRed - _minRed; + const auto greenLength = _maxGreen - _minGreen; + const auto blueLength = _maxBlue - _minBlue; + + if (redLength >= greenLength && redLength >= blueLength) { + return COMPONENT_RED; + } else if (greenLength >= redLength && greenLength >= blueLength) { + return COMPONENT_GREEN; + } else { + return COMPONENT_BLUE; + } +} + +int ColorCutQuantizer::Vbox::findSplitPoint() { + const auto longestDimension = longestColorDimension(); + auto &colors = _quantizer->_colors; + const auto &hist = _quantizer->_histogram; + + modifySignificantOctet(colors, longestDimension, _lowerIndex, _upperIndex); + std::sort(colors.begin() + _lowerIndex, colors.begin() + _upperIndex + 1); + modifySignificantOctet(colors, longestDimension, _lowerIndex, _upperIndex); + + const auto midPoint = _population / 2; + int count = 0; + for (int i = _lowerIndex; i <= _upperIndex; ++i) { + count += hist[colors[i]]; + if (count >= midPoint) { + return std::min(_upperIndex - 1, i); + } + } + + return _lowerIndex; +} + +Swatch ColorCutQuantizer::Vbox::averageColor() const { + const auto &colors = _quantizer->_colors; + const auto &hist = _quantizer->_histogram; + + int redSum = 0; + int greenSum = 0; + int blueSum = 0; + int totalPopulation = 0; + + for (int i = _lowerIndex; i <= _upperIndex; ++i) { + const auto color = colors[i]; + const auto colorPopulation = hist[color]; + + totalPopulation += colorPopulation; + redSum += colorPopulation * quantizedRed(color); + greenSum += colorPopulation * quantizedGreen(color); + blueSum += colorPopulation * quantizedBlue(color); + } + + const auto redMean = static_cast(std::round(redSum / static_cast(totalPopulation))); + const auto greenMean = static_cast(std::round(greenSum / static_cast(totalPopulation))); + const auto blueMean = static_cast(std::round(blueSum / static_cast(totalPopulation))); + + return Swatch(approximateToRgb888(redMean, greenMean, blueMean), totalPopulation); +} + +} // namespace Ayu::Ui diff --git a/Telegram/SourceFiles/ayu/ui/utils/color_cut_quantizer.h b/Telegram/SourceFiles/ayu/ui/utils/color_cut_quantizer.h new file mode 100644 index 0000000000..9c11992681 --- /dev/null +++ b/Telegram/SourceFiles/ayu/ui/utils/color_cut_quantizer.h @@ -0,0 +1,90 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#pragma once + +#include "palette.h" +#include +#include +#include + +namespace Ayu::Ui { + +class ColorCutQuantizer +{ +public: + using Filter = std::function &)>; + + ColorCutQuantizer( + const std::vector &pixels, + int maxColors, + const std::vector &filters); + + [[nodiscard]] std::vector quantizedColors() const; + +private: + class Vbox + { + public: + Vbox(int lowerIndex, int upperIndex, ColorCutQuantizer *quantizer); + + [[nodiscard]] int volume() const; + [[nodiscard]] bool canSplit() const; + [[nodiscard]] int colorCount() const; + + void fitBox(); + Vbox splitBox(); + [[nodiscard]] Swatch averageColor() const; + + private: + [[nodiscard]] int longestColorDimension() const; + [[nodiscard]] int findSplitPoint(); + + int _lowerIndex; + int _upperIndex; + int _population; + int _minRed, _maxRed; + int _minGreen, _maxGreen; + int _minBlue, _maxBlue; + ColorCutQuantizer *_quantizer; + }; + + static constexpr int COMPONENT_RED = -3; + static constexpr int COMPONENT_GREEN = -2; + static constexpr int COMPONENT_BLUE = -1; + static constexpr int QUANTIZE_WORD_WIDTH = 5; + static constexpr int QUANTIZE_WORD_MASK = (1 << QUANTIZE_WORD_WIDTH) - 1; + + std::vector quantizePixels(int maxColors); + void splitBoxes( + std::priority_queue, std::function> &queue, + int maxSize); + std::vector generateAverageColors(const std::vector &vboxes); + + [[nodiscard]] bool shouldIgnoreColor(int color565) const; + [[nodiscard]] bool shouldIgnoreColor(const Swatch &swatch) const; + [[nodiscard]] bool shouldIgnoreColor(QRgb rgb, const std::array &hsl) const; + + static int quantizeFromRgb888(QRgb color); + static int approximateToRgb888(int r, int g, int b); + static int approximateToRgb888(int color); + static int quantizedRed(int color); + static int quantizedGreen(int color); + static int quantizedBlue(int color); + static int modifyWordWidth(int value, int currentWidth, int targetWidth); + static void modifySignificantOctet(std::vector &colors, int dimension, int lower, int upper); + + std::vector _colors; + std::vector _histogram; + std::vector _quantizedColors; + std::vector _filters; + mutable std::array _tempHsl; + + friend class Vbox; +}; + +} // namespace Ayu::Ui + diff --git a/Telegram/SourceFiles/ayu/ui/utils/color_utils.cpp b/Telegram/SourceFiles/ayu/ui/utils/color_utils.cpp new file mode 100644 index 0000000000..e1f4086fa1 --- /dev/null +++ b/Telegram/SourceFiles/ayu/ui/utils/color_utils.cpp @@ -0,0 +1,230 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +// +// Code is based on https://github.com/androidx/androidx +#include "color_utils.h" + +#include +#include +#include + +namespace Ayu::Ui { + +std::array ColorUtils::RGBToHSL(int r, int g, int b) { + const auto rf = r / 255.0f; + const auto gf = g / 255.0f; + const auto bf = b / 255.0f; + + const auto max = std::max({rf, gf, bf}); + const auto min = std::min({rf, gf, bf}); + const auto deltaMaxMin = max - min; + + float h = 0.0f, s = 0.0f; + const auto l = (max + min) / 2.0f; + + if (max == min) { + h = s = 0.0f; + } else { + if (max == rf) { + h = std::fmod((gf - bf) / deltaMaxMin, 6.0f); + } else if (max == gf) { + h = ((bf - rf) / deltaMaxMin) + 2.0f; + } else { + h = ((rf - gf) / deltaMaxMin) + 4.0f; + } + + s = deltaMaxMin / (1.0f - std::abs(2.0f * l - 1.0f)); + } + + h = std::fmod(h * 60.0f, 360.0f); + if (h < 0) { + h += 360.0f; + } + + return { + constrain(h, 0.0f, 360.0f), + constrain(s, 0.0f, 1.0f), + constrain(l, 0.0f, 1.0f) + }; +} + +std::array ColorUtils::colorToHSL(QRgb color) { + return RGBToHSL(qRed(color), qGreen(color), qBlue(color)); +} + +QRgb ColorUtils::HSLToRGB(float h, float s, float l) { + const auto c = (1.0f - std::abs(2.0f * l - 1.0f)) * s; + const auto m = l - 0.5f * c; + const auto x = c * (1.0f - std::abs(std::fmod(h / 60.0f, 2.0f) - 1.0f)); + + const auto hueSegment = static_cast(h / 60.0f); + + int r = 0, g = 0, b = 0; + + switch (hueSegment) { + case 0: r = static_cast(std::round(255 * (c + m))); + g = static_cast(std::round(255 * (x + m))); + b = static_cast(std::round(255 * m)); + break; + case 1: r = static_cast(std::round(255 * (x + m))); + g = static_cast(std::round(255 * (c + m))); + b = static_cast(std::round(255 * m)); + break; + case 2: r = static_cast(std::round(255 * m)); + g = static_cast(std::round(255 * (c + m))); + b = static_cast(std::round(255 * (x + m))); + break; + case 3: r = static_cast(std::round(255 * m)); + g = static_cast(std::round(255 * (x + m))); + b = static_cast(std::round(255 * (c + m))); + break; + case 4: r = static_cast(std::round(255 * (x + m))); + g = static_cast(std::round(255 * m)); + b = static_cast(std::round(255 * (c + m))); + break; + case 5: + case 6: r = static_cast(std::round(255 * (c + m))); + g = static_cast(std::round(255 * m)); + b = static_cast(std::round(255 * (x + m))); + break; + } + + r = constrain(r, 0, 255); + g = constrain(g, 0, 255); + b = constrain(b, 0, 255); + + return qRgb(r, g, b); +} + +QRgb ColorUtils::HSLToRGB(const std::array &hsl) { + return HSLToRGB(hsl[0], hsl[1], hsl[2]); +} + +void ColorUtils::colorToXYZ(QRgb color, double *outXyz) { + auto r = qRed(color) / 255.0; + auto g = qGreen(color) / 255.0; + auto b = qBlue(color) / 255.0; + + if (r > 0.04045) { + r = std::pow((r + 0.055) / 1.055, 2.4); + } else { + r = r / 12.92; + } + + if (g > 0.04045) { + g = std::pow((g + 0.055) / 1.055, 2.4); + } else { + g = g / 12.92; + } + + if (b > 0.04045) { + b = std::pow((b + 0.055) / 1.055, 2.4); + } else { + b = b / 12.92; + } + + outXyz[0] = 100 * (r * 0.4124 + g * 0.3576 + b * 0.1805); + outXyz[1] = 100 * (r * 0.2126 + g * 0.7152 + b * 0.0722); + outXyz[2] = 100 * (r * 0.0193 + g * 0.1192 + b * 0.9505); +} + +double ColorUtils::calculateLuminance(QRgb color) { + double xyz[3]; + colorToXYZ(color, xyz); + return xyz[1] / 100.0; +} + +double ColorUtils::calculateContrast(QRgb foreground, QRgb background) { + if (qAlpha(background) != 255) { + return -1.0; + } + + if (qAlpha(foreground) < 255) { + foreground = compositeColors(foreground, background); + } + + const auto luminance1 = calculateLuminance(foreground) + 0.05; + const auto luminance2 = calculateLuminance(background) + 0.05; + + return std::max(luminance1, luminance2) / std::min(luminance1, luminance2); +} + +int ColorUtils::calculateMinimumAlpha(QRgb foreground, QRgb background, float minContrastRatio) { + if (qAlpha(background) != 255) { + return -1; + } + + auto testForeground = setAlphaComponent(foreground, 255); + auto testRatio = calculateContrast(testForeground, background); + if (testRatio < minContrastRatio) { + return -1; + } + + constexpr int MAX_ITERATIONS = 10; + constexpr int PRECISION = 1; + + int numIterations = 0; + int minAlpha = 0; + int maxAlpha = 255; + + while (numIterations <= MAX_ITERATIONS && (maxAlpha - minAlpha) > PRECISION) { + const auto testAlpha = (minAlpha + maxAlpha) / 2; + + testForeground = setAlphaComponent(foreground, testAlpha); + testRatio = calculateContrast(testForeground, background); + + if (testRatio < minContrastRatio) { + minAlpha = testAlpha; + } else { + maxAlpha = testAlpha; + } + + numIterations++; + } + + return maxAlpha; +} + +int ColorUtils::compositeAlpha(int foregroundAlpha, int backgroundAlpha) { + return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF); +} + +int ColorUtils::compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) { + if (a == 0) return 0; + return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF); +} + +QRgb ColorUtils::compositeColors(QRgb foreground, QRgb background) { + const auto bgAlpha = qAlpha(background); + const auto fgAlpha = qAlpha(foreground); + const auto a = compositeAlpha(fgAlpha, bgAlpha); + + const auto r = compositeComponent(qRed(foreground), + fgAlpha, + qRed(background), + bgAlpha, + a); + const auto g = compositeComponent(qGreen(foreground), + fgAlpha, + qGreen(background), + bgAlpha, + a); + const auto b = compositeComponent(qBlue(foreground), + fgAlpha, + qBlue(background), + bgAlpha, + a); + + return qRgba(r, g, b, a); +} + +QRgb ColorUtils::setAlphaComponent(QRgb color, int alpha) { + return qRgba(qRed(color), qGreen(color), qBlue(color), alpha); +} + +} // namespace Ayu::Ui + diff --git a/Telegram/SourceFiles/ayu/ui/utils/color_utils.h b/Telegram/SourceFiles/ayu/ui/utils/color_utils.h new file mode 100644 index 0000000000..9fda39d15b --- /dev/null +++ b/Telegram/SourceFiles/ayu/ui/utils/color_utils.h @@ -0,0 +1,45 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#pragma once + +#include +#include +#include + +namespace Ayu::Ui { + +class ColorUtils +{ +public: + static std::array RGBToHSL(int r, int g, int b); + static std::array colorToHSL(QRgb color); + + static QRgb HSLToRGB(float h, float s, float l); + static QRgb HSLToRGB(const std::array &hsl); + + static double calculateLuminance(QRgb color); + static double calculateContrast(QRgb foreground, QRgb background); + static int calculateMinimumAlpha(QRgb foreground, QRgb background, float minContrastRatio); + static QRgb compositeColors(QRgb foreground, QRgb background); + static QRgb setAlphaComponent(QRgb color, int alpha); + +private: + static constexpr float constrain(float value, float min, float max) { + return value < min ? min : (value > max ? max : value); + } + + static constexpr int constrain(int value, int min, int max) { + return value < min ? min : (value > max ? max : value); + } + + static void colorToXYZ(QRgb color, double *outXyz); + static int compositeAlpha(int foregroundAlpha, int backgroundAlpha); + static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a); +}; + +} // namespace Ayu::Ui + diff --git a/Telegram/SourceFiles/ayu/ui/utils/palette.cpp b/Telegram/SourceFiles/ayu/ui/utils/palette.cpp new file mode 100644 index 0000000000..4f637602ae --- /dev/null +++ b/Telegram/SourceFiles/ayu/ui/utils/palette.cpp @@ -0,0 +1,610 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +// +// Code is based on https://github.com/androidx/androidx/blob/androidx-main/palette/palette/src/main/java/androidx/palette/graphics/Palette.java +#include "palette.h" +#include "color_utils.h" +#include "color_cut_quantizer.h" + +#include +#include +#include + +namespace Ayu::Ui { + +Swatch::Swatch(QRgb color, int population) + : _red(qRed(color)) + , _green(qGreen(color)) + , _blue(qBlue(color)) + , _rgb(color) + , _population(population) { +} + +QRgb Swatch::rgb() const { + return _rgb; +} + +int Swatch::red() const { + return _red; +} + +int Swatch::green() const { + return _green; +} + +int Swatch::blue() const { + return _blue; +} + +int Swatch::population() const { + return _population; +} + +std::array Swatch::hsl() const { + if (!_hslCalculated) { + _hsl = ColorUtils::RGBToHSL(_red, _green, _blue); + _hslCalculated = true; + } + return _hsl; +} + +QColor Swatch::titleTextColor() const { + ensureTextColorsGenerated(); + return _titleTextColor; +} + +QColor Swatch::bodyTextColor() const { + ensureTextColorsGenerated(); + return _bodyTextColor; +} + +void Swatch::ensureTextColorsGenerated() const { + if (!_generatedTextColors) { + const auto lightBodyAlpha = ColorUtils::calculateMinimumAlpha( + qRgb(255, 255, 255), + _rgb, + Palette::MIN_CONTRAST_BODY_TEXT); + const auto lightTitleAlpha = ColorUtils::calculateMinimumAlpha( + qRgb(255, 255, 255), + _rgb, + Palette::MIN_CONTRAST_TITLE_TEXT); + + if (lightBodyAlpha != -1 && lightTitleAlpha != -1) { + _bodyTextColor = QColor(255, 255, 255, lightBodyAlpha); + _titleTextColor = QColor(255, 255, 255, lightTitleAlpha); + _generatedTextColors = true; + return; + } + + const auto darkBodyAlpha = ColorUtils::calculateMinimumAlpha( + qRgb(0, 0, 0), + _rgb, + Palette::MIN_CONTRAST_BODY_TEXT); + const auto darkTitleAlpha = ColorUtils::calculateMinimumAlpha( + qRgb(0, 0, 0), + _rgb, + Palette::MIN_CONTRAST_TITLE_TEXT); + + if (darkBodyAlpha != -1 && darkTitleAlpha != -1) { + _bodyTextColor = QColor(0, 0, 0, darkBodyAlpha); + _titleTextColor = QColor(0, 0, 0, darkTitleAlpha); + _generatedTextColors = true; + return; + } + + _bodyTextColor = lightBodyAlpha != -1 + ? QColor(255, 255, 255, lightBodyAlpha) + : QColor(0, 0, 0, darkBodyAlpha); + _titleTextColor = lightTitleAlpha != -1 + ? QColor(255, 255, 255, lightTitleAlpha) + : QColor(0, 0, 0, darkTitleAlpha); + _generatedTextColors = true; + } +} + +const Target Target::LIGHT_VIBRANT = []() +{ + Target t; + setDefaultLightLightnessValues(t); + setDefaultVibrantSaturationValues(t); + return t; +}(); + +const Target Target::VIBRANT = []() +{ + Target t; + setDefaultNormalLightnessValues(t); + setDefaultVibrantSaturationValues(t); + return t; +}(); + +const Target Target::DARK_VIBRANT = []() +{ + Target t; + setDefaultDarkLightnessValues(t); + setDefaultVibrantSaturationValues(t); + return t; +}(); + +const Target Target::LIGHT_MUTED = []() +{ + Target t; + setDefaultLightLightnessValues(t); + setDefaultMutedSaturationValues(t); + return t; +}(); + +const Target Target::MUTED = []() +{ + Target t; + setDefaultNormalLightnessValues(t); + setDefaultMutedSaturationValues(t); + return t; +}(); + +const Target Target::DARK_MUTED = []() +{ + Target t; + setDefaultDarkLightnessValues(t); + setDefaultMutedSaturationValues(t); + return t; +}(); + +Target::Target() { + setTargetDefaultValues(_saturationTargets); + setTargetDefaultValues(_lightnessTargets); + setDefaultWeights(*this); +} + +Target::Target(const Target &from) + : _saturationTargets(from._saturationTargets) + , _lightnessTargets(from._lightnessTargets) + , _weights(from._weights) + , _isExclusive(from._isExclusive) { +} + +float Target::minimumSaturation() const { + return _saturationTargets[0]; +} + +float Target::targetSaturation() const { + return _saturationTargets[1]; +} + +float Target::maximumSaturation() const { + return _saturationTargets[2]; +} + +float Target::minimumLightness() const { + return _lightnessTargets[0]; +} + +float Target::targetLightness() const { + return _lightnessTargets[1]; +} + +float Target::maximumLightness() const { + return _lightnessTargets[2]; +} + +float Target::saturationWeight() const { + return _weights[0]; +} + +float Target::lightnessWeight() const { + return _weights[1]; +} + +float Target::populationWeight() const { + return _weights[2]; +} + +bool Target::isExclusive() const { + return _isExclusive; +} + +void Target::normalizeWeights() { + float sum = 0.0f; + for (const auto w : _weights) { + sum += w; + } + if (sum != 0.0f) { + for (auto &w : _weights) { + w /= sum; + } + } +} + +void Target::setDefaultLightLightnessValues(Target &target) { + target._lightnessTargets[0] = 0.55f; + target._lightnessTargets[1] = 0.74f; + target._lightnessTargets[2] = 1.0f; +} + +void Target::setDefaultNormalLightnessValues(Target &target) { + target._lightnessTargets[0] = 0.3f; + target._lightnessTargets[1] = 0.5f; + target._lightnessTargets[2] = 0.7f; +} + +void Target::setDefaultDarkLightnessValues(Target &target) { + target._lightnessTargets[0] = 0.0f; + target._lightnessTargets[1] = 0.26f; + target._lightnessTargets[2] = 0.45f; +} + +void Target::setDefaultVibrantSaturationValues(Target &target) { + target._saturationTargets[0] = 0.35f; + target._saturationTargets[1] = 1.0f; + target._saturationTargets[2] = 1.0f; +} + +void Target::setDefaultMutedSaturationValues(Target &target) { + target._saturationTargets[0] = 0.0f; + target._saturationTargets[1] = 0.3f; + target._saturationTargets[2] = 0.4f; +} + +void Target::setTargetDefaultValues(std::array &values) { + values[0] = 0.0f; + values[1] = 0.5f; + values[2] = 1.0f; +} + +void Target::setDefaultWeights(Target &target) { + target._weights[0] = 0.24f; + target._weights[1] = 0.52f; + target._weights[2] = 0.24f; +} + +Palette::Palette( + std::vector swatches, + std::vector targets) + : _swatches(std::move(swatches)) + , _targets(std::move(targets)) { +} + +const std::vector &Palette::swatches() const { + return _swatches; +} + +const std::vector &Palette::targets() const { + return _targets; +} + +const Swatch *Palette::vibrantSwatch() const { + return swatchForTarget(Target::VIBRANT); +} + +const Swatch *Palette::lightVibrantSwatch() const { + return swatchForTarget(Target::LIGHT_VIBRANT); +} + +const Swatch *Palette::darkVibrantSwatch() const { + return swatchForTarget(Target::DARK_VIBRANT); +} + +const Swatch *Palette::mutedSwatch() const { + return swatchForTarget(Target::MUTED); +} + +const Swatch *Palette::lightMutedSwatch() const { + return swatchForTarget(Target::LIGHT_MUTED); +} + +const Swatch *Palette::darkMutedSwatch() const { + return swatchForTarget(Target::DARK_MUTED); +} + +const Swatch *Palette::dominantSwatch() const { + return _dominantSwatch; +} + +QRgb Palette::vibrantColor(QRgb defaultColor) const { + return colorForTarget(Target::VIBRANT, defaultColor); +} + +QRgb Palette::lightVibrantColor(QRgb defaultColor) const { + return colorForTarget(Target::LIGHT_VIBRANT, defaultColor); +} + +QRgb Palette::darkVibrantColor(QRgb defaultColor) const { + return colorForTarget(Target::DARK_VIBRANT, defaultColor); +} + +QRgb Palette::mutedColor(QRgb defaultColor) const { + return colorForTarget(Target::MUTED, defaultColor); +} + +QRgb Palette::lightMutedColor(QRgb defaultColor) const { + return colorForTarget(Target::LIGHT_MUTED, defaultColor); +} + +QRgb Palette::darkMutedColor(QRgb defaultColor) const { + return colorForTarget(Target::DARK_MUTED, defaultColor); +} + +QRgb Palette::dominantColor(QRgb defaultColor) const { + return _dominantSwatch ? _dominantSwatch->rgb() : defaultColor; +} + +const Swatch *Palette::swatchForTarget(const Target &target) const { + for (const auto &[key, swatch] : _selectedSwatches) { + if (key == &target) { + return swatch; + } + } + return nullptr; +} + +QRgb Palette::colorForTarget(const Target &target, QRgb defaultColor) const { + const auto swatch = swatchForTarget(target); + return swatch ? swatch->rgb() : defaultColor; +} + +void Palette::generate() { + _dominantSwatch = findDominantSwatch(); + + for (auto &target : _targets) { + target.normalizeWeights(); + const auto swatch = generateScoredTarget(target); + _selectedSwatches[&target] = swatch; + } + + _usedColors.clear(); +} + +const Swatch *Palette::generateScoredTarget(const Target &target) { + const auto maxScoreSwatch = getMaxScoredSwatchForTarget(target); + if (maxScoreSwatch && target.isExclusive()) { + _usedColors.insert(maxScoreSwatch->rgb()); + } + return maxScoreSwatch; +} + +const Swatch *Palette::getMaxScoredSwatchForTarget(const Target &target) { + float maxScore = 0.0f; + const Swatch *maxScoreSwatch = nullptr; + + for (const auto &swatch : _swatches) { + if (shouldBeScoredForTarget(swatch, target)) { + const auto score = generateScore(swatch, target); + if (!maxScoreSwatch || score > maxScore) { + maxScoreSwatch = &swatch; + maxScore = score; + } + } + } + + return maxScoreSwatch; +} + +bool Palette::shouldBeScoredForTarget(const Swatch &swatch, const Target &target) { + const auto hsl = swatch.hsl(); + return hsl[1] >= target.minimumSaturation() + && hsl[1] <= target.maximumSaturation() + && hsl[2] >= target.minimumLightness() + && hsl[2] <= target.maximumLightness() + && _usedColors.find(swatch.rgb()) == _usedColors.end(); +} + +float Palette::generateScore(const Swatch &swatch, const Target &target) { + const auto hsl = swatch.hsl(); + + float saturationScore = 0.0f; + float luminanceScore = 0.0f; + float populationScore = 0.0f; + + const auto maxPopulation = _dominantSwatch ? _dominantSwatch->population() : 1; + + if (target.saturationWeight() > 0) { + saturationScore = target.saturationWeight() + * (1.0f - std::abs(hsl[1] - target.targetSaturation())); + } + if (target.lightnessWeight() > 0) { + luminanceScore = target.lightnessWeight() + * (1.0f - std::abs(hsl[2] - target.targetLightness())); + } + if (target.populationWeight() > 0) { + populationScore = target.populationWeight() + * (static_cast(swatch.population()) / static_cast(maxPopulation)); + } + + return saturationScore + luminanceScore + populationScore; +} + +const Swatch *Palette::findDominantSwatch() { + int maxPop = 0; + const Swatch *maxSwatch = nullptr; + + for (const auto &swatch : _swatches) { + if (swatch.population() > maxPop) { + maxSwatch = &swatch; + maxPop = swatch.population(); + } + } + + return maxSwatch; +} + +Palette::Builder Palette::from(const QPixmap &pixmap) { + return Builder(pixmap); +} + +Palette::Builder Palette::from(const QImage &image) { + return Builder(image); +} + +Palette Palette::fromSwatches(const std::vector &swatches) { + return Builder(swatches).generate(); +} + +Palette::Filter Palette::Builder::DEFAULT_FILTER = [](QRgb rgb, const std::array &hsl) +{ + return hsl[2] > 0.05f && hsl[2] < 0.95f; +}; + +Palette::Builder::Builder(const QPixmap &pixmap) + : Builder(pixmap.toImage()) { +} + +Palette::Builder::Builder(const QImage &image) + : _image(image) + , _hasImage(true) { + _filters.push_back(DEFAULT_FILTER); + + _targets.push_back(Target::LIGHT_VIBRANT); + _targets.push_back(Target::VIBRANT); + _targets.push_back(Target::DARK_VIBRANT); + _targets.push_back(Target::LIGHT_MUTED); + _targets.push_back(Target::MUTED); + _targets.push_back(Target::DARK_MUTED); +} + +Palette::Builder::Builder(const std::vector &swatches) + : _swatches(swatches) + , _hasImage(false) { + _filters.push_back(DEFAULT_FILTER); +} + +Palette::Builder &Palette::Builder::maximumColorCount(int colors) { + _maxColors = colors; + return *this; +} + +Palette::Builder &Palette::Builder::resizeBitmapArea(int area) { + _resizeArea = area; + return *this; +} + +Palette::Builder &Palette::Builder::clearFilters() { + _filters.clear(); + return *this; +} + +Palette::Builder &Palette::Builder::addFilter(Filter filter) { + _filters.push_back(std::move(filter)); + return *this; +} + +Palette::Builder &Palette::Builder::setRegion(int left, int top, int right, int bottom) { + if (_hasImage) { + _region = QRect(0, 0, _image.width(), _image.height()); + _region = _region.intersected(QRect(left, top, right - left, bottom - top)); + _hasRegion = true; + } + return *this; +} + +Palette::Builder &Palette::Builder::clearRegion() { + _hasRegion = false; + _region = QRect(); + return *this; +} + +Palette::Builder &Palette::Builder::addTarget(const Target &target) { + bool found = false; + for (const auto &t : _targets) { + if (&t == &target) { + found = true; + break; + } + } + if (!found) { + _targets.push_back(target); + } + return *this; +} + +Palette::Builder &Palette::Builder::clearTargets() { + _targets.clear(); + return *this; +} + +Palette Palette::Builder::generate() { + std::vector swatches; + + if (_hasImage) { + auto bitmap = scaleBitmapDown(_image); + + if (_hasRegion) { + const auto scale = static_cast(bitmap.width()) / _image.width(); + const auto left = static_cast(std::floor(_region.left() * scale)); + const auto top = static_cast(std::floor(_region.top() * scale)); + const auto right = std::min( + static_cast(std::ceil(_region.right() * scale)), + bitmap.width()); + const auto bottom = std::min( + static_cast(std::ceil(_region.bottom() * scale)), + bitmap.height()); + _region = QRect(left, top, right - left, bottom - top); + } + + const auto pixels = getPixelsFromImage(bitmap); + + std::vector filterPtrs; + for (auto &filter : _filters) { + filterPtrs.push_back(&filter); + } + + ColorCutQuantizer quantizer( + pixels, + _maxColors, + filterPtrs); + + swatches = quantizer.quantizedColors(); + } else { + swatches = _swatches; + } + + auto palette = Palette(std::move(swatches), _targets); + palette.generate(); + + return palette; +} + +std::vector Palette::Builder::getPixelsFromImage(const QImage &image) { + std::vector pixels; + + const auto img = image.convertToFormat(QImage::Format_ARGB32); + + if (_hasRegion) { + pixels.reserve(_region.width() * _region.height()); + for (int y = _region.top(); y < _region.bottom(); ++y) { + const auto line = reinterpret_cast(img.scanLine(y)); + for (int x = _region.left(); x < _region.right(); ++x) { + pixels.push_back(line[x]); + } + } + } else { + pixels.reserve(img.width() * img.height()); + for (int y = 0; y < img.height(); ++y) { + const auto line = reinterpret_cast(img.scanLine(y)); + for (int x = 0; x < img.width(); ++x) { + pixels.push_back(line[x]); + } + } + } + + return pixels; +} + +QImage Palette::Builder::scaleBitmapDown(const QImage &image) { + const auto area = image.width() * image.height(); + + if (_resizeArea > 0 && area > _resizeArea) { + const auto scale = std::sqrt(static_cast(_resizeArea) / area); + const auto newWidth = static_cast(image.width() * scale); + const auto newHeight = static_cast(image.height() * scale); + return image.scaled(newWidth, newHeight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + } + + return image; +} + +} // namespace Ayu::Ui + diff --git a/Telegram/SourceFiles/ayu/ui/utils/palette.h b/Telegram/SourceFiles/ayu/ui/utils/palette.h new file mode 100644 index 0000000000..d331da67ad --- /dev/null +++ b/Telegram/SourceFiles/ayu/ui/utils/palette.h @@ -0,0 +1,183 @@ +// This is the source code of AyuGram for Desktop. +// +// We do not and cannot prevent the use of our code, +// but be respectful and credit the original author. +// +// Copyright @Radolyn, 2025 +#pragma once + +#include +#include +#include +#include + +namespace Ayu::Ui { + +class Swatch +{ +public: + Swatch(QRgb color, int population); + + [[nodiscard]] QRgb rgb() const; + [[nodiscard]] int red() const; + [[nodiscard]] int green() const; + [[nodiscard]] int blue() const; + [[nodiscard]] int population() const; + [[nodiscard]] std::array hsl() const; + + [[nodiscard]] QColor titleTextColor() const; + [[nodiscard]] QColor bodyTextColor() const; + +private: + void ensureTextColorsGenerated() const; + + int _red; + int _green; + int _blue; + QRgb _rgb; + int _population; + + mutable bool _generatedTextColors = false; + mutable QColor _titleTextColor; + mutable QColor _bodyTextColor; + mutable std::array _hsl; + mutable bool _hslCalculated = false; +}; + +class Target +{ +public: + static const Target LIGHT_VIBRANT; + static const Target VIBRANT; + static const Target DARK_VIBRANT; + static const Target LIGHT_MUTED; + static const Target MUTED; + static const Target DARK_MUTED; + + Target(); + Target(const Target &from); + + [[nodiscard]] float minimumSaturation() const; + [[nodiscard]] float targetSaturation() const; + [[nodiscard]] float maximumSaturation() const; + [[nodiscard]] float minimumLightness() const; + [[nodiscard]] float targetLightness() const; + [[nodiscard]] float maximumLightness() const; + [[nodiscard]] float saturationWeight() const; + [[nodiscard]] float lightnessWeight() const; + [[nodiscard]] float populationWeight() const; + [[nodiscard]] bool isExclusive() const; + + void normalizeWeights(); + +private: + static void setDefaultLightLightnessValues(Target &target); + static void setDefaultNormalLightnessValues(Target &target); + static void setDefaultDarkLightnessValues(Target &target); + static void setDefaultVibrantSaturationValues(Target &target); + static void setDefaultMutedSaturationValues(Target &target); + static void setTargetDefaultValues(std::array &values); + static void setDefaultWeights(Target &target); + + std::array _saturationTargets; + std::array _lightnessTargets; + std::array _weights; + bool _isExclusive = true; +}; + +class Palette +{ +public: + using Filter = std::function &)>; + + class Builder; + + [[nodiscard]] const std::vector &swatches() const; + [[nodiscard]] const std::vector &targets() const; + + [[nodiscard]] const Swatch *vibrantSwatch() const; + [[nodiscard]] const Swatch *lightVibrantSwatch() const; + [[nodiscard]] const Swatch *darkVibrantSwatch() const; + [[nodiscard]] const Swatch *mutedSwatch() const; + [[nodiscard]] const Swatch *lightMutedSwatch() const; + [[nodiscard]] const Swatch *darkMutedSwatch() const; + [[nodiscard]] const Swatch *dominantSwatch() const; + + [[nodiscard]] QRgb vibrantColor(QRgb defaultColor) const; + [[nodiscard]] QRgb lightVibrantColor(QRgb defaultColor) const; + [[nodiscard]] QRgb darkVibrantColor(QRgb defaultColor) const; + [[nodiscard]] QRgb mutedColor(QRgb defaultColor) const; + [[nodiscard]] QRgb lightMutedColor(QRgb defaultColor) const; + [[nodiscard]] QRgb darkMutedColor(QRgb defaultColor) const; + [[nodiscard]] QRgb dominantColor(QRgb defaultColor) const; + + [[nodiscard]] const Swatch *swatchForTarget(const Target &target) const; + [[nodiscard]] QRgb colorForTarget(const Target &target, QRgb defaultColor) const; + + static Builder from(const QPixmap &pixmap); + static Builder from(const QImage &image); + static Palette fromSwatches(const std::vector &swatches); + + static constexpr int DEFAULT_RESIZE_BITMAP_AREA = 112 * 112; + static constexpr int DEFAULT_CALCULATE_NUMBER_COLORS = 16; + static constexpr float MIN_CONTRAST_TITLE_TEXT = 3.0f; + static constexpr float MIN_CONTRAST_BODY_TEXT = 4.5f; + +private: + Palette( + std::vector swatches, + std::vector targets); + + void generate(); + const Swatch *generateScoredTarget(const Target &target); + const Swatch *getMaxScoredSwatchForTarget(const Target &target); + bool shouldBeScoredForTarget(const Swatch &swatch, const Target &target); + float generateScore(const Swatch &swatch, const Target &target); + const Swatch *findDominantSwatch(); + + std::vector _swatches; + std::vector _targets; + std::map _selectedSwatches; + std::set _usedColors; + const Swatch *_dominantSwatch = nullptr; + + friend class Builder; +}; + +class Palette::Builder +{ +public: + explicit Builder(const QPixmap &pixmap); + explicit Builder(const QImage &image); + explicit Builder(const std::vector &swatches); + + Builder &maximumColorCount(int colors); + Builder &resizeBitmapArea(int area); + Builder &clearFilters(); + Builder &addFilter(Filter filter); + Builder &setRegion(int left, int top, int right, int bottom); + Builder &clearRegion(); + Builder &addTarget(const Target &target); + Builder &clearTargets(); + + [[nodiscard]] Palette generate(); + +private: + std::vector getPixelsFromImage(const QImage &image); + QImage scaleBitmapDown(const QImage &image); + + std::vector _swatches; + QImage _image; + std::vector _targets; + int _maxColors = DEFAULT_CALCULATE_NUMBER_COLORS; + int _resizeArea = DEFAULT_RESIZE_BITMAP_AREA; + std::vector _filters; + QRect _region; + bool _hasRegion = false; + bool _hasImage = false; + + static Filter DEFAULT_FILTER; +}; + +} // namespace Ayu::Ui + diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index c6bf2e6203..7c68d0aef2 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -1293,6 +1293,6 @@ infoMusicButtonTitle: FlatLabel(defaultFlatLabel) { style: semiboldTextStyle; maxHeight: 20px; } -infoMusicButtonPadding: margins(48px, 6px, 12px, 6px); +infoMusicButtonPadding: margins(12px, 8px, 12px, 8px); infoMusicButtonBottom: 8px; infoMusicButtonLine: 2px; diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index 5b4dad2aee..f52fbc11ce 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -59,7 +59,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_menu_icons.h" // AyuGram includes +#include "ayu/ui/components/now_playing.h" #include "ayu/utils/telegram_helpers.h" +#include "data/data_file_origin.h" #include "ui/toast/toast.h" @@ -130,12 +132,15 @@ constexpr auto kGlareTimeout = crl::time(1000); } [[nodiscard]] MusicButtonData DocumentMusicButtonData( - not_null document) { + not_null document, HistoryItem *item) { if (const auto song = document->song()) { if (!song->performer.isEmpty() || !song->title.isEmpty()) { + const auto mediaView = document->createMediaView(); + mediaView->thumbnailWanted(Data::FileOrigin(item->fullId())); return { .performer = song->performer, .title = song->title, + .mediaView = mediaView }; } } @@ -867,21 +872,21 @@ void Cover::setupSavedMusic() { resize(width(), _st.height); } else if (!_musicButton) { using namespace Info::Saved; - _musicButton = std::make_unique( + _musicButton = std::make_unique( this, - DocumentMusicButtonData(document), + DocumentMusicButtonData(document, item), [=] { _controller->showSection(MakeMusic(_peer)); }); _musicButton->show(); widthValue( ) | rpl::start_with_next([=](int newWidth) { _musicButton->resizeToWidth(newWidth); - const auto skip = st::infoMusicButtonBottom; - _musicButton->moveToLeft(0, _st.height - skip, newWidth); + // const auto skip = st::infoMusicButtonBottom; + _musicButton->moveToLeft(0, _st.height, newWidth); resize(width(), _st.height + _musicButton->height()); }, _musicButton->lifetime()); } else { - _musicButton->updateData(DocumentMusicButtonData(document)); + _musicButton->updateData(DocumentMusicButtonData(document, item)); } }, lifetime()); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.h b/Telegram/SourceFiles/info/profile/info_profile_cover.h index 5b14719500..91e9625436 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.h +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.h @@ -45,7 +45,7 @@ struct InfoProfileCover; namespace Info::Profile { class EmojiStatusPanel; -class MusicButton; +class AyuMusicButton; class Badge; class TopicIconView final { @@ -182,7 +182,7 @@ private: //object_ptr _dropArea = { nullptr }; base::Timer _refreshStatusTimer; - std::unique_ptr _musicButton; + std::unique_ptr _musicButton; rpl::event_stream
_showSection; diff --git a/Telegram/SourceFiles/info/profile/info_profile_music_button.h b/Telegram/SourceFiles/info/profile/info_profile_music_button.h index 2e909478d5..749ae4d5be 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_music_button.h +++ b/Telegram/SourceFiles/info/profile/info_profile_music_button.h @@ -13,11 +13,16 @@ namespace Ui { class FlatLabel; } // namespace Ui +namespace Data { +class DocumentMedia; +} + namespace Info::Profile { struct MusicButtonData { QString performer; QString title; + std::shared_ptr mediaView; }; class MusicButton final : public Ui::RippleButton { diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 142ae29fe3..0ce5509264 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -154,8 +154,6 @@ PRIVATE info/profile/info_profile_icon.cpp info/profile/info_profile_icon.h - info/profile/info_profile_music_button.cpp - info/profile/info_profile_music_button.h info/userpic/info_userpic_bubble_wrap.cpp info/userpic/info_userpic_bubble_wrap.h info/userpic/info_userpic_color_circle_button.cpp