471 lines
13 KiB
C++
471 lines
13 KiB
C++
// This file is part of Desktop App Toolkit,
|
|
// a set of libraries for developing nice desktop applications.
|
|
//
|
|
// For license and copyright information please follow this link:
|
|
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
|
//
|
|
#include "ui/effects/round_area_with_shadow.h"
|
|
|
|
#include "ui/style/style_core.h"
|
|
#include "ui/image/image_prepare.h"
|
|
#include "ui/painter.h"
|
|
|
|
namespace Ui {
|
|
namespace {
|
|
|
|
constexpr auto kBgCacheIndex = 0;
|
|
constexpr auto kShadowCacheIndex = 0;
|
|
constexpr auto kOverlayMaskCacheIndex = 0;
|
|
constexpr auto kOverlayShadowCacheIndex = 1;
|
|
constexpr auto kOverlayCacheColumsCount = 2;
|
|
constexpr auto kDivider = 4;
|
|
|
|
} // namespace
|
|
|
|
[[nodiscard]] QImage RoundAreaWithShadow::PrepareImage(QSize size) {
|
|
const auto ratio = style::DevicePixelRatio();
|
|
auto result = QImage(
|
|
size * ratio,
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
result.setDevicePixelRatio(ratio);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] QImage RoundAreaWithShadow::PrepareFramesCache(
|
|
QSize frame,
|
|
int columns) {
|
|
static_assert(!(kFramesCount % kDivider));
|
|
|
|
return PrepareImage(QSize(
|
|
frame.width() * kDivider * columns,
|
|
frame.height() * kFramesCount / kDivider));
|
|
}
|
|
|
|
[[nodiscard]] QRect RoundAreaWithShadow::FrameCacheRect(
|
|
int frameIndex,
|
|
int column,
|
|
QSize frame) {
|
|
const auto ratio = style::DevicePixelRatio();
|
|
const auto origin = QPoint(
|
|
frame.width() * (kDivider * column + (frameIndex % kDivider)),
|
|
frame.height() * (frameIndex / kDivider));
|
|
return QRect(ratio * origin, ratio * frame);
|
|
}
|
|
|
|
RoundAreaWithShadow::RoundAreaWithShadow(
|
|
QSize inner,
|
|
QMargins shadow,
|
|
int twiceRadiusMax)
|
|
: _inner({}, inner)
|
|
, _outer(_inner.marginsAdded(shadow).size())
|
|
, _overlay(QRect(
|
|
0,
|
|
0,
|
|
std::max(inner.width(), twiceRadiusMax),
|
|
std::max(inner.height(), twiceRadiusMax)).marginsAdded(shadow).size())
|
|
, _cacheBg(PrepareFramesCache(_outer))
|
|
, _shadowParts(PrepareFramesCache(_outer))
|
|
, _overlayCacheParts(PrepareFramesCache(_overlay, kOverlayCacheColumsCount))
|
|
, _overlayMaskScaled(PrepareImage(_overlay))
|
|
, _overlayShadowScaled(PrepareImage(_overlay))
|
|
, _shadowBuffer(PrepareImage(_outer)) {
|
|
_inner.translate(QRect({}, _outer).center() - _inner.center());
|
|
}
|
|
|
|
ImageSubrect RoundAreaWithShadow::validateOverlayMask(
|
|
int frameIndex,
|
|
QSize innerSize,
|
|
float64 radius,
|
|
int twiceRadius,
|
|
float64 scale) {
|
|
const auto ratio = style::DevicePixelRatio();
|
|
const auto cached = (scale == 1.);
|
|
const auto full = cached
|
|
? FrameCacheRect(frameIndex, kOverlayMaskCacheIndex, _overlay)
|
|
: QRect(QPoint(), _overlay * ratio);
|
|
|
|
const auto minWidth = twiceRadius + _outer.width() - _inner.width();
|
|
const auto minHeight = twiceRadius + _outer.height() - _inner.height();
|
|
const auto maskSize = QSize(
|
|
std::max(_outer.width(), minWidth),
|
|
std::max(_outer.height(), minHeight));
|
|
|
|
const auto result = ImageSubrect{
|
|
cached ? &_overlayCacheParts : &_overlayMaskScaled,
|
|
QRect(full.topLeft(), maskSize * ratio),
|
|
};
|
|
if (cached && _validOverlayMask[frameIndex]) {
|
|
return result;
|
|
}
|
|
|
|
auto p = QPainter(result.image.get());
|
|
const auto position = full.topLeft() / ratio;
|
|
p.setCompositionMode(QPainter::CompositionMode_Source);
|
|
p.fillRect(QRect(position, maskSize), Qt::transparent);
|
|
|
|
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
const auto inner = QRect(position + _inner.topLeft(), innerSize);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(Qt::white);
|
|
if (scale != 1.) {
|
|
const auto center = inner.center();
|
|
p.save();
|
|
p.translate(center);
|
|
p.scale(scale, scale);
|
|
p.translate(-center);
|
|
}
|
|
p.drawRoundedRect(inner, radius, radius);
|
|
if (scale != 1.) {
|
|
p.restore();
|
|
}
|
|
|
|
if (cached) {
|
|
_validOverlayMask[frameIndex] = true;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
ImageSubrect RoundAreaWithShadow::validateOverlayShadow(
|
|
int frameIndex,
|
|
QSize innerSize,
|
|
float64 radius,
|
|
int twiceRadius,
|
|
float64 scale,
|
|
const ImageSubrect &mask) {
|
|
const auto ratio = style::DevicePixelRatio();
|
|
const auto cached = (scale == 1.);
|
|
const auto full = cached
|
|
? FrameCacheRect(frameIndex, kOverlayShadowCacheIndex, _overlay)
|
|
: QRect(QPoint(), _overlay * ratio);
|
|
|
|
const auto minWidth = twiceRadius + _outer.width() - _inner.width();
|
|
const auto minHeight = twiceRadius + _outer.height() - _inner.height();
|
|
const auto maskSize = QSize(
|
|
std::max(_outer.width(), minWidth),
|
|
std::max(_outer.height(), minHeight));
|
|
|
|
const auto result = ImageSubrect{
|
|
cached ? &_overlayCacheParts : &_overlayShadowScaled,
|
|
QRect(full.topLeft(), maskSize * ratio),
|
|
};
|
|
if (cached && _validOverlayShadow[frameIndex]) {
|
|
return result;
|
|
}
|
|
|
|
const auto position = full.topLeft() / ratio;
|
|
|
|
_overlayShadowScaled.fill(Qt::transparent);
|
|
const auto inner = QRect(_inner.topLeft(), innerSize);
|
|
const auto add = style::ConvertScale(2.5);
|
|
const auto shift = style::ConvertScale(0.5);
|
|
const auto extended = QRectF(inner).marginsAdded({ add, add, add, add });
|
|
{
|
|
auto p = QPainter(&_overlayShadowScaled);
|
|
p.setCompositionMode(QPainter::CompositionMode_Source);
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(_shadow);
|
|
if (scale != 1.) {
|
|
const auto center = inner.center();
|
|
p.translate(center);
|
|
p.scale(scale, scale);
|
|
p.translate(-center);
|
|
}
|
|
p.drawRoundedRect(extended.translated(0, shift), radius, radius);
|
|
p.end();
|
|
}
|
|
|
|
_overlayShadowScaled = Images::Blur(std::move(_overlayShadowScaled));
|
|
|
|
auto q = Painter(result.image);
|
|
if (result.image != &_overlayShadowScaled) {
|
|
q.setCompositionMode(QPainter::CompositionMode_Source);
|
|
q.drawImage(
|
|
QRect(position, maskSize),
|
|
_overlayShadowScaled,
|
|
QRect(QPoint(), maskSize * ratio));
|
|
}
|
|
q.setCompositionMode(QPainter::CompositionMode_DestinationOut);
|
|
q.drawImage(QRect(position, maskSize), *mask.image, mask.rect);
|
|
|
|
if (cached) {
|
|
_validOverlayShadow[frameIndex] = true;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void RoundAreaWithShadow::overlayExpandedBorder(
|
|
QPainter &p,
|
|
QSize size,
|
|
float64 expandRatio,
|
|
float64 radiusFrom,
|
|
float64 radiusTill,
|
|
float64 scale) {
|
|
const auto progress = expandRatio;
|
|
const auto frame = int(base::SafeRound(progress * (kFramesCount - 1)));
|
|
const auto cacheRatio = frame / float64(kFramesCount - 1);
|
|
const auto radius = radiusFrom + (radiusTill - radiusFrom) * cacheRatio;
|
|
const auto twiceRadius = int(base::SafeRound(radius * 2));
|
|
const auto innerSize = QSize(
|
|
std::max(_inner.width(), twiceRadius),
|
|
std::max(_inner.height(), twiceRadius));
|
|
|
|
const auto overlayMask = validateOverlayMask(
|
|
frame,
|
|
innerSize,
|
|
radius,
|
|
twiceRadius,
|
|
scale);
|
|
const auto overlayShadow = validateOverlayShadow(
|
|
frame,
|
|
innerSize,
|
|
radius,
|
|
twiceRadius,
|
|
scale,
|
|
overlayMask);
|
|
|
|
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
|
|
FillWithImage(p, QRect(QPoint(), size), overlayMask);
|
|
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
|
FillWithImage(p, QRect(QPoint(), size), overlayShadow);
|
|
}
|
|
|
|
QRect RoundAreaWithShadow::FillWithImage(
|
|
QPainter &p,
|
|
QRect geometry,
|
|
const ImageSubrect &pattern) {
|
|
const auto factor = style::DevicePixelRatio();
|
|
const auto &image = *pattern.image;
|
|
const auto source = pattern.rect;
|
|
const auto sourceWidth = (source.width() / factor);
|
|
const auto sourceHeight = (source.height() / factor);
|
|
if (geometry.width() == sourceWidth) {
|
|
const auto part = (sourceHeight / 2) - 1;
|
|
const auto fill = geometry.height() - 2 * part;
|
|
const auto half = part * factor;
|
|
const auto top = source.height() - half;
|
|
p.drawImage(
|
|
geometry.topLeft(),
|
|
image,
|
|
QRect(source.x(), source.y(), source.width(), half));
|
|
if (fill > 0) {
|
|
p.drawImage(
|
|
QRect(
|
|
geometry.topLeft() + QPoint(0, part),
|
|
QSize(sourceWidth, fill)),
|
|
image,
|
|
QRect(
|
|
source.x(),
|
|
source.y() + half,
|
|
source.width(),
|
|
top - half));
|
|
}
|
|
p.drawImage(
|
|
geometry.topLeft() + QPoint(0, part + fill),
|
|
image,
|
|
QRect(source.x(), source.y() + top, source.width(), half));
|
|
return QRect();
|
|
} else if (geometry.height() == sourceHeight) {
|
|
const auto part = (sourceWidth / 2) - 1;
|
|
const auto fill = geometry.width() - 2 * part;
|
|
const auto half = part * factor;
|
|
const auto left = source.width() - half;
|
|
p.drawImage(
|
|
geometry.topLeft(),
|
|
image,
|
|
QRect(source.x(), source.y(), half, source.height()));
|
|
if (fill > 0) {
|
|
p.drawImage(
|
|
QRect(
|
|
geometry.topLeft() + QPoint(part, 0),
|
|
QSize(fill, sourceHeight)),
|
|
image,
|
|
QRect(
|
|
source.x() + half,
|
|
source.y(),
|
|
left - half,
|
|
source.height()));
|
|
}
|
|
p.drawImage(
|
|
geometry.topLeft() + QPoint(part + fill, 0),
|
|
image,
|
|
QRect(source.x() + left, source.y(), half, source.height()));
|
|
return QRect();
|
|
} else if (geometry.width() > sourceWidth
|
|
&& geometry.height() > sourceHeight) {
|
|
const auto xpart = (sourceWidth / 2) - 1;
|
|
const auto xfill = geometry.width() - 2 * xpart;
|
|
const auto xhalf = xpart * factor;
|
|
const auto left = source.width() - xhalf;
|
|
const auto ypart = (sourceHeight / 2) - 1;
|
|
const auto yfill = geometry.height() - 2 * ypart;
|
|
const auto yhalf = ypart * factor;
|
|
const auto top = source.height() - yhalf;
|
|
p.drawImage(
|
|
geometry.topLeft(),
|
|
image,
|
|
QRect(source.x(), source.y(), xhalf, yhalf));
|
|
if (xfill > 0) {
|
|
p.drawImage(
|
|
QRect(
|
|
geometry.topLeft() + QPoint(xpart, 0),
|
|
QSize(xfill, ypart)),
|
|
image,
|
|
QRect(
|
|
source.x() + xhalf,
|
|
source.y(),
|
|
left - xhalf,
|
|
yhalf));
|
|
}
|
|
p.drawImage(
|
|
geometry.topLeft() + QPoint(xpart + xfill, 0),
|
|
image,
|
|
QRect(source.x() + left, source.y(), xhalf, yhalf));
|
|
|
|
if (yfill > 0) {
|
|
p.drawImage(
|
|
QRect(
|
|
geometry.topLeft() + QPoint(0, ypart),
|
|
QSize(xpart, yfill)),
|
|
image,
|
|
QRect(
|
|
source.x(),
|
|
source.y() + yhalf,
|
|
xhalf,
|
|
top - yhalf));
|
|
p.drawImage(
|
|
QRect(
|
|
geometry.topLeft() + QPoint(xpart + xfill, ypart),
|
|
QSize(xpart, yfill)),
|
|
image,
|
|
QRect(
|
|
source.x() + left,
|
|
source.y() + yhalf,
|
|
xhalf,
|
|
top - yhalf));
|
|
}
|
|
|
|
p.drawImage(
|
|
geometry.topLeft() + QPoint(0, ypart + yfill),
|
|
image,
|
|
QRect(source.x(), source.y() + top, xhalf, yhalf));
|
|
if (xfill > 0) {
|
|
p.drawImage(
|
|
QRect(
|
|
geometry.topLeft() + QPoint(xpart, ypart + yfill),
|
|
QSize(xfill, ypart)),
|
|
image,
|
|
QRect(
|
|
source.x() + xhalf,
|
|
source.y() + top,
|
|
left - xhalf,
|
|
yhalf));
|
|
}
|
|
p.drawImage(
|
|
geometry.topLeft() + QPoint(xpart + xfill, ypart + yfill),
|
|
image,
|
|
QRect(source.x() + left, source.y() + top, xhalf, yhalf));
|
|
|
|
return QRect(
|
|
geometry.topLeft() + QPoint(xpart, ypart),
|
|
QSize(xfill, yfill));
|
|
} else {
|
|
Unexpected("Values in RoundAreaWithShadow::fillWithImage.");
|
|
}
|
|
}
|
|
|
|
void RoundAreaWithShadow::setShadowColor(const QColor &shadow) {
|
|
if (_shadow == shadow) {
|
|
return;
|
|
}
|
|
_shadow = shadow;
|
|
ranges::fill(_validBg, false);
|
|
ranges::fill(_validShadow, false);
|
|
ranges::fill(_validOverlayShadow, false);
|
|
}
|
|
|
|
QRect RoundAreaWithShadow::validateShadow(
|
|
int frameIndex,
|
|
float64 scale,
|
|
float64 radius) {
|
|
const auto rect = FrameCacheRect(frameIndex, kShadowCacheIndex, _outer);
|
|
if (_validShadow[frameIndex]) {
|
|
return rect;
|
|
}
|
|
|
|
_shadowBuffer.fill(Qt::transparent);
|
|
auto p = QPainter(&_shadowBuffer);
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
const auto center = _inner.center();
|
|
const auto add = style::ConvertScale(2.5);
|
|
const auto shift = style::ConvertScale(0.5);
|
|
const auto big = QRectF(_inner).marginsAdded({ add, add, add, add });
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(_shadow);
|
|
if (scale != 1.) {
|
|
p.translate(center);
|
|
p.scale(scale, scale);
|
|
p.translate(-center);
|
|
}
|
|
p.drawRoundedRect(big.translated(0, shift), radius, radius);
|
|
p.end();
|
|
_shadowBuffer = Images::Blur(std::move(_shadowBuffer));
|
|
|
|
auto q = QPainter(&_shadowParts);
|
|
q.setCompositionMode(QPainter::CompositionMode_Source);
|
|
q.drawImage(rect.topLeft() / style::DevicePixelRatio(), _shadowBuffer);
|
|
|
|
_validShadow[frameIndex] = true;
|
|
return rect;
|
|
}
|
|
|
|
void RoundAreaWithShadow::setBackgroundColor(const QColor &background) {
|
|
if (_background == background) {
|
|
return;
|
|
}
|
|
_background = background;
|
|
ranges::fill(_validBg, false);
|
|
}
|
|
|
|
ImageSubrect RoundAreaWithShadow::validateFrame(
|
|
int frameIndex,
|
|
float64 scale,
|
|
float64 radius) {
|
|
const auto result = ImageSubrect{
|
|
&_cacheBg,
|
|
FrameCacheRect(frameIndex, kBgCacheIndex, _outer)
|
|
};
|
|
if (_validBg[frameIndex]) {
|
|
return result;
|
|
}
|
|
|
|
const auto position = result.rect.topLeft() / style::DevicePixelRatio();
|
|
const auto inner = _inner.translated(position);
|
|
const auto shadowSource = validateShadow(frameIndex, scale, radius);
|
|
|
|
auto p = QPainter(&_cacheBg);
|
|
p.setCompositionMode(QPainter::CompositionMode_Source);
|
|
p.drawImage(position, _shadowParts, shadowSource);
|
|
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
|
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(_background);
|
|
if (scale != 1.) {
|
|
const auto center = inner.center();
|
|
p.save();
|
|
p.translate(center);
|
|
p.scale(scale, scale);
|
|
p.translate(-center);
|
|
}
|
|
p.drawRoundedRect(inner, radius, radius);
|
|
if (scale != 1.) {
|
|
p.restore();
|
|
}
|
|
|
|
_validBg[frameIndex] = true;
|
|
return result;
|
|
}
|
|
|
|
} // namespace Ui
|