2021-08-26 13:25:48 +00:00
|
|
|
/*
|
|
|
|
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
|
|
|
|
*/
|
2021-08-26 15:02:21 +00:00
|
|
|
#include "ui/chat/chat_theme.h"
|
2021-08-26 13:25:48 +00:00
|
|
|
|
|
|
|
#include "ui/image/image_prepare.h"
|
|
|
|
#include "ui/ui_utility.h"
|
|
|
|
#include "ui/chat/message_bubble.h"
|
|
|
|
|
2021-08-26 15:02:21 +00:00
|
|
|
#include <crl/crl_async.h>
|
2021-08-26 13:25:48 +00:00
|
|
|
#include <QtGui/QGuiApplication>
|
|
|
|
|
2021-08-26 15:02:21 +00:00
|
|
|
namespace Ui {
|
2021-08-26 13:25:48 +00:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
constexpr auto kMaxChatEntryHistorySize = 50;
|
|
|
|
constexpr auto kCacheBackgroundTimeout = 3 * crl::time(1000);
|
|
|
|
constexpr auto kCacheBackgroundFastTimeout = crl::time(200);
|
|
|
|
constexpr auto kBackgroundFadeDuration = crl::time(200);
|
|
|
|
|
|
|
|
[[nodiscard]] CacheBackgroundResult CacheBackground(
|
|
|
|
const CacheBackgroundRequest &request) {
|
2021-08-26 15:02:21 +00:00
|
|
|
const auto gradient = request.background.gradientForFill.isNull()
|
2021-08-26 13:25:48 +00:00
|
|
|
? QImage()
|
2021-08-26 15:02:21 +00:00
|
|
|
: (request.gradientRotationAdd != 0)
|
2021-08-26 13:25:48 +00:00
|
|
|
? Images::GenerateGradient(
|
2021-08-26 15:02:21 +00:00
|
|
|
request.background.gradientForFill.size(),
|
|
|
|
request.background.colors,
|
|
|
|
(request.background.gradientRotation
|
|
|
|
+ request.gradientRotationAdd) % 360)
|
|
|
|
: request.background.gradientForFill;
|
|
|
|
if (request.background.isPattern
|
|
|
|
|| request.background.tile
|
|
|
|
|| request.background.prepared.isNull()) {
|
2021-08-26 13:25:48 +00:00
|
|
|
auto result = gradient.isNull()
|
|
|
|
? QImage(
|
2021-08-26 15:02:21 +00:00
|
|
|
request.area * style::DevicePixelRatio(),
|
2021-08-26 13:25:48 +00:00
|
|
|
QImage::Format_ARGB32_Premultiplied)
|
|
|
|
: gradient.scaled(
|
2021-08-26 15:02:21 +00:00
|
|
|
request.area * style::DevicePixelRatio(),
|
2021-08-26 13:25:48 +00:00
|
|
|
Qt::IgnoreAspectRatio,
|
|
|
|
Qt::SmoothTransformation);
|
2021-08-26 15:02:21 +00:00
|
|
|
result.setDevicePixelRatio(style::DevicePixelRatio());
|
|
|
|
if (!request.background.prepared.isNull()) {
|
2021-08-26 13:25:48 +00:00
|
|
|
QPainter p(&result);
|
|
|
|
if (!gradient.isNull()) {
|
2021-08-26 15:02:21 +00:00
|
|
|
if (request.background.patternOpacity >= 0.) {
|
2021-08-26 13:25:48 +00:00
|
|
|
p.setCompositionMode(QPainter::CompositionMode_SoftLight);
|
2021-08-26 15:02:21 +00:00
|
|
|
p.setOpacity(request.background.patternOpacity);
|
2021-08-26 13:25:48 +00:00
|
|
|
} else {
|
|
|
|
p.setCompositionMode(
|
|
|
|
QPainter::CompositionMode_DestinationIn);
|
|
|
|
}
|
|
|
|
}
|
2021-08-26 15:02:21 +00:00
|
|
|
const auto tiled = request.background.isPattern
|
|
|
|
? request.background.prepared.scaled(
|
|
|
|
request.area.height() * style::DevicePixelRatio(),
|
|
|
|
request.area.height() * style::DevicePixelRatio(),
|
2021-08-26 13:25:48 +00:00
|
|
|
Qt::KeepAspectRatio,
|
|
|
|
Qt::SmoothTransformation)
|
2021-08-26 15:02:21 +00:00
|
|
|
: request.background.preparedForTiled;
|
|
|
|
const auto w = tiled.width() / style::DevicePixelRatio();
|
|
|
|
const auto h = tiled.height() / style::DevicePixelRatio();
|
|
|
|
const auto cx = int(std::ceil(request.area.width() / w));
|
|
|
|
const auto cy = int(std::ceil(request.area.height() / h));
|
2021-08-26 13:25:48 +00:00
|
|
|
const auto rows = cy;
|
2021-08-26 15:02:21 +00:00
|
|
|
const auto cols = request.background.isPattern ? (((cx / 2) * 2) + 1) : cx;
|
|
|
|
const auto xshift = request.background.isPattern
|
2021-08-26 13:25:48 +00:00
|
|
|
? (request.area.width() - cols * w) / 2
|
|
|
|
: 0;
|
|
|
|
for (auto y = 0; y != rows; ++y) {
|
|
|
|
for (auto x = 0; x != cols; ++x) {
|
|
|
|
p.drawImage(QPointF(xshift + x * w, y * h), tiled);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!gradient.isNull()
|
2021-08-26 15:02:21 +00:00
|
|
|
&& request.background.patternOpacity < 0.
|
|
|
|
&& request.background.patternOpacity > -1.) {
|
2021-08-26 13:25:48 +00:00
|
|
|
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
2021-08-26 15:02:21 +00:00
|
|
|
p.setOpacity(1. + request.background.patternOpacity);
|
2021-08-26 13:25:48 +00:00
|
|
|
p.fillRect(QRect(QPoint(), request.area), Qt::black);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
.image = std::move(result).convertToFormat(
|
|
|
|
QImage::Format_ARGB32_Premultiplied),
|
|
|
|
.gradient = gradient,
|
|
|
|
.area = request.area,
|
|
|
|
};
|
|
|
|
} else {
|
2021-08-26 15:02:21 +00:00
|
|
|
const auto rects = ComputeChatBackgroundRects(
|
2021-08-26 13:25:48 +00:00
|
|
|
request.area,
|
2021-08-26 15:02:21 +00:00
|
|
|
request.background.prepared.size());
|
|
|
|
auto result = request.background.prepared.copy(rects.from).scaled(
|
|
|
|
rects.to.width() * style::DevicePixelRatio(),
|
|
|
|
rects.to.height() * style::DevicePixelRatio(),
|
2021-08-26 13:25:48 +00:00
|
|
|
Qt::IgnoreAspectRatio,
|
|
|
|
Qt::SmoothTransformation);
|
2021-08-26 15:02:21 +00:00
|
|
|
result.setDevicePixelRatio(style::DevicePixelRatio());
|
2021-08-26 13:25:48 +00:00
|
|
|
return {
|
|
|
|
.image = std::move(result).convertToFormat(
|
|
|
|
QImage::Format_ARGB32_Premultiplied),
|
|
|
|
.gradient = gradient,
|
|
|
|
.area = request.area,
|
|
|
|
.x = rects.to.x(),
|
|
|
|
.y = rects.to.y(),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
2021-08-26 15:02:21 +00:00
|
|
|
bool operator==(const ChatThemeBackground &a, const ChatThemeBackground &b) {
|
|
|
|
return (a.prepared.cacheKey() == b.prepared.cacheKey())
|
|
|
|
&& (a.gradientForFill.cacheKey() == b.gradientForFill.cacheKey())
|
|
|
|
&& (a.tile == b.tile)
|
|
|
|
&& (a.patternOpacity == b.patternOpacity);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool operator!=(const ChatThemeBackground &a, const ChatThemeBackground &b) {
|
|
|
|
return !(a == b);
|
|
|
|
}
|
|
|
|
|
2021-08-26 13:25:48 +00:00
|
|
|
bool operator==(
|
|
|
|
const CacheBackgroundRequest &a,
|
|
|
|
const CacheBackgroundRequest &b) {
|
2021-08-26 15:02:21 +00:00
|
|
|
return (a.background == b.background)
|
2021-08-26 13:25:48 +00:00
|
|
|
&& (a.area == b.area)
|
2021-08-26 15:02:21 +00:00
|
|
|
&& (a.gradientRotationAdd == b.gradientRotationAdd)
|
|
|
|
&& (a.gradientProgress == b.gradientProgress);
|
2021-08-26 13:25:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool operator!=(
|
|
|
|
const CacheBackgroundRequest &a,
|
|
|
|
const CacheBackgroundRequest &b) {
|
|
|
|
return !(a == b);
|
|
|
|
}
|
|
|
|
|
|
|
|
CachedBackground::CachedBackground(CacheBackgroundResult &&result)
|
|
|
|
: pixmap(Ui::PixmapFromImage(std::move(result.image)))
|
|
|
|
, area(result.area)
|
|
|
|
, x(result.x)
|
|
|
|
, y(result.y) {
|
|
|
|
}
|
|
|
|
|
|
|
|
ChatTheme::ChatTheme() {
|
|
|
|
}
|
|
|
|
|
|
|
|
// Runs from background thread.
|
2021-08-26 15:02:21 +00:00
|
|
|
ChatTheme::ChatTheme(ChatThemeDescriptor &&descriptor)
|
|
|
|
: _id(descriptor.id)
|
2021-08-26 13:25:48 +00:00
|
|
|
, _palette(std::make_unique<style::palette>()) {
|
2021-08-26 15:02:21 +00:00
|
|
|
descriptor.preparePalette(*_palette);
|
|
|
|
setBackground(descriptor.prepareBackground());
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChatTheme::setBackground(ChatThemeBackground &&background) {
|
|
|
|
_mutableBackground = std::move(background);
|
|
|
|
_backgroundState = {};
|
|
|
|
_backgroundNext = {};
|
|
|
|
_backgroundFade.stop();
|
|
|
|
if (_cacheBackgroundTimer) {
|
|
|
|
_cacheBackgroundTimer->cancel();
|
|
|
|
}
|
|
|
|
_repaintBackgroundRequests.fire({});
|
2021-08-26 13:25:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
uint64 ChatTheme::key() const {
|
|
|
|
return _id;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChatTheme::setBubblesBackground(QImage image) {
|
|
|
|
_bubblesBackgroundPrepared = std::move(image);
|
|
|
|
if (!_bubblesBackground.area.isEmpty()) {
|
|
|
|
_bubblesBackground = CacheBackground({
|
2021-08-26 15:02:21 +00:00
|
|
|
.background = {
|
|
|
|
.prepared = _bubblesBackgroundPrepared,
|
|
|
|
},
|
2021-08-26 13:25:48 +00:00
|
|
|
.area = _bubblesBackground.area,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (!_bubblesBackgroundPattern) {
|
|
|
|
_bubblesBackgroundPattern = Ui::PrepareBubblePattern();
|
|
|
|
}
|
|
|
|
_bubblesBackgroundPattern->pixmap = _bubblesBackground.pixmap;
|
|
|
|
_repaintBackgroundRequests.fire({});
|
|
|
|
}
|
|
|
|
|
2021-08-26 15:02:21 +00:00
|
|
|
ChatPaintContext ChatTheme::preparePaintContext(
|
2021-08-26 13:25:48 +00:00
|
|
|
QRect viewport,
|
|
|
|
QRect clip) {
|
|
|
|
_bubblesBackground.area = viewport.size();
|
|
|
|
//if (!_bubblesBackgroundPrepared.isNull()
|
|
|
|
// && _bubblesBackground.area != viewport.size()
|
|
|
|
// && !viewport.isEmpty()) {
|
|
|
|
// // #TODO bubbles delayed caching
|
|
|
|
// _bubblesBackground = CacheBackground({
|
|
|
|
// .prepared = _bubblesBackgroundPrepared,
|
|
|
|
// .area = viewport.size(),
|
|
|
|
// });
|
|
|
|
// _bubblesBackgroundPattern->pixmap = _bubblesBackground.pixmap;
|
|
|
|
//}
|
|
|
|
return {
|
|
|
|
.st = _palette ? _palette.get() : style::main_palette::get(),
|
|
|
|
.bubblesPattern = _bubblesBackgroundPattern.get(),
|
|
|
|
.viewport = viewport,
|
|
|
|
.clip = clip,
|
|
|
|
.now = crl::now(),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const BackgroundState &ChatTheme::backgroundState(QSize area) {
|
|
|
|
if (!_cacheBackgroundTimer) {
|
|
|
|
_cacheBackgroundTimer.emplace([=] { cacheBackground(); });
|
|
|
|
}
|
|
|
|
_backgroundState.shown = _backgroundFade.value(1.);
|
|
|
|
if (_backgroundState.now.pixmap.isNull()
|
2021-08-26 15:02:21 +00:00
|
|
|
&& !background().gradientForFill.isNull()) {
|
2021-08-26 13:25:48 +00:00
|
|
|
// We don't support direct painting of patterned gradients.
|
|
|
|
// So we need to sync-generate cache image here.
|
|
|
|
setCachedBackground(CacheBackground(currentCacheRequest(area)));
|
|
|
|
_cacheBackgroundTimer->cancel();
|
|
|
|
} else if (_backgroundState.now.area != area) {
|
|
|
|
if (_willCacheForArea != area
|
|
|
|
|| (!_cacheBackgroundTimer->isActive()
|
|
|
|
&& !_backgroundCachingRequest)) {
|
|
|
|
_willCacheForArea = area;
|
|
|
|
_lastAreaChangeTime = crl::now();
|
|
|
|
_cacheBackgroundTimer->callOnce(kCacheBackgroundFastTimeout);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
generateNextBackgroundRotation();
|
|
|
|
return _backgroundState;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ChatTheme::readyForBackgroundRotation() const {
|
|
|
|
Expects(_cacheBackgroundTimer.has_value());
|
|
|
|
|
|
|
|
return !anim::Disabled()
|
|
|
|
&& !_backgroundFade.animating()
|
|
|
|
&& !_cacheBackgroundTimer->isActive()
|
|
|
|
&& !_backgroundState.now.pixmap.isNull();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChatTheme::generateNextBackgroundRotation() {
|
|
|
|
if (_backgroundCachingRequest
|
|
|
|
|| !_backgroundNext.image.isNull()
|
|
|
|
|| !readyForBackgroundRotation()) {
|
|
|
|
return;
|
|
|
|
}
|
2021-08-26 15:02:21 +00:00
|
|
|
if (background().colors.size() < 3) {
|
2021-08-26 13:25:48 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
constexpr auto kAddRotation = 315;
|
|
|
|
const auto request = currentCacheRequest(
|
|
|
|
_backgroundState.now.area,
|
|
|
|
kAddRotation);
|
|
|
|
if (!request) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
cacheBackgroundAsync(request, [=](CacheBackgroundResult &&result) {
|
|
|
|
const auto forRequest = base::take(_backgroundCachingRequest);
|
|
|
|
if (!readyForBackgroundRotation()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const auto request = currentCacheRequest(
|
|
|
|
_backgroundState.now.area,
|
|
|
|
kAddRotation);
|
|
|
|
if (forRequest == request) {
|
2021-08-26 15:02:21 +00:00
|
|
|
_mutableBackground.gradientRotation
|
|
|
|
= (_mutableBackground.gradientRotation + kAddRotation) % 360;
|
2021-08-26 13:25:48 +00:00
|
|
|
_backgroundNext = std::move(result);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
auto ChatTheme::currentCacheRequest(QSize area, int addRotation) const
|
|
|
|
-> CacheBackgroundRequest {
|
2021-08-26 15:02:21 +00:00
|
|
|
if (background().colorForFill) {
|
2021-08-26 13:25:48 +00:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
return {
|
2021-08-26 15:02:21 +00:00
|
|
|
.background = background(),
|
2021-08-26 13:25:48 +00:00
|
|
|
.area = area,
|
2021-08-26 15:02:21 +00:00
|
|
|
.gradientRotationAdd = addRotation,
|
|
|
|
// .recreateGradient = (addRotation != 0),
|
2021-08-26 13:25:48 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChatTheme::cacheBackground() {
|
|
|
|
Expects(_cacheBackgroundTimer.has_value());
|
|
|
|
|
|
|
|
const auto now = crl::now();
|
|
|
|
if (now - _lastAreaChangeTime < kCacheBackgroundTimeout
|
|
|
|
&& QGuiApplication::mouseButtons() != 0) {
|
|
|
|
_cacheBackgroundTimer->callOnce(kCacheBackgroundFastTimeout);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
cacheBackgroundNow();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChatTheme::cacheBackgroundNow() {
|
|
|
|
if (!_backgroundCachingRequest) {
|
|
|
|
if (const auto request = currentCacheRequest(_willCacheForArea)) {
|
|
|
|
cacheBackgroundAsync(request);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChatTheme::cacheBackgroundAsync(
|
|
|
|
const CacheBackgroundRequest &request,
|
|
|
|
Fn<void(CacheBackgroundResult&&)> done) {
|
|
|
|
_backgroundCachingRequest = request;
|
|
|
|
const auto weak = base::make_weak(this);
|
|
|
|
crl::async([=] {
|
|
|
|
if (!weak) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
crl::on_main(weak, [=, result = CacheBackground(request)]() mutable {
|
|
|
|
if (done) {
|
|
|
|
done(std::move(result));
|
|
|
|
} else if (const auto request = currentCacheRequest(
|
|
|
|
_willCacheForArea)) {
|
|
|
|
if (_backgroundCachingRequest != request) {
|
|
|
|
cacheBackgroundAsync(request);
|
|
|
|
} else {
|
|
|
|
_backgroundCachingRequest = {};
|
|
|
|
setCachedBackground(std::move(result));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChatTheme::setCachedBackground(CacheBackgroundResult &&cached) {
|
|
|
|
_backgroundNext = {};
|
|
|
|
|
2021-08-26 15:02:21 +00:00
|
|
|
if (background().gradientForFill.isNull()
|
2021-08-26 13:25:48 +00:00
|
|
|
|| _backgroundState.now.pixmap.isNull()
|
|
|
|
|| anim::Disabled()) {
|
|
|
|
_backgroundFade.stop();
|
|
|
|
_backgroundState.shown = 1.;
|
|
|
|
_backgroundState.now = std::move(cached);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// #TODO themes compose several transitions.
|
|
|
|
_backgroundState.was = std::move(_backgroundState.now);
|
|
|
|
_backgroundState.now = std::move(cached);
|
|
|
|
_backgroundState.shown = 0.;
|
|
|
|
const auto callback = [=] {
|
|
|
|
if (!_backgroundFade.animating()) {
|
|
|
|
_backgroundState.was = {};
|
|
|
|
_backgroundState.shown = 1.;
|
|
|
|
}
|
|
|
|
_repaintBackgroundRequests.fire({});
|
|
|
|
};
|
|
|
|
_backgroundFade.start(
|
|
|
|
callback,
|
|
|
|
0.,
|
|
|
|
1.,
|
|
|
|
kBackgroundFadeDuration);
|
|
|
|
}
|
|
|
|
|
|
|
|
rpl::producer<> ChatTheme::repaintBackgroundRequests() const {
|
|
|
|
return _repaintBackgroundRequests.events();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChatTheme::rotateComplexGradientBackground() {
|
|
|
|
if (!_backgroundFade.animating() && !_backgroundNext.image.isNull()) {
|
2021-08-26 15:02:21 +00:00
|
|
|
if (_mutableBackground.gradientForFill.size()
|
|
|
|
== _backgroundNext.gradient.size()) {
|
|
|
|
_mutableBackground.gradientForFill
|
|
|
|
= std::move(_backgroundNext.gradient);
|
|
|
|
}
|
2021-08-26 13:25:48 +00:00
|
|
|
setCachedBackground(base::take(_backgroundNext));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-26 15:02:21 +00:00
|
|
|
ChatBackgroundRects ComputeChatBackgroundRects(
|
|
|
|
QSize fillSize,
|
|
|
|
QSize imageSize) {
|
|
|
|
if (uint64(imageSize.width()) * fillSize.height()
|
|
|
|
> uint64(imageSize.height()) * fillSize.width()) {
|
|
|
|
const auto pxsize = fillSize.height() / float64(imageSize.height());
|
|
|
|
auto takewidth = int(std::ceil(fillSize.width() / pxsize));
|
|
|
|
if (takewidth > imageSize.width()) {
|
|
|
|
takewidth = imageSize.width();
|
|
|
|
} else if ((imageSize.width() % 2) != (takewidth % 2)) {
|
|
|
|
++takewidth;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
.from = QRect(
|
|
|
|
(imageSize.width() - takewidth) / 2,
|
|
|
|
0,
|
|
|
|
takewidth,
|
|
|
|
imageSize.height()),
|
|
|
|
.to = QRect(
|
|
|
|
int((fillSize.width() - takewidth * pxsize) / 2.),
|
|
|
|
0,
|
|
|
|
int(std::ceil(takewidth * pxsize)),
|
|
|
|
fillSize.height()),
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
const auto pxsize = fillSize.width() / float64(imageSize.width());
|
|
|
|
auto takeheight = int(std::ceil(fillSize.height() / pxsize));
|
|
|
|
if (takeheight > imageSize.height()) {
|
|
|
|
takeheight = imageSize.height();
|
|
|
|
} else if ((imageSize.height() % 2) != (takeheight % 2)) {
|
|
|
|
++takeheight;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
.from = QRect(
|
|
|
|
0,
|
|
|
|
(imageSize.height() - takeheight) / 2,
|
|
|
|
imageSize.width(),
|
|
|
|
takeheight),
|
|
|
|
.to = QRect(
|
|
|
|
0,
|
|
|
|
int((fillSize.height() - takeheight * pxsize) / 2.),
|
|
|
|
fillSize.width(),
|
|
|
|
int(std::ceil(takeheight * pxsize))),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-26 13:25:48 +00:00
|
|
|
} // namespace Window::Theme
|