441 lines
12 KiB
C++
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
|