init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,181 @@
// 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/animation_value.h"
#include "ui/painter.h"
#include <QtCore/QtMath> // M_PI
namespace anim {
namespace {
rpl::variable<bool> AnimationsDisabled = false;
int SlowMultiplierMinusOne/* = 0*/;
} // namespace
transition linear = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto result = delta * dt;
Ensures(!std::isnan(result));
return result;
};
transition sineInOut = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto result = -(delta / 2) * (cos(M_PI * dt) - 1);
Ensures(!std::isnan(result));
return result;
};
transition halfSine = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto result = delta * sin(M_PI * dt / 2);
Ensures(!std::isnan(result));
return result;
};
transition easeOutBack = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
static constexpr auto s = 1.70158;
const auto t = dt - 1;
Assert(!std::isnan(t));
const auto result = delta * (t * t * ((s + 1) * t + s) + 1);
Ensures(!std::isnan(result));
return result;
};
transition easeInCirc = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto result = -delta * (sqrt(1 - dt * dt) - 1);
Ensures(!std::isnan(result));
return result;
};
transition easeOutCirc = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto t = dt - 1;
Assert(!std::isnan(t));
const auto result = delta * sqrt(1 - t * t);
Ensures(!std::isnan(result));
return result;
};
transition easeInCubic = [](const float64 &delta, const float64 &dt) {
const auto result = delta * dt * dt * dt;
Ensures(!std::isnan(result));
return result;
};
transition easeOutCubic = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto t = dt - 1;
Assert(!std::isnan(t));
const auto result = delta * (t * t * t + 1);
Ensures(!std::isnan(result));
return result;
};
transition easeInQuint = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto t2 = dt * dt;
Assert(!std::isnan(t2));
const auto result = delta * t2 * t2 * dt;
Ensures(!std::isnan(result));
return result;
};
transition easeOutQuint = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto t = dt - 1, t2 = t * t;
Assert(!std::isnan(t));
Assert(!std::isnan(t2));
const auto result = delta * (t2 * t2 * t + 1);
Ensures(!std::isnan(result));
return result;
};
rpl::producer<bool> Disables() {
return AnimationsDisabled.value();
};
bool Disabled() {
return AnimationsDisabled.current();
}
void SetDisabled(bool disabled) {
AnimationsDisabled = disabled;
}
int SlowMultiplier() {
return (SlowMultiplierMinusOne + 1);
}
void SetSlowMultiplier(int multiplier) {
Expects(multiplier > 0);
SlowMultiplierMinusOne = multiplier - 1;
}
void DrawStaticLoading(
QPainter &p,
QRectF rect,
float64 stroke,
QPen pen,
QBrush brush) {
PainterHighQualityEnabler hq(p);
p.setBrush(brush);
pen.setWidthF(stroke);
pen.setCapStyle(Qt::RoundCap);
pen.setJoinStyle(Qt::RoundJoin);
p.setPen(pen);
p.drawEllipse(rect);
const auto center = rect.center();
const auto first = QPointF(center.x(), rect.y() + 1.5 * stroke);
const auto delta = center.y() - first.y();
const auto second = QPointF(center.x() + delta * 2 / 3., center.y());
if (delta > 0) {
QPainterPath path;
path.moveTo(first);
path.lineTo(center);
path.lineTo(second);
p.drawPath(path);
}
}
} // anim

View File

@@ -0,0 +1,420 @@
// 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
//
#pragma once
#include "ui/style/style_core.h"
#include "base/basic_types.h"
#include <QtGui/QPainterPath>
#include <crl/crl_time.h>
namespace anim {
enum class type : uchar {
normal,
instant,
};
enum class activation : uchar {
normal,
background,
};
enum class repeat : uchar {
loop,
once,
};
using transition = Fn<float64(float64 delta, float64 dt)>;
extern transition linear;
extern transition sineInOut;
extern transition halfSine;
extern transition easeOutBack;
extern transition easeInCirc;
extern transition easeOutCirc;
extern transition easeInCubic;
extern transition easeOutCubic;
extern transition easeInQuint;
extern transition easeOutQuint;
inline transition bumpy(float64 bump) {
auto dt0 = (bump - sqrt(bump * (bump - 1.)));
auto k = (1 / (2 * dt0 - 1));
return [bump, dt0, k](float64 delta, float64 dt) {
return delta * (bump - k * (dt - dt0) * (dt - dt0));
};
}
// Basic animated value.
class value {
public:
using ValueType = float64;
value() = default;
value(float64 from) : _cur(from), _from(from) {
}
value(float64 from, float64 to) : _cur(from), _from(from), _delta(to - from) {
}
void start(float64 to) {
_from = _cur;
_delta = to - _from;
}
void restart() {
_delta = _from + _delta - _cur;
_from = _cur;
}
float64 from() const {
return _from;
}
float64 current() const {
return _cur;
}
float64 to() const {
return _from + _delta;
}
void add(float64 delta) {
_from += delta;
_cur += delta;
}
value &update(float64 dt, transition func) {
_cur = _from + func(_delta, dt);
return *this;
}
void finish() {
_cur = _from + _delta;
_from = _cur;
_delta = 0;
}
private:
float64 _cur = 0.;
float64 _from = 0.;
float64 _delta = 0.;
};
TG_FORCE_INLINE float64 interpolateToF(int a, int b, float64 b_ratio) {
return a + float64(b - a) * b_ratio;
}
TG_FORCE_INLINE int interpolate(int a, int b, float64 b_ratio) {
return base::SafeRound(interpolateToF(a, b, b_ratio));
}
#ifdef ARCH_CPU_32_BITS
#define SHIFTED_USE_32BIT
#endif // ARCH_CPU_32_BITS
#ifdef SHIFTED_USE_32BIT
using ShiftedMultiplier = uint32;
struct Shifted {
Shifted() = default;
Shifted(uint32 low, uint32 high) : low(low), high(high) {
}
uint32 low = 0;
uint32 high = 0;
};
TG_FORCE_INLINE Shifted operator+(Shifted a, Shifted b) {
return Shifted(a.low + b.low, a.high + b.high);
}
TG_FORCE_INLINE Shifted operator*(Shifted shifted, ShiftedMultiplier multiplier) {
return Shifted(shifted.low * multiplier, shifted.high * multiplier);
}
TG_FORCE_INLINE Shifted operator*(ShiftedMultiplier multiplier, Shifted shifted) {
return Shifted(shifted.low * multiplier, shifted.high * multiplier);
}
TG_FORCE_INLINE Shifted shifted(uint32 components) {
return Shifted(
(components & 0x000000FFU) | ((components & 0x0000FF00U) << 8),
((components & 0x00FF0000U) >> 16) | ((components & 0xFF000000U) >> 8));
}
TG_FORCE_INLINE uint32 unshifted(Shifted components) {
return ((components.low & 0x0000FF00U) >> 8)
| ((components.low & 0xFF000000U) >> 16)
| ((components.high & 0x0000FF00U) << 8)
| (components.high & 0xFF000000U);
}
TG_FORCE_INLINE Shifted reshifted(Shifted components) {
return Shifted((components.low >> 8) & 0x00FF00FFU, (components.high >> 8) & 0x00FF00FFU);
}
TG_FORCE_INLINE Shifted shifted(QColor color) {
// Make it premultiplied.
auto alpha = static_cast<uint32>((color.alpha() & 0xFF) + 1);
auto components = Shifted(static_cast<uint32>(color.blue() & 0xFF) | (static_cast<uint32>(color.green() & 0xFF) << 16),
static_cast<uint32>(color.red() & 0xFF) | (static_cast<uint32>(255) << 16));
return reshifted(components * alpha);
}
TG_FORCE_INLINE uint32 getPremultiplied(QColor color) {
// Make it premultiplied.
auto alpha = static_cast<uint32>((color.alpha() & 0xFF) + 1);
auto components = Shifted(static_cast<uint32>(color.blue() & 0xFF) | (static_cast<uint32>(color.green() & 0xFF) << 16),
static_cast<uint32>(color.red() & 0xFF) | (static_cast<uint32>(255) << 16));
return unshifted(components * alpha);
}
TG_FORCE_INLINE uint32 getAlpha(Shifted components) {
return (components.high & 0x00FF0000U) >> 16;
}
TG_FORCE_INLINE Shifted non_premultiplied(QColor color) {
return Shifted(static_cast<uint32>(color.blue() & 0xFF) | (static_cast<uint32>(color.green() & 0xFF) << 16),
static_cast<uint32>(color.red() & 0xFF) | (static_cast<uint32>(color.alpha() & 0xFF) << 16));
}
TG_FORCE_INLINE QColor color(QColor a, QColor b, float64 b_ratio) {
auto bOpacity = std::clamp(interpolate(0, 255, b_ratio), 0, 255) + 1;
auto aOpacity = (256 - bOpacity);
auto components = (non_premultiplied(a) * aOpacity + non_premultiplied(b) * bOpacity);
return {
static_cast<int>((components.high >> 8) & 0xFF),
static_cast<int>((components.low >> 24) & 0xFF),
static_cast<int>((components.low >> 8) & 0xFF),
static_cast<int>((components.high >> 24) & 0xFF),
};
}
#else // SHIFTED_USE_32BIT
using ShiftedMultiplier = uint64;
struct Shifted {
Shifted() = default;
Shifted(uint32 value) : value(value) {
}
Shifted(uint64 value) : value(value) {
}
uint64 value = 0;
};
TG_FORCE_INLINE Shifted operator+(Shifted a, Shifted b) {
return Shifted(a.value + b.value);
}
TG_FORCE_INLINE Shifted operator*(Shifted shifted, ShiftedMultiplier multiplier) {
return Shifted(shifted.value * multiplier);
}
TG_FORCE_INLINE Shifted operator*(ShiftedMultiplier multiplier, Shifted shifted) {
return Shifted(shifted.value * multiplier);
}
TG_FORCE_INLINE Shifted shifted(uint32 components) {
auto wide = static_cast<uint64>(components);
return (wide & 0x00000000000000FFULL)
| ((wide & 0x000000000000FF00ULL) << 8)
| ((wide & 0x0000000000FF0000ULL) << 16)
| ((wide & 0x00000000FF000000ULL) << 24);
}
TG_FORCE_INLINE uint32 unshifted(Shifted components) {
return static_cast<uint32>((components.value & 0x000000000000FF00ULL) >> 8)
| static_cast<uint32>((components.value & 0x00000000FF000000ULL) >> 16)
| static_cast<uint32>((components.value & 0x0000FF0000000000ULL) >> 24)
| static_cast<uint32>((components.value & 0xFF00000000000000ULL) >> 32);
}
TG_FORCE_INLINE Shifted reshifted(Shifted components) {
return (components.value >> 8) & 0x00FF00FF00FF00FFULL;
}
TG_FORCE_INLINE Shifted shifted(QColor color) {
// Make it premultiplied.
auto alpha = static_cast<uint64>((color.alpha() & 0xFF) + 1);
auto components = static_cast<uint64>(color.blue() & 0xFF)
| (static_cast<uint64>(color.green() & 0xFF) << 16)
| (static_cast<uint64>(color.red() & 0xFF) << 32)
| (static_cast<uint64>(255) << 48);
return reshifted(components * alpha);
}
TG_FORCE_INLINE uint32 getPremultiplied(QColor color) {
// Make it premultiplied.
auto alpha = static_cast<uint64>((color.alpha() & 0xFF) + 1);
auto components = static_cast<uint64>(color.blue() & 0xFF)
| (static_cast<uint64>(color.green() & 0xFF) << 16)
| (static_cast<uint64>(color.red() & 0xFF) << 32)
| (static_cast<uint64>(255) << 48);
return unshifted(components * alpha);
}
TG_FORCE_INLINE uint32 getAlpha(Shifted components) {
return (components.value & 0x00FF000000000000ULL) >> 48;
}
TG_FORCE_INLINE Shifted non_premultiplied(QColor color) {
return static_cast<uint64>(color.blue() & 0xFF)
| (static_cast<uint64>(color.green() & 0xFF) << 16)
| (static_cast<uint64>(color.red() & 0xFF) << 32)
| (static_cast<uint64>(color.alpha() & 0xFF) << 48);
}
TG_FORCE_INLINE QColor color(QColor a, QColor b, float64 b_ratio) {
auto bOpacity = std::clamp(interpolate(0, 255, b_ratio), 0, 255) + 1;
auto aOpacity = (256 - bOpacity);
auto components = (non_premultiplied(a) * aOpacity + non_premultiplied(b) * bOpacity);
return {
static_cast<int>((components.value >> 40) & 0xFF),
static_cast<int>((components.value >> 24) & 0xFF),
static_cast<int>((components.value >> 8) & 0xFF),
static_cast<int>((components.value >> 56) & 0xFF),
};
}
#endif // SHIFTED_USE_32BIT
TG_FORCE_INLINE QColor color(style::color a, QColor b, float64 b_ratio) {
return color(a->c, b, b_ratio);
}
TG_FORCE_INLINE QColor color(QColor a, style::color b, float64 b_ratio) {
return color(a, b->c, b_ratio);
}
TG_FORCE_INLINE QColor color(style::color a, style::color b, float64 b_ratio) {
return color(a->c, b->c, b_ratio);
}
TG_FORCE_INLINE QPen pen(QColor a, QColor b, float64 b_ratio) {
return color(a, b, b_ratio);
}
TG_FORCE_INLINE QPen pen(style::color a, QColor b, float64 b_ratio) {
return (b_ratio > 0) ? pen(a->c, b, b_ratio) : a;
}
TG_FORCE_INLINE QPen pen(QColor a, style::color b, float64 b_ratio) {
return (b_ratio < 1) ? pen(a, b->c, b_ratio) : b;
}
TG_FORCE_INLINE QPen pen(style::color a, style::color b, float64 b_ratio) {
return (b_ratio > 0) ? ((b_ratio < 1) ? pen(a->c, b->c, b_ratio) : b) : a;
}
TG_FORCE_INLINE QBrush brush(QColor a, QColor b, float64 b_ratio) {
return color(a, b, b_ratio);
}
TG_FORCE_INLINE QBrush brush(style::color a, QColor b, float64 b_ratio) {
return (b_ratio > 0) ? brush(a->c, b, b_ratio) : a;
}
TG_FORCE_INLINE QBrush brush(QColor a, style::color b, float64 b_ratio) {
return (b_ratio < 1) ? brush(a, b->c, b_ratio) : b;
}
TG_FORCE_INLINE QBrush brush(style::color a, style::color b, float64 b_ratio) {
return (b_ratio > 0) ? ((b_ratio < 1) ? brush(a->c, b->c, b_ratio) : b) : a;
}
TG_FORCE_INLINE QColor with_alpha(QColor color, float64 alpha) {
color.setAlphaF(color.alphaF() * alpha);
return color;
}
template <int N>
QPainterPath interpolate(QPointF (&from)[N], QPointF (&to)[N], float64 k) {
static_assert(N > 1, "Wrong points count in path!");
auto from_coef = 1. - k, to_coef = k;
QPainterPath result;
auto x = from[0].x() * from_coef + to[0].x() * to_coef;
auto y = from[0].y() * from_coef + to[0].y() * to_coef;
result.moveTo(x, y);
for (int i = 1; i != N; ++i) {
result.lineTo(from[i].x() * from_coef + to[i].x() * to_coef, from[i].y() * from_coef + to[i].y() * to_coef);
}
result.lineTo(x, y);
return result;
}
template <int N>
QPainterPath path(QPointF (&from)[N]) {
static_assert(N > 1, "Wrong points count in path!");
QPainterPath result;
auto x = from[0].x();
auto y = from[0].y();
result.moveTo(x, y);
for (int i = 1; i != N; ++i) {
result.lineTo(from[i].x(), from[i].y());
}
result.lineTo(x, y);
return result;
}
rpl::producer<bool> Disables();
bool Disabled();
void SetDisabled(bool disabled);
int SlowMultiplier();
void SetSlowMultiplier(int multiplier); // 1 - default, 10 - slow x10.
void DrawStaticLoading(
QPainter &p,
QRectF rect,
float64 stroke,
QPen pen,
QBrush brush = Qt::NoBrush);
class continuous_value {
public:
continuous_value() = default;
continuous_value(float64 duration) : _duration(duration) {
}
void start(float64 to, float64 duration) {
_to = to;
_delta = (_to - _cur) / duration;
}
void start(float64 to) {
start(to, _duration);
}
void reset() {
_to = _cur = _delta = 0.;
}
float64 current() const {
return _cur;
}
float64 to() const {
return _to;
}
float64 delta() const {
return _delta;
}
void update(crl::time dt, Fn<void(float64 &)> &&callback = nullptr) {
if (_to != _cur) {
_cur += _delta * dt;
if ((_to != _cur) && ((_delta > 0) == (_cur > _to))) {
_cur = _to;
}
if (callback) {
callback(_cur);
}
}
}
private:
float64 _duration = 0.;
float64 _to = 0.;
float64 _cur = 0.;
float64 _delta = 0.;
};
} // namespace anim

View File

@@ -0,0 +1,26 @@
// 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
//
#pragma once
namespace anim {
TG_FORCE_INLINE float64 interpolateF(float a, float b, float64 b_ratio) {
return a + float64(b - a) * b_ratio;
};
TG_FORCE_INLINE QRectF interpolatedRectF(
const QRectF &r1,
const QRectF &r2,
float64 ratio) {
return QRectF(
interpolateF(r1.x(), r2.x(), ratio),
interpolateF(r1.y(), r2.y(), ratio),
interpolateF(r1.width(), r2.width(), ratio),
interpolateF(r1.height(), r2.height(), ratio));
}
} // namespace anim

View File

@@ -0,0 +1,212 @@
// 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/animations.h"
#include "base/invoke_queued.h"
#include "ui/ui_utility.h"
#include "styles/style_basic.h"
#include <QtCore/QPointer>
#include <crl/crl_on_main.h>
#include <crl/crl.h>
#include <rpl/filter.h>
#include <range/v3/algorithm/remove_if.hpp>
#include <range/v3/algorithm/remove.hpp>
#include <range/v3/algorithm/find.hpp>
namespace Ui {
namespace Animations {
namespace {
constexpr auto kAnimationTick = crl::time(1000) / st::universalDuration;
constexpr auto kIgnoreUpdatesTimeout = crl::time(4);
Manager *ManagerInstance = nullptr;
} // namespace
void Basic::start() {
Expects(ManagerInstance != nullptr);
if (animating()) {
restart();
} else {
ManagerInstance->start(this);
}
}
void Basic::stop() {
Expects(ManagerInstance != nullptr);
if (animating()) {
ManagerInstance->stop(this);
}
}
void Basic::restart() {
Expects(_started >= 0);
_started = crl::now();
Ensures(_started >= 0);
}
void Basic::markStarted() {
Expects(_started < 0);
_started = crl::now();
Ensures(_started >= 0);
}
void Basic::markStopped() {
Expects(_started >= 0);
_started = -1;
}
Manager::Manager() {
Expects(ManagerInstance == nullptr);
ManagerInstance = this;
crl::on_main_update_requests(
) | rpl::filter([=] {
return (_lastUpdateTime + kIgnoreUpdatesTimeout < crl::now());
}) | rpl::on_next([=] {
update();
}, _lifetime);
}
Manager::~Manager() {
Expects(ManagerInstance == this);
Expects(_active.empty());
Expects(_starting.empty());
ManagerInstance = nullptr;
}
void Manager::start(not_null<Basic*> animation) {
_forceImmediateUpdate = true;
if (_updating) {
_starting.emplace_back(animation.get());
} else {
schedule();
_active.emplace_back(animation.get());
}
}
void Manager::stop(not_null<Basic*> animation) {
if (empty(_active) && empty(_starting)) {
return;
}
const auto value = animation.get();
const auto proj = &ActiveBasicPointer::get;
auto &list = _updating ? _starting : _active;
list.erase(ranges::remove(list, value, proj), end(list));
if (_updating) {
const auto i = ranges::find(_active, value, proj);
if (i != end(_active)) {
*i = nullptr;
_removedWhileUpdating = true;
}
} else if (empty(_active)) {
stopTimer();
}
}
void Manager::update() {
if (_active.empty() || _updating || _scheduled) {
return;
}
const auto now = crl::now();
if (_forceImmediateUpdate) {
_forceImmediateUpdate = false;
}
schedule();
_updating = true;
const auto guard = gsl::finally([&] { _updating = false; });
_lastUpdateTime = now;
const auto isFinished = [&](const ActiveBasicPointer &element) {
return !element.call(now);
};
_active.erase(ranges::remove_if(_active, isFinished), end(_active));
if (_removedWhileUpdating) {
_removedWhileUpdating = false;
const auto proj = &ActiveBasicPointer::get;
_active.erase(ranges::remove(_active, nullptr, proj), end(_active));
}
if (!empty(_starting)) {
_active.insert(
end(_active),
std::make_move_iterator(begin(_starting)),
std::make_move_iterator(end(_starting)));
_starting.clear();
}
}
void Manager::updateQueued() {
Expects(_timerId == 0);
_timerId = -1;
InvokeQueued(delayedCallGuard(), [=] {
Expects(_timerId < 0);
_timerId = 0;
update();
});
}
void Manager::schedule() {
if (_scheduled || _timerId < 0) {
return;
}
stopTimer();
_scheduled = true;
PostponeCall(delayedCallGuard(), [=] {
_scheduled = false;
if (_active.empty()) {
return;
}
if (_forceImmediateUpdate) {
_forceImmediateUpdate = false;
updateQueued();
} else {
const auto next = _lastUpdateTime + kAnimationTick;
const auto now = crl::now();
if (now < next) {
_timerId = startTimer(next - now, Qt::PreciseTimer);
} else {
updateQueued();
}
}
});
}
not_null<const QObject*> Manager::delayedCallGuard() const {
return static_cast<const QObject*>(this);
}
void Manager::stopTimer() {
if (_timerId > 0) {
killTimer(base::take(_timerId));
}
}
void Manager::timerEvent(QTimerEvent *e) {
update();
}
} // namespace Animations
} // namespace Ui

View File

@@ -0,0 +1,460 @@
// 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
//
#pragma once
#include "ui/effects/animation_value.h"
#include <crl/crl_time.h>
#include <rpl/lifetime.h>
#include <QtCore/QObject>
namespace Ui {
namespace Animations {
class Manager;
class Basic final {
public:
Basic() = default;
Basic(const Basic &other) = delete;
Basic &operator=(const Basic &other) = delete;
Basic(Basic &&other);
Basic &operator=(Basic &&other);
template <typename Callback>
explicit Basic(Callback &&callback);
template <typename Callback>
void init(Callback &&callback);
void start();
void stop();
[[nodiscard]] crl::time started() const;
[[nodiscard]] bool animating() const;
~Basic();
private:
friend class Manager;
template <typename Callback>
[[nodiscard]] static Fn<bool(crl::time)> Prepare(Callback &&callback);
[[nodiscard]] bool call(crl::time now) const;
void restart();
void markStarted();
void markStopped();
crl::time _started = -1;
Fn<bool(crl::time)> _callback;
};
class Simple final {
public:
template <typename Callback>
void start(
Callback &&callback,
float64 from,
float64 to,
crl::time duration,
anim::transition transition = anim::linear);
void change(
float64 to,
crl::time duration,
anim::transition transition = anim::linear);
void stop();
[[nodiscard]] bool animating() const;
[[nodiscard]] float64 value(float64 final) const;
private:
class ShortTracker {
public:
ShortTracker() {
restart();
}
ShortTracker(const ShortTracker &other) = delete;
ShortTracker &operator=(const ShortTracker &other) = delete;
~ShortTracker() {
release();
}
void restart() {
if (!std::exchange(_paused, true)) {
style::internal::StartShortAnimation();
}
}
void release() {
if (std::exchange(_paused, false)) {
style::internal::StopShortAnimation();
}
}
private:
bool _paused = false;
};
struct Data {
explicit Data(float64 initial) : value(initial) {
}
~Data() {
if (markOnDelete) {
*markOnDelete = true;
}
}
Basic animation;
anim::transition transition;
float64 from = 0.;
float64 delta = 0.;
float64 value = 0.;
float64 duration = 0.;
bool *markOnDelete = nullptr;
ShortTracker tracker;
};
template <typename Callback>
[[nodiscard]] static decltype(auto) Prepare(Callback &&callback);
void prepare(float64 from, crl::time duration);
void startPrepared(
float64 to,
crl::time duration,
anim::transition transition);
static constexpr auto kLongAnimationDuration = crl::time(1000);
mutable std::unique_ptr<Data> _data;
};
class Manager final : private QObject {
public:
Manager();
~Manager();
void update();
private:
class ActiveBasicPointer {
public:
ActiveBasicPointer(Basic *value = nullptr) : _value(value) {
if (_value) {
_value->markStarted();
}
}
ActiveBasicPointer(ActiveBasicPointer &&other)
: _value(base::take(other._value)) {
}
ActiveBasicPointer &operator=(ActiveBasicPointer &&other) {
if (_value != other._value) {
if (_value) {
_value->markStopped();
}
_value = base::take(other._value);
}
return *this;
}
~ActiveBasicPointer() {
if (_value) {
_value->markStopped();
}
}
[[nodiscard]] bool call(crl::time now) const {
return _value && _value->call(now);
}
friend inline bool operator==(
const ActiveBasicPointer &a,
const ActiveBasicPointer &b) {
return a._value == b._value;
}
Basic *get() const {
return _value;
}
private:
Basic *_value = nullptr;
};
friend class Basic;
void timerEvent(QTimerEvent *e) override;
void start(not_null<Basic*> animation);
void stop(not_null<Basic*> animation);
void schedule();
void updateQueued();
void stopTimer();
not_null<const QObject*> delayedCallGuard() const;
crl::time _lastUpdateTime = 0;
int _timerId = 0;
bool _updating = false;
bool _removedWhileUpdating = false;
bool _scheduled = false;
bool _forceImmediateUpdate = false;
std::vector<ActiveBasicPointer> _active;
std::vector<ActiveBasicPointer> _starting;
rpl::lifetime _lifetime;
};
template <typename Callback>
Fn<bool(crl::time)> Basic__PrepareCrlTime(Callback &&callback) {
using Return = decltype(callback(crl::time(0)));
if constexpr (std::is_convertible_v<Return, bool>) {
return std::forward<Callback>(callback);
} else if constexpr (std::is_same_v<Return, void>) {
return [callback = std::forward<Callback>(callback)](
crl::time time) {
callback(time);
return true;
};
} else {
static_assert(false_t(callback), "Expected void or bool.");
}
}
template <typename Callback>
Fn<bool(crl::time)> Basic__PreparePlain(Callback &&callback) {
using Return = decltype(callback());
if constexpr (std::is_convertible_v<Return, bool>) {
return [callback = std::forward<Callback>(callback)](crl::time) {
return callback();
};
} else if constexpr (std::is_same_v<Return, void>) {
return [callback = std::forward<Callback>(callback)](crl::time) {
callback();
return true;
};
} else {
static_assert(false_t(callback), "Expected void or bool.");
}
}
template <typename Callback>
inline Fn<bool(crl::time)> Basic::Prepare(Callback &&callback) {
if constexpr (rpl::details::is_callable_plain_v<Callback, crl::time>) {
return Basic__PrepareCrlTime(std::forward<Callback>(callback));
} else if constexpr (rpl::details::is_callable_plain_v<Callback>) {
return Basic__PreparePlain(std::forward<Callback>(callback));
} else {
static_assert(false_t(callback), "Expected crl::time or no args.");
}
}
template <typename Callback>
inline Basic::Basic(Callback &&callback)
: _callback(Prepare(std::forward<Callback>(callback))) {
}
template <typename Callback>
inline void Basic::init(Callback &&callback) {
_callback = Prepare(std::forward<Callback>(callback));
}
TG_FORCE_INLINE crl::time Basic::started() const {
return _started;
}
TG_FORCE_INLINE bool Basic::animating() const {
return (_started >= 0);
}
TG_FORCE_INLINE bool Basic::call(crl::time now) const {
Expects(_started >= 0);
// _started may be greater than now if we called restart while iterating.
const auto onstack = _callback;
return onstack(std::max(_started, now));
}
inline Basic::Basic(Basic &&other) : _callback(base::take(other._callback)) {
if (other.animating()) {
const auto started = other._started;
other.stop();
start();
other._started = started;
}
}
inline Basic &Basic::operator=(Basic &&other) {
_callback = base::take(other._callback);
if (animating()) {
stop();
}
if (other.animating()) {
const auto started = other._started;
other.stop();
start();
other._started = started;
}
return *this;
}
inline Basic::~Basic() {
stop();
}
template <typename Callback>
decltype(auto) Simple__PrepareFloat64(Callback &&callback) {
using Return = decltype(callback(float64(0.)));
if constexpr (std::is_convertible_v<Return, bool>) {
return std::forward<Callback>(callback);
} else if constexpr (std::is_same_v<Return, void>) {
return [callback = std::forward<Callback>(callback)](
float64 value) {
callback(value);
return true;
};
} else {
static_assert(false_t(callback), "Expected void or float64.");
}
}
template <typename Callback>
decltype(auto) Simple__PreparePlain(Callback &&callback) {
using Return = decltype(callback());
if constexpr (std::is_convertible_v<Return, bool>) {
return [callback = std::forward<Callback>(callback)](float64) {
return callback();
};
} else if constexpr (std::is_same_v<Return, void>) {
return [callback = std::forward<Callback>(callback)](float64) {
callback();
return true;
};
} else {
static_assert(false_t(callback), "Expected void or bool.");
}
}
template <typename Callback>
decltype(auto) Simple::Prepare(Callback &&callback) {
if constexpr (rpl::details::is_callable_plain_v<Callback, float64>) {
return Simple__PrepareFloat64(std::forward<Callback>(callback));
} else if constexpr (rpl::details::is_callable_plain_v<Callback>) {
return Simple__PreparePlain(std::forward<Callback>(callback));
} else {
static_assert(false_t(callback), "Expected float64 or no args.");
}
}
template <typename Callback>
inline void Simple::start(
Callback &&callback,
float64 from,
float64 to,
crl::time duration,
anim::transition transition) {
prepare(from, duration);
_data->animation.init([
that = _data.get(),
callback = Prepare(std::forward<Callback>(callback))
](crl::time now) {
Assert(!std::isnan(double(now - that->animation.started())));
const auto time = anim::Disabled()
? that->duration
: (now - that->animation.started());
Assert(!std::isnan(time));
Assert(!std::isnan(that->delta));
Assert(!std::isnan(that->duration));
const auto finished = (time >= that->duration);
Assert(finished || that->duration > 0);
const auto progressRatio = finished ? 1. : time / that->duration;
Assert(!std::isnan(progressRatio));
const auto progress = finished
? that->delta
: that->transition(that->delta, progressRatio);
Assert(!std::isnan(that->from));
Assert(!std::isnan(progress));
that->value = that->from + progress;
Assert(!std::isnan(that->value));
if (finished) {
that->animation.stop();
}
auto deleted = false;
that->markOnDelete = &deleted;
const auto result = callback(that->value) && !finished;
if (!deleted) {
that->markOnDelete = nullptr;
if (!result) {
that->tracker.release();
}
}
return result;
});
startPrepared(to, duration, transition);
}
inline void Simple::change(
float64 to,
crl::time duration,
anim::transition transition) {
Expects(_data != nullptr);
prepare(0. /* ignored */, duration);
startPrepared(to, duration, transition);
}
inline void Simple::prepare(float64 from, crl::time duration) {
const auto isLong = (duration > kLongAnimationDuration);
if (!_data) {
_data = std::make_unique<Data>(from);
} else if (!isLong) {
_data->tracker.restart();
}
if (isLong) {
_data->tracker.release();
}
}
inline void Simple::stop() {
_data = nullptr;
}
inline bool Simple::animating() const {
if (!_data) {
return false;
} else if (!_data->animation.animating()) {
_data = nullptr;
return false;
}
return true;
}
TG_FORCE_INLINE float64 Simple::value(float64 final) const {
if (animating()) {
Assert(!std::isnan(_data->value));
return _data->value;
}
Assert(!std::isnan(final));
return final;
}
inline void Simple::startPrepared(
float64 to,
crl::time duration,
anim::transition transition) {
_data->from = _data->value;
_data->delta = to - _data->from;
_data->duration = duration * anim::SlowMultiplier();
_data->transition = transition;
_data->animation.start();
}
} // namespace Animations
} // namespace Ui

View File

@@ -0,0 +1,206 @@
// 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/cross_animation.h"
#include "ui/effects/animation_value.h"
#include "ui/arc_angles.h"
#include "ui/painter.h"
#include <QtCore/QtMath>
#include <QtGui/QPainterPath>
namespace Ui {
namespace {
constexpr auto kPointCount = 12;
constexpr auto kStaticLoadingValue = float64(-666);
//
// 1 3
// X X X X
// X X X X
// 0 X X 4
// X X X X
// X 2 X
// X X
// X X
// 11 5
// X X
// X X
// X 8 X
// X X X X
// 10 X X 6
// X X X X
// X X X X
// 9 7
//
void transformLoadingCross(float64 loading, std::array<QPointF, kPointCount> &points, int &paintPointsCount) {
auto moveTo = [](QPointF &point, QPointF &to, float64 ratio) {
point = point * (1. - ratio) + to * ratio;
};
auto moveFrom = [](QPointF &point, QPointF &from, float64 ratio) {
point = from * (1. - ratio) + point * ratio;
};
auto paintPoints = [&points, &paintPointsCount](std::initializer_list<int> &&indices) {
auto index = 0;
for (auto paintIndex : indices) {
points[index++] = points[paintIndex];
}
paintPointsCount = indices.size();
};
if (loading < 0.125) {
auto ratio = loading / 0.125;
moveTo(points[6], points[5], ratio);
moveTo(points[7], points[8], ratio);
} else if (loading < 0.25) {
auto ratio = (loading - 0.125) / 0.125;
moveTo(points[9], points[8], ratio);
moveTo(points[10], points[11], ratio);
paintPoints({ 0, 1, 2, 3, 4, 9, 10, 11 });
} else if (loading < 0.375) {
auto ratio = (loading - 0.25) / 0.125;
moveTo(points[0], points[11], ratio);
moveTo(points[1], points[2], ratio);
paintPoints({ 0, 1, 2, 3, 4, 8 });
} else if (loading < 0.5) {
auto ratio = (loading - 0.375) / 0.125;
moveTo(points[8], points[4], ratio);
moveTo(points[11], points[3], ratio);
paintPoints({ 3, 4, 8, 11 });
} else if (loading < 0.625) {
auto ratio = (loading - 0.5) / 0.125;
moveFrom(points[8], points[4], ratio);
moveFrom(points[11], points[3], ratio);
paintPoints({ 3, 4, 8, 11 });
} else if (loading < 0.75) {
auto ratio = (loading - 0.625) / 0.125;
moveFrom(points[6], points[5], ratio);
moveFrom(points[7], points[8], ratio);
paintPoints({ 3, 4, 5, 6, 7, 11 });
} else if (loading < 0.875) {
auto ratio = (loading - 0.75) / 0.125;
moveFrom(points[9], points[8], ratio);
moveFrom(points[10], points[11], ratio);
paintPoints({ 3, 4, 5, 6, 7, 8, 9, 10 });
} else {
auto ratio = (loading - 0.875) / 0.125;
moveFrom(points[0], points[11], ratio);
moveFrom(points[1], points[2], ratio);
}
}
} // namespace
void CrossAnimation::paintStaticLoading(
QPainter &p,
const style::CrossAnimation &st,
style::color color,
int x,
int y,
int outerWidth,
float64 shown) {
paint(p, st, color, x, y, outerWidth, shown, kStaticLoadingValue);
}
void CrossAnimation::paint(
QPainter &p,
const style::CrossAnimation &st,
style::color color,
int x,
int y,
int outerWidth,
float64 shown,
float64 loading) {
PainterHighQualityEnabler hq(p);
const auto stroke = style::ConvertScaleExact(st.stroke);
const auto deleteScale = shown + st.minScale * (1. - shown);
const auto deleteSkip = (deleteScale * st.skip)
+ (1. - deleteScale) * (st.size / 2);
const auto deleteLeft = 0.
+ style::rtlpoint(x + deleteSkip, 0, outerWidth).x();
const auto deleteTop = y + deleteSkip + 0.;
const auto deleteWidth = st.size - 2 * deleteSkip;
const auto deleteHeight = st.size - 2 * deleteSkip;
const auto deleteStroke = stroke / M_SQRT2;
std::array<QPointF, kPointCount> pathDelete = { {
{ deleteLeft, deleteTop + deleteStroke },
{ deleteLeft + deleteStroke, deleteTop },
{ deleteLeft + (deleteWidth / 2.), deleteTop + (deleteHeight / 2.) - deleteStroke },
{ deleteLeft + deleteWidth - deleteStroke, deleteTop },
{ deleteLeft + deleteWidth, deleteTop + deleteStroke },
{ deleteLeft + (deleteWidth / 2.) + deleteStroke, deleteTop + (deleteHeight / 2.) },
{ deleteLeft + deleteWidth, deleteTop + deleteHeight - deleteStroke },
{ deleteLeft + deleteWidth - deleteStroke, deleteTop + deleteHeight },
{ deleteLeft + (deleteWidth / 2.), deleteTop + (deleteHeight / 2.) + deleteStroke },
{ deleteLeft + deleteStroke, deleteTop + deleteHeight },
{ deleteLeft, deleteTop + deleteHeight - deleteStroke },
{ deleteLeft + (deleteWidth / 2.) - deleteStroke, deleteTop + (deleteHeight / 2.) },
} };
auto pathDeleteSize = kPointCount;
const auto staticLoading = (loading == kStaticLoadingValue);
auto loadingArcLength = staticLoading ? arc::kFullLength : 0;
if (loading > 0.) {
transformLoadingCross(loading, pathDelete, pathDeleteSize);
auto loadingArc = (loading >= 0.5) ? (loading - 1.) : loading;
loadingArcLength = qRound(-loadingArc * 2 * arc::kFullLength);
}
if (!staticLoading) {
if (shown < 1.) {
auto alpha = -(shown - 1.) * M_PI_2;
auto cosalpha = cos(alpha);
auto sinalpha = sin(alpha);
auto shiftx = deleteLeft + (deleteWidth / 2.);
auto shifty = deleteTop + (deleteHeight / 2.);
for (auto &point : pathDelete) {
auto x = point.x() - shiftx;
auto y = point.y() - shifty;
point.setX(shiftx + x * cosalpha - y * sinalpha);
point.setY(shifty + y * cosalpha + x * sinalpha);
}
}
QPainterPath path;
path.moveTo(pathDelete[0]);
for (int i = 1; i != pathDeleteSize; ++i) {
path.lineTo(pathDelete[i]);
}
path.lineTo(pathDelete[0]);
p.fillPath(path, color);
}
if (loadingArcLength != 0) {
auto roundSkip = (st.size * (1 - M_SQRT2) + 2 * M_SQRT2 * deleteSkip + stroke) / 2;
auto roundPart = QRectF(x + roundSkip, y + roundSkip, st.size - 2 * roundSkip, st.size - 2 * roundSkip);
if (staticLoading) {
anim::DrawStaticLoading(p, roundPart, stroke, color);
} else {
auto loadingArcStart = arc::kQuarterLength / 2;
if (shown < 1.) {
loadingArcStart -= qRound(-(shown - 1.) * arc::kQuarterLength);
}
if (loadingArcLength < 0) {
loadingArcStart += loadingArcLength;
loadingArcLength = -loadingArcLength;
}
p.setBrush(Qt::NoBrush);
auto pen = color->p;
pen.setWidthF(stroke);
pen.setCapStyle(Qt::RoundCap);
p.setPen(pen);
p.drawArc(roundPart, loadingArcStart, loadingArcLength);
}
}
}
} // namespace Ui

View File

@@ -0,0 +1,37 @@
// 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
//
#pragma once
#include "styles/style_widgets.h"
class Painter;
namespace Ui {
class CrossAnimation {
public:
static void paint(
QPainter &p,
const style::CrossAnimation &st,
style::color color,
int x,
int y,
int outerWidth,
float64 shown,
float64 loading = 0.);
static void paintStaticLoading(
QPainter &p,
const style::CrossAnimation &st,
style::color color,
int x,
int y,
int outerWidth,
float64 shown);
};
} // namespace Ui

View File

@@ -0,0 +1,130 @@
// 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/cross_line.h"
#include "ui/painter.h"
namespace Ui {
namespace {
[[nodiscard]] float64 StrokeWidth(
const style::CrossLineAnimation &st) noexcept {
return float64(st.stroke)
/ (st.strokeDenominator ? st.strokeDenominator : 1);
}
} // namespace
CrossLineAnimation::CrossLineAnimation(
const style::CrossLineAnimation &st,
bool reversed,
float angle)
: _st(st)
, _reversed(reversed)
, _transparentPen(
Qt::transparent,
StrokeWidth(st),
Qt::SolidLine,
Qt::RoundCap)
, _strokePen(st.fg, StrokeWidth(st), Qt::SolidLine, Qt::RoundCap)
, _line(st.startPosition, st.endPosition) {
_line.setAngle(angle);
}
void CrossLineAnimation::paint(
QPainter &p,
QPoint position,
float64 progress,
std::optional<QColor> colorOverride) {
paint(p, position.x(), position.y(), progress, colorOverride);
}
void CrossLineAnimation::paint(
QPainter &p,
int left,
int top,
float64 progress,
std::optional<QColor> colorOverride) {
if (progress == 0.) {
if (colorOverride) {
_st.icon.paint(p, left, top, _st.icon.width(), *colorOverride);
} else {
_st.icon.paint(p, left, top, _st.icon.width());
}
} else if (progress == 1.) {
auto &complete = colorOverride
? _completeCrossOverride
: _completeCross;
if (complete.isNull()) {
fillFrame(progress, colorOverride);
complete = _frame;
}
p.drawImage(left, top, complete);
} else {
fillFrame(progress, colorOverride);
p.drawImage(left, top, _frame);
}
}
void CrossLineAnimation::fillFrame(
float64 progress,
std::optional<QColor> colorOverride) {
const auto ratio = style::DevicePixelRatio();
if (_frame.isNull()) {
_frame = QImage(
_st.icon.size() * ratio,
QImage::Format_ARGB32_Premultiplied);
_frame.setDevicePixelRatio(ratio);
}
_frame.fill(Qt::transparent);
auto topLine = _line;
topLine.setLength(topLine.length() * progress);
auto bottomLine = topLine.translated(0, _strokePen.widthF() + 1);
auto q = QPainter(&_frame);
PainterHighQualityEnabler hq(q);
const auto colorize = ((colorOverride && colorOverride->alpha() != 255)
|| (!colorOverride && _st.fg->c.alpha() != 255));
const auto color = colorize
? QColor(255, 255, 255)
: colorOverride;
if (color) {
_st.icon.paint(q, 0, 0, _st.icon.width(), *color);
} else {
_st.icon.paint(q, 0, 0, _st.icon.width());
}
if (color) {
auto pen = _strokePen;
pen.setColor(*color);
q.setPen(pen);
} else {
q.setPen(_strokePen);
}
q.drawLine(_reversed ? topLine : bottomLine);
q.setCompositionMode(QPainter::CompositionMode_Source);
q.setPen(_transparentPen);
q.drawLine(_reversed ? bottomLine : topLine);
q.end();
if (colorize) {
style::colorizeImage(
_frame,
colorOverride.value_or(_st.fg->c),
&_frame);
}
}
void CrossLineAnimation::invalidate() {
_completeCross = QImage();
_completeCrossOverride = QImage();
_strokePen = QPen(_st.fg, StrokeWidth(_st), Qt::SolidLine, Qt::RoundCap);
}
} // namespace Ui

View File

@@ -0,0 +1,48 @@
// 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
//
#pragma once
#include "styles/style_widgets.h"
namespace Ui {
class CrossLineAnimation {
public:
CrossLineAnimation(
const style::CrossLineAnimation &st,
bool reversed = false,
float angle = 315);
void paint(
QPainter &p,
QPoint position,
float64 progress,
std::optional<QColor> colorOverride = std::nullopt);
void paint(
QPainter &p,
int left,
int top,
float64 progress,
std::optional<QColor> colorOverride = std::nullopt);
void invalidate();
private:
void fillFrame(float64 progress, std::optional<QColor> colorOverride);
const style::CrossLineAnimation &_st;
const bool _reversed;
const QPen _transparentPen;
QPen _strokePen;
QLineF _line;
QImage _frame;
QImage _completeCross;
QImage _completeCrossOverride;
};
} // namespace Ui

View File

@@ -0,0 +1,161 @@
// 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/fade_animation.h"
#include "ui/ui_utility.h"
#include "ui/painter.h"
namespace Ui {
namespace {
constexpr int kWideScale = 5;
} // namespace
FadeAnimation::FadeAnimation(RpWidget *widget, float64 scale)
: _widget(widget)
, _scale(scale) {
}
bool FadeAnimation::paint(QPainter &p) {
if (_cache.isNull()) return false;
const auto cache = _cache;
auto opacity = _animation.value(_visible ? 1. : 0.);
p.setOpacity(opacity);
if (_scale < 1.) {
PainterHighQualityEnabler hq(p);
auto targetRect = QRect(
(1 - kWideScale) / 2 * _size.width(),
(1 - kWideScale) / 2 * _size.height(),
kWideScale * _size.width(),
kWideScale * _size.height());
auto scale = opacity + (1. - opacity) * _scale;
auto shownWidth = anim::interpolate(
(1 - kWideScale) / 2 * _size.width(),
0,
scale);
auto shownHeight = anim::interpolate(
(1 - kWideScale) / 2 * _size.height(),
0,
scale);
auto margins = QMargins(
shownWidth,
shownHeight,
shownWidth,
shownHeight);
p.drawPixmap(targetRect.marginsAdded(margins), cache);
} else {
p.drawPixmap(0, 0, cache);
}
return true;
}
void FadeAnimation::refreshCache() {
if (!_cache.isNull()) {
_cache = QPixmap();
_cache = grabContent();
Assert(!_cache.isNull());
}
}
QPixmap FadeAnimation::grabContent() {
SendPendingMoveResizeEvents(_widget);
_size = _widget->size();
const auto pixelRatio = style::DevicePixelRatio();
if (_size.isEmpty()) {
auto image = QImage(
pixelRatio,
pixelRatio,
QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::transparent);
return PixmapFromImage(std::move(image));
}
auto widgetContent = GrabWidget(_widget);
if (_scale < 1.) {
auto result = QImage(kWideScale * _size * pixelRatio, QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(pixelRatio);
result.fill(Qt::transparent);
{
Painter p(&result);
p.drawPixmap((kWideScale - 1) / 2 * _size.width(), (kWideScale - 1) / 2 * _size.height(), widgetContent);
}
return PixmapFromImage(std::move(result));
}
return widgetContent;
}
void FadeAnimation::setFinishedCallback(FinishedCallback &&callback) {
_finishedCallback = std::move(callback);
}
void FadeAnimation::setUpdatedCallback(UpdatedCallback &&callback) {
_updatedCallback = std::move(callback);
}
void FadeAnimation::show() {
_visible = true;
stopAnimation();
}
void FadeAnimation::hide() {
_visible = false;
stopAnimation();
}
void FadeAnimation::stopAnimation() {
_animation.stop();
if (!_cache.isNull()) {
_cache = QPixmap();
if (_finishedCallback) {
_finishedCallback();
}
}
if (_visible == _widget->isHidden()) {
_widget->setVisible(_visible);
}
}
void FadeAnimation::fadeIn(int duration) {
if (_visible) return;
_visible = true;
startAnimation(duration);
}
void FadeAnimation::fadeOut(int duration) {
if (!_visible) return;
_visible = false;
startAnimation(duration);
}
void FadeAnimation::startAnimation(int duration) {
if (_cache.isNull()) {
_cache = grabContent();
Assert(!_cache.isNull());
}
auto from = _visible ? 0. : 1.;
auto to = _visible ? 1. : 0.;
_animation.start([this]() { updateCallback(); }, from, to, duration);
updateCallback();
if (_widget->isHidden()) {
_widget->show();
}
}
void FadeAnimation::updateCallback() {
_widget->update();
if (_updatedCallback) {
_updatedCallback(_animation.value(_visible ? 1. : 0.));
}
if (!_animation.animating()) {
stopAnimation();
}
}
} // namespace Ui

View File

@@ -0,0 +1,65 @@
// 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
//
#pragma once
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
#include "styles/style_widgets.h"
namespace Ui {
class FadeAnimation {
public:
FadeAnimation(RpWidget *widget, float64 scale = 1.);
bool paint(QPainter &p);
void refreshCache();
using FinishedCallback = Fn<void()>;
void setFinishedCallback(FinishedCallback &&callback);
using UpdatedCallback = Fn<void(float64)>;
void setUpdatedCallback(UpdatedCallback &&callback);
void show();
void hide();
void fadeIn(int duration);
void fadeOut(int duration);
void finish() {
stopAnimation();
}
bool animating() const {
return _animation.animating();
}
bool visible() const {
return _visible;
}
private:
void startAnimation(int duration);
void stopAnimation();
void updateCallback();
QPixmap grabContent();
RpWidget *_widget = nullptr;
float64 _scale = 1.;
Ui::Animations::Simple _animation;
QSize _size;
QPixmap _cache;
bool _visible = false;
FinishedCallback _finishedCallback;
UpdatedCallback _updatedCallback;
};
} // namespace Ui

View File

@@ -0,0 +1,79 @@
// 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/frame_generator.h"
#include "ui/image/image_prepare.h"
namespace Ui {
ImageFrameGenerator::ImageFrameGenerator(const QByteArray &bytes)
: _bytes(bytes) {
}
ImageFrameGenerator::ImageFrameGenerator(const QImage &image)
: _image(image) {
}
int ImageFrameGenerator::count() {
return 1;
}
double ImageFrameGenerator::rate() {
return 1.;
}
FrameGenerator::Frame ImageFrameGenerator::renderNext(
QImage storage,
QSize size,
Qt::AspectRatioMode mode) {
return renderCurrent(std::move(storage), size, mode);
}
FrameGenerator::Frame ImageFrameGenerator::renderCurrent(
QImage storage,
QSize size,
Qt::AspectRatioMode mode) {
if (_image.isNull() && !_bytes.isEmpty()) {
_image = Images::Read({
.content = _bytes,
}).image;
_bytes = QByteArray();
}
if (_image.isNull()) {
return {};
}
auto scaled = _image.scaled(
size,
mode,
Qt::SmoothTransformation
).convertToFormat(QImage::Format_ARGB32_Premultiplied);
if (scaled.size() == size) {
return { .image = std::move(scaled) };
}
auto result = QImage(size, QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
const auto skipx = (size.width() - scaled.width()) / 2;
const auto skipy = (size.height() - scaled.height()) / 2;
const auto srcPerLine = scaled.bytesPerLine();
const auto dstPerLine = result.bytesPerLine();
const auto lineBytes = scaled.width() * 4;
auto src = scaled.constBits();
auto dst = result.bits() + (skipx * 4) + (skipy * srcPerLine);
for (auto y = 0, height = scaled.height(); y != height; ++y) {
memcpy(dst, src, lineBytes);
src += srcPerLine;
dst += dstPerLine;
}
return { .image = std::move(result), .last = true };
}
void ImageFrameGenerator::jumpToStart() {
}
} // namespace Ui

View File

@@ -0,0 +1,69 @@
// 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
//
#pragma once
#include <QtGui/QImage>
#include <crl/crl_time.h>
namespace Ui {
class FrameGenerator {
public:
virtual ~FrameGenerator() = default;
// 0 means unknown.
[[nodiscard]] virtual int count() = 0;
// 0. means unknown.
[[nodiscard]] virtual double rate() = 0;
struct Frame {
crl::time duration = 0;
QImage image;
bool last = false;
};
[[nodiscard]] virtual Frame renderNext(
QImage storage,
QSize size,
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) = 0;
[[nodiscard]] virtual Frame renderCurrent(
QImage storage,
QSize size,
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) = 0;
virtual void jumpToStart() = 0;
};
class ImageFrameGenerator final : public Ui::FrameGenerator {
public:
explicit ImageFrameGenerator(const QByteArray &bytes);
explicit ImageFrameGenerator(const QImage &image);
int count() override;
double rate() override;
Frame renderNext(
QImage storage,
QSize size,
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override;
Frame renderCurrent(
QImage storage,
QSize size,
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override;
void jumpToStart() override;
private:
QByteArray _bytes;
QImage _image;
};
[[nodiscard]] bool GoodStorageForFrame(const QImage &storage, QSize size);
[[nodiscard]] QImage CreateFrameStorage(QSize size);
} // namespace Ui

View File

@@ -0,0 +1,30 @@
// 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/gradient.h"
namespace anim {
QColor gradient_color_at(const QGradientStops &stops, float64 ratio) {
for (auto i = 1; i < stops.size(); i++) {
const auto currentPoint = stops[i].first;
const auto previousPoint = stops[i - 1].first;
if ((ratio <= currentPoint) && (ratio >= previousPoint)) {
return anim::color(
stops[i - 1].second,
stops[i].second,
(ratio - previousPoint) / (currentPoint - previousPoint));
}
}
return QColor();
}
QColor gradient_color_at(const QGradient &gradient, float64 ratio) {
return gradient_color_at(gradient.stops(), ratio);
}
} // namespace anim

View File

@@ -0,0 +1,257 @@
// 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
//
#pragma once
#include "base/flat_map.h"
#include "ui/effects/animation_value.h"
#include <QtGui/QLinearGradient>
#include <QtGui/QRadialGradient>
namespace anim {
[[nodiscard]] QColor gradient_color_at(
const QGradientStops &stops,
float64 ratio);
[[nodiscard]] QColor gradient_color_at(
const QGradient &gradient,
float64 ratio);
struct gradient_colors {
explicit gradient_colors(QColor color) {
stops.push_back({ 0., color });
stops.push_back({ 1., color });
}
explicit gradient_colors(std::vector<QColor> colors) {
if (colors.size() == 1) {
gradient_colors(colors.front());
return;
}
const auto last = float(colors.size() - 1);
for (auto i = 0; i < colors.size(); i++) {
stops.push_back({ i / last, std::move(colors[i]) });
}
}
explicit gradient_colors(QGradientStops colors)
: stops(std::move(colors)) {
}
QGradientStops stops;
};
namespace details {
template <typename T, typename Derived>
class gradients {
public:
gradients() = default;
gradients(base::flat_map<T, std::vector<QColor>> colors) {
Expects(!colors.empty());
for (const auto &[key, value] : colors) {
auto c = gradient_colors(std::move(value));
_gradients.emplace(key, gradient_with_stops(std::move(c.stops)));
}
}
gradients(base::flat_map<T, gradient_colors> colors) {
Expects(!colors.empty());
for (const auto &[key, c] : colors) {
_gradients.emplace(key, gradient_with_stops(std::move(c.stops)));
}
}
QGradient gradient(T state1, T state2, float64 b_ratio) const {
Expects(!_gradients.empty());
if (b_ratio == 0.) {
return _gradients.find(state1)->second;
} else if (b_ratio == 1.) {
return _gradients.find(state2)->second;
}
auto gradient = empty_gradient();
const auto gradient1 = _gradients.find(state1);
const auto gradient2 = _gradients.find(state2);
Assert(gradient1 != end(_gradients));
Assert(gradient2 != end(_gradients));
const auto stopsFrom = gradient1->second.stops();
const auto stopsTo = gradient2->second.stops();
if ((stopsFrom.size() == stopsTo.size())
&& ranges::equal(
stopsFrom,
stopsTo,
ranges::equal_to(),
&QGradientStop::first,
&QGradientStop::first)) {
const auto size = stopsFrom.size();
const auto &p = b_ratio;
for (auto i = 0; i < size; i++) {
auto c = color(stopsFrom[i].second, stopsTo[i].second, p);
gradient.setColorAt(stopsTo[i].first, std::move(c));
}
return gradient;
}
const auto invert = (stopsFrom.size() > stopsTo.size());
if (invert) {
b_ratio = 1. - b_ratio;
}
const auto &stops1 = invert ? stopsTo : stopsFrom;
const auto &stops2 = invert ? stopsFrom : stopsTo;
const auto size1 = stops1.size();
const auto size2 = stops2.size();
for (auto i = 0; i < size1; i++) {
const auto point1 = stops1[i].first;
const auto previousPoint1 = i ? stops1[i - 1].first : -1.;
for (auto n = 0; n < size2; n++) {
const auto point2 = stops2[n].first;
if ((point2 <= previousPoint1) || (point2 > point1)) {
continue;
}
const auto color2 = stops2[n].second;
QColor result;
if (point2 < point1) {
const auto pointRatio2 = (point2 - previousPoint1)
/ (point1 - previousPoint1);
const auto color1 = color(
stops1[i - 1].second,
stops1[i].second,
pointRatio2);
result = color(color1, color2, b_ratio);
} else {
// point2 == point1
result = color(stops1[i].second, color2, b_ratio);
}
gradient.setColorAt(point2, std::move(result));
}
}
return gradient;
}
protected:
void cache_gradients() {
auto copy = std::move(_gradients);
for (const auto &[key, value] : copy) {
_gradients.emplace(key, gradient_with_stops(value.stops()));
}
}
private:
QGradient empty_gradient() const {
return static_cast<const Derived*>(this)->empty_gradient();
}
QGradient gradient_with_stops(QGradientStops stops) const {
auto gradient = empty_gradient();
gradient.setStops(std::move(stops));
return gradient;
}
base::flat_map<T, QGradient> _gradients;
};
} // namespace details
template <typename T>
class linear_gradients final
: public details::gradients<T, linear_gradients<T>> {
using parent = details::gradients<T, linear_gradients<T>>;
public:
linear_gradients() = default;
linear_gradients(
base::flat_map<T, std::vector<QColor>> colors,
QPointF point1,
QPointF point2)
: parent(std::move(colors)) {
set_points(point1, point2);
}
linear_gradients(
base::flat_map<T, gradient_colors> colors,
QPointF point1,
QPointF point2)
: parent(std::move(colors)) {
set_points(point1, point2);
}
void set_points(QPointF point1, QPointF point2) {
if (_point1 == point1 && _point2 == point2) {
return;
}
_point1 = point1;
_point2 = point2;
parent::cache_gradients();
}
private:
friend class details::gradients<T, linear_gradients<T>>;
QGradient empty_gradient() const {
return QLinearGradient(_point1, _point2);
}
QPointF _point1;
QPointF _point2;
};
template <typename T>
class radial_gradients final
: public details::gradients<T, radial_gradients<T>> {
using parent = details::gradients<T, radial_gradients<T>>;
public:
radial_gradients() = default;
radial_gradients(
base::flat_map<T, std::vector<QColor>> colors,
QPointF center,
float radius)
: parent(std::move(colors)) {
set_points(center, radius);
}
radial_gradients(
base::flat_map<T, gradient_colors> colors,
QPointF center,
float radius)
: parent(std::move(colors)) {
set_points(center, radius);
}
void set_points(QPointF center, float radius) {
if (_center == center && _radius == radius) {
return;
}
_center = center;
_radius = radius;
parent::cache_gradients();
}
private:
friend class details::gradients<T, radial_gradients<T>>;
QGradient empty_gradient() const {
return QRadialGradient(_center, _radius);
}
QPointF _center;
float _radius = 0.;
};
} // namespace anim

View File

@@ -0,0 +1,272 @@
// 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/numbers_animation.h"
#include "ui/painter.h"
#include "styles/style_widgets.h"
#include <QtGui/QPainter>
namespace Ui {
NumbersAnimation::NumbersAnimation(
const style::font &font,
Fn<void()> animationCallback)
: _font(font)
, _duration(st::slideWrapDuration)
, _animationCallback(std::move(animationCallback)) {
for (auto ch = '0'; ch != '9'; ++ch) {
accumulate_max(_digitWidth, _font->width(ch));
}
}
void NumbersAnimation::setDuration(int duration) {
_duration = duration;
}
void NumbersAnimation::setDisabledMonospace(bool value) {
_disabledMonospace = value;
}
void NumbersAnimation::setText(const QString &text, int value) {
if (_a_ready.animating()) {
_delayedText = text;
_delayedValue = value;
} else {
realSetText(text, value);
}
}
void NumbersAnimation::animationCallback() {
if (_animationCallback) {
_animationCallback();
}
if (_widthChangedCallback) {
_widthChangedCallback();
}
if (!_a_ready.animating() && !_delayedText.isEmpty()) {
setText(_delayedText, _delayedValue);
}
}
void NumbersAnimation::realSetText(QString text, int value) {
_delayedText = QString();
_delayedValue = 0;
_growing = (value > _value);
_value = value;
auto newSize = text.size();
while (_digits.size() < newSize) {
_digits.push_front(Digit());
}
while (_digits.size() > newSize && !_digits.front().to.unicode()) {
_digits.pop_front();
}
auto animating = false;
auto toFullWidth = 0;
auto bothFullWidth = 0;
for (auto i = 0, size = int(_digits.size()); i != size; ++i) {
auto &digit = _digits[i];
const auto from = digit.from = digit.to;
digit.fromWidth = digit.toWidth;
const auto to = digit.to = (newSize + i < size)
? QChar(0)
: text[newSize + i - size];
digit.toWidth = to.unicode() ? _font->width(to) : 0;
if (from != to) {
animating = true;
}
const auto toCharWidth = (!_disabledMonospace || to.isDigit())
? _digitWidth
: digit.toWidth;
const auto fromCharWidth = (!_disabledMonospace || from.isDigit())
? _digitWidth
: digit.fromWidth;
const auto charWidth = std::max(toCharWidth, fromCharWidth);
bothFullWidth += charWidth;
if (to.unicode()) {
toFullWidth += charWidth;
}
}
_fromWidth = _toWidth;
_toWidth = toFullWidth;
_bothWidth = bothFullWidth;
if (animating) {
_a_ready.start(
[this] { animationCallback(); },
0.,
1.,
_duration);
}
}
int NumbersAnimation::countWidth() const {
return anim::interpolate(
_fromWidth,
_toWidth,
anim::easeOutCirc(1., _a_ready.value(1.)));
}
int NumbersAnimation::maxWidth() const {
return std::max(_fromWidth, _toWidth);
}
void NumbersAnimation::finishAnimating() {
auto width = countWidth();
_a_ready.stop();
if (_widthChangedCallback && countWidth() != width) {
_widthChangedCallback();
}
if (!_delayedText.isEmpty()) {
setText(_delayedText, _delayedValue);
}
}
void NumbersAnimation::paint(QPainter &p, int x, int y, int outerWidth) {
auto digitsCount = _digits.size();
if (!digitsCount) return;
auto progress = anim::easeOutCirc(1., _a_ready.value(1.));
auto width = anim::interpolate(_fromWidth, _toWidth, progress);
const auto initial = p.opacity();
QString singleChar('0');
if (style::RightToLeft()) x = outerWidth - x - width;
x += width - _bothWidth;
auto fromTop = anim::interpolate(0, _font->height, progress) * (_growing ? 1 : -1);
auto toTop = anim::interpolate(_font->height, 0, progress) * (_growing ? -1 : 1);
for (auto i = 0; i != digitsCount; ++i) {
auto &digit = _digits[i];
auto from = digit.from;
auto to = digit.to;
const auto toCharWidth = (!_disabledMonospace || to.isDigit())
? _digitWidth
: digit.toWidth;
const auto fromCharWidth = (!_disabledMonospace || from.isDigit())
? _digitWidth
: digit.fromWidth;
if (from == to) {
p.setOpacity(initial);
singleChar[0] = from;
p.drawText(x + (toCharWidth - digit.fromWidth) / 2, y + _font->ascent, singleChar);
} else {
if (from.unicode()) {
p.setOpacity(initial * (1. - progress));
singleChar[0] = from;
p.drawText(x + (fromCharWidth - digit.fromWidth) / 2, y + fromTop + _font->ascent, singleChar);
}
if (to.unicode()) {
p.setOpacity(initial * progress);
singleChar[0] = to;
p.drawText(x + (toCharWidth - digit.toWidth) / 2, y + toTop + _font->ascent, singleChar);
}
}
x += std::max(toCharWidth, fromCharWidth);
}
p.setOpacity(initial);
}
LabelWithNumbers::LabelWithNumbers(
QWidget *parent,
const style::FlatLabel &st,
int textTop,
const StringWithNumbers &value)
: RpWidget(parent)
, _st(st)
, _textTop(textTop)
, _before(GetBefore(value))
, _after(GetAfter(value))
, _numbers(_st.style.font, [=] { update(); })
, _beforeWidth(_st.style.font->width(_before))
, _afterWidth(st.style.font->width(_after)) {
Expects((value.offset < 0) == (value.length == 0));
_numbers.setWidthChangedCallback([=] {
updateNaturalWidth();
});
const auto numbers = GetNumbers(value);
_numbers.setText(numbers, numbers.toInt());
_numbers.finishAnimating();
}
QString LabelWithNumbers::GetBefore(const StringWithNumbers &value) {
return value.text.mid(0, value.offset);
}
QString LabelWithNumbers::GetAfter(const StringWithNumbers &value) {
return (value.offset >= 0)
? value.text.mid(value.offset + value.length)
: QString();
}
QString LabelWithNumbers::GetNumbers(const StringWithNumbers &value) {
return (value.offset >= 0)
? value.text.mid(value.offset, value.length)
: QString();
}
void LabelWithNumbers::setValue(const StringWithNumbers &value) {
_before = GetBefore(value);
_after = GetAfter(value);
const auto numbers = GetNumbers(value);
_numbers.setText(numbers, numbers.toInt());
const auto oldBeforeWidth = std::exchange(
_beforeWidth,
_st.style.font->width(_before));
_beforeWidthAnimation.start(
[this] { update(); },
oldBeforeWidth,
_beforeWidth,
st::slideWrapDuration,
anim::easeOutCirc);
_afterWidth = _st.style.font->width(_after);
updateNaturalWidth();
}
void LabelWithNumbers::updateNaturalWidth() {
setNaturalWidth(_beforeWidth + _numbers.maxWidth() + _afterWidth);
}
void LabelWithNumbers::finishAnimating() {
_beforeWidthAnimation.stop();
_numbers.finishAnimating();
update();
}
void LabelWithNumbers::paintEvent(QPaintEvent *e) {
Painter p(this);
const auto beforeWidth = _beforeWidthAnimation.value(_beforeWidth);
p.setFont(_st.style.font);
p.setBrush(Qt::NoBrush);
p.setPen(_st.textFg);
auto left = 0;
const auto outerWidth = width();
p.setClipRect(0, 0, left + beforeWidth, height());
p.drawTextLeft(left, _textTop, outerWidth, _before, _beforeWidth);
left += beforeWidth;
p.setClipping(false);
_numbers.paint(p, left, _textTop, outerWidth);
left += _numbers.countWidth();
const auto availableWidth = outerWidth - left;
const auto text = (availableWidth < _afterWidth)
? _st.style.font->elided(_after, availableWidth)
: _after;
const auto textWidth = (availableWidth < _afterWidth) ? -1 : _afterWidth;
p.drawTextLeft(left, _textTop, outerWidth, text, textWidth);
}
} // namespace Ui

View File

@@ -0,0 +1,114 @@
// 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
//
#pragma once
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
namespace style {
struct FlatLabel;
} // namespace style
namespace Ui {
class NumbersAnimation {
public:
NumbersAnimation(
const style::font &font,
Fn<void()> animationCallback);
void setWidthChangedCallback(Fn<void()> callback) {
_widthChangedCallback = std::move(callback);
}
void setText(const QString &text, int value);
void setDuration(int duration);
void setDisabledMonospace(bool value);
void finishAnimating();
void paint(QPainter &p, int x, int y, int outerWidth);
int countWidth() const;
int maxWidth() const;
private:
struct Digit {
QChar from = QChar(0);
QChar to = QChar(0);
int fromWidth = 0;
int toWidth = 0;
};
void animationCallback();
void realSetText(QString text, int value);
const style::font &_font;
int _duration;
QList<Digit> _digits;
int _digitWidth = 0;
int _fromWidth = 0;
int _toWidth = 0;
int _bothWidth = 0;
Ui::Animations::Simple _a_ready;
QString _delayedText;
int _delayedValue = 0;
int _value = 0;
bool _growing = false;
bool _disabledMonospace = false;
Fn<void()> _animationCallback;
Fn<void()> _widthChangedCallback;
};
struct StringWithNumbers {
static StringWithNumbers FromString(const QString &text) {
return { text };
}
QString text;
int offset = -1;
int length = 0;
};
class LabelWithNumbers : public Ui::RpWidget {
public:
LabelWithNumbers(
QWidget *parent,
const style::FlatLabel &st,
int textTop,
const StringWithNumbers &value);
void setValue(const StringWithNumbers &value);
void finishAnimating();
protected:
void paintEvent(QPaintEvent *e) override;
private:
[[nodiscard]] static QString GetBefore(const StringWithNumbers &value);
[[nodiscard]] static QString GetAfter(const StringWithNumbers &value);
[[nodiscard]] static QString GetNumbers(const StringWithNumbers &value);
void updateNaturalWidth();
const style::FlatLabel &_st;
int _textTop;
QString _before;
QString _after;
NumbersAnimation _numbers;
int _beforeWidth = 0;
int _afterWidth = 0;
Ui::Animations::Simple _beforeWidthAnimation;
};
} // namespace Ui

View File

@@ -0,0 +1,539 @@
// 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/panel_animation.h"
#include "ui/effects/animation_value.h"
#include "ui/ui_utility.h"
#include <QtGui/QPainter>
namespace Ui {
void RoundShadowAnimation::start(int frameWidth, int frameHeight, float64 devicePixelRatio) {
Expects(!started());
_frameWidth = frameWidth;
_frameHeight = frameHeight;
_frame = QImage(_frameWidth, _frameHeight, QImage::Format_ARGB32_Premultiplied);
_frame.setDevicePixelRatio(devicePixelRatio);
_frameIntsPerLine = (_frame.bytesPerLine() >> 2);
_frameInts = reinterpret_cast<uint32*>(_frame.bits());
_frameIntsPerLineAdded = _frameIntsPerLine - _frameWidth;
Assert(_frame.depth() == static_cast<int>(sizeof(uint32) << 3));
Assert(_frame.bytesPerLine() == (_frameIntsPerLine << 2));
Assert(_frameIntsPerLineAdded >= 0);
}
void RoundShadowAnimation::setShadow(const style::Shadow &st) {
_shadow.extend = st.extend * style::DevicePixelRatio();
_shadow.left = cloneImage(st.left);
if (_shadow.valid()) {
_shadow.topLeft = cloneImage(st.topLeft);
_shadow.top = cloneImage(st.top);
_shadow.topRight = cloneImage(st.topRight);
_shadow.right = cloneImage(st.right);
_shadow.bottomRight = cloneImage(st.bottomRight);
_shadow.bottom = cloneImage(st.bottom);
_shadow.bottomLeft = cloneImage(st.bottomLeft);
Assert(!_shadow.topLeft.isNull()
&& !_shadow.top.isNull()
&& !_shadow.topRight.isNull()
&& !_shadow.right.isNull()
&& !_shadow.bottomRight.isNull()
&& !_shadow.bottom.isNull()
&& !_shadow.bottomLeft.isNull());
} else {
_shadow.topLeft =
_shadow.top =
_shadow.topRight =
_shadow.right =
_shadow.bottomRight =
_shadow.bottom =
_shadow.bottomLeft = QImage();
}
}
void RoundShadowAnimation::setCornerMasks(
const std::array<QImage, 4> &corners) {
setCornerMask(_topLeft, corners[0]);
setCornerMask(_topRight, corners[1]);
setCornerMask(_bottomLeft, corners[2]);
setCornerMask(_bottomRight, corners[3]);
}
void RoundShadowAnimation::setCornerMask(Corner &corner, const QImage &image) {
Expects(!started());
corner.image = image;
if (corner.valid()) {
corner.width = corner.image.width();
corner.height = corner.image.height();
corner.bytes = corner.image.constBits();
corner.bytesPerPixel = (corner.image.depth() >> 3);
corner.bytesPerLineAdded = corner.image.bytesPerLine() - corner.width * corner.bytesPerPixel;
Assert(corner.image.depth() == (corner.bytesPerPixel << 3));
Assert(corner.bytesPerLineAdded >= 0);
} else {
corner.width = corner.height = 0;
}
}
QImage RoundShadowAnimation::cloneImage(const style::icon &source) {
if (source.empty()) return QImage();
auto result = QImage(
source.size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(style::DevicePixelRatio());
result.fill(Qt::transparent);
{
QPainter p(&result);
source.paint(p, 0, 0, source.width());
}
return result;
}
void RoundShadowAnimation::paintCorner(Corner &corner, int left, int top) {
auto mask = corner.bytes;
auto bytesPerPixel = corner.bytesPerPixel;
auto bytesPerLineAdded = corner.bytesPerLineAdded;
auto frameInts = _frameInts + top * _frameIntsPerLine + left;
auto frameIntsPerLineAdd = _frameIntsPerLine - corner.width;
for (auto y = 0; y != corner.height; ++y) {
for (auto x = 0; x != corner.width; ++x) {
auto alpha = static_cast<uint32>(*mask) + 1;
*frameInts = anim::unshifted(anim::shifted(*frameInts) * alpha);
++frameInts;
mask += bytesPerPixel;
}
frameInts += frameIntsPerLineAdd;
mask += bytesPerLineAdded;
}
}
void RoundShadowAnimation::paintShadow(int left, int top, int right, int bottom) {
paintShadowCorner(left, top, _shadow.topLeft);
paintShadowCorner(right - _shadow.topRight.width(), top, _shadow.topRight);
paintShadowCorner(right - _shadow.bottomRight.width(), bottom - _shadow.bottomRight.height(), _shadow.bottomRight);
paintShadowCorner(left, bottom - _shadow.bottomLeft.height(), _shadow.bottomLeft);
paintShadowVertical(left, top + _shadow.topLeft.height(), bottom - _shadow.bottomLeft.height(), _shadow.left);
paintShadowVertical(right - _shadow.right.width(), top + _shadow.topRight.height(), bottom - _shadow.bottomRight.height(), _shadow.right);
paintShadowHorizontal(left + _shadow.topLeft.width(), right - _shadow.topRight.width(), top, _shadow.top);
paintShadowHorizontal(left + _shadow.bottomLeft.width(), right - _shadow.bottomRight.width(), bottom - _shadow.bottom.height(), _shadow.bottom);
}
void RoundShadowAnimation::paintShadowCorner(int left, int top, const QImage &image) {
auto imageWidth = image.width();
auto imageHeight = image.height();
auto imageInts = reinterpret_cast<const uint32*>(image.constBits());
auto imageIntsPerLine = (image.bytesPerLine() >> 2);
auto imageIntsPerLineAdded = imageIntsPerLine - imageWidth;
if (left < 0) {
auto shift = -base::take(left);
imageWidth -= shift;
imageInts += shift;
}
if (top < 0) {
auto shift = -base::take(top);
imageHeight -= shift;
imageInts += shift * imageIntsPerLine;
}
if (left + imageWidth > _frameWidth) {
imageWidth = _frameWidth - left;
}
if (top + imageHeight > _frameHeight) {
imageHeight = _frameHeight - top;
}
if (imageWidth < 0 || imageHeight < 0) return;
auto frameInts = _frameInts + top * _frameIntsPerLine + left;
auto frameIntsPerLineAdd = _frameIntsPerLine - imageWidth;
for (auto y = 0; y != imageHeight; ++y) {
for (auto x = 0; x != imageWidth; ++x) {
auto source = *frameInts;
auto shadowAlpha = qMax(_frameAlpha - int(source >> 24), 0);
*frameInts = anim::unshifted(anim::shifted(source) * 256 + anim::shifted(*imageInts) * shadowAlpha);
++frameInts;
++imageInts;
}
frameInts += frameIntsPerLineAdd;
imageInts += imageIntsPerLineAdded;
}
}
void RoundShadowAnimation::paintShadowVertical(int left, int top, int bottom, const QImage &image) {
auto imageWidth = image.width();
auto imageInts = reinterpret_cast<const uint32*>(image.constBits());
if (left < 0) {
auto shift = -base::take(left);
imageWidth -= shift;
imageInts += shift;
}
if (top < 0) top = 0;
accumulate_min(bottom, _frameHeight);
accumulate_min(imageWidth, _frameWidth - left);
if (imageWidth < 0 || bottom <= top) return;
auto frameInts = _frameInts + top * _frameIntsPerLine + left;
auto frameIntsPerLineAdd = _frameIntsPerLine - imageWidth;
for (auto y = top; y != bottom; ++y) {
for (auto x = 0; x != imageWidth; ++x) {
auto source = *frameInts;
auto shadowAlpha = _frameAlpha - (source >> 24);
*frameInts = anim::unshifted(anim::shifted(source) * 256 + anim::shifted(*imageInts) * shadowAlpha);
++frameInts;
++imageInts;
}
frameInts += frameIntsPerLineAdd;
imageInts -= imageWidth;
}
}
void RoundShadowAnimation::paintShadowHorizontal(int left, int right, int top, const QImage &image) {
auto imageHeight = image.height();
auto imageInts = reinterpret_cast<const uint32*>(image.constBits());
auto imageIntsPerLine = (image.bytesPerLine() >> 2);
if (top < 0) {
auto shift = -base::take(top);
imageHeight -= shift;
imageInts += shift * imageIntsPerLine;
}
if (left < 0) left = 0;
accumulate_min(right, _frameWidth);
accumulate_min(imageHeight, _frameHeight - top);
if (imageHeight < 0 || right <= left) return;
auto frameInts = _frameInts + top * _frameIntsPerLine + left;
auto frameIntsPerLineAdd = _frameIntsPerLine - (right - left);
for (auto y = 0; y != imageHeight; ++y) {
auto imagePattern = anim::shifted(*imageInts);
for (auto x = left; x != right; ++x) {
auto source = *frameInts;
auto shadowAlpha = _frameAlpha - (source >> 24);
*frameInts = anim::unshifted(anim::shifted(source) * 256 + imagePattern * shadowAlpha);
++frameInts;
}
frameInts += frameIntsPerLineAdd;
imageInts += imageIntsPerLine;
}
}
void PanelAnimation::setFinalImage(QImage &&finalImage, QRect inner) {
Expects(!started());
const auto pixelRatio = style::DevicePixelRatio();
_finalImage = PixmapFromImage(
std::move(finalImage).convertToFormat(
QImage::Format_ARGB32_Premultiplied));
Assert(!_finalImage.isNull());
_finalWidth = _finalImage.width();
_finalHeight = _finalImage.height();
Assert(!(_finalWidth % pixelRatio));
Assert(!(_finalHeight % pixelRatio));
_finalInnerLeft = inner.x();
_finalInnerTop = inner.y();
_finalInnerWidth = inner.width();
_finalInnerHeight = inner.height();
Assert(!(_finalInnerLeft % pixelRatio));
Assert(!(_finalInnerTop % pixelRatio));
Assert(!(_finalInnerWidth % pixelRatio));
Assert(!(_finalInnerHeight % pixelRatio));
_finalInnerRight = _finalInnerLeft + _finalInnerWidth;
_finalInnerBottom = _finalInnerTop + _finalInnerHeight;
Assert(QRect(0, 0, _finalWidth, _finalHeight).contains(inner));
setStartWidth();
setStartHeight();
setStartAlpha();
setStartFadeTop();
createFadeMask();
setWidthDuration();
setHeightDuration();
setAlphaDuration();
if (!_skipShadow) {
setShadow(_st.shadow);
}
auto checkCorner = [this, inner](Corner &corner) {
if (!corner.valid()) return;
if ((_startWidth >= 0 && _startWidth < _finalWidth)
|| (_startHeight >= 0 && _startHeight < _finalHeight)) {
Assert(corner.width <= inner.width());
Assert(corner.height <= inner.height());
}
};
checkCorner(_topLeft);
checkCorner(_topRight);
checkCorner(_bottomLeft);
checkCorner(_bottomRight);
}
void PanelAnimation::setStartWidth() {
_startWidth = qRound(_st.startWidth * _finalInnerWidth);
if (_startWidth >= 0) Assert(_startWidth <= _finalInnerWidth);
}
void PanelAnimation::setStartHeight() {
_startHeight = qRound(_st.startHeight * _finalInnerHeight);
if (_startHeight >= 0) Assert(_startHeight <= _finalInnerHeight);
}
void PanelAnimation::setStartAlpha() {
_startAlpha = qRound(_st.startOpacity * 255);
Assert(_startAlpha >= 0 && _startAlpha < 256);
}
void PanelAnimation::setStartFadeTop() {
_startFadeTop = qRound(_st.startFadeTop * _finalInnerHeight);
}
void PanelAnimation::createFadeMask() {
auto resultHeight = qRound(_finalImage.height() * _st.fadeHeight);
if (auto remove = (resultHeight % style::DevicePixelRatio())) {
resultHeight -= remove;
}
auto finalAlpha = qRound(_st.fadeOpacity * 255);
Assert(finalAlpha >= 0 && finalAlpha < 256);
auto result = QImage(style::DevicePixelRatio(), resultHeight, QImage::Format_ARGB32_Premultiplied);
auto ints = reinterpret_cast<uint32*>(result.bits());
auto intsPerLineAdded = (result.bytesPerLine() >> 2) - style::DevicePixelRatio();
auto up = (_origin == PanelAnimation::Origin::BottomLeft || _origin == PanelAnimation::Origin::BottomRight);
auto from = up ? resultHeight : 0, to = resultHeight - from, delta = up ? -1 : 1;
auto fadeFirstAlpha = up ? (finalAlpha + 1) : 1;
auto fadeLastAlpha = up ? 1 : (finalAlpha + 1);
_fadeFirst = QBrush(QColor(_st.fadeBg->c.red(), _st.fadeBg->c.green(), _st.fadeBg->c.blue(), (_st.fadeBg->c.alpha() * fadeFirstAlpha) >> 8));
_fadeLast = QBrush(QColor(_st.fadeBg->c.red(), _st.fadeBg->c.green(), _st.fadeBg->c.blue(), (_st.fadeBg->c.alpha() * fadeLastAlpha) >> 8));
for (auto y = from; y != to; y += delta) {
auto alpha = static_cast<uint32>(finalAlpha * y) / resultHeight;
auto value = (0xFFU << 24) | (alpha << 16) | (alpha << 8) | alpha;
for (auto x = 0; x != style::DevicePixelRatio(); ++x) {
*ints++ = value;
}
ints += intsPerLineAdded;
}
_fadeMask = PixmapFromImage(style::colorizeImage(result, _st.fadeBg));
_fadeHeight = _fadeMask.height();
}
void PanelAnimation::setSkipShadow(bool skipShadow) {
Assert(!started());
_skipShadow = skipShadow;
}
void PanelAnimation::setWidthDuration() {
_widthDuration = _st.widthDuration;
Assert(_widthDuration >= 0.);
Assert(_widthDuration <= 1.);
}
void PanelAnimation::setHeightDuration() {
Assert(!started());
_heightDuration = _st.heightDuration;
Assert(_heightDuration >= 0.);
Assert(_heightDuration <= 1.);
}
void PanelAnimation::setAlphaDuration() {
Assert(!started());
_alphaDuration = _st.opacityDuration;
Assert(_alphaDuration >= 0.);
Assert(_alphaDuration <= 1.);
}
void PanelAnimation::start() {
Assert(!_finalImage.isNull());
RoundShadowAnimation::start(_finalWidth, _finalHeight, _finalImage.devicePixelRatio());
auto checkCorner = [this](const Corner &corner) {
if (!corner.valid()) return;
if (_startWidth >= 0) Assert(corner.width <= _startWidth);
if (_startHeight >= 0) Assert(corner.height <= _startHeight);
Assert(corner.width <= _finalInnerWidth);
Assert(corner.height <= _finalInnerHeight);
};
checkCorner(_topLeft);
checkCorner(_topRight);
checkCorner(_bottomLeft);
checkCorner(_bottomRight);
}
auto PanelAnimation::computeState(float64 dt, float64 opacity) const
-> PaintState {
auto &transition = anim::easeOutCirc;
if (dt < _alphaDuration) {
opacity *= transition(1., dt / _alphaDuration);
}
const auto widthProgress = (_startWidth < 0 || dt >= _widthDuration)
? 1.
: transition(1., dt / _widthDuration);
const auto heightProgress = (_startHeight < 0 || dt >= _heightDuration)
? 1.
: transition(1., dt / _heightDuration);
const auto frameWidth = (widthProgress < 1.)
? anim::interpolate(_startWidth, _finalInnerWidth, widthProgress)
: _finalInnerWidth;
const auto frameHeight = (heightProgress < 1.)
? anim::interpolate(_startHeight, _finalInnerHeight, heightProgress)
: _finalInnerHeight;
const auto ratio = style::DevicePixelRatio();
return {
.opacity = opacity,
.widthProgress = widthProgress,
.heightProgress = heightProgress,
.fade = transition(1., dt),
.width = frameWidth / ratio,
.height = frameHeight / ratio,
};
}
auto PanelAnimation::paintFrame(
QPainter &p,
int x,
int y,
int outerWidth,
float64 dt,
float64 opacity)
-> PaintState {
Assert(started());
Assert(dt >= 0.);
const auto pixelRatio = style::DevicePixelRatio();
const auto state = computeState(dt, opacity);
opacity = state.opacity;
_frameAlpha = anim::interpolate(1, 256, opacity);
const auto frameWidth = state.width * pixelRatio;
const auto frameHeight = state.height * pixelRatio;
auto frameLeft = (_origin == Origin::TopLeft || _origin == Origin::BottomLeft) ? _finalInnerLeft : (_finalInnerRight - frameWidth);
auto frameTop = (_origin == Origin::TopLeft || _origin == Origin::TopRight) ? _finalInnerTop : (_finalInnerBottom - frameHeight);
auto frameRight = frameLeft + frameWidth;
auto frameBottom = frameTop + frameHeight;
auto fadeTop = (_fadeHeight > 0) ? std::clamp(anim::interpolate(_startFadeTop, _finalInnerHeight, state.fade), 0, frameHeight) : frameHeight;
if (auto decrease = (fadeTop % pixelRatio)) {
fadeTop -= decrease;
}
auto fadeBottom = (fadeTop < frameHeight) ? std::min(fadeTop + _fadeHeight, frameHeight) : frameHeight;
auto fadeSkipLines = 0;
if (_origin == Origin::BottomLeft || _origin == Origin::BottomRight) {
fadeTop = frameHeight - fadeTop;
fadeBottom = frameHeight - fadeBottom;
qSwap(fadeTop, fadeBottom);
fadeSkipLines = fadeTop + _fadeHeight - fadeBottom;
}
fadeTop += frameTop;
fadeBottom += frameTop;
if (opacity < 1.) {
_frame.fill(Qt::transparent);
}
{
QPainter p(&_frame);
p.setOpacity(opacity);
auto painterFrameLeft = frameLeft / pixelRatio;
auto painterFrameTop = frameTop / pixelRatio;
auto painterFadeBottom = fadeBottom / pixelRatio;
p.drawPixmap(painterFrameLeft, painterFrameTop, _finalImage, frameLeft, frameTop, frameWidth, frameHeight);
if (_fadeHeight) {
if (frameTop != fadeTop) {
p.fillRect(painterFrameLeft, painterFrameTop, frameWidth, fadeTop - frameTop, _fadeFirst);
}
if (fadeTop != fadeBottom) {
auto painterFadeTop = fadeTop / pixelRatio;
auto painterFrameWidth = frameWidth / pixelRatio;
p.drawPixmap(painterFrameLeft, painterFadeTop, painterFrameWidth, painterFadeBottom - painterFadeTop, _fadeMask, 0, fadeSkipLines, pixelRatio, fadeBottom - fadeTop);
}
if (fadeBottom != frameBottom) {
p.fillRect(painterFrameLeft, painterFadeBottom, frameWidth, frameBottom - fadeBottom, _fadeLast);
}
}
}
// Draw corners
paintCorner(_topLeft, frameLeft, frameTop);
paintCorner(_topRight, frameRight - _topRight.width, frameTop);
paintCorner(_bottomLeft, frameLeft, frameBottom - _bottomLeft.height);
paintCorner(_bottomRight, frameRight - _bottomRight.width, frameBottom - _bottomRight.height);
// Draw shadow upon the transparent
auto outerLeft = frameLeft;
auto outerTop = frameTop;
auto outerRight = frameRight;
auto outerBottom = frameBottom;
if (_shadow.valid()) {
outerLeft -= _shadow.extend.left();
outerTop -= _shadow.extend.top();
outerRight += _shadow.extend.right();
outerBottom += _shadow.extend.bottom();
}
if (pixelRatio > 1) {
if (auto skipLeft = (outerLeft % pixelRatio)) {
outerLeft -= skipLeft;
}
if (auto skipTop = (outerTop % pixelRatio)) {
outerTop -= skipTop;
}
if (auto skipRight = (outerRight % pixelRatio)) {
outerRight += (pixelRatio - skipRight);
}
if (auto skipBottom = (outerBottom % pixelRatio)) {
outerBottom += (pixelRatio - skipBottom);
}
}
if (opacity == 1.) {
// Fill above the frame top with transparent.
auto fillTopInts = (_frameInts + outerTop * _frameIntsPerLine + outerLeft);
auto fillWidth = (outerRight - outerLeft) * sizeof(uint32);
for (auto fillTop = frameTop - outerTop; fillTop != 0; --fillTop) {
memset(fillTopInts, 0, fillWidth);
fillTopInts += _frameIntsPerLine;
}
// Fill to the left and to the right of the frame with transparent.
auto fillLeft = (frameLeft - outerLeft) * sizeof(uint32);
auto fillRight = (outerRight - frameRight) * sizeof(uint32);
if (fillLeft || fillRight) {
auto fillInts = _frameInts + frameTop * _frameIntsPerLine;
for (auto y = frameTop; y != frameBottom; ++y) {
memset(fillInts + outerLeft, 0, fillLeft);
memset(fillInts + frameRight, 0, fillRight);
fillInts += _frameIntsPerLine;
}
}
// Fill below the frame bottom with transparent.
auto fillBottomInts = (_frameInts + frameBottom * _frameIntsPerLine + outerLeft);
for (auto fillBottom = outerBottom - frameBottom; fillBottom != 0; --fillBottom) {
memset(fillBottomInts, 0, fillWidth);
fillBottomInts += _frameIntsPerLine;
}
}
if (_shadow.valid()) {
paintShadow(outerLeft, outerTop, outerRight, outerBottom);
}
// Debug
//frameInts = _frameInts;
//auto pattern = anim::shifted((static_cast<uint32>(0xFF) << 24) | (static_cast<uint32>(0xFF) << 16) | (static_cast<uint32>(0xFF) << 8) | static_cast<uint32>(0xFF));
//for (auto y = 0; y != _finalHeight; ++y) {
// for (auto x = 0; x != _finalWidth; ++x) {
// auto source = *frameInts;
// auto sourceAlpha = (source >> 24);
// *frameInts = anim::unshifted(anim::shifted(source) * 256 + pattern * (256 - sourceAlpha));
// ++frameInts;
// }
// frameInts += _frameIntsPerLineAdded;
//}
p.drawImage(style::rtlpoint(x + (outerLeft / pixelRatio), y + (outerTop / pixelRatio), outerWidth), _frame, QRect(outerLeft, outerTop, outerRight - outerLeft, outerBottom - outerTop));
return state;
}
} // namespace Ui

View File

@@ -0,0 +1,143 @@
// 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
//
#pragma once
#include "styles/style_widgets.h"
namespace Ui {
class RoundShadowAnimation {
public:
void setCornerMasks(const std::array<QImage, 4> &corners);
protected:
void start(int frameWidth, int frameHeight, float64 devicePixelRatio);
void setShadow(const style::Shadow &st);
bool started() const {
return !_frame.isNull();
}
struct Corner {
QImage image;
int width = 0;
int height = 0;
const uchar *bytes = nullptr;
int bytesPerPixel = 0;
int bytesPerLineAdded = 0;
bool valid() const {
return !image.isNull();
}
};
void setCornerMask(Corner &corner, const QImage &image);
void paintCorner(Corner &corner, int left, int top);
struct Shadow {
style::margins extend;
QImage left, topLeft, top, topRight, right, bottomRight, bottom, bottomLeft;
bool valid() const {
return !left.isNull();
}
};
QImage cloneImage(const style::icon &source);
void paintShadow(int left, int top, int right, int bottom);
void paintShadowCorner(int left, int top, const QImage &image);
void paintShadowVertical(int left, int top, int bottom, const QImage &image);
void paintShadowHorizontal(int left, int right, int top, const QImage &image);
Shadow _shadow;
Corner _topLeft;
Corner _topRight;
Corner _bottomLeft;
Corner _bottomRight;
QImage _frame;
uint32 *_frameInts = nullptr;
int _frameWidth = 0;
int _frameHeight = 0;
int _frameAlpha = 0; // recounted each getFrame()
int _frameIntsPerLine = 0;
int _frameIntsPerLineAdded = 0;
};
class PanelAnimation : public RoundShadowAnimation {
public:
enum class Origin {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
};
PanelAnimation(const style::PanelAnimation &st, Origin origin) : _st(st), _origin(origin) {
}
struct PaintState {
float64 opacity = 0.;
float64 widthProgress = 0.;
float64 heightProgress = 0.;
float64 fade = 0.;
int width = 0;
int height = 0;
};
void setFinalImage(QImage &&finalImage, QRect inner);
void setSkipShadow(bool skipShadow);
void start();
[[nodiscard]] PaintState computeState(float64 dt, float64 opacity) const;
PaintState paintFrame(
QPainter &p,
int x,
int y,
int outerWidth,
float64 dt,
float64 opacity);
private:
void setStartWidth();
void setStartHeight();
void setStartAlpha();
void setStartFadeTop();
void createFadeMask();
void setWidthDuration();
void setHeightDuration();
void setAlphaDuration();
const style::PanelAnimation &_st;
Origin _origin = Origin::TopLeft;
QPixmap _finalImage;
int _finalWidth = 0;
int _finalHeight = 0;
int _finalInnerLeft = 0;
int _finalInnerTop = 0;
int _finalInnerRight = 0;
int _finalInnerBottom = 0;
int _finalInnerWidth = 0;
int _finalInnerHeight = 0;
bool _skipShadow = false;
int _startWidth = -1;
int _startHeight = -1;
int _startAlpha = 0;
int _startFadeTop = 0;
QPixmap _fadeMask;
int _fadeHeight = 0;
QBrush _fadeFirst, _fadeLast;
float64 _widthDuration = 1.;
float64 _heightDuration = 1.;
float64 _alphaDuration = 1.;
};
} // namespace Ui

View File

@@ -0,0 +1,180 @@
// 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/path_shift_gradient.h"
#include "base/call_delayed.h"
#include "ui/effects/animations.h"
namespace Ui {
namespace {
constexpr auto kSlideDuration = crl::time(1000);
constexpr auto kWaitDuration = crl::time(1000);
constexpr auto kFullDuration = kSlideDuration + kWaitDuration;
} // namespace
struct PathShiftGradient::AnimationData {
Ui::Animations::Simple animation;
base::flat_set<not_null<PathShiftGradient*>> active;
bool scheduled = false;
};
std::weak_ptr<PathShiftGradient::AnimationData> PathShiftGradient::Animation;
PathShiftGradient::PathShiftGradient(
const style::color &bg,
const style::color &fg,
Fn<void()> animationCallback,
rpl::producer<> paletteUpdated)
: _bg(bg)
, _fg(fg)
, _animationCallback(std::move(animationCallback)) {
refreshColors();
if (!paletteUpdated) {
paletteUpdated = style::PaletteChanged();
}
std::move(
paletteUpdated
) | rpl::on_next([=] {
refreshColors();
}, _lifetime);
}
PathShiftGradient::~PathShiftGradient() {
if (const auto strong = _animation.get()) {
strong->active.erase(this);
}
}
void PathShiftGradient::overrideColors(
const style::color &bg,
const style::color &fg) {
_colorsOverriden = true;
refreshColors(bg, fg);
}
void PathShiftGradient::clearOverridenColors() {
if (!_colorsOverriden) {
return;
}
_colorsOverriden = false;
refreshColors();
}
void PathShiftGradient::startFrame(
int viewportLeft,
int viewportWidth,
int gradientWidth) {
_viewportLeft = viewportLeft;
_viewportWidth = viewportWidth;
_gradientWidth = gradientWidth;
_geometryUpdated = false;
}
void PathShiftGradient::updateGeometry() {
if (_geometryUpdated) {
return;
}
_geometryUpdated = true;
const auto now = crl::now();
const auto period = now % kFullDuration;
if (period >= kSlideDuration) {
_gradientEnabled = false;
return;
}
const auto progress = period / float64(kSlideDuration);
_gradientStart = anim::interpolate(
_viewportLeft - _gradientWidth,
_viewportLeft + _viewportWidth,
progress);
_gradientFinalStop = _gradientStart + _gradientWidth;
_gradientEnabled = true;
}
bool PathShiftGradient::paint(Fn<bool(const Background&)> painter) {
updateGeometry();
if (_gradientEnabled) {
_gradient.setStart(_gradientStart, 0);
_gradient.setFinalStop(_gradientFinalStop, 0);
}
const auto background = _gradientEnabled
? Background(&_gradient)
: _bgOverride
? *_bgOverride
: _bg;
if (!painter(background)) {
return false;
}
activateAnimation();
return true;
}
void PathShiftGradient::activateAnimation() {
if (_animationActive) {
return;
}
_animationActive = true;
if (!_animation) {
_animation = Animation.lock();
if (!_animation) {
_animation = std::make_shared<AnimationData>();
Animation = _animation;
}
}
const auto raw = _animation.get();
if (_animationCallback) {
raw->active.emplace(this);
}
const auto globalCallback = [] {
const auto strong = Animation.lock();
if (!strong) {
return;
}
strong->scheduled = false;
while (!strong->active.empty()) {
const auto entry = strong->active.back();
strong->active.erase(strong->active.end() - 1);
entry->_animationActive = false;
entry->_animationCallback();
}
};
const auto now = crl::now();
const auto period = now % kFullDuration;
if (period >= kSlideDuration) {
const auto tillWaitFinish = kFullDuration - period;
if (!raw->scheduled) {
raw->scheduled = true;
raw->animation.stop();
base::call_delayed(tillWaitFinish, globalCallback);
}
} else {
const auto tillSlideFinish = kSlideDuration - period;
if (!raw->animation.animating()) {
raw->animation.start(globalCallback, 0., 1., tillSlideFinish);
}
}
}
void PathShiftGradient::refreshColors() {
refreshColors(_bg, _fg);
}
void PathShiftGradient::refreshColors(
const style::color &bg,
const style::color &fg) {
_gradient.setStops({
{ 0., bg->c },
{ 0.5, fg->c },
{ 1., bg->c },
});
_bgOverride = _colorsOverriden ? &bg : nullptr;
}
} // namespace Ui

View File

@@ -0,0 +1,65 @@
// 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
//
#pragma once
#include "ui/style/style_core_types.h"
#include <QtGui/QLinearGradient>
namespace Ui {
class PathShiftGradient final {
public:
PathShiftGradient(
const style::color &bg,
const style::color &fg,
Fn<void()> animationCallback,
rpl::producer<> paletteUpdated = nullptr);
~PathShiftGradient();
void startFrame(
int viewportLeft,
int viewportWidth,
int gradientWidth);
void overrideColors(const style::color &bg, const style::color &fg);
void clearOverridenColors();
using Background = std::variant<QLinearGradient*, style::color>;
bool paint(Fn<bool(const Background&)> painter);
private:
struct AnimationData;
void refreshColors();
void refreshColors(const style::color &bg, const style::color &fg);
void updateGeometry();
void activateAnimation();
static std::weak_ptr<AnimationData> Animation;
const style::color &_bg;
const style::color &_fg;
const style::color *_bgOverride = nullptr;
QLinearGradient _gradient;
std::shared_ptr<AnimationData> _animation;
const Fn<void()> _animationCallback;
int _viewportLeft = 0;
int _viewportWidth = 0;
int _gradientWidth = 0;
int _gradientStart = 0;
int _gradientFinalStop = 0;
bool _gradientEnabled = false;
bool _geometryUpdated = false;
bool _animationActive = false;
bool _colorsOverriden = false;
rpl::lifetime _lifetime;
};
} // namespace Ui

View File

@@ -0,0 +1,338 @@
// 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/radial_animation.h"
#include "ui/arc_angles.h"
#include "ui/painter.h"
#include "styles/style_widgets.h"
namespace Ui {
namespace {
constexpr auto kFullArcLength = arc::kFullLength;
} // namespace
const int RadialState::kFull = kFullArcLength;
void RadialAnimation::start(float64 prg) {
_firstStart = _lastStart = _lastTime = crl::now();
const auto iprg = qRound(qMax(prg, 0.0001) * arc::kAlmostFullLength);
const auto iprgstrict = qRound(prg * arc::kAlmostFullLength);
_arcEnd = anim::value(iprgstrict, iprg);
_animation.start();
}
bool RadialAnimation::update(float64 prg, bool finished, crl::time ms) {
const auto iprg = qRound(qMax(prg, 0.0001) * arc::kAlmostFullLength);
const auto result = (iprg != qRound(_arcEnd.to()))
|| (_finished != finished);
if (_finished != finished) {
_arcEnd.start(iprg);
_finished = finished;
_lastStart = _lastTime;
} else if (result) {
_arcEnd.start(iprg);
_lastStart = _lastTime;
}
_lastTime = ms;
const auto dt = float64(ms - _lastStart);
const auto fulldt = float64(ms - _firstStart);
const auto opacitydt = _finished
? (_lastStart - _firstStart)
: fulldt;
_opacity = qMin(opacitydt / st::radialDuration, 1.);
if (anim::Disabled()) {
_arcEnd.update(1., anim::linear);
if (finished) {
stop();
}
} else if (!finished) {
_arcEnd.update(1. - (st::radialDuration / (st::radialDuration + dt)), anim::linear);
} else if (dt >= st::radialDuration) {
_arcEnd.update(1., anim::linear);
stop();
} else {
auto r = dt / st::radialDuration;
_arcEnd.update(r, anim::linear);
_opacity *= 1 - r;
}
auto fromstart = fulldt / st::radialPeriod;
_arcStart.update(fromstart - std::floor(fromstart), anim::linear);
return result;
}
void RadialAnimation::stop() {
_firstStart = _lastStart = _lastTime = 0;
_arcEnd = anim::value();
_animation.stop();
}
void RadialAnimation::draw(
QPainter &p,
const QRectF &inner,
float64 thickness,
style::color color) const {
const auto state = computeState();
auto o = p.opacity();
p.setOpacity(o * state.shown);
auto pen = color->p;
auto was = p.pen();
pen.setWidthF(thickness);
pen.setCapStyle(Qt::RoundCap);
p.setPen(pen);
{
PainterHighQualityEnabler hq(p);
p.drawArc(inner, state.arcFrom, state.arcLength);
}
p.setPen(was);
p.setOpacity(o);
}
RadialState RadialAnimation::computeState() const {
auto length = arc::kMinLength + qRound(_arcEnd.current());
auto from = arc::kQuarterLength
- length
- (anim::Disabled() ? 0 : qRound(_arcStart.current()));
if (style::RightToLeft()) {
from = arc::kQuarterLength - (from - arc::kQuarterLength) - length;
if (from < 0) from += arc::kFullLength;
}
return { _opacity, from, length };
}
void InfiniteRadialAnimation::init() {
anim::Disables() | rpl::filter([=] {
return animating();
}) | rpl::on_next([=](bool disabled) {
if (!disabled && !_animation.animating()) {
_animation.start();
} else if (disabled && _animation.animating()) {
_animation.stop();
}
}, _lifetime);
}
void InfiniteRadialAnimation::start(crl::time skip) {
if (!animating()) {
const auto now = crl::now();
_workStarted = std::max(now + _st.sineDuration - skip, crl::time(1));
_workFinished = 0;
}
if (!anim::Disabled() && !_animation.animating()) {
_animation.start();
}
}
void InfiniteRadialAnimation::stop(anim::type animated) {
const auto now = crl::now();
if (anim::Disabled() || animated == anim::type::instant) {
_workFinished = now;
}
if (!_workFinished) {
const auto zero = _workStarted - _st.sineDuration;
const auto index = (now - zero + _st.sinePeriod - _st.sineShift)
/ _st.sinePeriod;
_workFinished = zero
+ _st.sineShift
+ (index * _st.sinePeriod)
+ _st.sineDuration;
} else if (_workFinished <= now) {
_animation.stop();
}
}
void InfiniteRadialAnimation::draw(
QPainter &p,
QPoint position,
int outerWidth) {
Draw(
p,
computeState(),
position,
_st.size,
outerWidth,
_st.color,
_st.thickness);
}
void InfiniteRadialAnimation::draw(
QPainter &p,
QPoint position,
QSize size,
int outerWidth) {
Draw(
p,
computeState(),
position,
size,
outerWidth,
_st.color,
_st.thickness);
}
void InfiniteRadialAnimation::Draw(
QPainter &p,
const RadialState &state,
QPoint position,
QSize size,
int outerWidth,
QPen pen,
int thickness) {
auto o = p.opacity();
p.setOpacity(o * state.shown);
const auto rect = style::rtlrect(
position.x(),
position.y(),
size.width(),
size.height(),
outerWidth);
const auto was = p.pen();
const auto brush = p.brush();
if (anim::Disabled()) {
anim::DrawStaticLoading(p, rect, thickness, pen);
} else {
pen.setWidth(thickness);
pen.setCapStyle(Qt::RoundCap);
p.setPen(pen);
{
PainterHighQualityEnabler hq(p);
p.drawArc(
rect,
state.arcFrom,
state.arcLength);
}
}
p.setPen(was);
p.setBrush(brush);
p.setOpacity(o);
}
RadialState InfiniteRadialAnimation::computeState() {
const auto now = crl::now();
const auto linear = kFullArcLength
- int(((now * kFullArcLength) / _st.linearPeriod) % kFullArcLength);
if (!animating()) {
const auto shown = 0.;
_animation.stop();
return {
shown,
linear,
kFullArcLength };
}
if (anim::Disabled()) {
return { 1., 0, kFullArcLength };
}
const auto min = int(base::SafeRound(kFullArcLength * _st.arcMin));
const auto max = int(base::SafeRound(kFullArcLength * _st.arcMax));
if (now <= _workStarted) {
// zero .. _workStarted
const auto zero = _workStarted - _st.sineDuration;
const auto shown = (now - zero) / float64(_st.sineDuration);
const auto length = anim::interpolate(
kFullArcLength,
min,
anim::sineInOut(1., std::clamp(shown, 0., 1.)));
return {
shown,
linear,
length };
} else if (!_workFinished || now <= _workFinished - _st.sineDuration) {
// _workStared .. _workFinished - _st.sineDuration
const auto shown = 1.;
const auto cycles = (now - _workStarted) / _st.sinePeriod;
const auto relative = (now - _workStarted) % _st.sinePeriod;
const auto smallDuration = _st.sineShift - _st.sineDuration;
const auto basic = int((linear
+ min
+ (cycles * (kFullArcLength + min - max))) % kFullArcLength);
if (relative <= smallDuration) {
// localZero .. growStart
return {
shown,
basic - min,
min };
} else if (relative <= smallDuration + _st.sineDuration) {
// growStart .. growEnd
const auto growLinear = (relative - smallDuration) /
float64(_st.sineDuration);
const auto growProgress = anim::sineInOut(1., growLinear);
const auto length = anim::interpolate(min, max, growProgress);
return {
shown,
basic - length,
length };
} else if (relative <= _st.sinePeriod - _st.sineDuration) {
// growEnd .. shrinkStart
return {
shown,
basic - max,
max };
} else {
// shrinkStart .. shrinkEnd
const auto shrinkLinear = (relative
- (_st.sinePeriod - _st.sineDuration))
/ float64(_st.sineDuration);
const auto shrinkProgress = anim::sineInOut(1., shrinkLinear);
const auto shrink = anim::interpolate(
0,
max - min,
shrinkProgress);
return {
shown,
basic - max,
max - shrink }; // interpolate(max, min, shrinkProgress)
}
} else {
// _workFinished - _st.sineDuration .. _workFinished
const auto hidden = (now - (_workFinished - _st.sineDuration))
/ float64(_st.sineDuration);
const auto cycles = (_workFinished - _workStarted) / _st.sinePeriod;
const auto basic = int((linear
+ min
+ cycles * (kFullArcLength + min - max)) % kFullArcLength);
const auto length = anim::interpolate(
min,
kFullArcLength,
anim::sineInOut(1., std::clamp(hidden, 0., 1.)));
return {
1. - hidden,
basic - length,
length };
}
//const auto frontPeriods = time / st.sinePeriod;
//const auto frontCurrent = time % st.sinePeriod;
//const auto frontProgress = anim::sineInOut(
// st.arcMax - st.arcMin,
// std::min(frontCurrent, crl::time(st.sineDuration))
// / float64(st.sineDuration));
//const auto backTime = std::max(time - st.sineShift, 0LL);
//const auto backPeriods = backTime / st.sinePeriod;
//const auto backCurrent = backTime % st.sinePeriod;
//const auto backProgress = anim::sineInOut(
// st.arcMax - st.arcMin,
// std::min(backCurrent, crl::time(st.sineDuration))
// / float64(st.sineDuration));
//const auto front = linear + base::SafeRound((st.arcMin + frontProgress + frontPeriods * (st.arcMax - st.arcMin)) * kFullArcLength);
//const auto from = linear + base::SafeRound((backProgress + backPeriods * (st.arcMax - st.arcMin)) * kFullArcLength);
//const auto length = (front - from);
//return {
// _opacity,
// from,
// length
//};
}
} // namespace Ui

View File

@@ -0,0 +1,123 @@
// 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
//
#pragma once
#include "ui/effects/animations.h"
namespace style {
struct InfiniteRadialAnimation;
} // namespace style
namespace Ui {
struct RadialState {
static const int kFull;
float64 shown = 0.;
int arcFrom = 0;
int arcLength = kFull;
};
class RadialAnimation {
public:
template <typename Callback>
RadialAnimation(Callback &&callback);
[[nodiscard]] float64 opacity() const {
return _opacity;
}
[[nodiscard]] bool animating() const {
return _animation.animating();
}
void start(float64 prg);
bool update(float64 prg, bool finished, crl::time ms);
void stop();
void draw(
QPainter &p,
const QRectF &inner,
float64 thickness,
style::color color) const;
[[nodiscard]] RadialState computeState() const;
private:
crl::time _firstStart = 0;
crl::time _lastStart = 0;
crl::time _lastTime = 0;
float64 _opacity = 0.;
anim::value _arcEnd;
anim::value _arcStart;
Ui::Animations::Basic _animation;
bool _finished = false;
};
template <typename Callback>
inline RadialAnimation::RadialAnimation(Callback &&callback)
: _arcStart(0, RadialState::kFull)
, _animation(std::forward<Callback>(callback)) {
}
class InfiniteRadialAnimation {
public:
template <typename Callback>
InfiniteRadialAnimation(
Callback &&callback,
const style::InfiniteRadialAnimation &st);
[[nodiscard]] bool animating() const {
return _workStarted && (!_workFinished || _workFinished > crl::now());
}
void start(crl::time skip = 0);
void stop(anim::type animated = anim::type::normal);
void draw(
QPainter &p,
QPoint position,
int outerWidth);
void draw(
QPainter &p,
QPoint position,
QSize size,
int outerWidth);
static void Draw(
QPainter &p,
const RadialState &state,
QPoint position,
QSize size,
int outerWidth,
QPen pen,
int thickness);
[[nodiscard]] RadialState computeState();
private:
void init();
const style::InfiniteRadialAnimation &_st;
crl::time _workStarted = 0;
crl::time _workFinished = 0;
Ui::Animations::Basic _animation;
rpl::lifetime _lifetime;
};
template <typename Callback>
inline InfiniteRadialAnimation::InfiniteRadialAnimation(
Callback &&callback,
const style::InfiniteRadialAnimation &st)
: _st(st)
, _animation(std::forward<Callback>(callback)) {
init();
}
} // namespace Ui

View File

@@ -0,0 +1,324 @@
// 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/ripple_animation.h"
#include "ui/effects/animations.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "ui/image/image_prepare.h"
#include "styles/style_widgets.h"
namespace Ui {
class RippleAnimation::Ripple {
public:
Ripple(
const style::RippleAnimation &st,
QPoint origin,
int startRadius,
const QPixmap &mask,
Fn<void()> update);
Ripple(
const style::RippleAnimation &st,
const QPixmap &mask,
Fn<void()> update);
void paint(
QPainter &p,
const QPixmap &mask,
const QColor *colorOverride);
void stop();
void unstop();
void finish();
void clearCache();
bool finished() const {
return _hiding && !_hide.animating();
}
private:
const style::RippleAnimation &_st;
Fn<void()> _update;
QPoint _origin;
int _radiusFrom = 0;
int _radiusTo = 0;
bool _hiding = false;
Ui::Animations::Simple _show;
Ui::Animations::Simple _hide;
QPixmap _cache;
QImage _frame;
};
RippleAnimation::Ripple::Ripple(
const style::RippleAnimation &st,
QPoint origin,
int startRadius,
const QPixmap &mask,
Fn<void()> update)
: _st(st)
, _update(std::move(update))
, _origin(origin)
, _radiusFrom(startRadius)
, _frame(mask.size(), QImage::Format_ARGB32_Premultiplied) {
_frame.setDevicePixelRatio(mask.devicePixelRatio());
const auto pixelRatio = style::DevicePixelRatio();
QPoint points[] = {
{ 0, 0 },
{ _frame.width() / pixelRatio, 0 },
{ _frame.width() / pixelRatio, _frame.height() / pixelRatio },
{ 0, _frame.height() / pixelRatio },
};
for (auto point : points) {
accumulate_max(
_radiusTo,
style::point::dotProduct(_origin - point, _origin - point));
}
_radiusTo = qRound(sqrt(_radiusTo));
_show.start(_update, 0., 1., _st.showDuration, anim::easeOutQuint);
}
RippleAnimation::Ripple::Ripple(const style::RippleAnimation &st, const QPixmap &mask, Fn<void()> update)
: _st(st)
, _update(std::move(update))
, _origin(
mask.width() / (2 * style::DevicePixelRatio()),
mask.height() / (2 * style::DevicePixelRatio()))
, _radiusFrom(mask.width() + mask.height())
, _frame(mask.size(), QImage::Format_ARGB32_Premultiplied) {
_frame.setDevicePixelRatio(mask.devicePixelRatio());
_radiusTo = _radiusFrom;
_hide.start(_update, 0., 1., _st.hideDuration);
}
void RippleAnimation::Ripple::paint(
QPainter &p,
const QPixmap &mask,
const QColor *colorOverride) {
auto opacity = _hide.value(_hiding ? 0. : 1.);
if (opacity == 0.) {
return;
}
if (_cache.isNull() || colorOverride != nullptr) {
const auto shown = _show.value(1.);
Assert(!std::isnan(shown));
const auto diff = float64(_radiusTo - _radiusFrom);
Assert(!std::isnan(diff));
const auto mult = diff * shown;
Assert(!std::isnan(mult));
const auto interpolated = _radiusFrom + mult;
//anim::interpolateF(_radiusFrom, _radiusTo, shown);
Assert(!std::isnan(interpolated));
auto radius = int(base::SafeRound(interpolated));
//anim::interpolate(_radiusFrom, _radiusTo, _show.value(1.));
_frame.fill(Qt::transparent);
{
QPainter p(&_frame);
p.setPen(Qt::NoPen);
if (colorOverride) {
p.setBrush(*colorOverride);
} else {
p.setBrush(_st.color);
}
{
PainterHighQualityEnabler hq(p);
p.drawEllipse(_origin, radius, radius);
}
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
p.drawPixmap(0, 0, mask);
}
if (radius == _radiusTo && colorOverride == nullptr) {
_cache = PixmapFromImage(std::move(_frame));
}
}
auto saved = p.opacity();
if (opacity != 1.) p.setOpacity(saved * opacity);
if (_cache.isNull()) {
p.drawImage(0, 0, _frame);
} else {
p.drawPixmap(0, 0, _cache);
}
if (opacity != 1.) p.setOpacity(saved);
}
void RippleAnimation::Ripple::stop() {
_hiding = true;
_hide.start(_update, 1., 0., _st.hideDuration);
}
void RippleAnimation::Ripple::unstop() {
if (_hiding) {
if (_hide.animating()) {
_hide.start(_update, 0., 1., _st.hideDuration);
}
_hiding = false;
}
}
void RippleAnimation::Ripple::finish() {
if (_update) {
_update();
}
_show.stop();
_hide.stop();
}
void RippleAnimation::Ripple::clearCache() {
_cache = QPixmap();
}
RippleAnimation::RippleAnimation(
const style::RippleAnimation &st,
QImage mask,
Fn<void()> callback)
: _st(st)
, _mask(PixmapFromImage(std::move(mask)))
, _update(std::move(callback)) {
}
void RippleAnimation::add(QPoint origin, int startRadius) {
lastStop();
_ripples.push_back(
std::make_unique<Ripple>(_st, origin, startRadius, _mask, _update));
}
void RippleAnimation::addFading() {
lastStop();
_ripples.push_back(std::make_unique<Ripple>(_st, _mask, _update));
}
void RippleAnimation::lastStop() {
if (!_ripples.empty()) {
_ripples.back()->stop();
}
}
void RippleAnimation::lastUnstop() {
if (!_ripples.empty()) {
_ripples.back()->unstop();
}
}
void RippleAnimation::lastFinish() {
if (!_ripples.empty()) {
_ripples.back()->finish();
}
}
void RippleAnimation::forceRepaint() {
for (const auto &ripple : _ripples) {
ripple->clearCache();
}
if (_update) {
_update();
}
}
void RippleAnimation::paint(
QPainter &p,
int x,
int y,
int outerWidth,
const QColor *colorOverride) {
if (_ripples.empty()) {
return;
}
if (style::RightToLeft()) {
x = outerWidth - x - (_mask.width() / style::DevicePixelRatio());
}
p.translate(x, y);
for (const auto &ripple : _ripples) {
ripple->paint(p, _mask, colorOverride);
}
p.translate(-x, -y);
clearFinished();
}
QImage RippleAnimation::MaskByDrawer(
QSize size,
bool filled,
Fn<void(QPainter &p)> drawer) {
auto result = QImage(
size * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(style::DevicePixelRatio());
result.fill(filled ? QColor(255, 255, 255) : Qt::transparent);
if (drawer) {
Painter p(&result);
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setBrush(QColor(255, 255, 255));
drawer(p);
}
return result;
}
QImage RippleAnimation::RectMask(QSize size) {
return MaskByDrawer(size, true, nullptr);
}
QImage RippleAnimation::RoundRectMask(QSize size, int radius) {
return MaskByDrawer(size, false, [&](QPainter &p) {
p.drawRoundedRect(0, 0, size.width(), size.height(), radius, radius);
});
}
QImage RippleAnimation::RoundRectMask(
QSize size,
Images::CornersMaskRef corners) {
return MaskByDrawer(size, true, [&](QPainter &p) {
p.setCompositionMode(QPainter::CompositionMode_Source);
const auto ratio = style::DevicePixelRatio();
const auto corner = [&](int index, bool right, bool bottom) {
if (const auto image = corners.p[index]) {
if (!image->isNull()) {
const auto width = image->width() / ratio;
const auto height = image->height() / ratio;
p.drawImage(
QRect(
right ? (size.width() - width) : 0,
bottom ? (size.height() - height) : 0,
width,
height),
*image);
}
}
};
corner(0, false, false);
corner(1, true, false);
corner(2, false, true);
corner(3, true, true);
});
}
QImage RippleAnimation::EllipseMask(QSize size) {
return MaskByDrawer(size, false, [&](QPainter &p) {
p.drawEllipse(0, 0, size.width(), size.height());
});
}
void RippleAnimation::clearFinished() {
while (!_ripples.empty() && _ripples.front()->finished()) {
_ripples.pop_front();
}
}
void RippleAnimation::clear() {
_ripples.clear();
}
RippleAnimation::~RippleAnimation() = default;
} // namespace Ui

View File

@@ -0,0 +1,72 @@
// 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
//
#pragma once
#include <deque>
namespace Images {
struct CornersMaskRef;
} // namespace Images
namespace style {
struct RippleAnimation;
} // namespace style
namespace Ui {
class RippleAnimation {
public:
// White upon transparent mask,
// like colorizeImage(black-white-mask, white).
RippleAnimation(
const style::RippleAnimation &st,
QImage mask,
Fn<void()> update);
void add(QPoint origin, int startRadius = 0);
void addFading();
void lastStop();
void lastUnstop();
void lastFinish();
void forceRepaint();
void paint(
QPainter &p,
int x,
int y,
int outerWidth,
const QColor *colorOverride = nullptr);
bool empty() const {
return _ripples.empty();
}
static QImage MaskByDrawer(
QSize size,
bool filled,
Fn<void(QPainter &p)> drawer);
static QImage RectMask(QSize size);
static QImage RoundRectMask(QSize size, int radius);
static QImage RoundRectMask(QSize size, Images::CornersMaskRef corners);
static QImage EllipseMask(QSize size);
~RippleAnimation();
private:
void clear();
void clearFinished();
const style::RippleAnimation &_st;
QPixmap _mask;
Fn<void()> _update;
class Ripple;
std::deque<std::unique_ptr<Ripple>> _ripples;
};
} // namespace Ui

View File

@@ -0,0 +1,471 @@
// 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

View File

@@ -0,0 +1,92 @@
// 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
//
#pragma once
namespace Ui {
struct ImageSubrect {
not_null<QImage*> image;
QRect rect;
};
class RoundAreaWithShadow final {
public:
static constexpr auto kFramesCount = 32;
[[nodiscard]] static QImage PrepareImage(QSize size);
[[nodiscard]] static QImage PrepareFramesCache(
QSize frame,
int columns = 1);
[[nodiscard]] static QRect FrameCacheRect(
int frameIndex,
int column,
QSize frame);
// Returns center area which could be just filled with a solid color.
static QRect FillWithImage(
QPainter &p,
QRect geometry,
const ImageSubrect &pattern);
RoundAreaWithShadow(QSize inner, QMargins shadow, int twiceRadiusMax);
void setBackgroundColor(const QColor &background);
void setShadowColor(const QColor &shadow);
[[nodiscard]] ImageSubrect validateFrame(
int frameIndex,
float64 scale,
float64 radius);
[[nodiscard]] ImageSubrect validateOverlayMask(
int frameIndex,
QSize innerSize,
float64 radius,
int twiceRadius,
float64 scale);
[[nodiscard]] ImageSubrect validateOverlayShadow(
int frameIndex,
QSize innerSize,
float64 radius,
int twiceRadius,
float64 scale,
const ImageSubrect &mask);
void overlayExpandedBorder(
QPainter &p,
QSize size,
float64 expandRatio,
float64 radiusFrom,
float64 radiusTill,
float64 scale);
private:
[[nodiscard]] QRect validateShadow(
int frameIndex,
float64 scale,
float64 radius);
QRect _inner;
QSize _outer;
QSize _overlay;
std::array<bool, kFramesCount> _validBg = { { false } };
std::array<bool, kFramesCount> _validShadow = { { false } };
std::array<bool, kFramesCount> _validOverlayMask = { { false } };
std::array<bool, kFramesCount> _validOverlayShadow = { { false } };
QColor _background;
QColor _gradient;
QColor _shadow;
QImage _cacheBg;
QImage _shadowParts;
QImage _overlayCacheParts;
QImage _overlayMaskScaled;
QImage _overlayShadowScaled;
QImage _shadowBuffer;
};
} // namespace Ui

View File

@@ -0,0 +1,113 @@
// 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/show_animation.h"
#include "ui/effects/animations.h"
#include "ui/rp_widget.h"
#include "ui/ui_utility.h"
#include "styles/style_widgets.h"
namespace Ui::Animations {
namespace {
void AnimateWidgets(const Widgets &targets, bool show) {
enum class Finish {
Bad,
Good,
};
struct Object {
base::unique_qptr<Ui::RpWidget> container;
base::weak_qptr<Ui::RpWidget> weakTarget;
};
struct State {
rpl::event_stream<Finish> destroy;
Ui::Animations::Simple animation;
std::vector<Object> objects;
};
auto lifetime = std::make_shared<rpl::lifetime>();
const auto state = lifetime->make_state<State>();
const auto from = show ? 0. : 1.;
const auto to = show ? 1. : 0.;
for (const auto &target : targets) {
state->objects.push_back({
base::make_unique_q<Ui::RpWidget>(target->parentWidget()),
base::make_weak(target),
});
const auto pixmap = Ui::GrabWidget(target);
const auto raw = state->objects.back().container.get();
raw->paintRequest(
) | rpl::on_next([=] {
QPainter p(raw);
p.setOpacity(state->animation.value(to));
p.drawPixmap(QPoint(), pixmap);
}, raw->lifetime());
target->geometryValue(
) | rpl::on_next([=](const QRect &r) {
raw->setGeometry(r);
}, raw->lifetime());
raw->show();
if (!show) {
target->hide();
}
}
state->destroy.events(
) | rpl::take(
1
) | rpl::on_next([=](Finish type) mutable {
if (type == Finish::Good && show) {
for (const auto &object : state->objects) {
if (object.weakTarget) {
object.weakTarget->show();
}
}
}
if (lifetime) {
base::take(lifetime)->destroy();
}
}, *lifetime);
state->animation.start(
[=](auto value) {
for (const auto &object : state->objects) {
if (object.container) {
object.container->update();
}
if (!object.weakTarget && show) {
state->destroy.fire(Finish::Bad);
return;
}
}
if (value == to) {
state->destroy.fire(Finish::Good);
}
},
from,
to,
st::defaultToggle.duration);
}
} // namespace
void ShowWidgets(const Widgets &targets) {
AnimateWidgets(targets, true);
}
void HideWidgets(const Widgets &targets) {
AnimateWidgets(targets, false);
}
} // namespace Ui::Animations

View File

@@ -0,0 +1,20 @@
// 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
//
#pragma once
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Ui::Animations {
using Widgets = std::vector<not_null<Ui::RpWidget*>>;
void ShowWidgets(const Widgets &targets);
void HideWidgets(const Widgets &targets);
} // namespace Ui::Animations

View File

@@ -0,0 +1,89 @@
// 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/slide_animation.h"
#include <QtGui/QPainter>
namespace Ui {
void SlideAnimation::setSnapshots(
QPixmap leftSnapshot,
QPixmap rightSnapshot) {
Expects(!leftSnapshot.isNull());
Expects(!rightSnapshot.isNull());
_leftSnapshot = std::move(leftSnapshot);
_rightSnapshot = std::move(rightSnapshot);
_leftSnapshot.setDevicePixelRatio(style::DevicePixelRatio());
_rightSnapshot.setDevicePixelRatio(style::DevicePixelRatio());
}
void SlideAnimation::paintFrame(QPainter &p, int x, int y, int outerWidth) {
if (!animating()) {
return;
}
const auto pixelRatio = style::DevicePixelRatio();
const auto state = this->state();
const auto leftCoord = _slideLeft
? anim::interpolate(-_leftSnapshotWidth, 0, state.leftProgress)
: anim::interpolate(0, -_leftSnapshotWidth, state.leftProgress);
const auto rightCoord = _slideLeft
? anim::interpolate(0, _rightSnapshotWidth, state.rightProgress)
: anim::interpolate(_rightSnapshotWidth, 0, state.rightProgress);
if (_overflowHidden) {
const auto leftWidth = (_leftSnapshotWidth + leftCoord);
if (leftWidth > 0) {
p.setOpacity(state.leftAlpha);
p.drawPixmap(
x,
y,
leftWidth,
_leftSnapshotHeight,
_leftSnapshot,
(_leftSnapshot.width() - leftWidth * pixelRatio),
0,
leftWidth * pixelRatio,
_leftSnapshot.height());
}
const auto rightWidth = _rightSnapshotWidth - rightCoord;
if (rightWidth > 0) {
p.setOpacity(state.rightAlpha);
p.drawPixmap(
x + rightCoord,
y,
_rightSnapshot,
0,
0,
rightWidth * pixelRatio,
_rightSnapshot.height());
}
} else {
p.setOpacity(state.leftAlpha);
p.drawPixmap(x + leftCoord, y, _leftSnapshot);
p.setOpacity(state.rightAlpha);
p.drawPixmap(x + rightCoord, y, _rightSnapshot);
}
}
SlideAnimation::State SlideAnimation::state() const {
const auto dt = _animation.value(1.);
const auto easeOut = anim::easeOutCirc(1., dt);
const auto easeIn = anim::easeInCirc(1., dt);
const auto arrivingAlpha = easeIn;
const auto departingAlpha = 1. - easeOut;
auto result = State();
result.leftProgress = _slideLeft ? easeOut : easeIn;
result.leftAlpha = _slideLeft ? arrivingAlpha : departingAlpha;
result.rightProgress = _slideLeft ? easeIn : easeOut;
result.rightAlpha = _slideLeft ? departingAlpha : arrivingAlpha;
return result;
}
} // namespace Ui

View File

@@ -0,0 +1,66 @@
// 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
//
#pragma once
#include "ui/effects/animations.h"
namespace Ui {
class SlideAnimation {
public:
struct State {
float64 leftProgress = 0.;
float64 leftAlpha = 0.;
float64 rightProgress = 0.;
float64 rightAlpha = 0.;
};
void setSnapshots(QPixmap leftSnapshot, QPixmap rightSnapshot);
void setOverflowHidden(bool hidden) {
_overflowHidden = hidden;
}
template <typename Lambda>
void start(bool slideLeft, Lambda &&updateCallback, float64 duration);
void paintFrame(QPainter &p, int x, int y, int outerWidth);
[[nodiscard]] State state() const;
[[nodiscard]] bool animating() const {
return _animation.animating();
}
private:
Ui::Animations::Simple _animation;
QPixmap _leftSnapshot;
QPixmap _rightSnapshot;
bool _slideLeft = false;
bool _overflowHidden = true;
int _leftSnapshotWidth = 0;
int _leftSnapshotHeight = 0;
int _rightSnapshotWidth = 0;
};
template <typename Lambda>
void SlideAnimation::start(
bool slideLeft,
Lambda &&updateCallback,
float64 duration) {
_slideLeft = slideLeft;
if (_slideLeft) {
std::swap(_leftSnapshot, _rightSnapshot);
}
const auto pixelRatio = style::DevicePixelRatio();
_leftSnapshotWidth = _leftSnapshot.width() / pixelRatio;
_leftSnapshotHeight = _leftSnapshot.height() / pixelRatio;
_rightSnapshotWidth = _rightSnapshot.width() / pixelRatio;
_animation.start(std::forward<Lambda>(updateCallback), 0., 1., duration);
}
} // namespace Ui

View File

@@ -0,0 +1,878 @@
// 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/spoiler_mess.h"
#include "ui/effects/animations.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "ui/integration.h"
#include "base/random.h"
#include "base/flags.h"
#include <QtCore/QBuffer>
#include <QtCore/QFile>
#include <QtCore/QDir>
#include <crl/crl_async.h>
#include <xxhash.h>
#include <mutex>
#include <condition_variable>
namespace Ui {
namespace {
constexpr auto kVersion = 2;
constexpr auto kFramesPerRow = 10;
constexpr auto kImageSpoilerDarkenAlpha = 32;
constexpr auto kMaxCacheSize = 5 * 1024 * 1024;
constexpr auto kDefaultFrameDuration = crl::time(33);
constexpr auto kDefaultFramesCount = 60;
constexpr auto kAutoPauseTimeout = crl::time(1000);
[[nodiscard]] SpoilerMessDescriptor DefaultDescriptorText() {
const auto ratio = style::DevicePixelRatio();
const auto size = style::ConvertScale(128) * ratio;
return {
.particleFadeInDuration = crl::time(200),
.particleShownDuration = crl::time(200),
.particleFadeOutDuration = crl::time(200),
.particleSizeMin = style::ConvertScaleExact(1.5) * ratio,
.particleSizeMax = style::ConvertScaleExact(2.) * ratio,
.particleSpeedMin = style::ConvertScaleExact(4.),
.particleSpeedMax = style::ConvertScaleExact(8.),
.particleSpritesCount = 5,
.particlesCount = 9000,
.canvasSize = size,
.framesCount = kDefaultFramesCount,
.frameDuration = kDefaultFrameDuration,
};
}
[[nodiscard]] SpoilerMessDescriptor DefaultDescriptorImage() {
const auto ratio = style::DevicePixelRatio();
const auto size = style::ConvertScale(128) * ratio;
return {
.particleFadeInDuration = crl::time(300),
.particleShownDuration = crl::time(0),
.particleFadeOutDuration = crl::time(300),
.particleSizeMin = style::ConvertScaleExact(1.5) * ratio,
.particleSizeMax = style::ConvertScaleExact(2.) * ratio,
.particleSpeedMin = style::ConvertScaleExact(10.),
.particleSpeedMax = style::ConvertScaleExact(20.),
.particleSpritesCount = 5,
.particlesCount = 3000,
.canvasSize = size,
.framesCount = kDefaultFramesCount,
.frameDuration = kDefaultFrameDuration,
};
}
} // namespace
class SpoilerAnimationManager final {
public:
explicit SpoilerAnimationManager(not_null<SpoilerAnimation*> animation);
void add(not_null<SpoilerAnimation*> animation);
void remove(not_null<SpoilerAnimation*> animation);
private:
void destroyIfEmpty();
Ui::Animations::Basic _animation;
base::flat_set<not_null<SpoilerAnimation*>> _list;
};
namespace {
struct DefaultSpoilerWaiter {
std::condition_variable variable;
std::mutex mutex;
};
struct DefaultSpoiler {
std::atomic<const SpoilerMessCached*> cached/* = nullptr*/;
std::atomic<DefaultSpoilerWaiter*> waiter/* = nullptr*/;
};
DefaultSpoiler DefaultTextMask;
DefaultSpoiler DefaultImageCached;
SpoilerAnimationManager *DefaultAnimationManager/* = nullptr*/;
struct Header {
uint32 version = 0;
uint32 dataLength = 0;
uint32 dataHash = 0;
int32 framesCount = 0;
int32 canvasSize = 0;
int32 frameDuration = 0;
};
struct Particle {
crl::time start = 0;
int spriteIndex = 0;
int x = 0;
int y = 0;
float64 dx = 0.;
float64 dy = 0.;
};
[[nodiscard]] std::pair<float64, float64> RandomSpeed(
const SpoilerMessDescriptor &descriptor,
base::BufferedRandom<uint32> &random) {
const auto count = descriptor.particlesCount;
const auto speedMax = descriptor.particleSpeedMax;
const auto speedMin = descriptor.particleSpeedMin;
const auto value = RandomIndex(2 * count + 2, random);
const auto negative = (value < count + 1);
const auto module = (negative ? value : (value - count - 1));
const auto speed = speedMin + (((speedMax - speedMin) * module) / count);
const auto lifetime = descriptor.particleFadeInDuration
+ descriptor.particleShownDuration
+ descriptor.particleFadeOutDuration;
const auto max = int(std::ceil(speedMax * lifetime));
const auto k = speed / lifetime;
const auto x = (speedMax > 0)
? ((RandomIndex(2 * max + 1, random) - max) / float64(max))
: 0.;
const auto y = (speedMax > 0)
? (sqrt(1 - x * x) * (negative ? -1 : 1))
: 0.;
return { k * x, k * y };
}
[[nodiscard]] Particle GenerateParticle(
const SpoilerMessDescriptor &descriptor,
int index,
base::BufferedRandom<uint32> &random) {
const auto speed = RandomSpeed(descriptor, random);
return {
.start = (index * descriptor.framesCount * descriptor.frameDuration
/ descriptor.particlesCount),
.spriteIndex = RandomIndex(descriptor.particleSpritesCount, random),
.x = RandomIndex(descriptor.canvasSize, random),
.y = RandomIndex(descriptor.canvasSize, random),
.dx = speed.first,
.dy = speed.second,
};
}
[[nodiscard]] QImage GenerateSprite(
const SpoilerMessDescriptor &descriptor,
int index,
int size,
base::BufferedRandom<uint32> &random) {
Expects(index >= 0 && index < descriptor.particleSpritesCount);
const auto count = descriptor.particleSpritesCount;
const auto middle = count / 2;
const auto min = descriptor.particleSizeMin;
const auto delta = descriptor.particleSizeMax - min;
const auto width = (index < middle)
? (min + delta * (middle - index) / float64(middle))
: min;
const auto height = (index > middle)
? (min + delta * (index - middle) / float64(count - 1 - middle))
: min;
const auto radius = min / 2.;
auto result = QImage(size, size, QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
auto p = QPainter(&result);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(Qt::white);
QPainterPath path;
path.addRoundedRect(1., 1., width, height, radius, radius);
p.drawPath(path);
p.end();
return result;
}
[[nodiscard]] QString DefaultMaskCacheFolder() {
const auto base = Integration::Instance().emojiCacheFolder();
return base.isEmpty() ? QString() : (base + "/spoiler");
}
[[nodiscard]] std::optional<SpoilerMessCached> ReadDefaultMask(
const QString &name,
std::optional<SpoilerMessCached::Validator> validator) {
const auto folder = DefaultMaskCacheFolder();
if (folder.isEmpty()) {
return {};
}
auto file = QFile(folder + '/' + name);
return (file.open(QIODevice::ReadOnly) && file.size() <= kMaxCacheSize)
? SpoilerMessCached::FromSerialized(file.readAll(), validator)
: std::nullopt;
}
void WriteDefaultMask(
const QString &name,
const SpoilerMessCached &mask) {
const auto folder = DefaultMaskCacheFolder();
if (!QDir().mkpath(folder)) {
return;
}
const auto bytes = mask.serialize();
auto file = QFile(folder + '/' + name);
if (file.open(QIODevice::WriteOnly) && bytes.size() <= kMaxCacheSize) {
file.write(bytes);
}
}
void Register(not_null<SpoilerAnimation*> animation) {
if (DefaultAnimationManager) {
DefaultAnimationManager->add(animation);
} else {
new SpoilerAnimationManager(animation);
}
}
void Unregister(not_null<SpoilerAnimation*> animation) {
Expects(DefaultAnimationManager != nullptr);
DefaultAnimationManager->remove(animation);
}
// DescriptorFactory: (void) -> SpoilerMessDescriptor.
// Postprocess: (unique_ptr<MessCached>) -> unique_ptr<MessCached>.
template <typename DescriptorFactory, typename Postprocess>
void PrepareDefaultSpoiler(
DefaultSpoiler &spoiler,
const char *nameFactory,
DescriptorFactory descriptorFactory,
Postprocess postprocess) {
if (spoiler.waiter.load()) {
return;
}
const auto waiter = new DefaultSpoilerWaiter();
auto expected = (DefaultSpoilerWaiter*)nullptr;
if (!spoiler.waiter.compare_exchange_strong(expected, waiter)) {
delete waiter;
return;
}
const auto name = QString::fromUtf8(nameFactory);
crl::async([=, &spoiler] {
const auto descriptor = descriptorFactory();
auto cached = ReadDefaultMask(name, SpoilerMessCached::Validator{
.frameDuration = descriptor.frameDuration,
.framesCount = descriptor.framesCount,
.canvasSize = descriptor.canvasSize,
});
spoiler.cached = postprocess(cached
? std::make_unique<SpoilerMessCached>(std::move(*cached))
: std::make_unique<SpoilerMessCached>(
GenerateSpoilerMess(descriptor))
).release();
auto lock = std::unique_lock(waiter->mutex);
waiter->variable.notify_all();
if (!cached) {
WriteDefaultMask(name, *spoiler.cached);
}
});
}
[[nodiscard]] const SpoilerMessCached &WaitDefaultSpoiler(
DefaultSpoiler &spoiler) {
const auto &cached = spoiler.cached;
if (const auto result = cached.load()) {
return *result;
}
const auto waiter = spoiler.waiter.load();
Assert(waiter != nullptr);
while (true) {
auto lock = std::unique_lock(waiter->mutex);
if (const auto result = cached.load()) {
return *result;
}
waiter->variable.wait(lock);
}
}
} // namespace
SpoilerAnimationManager::SpoilerAnimationManager(
not_null<SpoilerAnimation*> animation)
: _animation([=](crl::time now) {
for (auto i = begin(_list); i != end(_list);) {
if ((*i)->repaint(now)) {
++i;
} else {
i = _list.erase(i);
}
}
destroyIfEmpty();
})
, _list{ { animation } } {
Expects(!DefaultAnimationManager);
DefaultAnimationManager = this;
_animation.start();
}
void SpoilerAnimationManager::add(not_null<SpoilerAnimation*> animation) {
_list.emplace(animation);
}
void SpoilerAnimationManager::remove(not_null<SpoilerAnimation*> animation) {
_list.remove(animation);
destroyIfEmpty();
}
void SpoilerAnimationManager::destroyIfEmpty() {
if (_list.empty()) {
Assert(DefaultAnimationManager == this);
delete base::take(DefaultAnimationManager);
}
}
SpoilerMessCached GenerateSpoilerMess(
const SpoilerMessDescriptor &descriptor) {
Expects(descriptor.framesCount > 0);
Expects(descriptor.frameDuration > 0);
Expects(descriptor.particlesCount > 0);
Expects(descriptor.canvasSize > 0);
Expects(descriptor.particleSizeMax >= descriptor.particleSizeMin);
Expects(descriptor.particleSizeMin > 0.);
const auto frames = descriptor.framesCount;
const auto rows = (frames + kFramesPerRow - 1) / kFramesPerRow;
const auto columns = std::min(frames, kFramesPerRow);
const auto size = descriptor.canvasSize;
const auto count = descriptor.particlesCount;
const auto width = size * columns;
const auto height = size * rows;
const auto spriteSize = 2 + int(std::ceil(descriptor.particleSizeMax));
const auto singleDuration = descriptor.particleFadeInDuration
+ descriptor.particleShownDuration
+ descriptor.particleFadeOutDuration;
const auto fullDuration = frames * descriptor.frameDuration;
Assert(fullDuration > singleDuration);
auto random = base::BufferedRandom<uint32>(count * 5);
auto particles = std::vector<Particle>();
particles.reserve(descriptor.particlesCount);
for (auto i = 0; i != descriptor.particlesCount; ++i) {
particles.push_back(GenerateParticle(descriptor, i, random));
}
auto sprites = std::vector<QImage>();
sprites.reserve(descriptor.particleSpritesCount);
for (auto i = 0; i != descriptor.particleSpritesCount; ++i) {
sprites.push_back(GenerateSprite(descriptor, i, spriteSize, random));
}
auto frame = 0;
auto image = QImage(width, height, QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::transparent);
auto p = QPainter(&image);
const auto paintOneAt = [&](const Particle &particle, crl::time now) {
if (now <= 0 || now >= singleDuration) {
return;
}
const auto clamp = [&](int value) {
return ((value % size) + size) % size;
};
const auto x = clamp(
particle.x + int(base::SafeRound(now * particle.dx)));
const auto y = clamp(
particle.y + int(base::SafeRound(now * particle.dy)));
const auto opacity = (now < descriptor.particleFadeInDuration)
? (now / float64(descriptor.particleFadeInDuration))
: (now > singleDuration - descriptor.particleFadeOutDuration)
? ((singleDuration - now)
/ float64(descriptor.particleFadeOutDuration))
: 1.;
p.setOpacity(opacity);
const auto &sprite = sprites[particle.spriteIndex];
p.drawImage(x, y, sprite);
if (x + spriteSize > size) {
p.drawImage(x - size, y, sprite);
if (y + spriteSize > size) {
p.drawImage(x, y - size, sprite);
p.drawImage(x - size, y - size, sprite);
}
} else if (y + spriteSize > size) {
p.drawImage(x, y - size, sprite);
}
};
const auto paintOne = [&](const Particle &particle, crl::time now) {
paintOneAt(particle, now - particle.start);
paintOneAt(particle, now + fullDuration - particle.start);
};
for (auto y = 0; y != rows; ++y) {
for (auto x = 0; x != columns; ++x) {
const auto rect = QRect(x * size, y * size, size, size);
p.setClipRect(rect);
p.translate(rect.topLeft());
const auto time = frame * descriptor.frameDuration;
for (auto index = 0; index != count; ++index) {
paintOne(particles[index], time);
}
p.translate(-rect.topLeft());
if (++frame >= frames) {
break;
}
}
}
return SpoilerMessCached(
std::move(image),
frames,
descriptor.frameDuration,
size);
}
void FillSpoilerRect(
QPainter &p,
QRect rect,
const SpoilerMessFrame &frame,
QPoint originShift) {
if (rect.isEmpty()) {
return;
}
const auto &image = *frame.image;
const auto source = frame.source;
const auto ratio = style::DevicePixelRatio();
const auto origin = rect.topLeft() + originShift;
const auto size = source.width() / ratio;
const auto xSkipFrames = (origin.x() <= rect.x())
? ((rect.x() - origin.x()) / size)
: -((origin.x() - rect.x() + size - 1) / size);
const auto ySkipFrames = (origin.y() <= rect.y())
? ((rect.y() - origin.y()) / size)
: -((origin.y() - rect.y() + size - 1) / size);
const auto xFrom = origin.x() + size * xSkipFrames;
const auto yFrom = origin.y() + size * ySkipFrames;
Assert((xFrom <= rect.x())
&& (yFrom <= rect.y())
&& (xFrom + size > rect.x())
&& (yFrom + size > rect.y()));
const auto xTill = rect.x() + rect.width();
const auto yTill = rect.y() + rect.height();
const auto xCount = (xTill - xFrom + size - 1) / size;
const auto yCount = (yTill - yFrom + size - 1) / size;
Assert(xCount > 0 && yCount > 0);
const auto xFullFrom = (xFrom < rect.x()) ? 1 : 0;
const auto yFullFrom = (yFrom < rect.y()) ? 1 : 0;
const auto xFullTill = xCount - (xFrom + xCount * size > xTill ? 1 : 0);
const auto yFullTill = yCount - (yFrom + yCount * size > yTill ? 1 : 0);
const auto targetRect = [&](int x, int y) {
return QRect(xFrom + x * size, yFrom + y * size, size, size);
};
const auto drawFull = [&](int x, int y) {
p.drawImage(targetRect(x, y), image, source);
};
const auto drawPart = [&](int x, int y) {
const auto target = targetRect(x, y);
const auto fill = target.intersected(rect);
Assert(!fill.isEmpty());
p.drawImage(fill, image, QRect(
source.topLeft() + ((fill.topLeft() - target.topLeft()) * ratio),
fill.size() * ratio));
};
if (yFullFrom) {
for (auto x = 0; x != xCount; ++x) {
drawPart(x, 0);
}
}
if (yFullFrom < yFullTill) {
if (xFullFrom) {
for (auto y = yFullFrom; y != yFullTill; ++y) {
drawPart(0, y);
}
}
if (xFullFrom < xFullTill) {
for (auto y = yFullFrom; y != yFullTill; ++y) {
for (auto x = xFullFrom; x != xFullTill; ++x) {
drawFull(x, y);
}
}
}
if (xFullFrom <= xFullTill && xFullTill < xCount) {
for (auto y = yFullFrom; y != yFullTill; ++y) {
drawPart(xFullTill, y);
}
}
}
if (yFullFrom <= yFullTill && yFullTill < yCount) {
for (auto x = 0; x != xCount; ++x) {
drawPart(x, yFullTill);
}
}
}
void FillSpoilerRect(
QPainter &p,
QRect rect,
Images::CornersMaskRef mask,
const SpoilerMessFrame &frame,
QImage &cornerCache,
QPoint originShift) {
using namespace Images;
if ((!mask.p[kTopLeft] || mask.p[kTopLeft]->isNull())
&& (!mask.p[kTopRight] || mask.p[kTopRight]->isNull())
&& (!mask.p[kBottomLeft] || mask.p[kBottomLeft]->isNull())
&& (!mask.p[kBottomRight] || mask.p[kBottomRight]->isNull())) {
FillSpoilerRect(p, rect, frame, originShift);
return;
}
const auto ratio = style::DevicePixelRatio();
const auto cornerSize = [&](int index) {
const auto corner = mask.p[index];
return (!corner || corner->isNull()) ? 0 : (corner->width() / ratio);
};
const auto verticalSkip = [&](int left, int right) {
return std::max(cornerSize(left), cornerSize(right));
};
const auto fillBg = [&](QRect part) {
FillSpoilerRect(
p,
part.translated(rect.topLeft()),
frame,
originShift - rect.topLeft() - part.topLeft());
};
const auto fillCorner = [&](int x, int y, int index) {
const auto position = QPoint(x, y);
const auto corner = mask.p[index];
if (!corner || corner->isNull()) {
return;
}
if (cornerCache.width() < corner->width()
|| cornerCache.height() < corner->height()) {
cornerCache = QImage(
std::max(cornerCache.width(), corner->width()),
std::max(cornerCache.height(), corner->height()),
QImage::Format_ARGB32_Premultiplied);
cornerCache.setDevicePixelRatio(ratio);
}
const auto size = corner->size() / ratio;
const auto target = QRect(QPoint(), size);
auto q = QPainter(&cornerCache);
q.setCompositionMode(QPainter::CompositionMode_Source);
FillSpoilerRect(
q,
target,
frame,
originShift - rect.topLeft() - position);
q.setCompositionMode(QPainter::CompositionMode_DestinationIn);
q.drawImage(target, *corner);
q.end();
p.drawImage(
QRect(rect.topLeft() + position, size),
cornerCache,
QRect(QPoint(), corner->size()));
};
const auto top = verticalSkip(kTopLeft, kTopRight);
const auto bottom = verticalSkip(kBottomLeft, kBottomRight);
if (top) {
const auto left = cornerSize(kTopLeft);
const auto right = cornerSize(kTopRight);
if (left) {
fillCorner(0, 0, kTopLeft);
if (const auto add = top - left) {
fillBg({ 0, left, left, add });
}
}
if (const auto fill = rect.width() - left - right; fill > 0) {
fillBg({ left, 0, fill, top });
}
if (right) {
fillCorner(rect.width() - right, 0, kTopRight);
if (const auto add = top - right) {
fillBg({ rect.width() - right, right, right, add });
}
}
}
if (const auto h = rect.height() - top - bottom; h > 0) {
fillBg({ 0, top, rect.width(), h });
}
if (bottom) {
const auto left = cornerSize(kBottomLeft);
const auto right = cornerSize(kBottomRight);
if (left) {
fillCorner(0, rect.height() - left, kBottomLeft);
if (const auto add = bottom - left) {
fillBg({ 0, rect.height() - bottom, left, add });
}
}
if (const auto fill = rect.width() - left - right; fill > 0) {
fillBg({ left, rect.height() - bottom, fill, bottom });
}
if (right) {
fillCorner(
rect.width() - right,
rect.height() - right,
kBottomRight);
if (const auto add = bottom - right) {
fillBg({
rect.width() - right,
rect.height() - bottom,
right,
add,
});
}
}
}
}
SpoilerMessCached::SpoilerMessCached(
QImage image,
int framesCount,
crl::time frameDuration,
int canvasSize)
: _image(std::move(image))
, _frameDuration(frameDuration)
, _framesCount(framesCount)
, _canvasSize(canvasSize) {
Expects(_frameDuration > 0);
Expects(_framesCount > 0);
Expects(_canvasSize > 0);
Expects(_image.size() == QSize(
std::min(_framesCount, kFramesPerRow) * _canvasSize,
((_framesCount + kFramesPerRow - 1) / kFramesPerRow) * _canvasSize));
}
SpoilerMessCached::SpoilerMessCached(
const SpoilerMessCached &mask,
const QColor &color)
: SpoilerMessCached(
style::colorizeImage(*mask.frame(0).image, color),
mask.framesCount(),
mask.frameDuration(),
mask.canvasSize()) {
}
SpoilerMessFrame SpoilerMessCached::frame(int index) const {
const auto row = index / kFramesPerRow;
const auto column = index - row * kFramesPerRow;
return {
.image = &_image,
.source = QRect(
column * _canvasSize,
row * _canvasSize,
_canvasSize,
_canvasSize),
};
}
SpoilerMessFrame SpoilerMessCached::frame() const {
return frame((crl::now() / _frameDuration) % _framesCount);
}
crl::time SpoilerMessCached::frameDuration() const {
return _frameDuration;
}
int SpoilerMessCached::framesCount() const {
return _framesCount;
}
int SpoilerMessCached::canvasSize() const {
return _canvasSize;
}
QByteArray SpoilerMessCached::serialize() const {
Expects(_frameDuration < std::numeric_limits<int32>::max());
const auto skip = sizeof(Header);
auto result = QByteArray(skip, Qt::Uninitialized);
auto header = Header{
.version = kVersion,
.framesCount = _framesCount,
.canvasSize = _canvasSize,
.frameDuration = int32(_frameDuration),
};
const auto width = int(_image.width());
const auto height = int(_image.height());
auto grayscale = QImage(width, height, QImage::Format_Grayscale8);
{
auto tobytes = grayscale.bits();
auto frombytes = _image.constBits();
const auto toadd = grayscale.bytesPerLine() - width;
const auto fromadd = _image.bytesPerLine() - (width * 4);
for (auto y = 0; y != height; ++y) {
for (auto x = 0; x != width; ++x) {
*tobytes++ = *frombytes;
frombytes += 4;
}
tobytes += toadd;
frombytes += fromadd;
}
}
auto device = QBuffer(&result);
device.open(QIODevice::WriteOnly);
device.seek(skip);
grayscale.save(&device, "PNG");
device.close();
header.dataLength = result.size() - skip;
header.dataHash = XXH32(result.data() + skip, header.dataLength, 0);
memcpy(result.data(), &header, skip);
return result;
}
std::optional<SpoilerMessCached> SpoilerMessCached::FromSerialized(
QByteArray data,
std::optional<Validator> validator) {
const auto skip = sizeof(Header);
const auto length = data.size();
const auto bytes = reinterpret_cast<const uchar*>(data.constData());
if (length <= skip) {
return {};
}
auto header = Header();
memcpy(&header, bytes, skip);
if (header.version != kVersion
|| header.canvasSize <= 0
|| header.framesCount <= 0
|| header.frameDuration <= 0
|| (validator
&& (validator->frameDuration != header.frameDuration
|| validator->framesCount != header.framesCount
|| validator->canvasSize != header.canvasSize))
|| (skip + header.dataLength != length)
|| (XXH32(bytes + skip, header.dataLength, 0) != header.dataHash)) {
return {};
}
auto grayscale = QImage();
if (!grayscale.loadFromData(bytes + skip, header.dataLength, "PNG")
|| (grayscale.format() != QImage::Format_Grayscale8)) {
return {};
}
const auto count = header.framesCount;
const auto rows = (count + kFramesPerRow - 1) / kFramesPerRow;
const auto columns = std::min(count, kFramesPerRow);
const auto width = grayscale.width();
const auto height = grayscale.height();
if (QSize(width, height) != QSize(columns, rows) * header.canvasSize) {
return {};
}
auto image = QImage(width, height, QImage::Format_ARGB32_Premultiplied);
{
Assert(image.bytesPerLine() % 4 == 0);
auto toints = reinterpret_cast<uint32*>(image.bits());
auto frombytes = grayscale.constBits();
const auto toadd = (image.bytesPerLine() / 4) - width;
const auto fromadd = grayscale.bytesPerLine() - width;
for (auto y = 0; y != height; ++y) {
for (auto x = 0; x != width; ++x) {
const auto byte = uint32(*frombytes++);
*toints++ = (byte << 24) | (byte << 16) | (byte << 8) | byte;
}
toints += toadd;
frombytes += fromadd;
}
}
return SpoilerMessCached(
std::move(image),
count,
header.frameDuration,
header.canvasSize);
}
SpoilerAnimation::SpoilerAnimation(Fn<void()> repaint)
: _repaint(std::move(repaint)) {
Expects(_repaint != nullptr);
}
SpoilerAnimation::~SpoilerAnimation() {
if (_animating) {
_animating = false;
Unregister(this);
}
}
int SpoilerAnimation::index(crl::time now, bool paused) {
_scheduled = false;
const auto add = std::min(now - _last, kDefaultFrameDuration);
if (anim::Disabled()) {
paused = true;
}
if (!paused || _last) {
_accumulated += add;
_last = paused ? 0 : now;
}
const auto absolute = (_accumulated / kDefaultFrameDuration);
if (!paused && !_animating) {
_animating = true;
Register(this);
} else if (paused && _animating) {
_animating = false;
Unregister(this);
}
return absolute % kDefaultFramesCount;
}
Fn<void()> SpoilerAnimation::repaintCallback() const {
return _repaint;
}
bool SpoilerAnimation::repaint(crl::time now) {
if (!_scheduled) {
_scheduled = true;
_repaint();
} else if (_animating && _last && _last + kAutoPauseTimeout <= now) {
_animating = false;
return false;
}
return true;
}
void PreloadTextSpoilerMask() {
PrepareDefaultSpoiler(
DefaultTextMask,
"text",
DefaultDescriptorText,
[](std::unique_ptr<SpoilerMessCached> cached) { return cached; });
}
const SpoilerMessCached &DefaultTextSpoilerMask() {
[[maybe_unused]] static const auto once = [&] {
PreloadTextSpoilerMask();
return 0;
}();
return WaitDefaultSpoiler(DefaultTextMask);
}
void PreloadImageSpoiler() {
const auto postprocess = [](std::unique_ptr<SpoilerMessCached> cached) {
Expects(cached != nullptr);
const auto frame = cached->frame(0);
auto image = QImage(
frame.image->size(),
QImage::Format_ARGB32_Premultiplied);
image.fill(QColor(0, 0, 0, kImageSpoilerDarkenAlpha));
auto p = QPainter(&image);
p.drawImage(0, 0, *frame.image);
p.end();
return std::make_unique<SpoilerMessCached>(
std::move(image),
cached->framesCount(),
cached->frameDuration(),
cached->canvasSize());
};
PrepareDefaultSpoiler(
DefaultImageCached,
"image",
DefaultDescriptorImage,
postprocess);
}
const SpoilerMessCached &DefaultImageSpoiler() {
[[maybe_unused]] static const auto once = [&] {
PreloadImageSpoiler();
return 0;
}();
return WaitDefaultSpoiler(DefaultImageCached);
}
} // namespace Ui

View File

@@ -0,0 +1,117 @@
// 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
//
#pragma once
#include <crl/crl_time.h>
namespace Images {
struct CornersMaskRef;
} // namespace Images
namespace Ui {
struct SpoilerMessDescriptor {
crl::time particleFadeInDuration = 0;
crl::time particleShownDuration = 0;
crl::time particleFadeOutDuration = 0;
float64 particleSizeMin = 0.;
float64 particleSizeMax = 0.;
float64 particleSpeedMin = 0.;
float64 particleSpeedMax = 0.;
int particleSpritesCount = 0;
int particlesCount = 0;
int canvasSize = 0;
int framesCount = 0;
crl::time frameDuration = 0;
};
struct SpoilerMessFrame {
not_null<const QImage*> image;
QRect source;
};
void FillSpoilerRect(
QPainter &p,
QRect rect,
const SpoilerMessFrame &frame,
QPoint originShift = {});
void FillSpoilerRect(
QPainter &p,
QRect rect,
Images::CornersMaskRef mask,
const SpoilerMessFrame &frame,
QImage &cornerCache,
QPoint originShift = {});
class SpoilerMessCached final {
public:
SpoilerMessCached(
QImage image,
int framesCount,
crl::time frameDuration,
int canvasSize);
SpoilerMessCached(const SpoilerMessCached &mask, const QColor &color);
[[nodiscard]] SpoilerMessFrame frame(int index) const;
[[nodiscard]] SpoilerMessFrame frame() const; // Current by time.
[[nodiscard]] crl::time frameDuration() const;
[[nodiscard]] int framesCount() const;
[[nodiscard]] int canvasSize() const;
struct Validator {
crl::time frameDuration = 0;
int framesCount = 0;
int canvasSize = 0;
};
[[nodiscard]] QByteArray serialize() const;
[[nodiscard]] static std::optional<SpoilerMessCached> FromSerialized(
QByteArray data,
std::optional<Validator> validator = {});
private:
QImage _image;
crl::time _frameDuration = 0;
int _framesCount = 0;
int _canvasSize = 0;
};
// Works with default frame duration and default frame count.
class SpoilerAnimationManager;
class SpoilerAnimation final {
public:
explicit SpoilerAnimation(Fn<void()> repaint);
~SpoilerAnimation();
[[nodiscard]] int index(crl::time now, bool paused);
[[nodiscard]] Fn<void()> repaintCallback() const;
private:
friend class SpoilerAnimationManager;
[[nodiscard]] bool repaint(crl::time now);
const Fn<void()> _repaint;
crl::time _accumulated = 0;
crl::time _last = 0;
bool _animating : 1 = false;
bool _scheduled : 1 = false;
};
[[nodiscard]] SpoilerMessCached GenerateSpoilerMess(
const SpoilerMessDescriptor &descriptor);
void PreloadTextSpoilerMask();
[[nodiscard]] const SpoilerMessCached &DefaultTextSpoilerMask();
void PreloadImageSpoiler();
[[nodiscard]] const SpoilerMessCached &DefaultImageSpoiler();
} // namespace Ui