tdesktop/Telegram/SourceFiles/ui/chat/message_bubble.cpp

441 lines
12 KiB
C++

/*
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/message_bubble.h"
#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 {
namespace {
template <
typename FillBg, // fillBg(QRect rect)
typename FillSh, // fillSh(QRect rect)
typename FillRounded, // fillRounded(QRect rect, RectParts parts)
typename PaintTail> // paintTail(QPoint bottomPosition) -> tailWidth
void PaintBubbleGeneric(
const SimpleBubble &args,
FillBg &&fillBg,
FillSh &&fillSh,
FillRounded &&fillRounded,
PaintTail &&paintTail) {
auto parts = RectPart::None | RectPart::NoTopBottom;
auto rect = args.geometry;
if (args.skip & RectPart::Top) {
if (args.skip & RectPart::Bottom) {
fillBg(rect);
return;
}
rect.setTop(rect.y() - st::historyMessageRadius);
} else {
parts |= RectPart::FullTop;
}
const auto skipBottom = (args.skip & RectPart::Bottom);
if (skipBottom) {
rect.setHeight(rect.height() + st::historyMessageRadius);
} else {
parts |= RectPart::Bottom;
}
if (!skipBottom && args.tailSide == RectPart::Right) {
parts |= RectPart::BottomLeft;
fillBg({
rect.x() + rect.width() - st::historyMessageRadius,
rect.y() + rect.height() - st::historyMessageRadius,
st::historyMessageRadius,
st::historyMessageRadius });
const auto tailWidth = paintTail({
rect.x() + rect.width(),
rect.y() + rect.height() });
fillSh({
rect.x() + rect.width() - st::historyMessageRadius,
rect.y() + rect.height(),
st::historyMessageRadius + tailWidth,
st::msgShadow });
} else if (!skipBottom && args.tailSide == RectPart::Left) {
parts |= RectPart::BottomRight;
fillBg({
rect.x(),
rect.y() + rect.height() - st::historyMessageRadius,
st::historyMessageRadius,
st::historyMessageRadius });
const auto tailWidth = paintTail({
rect.x(),
rect.y() + rect.height() });
fillSh({
rect.x() - tailWidth,
rect.y() + rect.height(),
st::historyMessageRadius + tailWidth,
st::msgShadow });
} else if (!skipBottom) {
parts |= RectPart::FullBottom;
}
fillRounded(rect, parts);
}
void PaintPatternBubble(QPainter &p, const SimpleBubble &args) {
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);
const auto &tail = (args.tailSide == RectPart::Right)
? pattern->tailRight
: pattern->tailLeft;
const auto tailShift = (args.tailSide == RectPart::Right
? QPoint(0, tail.height())
: QPoint(tail.width(), tail.height())) / int(tail.devicePixelRatio());
const auto fillBg = [&](const QRect &rect) {
const auto fill = rect.intersected(args.patternViewport);
if (!fill.isEmpty()) {
PaintPatternBubblePart(
p,
args.patternViewport,
pattern->pixmap,
fill);
}
};
const auto fillSh = [&](const QRect &rect) {
if (!(args.skip & RectPart::Bottom)) {
p.setOpacity(shadowOpacity);
fillBg(rect);
p.setOpacity(opacity);
}
};
const auto fillPattern = [&](
int x,
int y,
const QImage &mask,
QImage &cache) {
PaintPatternBubblePart(
p,
args.patternViewport,
pattern->pixmap,
QRect(QPoint(x, y), mask.size() / int(mask.devicePixelRatio())),
mask,
cache);
};
const auto fillCorner = [&](int x, int y, int index) {
fillPattern(
x,
y,
pattern->corners[index],
(index < 2) ? pattern->cornerTopCache : pattern->cornerBottomCache);
};
const auto fillRounded = [&](const QRect &rect, RectParts parts) {
const auto x = rect.x();
const auto y = rect.y();
const auto w = rect.width();
const auto h = rect.height();
const auto cornerWidth = pattern->corners[0].width()
/ style::DevicePixelRatio();
const auto cornerHeight = pattern->corners[0].height()
/ style::DevicePixelRatio();
if (w < 2 * cornerWidth || h < 2 * cornerHeight) {
return;
}
if (w > 2 * cornerWidth) {
if (parts & RectPart::Top) {
fillBg({
x + cornerWidth,
y,
w - 2 * cornerWidth,
cornerHeight });
}
if (parts & RectPart::Bottom) {
fillBg({
x + cornerWidth,
y + h - cornerHeight,
w - 2 * cornerWidth,
cornerHeight });
if (sh) {
fillSh({
x + cornerWidth,
y + h,
w - 2 * cornerWidth,
st::msgShadow });
}
}
}
if (h > 2 * cornerHeight) {
if ((parts & RectPart::NoTopBottom) == RectPart::NoTopBottom) {
fillBg({
x,
y + cornerHeight,
w,
h - 2 * cornerHeight });
} else {
if (parts & RectPart::Left) {
fillBg({
x,
y + cornerHeight,
cornerWidth,
h - 2 * cornerHeight });
}
if ((parts & RectPart::Center) && w > 2 * cornerWidth) {
fillBg({
x + cornerWidth,
y + cornerHeight,
w - 2 * cornerWidth,
h - 2 * cornerHeight });
}
if (parts & RectPart::Right) {
fillBg({
x + w - cornerWidth,
y + cornerHeight,
cornerWidth,
h - 2 * cornerHeight });
}
}
}
if (parts & RectPart::TopLeft) {
fillCorner(x, y, 0);
}
if (parts & RectPart::TopRight) {
fillCorner(x + w - cornerWidth, y, 1);
}
if (parts & RectPart::BottomLeft) {
fillCorner(x, y + h - cornerHeight, 2);
}
if (parts & RectPart::BottomRight) {
fillCorner(x + w - cornerWidth, y + h - cornerHeight, 3);
}
};
const auto paintTail = [&](QPoint bottomPosition) {
const auto position = bottomPosition - tailShift;
fillPattern(position.x(), position.y(), tail, pattern->tailCache);
return tail.width() / int(tail.devicePixelRatio());
};
p.setOpacity(opacity);
PaintBubbleGeneric(args, fillBg, fillSh, fillRounded, paintTail);
p.setOpacity(1.);
}
void PaintSolidBubble(QPainter &p, const SimpleBubble &args) {
const auto &st = args.st->messageStyle(args.outbg, args.selected);
const auto &bg = st.msgBg;
const auto sh = (args.skip & RectPart::Bottom)
? nullptr
: &st.msgShadow;
const auto &tail = (args.tailSide == RectPart::Right)
? st.tailRight
: st.tailLeft;
const auto tailShift = (args.tailSide == RectPart::Right)
? QPoint(0, tail.height())
: QPoint(tail.width(), tail.height());
PaintBubbleGeneric(args, [&](const QRect &rect) {
p.fillRect(rect, bg);
}, [&](const QRect &rect) {
p.fillRect(rect, *sh);
}, [&](const QRect &rect, RectParts parts) {
Ui::FillRoundRect(p, rect, bg, st.msgBgCorners, sh, parts);
}, [&](const QPoint &bottomPosition) {
tail.paint(p, bottomPosition - tailShift, args.outerWidth);
return tail.width();
});
}
} // namespace
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) {
auto result = QImage(
bottomCorner.width(),
(bottomCorner.height()
+ st::msgShadow * int(bottomCorner.devicePixelRatio())),
QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
result.setDevicePixelRatio(bottomCorner.devicePixelRatio());
auto p = QPainter(&result);
p.setOpacity(st->msgInShadow()->c.alphaF());
p.drawImage(0, st::msgShadow, bottomCorner);
p.setOpacity(1.);
p.drawImage(0, 0, bottomCorner);
p.end();
bottomCorner = std::move(result);
};
addShadow(result->corners[2]);
addShadow(result->corners[3]);
result->cornerTopCache = QImage(
result->corners[0].size(),
QImage::Format_ARGB32_Premultiplied);
result->cornerBottomCache = QImage(
result->corners[2].size(),
QImage::Format_ARGB32_Premultiplied);
return result;
}
void FinishBubblePatternOnMain(not_null<BubblePattern*> pattern) {
pattern->tailLeft = st::historyBubbleTailOutLeft.instance(Qt::white);
pattern->tailRight = st::historyBubbleTailOutRight.instance(Qt::white);
pattern->tailCache = QImage(
pattern->tailLeft.size(),
QImage::Format_ARGB32_Premultiplied);
}
void PaintBubble(QPainter &p, const SimpleBubble &args) {
if (!args.selected
&& args.outbg
&& args.pattern
&& !args.patternViewport.isEmpty()
&& !args.pattern->pixmap.size().isEmpty()) {
PaintPatternBubble(p, args);
} else {
PaintSolidBubble(p, args);
}
}
void PaintBubble(QPainter &p, const ComplexBubble &args) {
if (args.selection.empty()) {
PaintBubble(p, args.simple);
return;
}
const auto rect = args.simple.geometry;
const auto left = rect.x();
const auto width = rect.width();
const auto top = rect.y();
const auto bottom = top + rect.height();
const auto paintOne = [&](QRect geometry, bool selected, RectParts skip) {
auto simple = args.simple;
simple.geometry = geometry;
simple.selected = selected;
simple.skip = skip;
PaintBubble(p, simple);
};
auto from = top;
for (const auto &selected : args.selection) {
if (selected.top > from) {
const auto skip = RectPart::Bottom
| (from > top ? RectPart::Top : RectPart::None);
paintOne(
QRect(left, from, width, selected.top - from),
false,
skip);
}
const auto skip = ((selected.top > top)
? RectPart::Top
: RectPart::None)
| ((selected.top + selected.height < bottom)
? RectPart::Bottom
: RectPart::None);
paintOne(
QRect(left, selected.top, width, selected.height),
true,
skip);
from = selected.top + selected.height;
}
if (from < bottom) {
paintOne(
QRect(left, from, width, bottom - from),
false,
RectPart::Top);
}
}
void PaintPatternBubblePart(
QPainter &p,
const QRect &viewport,
const QPixmap &pixmap,
const QRect &target) {
const auto factor = pixmap.devicePixelRatio();
if (viewport.size() * factor == pixmap.size()) {
const auto fill = target.intersected(viewport);
if (fill.isEmpty()) {
return;
}
p.drawPixmap(fill, pixmap, QRect(
(fill.topLeft() - viewport.topLeft()) * factor,
fill.size() * factor));
} else {
const auto to = viewport;
const auto from = QRect(QPoint(), pixmap.size());
const auto deviceRect = QRect(
QPoint(),
QSize(p.device()->width(), p.device()->height()));
const auto clip = (target != deviceRect);
if (clip) {
p.setClipRect(target);
}
p.drawPixmap(to, pixmap, from);
if (clip) {
p.setClipping(false);
}
}
}
void PaintPatternBubblePart(
QPainter &p,
const QRect &viewport,
const QPixmap &pixmap,
const QRect &target,
const QImage &mask,
QImage &cache) {
Expects(mask.bytesPerLine() == mask.width() * 4);
Expects(mask.format() == QImage::Format_ARGB32_Premultiplied);
if (cache.size() != mask.size()) {
cache = QImage(
mask.size(),
QImage::Format_ARGB32_Premultiplied);
}
cache.setDevicePixelRatio(mask.devicePixelRatio());
Assert(cache.bytesPerLine() == cache.width() * 4);
memcpy(cache.bits(), mask.constBits(), mask.sizeInBytes());
auto q = QPainter(&cache);
q.setCompositionMode(QPainter::CompositionMode_SourceIn);
PaintPatternBubblePart(
q,
viewport.translated(-target.topLeft()),
pixmap,
QRect(QPoint(), cache.size() / int(cache.devicePixelRatio())));
q.end();
p.drawImage(target, cache);
}
void PaintPatternBubblePart(
QPainter &p,
const QRect &viewport,
const QPixmap &pixmap,
const QRect &target,
Fn<void(QPainter&)> paintContent,
QImage &cache) {
Expects(paintContent != nullptr);
const auto targetOrigin = target.topLeft();
const auto targetSize = target.size();
if (cache.size() != targetSize * style::DevicePixelRatio()) {
cache = QImage(
target.size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
cache.setDevicePixelRatio(style::DevicePixelRatio());
}
cache.fill(Qt::transparent);
auto q = QPainter(&cache);
q.translate(-targetOrigin);
paintContent(q);
q.translate(targetOrigin);
q.setCompositionMode(QPainter::CompositionMode_SourceIn);
PaintPatternBubblePart(
q,
viewport.translated(-targetOrigin),
pixmap,
QRect(QPoint(), targetSize));
q.end();
p.drawImage(target, cache);
}
} // namespace Ui