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
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:
181
Telegram/lib_ui/ui/effects/animation_value.cpp
Normal file
181
Telegram/lib_ui/ui/effects/animation_value.cpp
Normal 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
|
||||
420
Telegram/lib_ui/ui/effects/animation_value.h
Normal file
420
Telegram/lib_ui/ui/effects/animation_value.h
Normal 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
|
||||
26
Telegram/lib_ui/ui/effects/animation_value_f.h
Normal file
26
Telegram/lib_ui/ui/effects/animation_value_f.h
Normal 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
|
||||
212
Telegram/lib_ui/ui/effects/animations.cpp
Normal file
212
Telegram/lib_ui/ui/effects/animations.cpp
Normal 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
|
||||
460
Telegram/lib_ui/ui/effects/animations.h
Normal file
460
Telegram/lib_ui/ui/effects/animations.h
Normal 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
|
||||
206
Telegram/lib_ui/ui/effects/cross_animation.cpp
Normal file
206
Telegram/lib_ui/ui/effects/cross_animation.cpp
Normal 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
|
||||
37
Telegram/lib_ui/ui/effects/cross_animation.h
Normal file
37
Telegram/lib_ui/ui/effects/cross_animation.h
Normal 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
|
||||
130
Telegram/lib_ui/ui/effects/cross_line.cpp
Normal file
130
Telegram/lib_ui/ui/effects/cross_line.cpp
Normal 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
|
||||
48
Telegram/lib_ui/ui/effects/cross_line.h
Normal file
48
Telegram/lib_ui/ui/effects/cross_line.h
Normal 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
|
||||
161
Telegram/lib_ui/ui/effects/fade_animation.cpp
Normal file
161
Telegram/lib_ui/ui/effects/fade_animation.cpp
Normal 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
|
||||
65
Telegram/lib_ui/ui/effects/fade_animation.h
Normal file
65
Telegram/lib_ui/ui/effects/fade_animation.h
Normal 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
|
||||
79
Telegram/lib_ui/ui/effects/frame_generator.cpp
Normal file
79
Telegram/lib_ui/ui/effects/frame_generator.cpp
Normal 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
|
||||
69
Telegram/lib_ui/ui/effects/frame_generator.h
Normal file
69
Telegram/lib_ui/ui/effects/frame_generator.h
Normal 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
|
||||
30
Telegram/lib_ui/ui/effects/gradient.cpp
Normal file
30
Telegram/lib_ui/ui/effects/gradient.cpp
Normal 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
|
||||
257
Telegram/lib_ui/ui/effects/gradient.h
Normal file
257
Telegram/lib_ui/ui/effects/gradient.h
Normal 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
|
||||
272
Telegram/lib_ui/ui/effects/numbers_animation.cpp
Normal file
272
Telegram/lib_ui/ui/effects/numbers_animation.cpp
Normal 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
|
||||
114
Telegram/lib_ui/ui/effects/numbers_animation.h
Normal file
114
Telegram/lib_ui/ui/effects/numbers_animation.h
Normal 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
|
||||
539
Telegram/lib_ui/ui/effects/panel_animation.cpp
Normal file
539
Telegram/lib_ui/ui/effects/panel_animation.cpp
Normal 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
|
||||
143
Telegram/lib_ui/ui/effects/panel_animation.h
Normal file
143
Telegram/lib_ui/ui/effects/panel_animation.h
Normal 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
|
||||
180
Telegram/lib_ui/ui/effects/path_shift_gradient.cpp
Normal file
180
Telegram/lib_ui/ui/effects/path_shift_gradient.cpp
Normal 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
|
||||
65
Telegram/lib_ui/ui/effects/path_shift_gradient.h
Normal file
65
Telegram/lib_ui/ui/effects/path_shift_gradient.h
Normal 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
|
||||
338
Telegram/lib_ui/ui/effects/radial_animation.cpp
Normal file
338
Telegram/lib_ui/ui/effects/radial_animation.cpp
Normal 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
|
||||
123
Telegram/lib_ui/ui/effects/radial_animation.h
Normal file
123
Telegram/lib_ui/ui/effects/radial_animation.h
Normal 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
|
||||
324
Telegram/lib_ui/ui/effects/ripple_animation.cpp
Normal file
324
Telegram/lib_ui/ui/effects/ripple_animation.cpp
Normal 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
|
||||
72
Telegram/lib_ui/ui/effects/ripple_animation.h
Normal file
72
Telegram/lib_ui/ui/effects/ripple_animation.h
Normal 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
|
||||
471
Telegram/lib_ui/ui/effects/round_area_with_shadow.cpp
Normal file
471
Telegram/lib_ui/ui/effects/round_area_with_shadow.cpp
Normal 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
|
||||
92
Telegram/lib_ui/ui/effects/round_area_with_shadow.h
Normal file
92
Telegram/lib_ui/ui/effects/round_area_with_shadow.h
Normal 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
|
||||
113
Telegram/lib_ui/ui/effects/show_animation.cpp
Normal file
113
Telegram/lib_ui/ui/effects/show_animation.cpp
Normal 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
|
||||
20
Telegram/lib_ui/ui/effects/show_animation.h
Normal file
20
Telegram/lib_ui/ui/effects/show_animation.h
Normal 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
|
||||
89
Telegram/lib_ui/ui/effects/slide_animation.cpp
Normal file
89
Telegram/lib_ui/ui/effects/slide_animation.cpp
Normal 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
|
||||
66
Telegram/lib_ui/ui/effects/slide_animation.h
Normal file
66
Telegram/lib_ui/ui/effects/slide_animation.h
Normal 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
|
||||
878
Telegram/lib_ui/ui/effects/spoiler_mess.cpp
Normal file
878
Telegram/lib_ui/ui/effects/spoiler_mess.cpp
Normal 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
|
||||
117
Telegram/lib_ui/ui/effects/spoiler_mess.h
Normal file
117
Telegram/lib_ui/ui/effects/spoiler_mess.h
Normal 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
|
||||
Reference in New Issue
Block a user