2
0
mirror of https://github.com/kotatogram/kotatogram-desktop synced 2025-08-31 14:45:14 +00:00

Colorize bubbles according to a custom chat theme.

This commit is contained in:
John Preston
2021-08-27 23:44:47 +03:00
parent 5de83ef30c
commit beff635e45
38 changed files with 479 additions and 165 deletions

View File

@@ -18,19 +18,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Ui {
namespace {
struct CornersPixmaps {
QPixmap p[4];
};
std::vector<CornersPixmaps> Corners;
base::flat_map<uint32, CornersPixmaps> CornersMap;
QImage CornersMaskLarge[4], CornersMaskSmall[4];
rpl::lifetime PaletteChangedLifetime;
void PrepareCorners(CachedRoundCorners index, int32 radius, const QBrush &brush, const style::color *shadow = nullptr, QImage *cors = nullptr) {
Expects(Corners.size() > index);
[[nodiscard]] std::array<QImage, 4> PrepareCorners(int32 radius, const QBrush &brush, const style::color *shadow = nullptr) {
int32 r = radius * style::DevicePixelRatio(), s = st::msgShadow * style::DevicePixelRatio();
QImage rect(r * 3, r * 3 + (shadow ? s : 0), QImage::Format_ARGB32_Premultiplied), localCors[4];
QImage rect(r * 3, r * 3 + (shadow ? s : 0), QImage::Format_ARGB32_Premultiplied);
{
Painter p(&rect);
PainterHighQualityEnabler hq(p);
@@ -46,27 +41,31 @@ void PrepareCorners(CachedRoundCorners index, int32 radius, const QBrush &brush,
p.setBrush(brush);
p.drawRoundedRect(0, 0, r * 3, r * 3, r, r);
}
if (!cors) cors = localCors;
cors[0] = rect.copy(0, 0, r, r);
cors[1] = rect.copy(r * 2, 0, r, r);
cors[2] = rect.copy(0, r * 2, r, r + (shadow ? s : 0));
cors[3] = rect.copy(r * 2, r * 2, r, r + (shadow ? s : 0));
if (index != SmallMaskCorners && index != LargeMaskCorners) {
for (int i = 0; i < 4; ++i) {
Corners[index].p[i] = PixmapFromImage(std::move(cors[i]));
Corners[index].p[i].setDevicePixelRatio(style::DevicePixelRatio());
}
auto result = std::array<QImage, 4>();
result[0] = rect.copy(0, 0, r, r);
result[1] = rect.copy(r * 2, 0, r, r);
result[2] = rect.copy(0, r * 2, r, r + (shadow ? s : 0));
result[3] = rect.copy(r * 2, r * 2, r, r + (shadow ? s : 0));
return result;
}
void PrepareCorners(CachedRoundCorners index, int32 radius, const QBrush &brush, const style::color *shadow = nullptr) {
Expects(index < Corners.size());
auto images = PrepareCorners(radius, brush, shadow);
for (int i = 0; i < 4; ++i) {
Corners[index].p[i] = PixmapFromImage(std::move(images[i]));
Corners[index].p[i].setDevicePixelRatio(style::DevicePixelRatio());
}
}
void CreateMaskCorners() {
QImage mask[4];
PrepareCorners(SmallMaskCorners, st::roundRadiusSmall, QColor(255, 255, 255), nullptr, mask);
auto mask = PrepareCorners(st::roundRadiusSmall, QColor(255, 255, 255), nullptr);
for (int i = 0; i < 4; ++i) {
CornersMaskSmall[i] = mask[i].convertToFormat(QImage::Format_ARGB32_Premultiplied);
CornersMaskSmall[i].setDevicePixelRatio(style::DevicePixelRatio());
}
PrepareCorners(LargeMaskCorners, st::historyMessageRadius, QColor(255, 255, 255), nullptr, mask);
mask = PrepareCorners(st::historyMessageRadius, QColor(255, 255, 255), nullptr);
for (int i = 0; i < 4; ++i) {
CornersMaskLarge[i] = mask[i].convertToFormat(QImage::Format_ARGB32_Premultiplied);
CornersMaskLarge[i].setDevicePixelRatio(style::DevicePixelRatio());
@@ -231,23 +230,40 @@ void FillRoundShadow(Painter &p, int32 x, int32 y, int32 w, int32 h, style::colo
}
}
void FillRoundRect(Painter &p, int32 x, int32 y, int32 w, int32 h, style::color bg, ImageRoundRadius radius, RectParts parts) {
auto colorKey = ((uint32(bg->c.alpha()) & 0xFF) << 24) | ((uint32(bg->c.red()) & 0xFF) << 16) | ((uint32(bg->c.green()) & 0xFF) << 8) | ((uint32(bg->c.blue()) & 0xFF) << 24);
auto i = CornersMap.find(colorKey);
if (i == CornersMap.cend()) {
QImage images[4];
switch (radius) {
case ImageRoundRadius::Small: PrepareCorners(SmallMaskCorners, st::roundRadiusSmall, bg, nullptr, images); break;
case ImageRoundRadius::Large: PrepareCorners(LargeMaskCorners, st::historyMessageRadius, bg, nullptr, images); break;
default: p.fillRect(x, y, w, h, bg); return;
}
CornersPixmaps PrepareCornerPixmaps(int32 radius, style::color bg, const style::color *sh) {
auto images = PrepareCorners(radius, bg, sh);
auto result = CornersPixmaps();
for (int j = 0; j < 4; ++j) {
result.p[j] = PixmapFromImage(std::move(images[j]));
result.p[j].setDevicePixelRatio(style::DevicePixelRatio());
}
return result;
}
CornersPixmaps pixmaps;
for (int j = 0; j < 4; ++j) {
pixmaps.p[j] = PixmapFromImage(std::move(images[j]));
pixmaps.p[j].setDevicePixelRatio(style::DevicePixelRatio());
}
i = CornersMap.emplace(colorKey, pixmaps).first;
CornersPixmaps PrepareCornerPixmaps(ImageRoundRadius radius, style::color bg, const style::color *sh) {
switch (radius) {
case ImageRoundRadius::Small:
return PrepareCornerPixmaps(st::roundRadiusSmall, bg, sh);
case ImageRoundRadius::Large:
return PrepareCornerPixmaps(st::historyMessageRadius, bg, sh);
}
Unexpected("Image round radius in PrepareCornerPixmaps.");
}
void FillRoundRect(Painter &p, int32 x, int32 y, int32 w, int32 h, style::color bg, ImageRoundRadius radius, RectParts parts) {
if (radius == ImageRoundRadius::None) {
p.fillRect(x, y, w, h, bg);
return;
}
const auto colorKey = ((uint32(bg->c.alpha()) & 0xFF) << 24)
| ((uint32(bg->c.red()) & 0xFF) << 16)
| ((uint32(bg->c.green()) & 0xFF) << 8)
| ((uint32(bg->c.blue()) & 0xFF));
auto i = CornersMap.find(colorKey);
if (i == end(CornersMap)) {
i = CornersMap.emplace(
colorKey,
PrepareCornerPixmaps(radius, bg, nullptr)).first;
}
FillRoundRect(p, x, y, w, h, bg, i->second, nullptr, parts);
}

View File

@@ -15,10 +15,11 @@ enum class ImageRoundRadius;
namespace Ui {
enum CachedRoundCorners : int {
SmallMaskCorners = 0x00, // for images
LargeMaskCorners,
struct CornersPixmaps {
QPixmap p[4];
};
enum CachedRoundCorners : int {
BoxCorners,
MenuCorners,
BotKbOverCorners,
@@ -69,6 +70,16 @@ inline void FillRoundRect(Painter &p, const QRect &rect, style::color bg, ImageR
return FillRoundRect(p, rect.x(), rect.y(), rect.width(), rect.height(), bg, radius, parts);
}
[[nodiscard]] CornersPixmaps PrepareCornerPixmaps(
int32 radius,
style::color bg,
const style::color *sh);
[[nodiscard]] CornersPixmaps PrepareCornerPixmaps(
ImageRoundRadius radius,
style::color bg,
const style::color *sh);
void FillRoundRect(Painter &p, int32 x, int32 y, int32 w, int32 h, style::color bg, const CornersPixmaps &corner, const style::color *shadow = nullptr, RectParts parts = RectPart::Full);
void StartCachedCorners();
void FinishCachedCorners();

View File

@@ -0,0 +1,125 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "styles/style_chat.h"
namespace Ui {
ChatStyle::ChatStyle() {
finalize();
messageIcon(
&MessageStyle::tailLeft,
st::historyBubbleTailInLeft,
st::historyBubbleTailInLeftSelected,
st::historyBubbleTailOutLeft,
st::historyBubbleTailOutLeftSelected);
messageIcon(
&MessageStyle::tailRight,
st::historyBubbleTailInRight,
st::historyBubbleTailInRightSelected,
st::historyBubbleTailOutRight,
st::historyBubbleTailOutRightSelected);
messageColor(
&MessageStyle::msgBg,
msgInBg(),
msgInBgSelected(),
msgOutBg(),
msgOutBgSelected());
messageColor(
&MessageStyle::msgShadow,
msgInShadow(),
msgInShadowSelected(),
msgOutShadow(),
msgOutShadowSelected());
}
void ChatStyle::apply(not_null<ChatTheme*> theme) {
const auto themePalette = theme->palette();
assignPalette(themePalette
? themePalette
: style::main_palette::get().get());
if (themePalette) {
_defaultPaletteChangeLifetime.destroy();
} else {
style::PaletteChanged(
) | rpl::start_with_next([=] {
assignPalette(style::main_palette::get());
}, _defaultPaletteChangeLifetime);
}
}
void ChatStyle::assignPalette(not_null<const style::palette*> palette) {
*static_cast<style::palette*>(this) = *palette;
style::internal::resetIcons();
for (auto &style : _messageStyles) {
style.corners = {};
}
}
const MessageStyle &ChatStyle::messageStyle(bool outbg, bool selected) const {
auto &result = messageStyleRaw(outbg, selected);
if (result.corners.p[0].isNull()) {
result.corners = Ui::PrepareCornerPixmaps(
st::historyMessageRadius,
result.msgBg,
&result.msgShadow);
}
return result;
}
MessageStyle &ChatStyle::messageStyleRaw(bool outbg, bool selected) const {
return _messageStyles[(outbg ? 2 : 0) + (selected ? 1 : 0)];
}
void ChatStyle::icon(style::icon &my, const style::icon &original) {
my = original.withPalette(*this);
}
MessageStyle &ChatStyle::messageIn() {
return messageStyleRaw(false, false);
}
MessageStyle &ChatStyle::messageInSelected() {
return messageStyleRaw(false, true);
}
MessageStyle &ChatStyle::messageOut() {
return messageStyleRaw(true, false);
}
MessageStyle &ChatStyle::messageOutSelected() {
return messageStyleRaw(true, true);
}
void ChatStyle::messageIcon(
style::icon MessageStyle::*my,
const style::icon &originalIn,
const style::icon &originalInSelected,
const style::icon &originalOut,
const style::icon &originalOutSelected) {
icon(messageIn().*my, originalIn);
icon(messageInSelected().*my, originalInSelected);
icon(messageOut().*my, originalOut);
icon(messageOutSelected().*my, originalOutSelected);
}
void ChatStyle::messageColor(
style::color MessageStyle::*my,
const style::color &originalIn,
const style::color &originalInSelected,
const style::color &originalOut,
const style::color &originalOutSelected) {
messageIn().*my = originalIn;
messageInSelected().*my = originalInSelected;
messageOut().*my = originalOut;
messageOutSelected().*my = originalOutSelected;
}
} // namespace Ui

View File

@@ -0,0 +1,65 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/cached_round_corners.h"
namespace Ui {
class ChatTheme;
struct MessageStyle {
CornersPixmaps corners;
style::icon tailLeft = { Qt::Uninitialized };
style::icon tailRight = { Qt::Uninitialized };
style::color msgBg;
style::color msgShadow;
};
class ChatStyle final : public style::palette {
public:
ChatStyle();
void apply(not_null<ChatTheme*> theme);
[[nodiscard]] const MessageStyle &messageStyle(
bool outbg,
bool selected) const;
private:
void assignPalette(not_null<const style::palette*> palette);
void icon(style::icon &my, const style::icon &original);
[[nodiscard]] MessageStyle &messageStyleRaw(
bool outbg,
bool selected) const;
[[nodiscard]] MessageStyle &messageIn();
[[nodiscard]] MessageStyle &messageInSelected();
[[nodiscard]] MessageStyle &messageOut();
[[nodiscard]] MessageStyle &messageOutSelected();
void messageIcon(
style::icon MessageStyle::*my,
const style::icon &originalIn,
const style::icon &originalInSelected,
const style::icon &originalOut,
const style::icon &originalOutSelected);
void messageColor(
style::color MessageStyle::*my,
const style::color &originalIn,
const style::color &originalInSelected,
const style::color &originalOut,
const style::color &originalOutSelected);
mutable std::array<MessageStyle, 4> _messageStyles;
rpl::lifetime _defaultPaletteChangeLifetime;
};
} // namespace Ui

View File

@@ -30,6 +30,8 @@ constexpr auto kMaxSize = 2960;
[[nodiscard]] CacheBackgroundResult CacheBackground(
const CacheBackgroundRequest &request) {
Expects(!request.area.isEmpty());
const auto gradient = request.background.gradientForFill.isNull()
? QImage()
: (request.gradientRotationAdd != 0)
@@ -69,12 +71,14 @@ constexpr auto kMaxSize = 2960;
Qt::KeepAspectRatio,
Qt::SmoothTransformation)
: request.background.preparedForTiled;
const auto w = tiled.width() / style::DevicePixelRatio();
const auto h = tiled.height() / style::DevicePixelRatio();
const auto w = tiled.width() / float(style::DevicePixelRatio());
const auto h = tiled.height() / float(style::DevicePixelRatio());
const auto cx = int(std::ceil(request.area.width() / w));
const auto cy = int(std::ceil(request.area.height() / h));
const auto rows = cy;
const auto cols = request.background.isPattern ? (((cx / 2) * 2) + 1) : cx;
const auto cols = request.background.isPattern
? (((cx / 2) * 2) + 1)
: cx;
const auto xshift = request.background.isPattern
? (request.area.width() - cols * w) / 2
: 0;
@@ -202,13 +206,14 @@ void ChatTheme::setBubblesBackground(QImage image) {
});
}
if (!_bubblesBackgroundPattern) {
_bubblesBackgroundPattern = PrepareBubblePattern();
_bubblesBackgroundPattern = PrepareBubblePattern(palette());
}
_bubblesBackgroundPattern->pixmap = _bubblesBackground.pixmap;
_repaintBackgroundRequests.fire({});
}
ChatPaintContext ChatTheme::preparePaintContext(
not_null<const ChatStyle*> st,
QRect viewport,
QRect clip) {
_bubblesBackground.area = viewport.size();
@@ -223,7 +228,7 @@ ChatPaintContext ChatTheme::preparePaintContext(
// _bubblesBackgroundPattern->pixmap = _bubblesBackground.pixmap;
//}
return {
.st = _palette ? _palette.get() : style::main_palette::get(),
.st = st,
.bubblesPattern = _bubblesBackgroundPattern.get(),
.viewport = viewport,
.clip = clip,
@@ -240,6 +245,7 @@ const BackgroundState &ChatTheme::backgroundState(QSize area) {
&& !background().gradientForFill.isNull()) {
// We don't support direct painting of patterned gradients.
// So we need to sync-generate cache image here.
_willCacheForArea = area;
setCachedBackground(CacheBackground(currentCacheRequest(area)));
_cacheBackgroundTimer->cancel();
} else if (_backgroundState.now.area != area) {

View File

@@ -13,10 +13,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Ui {
class ChatStyle;
struct BubblePattern;
struct ChatPaintContext {
not_null<const style::palette*> st;
not_null<const ChatStyle*> st;
const BubblePattern *bubblesPattern = nullptr;
QRect viewport;
QRect clip;
@@ -112,22 +113,23 @@ public:
ChatTheme(ChatThemeDescriptor &&descriptor);
[[nodiscard]] uint64 key() const;
[[nodiscard]] not_null<const style::palette*> palette() const {
[[nodiscard]] const style::palette *palette() const {
return _palette.get();
}
void setBackground(ChatThemeBackground &&background);
void updateBackgroundImageFrom(ChatThemeBackground &&background);
const ChatThemeBackground &background() const {
[[nodiscard]] const ChatThemeBackground &background() const {
return _mutableBackground;
}
void setBubblesBackground(QImage image);
const BubblePattern *bubblesBackgroundPattern() const {
[[nodiscard]] const BubblePattern *bubblesBackgroundPattern() const {
return _bubblesBackgroundPattern.get();
}
[[nodiscard]] ChatPaintContext preparePaintContext(
not_null<const ChatStyle*> st,
QRect viewport,
QRect clip);
[[nodiscard]] const BackgroundState &backgroundState(QSize area);

View File

@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/cached_round_corners.h"
#include "ui/image/image_prepare.h"
#include "ui/chat/chat_style.h"
#include "styles/style_chat.h"
namespace Ui {
@@ -79,12 +80,10 @@ void PaintBubbleGeneric(
}
void PaintPatternBubble(Painter &p, const SimpleBubble &args) {
const auto opacity = st::msgOutBg->c.alphaF();
const auto shadowOpacity = opacity * st::msgOutShadow->c.alphaF();
const auto opacity = args.st->msgOutBg()->c.alphaF();
const auto shadowOpacity = opacity * args.st->msgOutShadow()->c.alphaF();
const auto pattern = args.pattern;
const auto sh = (args.skip & RectPart::Bottom)
? nullptr
: &st::msgOutShadow;
const auto sh = !(args.skip & RectPart::Bottom);
const auto &tail = (args.tailSide == RectPart::Right)
? pattern->tailRight
: pattern->tailLeft;
@@ -221,30 +220,14 @@ void PaintPatternBubble(Painter &p, const SimpleBubble &args) {
}
void PaintSolidBubble(Painter &p, const SimpleBubble &args) {
const auto &bg = args.selected
? (args.outbg ? st::msgOutBgSelected : st::msgInBgSelected)
: (args.outbg ? st::msgOutBg : st::msgInBg);
const auto &st = args.st->messageStyle(args.outbg, args.selected);
const auto &bg = st.msgBg;
const auto sh = (args.skip & RectPart::Bottom)
? nullptr
: args.selected
? &(args.outbg ? st::msgOutShadowSelected : st::msgInShadowSelected)
: &(args.outbg ? st::msgOutShadow : st::msgInShadow);
const auto corners = args.selected
? (args.outbg
? MessageOutSelectedCorners
: MessageInSelectedCorners)
: (args.outbg ? MessageOutCorners : MessageInCorners);
: &st.msgShadow;
const auto &tail = (args.tailSide == RectPart::Right)
? (args.selected
? st::historyBubbleTailOutRightSelected
: st::historyBubbleTailOutRight)
: args.selected
? (args.outbg
? st::historyBubbleTailOutLeftSelected
: st::historyBubbleTailInLeftSelected)
: (args.outbg
? st::historyBubbleTailOutLeft
: st::historyBubbleTailInLeft);
? st.tailRight
: st.tailLeft;
const auto tailShift = (args.tailSide == RectPart::Right)
? QPoint(0, tail.height())
: QPoint(tail.width(), tail.height());
@@ -253,7 +236,7 @@ void PaintSolidBubble(Painter &p, const SimpleBubble &args) {
}, [&](const QRect &rect) {
p.fillRect(rect, *sh);
}, [&](const QRect &rect, RectParts parts) {
Ui::FillRoundRect(p, rect, bg, corners, sh, parts);
Ui::FillRoundRect(p, rect.x(), rect.y(), rect.width(), rect.height(), bg, st.corners, sh, parts);
}, [&](const QPoint &bottomPosition) {
tail.paint(p, bottomPosition - tailShift, args.outerWidth);
return tail.width();
@@ -262,7 +245,8 @@ void PaintSolidBubble(Painter &p, const SimpleBubble &args) {
} // namespace
std::unique_ptr<BubblePattern> PrepareBubblePattern() {
std::unique_ptr<BubblePattern> PrepareBubblePattern(
not_null<const style::palette*> st) {
auto result = std::make_unique<Ui::BubblePattern>();
result->corners = Images::CornersMask(st::historyMessageRadius);
const auto addShadow = [&](QImage &bottomCorner) {
@@ -274,7 +258,7 @@ std::unique_ptr<BubblePattern> PrepareBubblePattern() {
result.fill(Qt::transparent);
result.setDevicePixelRatio(bottomCorner.devicePixelRatio());
auto p = QPainter(&result);
p.setOpacity(st::msgInShadow->c.alphaF());
p.setOpacity(st->msgInShadow()->c.alphaF());
p.drawImage(0, st::msgShadow, bottomCorner);
p.setOpacity(1.);
p.drawImage(0, 0, bottomCorner);

View File

@@ -13,6 +13,9 @@ class Painter;
namespace Ui {
class ChatTheme;
class ChatStyle;
struct BubbleSelectionInterval {
int top = 0;
int height = 0;
@@ -28,9 +31,11 @@ struct BubblePattern {
mutable QImage tailCache;
};
[[nodiscard]] std::unique_ptr<BubblePattern> PrepareBubblePattern();
[[nodiscard]] std::unique_ptr<BubblePattern> PrepareBubblePattern(
not_null<const style::palette*> st);
struct SimpleBubble {
not_null<const ChatStyle*> st;
QRect geometry;
const BubblePattern *pattern = nullptr;
QRect patternViewport;