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
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s

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

View File

@@ -0,0 +1,233 @@
// 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/abstract_button.h"
#include "ui/ui_utility.h"
#include "ui/integration.h"
#include <QtGui/QtEvents>
#include <rpl/filter.h>
#include <rpl/mappers.h>
namespace Ui {
AbstractButton::AbstractButton(QWidget *parent) : RpWidget(parent) {
setMouseTracking(true);
using namespace rpl::mappers;
shownValue()
| rpl::filter(_1 == false)
| rpl::on_next([this] { clearState(); }, lifetime());
}
void AbstractButton::leaveEventHook(QEvent *e) {
if (_state & StateFlag::Down) {
return;
}
setOver(false, StateChangeSource::ByHover);
return RpWidget::leaveEventHook(e);
}
void AbstractButton::enterEventHook(QEnterEvent *e) {
checkIfOver(mapFromGlobal(QCursor::pos()));
return RpWidget::enterEventHook(e);
}
void AbstractButton::setAcceptBoth(bool acceptBoth) {
_acceptBoth = acceptBoth;
}
void AbstractButton::checkIfOver(QPoint localPos) {
auto over = rect().marginsRemoved(getMargins()).contains(localPos);
setOver(over, StateChangeSource::ByHover);
}
void AbstractButton::mousePressEvent(QMouseEvent *e) {
checkIfOver(e->pos());
if (_state & StateFlag::Over) {
const auto set = setDown(
true,
StateChangeSource::ByPress,
e->modifiers(),
e->button());
if (set) {
e->accept();
}
}
}
void AbstractButton::mouseMoveEvent(QMouseEvent *e) {
if (rect().marginsRemoved(getMargins()).contains(e->pos())) {
setOver(true, StateChangeSource::ByHover);
} else {
setOver(false, StateChangeSource::ByHover);
}
}
void AbstractButton::mouseReleaseEvent(QMouseEvent *e) {
const auto set = setDown(
false,
StateChangeSource::ByPress,
e->modifiers(),
e->button());
if (set) {
e->accept();
}
}
bool AbstractButton::isSubmitEvent(not_null<QKeyEvent*> e) const {
return !e->isAutoRepeat()
&& (e->key() == Qt::Key_Space
|| e->key() == Qt::Key_Return
|| e->key() == Qt::Key_Enter);
}
void AbstractButton::keyPressEvent(QKeyEvent *e) {
if (isSubmitEvent(e)) {
setDown(
true,
StateChangeSource::ByPress,
e->modifiers(),
Qt::LeftButton);
e->accept();
} else {
RpWidget::keyPressEvent(e);
}
}
void AbstractButton::keyReleaseEvent(QKeyEvent *e) {
if (isSubmitEvent(e)) {
e->accept();
if (isDown()) {
setDown(
false,
StateChangeSource::ByPress,
e->modifiers(),
Qt::LeftButton);
clicked(e->modifiers(), Qt::LeftButton);
}
} else {
RpWidget::keyReleaseEvent(e);
}
}
void AbstractButton::clicked(
Qt::KeyboardModifiers modifiers,
Qt::MouseButton button) {
_modifiers = modifiers;
const auto weak = base::make_weak(this);
if (button == Qt::LeftButton) {
if (const auto callback = _clickedCallback) {
callback();
}
}
if (weak) {
_clicks.fire_copy(button);
}
}
void AbstractButton::setPointerCursor(bool enablePointerCursor) {
if (_enablePointerCursor != enablePointerCursor) {
_enablePointerCursor = enablePointerCursor;
updateCursor();
}
}
void AbstractButton::setOver(bool over, StateChangeSource source) {
if (over == isOver()) {
return;
}
const auto was = _state;
if (over) {
_state |= StateFlag::Over;
Integration::Instance().registerLeaveSubscription(this);
} else {
_state &= ~State(StateFlag::Over);
Integration::Instance().unregisterLeaveSubscription(this);
}
onStateChanged(was, source);
updateCursor();
update();
}
bool AbstractButton::setDown(
bool down,
StateChangeSource source,
Qt::KeyboardModifiers modifiers,
Qt::MouseButton button) {
if (down
&& !(_state & StateFlag::Down)
&& (_acceptBoth || button == Qt::LeftButton)) {
auto was = _state;
_state |= StateFlag::Down;
const auto weak = base::make_weak(this);
onStateChanged(was, source);
if (weak) {
accessibilityStateChanged({ .pressed = true });
}
return true;
} else if (!down && (_state & StateFlag::Down)) {
const auto was = _state;
_state &= ~State(StateFlag::Down);
const auto weak = base::make_weak(this);
onStateChanged(was, source);
if (weak) {
accessibilityStateChanged({ .pressed = true });
if (was & StateFlag::Over) {
clicked(modifiers, button);
} else {
setOver(false, source);
}
}
return true;
}
return false;
}
void AbstractButton::updateCursor() {
const auto pointerCursor = _enablePointerCursor && isOver();
if (_pointerCursor != pointerCursor) {
_pointerCursor = pointerCursor;
setCursor(_pointerCursor ? style::cur_pointer : style::cur_default);
}
}
void AbstractButton::setDisabled(bool disabled) {
auto was = _state;
if (disabled && !(_state & StateFlag::Disabled)) {
_state |= StateFlag::Disabled;
onStateChanged(was, StateChangeSource::ByUser);
} else if (!disabled && (_state & StateFlag::Disabled)) {
_state &= ~State(StateFlag::Disabled);
onStateChanged(was, StateChangeSource::ByUser);
}
}
void AbstractButton::clearState() {
auto was = _state;
_state = StateFlag::None;
onStateChanged(was, StateChangeSource::ByUser);
}
AccessibilityState AbstractButton::accessibilityState() const {
return { .pressed = isDown() };
}
void AbstractButton::accessibilityDoAction(const QString &name) {
if (name == QAccessibleActionInterface::pressAction()) {
if (!isDisabled()) {
clicked(Qt::NoModifier, Qt::LeftButton);
}
}
}
} // namespace Ui

View File

@@ -0,0 +1,125 @@
// 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 <rpl/event_stream.h>
#include "ui/rp_widget.h"
#include "base/flags.h"
namespace Ui {
class AbstractButton : public RpWidget {
public:
AbstractButton(QWidget *parent);
[[nodiscard]] Qt::KeyboardModifiers clickModifiers() const {
return _modifiers;
}
void setDisabled(bool disabled = true);
virtual void clearState();
[[nodiscard]] bool isOver() const {
return _state & StateFlag::Over;
}
[[nodiscard]] bool isDown() const {
return _state & StateFlag::Down;
}
[[nodiscard]] bool isDisabled() const {
return _state & StateFlag::Disabled;
}
void setSynteticOver(bool over) {
setOver(over, StateChangeSource::ByPress);
}
void setSynteticDown(bool down, Qt::MouseButton button = Qt::LeftButton) {
setDown(down, StateChangeSource::ByPress, {}, button);
}
void setPointerCursor(bool enablePointerCursor);
void setAcceptBoth(bool acceptBoth = true);
void setClickedCallback(Fn<void()> callback) {
_clickedCallback = std::move(callback);
}
rpl::producer<Qt::MouseButton> clicks() const {
return _clicks.events();
}
template <typename Handler>
void addClickHandler(Handler &&handler) {
clicks(
) | rpl::on_next(
std::forward<Handler>(handler),
lifetime());
}
void clicked(Qt::KeyboardModifiers modifiers, Qt::MouseButton button);
QAccessible::Role accessibilityRole() override {
return QAccessible::Role::Button;
}
AccessibilityState accessibilityState() const override;
void accessibilityDoAction(const QString &name) override;
protected:
void enterEventHook(QEnterEvent *e) override;
void leaveEventHook(QEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
void keyReleaseEvent(QKeyEvent *e) override;
protected:
enum class StateFlag {
None = 0,
Over = (1 << 0),
Down = (1 << 1),
Disabled = (1 << 2),
};
friend constexpr bool is_flag_type(StateFlag) { return true; };
using State = base::flags<StateFlag>;
State state() const {
return _state;
}
enum class StateChangeSource {
ByUser = 0x00,
ByPress = 0x01,
ByHover = 0x02,
};
void setOver(bool over, StateChangeSource source = StateChangeSource::ByUser);
bool setDown(
bool down,
StateChangeSource source,
Qt::KeyboardModifiers modifiers,
Qt::MouseButton button);
virtual void onStateChanged(State was, StateChangeSource source) {
}
private:
void updateCursor();
void checkIfOver(QPoint localPos);
[[nodiscard]] bool isSubmitEvent(not_null<QKeyEvent*> e) const;
State _state = StateFlag::None;
Qt::KeyboardModifiers _modifiers;
bool _enablePointerCursor = true;
bool _pointerCursor = false;
bool _acceptBoth = false;
Fn<void()> _clickedCallback;
rpl::event_stream<Qt::MouseButton> _clicks;
};
} // namespace Ui

View File

@@ -0,0 +1,27 @@
// 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/accessible/ui_accessible_factory.h"
#include "ui/rp_widget.h"
#include "base/screen_reader_state.h"
#include <QAccessibleWidget>
namespace Ui::Accessible {
namespace {
[[nodiscard]] QAccessibleInterface *Method(const QString&, QObject *object) {
const auto rpWidget = qobject_cast<Ui::RpWidget*>(object);
return rpWidget ? rpWidget->accessibilityCreate() : nullptr;
}
} // namespace
void Init() {
QAccessible::installFactory(Method);
}
} // namespace Ui::Accessible

View File

@@ -0,0 +1,13 @@
// 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::Accessible {
void Init();
} // namespace Ui::Accessible

View File

@@ -0,0 +1,131 @@
// 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/accessible/ui_accessible_widget.h"
#include "base/debug_log.h"
#include "base/integration.h"
#include "base/screen_reader_state.h"
#include "base/timer.h"
#include "ui/rp_widget.h"
namespace Ui::Accessible {
namespace {
constexpr auto kCleanupDelay = 5 * crl::time(1000);
class FocusManager final {
public:
FocusManager();
void registerWidget(not_null<RpWidget*> widget);
private:
void cleanup();
std::vector<QPointer<RpWidget>> _widgets;
base::Timer _cleanupTimer;
bool _active = false;
rpl::lifetime _lifetime;
};
FocusManager::FocusManager() : _cleanupTimer([=] { cleanup(); }) {
base::ScreenReaderState::Instance()->activeValue(
) | rpl::on_next([=](bool active) {
_active = active;
LOG(("Screen Reader: %1").arg(active ? "active" : "inactive"));
cleanup();
for (const auto &widget : _widgets) {
widget->setFocusPolicy(active ? Qt::TabFocus : Qt::NoFocus);
}
}, _lifetime);
}
void FocusManager::registerWidget(not_null<RpWidget*> widget) {
const auto role = widget->accessibilityRole();
if (role != QAccessible::Role::Button
&& role != QAccessible::Role::Link
&& role != QAccessible::Role::CheckBox
&& role != QAccessible::Role::Slider) {
return;
}
if (_active) {
widget->setFocusPolicy(Qt::TabFocus);
}
_widgets.push_back(widget.get());
if (!_cleanupTimer.isActive()) {
_cleanupTimer.callOnce(kCleanupDelay);
}
}
void FocusManager::cleanup() {
_widgets.erase(
ranges::remove_if(
_widgets,
[](const QPointer<RpWidget> &widget) { return !widget; }),
end(_widgets));
}
[[nodiscard]] FocusManager &Manager() {
static FocusManager Instance;
return Instance;
}
} // namespace
Widget::Widget(not_null<RpWidget*> widget) : QAccessibleWidget(widget) {
Manager().registerWidget(widget);
}
[[nodiscard]] not_null<RpWidget*> Widget::rp() const {
return static_cast<RpWidget*>(widget());
}
QAccessible::Role Widget::role() const {
return rp()->accessibilityRole();
}
QAccessible::State Widget::state() const {
auto result = QAccessibleWidget::state();
rp()->accessibilityState().writeTo(result);
return result;
}
QStringList Widget::actionNames() const {
return QAccessibleWidget::actionNames()
+ rp()->accessibilityActionNames();
}
void Widget::doAction(const QString &actionName) {
QAccessibleWidget::doAction(actionName);
base::Integration::Instance().enterFromEventLoop([&] {
rp()->accessibilityDoAction(actionName);
});
}
QString Widget::text(QAccessible::Text t) const {
const auto result = QAccessibleWidget::text(t);
if (!result.isEmpty()) {
return result;
}
switch (t) {
case QAccessible::Name: {
return rp()->accessibilityName();
}
case QAccessible::Description: {
return rp()->accessibilityDescription();
}
case QAccessible::Value: {
return rp()->accessibilityValue();
}
}
return result;
}
} // namespace Ui::Accessible

View File

@@ -0,0 +1,31 @@
// 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 <QAccessibleWidget>
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Ui::Accessible {
class Widget : public QAccessibleWidget {
public:
explicit Widget(not_null<RpWidget*> widget);
[[nodiscard]] not_null<RpWidget*> rp() const;
QString text(QAccessible::Text t) const override;
QAccessible::Role role() const override;
QAccessible::State state() const override;
QStringList actionNames() const override;
void doAction(const QString &actionName) override;
};
} // namespace Ui::Accessible

View File

@@ -0,0 +1,429 @@
// 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/animated_icon.h"
#include "ui/image/image_prepare.h"
#include "ui/style/style_core.h"
#include "ui/effects/frame_generator.h"
#include <QtGui/QPainter>
#include <crl/crl_async.h>
#include <crl/crl_semaphore.h>
#include <crl/crl_on_main.h>
namespace Ui {
namespace {
constexpr auto kDefaultDuration = crl::time(800);
} // namespace
struct AnimatedIcon::Frame {
FrameGenerator::Frame generated;
QImage resizedImage;
int index = 0;
};
class AnimatedIcon::Impl final : public std::enable_shared_from_this<Impl> {
public:
explicit Impl(base::weak_ptr<AnimatedIcon> weak);
void prepareFromAsync(
FnMut<std::unique_ptr<FrameGenerator>()> factory,
QSize sizeOverride);
void waitTillPrepared() const;
[[nodiscard]] bool valid() const;
[[nodiscard]] QSize size() const;
[[nodiscard]] int framesCount() const;
[[nodiscard]] double frameRate() const;
[[nodiscard]] Frame &frame();
[[nodiscard]] const Frame &frame() const;
[[nodiscard]] crl::time animationDuration() const;
void moveToFrame(int frame, QSize updatedDesiredSize);
private:
enum class PreloadState {
None,
Preloading,
Ready,
};
// Called from crl::async.
void renderPreloadFrame();
std::unique_ptr<FrameGenerator> _generator;
Frame _current;
QSize _desiredSize;
std::atomic<PreloadState> _preloadState = PreloadState::None;
Frame _preloaded; // Changed on main or async depending on _preloadState.
QSize _preloadImageSize;
base::weak_ptr<AnimatedIcon> _weak;
int _framesCount = 0;
double _frameRate = 0.;
mutable crl::semaphore _semaphore;
mutable bool _ready = false;
};
AnimatedIcon::Impl::Impl(base::weak_ptr<AnimatedIcon> weak)
: _weak(weak) {
}
void AnimatedIcon::Impl::prepareFromAsync(
FnMut<std::unique_ptr<FrameGenerator>()> factory,
QSize sizeOverride) {
const auto guard = gsl::finally([&] { _semaphore.release(); });
if (!_weak) {
return;
}
auto generator = factory ? factory() : nullptr;
if (!generator || !_weak) {
return;
}
_framesCount = generator->count();
_frameRate = generator->rate();
_current.generated = generator->renderNext(QImage(), sizeOverride);
if (_current.generated.image.isNull()) {
return;
}
_generator = std::move(generator);
_desiredSize = sizeOverride.isEmpty()
? style::ConvertScale(_current.generated.image.size())
: sizeOverride;
}
void AnimatedIcon::Impl::waitTillPrepared() const {
if (!_ready) {
_semaphore.acquire();
_ready = true;
}
}
bool AnimatedIcon::Impl::valid() const {
waitTillPrepared();
return (_generator != nullptr);
}
QSize AnimatedIcon::Impl::size() const {
waitTillPrepared();
return _desiredSize;
}
int AnimatedIcon::Impl::framesCount() const {
waitTillPrepared();
return _framesCount;
}
double AnimatedIcon::Impl::frameRate() const {
waitTillPrepared();
return _frameRate;
}
AnimatedIcon::Frame &AnimatedIcon::Impl::frame() {
waitTillPrepared();
return _current;
}
const AnimatedIcon::Frame &AnimatedIcon::Impl::frame() const {
waitTillPrepared();
return _current;
}
crl::time AnimatedIcon::Impl::animationDuration() const {
waitTillPrepared();
const auto rate = _generator ? _generator->rate() : 0.;
const auto frames = _generator ? _generator->count() : 0;
return (frames && rate >= 1.)
? crl::time(base::SafeRound(frames / rate * 1000.))
: 0;
}
void AnimatedIcon::Impl::moveToFrame(int frame, QSize updatedDesiredSize) {
waitTillPrepared();
const auto state = _preloadState.load();
const auto shown = _current.index;
if (!updatedDesiredSize.isEmpty()) {
_desiredSize = updatedDesiredSize;
}
const auto desiredImageSize = _desiredSize * style::DevicePixelRatio();
if (!_generator
|| state == PreloadState::Preloading
|| (shown == frame
&& (_current.generated.image.size() == desiredImageSize))) {
return;
} else if (state == PreloadState::Ready) {
if (_preloaded.index == frame
&& (shown != frame
|| _preloaded.generated.image.size() == desiredImageSize)) {
std::swap(_current, _preloaded);
if (_current.generated.image.size() == desiredImageSize) {
return;
}
} else if ((shown < _preloaded.index && _preloaded.index < frame)
|| (shown > _preloaded.index && _preloaded.index > frame)) {
std::swap(_current, _preloaded);
}
}
_preloadImageSize = desiredImageSize;
_preloaded.index = frame;
_preloadState = PreloadState::Preloading;
crl::async([guard = shared_from_this()] {
guard->renderPreloadFrame();
});
}
void AnimatedIcon::Impl::renderPreloadFrame() {
if (!_weak) {
return;
}
if (_preloaded.index == 0) {
_generator->jumpToStart();
}
_preloaded.generated = (_preloaded.index && _preloaded.index == _current.index)
? _generator->renderCurrent(
std::move(_preloaded.generated.image),
_preloadImageSize)
: _generator->renderNext(
std::move(_preloaded.generated.image),
_preloadImageSize);
_preloaded.resizedImage = QImage();
_preloadState = PreloadState::Ready;
crl::on_main(_weak, [=] {
_weak->frameJumpFinished();
});
}
AnimatedIcon::AnimatedIcon(AnimatedIconDescriptor &&descriptor)
: _impl(std::make_shared<Impl>(base::make_weak(this)))
, _colorized(descriptor.colorized) {
crl::async([
impl = _impl,
factory = std::move(descriptor.generator),
sizeOverride = descriptor.sizeOverride
]() mutable {
impl->prepareFromAsync(std::move(factory), sizeOverride);
});
}
void AnimatedIcon::wait() const {
_impl->waitTillPrepared();
}
bool AnimatedIcon::valid() const {
return _impl->valid();
}
int AnimatedIcon::frameIndex() const {
return _impl->frame().index;
}
int AnimatedIcon::framesCount() const {
return _impl->framesCount();
}
double AnimatedIcon::frameRate() const {
return _impl->frameRate();
}
QImage AnimatedIcon::frame(const QColor &textColor) const {
return frame(textColor, QSize(), nullptr).image;
}
QImage AnimatedIcon::notColorizedFrame() const {
return notColorizedFrame(QSize(), nullptr).image;
}
AnimatedIcon::ResizedFrame AnimatedIcon::frame(
const QColor &textColor,
QSize desiredSize,
Fn<void()> updateWithPerfect) const {
auto result = notColorizedFrame(
desiredSize,
std::move(updateWithPerfect));
if (_colorized) {
auto &image = result.image;
style::colorizeImage(image, textColor, &image, {}, {}, true);
}
return result;
}
AnimatedIcon::ResizedFrame AnimatedIcon::notColorizedFrame(
QSize desiredSize,
Fn<void()> updateWithPerfect) const {
auto &frame = _impl->frame();
preloadNextFrame(crl::now(), &frame, desiredSize);
const auto desired = size() * style::DevicePixelRatio();
if (frame.generated.image.isNull()) {
return { frame.generated.image };
} else if (frame.generated.image.size() == desired) {
return { frame.generated.image };
} else if (frame.resizedImage.size() != desired) {
frame.resizedImage = frame.generated.image.scaled(
desired,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
if (updateWithPerfect) {
_repaint = std::move(updateWithPerfect);
}
return { frame.resizedImage, true };
}
int AnimatedIcon::width() const {
return size().width();
}
int AnimatedIcon::height() const {
return size().height();
}
QSize AnimatedIcon::size() const {
return _impl->size();
}
void AnimatedIcon::paint(QPainter &p, int x, int y) {
auto &frame = _impl->frame();
preloadNextFrame(crl::now(), &frame);
if (frame.generated.image.isNull()) {
return;
}
const auto rect = QRect{ QPoint(x, y), size() };
p.drawImage(rect, frame.generated.image);
}
void AnimatedIcon::paintInCenter(QPainter &p, QRect rect) {
const auto my = size();
paint(
p,
rect.x() + (rect.width() - my.width()) / 2,
rect.y() + (rect.height() - my.height()) / 2);
}
void AnimatedIcon::animate(Fn<void()> update) {
if (effectiveFramesCount() != 1 && !anim::Disabled()) {
_repaint = std::move(update);
_animation.stop();
_animationCurrentIndex = (_customStartFrame >= 0)
? _customStartFrame
: 0;
_impl->moveToFrame(_animationCurrentIndex, QSize());
_animationDuration = _impl->animationDuration();
if (_customEndFrame >= 0) {
const auto rate = frameRate();
if (rate > 0) {
_animationDuration = crl::time(base::SafeRound(
effectiveFramesCount() / rate * 1000.));
}
}
_animationCurrentStart = _animationStarted = crl::now();
continueAnimation(_animationCurrentStart);
}
}
void AnimatedIcon::continueAnimation(crl::time now) {
const auto callback = [=](float64 value) {
if (anim::Disabled()) {
return;
}
const auto elapsed = int(value);
const auto now = _animationStartTime + elapsed;
if (!_animationDuration && elapsed > kDefaultDuration / 2) {
auto animation = std::move(_animation);
continueAnimation(now);
}
preloadNextFrame(now);
if (_repaint) _repaint();
};
const auto duration = _animationDuration
? _animationDuration
: kDefaultDuration;
_animationStartTime = now;
_animation.start(callback, 0., 1. * duration, duration);
}
void AnimatedIcon::jumpToStart(Fn<void()> update) {
_repaint = std::move(update);
_animation.stop();
_animationCurrentIndex = 0;
_impl->moveToFrame(0, QSize());
}
void AnimatedIcon::setCustomEndFrame(int frame) {
_customEndFrame = (frame >= 0 && frame < framesCount()) ? frame : -1;
}
void AnimatedIcon::setCustomStartFrame(int frame) {
_customStartFrame = (frame >= 0 && frame < framesCount()) ? frame : -1;
}
int AnimatedIcon::effectiveFramesCount() const {
const auto total = framesCount();
return (_customEndFrame >= 0 && _customEndFrame < total)
? (_customEndFrame + 1)
: total;
}
void AnimatedIcon::frameJumpFinished() {
if (_repaint && !animating()) {
_repaint();
_repaint = nullptr;
}
}
int AnimatedIcon::wantedFrameIndex(
crl::time now,
const Frame *resolvedCurrent) const {
const auto frame = resolvedCurrent ? resolvedCurrent : &_impl->frame();
if (frame->index == _animationCurrentIndex + 1) {
++_animationCurrentIndex;
_animationCurrentStart = _animationNextStart;
}
if (!_animation.animating()) {
return _animationCurrentIndex;
}
if (frame->index == _animationCurrentIndex) {
const auto duration = frame->generated.duration;
const auto next = _animationCurrentStart + duration;
const auto effectiveCount = effectiveFramesCount();
const auto isLastFrame = (_animationCurrentIndex >= effectiveCount - 1);
if (isLastFrame) {
_animation.stop();
if (_repaint) _repaint();
return _animationCurrentIndex;
} else if (now < next) {
return _animationCurrentIndex;
}
_animationNextStart = next;
return _animationCurrentIndex + 1;
}
Assert(!_animationCurrentIndex);
return 0;
}
void AnimatedIcon::preloadNextFrame(
crl::time now,
const Frame *resolvedCurrent,
QSize updatedDesiredSize) const {
_impl->moveToFrame(
wantedFrameIndex(now, resolvedCurrent),
updatedDesiredSize);
}
bool AnimatedIcon::animating() const {
return _animation.animating();
}
std::unique_ptr<AnimatedIcon> MakeAnimatedIcon(
AnimatedIconDescriptor &&descriptor) {
return std::make_unique<AnimatedIcon>(std::move(descriptor));
}
} // namespace Lottie

View File

@@ -0,0 +1,102 @@
// 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 "ui/effects/animations.h"
#include "base/weak_ptr.h"
#include <crl/crl_time.h>
#include <QtCore/QByteArray>
#include <optional>
namespace Ui {
class FrameGenerator;
struct AnimatedIconDescriptor {
FnMut<std::unique_ptr<FrameGenerator>()> generator;
QSize sizeOverride;
bool colorized = false;
};
class AnimatedIcon final : public base::has_weak_ptr {
public:
explicit AnimatedIcon(AnimatedIconDescriptor &&descriptor);
AnimatedIcon(const AnimatedIcon &other) = delete;
AnimatedIcon &operator=(const AnimatedIcon &other) = delete;
AnimatedIcon(AnimatedIcon &&other) = delete; // _animation captures this.
AnimatedIcon &operator=(AnimatedIcon &&other) = delete;
[[nodiscard]] bool valid() const;
[[nodiscard]] int frameIndex() const;
[[nodiscard]] int framesCount() const;
[[nodiscard]] double frameRate() const;
[[nodiscard]] QImage frame(const QColor &textColor) const;
[[nodiscard]] QImage notColorizedFrame() const;
[[nodiscard]] int width() const;
[[nodiscard]] int height() const;
[[nodiscard]] QSize size() const;
struct ResizedFrame {
QImage image;
bool scaled = false;
};
[[nodiscard]] ResizedFrame frame(
const QColor &textColor,
QSize desiredSize,
Fn<void()> updateWithPerfect) const;
[[nodiscard]] ResizedFrame notColorizedFrame(
QSize desiredSize,
Fn<void()> updateWithPerfect) const;
void animate(Fn<void()> update);
void jumpToStart(Fn<void()> update);
void setCustomEndFrame(int frame);
void setCustomStartFrame(int frame);
void paint(QPainter &p, int x, int y);
void paintInCenter(QPainter &p, QRect rect);
[[nodiscard]] bool animating() const;
private:
struct Frame;
class Impl;
friend class Impl;
void wait() const;
[[nodiscard]] int wantedFrameIndex(
crl::time now,
const Frame *resolvedCurrent = nullptr) const;
void preloadNextFrame(
crl::time now,
const Frame *resolvedCurrent = nullptr,
QSize updatedDesiredSize = QSize()) const;
void frameJumpFinished();
void continueAnimation(crl::time now);
[[nodiscard]] int effectiveFramesCount() const;
std::shared_ptr<Impl> _impl;
crl::time _animationStartTime = 0;
crl::time _animationStarted = 0;
mutable Animations::Simple _animation;
mutable Fn<void()> _repaint;
mutable crl::time _animationDuration = 0;
mutable crl::time _animationCurrentStart = 0;
mutable crl::time _animationNextStart = 0;
mutable int _animationCurrentIndex = 0;
bool _colorized = false;
int _customEndFrame = -1;
int _customStartFrame = -1;
};
[[nodiscard]] std::unique_ptr<AnimatedIcon> MakeAnimatedIcon(
AnimatedIconDescriptor &&descriptor);
} // namespace Ui

View File

@@ -0,0 +1,18 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace arc {
constexpr auto kFullLength = 360 * 16;
constexpr auto kHalfLength = (kFullLength / 2);
constexpr auto kQuarterLength = (kFullLength / 4);
constexpr auto kMinLength = (kFullLength / 360);
constexpr auto kAlmostFullLength = (kFullLength - kMinLength);
} // namespace arc

View File

@@ -0,0 +1,135 @@
// 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
//
using "ui/colors.palette";
TextPalette {
linkFg: color;
monoFg: color;
spoilerFg: color;
selectBg: color;
selectFg: color;
selectLinkFg: color;
selectMonoFg: color;
selectSpoilerFg: color;
selectOverlay: color;
linkAlwaysActive: bool;
}
QuoteStyle {
padding: margins;
verticalSkip: pixels;
header: pixels;
headerPosition: point;
icon: icon;
iconPosition: point;
expand: icon;
expandPosition: point;
collapse: icon;
collapsePosition: point;
outline: pixels;
outlineShift: pixels;
radius: pixels;
scrollable: bool;
}
TextStyle {
font: font;
linkUnderline: int;
lineHeight: pixels;
blockquote: QuoteStyle;
pre: QuoteStyle;
}
kLinkUnderlineNever: 0;
kLinkUnderlineActive: 1;
kLinkUnderlineAlways: 2;
fsize: 13px;
normalFont: font(fsize);
semiboldFont: font(fsize semibold);
boxFontSize: 14px;
boxTextFont: font(boxFontSize);
emojiSize: 18px;
emojiPadding: 1px;
lineWidth: 1px;
IconEmoji {
icon: icon;
padding: margins;
useIconColor: bool;
}
defaultTextPalette: TextPalette {
linkFg: windowActiveTextFg;
monoFg: msgInMonoFg;
spoilerFg: msgInDateFg;
selectBg: msgInBgSelected;
selectFg: transparent; // use painter current pen instead
selectLinkFg: historyLinkInFgSelected;
selectMonoFg: msgInMonoFgSelected;
selectSpoilerFg: msgInDateFgSelected;
selectOverlay: msgSelectOverlay;
}
defaultQuoteStyle: QuoteStyle {
}
defaultTextStyle: TextStyle {
font: normalFont;
linkUnderline: kLinkUnderlineActive;
lineHeight: 0px;
blockquote: defaultQuoteStyle;
pre: defaultQuoteStyle;
}
semiboldTextStyle: TextStyle(defaultTextStyle) {
font: semiboldFont;
}
slideDuration: 240;
slideShift: 100px;
slideShadow: icon {{ "slide_shadow", slideFadeOutShadowFg }};
slideWrapDuration: 150;
fadeWrapDuration: 200;
linkCropLimit: 360px;
linkFont: normalFont;
linkFontOver: font(fsize underline);
roundRadiusLarge: 6px;
roundRadiusSmall: 3px;
dateRadius: roundRadiusLarge;
noContactsHeight: 100px;
noContactsFont: font(fsize);
noContactsColor: windowSubTextFg;
activeFadeInDuration: 500;
activeFadeOutDuration: 3000;
smallCloseIcon: icon {{ "simple_close", smallCloseIconFg }};
smallCloseIconOver: icon {{ "simple_close", smallCloseIconFgOver }};
radialSize: size(50px, 50px);
radialLine: 3px;
radialDuration: 350;
radialPeriod: 3000;
locationSize: size(320px, 240px);
transparentPlaceholderSize: 4px;
defaultVerticalListSkip: 6px;
shakeShift: 4px;
shakeDuration: 300;
universalDuration: 120;
// floating badge colors
roundedFg: radialFg;
roundedBg: radialBg; // closest to #00000066

View File

@@ -0,0 +1,134 @@
// 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/basic_click_handlers.h"
#include "ui/widgets/tooltip.h"
#include "ui/text/text_entity.h"
#include "ui/integration.h"
#include "base/qthelp_url.h"
#include "base/qt/qt_string_view.h"
#include <QtCore/QUrl>
#include <QtCore/QRegularExpression>
#include <QtGui/QDesktopServices>
#include <QtGui/QGuiApplication>
QString TextClickHandler::readable() const {
const auto result = url();
return !result.startsWith(qstr("internal:"))
? result
: result.startsWith(qstr("internal:url:"))
? result.mid(qstr("internal:url:").size())
: QString();
}
UrlClickHandler::UrlClickHandler(const QString &url, bool fullDisplayed)
: TextClickHandler(fullDisplayed)
, _originalUrl(url) {
if (isEmail()) {
_readable = _originalUrl;
} else if (!_originalUrl.startsWith(qstr("internal:"))) {
const auto original = QUrl(_originalUrl);
const auto good = QUrl(original.isValid()
? original.toEncoded()
: QString());
_readable = good.isValid() ? good.toDisplayString() : _originalUrl;
} else if (_originalUrl.startsWith(qstr("internal:url:"))) {
const auto external = _originalUrl.mid(qstr("internal:url:").size());
const auto original = QUrl(external);
const auto good = QUrl(original.isValid()
? original.toEncoded()
: QString());
_readable = good.isValid() ? good.toDisplayString() : external;
}
}
QString UrlClickHandler::copyToClipboardContextItemText() const {
return isEmail()
? Ui::Integration::Instance().phraseContextCopyEmail()
: Ui::Integration::Instance().phraseContextCopyLink();
}
QString UrlClickHandler::EncodeForOpening(const QString &originalUrl) {
if (IsEmail(originalUrl)) {
return originalUrl;
}
static const auto TonExp = QRegularExpression(u"^[^/@:]+\\.ton($|/)"_q);
if (TonExp.match(originalUrl.toLower()).hasMatch()) {
return u"tonsite://"_q + originalUrl;
}
const auto u = QUrl(originalUrl);
const auto good = QUrl(u.isValid() ? u.toEncoded() : QString());
const auto result = good.isValid()
? QString::fromUtf8(good.toEncoded())
: originalUrl;
static const auto RegExp = QRegularExpression(u"^[a-zA-Z]+:"_q);
if (!result.isEmpty()
&& !RegExp.match(result).hasMatch()) {
// No protocol.
return u"https://"_q + result;
}
return result;
}
void UrlClickHandler::Open(QString url, QVariant context) {
Ui::Tooltip::Hide();
if (!Ui::Integration::Instance().handleUrlClick(url, context)
&& !url.isEmpty()) {
if (IsEmail(url)) {
url = "mailto: " + url;
}
QDesktopServices::openUrl(url);
}
}
bool UrlClickHandler::IsSuspicious(const QString &url) {
static const auto Check1 = QRegularExpression(
"^((https?|s?ftp)://)?([^/#\\:\\?]+)([/#\\:\\?]|$)",
QRegularExpression::CaseInsensitiveOption);
const auto match1 = Check1.match(url);
if (!match1.hasMatch()) {
return false;
}
const auto domain = match1.capturedView(3);
static const auto Check2 = QRegularExpression("^(.*)\\.[a-zA-Z]+$");
const auto match2 = Check2.match(domain);
if (!match2.hasMatch()) {
return false;
}
const auto part = match2.capturedView(1);
static const auto Check3 = QRegularExpression("[^a-zA-Z0-9\\.\\-]");
return Check3.match(part).hasMatch();
}
QString UrlClickHandler::ShowEncoded(const QString &url) {
if (const auto u = QUrl(url); u.isValid()) {
return QString::fromUtf8(u.toEncoded());
}
static const auto Check1 = QRegularExpression(
"^(https?://)?([^/#\\:]+)([/#\\:]|$)",
QRegularExpression::CaseInsensitiveOption);
if (const auto match1 = Check1.match(url); match1.hasMatch()) {
const auto domain = match1.captured(1).append(match1.capturedView(2));
if (const auto u = QUrl(domain); u.isValid()) {
return QString(
).append(QString::fromUtf8(u.toEncoded())
).append(base::StringViewMid(url, match1.capturedEnd(2)));
}
}
return url;
}
auto UrlClickHandler::getTextEntity() const -> TextEntity {
const auto type = isEmail() ? EntityType::Email : EntityType::Url;
return { type, _originalUrl };
}

View File

@@ -0,0 +1,84 @@
// 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/algorithm.h"
#include "ui/click_handler.h"
class TextClickHandler : public ClickHandler {
public:
TextClickHandler(bool fullDisplayed = true)
: _fullDisplayed(fullDisplayed) {
}
QString copyToClipboardText() const override {
return url();
}
QString tooltip() const override {
return _fullDisplayed ? QString() : readable();
}
void setFullDisplayed(bool full) {
_fullDisplayed = full;
}
protected:
virtual QString readable() const;
bool _fullDisplayed;
};
class UrlClickHandler : public TextClickHandler {
public:
UrlClickHandler(const QString &url, bool fullDisplayed = true);
[[nodiscard]] QString originalUrl() const {
return _originalUrl;
}
QString copyToClipboardContextItemText() const override;
QString dragText() const override {
return url();
}
TextEntity getTextEntity() const override;
static void Open(QString url, QVariant context = {});
void onClick(ClickContext context) const override {
const auto button = context.button;
if (button == Qt::LeftButton || button == Qt::MiddleButton) {
Open(url(), context.other);
}
}
[[nodiscard]] static bool IsEmail(const QString &url) {
const auto at = url.indexOf('@'), slash = url.indexOf('/');
return ((at > 0) && (slash < 0 || slash > at));
}
[[nodiscard]] static QString EncodeForOpening(const QString &originalUrl);
[[nodiscard]] static bool IsSuspicious(const QString &url);
[[nodiscard]] static QString ShowEncoded(const QString &url);
protected:
QString url() const override {
return EncodeForOpening(_originalUrl);
}
QString readable() const override {
return _readable;
}
private:
[[nodiscard]] bool isEmail() const {
return IsEmail(_originalUrl);
}
QString _originalUrl, _readable;
};

View File

@@ -0,0 +1,71 @@
// 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/cached_special_layer_shadow_corners.h"
#include "ui/effects/ripple_animation.h"
#include "styles/style_layers.h"
namespace Ui {
namespace {
[[nodiscard]] std::array<QImage, 4> PrepareSpecialLayerShadowCorners() {
const auto &st = st::boxRoundShadow;
const auto s = QSize(
st::boxRadius * 2 + st.extend.left(),
st::boxRadius * 2 + st.extend.right());
const auto mask = Ui::RippleAnimation::MaskByDrawer(s, false, [&](
QPainter &p) {
p.drawRoundedRect(QRect(QPoint(), s), st::boxRadius, st::boxRadius);
});
struct Corner {
const style::icon &icon;
QPoint factor;
};
const auto corners = std::vector<Corner>{
Corner{ st.topLeft, QPoint(1, 1) },
Corner{ st.bottomLeft, QPoint(1, 0) },
Corner{ st.topRight, QPoint(0, 1) },
Corner{ st.bottomRight, QPoint(0, 0) },
};
const auto processCorner = [&](int i) {
const auto &corner = corners[i];
auto result = QImage(
corner.icon.size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(style::DevicePixelRatio());
result.fill(Qt::transparent);
{
QPainter p(&result);
corner.icon.paint(p, 0, 0, corner.icon.width());
p.setCompositionMode(QPainter::CompositionMode_DestinationOut);
p.drawImage(
corner.icon.width() * corner.factor.x()
- mask.width() / style::DevicePixelRatio() / 2,
corner.icon.height() * corner.factor.y()
- mask.height() / style::DevicePixelRatio() / 2,
mask);
}
return result;
};
return std::array<QImage, 4>{ {
processCorner(0),
processCorner(1),
processCorner(2),
processCorner(3),
} };
}
} // namespace
const std::array<QImage, 4> &SpecialLayerShadowCorners() {
static const auto custom = PrepareSpecialLayerShadowCorners();
return custom;
}
} // namespace Ui

View File

@@ -0,0 +1,13 @@
// 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 {
[[nodiscard]] const std::array<QImage, 4> &SpecialLayerShadowCorners();
} // namespace Ui

View File

@@ -0,0 +1,186 @@
// 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/click_handler.h"
#include "base/algorithm.h"
#include "ui/text/text_entity.h"
#include "ui/integration.h"
#include <QtCore/QPointer>
namespace {
ClickHandlerPtr &ClickHandlerActive() {
static auto result = ClickHandlerPtr();
return result;
}
ClickHandlerPtr &ClickHandlerPressed() {
static auto result = ClickHandlerPtr();
return result;
}
} // namespace
ClickHandlerHost *ClickHandler::_activeHost = nullptr;
ClickHandlerHost *ClickHandler::_pressedHost = nullptr;
ClickHandlerHost::~ClickHandlerHost() {
ClickHandler::hostDestroyed(this);
}
bool ClickHandler::setActive(
const ClickHandlerPtr &p,
ClickHandlerHost *host) {
auto &active = ClickHandlerActive();
auto &pressed = ClickHandlerPressed();
if (active == p) {
return false;
}
// emit clickHandlerActiveChanged only when there is no
// other pressed click handler currently, if there is
// this method will be called when it is unpressed
if (active) {
const auto emitClickHandlerActiveChanged = false
|| !pressed
|| (pressed == active);
const auto wasactive = base::take(active);
if (_activeHost) {
if (emitClickHandlerActiveChanged) {
_activeHost->clickHandlerActiveChanged(wasactive, false);
}
_activeHost = nullptr;
}
}
if (p) {
active = p;
if ((_activeHost = host)) {
bool emitClickHandlerActiveChanged = (!pressed || pressed == active);
if (emitClickHandlerActiveChanged) {
_activeHost->clickHandlerActiveChanged(active, true);
}
}
}
return true;
}
bool ClickHandler::clearActive(ClickHandlerHost *host) {
if (host && _activeHost != host) {
return false;
}
return setActive(ClickHandlerPtr(), host);
}
void ClickHandler::pressed() {
auto &active = ClickHandlerActive();
auto &pressed = ClickHandlerPressed();
unpressed();
if (!active) {
return;
}
pressed = active;
if ((_pressedHost = _activeHost)) {
_pressedHost->clickHandlerPressedChanged(pressed, true);
}
}
ClickHandlerPtr ClickHandler::unpressed() {
auto &active = ClickHandlerActive();
auto &pressed = ClickHandlerPressed();
if (pressed) {
const auto activated = (active == pressed);
const auto waspressed = base::take(pressed);
if (_pressedHost) {
_pressedHost->clickHandlerPressedChanged(waspressed, false);
_pressedHost = nullptr;
}
if (activated) {
return active;
} else if (active && _activeHost) {
// emit clickHandlerActiveChanged for current active
// click handler, which we didn't emit while we has
// a pressed click handler
_activeHost->clickHandlerActiveChanged(active, true);
}
}
return ClickHandlerPtr();
}
ClickHandlerPtr ClickHandler::getActive() {
return ClickHandlerActive();
}
ClickHandlerPtr ClickHandler::getPressed() {
return ClickHandlerPressed();
}
bool ClickHandler::showAsActive(const ClickHandlerPtr &p) {
auto &active = ClickHandlerActive();
auto &pressed = ClickHandlerPressed();
return p && (p == active) && (!pressed || (p == pressed));
}
bool ClickHandler::showAsPressed(const ClickHandlerPtr &p) {
auto &active = ClickHandlerActive();
auto &pressed = ClickHandlerPressed();
return p && (p == active) && (p == pressed);
}
void ClickHandler::hostDestroyed(ClickHandlerHost *host) {
auto &active = ClickHandlerActive();
auto &pressed = ClickHandlerPressed();
if (_activeHost == host) {
active = nullptr;
_activeHost = nullptr;
}
if (_pressedHost == host) {
pressed = nullptr;
_pressedHost = nullptr;
}
}
auto ClickHandler::getTextEntity() const -> TextEntity {
return { EntityType::Invalid };
}
void ClickHandler::setProperty(int id, QVariant value) {
_properties[id] = std::move(value);
}
const QVariant &ClickHandler::property(int id) const {
static const QVariant kEmpty;
const auto i = _properties.find(id);
return (i != end(_properties)) ? i->second : kEmpty;
}
void ActivateClickHandler(
not_null<QWidget*> guard,
ClickHandlerPtr handler,
ClickContext context) {
crl::on_main(guard, [=, weak = std::weak_ptr<ClickHandler>(handler)] {
if (const auto strong = weak.lock()) {
// if (Ui::Integration::Instance().allowClickHandlerActivation(strong, context)) {
strong->onClick(context);
// }
}
});
}
void ActivateClickHandler(
not_null<QWidget*> guard,
ClickHandlerPtr handler,
Qt::MouseButton button) {
ActivateClickHandler(guard, handler, ClickContext{ button });
}

View File

@@ -0,0 +1,155 @@
// 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/basic_types.h"
#include "base/flat_map.h"
#include <QtCore/QVariant>
class ClickHandler;
using ClickHandlerPtr = std::shared_ptr<ClickHandler>;
struct ClickContext {
Qt::MouseButton button = Qt::LeftButton;
QVariant other;
};
class ClickHandlerHost {
protected:
virtual void clickHandlerActiveChanged(const ClickHandlerPtr &action, bool active) {
}
virtual void clickHandlerPressedChanged(const ClickHandlerPtr &action, bool pressed) {
}
virtual ~ClickHandlerHost() = 0;
friend class ClickHandler;
};
enum class EntityType : uchar;
class ClickHandler {
public:
virtual ~ClickHandler() {
}
virtual void onClick(ClickContext context) const = 0;
// Some sort of `id`, for text links contains urls.
virtual QString url() const {
return QString();
}
// What text to show in a tooltip when mouse is over that click handler as a link in Text.
virtual QString tooltip() const {
return QString();
}
// What to drop in the input fields when dragging that click handler as a link from Text.
virtual QString dragText() const {
return QString();
}
// Copy to clipboard support.
virtual QString copyToClipboardText() const {
return QString();
}
virtual QString copyToClipboardContextItemText() const {
return QString();
}
// Entities in text support.
struct TextEntity {
EntityType type = EntityType();
QString data;
};
virtual TextEntity getTextEntity() const;
void setProperty(int id, QVariant value);
[[nodiscard]] const QVariant &property(int id) const;
// This method should be called on mouse over a click handler.
// It returns true if the active handler was changed or false otherwise.
static bool setActive(const ClickHandlerPtr &p, ClickHandlerHost *host = nullptr);
// This method should be called when mouse leaves the host.
// It returns true if the active handler was changed or false otherwise.
static bool clearActive(ClickHandlerHost *host = nullptr);
// This method should be called on mouse press event.
static void pressed();
// This method should be called on mouse release event.
// The activated click handler (if any) is returned.
static ClickHandlerPtr unpressed();
static ClickHandlerPtr getActive();
static ClickHandlerPtr getPressed();
static bool showAsActive(const ClickHandlerPtr &p);
static bool showAsPressed(const ClickHandlerPtr &p);
static void hostDestroyed(ClickHandlerHost *host);
private:
static ClickHandlerHost *_activeHost;
static ClickHandlerHost *_pressedHost;
base::flat_map<int, QVariant> _properties;
};
class LeftButtonClickHandler : public ClickHandler {
public:
void onClick(ClickContext context) const override final {
if (context.button == Qt::LeftButton) {
onClickImpl();
}
}
protected:
virtual void onClickImpl() const = 0;
};
class GenericClickHandler : public ClickHandler {
public:
GenericClickHandler(Fn<void()> handler)
: _handler([handler = std::move(handler)](ClickContext) { handler(); }) {
}
GenericClickHandler(Fn<void(ClickContext)> handler)
: _handler(std::move(handler)) {
}
void onClick(ClickContext context) const override {
if (_handler) {
_handler(context);
}
}
private:
Fn<void(ClickContext)> _handler;
};
class LambdaClickHandler : public GenericClickHandler {
public:
using GenericClickHandler::GenericClickHandler;
void onClick(ClickContext context) const override final {
if (context.button == Qt::LeftButton) {
GenericClickHandler::onClick(std::move(context));
}
}
};
void ActivateClickHandler(
not_null<QWidget*> guard,
ClickHandlerPtr handler,
ClickContext context);
void ActivateClickHandler(
not_null<QWidget*> guard,
ClickHandlerPtr handler,
Qt::MouseButton button);

View File

@@ -0,0 +1,678 @@
// 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
//
// basic
windowBg: #ffffff; // white: fallback for background
windowFg: #000000; // black: fallback for text
windowBgOver: #f1f1f1; // light gray: fallback for background with mouse over
windowBgRipple: #e5e5e5; // darker gray: fallback for ripple effect
windowFgOver: windowFg; // black: fallback for text with mouse over
windowSubTextFg: #999999; // gray: fallback for additional text
windowSubTextFgOver: #919191; // darker gray: fallback for additional text with mouse over
windowBoldFg: #222222; // dark gray: fallback for bold text
windowBoldFgOver: #222222; // dark gray: fallback for bold text with mouse over
windowBgActive: #40a7e3; // bright blue: fallback for blue filled active areas
windowFgActive: #ffffff; // white: fallback for text on active areas
windowActiveTextFg: #168acd; // online blue: fallback for active text like online status
windowShadowFg: #000000; // black: fallback for shadow
windowShadowFgFallback: #f1f1f1; // gray: fallback for shadow without opacity
shadowFg: #00000018; // most shadows (including opacity)
slideFadeOutBg: #0000003c; // slide animation (chat to profile) fade out filling
slideFadeOutShadowFg: windowShadowFg; // slide animation (chat to profile) fade out right section shadow
imageBg: #000000; // image background fallback (when photo size is less than minimum allowed)
imageBgTransparent: #ffffff; // image background when displaying an image with opacity where no opacity is needed
// widgets
activeButtonBg: windowBgActive; // default active button background
activeButtonBgOver: #39a5db; // default active button background with mouse over
activeButtonBgRipple: #2095d0; // default active button ripple effect
activeButtonFg: windowFgActive; // default active button text
activeButtonFgOver: activeButtonFg; // default active button text with mouse over
activeButtonSecondaryFg: #cceeff; // default active button additional text (selected messages counter in forward / delete buttons)
activeButtonSecondaryFgOver: activeButtonSecondaryFg; // default active button additional text with mouse over
activeLineFg: #37a1de; // default active line (like code input field bottom border when you log in and field is focused)
activeLineFgError: #e48383; // default active line for error state (like code input field bottom border when you log in and you've entered incorrect code)
lightButtonBg: windowBg; // default light button background (like buttons in boxes)
lightButtonBgOver: #e3f1fa; // default light button background with mouse over
lightButtonBgRipple: #c9e4f6; // default light button ripple effect
lightButtonFg: windowActiveTextFg; // default light button text
lightButtonFgOver: lightButtonFg; // default light button text with mouse over
attentionButtonFg: #d14e4e; // default attention button text (like confirm button on log out)
attentionButtonFgOver: #d14e4e; // default attention button text with mouse over
attentionButtonBgOver: #fcdfde; // default attention button background with mouse over
attentionButtonBgRipple: #f4c3c2; // default attention button ripple effect
menuBg: windowBg; // default popup menu background
menuBgOver: windowBgOver; // default popup menu item background with mouse over
menuBgRipple: windowBgRipple; // default popup menu item ripple effect
menuIconFg: #999999; // default popup menu item icon (like main menu)
menuIconFgOver: #8a8a8a; // default popup menu item icon with mouse over
menuSubmenuArrowFg: #373737; // default popup menu submenu arrow icon (like in message field context menu in case of RTL system language)
menuFgDisabled: #cccccc; // default popup menu item disabled text (like unavailable items in message field context menu)
menuSeparatorFg: #f1f1f1; // default popup menu separator (like in message field context menu)
scrollBarBg: #00000053; // default scroll bar current rectangle, the bar itself (like in chats list)
scrollBarBgOver: #0000007a; // default scroll bar current rectangle with mouse over it
scrollBg: #0000001a; // default scroll bar background
scrollBgOver: #0000002c; // default scroll bar background with mouse over the scroll bar
smallCloseIconFg: #c7c7c7; // small X icon (like in Show all sessions box to the right for sessions termination)
smallCloseIconFgOver: #a3a3a3; // small X icon with mouse over
radialFg: windowFgActive; // default radial loader line (like in Media Viewer when loading a photo)
radialBg: #00000056; // default radial loader background (like in Media Viewer when loading a photo)
placeholderFg: windowSubTextFg; // default input field placeholder when field is not focused (like in phone input field when you log in)
placeholderFgActive: #aaaaaa; // default input field placeholder when field is focused
inputBorderFg: #e0e0e0; // default input field bottom border (like in code input field when you log in and field is not focused)
filterInputBorderFg: #54c3f3; // default rounded input field border (like in chats list search field when field is focused)
filterInputActiveBg: windowBg; // default rounded input field active background (like in chats list search field when field is focused)
filterInputInactiveBg: windowBgOver; // default rounded input field inactive background (like in chats list search field when field is inactive)
checkboxFg: #b3b3b3; // default unchecked checkbox rounded rectangle
botKbBg: menuBgOver; // bot keyboard button background
botKbDownBg: menuBgRipple; // bot keyboard button ripple effect
botKbColor: windowBoldFgOver; // bot keyboard button text
sliderBgInactive: #e1eaef; // default slider not active bar (like in Settings when you choose interface scale or custom notifications count)
sliderBgActive: windowBgActive; // default slider active bar (like in Settings when you choose interface scale or custom notifications count)
tooltipBg: #eef2f5; // tooltip background (like when you put mouse over the message timestamp and wait)
tooltipFg: #5d6c80; // tooltip text
tooltipBorderFg: #c9d1db; // tooltip border
// custom title bar
titleShadow: #00000003; // one pixel line shadow at the bottom of custom window title
titleBg: windowBgOver; // custom window title background when window is inactive
titleBgActive: titleBg; // custom window title background when window is active
titleButtonBg: titleBg; // custom window title minimize/maximize/restore button background when window is inactive (Windows only)
titleButtonFg: #ababab; // custom window title minimize/maximize/restore button icon when window is inactive (Windows only)
titleButtonBgOver: #e5e5e5; // custom window title minimize/maximize/restore button background with mouse over when window is inactive (Windows only)
titleButtonFgOver: #9a9a9a; // custom window title minimize/maximize/restore button icon with mouse over when window is inactive (Windows only)
titleButtonBgActive: titleButtonBg; // custom window title minimize/maximize/restore button background when window is active (Windows only)
titleButtonFgActive: titleButtonFg; // custom window title minimize/maximize/restore button icon when window is active (Windows only)
titleButtonBgActiveOver: titleButtonBgOver; // custom window title minimize/maximize/restore button background with mouse over when window is active (Windows only)
titleButtonFgActiveOver: titleButtonFgOver; // custom window title minimize/maximize/restore button icon with mouse over when window is active (Windows only)
titleButtonCloseBg: titleButtonBg; // custom window title close button background when window is inactive (Windows only)
titleButtonCloseFg: titleButtonFg; // custom window title close button icon when window is inactive (Windows only)
titleButtonCloseBgOver: #e81123; // custom window title close button background with mouse over when window is inactive (Windows only)
titleButtonCloseFgOver: windowFgActive; // custom window title close button icon with mouse over when window is inactive (Windows only)
titleButtonCloseBgActive: titleButtonCloseBg; // custom window title close button background when window is active (Windows only)
titleButtonCloseFgActive: titleButtonCloseFg; // custom window title close button icon when window is active (Windows only)
titleButtonCloseBgActiveOver: titleButtonCloseBgOver; // custom window title close button background with mouse over when window is active (Windows only)
titleButtonCloseFgActiveOver: titleButtonCloseFgOver; // custom window title close button icon with mouse over when window is active (Windows only)
titleFg: #acacac; // custom window title text when window is inactive (Windows 11 and macOS)
titleFgActive: #3e3c3e; // custom window title text when window is active (Windows 11 and macOS)
// tray icon
trayCounterBg: #f23c34; // tray icon counter background
trayCounterBgMute: #888888; // tray icon counter background if all unread messages are muted
trayCounterFg: #ffffff; // tray icon counter text
trayCounterBgMacInvert: #ffffff; // tray icon counter background when tray icon is pressed or when dark theme of macOS is used (macOS only)
trayCounterFgMacInvert: #ffffff01; // tray icon counter text when tray icon is pressed or when dark theme of macOS is used (macOS only)
// layers
layerBg: #0000007f; // box and main menu background layer fade
cancelIconFg: menuIconFg; // default for settings close icon and box search cancel icon
cancelIconFgOver: menuIconFgOver; // default for settings close icon and box search cancel icon with mouse over
// boxes
boxBg: windowBg; // box background
boxTextFg: windowFg; // box text
boxTextFgGood: #4ab44a; // accepted box text (like when choosing username that is not occupied)
boxTextFgError: #d84d4d; // rejecting box text (like when choosing username that is occupied)
boxTitleFg: #404040; // box title text
boxSearchBg: boxBg; // box search field background (like in contacts box)
boxTitleAdditionalFg: #808080; // box title additional text (like in create group box when you see chosen members count)
boxTitleCloseFg: cancelIconFg; // settings close icon and box search cancel icon (like in contacts box)
boxTitleCloseFgOver: cancelIconFgOver; // settings close icon and box search cancel icon (like in contacts box) with mouse over
boxDividerBg: windowBgOver; // gray divider in boxes and layers
boxDividerFg: windowShadowFg; // gray divider shadow in boxes and layers
paymentsTipActive: #01ad0f; // tip button text in payments checkout form
membersAboutLimitFg: windowSubTextFgOver; // text in channel members box about the limit (max 200 last members are shown)
contactsBg: windowBg; // contacts (and some other) box row background
contactsBgOver: windowBgOver; // contacts (and some other) box row background with mouse over
contactsNameFg: boxTextFg; // contacts (and some other) box row name text
contactsStatusFg: windowSubTextFg; // contacts (and some other) box row additional text (like last seen stamp)
contactsStatusFgOver: windowSubTextFgOver; // contacts (and some other) box row additional text (like last seen stamp) with mouse over
contactsStatusFgOnline: windowActiveTextFg; // contacts (and some other) box row active additional text (like online status)
photoCropFadeBg: layerBg; // avatar crop box fade background (when choosing a new photo in Settings or for a group)
photoCropPointFg: #ffffff7f; // avatar crop box corner rectangles (when choosing a new photo in Settings or for a group)
callArrowFg: #2dad2d | boxTextFgGood; // received phone call arrow (in calls list box)
callArrowMissedFg: #dd5b4a | boxTextFgError; // missed phone call arrow (in calls list box)
// intro
introBg: windowBg; // login background
introTitleFg: windowBoldFg; // login title text
introDescriptionFg: windowSubTextFg; // login description text
introCoverTopBg: #0f89d0; // intro gradient top (from)
introCoverBottomBg: #39b0f0; // intro gradient bottom (to)
introCoverIconsFg: #5ec6ff; // intro cloud graphics
introCoverPlaneTrace: #5ec6ff69; // intro plane traces
introCoverPlaneInner: #c6d8e8; // intro plane part
introCoverPlaneOuter: #a1bed4; // intro plane part
introCoverPlaneTop: #ffffff; // intro plane part
// dialogs
dialogsMenuIconFg: menuIconFg; // main menu and passcode lock icon
dialogsMenuIconFgOver: menuIconFgOver; // main menu and passcode lock icon with mouse over
dialogsBg: windowBg; // chat list background
dialogsNameFg: windowBoldFg; // chat list name text
dialogsChatIconFg: dialogsNameFg; // chat list group or channel icon
dialogsDateFg: windowSubTextFg; // chat list date text
dialogsTextFg: windowSubTextFg; // chat list message text
dialogsTextFgService: windowActiveTextFg; // chat list group sender name text (or media message type text)
dialogsDraftFg: #dd4b39; // chat list draft label
dialogsVerifiedIconBg: windowBgActive; // chat list verified icon background
dialogsVerifiedIconFg: windowFgActive; // chat list verified icon check
dialogsSendingIconFg: #c1c1c1; // chat list sending message icon (clock)
dialogsSentIconFg: #5dc452; // chat list sent message tick / double tick icon
dialogsUnreadBg: windowBgActive; // chat list unread badge background for not muted chat
dialogsUnreadBgMuted: #bbbbbb; // chat list unread badge background for muted chat
dialogsUnreadFg: windowFgActive; // chat list unread badge text
dialogsArchiveFg: #525252 | dialogsNameFg; // chat list archive name text
dialogsOnlineBadgeFg: #4dc920 | dialogsUnreadBg; // chat list online status
dialogsScamFg: dialogsDraftFg; // chat list scam label
dialogsBgOver: windowBgOver; // chat list background with mouse over
dialogsNameFgOver: windowBoldFgOver; // chat list name text with mouse over
dialogsChatIconFgOver: dialogsNameFgOver; // chat list group or channel icon with mouse over
dialogsDateFgOver: windowSubTextFgOver; // chat list date text with mouse over
dialogsTextFgOver: windowSubTextFgOver; // chat list message text with mouse over
dialogsTextFgServiceOver: dialogsTextFgService; // chat list group sender name text with mouse over
dialogsDraftFgOver: dialogsDraftFg; // chat list draft label with mouse over
dialogsVerifiedIconBgOver: dialogsVerifiedIconBg; // chat list verified icon background with mouse over
dialogsVerifiedIconFgOver: dialogsVerifiedIconFg; // chat list verified icon check with mouse over
dialogsSendingIconFgOver: dialogsSendingIconFg; // chat list sending message icon (clock) with mouse over
dialogsSentIconFgOver: #58b84d; // chat list sent message tick / double tick icon with mouse over
dialogsUnreadBgOver: dialogsUnreadBg; // chat list unread badge background for not muted chat with mouse over
dialogsUnreadBgMutedOver: dialogsUnreadBgMuted; // chat list unread badge background for muted chat with mouse over
dialogsUnreadFgOver: dialogsUnreadFg; // chat list unread badge text with mouse over
dialogsArchiveFgOver: #525252 | dialogsNameFgOver; // chat list archive name text with mouse over
dialogsScamFgOver: dialogsDraftFgOver; // chat list scam label with mouse over
dialogsBgActive: #419fd9; // chat list background for current (active) chat
dialogsNameFgActive: windowFgActive; // chat list name text for current (active) chat
dialogsChatIconFgActive: dialogsNameFgActive; // chat list group or channel icon for current (active) chat
dialogsDateFgActive: windowFgActive; // chat list date text for current (active) chat
dialogsTextFgActive: windowFgActive; // chat list message text for current (active) chat
dialogsTextFgServiceActive: dialogsTextFgActive; // chat list group sender name text for current (active) chat
dialogsDraftFgActive: #c6e1f7; // chat list draft label for current (active) chat
dialogsVerifiedIconBgActive: dialogsTextFgActive; // chat list verified icon background for current (active) chat
dialogsVerifiedIconFgActive: dialogsBgActive; // chat list verified icon check for current (active) chat
dialogsSendingIconFgActive: #ffffff99; // chat list sending message icon (clock) for current (active) chat
dialogsSentIconFgActive: dialogsTextFgActive; // chat list sent message tick / double tick icon for current (active) chat
dialogsUnreadBgActive: dialogsTextFgActive; // chat list unread badge background for not muted chat for current (active) chat
dialogsUnreadBgMutedActive: dialogsDraftFgActive; // chat list unread badge background for muted chat for current (active) chat
dialogsUnreadFgActive: dialogsBgActive; // chat list unread badge text for current (active) chat
dialogsOnlineBadgeFgActive: #ffffff; // chat list online status for current (active) chat
dialogsScamFgActive: dialogsDraftFgActive; // chat list scam label for current (active) chat
dialogsRippleBg: windowBgRipple; // chat list background ripple effect
dialogsRippleBgActive: activeButtonBgRipple; // chat list background ripple effect for current (active) chat
searchedBarBg: windowBgOver; // search results bar background (in chats list, contacts box..)
searchedBarFg: windowSubTextFgOver; // search results bar text (in chats list, contacts box..)
// history
topBarBg: windowBg; // top bar background (in chat view, media overview..)
emojiPanBg: windowBg; // emoji panel background
emojiPanCategories: #f7f7f7 | windowBg; // emoji panel categories background
emojiPanHeaderFg: windowSubTextFg; // emoji panel section header text
emojiPanHeaderBg: #fffffff2 | emojiPanBg; // emoji panel section header background
emojiIconFg: #999999; // emoji category icon
emojiSubIconFgActive: #666666 | windowBoldFg; // active emoji subcategory icon
stickerPanDeleteBg: #000000ff; // delete X button background for custom sent stickers in stickers panel (legacy)
stickerPanDeleteFg: windowFgActive; // delete X button icon for custom sent stickers in stickers panel (legacy)
stickerPreviewBg: #ffffffb0; // sticker and GIF preview background (when you press and hold on a sticker)
stickerPanPremium1: #5a99ff; // premium sticker pack icon gradient 1
stickerPanPremium2: #45b9f3; // premium sticker pack icon gradient 2
historyTextInFg: windowFg; // inbox message text
historyTextInFgSelected: historyTextInFg; // inbox message selected text or text in a selected message
historyTextOutFg: windowFg; // outbox message text
historyTextOutFgSelected: historyTextOutFg; // outbox message selected text or text in a selected message
historyLinkInFg: windowActiveTextFg; // inbox message link
historyLinkInFgSelected: historyLinkInFg; // inbox message link in a selected text or message
historyLinkOutFg: windowActiveTextFg; // outbox message link
historyLinkOutFgSelected: historyLinkOutFg; // outbox message link in a selected text or message
historyFileNameInFg: historyTextInFg; // inbox media filename text
historyFileNameInFgSelected: historyFileNameInFg; // inbox media filename text in a selected message
historyFileNameOutFg: historyTextOutFg; // outbox media filename text
historyFileNameOutFgSelected: historyFileNameOutFg; // outbox media filename text in a selected message
historyOutIconFg: #57b84c; // outbox message tick / double tick icon
historyOutIconFgSelected: #45a3aa; // outbox message tick / double tick icon in a selected message
historyIconFgInverted: windowFgActive; // media message tick / double tick icon (like in sent photo)
historySendingOutIconFg: #98d292; // outbox sending message icon (clock)
historySendingInIconFg: #a0adb5; // inbox sending message icon (clock) (like in sent messages to yourself or in sent messages to a channel)
historySendingInvertedIconFg: #ffffffc8; // media sending message icon (clock) (like in sent photo)
historyCallArrowInFg: #32b032; // received phone call arrow
historyCallArrowInFgSelected: #2592a8; // received phone call arrow in a selected message
historyCallArrowMissedInFg: callArrowMissedFg; // missed phone call arrow
historyCallArrowMissedInFgSelected: callArrowMissedFg; // missed phone call arrow in a selected message
historyCallArrowOutFg: historyCallArrowInFg; // outgoing phone call arrow
historyCallArrowOutFgSelected: historyCallArrowInFgSelected; // outgoing phone call arrow
historyUnreadBarBg: #fcfbfa; // new unread messages bar background
historyUnreadBarBorder: shadowFg; // new unread messages bar shadow
historyUnreadBarFg: #538bb4; // new unread messages bar text
historyForwardChooseBg: #0000004c; // forwarding messages in a large window size "choose recipient" background
historyForwardChooseFg: windowFgActive; // forwarding messages in a large window size "choose recipient" text
historyPeer1NameFg: #c03d33; // red group member name
historyPeer1NameFgSelected: historyPeer1NameFg; // red group member name in a selected message
historyPeer1UserpicBg: #ff845e; // red userpic background
historyPeer2NameFg: #4fad2d; // green group member name
historyPeer2NameFgSelected: historyPeer2NameFg; // green group member name in a selected message
historyPeer2UserpicBg: #9ad164; // green userpic background
historyPeer3NameFg: #d09306; // yellow group member name (actually unused)
historyPeer3NameFgSelected: historyPeer3NameFg; // yellow group member name in a selected message (actually unused)
historyPeer3UserpicBg: #e5ca77; // yellow userpic background (actually unused)
historyPeer4NameFg: windowActiveTextFg; // blue group member name
historyPeer4NameFgSelected: historyPeer4NameFg; // blue group member name in a selected message
historyPeer4UserpicBg: #5caffa; // blue userpic background
historyPeer5NameFg: #8544d6; // purple group member name
historyPeer5NameFgSelected: historyPeer5NameFg; // purple group member name in a selected message
historyPeer5UserpicBg: #b694f9; // purple userpic background
historyPeer6NameFg: #cd4073; // pink group member name
historyPeer6NameFgSelected: historyPeer6NameFg; // pink group member name in a selected message
historyPeer6UserpicBg: #ff8aac; // pink userpic background
historyPeer7NameFg: #2996ad; // sea group member name
historyPeer7NameFgSelected: historyPeer7NameFg; // sea group member name in a selected message
historyPeer7UserpicBg: #5bcbe3; // sea userpic background
historyPeer8NameFg: #ce671b; // orange group member name
historyPeer8NameFgSelected: historyPeer8NameFg; // orange group member name in a selected message
historyPeer8UserpicBg: #febb5b; // orange userpic background
historyPeerUserpicFg: windowFgActive; // default userpic initials
historyPeerSavedMessagesBg: historyPeer4UserpicBg; // saved messages userpic background
historyPeerArchiveUserpicBg: dialogsUnreadBgMuted; // archive folder userpic background
historyPeer1UserpicBg2: #d45246 | historyPeer1UserpicBg; // the second red userpic background
historyPeer2UserpicBg2: #46ba43 | historyPeer2UserpicBg; // the second green userpic background
historyPeer3UserpicBg2: #e5ca77 | historyPeer3UserpicBg; // the second yellow userpic background (actually unused)
historyPeer4UserpicBg2: #408acf | historyPeer4UserpicBg; // the second blue userpic background
historyPeer5UserpicBg2: #6c61df | historyPeer5UserpicBg; // the second purple userpic background
historyPeer6UserpicBg2: #d95574 | historyPeer6UserpicBg; // the second pink userpic background
historyPeer7UserpicBg2: #359ad4 | historyPeer7UserpicBg; // the second sea userpic background
historyPeer8UserpicBg2: #f68136 | historyPeer8UserpicBg; // the second orange userpic background
historyPeerSavedMessagesBg2: historyPeer4UserpicBg2; // the second saved messages userpic background
settingsIconBg1: #f06964; // red settings icon background
settingsIconBg2: #6dc534; // green settings icon background
settingsIconBg3: #ed9f20; // light-orange settings icon background
settingsIconBg4: #56b3f5; // light-blue settings icon background
settingsIconBg5: #7595ff; // dark-blue settings icon background
settingsIconBg6: #b580e2; // purple settings icon background
settingsIconBg8: #f2925b; // dark-orange settings icon background
settingsIconBgArchive: #9da2b0; // archive main menu icon background
settingsIconFg: #ffffff; // settings icon shape
// Some values are marked as (adjusted), it means they're adjusted by
// hue and saturation of the average background color if user chooses
// some other (not bundled to this color theme) background. If the
// bundled background is used those colors are not adjusted in any way.
historyScrollBarBg: #517c417a; // scroll bar current rectangle, the bar itself in the chat view (adjusted)
historyScrollBarBgOver: #517c41bc; // scroll bar current rectangle with mouse over it in the chat view (adjusted)
historyScrollBg: #517c414c; // scroll bar background (adjusted)
historyScrollBgOver: #517c416b; // scroll bar background with mouse over the scroll bar (adjusted)
msgInBg: windowBg; // inbox message background
msgInBgSelected: #c2dcf2; // inbox selected message background (and background of selected text in those messages)
msgOutBg: #effdde; // outbox message background
msgOutBgSelected: #b7dbdb; // outbox selected message background (and background of selected text in those messages)
msgSelectOverlay: #358cd44c; // overlay which is filling the media parts of selected messages (like in selected photo message)
msgStickerOverlay: #358cd47f; // overlay which is filling the selected sticker message
msgInServiceFg: windowActiveTextFg; // inbox message information text (like information about a forwarded message original sender)
msgInServiceFgSelected: windowActiveTextFg; // inbox selected message information text (like information about a forwarded message original sender)
msgOutServiceFg: #45a32d; // outbox message information text (like information about a forwarded message original sender)
msgOutServiceFgSelected: #469992; // outbox message information text (like information about a forwarded message original sender)
msgInShadow: #748ea229; // inbox message shadow (below the bubble)
msgInShadowSelected: #548dbb29; // inbox selected message shadow (below the bubble)
msgOutShadow: #3ac3461d; // outbox message shadow (below the bubble)
msgOutShadowSelected: #37a78d22; // outbox selected message shadow (below the bubble)
msgInDateFg: #a0acb6; // inbox message time text
msgInDateFgSelected: #6a9cc5; // inbox selected message time text
msgOutDateFg: #6db566; // outbox message time text
msgOutDateFgSelected: #56b2a6; // outbox selected message time text
msgServiceFg: windowFgActive; // service message text (like date dividers or service message about the group title being changed)
msgServiceBg: #517c417f; // service message background (like in a service message about group title being changed) (adjusted)
msgServiceBgSelected: #96b38ba2; // service message selected text background (like in a service message about group title being changed) (adjusted)
msgInReplyBarColor: activeLineFg; // inbox message reply outline
msgInReplyBarSelColor: activeLineFg; // inbox selected message reply outline
msgOutReplyBarColor: #5eb854; // outbox message reply outline
msgOutReplyBarSelColor: historyOutIconFgSelected; // outbox selected message reply outline
msgImgReplyBarColor: msgServiceFg; // sticker message reply outline
msgInMonoFg: #4e7391; // inbox message monospace text (like a message sent with `test` text)
msgOutMonoFg: #459866; // outbox message monospace text
msgInMonoFgSelected: msgInMonoFg; // inbox message monospace text in a selected text or message
msgOutMonoFgSelected: msgOutMonoFg; // outbox message monospace text in a selected text or message
msgDateImgFg: msgServiceFg; // media message time text (like time text in a sent photo)
msgDateImgBg: #00000054; // media message time bubble background (like time bubble in a sent photo) or file with thumbnail download icon circle background
msgDateImgBgOver: #00000074; // media message download icon circle background with mouse over (like file with thumbnail download icon)
msgDateImgBgSelected: #1c4a7187; // selected media message time bubble background
msgFileThumbLinkInFg: lightButtonFg; // inbox media file message with thumbnail download / open with button text
msgFileThumbLinkInFgSelected: lightButtonFgOver; // inbox selected media file message with thumbnail download / open with button text
msgFileThumbLinkOutFg: #4ba831; // outbox media file message with thumbnail download / open with button text
msgFileThumbLinkOutFgSelected: #31a298; // outbox selected media file message with thumbnail download / open with button text
msgFileInBg: windowBgActive; // inbox audio file download circle background
msgFileInBgOver: #4eade3; // inbox audio file download circle background with mouse over
msgFileInBgSelected: #51a3d3; // inbox selected audio file download circle background
msgFileOutBg: #5fbe67; // outbox audio file download circle background
msgFileOutBgSelected: #50ac9b; // outbox selected audio file download circle background
msgFile1Bg: #72b1df; // blue shared links / files without image square thumbnail
msgFile1BgDark: #5c9ece; // blue shared files without image download circle background
msgFile1BgOver: #5294c4; // blue shared files without image download circle background with mouse over
msgFile1BgSelected: #5099d0; // blue shared files without image download circle background if file is selected
msgFile2Bg: #5fbe67; // green shared links / shared files without image square thumbnail
msgFile2BgDark: #4da859; // green shared files without image download circle background
msgFile2BgOver: #44a050; // green shared files without image download circle background with mouse over
msgFile2BgSelected: #50ac9b; // green shared files without image download circle background if file is selected
msgFile3Bg: #e47272; // red shared links / shared files without image square thumbnail
msgFile3BgDark: #cd5b5e; // red shared files without image download circle background
msgFile3BgOver: #c35154; // red shared files without image download circle background with mouse over
msgFile3BgSelected: #9f6a82; // red shared files without image download circle background if file is selected
msgFile4Bg: #efc274; // yellow shared links / shared files without image square thumbnail
msgFile4BgDark: #e6a561; // yellow shared files without image download circle background
msgFile4BgOver: #dc9c5a; // yellow shared files without image download circle background with mouse over
msgFile4BgSelected: #b19d84; // yellow shared files without image download circle background if file is selected
historyFileInIconFg: msgInBg; // inbox file without thumbnail (like audio file) download arrow icon
historyFileInIconFgSelected: msgInBgSelected; // inbox selected file without thumbnail (like audio file) download arrow icon
historyFileInRadialFg: historyFileInIconFg; // inbox file without thumbnail (like audio file) radial download animation line
historyFileInRadialFgSelected: historyFileInIconFgSelected; // inbox selected file without thumbnail (like audio file) radial download animation line
historyFileOutIconFg: msgOutBg; // outbox file without thumbnail (like audio file) download arrow icon
historyFileOutIconFgSelected: msgOutBgSelected; // outbox selected file without thumbnail (like audio file) download arrow icon
historyFileOutRadialFg: historyFileOutIconFg; // outbox file without thumbnail (like audio file) radial download animation line
historyFileOutRadialFgSelected: historyFileOutIconFgSelected; // outbox selected file without thumbnail (like audio file) radial download animation line
historyFileThumbIconFg: msgInBg; // file with thumbnail (or photo / video) download arrow icon
historyFileThumbIconFgSelected: msgInBgSelected; // selected file with thumbnail (or photo / video) download arrow icon
historyFileThumbRadialFg: historyFileThumbIconFg; // file with thumbnail (or photo / video) radial download animation line
historyFileThumbRadialFgSelected: historyFileThumbIconFgSelected; // selected file with thumbnail (or photo / video) radial download animation line
historyVideoMessageProgressFg: historyFileThumbIconFg; // radial playback progress in round video messages
msgWaveformInActive: windowBgActive; // inbox voice message active waveform lines (like played part of currently playing voice message)
msgWaveformInActiveSelected: #51a3d3; // inbox selected voice message active waveform lines (like played part of currently playing voice message)
msgWaveformInInactive: #d4dee6; // inbox voice message inactive waveform lines (like upcoming part of currently playing voice message)
msgWaveformInInactiveSelected: #9cc1e1; // inbox selected voice message inactive waveform lines (like upcoming part of currently playing voice message)
msgWaveformOutActive: #5ebd66; // outbox voice message active waveform lines (like played part of currently playing voice message)
msgWaveformOutActiveSelected: #6badad; // outbox selected voice message active waveform lines (like played part of currently playing voice message)
msgWaveformOutInactive: #b3e2b4; // outbox voice message inactive waveform lines (like upcoming part of currently playing voice message)
msgWaveformOutInactiveSelected: #91c3c3; // outbox selected voice message inactive waveform lines (like upcoming part of currently playing voice message)
msgBotKbOverBgAdd: #ffffff20; // this is painted over a bot inline keyboard button (which has msgServiceBg background) when mouse is over that button
msgBotKbIconFg: msgServiceFg; // bot inline keyboard button icon in the top-right corner (like in @vote bot when a poll is ready to be shared)
msgBotKbRippleBg: #00000020; // bot inline keyboard button ripple effect
mediaInFg: msgInDateFg; // inbox media message status text (like in file that is being downloaded)
mediaInFgSelected: msgInDateFgSelected; // inbox selected media message status text (like in file that is being downloaded)
mediaOutFg: msgOutDateFg; // outbox media message status text (like in file that is being downloaded)
mediaOutFgSelected: msgOutDateFgSelected; // outbox selected media message status text (like in file that is being downloaded)
youtubePlayIconBg: #e83131c8; // youtube play icon background (when a link to a youtube video with a webpage preview is sent)
youtubePlayIconFg: windowFgActive; // youtube play icon arrow (when a link to a youtube video with a webpage preview is sent)
videoPlayIconBg: #0000007f; // other video play icon background (like when a link to a vimeo video with a webpage preview is sent)
videoPlayIconFg: #ffffff; // other video play icon arrow (like when a link to a vimeo video with a webpage preview is sent)
toastBg: #2c3033e5; // toast notification background (like when you click on your t.me link when editing your username)
toastFg: #ffffff; // toast notification text (like when you click on your t.me link when editing your username)
historyToDownBg: windowBg; // arrow button background (to scroll to the end of the viewed chat)
historyToDownBgOver: windowBgOver; // arrow button background with mouse over
historyToDownBgRipple: windowBgRipple; // arrow button ripple effect
historyToDownFg: menuIconFg; // arrow button icon
historyToDownFgOver: menuIconFgOver; // arrow button icon with mouse over
historyToDownShadow: #00000040; // arrow button shadow
historyComposeAreaBg: msgInBg; // history compose area background (message write area / reply information / forwarding information)
historyComposeAreaFg: historyTextInFg; // history compose area text
historyComposeAreaFgService: msgInDateFg; // history compose area text when replying to a media message
historyComposeIconFg: menuIconFg; // history compose area icon (like emoji, attach, bot command..)
historyComposeIconFgOver: menuIconFgOver; // history compose area icon with mouse over
historySendIconFg: windowBgActive; // send message icon
historySendIconFgOver: windowBgActive; // send message icon with mouse over
historyPinnedBg: historyComposeAreaBg; // pinned message area background
historyReplyBg: historyComposeAreaBg; // reply / forward / edit message area background
historyReplyIconFg: windowBgActive; // reply / forward / edit message left icon
historyReplyCancelFg: cancelIconFg; // reply / forward / edit message cancel button
historyReplyCancelFgOver: cancelIconFgOver; // reply / forward / edit message cancel button with mouse over
historyComposeButtonBg: historyComposeAreaBg; // unblock / join channel / mute channel button background
historyComposeButtonBgOver: windowBgOver; // unblock / join channel / mute channel button background with mouse over
historyComposeButtonBgRipple: windowBgRipple; // unblock / join channel / mute channel button ripple effect
mapPointDrop: #fd4444; // geo location marker background
mapPointDot: #ffffff; // geo location marker point
// overview
overviewCheckBg: #00000040; // shared media / files / links checkbox background for not selected rows when some rows are selected
overviewCheckBgActive: windowBgActive; // shared media / files / links checkbox background for selected rows
overviewCheckBorder: windowBg; // shared media round checkbox border
overviewCheckFgActive: windowBg; // shared files / links checkbox icon for selected rows
overviewPhotoSelectOverlay: #40ace333; // shared photos / videos / links fill for selected rows
// profile
profileStatusFgOver: #7c99b2; // group members list in group profile user last seen text with mouse over
profileVerifiedCheckBg: windowBgActive; // profile verified check icon background
profileVerifiedCheckFg: windowFgActive; // profile verified check icon tick
profileAdminStartFg: windowBgActive; // group members list creator star icon
// settings
notificationsBoxMonitorFg: windowFg; // custom notifications settings box monitor color
notificationsBoxScreenBg: dialogsBgActive; // #6389a8; // custom notifications settings box monitor screen background
notificationSampleUserpicFg: windowBgActive; // custom notifications settings box small sample userpic placeholder
notificationSampleCloseFg: #d7d7d7 | windowSubTextFg; // custom notifications settings box small sample close button placeholder
notificationSampleTextFg: #d7d7d7 | windowSubTextFg; // custom notifications settings box small sample text placeholder
notificationSampleNameFg: #939393 | windowSubTextFg; // custom notifications settings box small sample name placeholder
mainMenuBg: windowBg; // main menu background
mainMenuCoverBg: dialogsBgActive; // main menu top cover background
mainMenuCloudFg: activeButtonFg; // main menu top cover saved messages / archive button icon
mainMenuCloudBg: #2785bf | activeButtonBgRipple; // main menu top cover saved messages / archive button background
mediaPlayerBg: windowBg; // audio file player background
mediaPlayerActiveFg: windowBgActive; // audio file player playback progress already played part
mediaPlayerInactiveFg: sliderBgInactive; // audio file player playback progress upcoming (not played yet) part with mouse over
mediaPlayerDisabledFg: #9dd1ef; // audio file player loading progress (when you're playing an audio file and switch to the previous one which is not loaded yet)
// mediaview
mediaviewFileBg: windowBg; // file rectangle background (when you view a png file in Media Viewer and go to a previous, not loaded yet, file)
mediaviewFileNameFg: windowFg; // file name in file rectangle
mediaviewFileSizeFg: windowSubTextFg; // file size text in file rectangle
mediaviewFileRedCornerFg: #d55959; // red file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .pdf)
mediaviewFileYellowCornerFg: #e8a659; // yellow file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .zip)
mediaviewFileGreenCornerFg: #49a957; // green file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .exe)
mediaviewFileBlueCornerFg: #599dcf; // blue file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .dmg)
mediaviewFileExtFg: activeButtonFg; // file extension text in file thumbnail placeholder in file rectangle
mediaviewMenuBg: #383838; // context menu in Media Viewer background
mediaviewMenuBgOver: #505050; // context menu item background with mouse over
mediaviewMenuBgRipple: #676767; // context menu item ripple effect
mediaviewMenuFg: windowFgActive; // context menu item text
mediaviewBg: #222222eb; // Media Viewer background
mediaviewVideoBg: imageBg; // Media Viewer background when viewing a video in full screen
mediaviewControlBg: #0000003c; // controls background (like next photo / previous photo)
mediaviewControlFg: #ffffff; // controls icon (like next photo / previous photo)
mediaviewCaptionBg: #11111180; // caption text background (when viewing photo with caption)
mediaviewCaptionFg: mediaviewControlFg; // caption text
mediaviewTextLinkFg: #4db8ff; // caption text link
mediaviewSaveMsgBg: toastBg; // save to file toast message background in Media Viewer
mediaviewSaveMsgFg: toastFg; // save to file toast message text
mediaviewPlaybackActive: #c7c7c7; // video playback progress already played part
mediaviewPlaybackInactive: #252525; // video playback progress upcoming (not played yet) part
mediaviewPlaybackActiveOver: #ffffff; // video playback progress already played part with mouse over
mediaviewPlaybackInactiveOver: #474747; // video playback progress upcoming (not played yet) part with mouse over
mediaviewPlaybackProgressFg: #ffffffc7; // video playback progress text
mediaviewPlaybackIconFg: mediaviewPlaybackActive; // video playback controls icon
mediaviewPlaybackIconFgOver: mediaviewPlaybackActiveOver; // video playback controls icon with mouse over
mediaviewPlaybackIconRipple: #ffffff14; // video playback controls ripple effect
mediaviewPipControlsFg: #ffffffd9; // picture-in-picture controls
mediaviewPipControlsFgOver: #ffffff; // picture-in-picture controls with mouse over
mediaviewPipPlaybackActive: #ffffffda; // picture-in-picture playback progress already played part
mediaviewPipPlaybackInactive: #ffffff26; // picture-in-picture playback progress upcoming (not played yet) part
mediaviewTransparentBg: #ffffff; // transparent filling part (when viewing a transparent .png file in Media Viewer)
mediaviewTransparentFg: #cccccc; // another transparent filling part
// notification
notificationBg: windowBg; // custom notification window background
// calls
callBg: #26282cf2; // old phone call popup background
callBgOpaque: #1b1f23; // phone call popup background
callBgButton: #1b1f237f; // phone call window control buttons bg
callNameFg: #ffffff; // phone call popup name text
callStatusFg: #aaabac; // phone call popup status text
callIconBg: #ffffff1f; // phone call mute mic and camera button background
callIconFg: #ffffff; // phone call popup answer, hangup, mute mic and camera icon
callIconBgActive: #ffffffe5; // phone call line busy cancel, muted mic and camera button background
callIconFgActive: #222222; // phone call line busy cancel, muted mic and camera icon
callIconActiveRipple: #f1f1f1; // phone call line busy cancel, muted mic and camera ripple effect
callAnswerBg: #66c95b; // phone call popup answer button background
callAnswerRipple: #52b149; // phone call popup answer button ripple effect
callAnswerBgOuter: #50eb4126; // phone call popup answer button outer ripple effect
callHangupBg: #d75a5a; // phone call popup hangup button background
callHangupRipple: #c04646; // phone call popup hangup button ripple effect
callMuteRipple: #ffffff12; // phone call popup mute mic and camera ripple effect
groupCallBg: #1a2026; // group call popup background
groupCallActiveFg: #4db8ff; // group call active controls text
groupCallMembersBg: #2c333d; // group call members list background
groupCallMembersBgOver: #323a45; // group call members list row with mouse over
groupCallMembersBgRipple: #39424f; // group call member row ripple effect
groupCallMembersFg: #ffffff; // group call member name text
groupCallMemberActiveIcon: #8deb90; // group call active member icon
groupCallMemberActiveStatus: #8deb90; // group call active member status text
groupCallMemberInactiveIcon: #84888f; // group call inactive member icon
groupCallMemberInactiveStatus: #61c0ff; // group call inactive member status text
groupCallMemberMutedIcon: #ed7372; // group call muted by admin member icon
groupCallMemberNotJoinedStatus: #91979e; // group call non joined member status text
groupCallIconFg: #ffffff; // group call mute / settings / leave icon
groupCallLive1: #0dcc39; // group call live button color1
groupCallLive2: #0bb6bd; // group call live button color2
groupCallMuted1: #0992ef; // group call muted button color1
groupCallMuted2: #16ccfb; // group call muted button color2
groupCallForceMutedBar1: #c65493; // group call force muted top bar color1
groupCallForceMutedBar2: #7a6af1; // group call force muted top bar color2
groupCallForceMutedBar3: #5f95e8; // group call force muted top bar color3
groupCallForceMuted1: #4f9cff; // group call force muted button color1
groupCallForceMuted2: #9b52e9; // group call force muted button color2
groupCallForceMuted3: #eb5353; // group call force muted button color3
groupCallMenuBg: #292d33; // group call popup menu background
groupCallMenuBgOver: #343940; // group call popup menu with mouse over
groupCallMenuBgRipple: #3a4047; // group call popup menu ripple effect
groupCallLeaveBg: #f75c5c7f; // group call leave button background
groupCallLeaveBgRipple: #f75c5c9e; // group call leave button ripple effect
groupCallVideoTextFg: #ffffffe0; // group call text over video
groupCallVideoSubTextFg: #ffffffc0; // group call additional text over video
callBarBg: dialogsBgActive; // active phone call bar background
callBarMuteRipple: dialogsRippleBgActive; // active phone call bar mute and hangup button ripple effect
callBarBgMuted: #8f8f8f | dialogsUnreadBgMuted; // phone call bar with muted mic background
callBarFg: dialogsNameFgActive; // phone call bar text and icons
importantTooltipBg: toastBg; // group call important tooltip background color
importantTooltipFg: toastFg; // group call important tooltip text color
importantTooltipFgLink: mediaviewTextLinkFg; // group call important tooltip text link color
outdatedFg: #ffffff; // operating system version is outdated bar text
outdateSoonBg: #e08543; // operating system version is soon outdated bar background
outdatedBg: #e05745; // operating system version is already outdated bar background
// spellcheck
spellUnderline: #ff000088 | attentionButtonFg; // misspelled words
walletTitleBg: #121213; // wallet window title background when window is inactive
walletTitleBgActive: walletTitleBg; // wallet window title background when window is active
walletTitleButtonBg: walletTitleBg; // wallet window title minimize/maximize/restore button background when window is inactive (Windows only)
walletTitleButtonFg: #5a5a5b; // wallet window title minimize/maximize/restore button icon when window is inactive (Windows only)
walletTitleButtonBgOver: #373738; // wallet window title minimize/maximize/restore button background with mouse over when window is inactive (Windows only)
walletTitleButtonFgOver: #747475; // wallet window title minimize/maximize/restore button icon with mouse over when window is inactive (Windows only)
walletTitleButtonBgActive: walletTitleButtonBg; // wallet window title minimize/maximize/restore button background when window is active (Windows only)
walletTitleButtonFgActive: walletTitleButtonFg; // wallet window title minimize/maximize/restore button icon when window is active (Windows only)
walletTitleButtonBgActiveOver: walletTitleButtonBgOver; // wallet window title minimize/maximize/restore button background with mouse over when window is active (Windows only)
walletTitleButtonFgActiveOver: walletTitleButtonFgOver; // wallet window title minimize/maximize/restore button icon with mouse over when window is active (Windows only)
walletTitleButtonCloseBg: walletTitleButtonBg; // wallet window title close button background when window is inactive (Windows only)
walletTitleButtonCloseFg: walletTitleButtonFg; // wallet window title close button icon when window is inactive (Windows only)
walletTitleButtonCloseBgOver: titleButtonCloseBgOver; // wallet window title close button background with mouse over when window is inactive (Windows only)
walletTitleButtonCloseFgOver: titleButtonCloseFgOver; // wallet window title close button icon with mouse over when window is inactive (Windows only)
walletTitleButtonCloseBgActive: walletTitleButtonCloseBg; // wallet window title close button background when window is active (Windows only)
walletTitleButtonCloseFgActive: walletTitleButtonCloseFg; // wallet window title close button icon when window is active (Windows only)
walletTitleButtonCloseBgActiveOver: walletTitleButtonCloseBgOver; // wallet window title close button background with mouse over when window is active (Windows only)
walletTitleButtonCloseFgActiveOver: walletTitleButtonCloseFgOver; // wallet window title close button icon with mouse over when window is active (Windows only)
walletTopBg: #1e1f21; // wallet top part background
walletBalanceFg: #ffffff; // wallet balance text
walletSubBalanceFg: #f9f9f9; // wallet balance label text
walletTopLabelFg: #999999; // wallet top updated label text
walletTopIconFg: walletTopLabelFg; // wallet top refresh and menu icons
walletTopIconRipple: #ffffff12; // wallet top menu icon ripple effect
sideBarBg: #293a4c; // filters side bar background
sideBarBgActive: #17212b; // filters side bar active background
sideBarBgRipple: #1e2b38; // filters side bar ripple effect
sideBarTextFg: #8897a6; // filters side bar text
sideBarTextFgActive: #64b9fa; // filters side bar active item text
sideBarIconFg: #8393a3; // filters side bar icon
sideBarIconFgActive: #5eb5f7; // filters side bar active item icon
sideBarBadgeBg: #5eb5f7; // filters side bar badge background
sideBarBadgeBgMuted: #8393a3; // filters side bar unimportant badge background
sideBarBadgeFg: #ffffff; // filters side bar badge text
songCoverOverlayFg: #00000066; // song cover overlay
photoEditorItemBaseHandleFg: #3ccaef; // photo editor handle circle
premiumButtonBg1: #55a5ff; // upgrade to premium button gradient 1
premiumButtonBg2: #a767ff; // upgrade to premium button gradient 2
premiumButtonBg3: #db5c9d; // upgrade to premium button gradient 3
premiumButtonFg: #ffffff; // upgrade to premium button text
premiumIconBg1: #f38926; // icon in premium settings gradient 1
premiumIconBg2: #e44456; // icon in premium settings gradient 2
premiumIconBg3: #4acd43; // icon in premium settings gradient 3
statisticsChartInactive: #e2eef999; // inactive area in footer of statistic charts
statisticsChartActive: #baccd9d8; // sides in footer of statistic charts
statisticsChartLineBlue: #327fe5; // represents blue color on statistical charts
statisticsChartLineGreen: #61c752; // represents green color on statistical charts
statisticsChartLineRed: #e05356; // represents red color on statistical charts
statisticsChartLineGolden: #eba52d; // represents golden color on statistical charts
statisticsChartLineLightblue: #58a8ed; // represents lightblue color on statistical charts
statisticsChartLineLightgreen: #8fcf39; // represents lightgreen color on statistical charts
statisticsChartLineOrange: #f28c39; // represents orange color on statistical charts
statisticsChartLineIndigo: #7f79f3; // represents indigo color on statistical charts
statisticsChartLinePurple: #9f79e8; // represents purple color on statistical charts
statisticsChartLineCyan: #40d0ca; // represents cyan color on statistical charts
creditsBg1: #ffb222; // credits icon gradient 1, normal
creditsBg2: #FFD951; // credits icon gradient 2, light
creditsBg3: #f0b400; // credits icon gradient 3, dark
creditsFg: #ba7000; // credits text on light background
creditsStroke: #da8735; // credits icon stroke
currencyFg: #168acd; // currency icon, blue

View File

@@ -0,0 +1,127 @@
// 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/delayed_activation.h"
#include "ui/ui_utility.h"
#include "base/call_delayed.h"
#include "base/invoke_queued.h"
#include "base/platform/base_platform_info.h"
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
#include "base/platform/linux/base_linux_xcb_utilities.h"
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
#include <QtCore/QPointer>
#include <QtWidgets/QApplication>
namespace Ui {
namespace {
constexpr auto kPreventTimeout = crl::time(100);
bool Paused/* = false*/;
bool Attempted/* = false*/;
int KeepingPaused/* = 0*/;
auto Window = QPointer<QWidget>();
bool Unpause(bool force = false) {
if ((force && !KeepingPaused) || Attempted) {
Attempted = false;
Paused = false;
return true;
}
return false;
}
} // namespace
void ActivateWindow(not_null<QWidget*> widget) {
const auto window = widget->window();
window->raise();
window->activateWindow();
ActivateWindowDelayed(window);
}
void ActivateWindowDelayed(not_null<QWidget*> widget) {
if (Paused) {
Attempted = true;
return;
} else if (std::exchange(Window, widget.get())) {
return;
}
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
const auto focusAncestor = [&] {
const auto focusWidget = QApplication::focusWidget();
if (!focusWidget || !widget->window()) {
return false;
}
return widget->window()->isAncestorOf(focusWidget);
}();
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
crl::on_main(Window, [=] {
const auto widget = base::take(Window);
if (!widget) {
return;
}
const auto window = widget->window();
if (!window || window->isHidden()) {
return;
}
window->raise();
window->activateWindow();
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
if (::Platform::IsX11() && focusAncestor) {
const base::Platform::XCB::Connection connection;
if (connection && !xcb_connection_has_error(connection)) {
free(
xcb_request_check(
connection,
xcb_set_input_focus_checked(
connection,
XCB_INPUT_FOCUS_PARENT,
window->winId(),
XCB_CURRENT_TIME)));
}
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
});
}
void PreventDelayedActivation() {
Window = nullptr;
Paused = true;
PostponeCall([] {
if (Unpause()) {
return;
}
InvokeQueued(qApp, [] {
if (Unpause()) {
return;
}
crl::on_main([] {
if (Unpause()) {
return;
}
base::call_delayed(kPreventTimeout, [] {
Unpause(true);
});
});
});
});
}
void KeepDelayedActivationPaused(bool keep) {
if (keep) {
++KeepingPaused;
} else if (KeepingPaused > 0) {
if (!--KeepingPaused && Paused) {
Unpause(true);
}
}
}
} // namespace Ui

View File

@@ -0,0 +1,16 @@
// 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 {
void ActivateWindow(not_null<QWidget*> widget);
void ActivateWindowDelayed(not_null<QWidget*> widget);
void PreventDelayedActivation();
void KeepDelayedActivationPaused(bool keep);
} // namespace Ui

View File

@@ -0,0 +1,32 @@
// 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/dpr/dpr_icon.h"
namespace dpr {
QImage IconFrame(
const style::icon &icon,
const QColor &color,
double ratio) {
const auto scale = style::Scale() * ratio;
const auto use = (scale > 200. || style::DevicePixelRatio() > 2)
? (300 / style::DevicePixelRatio())
: (scale > 100.)
? (200 / style::DevicePixelRatio())
: (100 / style::DevicePixelRatio());
auto image = icon.instance(color, use);
image.setDevicePixelRatio(1.);
const auto desired = icon.size() * ratio;
return (image.size() == desired)
? image
: image.scaled(
desired,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
} // namespace dpr

View File

@@ -0,0 +1,20 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "ui/style/style_core.h"
class QImage;
namespace dpr {
[[nodiscard]] QImage IconFrame(
const style::icon &icon,
const QColor &color,
double ratio);
} // namespace dpr

View File

@@ -0,0 +1,50 @@
// 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 <rpl/details/callable.h>
#include <QtGui/QImage>
#include <QtGui/QPainter>
namespace dpr {
// Validate(_cache, devicePixelRatioF(), size, [&](QPainter &p, QSize size) {
// ... paint using p ...
// }, (_cacheKey != cacheKey()), Qt::transparent);
template <typename Generator>
void Validate(
QImage &image,
double ratio,
QSize size,
Generator &&generator,
bool force,
std::optional<QColor> fill = {},
bool setResultRatio = true) {
size *= ratio;
const auto sizeChanged = (image.size() != size);
if (sizeChanged || force) {
if (sizeChanged) {
image = QImage(size, QImage::Format_ARGB32_Premultiplied);
}
if (fill) {
image.fill(*fill);
}
image.setDevicePixelRatio(1.);
auto p = QPainter(&image);
using namespace rpl::details;
if constexpr (is_callable_plain_v<Generator, QPainter&, QSize>) {
generator(p, size);
} else {
generator(p);
}
}
image.setDevicePixelRatio(setResultRatio ? ratio : 1.);
}
} // namespace dpr

View File

@@ -0,0 +1,63 @@
// 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/dragging_scroll_manager.h"
#include "base/timer.h"
namespace Ui {
namespace {
// 37px per 15ms while select-by-drag.
inline constexpr auto kMaxScrollSpeed = 37;
} // namespace
DraggingScrollManager::DraggingScrollManager() = default;
void DraggingScrollManager::scrollByTimer() {
const auto d = (_delta > 0)
? std::min(_delta * 3 / 20 + 1, kMaxScrollSpeed)
: std::max(_delta * 3 / 20 - 1, -kMaxScrollSpeed);
_scrolls.fire_copy(d);
}
void DraggingScrollManager::checkDeltaScroll(int delta) {
_delta = delta;
if (_delta) {
if (!_timer) {
_timer = std::make_unique<base::Timer>([=] { scrollByTimer(); });
}
_timer->callEach(15);
} else {
cancel();
}
}
void DraggingScrollManager::checkDeltaScroll(
const QPoint &point,
int top,
int bottom) {
const auto diff = point.y() - top;
checkDeltaScroll((diff < 0)
? diff
: (point.y() >= bottom)
? (point.y() - bottom + 1)
: 0);
}
void DraggingScrollManager::cancel() {
if (_timer) {
_timer->cancel();
_timer = nullptr;
}
}
rpl::producer<int> DraggingScrollManager::scrolls() const {
return _scrolls.events();
}
} // namespace Ui

View File

@@ -0,0 +1,34 @@
// 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 base {
class Timer;
} // namespace base
namespace Ui {
class DraggingScrollManager final {
public:
DraggingScrollManager();
void checkDeltaScroll(int delta);
void checkDeltaScroll(const QPoint &point, int top, int bottom);
void cancel();
[[nodiscard]] rpl::producer<int> scrolls() const;
private:
void scrollByTimer();
std::unique_ptr<base::Timer> _timer;
int _delta = 0;
rpl::event_stream<int> _scrolls;
};
} // namespace Ui

View File

@@ -0,0 +1,21 @@
// 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 DynamicImage {
public:
virtual ~DynamicImage() = default;
[[nodiscard]] virtual std::shared_ptr<DynamicImage> clone() = 0;
[[nodiscard]] virtual QImage image(int size) = 0;
virtual void subscribeToUpdates(Fn<void()> callback) = 0;
};
} // namespace Ui

View File

@@ -0,0 +1,181 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "ui/effects/animation_value.h"
#include "ui/painter.h"
#include <QtCore/QtMath> // M_PI
namespace anim {
namespace {
rpl::variable<bool> AnimationsDisabled = false;
int SlowMultiplierMinusOne/* = 0*/;
} // namespace
transition linear = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto result = delta * dt;
Ensures(!std::isnan(result));
return result;
};
transition sineInOut = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto result = -(delta / 2) * (cos(M_PI * dt) - 1);
Ensures(!std::isnan(result));
return result;
};
transition halfSine = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto result = delta * sin(M_PI * dt / 2);
Ensures(!std::isnan(result));
return result;
};
transition easeOutBack = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
static constexpr auto s = 1.70158;
const auto t = dt - 1;
Assert(!std::isnan(t));
const auto result = delta * (t * t * ((s + 1) * t + s) + 1);
Ensures(!std::isnan(result));
return result;
};
transition easeInCirc = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto result = -delta * (sqrt(1 - dt * dt) - 1);
Ensures(!std::isnan(result));
return result;
};
transition easeOutCirc = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto t = dt - 1;
Assert(!std::isnan(t));
const auto result = delta * sqrt(1 - t * t);
Ensures(!std::isnan(result));
return result;
};
transition easeInCubic = [](const float64 &delta, const float64 &dt) {
const auto result = delta * dt * dt * dt;
Ensures(!std::isnan(result));
return result;
};
transition easeOutCubic = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto t = dt - 1;
Assert(!std::isnan(t));
const auto result = delta * (t * t * t + 1);
Ensures(!std::isnan(result));
return result;
};
transition easeInQuint = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto t2 = dt * dt;
Assert(!std::isnan(t2));
const auto result = delta * t2 * t2 * dt;
Ensures(!std::isnan(result));
return result;
};
transition easeOutQuint = [](const float64 &delta, const float64 &dt) {
Expects(!std::isnan(delta));
Expects(!std::isnan(dt));
const auto t = dt - 1, t2 = t * t;
Assert(!std::isnan(t));
Assert(!std::isnan(t2));
const auto result = delta * (t2 * t2 * t + 1);
Ensures(!std::isnan(result));
return result;
};
rpl::producer<bool> Disables() {
return AnimationsDisabled.value();
};
bool Disabled() {
return AnimationsDisabled.current();
}
void SetDisabled(bool disabled) {
AnimationsDisabled = disabled;
}
int SlowMultiplier() {
return (SlowMultiplierMinusOne + 1);
}
void SetSlowMultiplier(int multiplier) {
Expects(multiplier > 0);
SlowMultiplierMinusOne = multiplier - 1;
}
void DrawStaticLoading(
QPainter &p,
QRectF rect,
float64 stroke,
QPen pen,
QBrush brush) {
PainterHighQualityEnabler hq(p);
p.setBrush(brush);
pen.setWidthF(stroke);
pen.setCapStyle(Qt::RoundCap);
pen.setJoinStyle(Qt::RoundJoin);
p.setPen(pen);
p.drawEllipse(rect);
const auto center = rect.center();
const auto first = QPointF(center.x(), rect.y() + 1.5 * stroke);
const auto delta = center.y() - first.y();
const auto second = QPointF(center.x() + delta * 2 / 3., center.y());
if (delta > 0) {
QPainterPath path;
path.moveTo(first);
path.lineTo(center);
path.lineTo(second);
p.drawPath(path);
}
}
} // anim

View File

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

View File

@@ -0,0 +1,26 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
namespace anim {
TG_FORCE_INLINE float64 interpolateF(float a, float b, float64 b_ratio) {
return a + float64(b - a) * b_ratio;
};
TG_FORCE_INLINE QRectF interpolatedRectF(
const QRectF &r1,
const QRectF &r2,
float64 ratio) {
return QRectF(
interpolateF(r1.x(), r2.x(), ratio),
interpolateF(r1.y(), r2.y(), ratio),
interpolateF(r1.width(), r2.width(), ratio),
interpolateF(r1.height(), r2.height(), ratio));
}
} // namespace anim

View File

@@ -0,0 +1,212 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "ui/effects/animations.h"
#include "base/invoke_queued.h"
#include "ui/ui_utility.h"
#include "styles/style_basic.h"
#include <QtCore/QPointer>
#include <crl/crl_on_main.h>
#include <crl/crl.h>
#include <rpl/filter.h>
#include <range/v3/algorithm/remove_if.hpp>
#include <range/v3/algorithm/remove.hpp>
#include <range/v3/algorithm/find.hpp>
namespace Ui {
namespace Animations {
namespace {
constexpr auto kAnimationTick = crl::time(1000) / st::universalDuration;
constexpr auto kIgnoreUpdatesTimeout = crl::time(4);
Manager *ManagerInstance = nullptr;
} // namespace
void Basic::start() {
Expects(ManagerInstance != nullptr);
if (animating()) {
restart();
} else {
ManagerInstance->start(this);
}
}
void Basic::stop() {
Expects(ManagerInstance != nullptr);
if (animating()) {
ManagerInstance->stop(this);
}
}
void Basic::restart() {
Expects(_started >= 0);
_started = crl::now();
Ensures(_started >= 0);
}
void Basic::markStarted() {
Expects(_started < 0);
_started = crl::now();
Ensures(_started >= 0);
}
void Basic::markStopped() {
Expects(_started >= 0);
_started = -1;
}
Manager::Manager() {
Expects(ManagerInstance == nullptr);
ManagerInstance = this;
crl::on_main_update_requests(
) | rpl::filter([=] {
return (_lastUpdateTime + kIgnoreUpdatesTimeout < crl::now());
}) | rpl::on_next([=] {
update();
}, _lifetime);
}
Manager::~Manager() {
Expects(ManagerInstance == this);
Expects(_active.empty());
Expects(_starting.empty());
ManagerInstance = nullptr;
}
void Manager::start(not_null<Basic*> animation) {
_forceImmediateUpdate = true;
if (_updating) {
_starting.emplace_back(animation.get());
} else {
schedule();
_active.emplace_back(animation.get());
}
}
void Manager::stop(not_null<Basic*> animation) {
if (empty(_active) && empty(_starting)) {
return;
}
const auto value = animation.get();
const auto proj = &ActiveBasicPointer::get;
auto &list = _updating ? _starting : _active;
list.erase(ranges::remove(list, value, proj), end(list));
if (_updating) {
const auto i = ranges::find(_active, value, proj);
if (i != end(_active)) {
*i = nullptr;
_removedWhileUpdating = true;
}
} else if (empty(_active)) {
stopTimer();
}
}
void Manager::update() {
if (_active.empty() || _updating || _scheduled) {
return;
}
const auto now = crl::now();
if (_forceImmediateUpdate) {
_forceImmediateUpdate = false;
}
schedule();
_updating = true;
const auto guard = gsl::finally([&] { _updating = false; });
_lastUpdateTime = now;
const auto isFinished = [&](const ActiveBasicPointer &element) {
return !element.call(now);
};
_active.erase(ranges::remove_if(_active, isFinished), end(_active));
if (_removedWhileUpdating) {
_removedWhileUpdating = false;
const auto proj = &ActiveBasicPointer::get;
_active.erase(ranges::remove(_active, nullptr, proj), end(_active));
}
if (!empty(_starting)) {
_active.insert(
end(_active),
std::make_move_iterator(begin(_starting)),
std::make_move_iterator(end(_starting)));
_starting.clear();
}
}
void Manager::updateQueued() {
Expects(_timerId == 0);
_timerId = -1;
InvokeQueued(delayedCallGuard(), [=] {
Expects(_timerId < 0);
_timerId = 0;
update();
});
}
void Manager::schedule() {
if (_scheduled || _timerId < 0) {
return;
}
stopTimer();
_scheduled = true;
PostponeCall(delayedCallGuard(), [=] {
_scheduled = false;
if (_active.empty()) {
return;
}
if (_forceImmediateUpdate) {
_forceImmediateUpdate = false;
updateQueued();
} else {
const auto next = _lastUpdateTime + kAnimationTick;
const auto now = crl::now();
if (now < next) {
_timerId = startTimer(next - now, Qt::PreciseTimer);
} else {
updateQueued();
}
}
});
}
not_null<const QObject*> Manager::delayedCallGuard() const {
return static_cast<const QObject*>(this);
}
void Manager::stopTimer() {
if (_timerId > 0) {
killTimer(base::take(_timerId));
}
}
void Manager::timerEvent(QTimerEvent *e) {
update();
}
} // namespace Animations
} // namespace Ui

View File

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

View File

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

View File

@@ -0,0 +1,37 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "styles/style_widgets.h"
class Painter;
namespace Ui {
class CrossAnimation {
public:
static void paint(
QPainter &p,
const style::CrossAnimation &st,
style::color color,
int x,
int y,
int outerWidth,
float64 shown,
float64 loading = 0.);
static void paintStaticLoading(
QPainter &p,
const style::CrossAnimation &st,
style::color color,
int x,
int y,
int outerWidth,
float64 shown);
};
} // namespace Ui

View File

@@ -0,0 +1,130 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "ui/effects/cross_line.h"
#include "ui/painter.h"
namespace Ui {
namespace {
[[nodiscard]] float64 StrokeWidth(
const style::CrossLineAnimation &st) noexcept {
return float64(st.stroke)
/ (st.strokeDenominator ? st.strokeDenominator : 1);
}
} // namespace
CrossLineAnimation::CrossLineAnimation(
const style::CrossLineAnimation &st,
bool reversed,
float angle)
: _st(st)
, _reversed(reversed)
, _transparentPen(
Qt::transparent,
StrokeWidth(st),
Qt::SolidLine,
Qt::RoundCap)
, _strokePen(st.fg, StrokeWidth(st), Qt::SolidLine, Qt::RoundCap)
, _line(st.startPosition, st.endPosition) {
_line.setAngle(angle);
}
void CrossLineAnimation::paint(
QPainter &p,
QPoint position,
float64 progress,
std::optional<QColor> colorOverride) {
paint(p, position.x(), position.y(), progress, colorOverride);
}
void CrossLineAnimation::paint(
QPainter &p,
int left,
int top,
float64 progress,
std::optional<QColor> colorOverride) {
if (progress == 0.) {
if (colorOverride) {
_st.icon.paint(p, left, top, _st.icon.width(), *colorOverride);
} else {
_st.icon.paint(p, left, top, _st.icon.width());
}
} else if (progress == 1.) {
auto &complete = colorOverride
? _completeCrossOverride
: _completeCross;
if (complete.isNull()) {
fillFrame(progress, colorOverride);
complete = _frame;
}
p.drawImage(left, top, complete);
} else {
fillFrame(progress, colorOverride);
p.drawImage(left, top, _frame);
}
}
void CrossLineAnimation::fillFrame(
float64 progress,
std::optional<QColor> colorOverride) {
const auto ratio = style::DevicePixelRatio();
if (_frame.isNull()) {
_frame = QImage(
_st.icon.size() * ratio,
QImage::Format_ARGB32_Premultiplied);
_frame.setDevicePixelRatio(ratio);
}
_frame.fill(Qt::transparent);
auto topLine = _line;
topLine.setLength(topLine.length() * progress);
auto bottomLine = topLine.translated(0, _strokePen.widthF() + 1);
auto q = QPainter(&_frame);
PainterHighQualityEnabler hq(q);
const auto colorize = ((colorOverride && colorOverride->alpha() != 255)
|| (!colorOverride && _st.fg->c.alpha() != 255));
const auto color = colorize
? QColor(255, 255, 255)
: colorOverride;
if (color) {
_st.icon.paint(q, 0, 0, _st.icon.width(), *color);
} else {
_st.icon.paint(q, 0, 0, _st.icon.width());
}
if (color) {
auto pen = _strokePen;
pen.setColor(*color);
q.setPen(pen);
} else {
q.setPen(_strokePen);
}
q.drawLine(_reversed ? topLine : bottomLine);
q.setCompositionMode(QPainter::CompositionMode_Source);
q.setPen(_transparentPen);
q.drawLine(_reversed ? bottomLine : topLine);
q.end();
if (colorize) {
style::colorizeImage(
_frame,
colorOverride.value_or(_st.fg->c),
&_frame);
}
}
void CrossLineAnimation::invalidate() {
_completeCross = QImage();
_completeCrossOverride = QImage();
_strokePen = QPen(_st.fg, StrokeWidth(_st), Qt::SolidLine, Qt::RoundCap);
}
} // namespace Ui

View File

@@ -0,0 +1,48 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "styles/style_widgets.h"
namespace Ui {
class CrossLineAnimation {
public:
CrossLineAnimation(
const style::CrossLineAnimation &st,
bool reversed = false,
float angle = 315);
void paint(
QPainter &p,
QPoint position,
float64 progress,
std::optional<QColor> colorOverride = std::nullopt);
void paint(
QPainter &p,
int left,
int top,
float64 progress,
std::optional<QColor> colorOverride = std::nullopt);
void invalidate();
private:
void fillFrame(float64 progress, std::optional<QColor> colorOverride);
const style::CrossLineAnimation &_st;
const bool _reversed;
const QPen _transparentPen;
QPen _strokePen;
QLineF _line;
QImage _frame;
QImage _completeCross;
QImage _completeCrossOverride;
};
} // namespace Ui

View File

@@ -0,0 +1,161 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "ui/effects/fade_animation.h"
#include "ui/ui_utility.h"
#include "ui/painter.h"
namespace Ui {
namespace {
constexpr int kWideScale = 5;
} // namespace
FadeAnimation::FadeAnimation(RpWidget *widget, float64 scale)
: _widget(widget)
, _scale(scale) {
}
bool FadeAnimation::paint(QPainter &p) {
if (_cache.isNull()) return false;
const auto cache = _cache;
auto opacity = _animation.value(_visible ? 1. : 0.);
p.setOpacity(opacity);
if (_scale < 1.) {
PainterHighQualityEnabler hq(p);
auto targetRect = QRect(
(1 - kWideScale) / 2 * _size.width(),
(1 - kWideScale) / 2 * _size.height(),
kWideScale * _size.width(),
kWideScale * _size.height());
auto scale = opacity + (1. - opacity) * _scale;
auto shownWidth = anim::interpolate(
(1 - kWideScale) / 2 * _size.width(),
0,
scale);
auto shownHeight = anim::interpolate(
(1 - kWideScale) / 2 * _size.height(),
0,
scale);
auto margins = QMargins(
shownWidth,
shownHeight,
shownWidth,
shownHeight);
p.drawPixmap(targetRect.marginsAdded(margins), cache);
} else {
p.drawPixmap(0, 0, cache);
}
return true;
}
void FadeAnimation::refreshCache() {
if (!_cache.isNull()) {
_cache = QPixmap();
_cache = grabContent();
Assert(!_cache.isNull());
}
}
QPixmap FadeAnimation::grabContent() {
SendPendingMoveResizeEvents(_widget);
_size = _widget->size();
const auto pixelRatio = style::DevicePixelRatio();
if (_size.isEmpty()) {
auto image = QImage(
pixelRatio,
pixelRatio,
QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::transparent);
return PixmapFromImage(std::move(image));
}
auto widgetContent = GrabWidget(_widget);
if (_scale < 1.) {
auto result = QImage(kWideScale * _size * pixelRatio, QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(pixelRatio);
result.fill(Qt::transparent);
{
Painter p(&result);
p.drawPixmap((kWideScale - 1) / 2 * _size.width(), (kWideScale - 1) / 2 * _size.height(), widgetContent);
}
return PixmapFromImage(std::move(result));
}
return widgetContent;
}
void FadeAnimation::setFinishedCallback(FinishedCallback &&callback) {
_finishedCallback = std::move(callback);
}
void FadeAnimation::setUpdatedCallback(UpdatedCallback &&callback) {
_updatedCallback = std::move(callback);
}
void FadeAnimation::show() {
_visible = true;
stopAnimation();
}
void FadeAnimation::hide() {
_visible = false;
stopAnimation();
}
void FadeAnimation::stopAnimation() {
_animation.stop();
if (!_cache.isNull()) {
_cache = QPixmap();
if (_finishedCallback) {
_finishedCallback();
}
}
if (_visible == _widget->isHidden()) {
_widget->setVisible(_visible);
}
}
void FadeAnimation::fadeIn(int duration) {
if (_visible) return;
_visible = true;
startAnimation(duration);
}
void FadeAnimation::fadeOut(int duration) {
if (!_visible) return;
_visible = false;
startAnimation(duration);
}
void FadeAnimation::startAnimation(int duration) {
if (_cache.isNull()) {
_cache = grabContent();
Assert(!_cache.isNull());
}
auto from = _visible ? 0. : 1.;
auto to = _visible ? 1. : 0.;
_animation.start([this]() { updateCallback(); }, from, to, duration);
updateCallback();
if (_widget->isHidden()) {
_widget->show();
}
}
void FadeAnimation::updateCallback() {
_widget->update();
if (_updatedCallback) {
_updatedCallback(_animation.value(_visible ? 1. : 0.));
}
if (!_animation.animating()) {
stopAnimation();
}
}
} // namespace Ui

View File

@@ -0,0 +1,65 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
#include "styles/style_widgets.h"
namespace Ui {
class FadeAnimation {
public:
FadeAnimation(RpWidget *widget, float64 scale = 1.);
bool paint(QPainter &p);
void refreshCache();
using FinishedCallback = Fn<void()>;
void setFinishedCallback(FinishedCallback &&callback);
using UpdatedCallback = Fn<void(float64)>;
void setUpdatedCallback(UpdatedCallback &&callback);
void show();
void hide();
void fadeIn(int duration);
void fadeOut(int duration);
void finish() {
stopAnimation();
}
bool animating() const {
return _animation.animating();
}
bool visible() const {
return _visible;
}
private:
void startAnimation(int duration);
void stopAnimation();
void updateCallback();
QPixmap grabContent();
RpWidget *_widget = nullptr;
float64 _scale = 1.;
Ui::Animations::Simple _animation;
QSize _size;
QPixmap _cache;
bool _visible = false;
FinishedCallback _finishedCallback;
UpdatedCallback _updatedCallback;
};
} // namespace Ui

View File

@@ -0,0 +1,79 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "ui/effects/frame_generator.h"
#include "ui/image/image_prepare.h"
namespace Ui {
ImageFrameGenerator::ImageFrameGenerator(const QByteArray &bytes)
: _bytes(bytes) {
}
ImageFrameGenerator::ImageFrameGenerator(const QImage &image)
: _image(image) {
}
int ImageFrameGenerator::count() {
return 1;
}
double ImageFrameGenerator::rate() {
return 1.;
}
FrameGenerator::Frame ImageFrameGenerator::renderNext(
QImage storage,
QSize size,
Qt::AspectRatioMode mode) {
return renderCurrent(std::move(storage), size, mode);
}
FrameGenerator::Frame ImageFrameGenerator::renderCurrent(
QImage storage,
QSize size,
Qt::AspectRatioMode mode) {
if (_image.isNull() && !_bytes.isEmpty()) {
_image = Images::Read({
.content = _bytes,
}).image;
_bytes = QByteArray();
}
if (_image.isNull()) {
return {};
}
auto scaled = _image.scaled(
size,
mode,
Qt::SmoothTransformation
).convertToFormat(QImage::Format_ARGB32_Premultiplied);
if (scaled.size() == size) {
return { .image = std::move(scaled) };
}
auto result = QImage(size, QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
const auto skipx = (size.width() - scaled.width()) / 2;
const auto skipy = (size.height() - scaled.height()) / 2;
const auto srcPerLine = scaled.bytesPerLine();
const auto dstPerLine = result.bytesPerLine();
const auto lineBytes = scaled.width() * 4;
auto src = scaled.constBits();
auto dst = result.bits() + (skipx * 4) + (skipy * srcPerLine);
for (auto y = 0, height = scaled.height(); y != height; ++y) {
memcpy(dst, src, lineBytes);
src += srcPerLine;
dst += dstPerLine;
}
return { .image = std::move(result), .last = true };
}
void ImageFrameGenerator::jumpToStart() {
}
} // namespace Ui

View File

@@ -0,0 +1,69 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include <QtGui/QImage>
#include <crl/crl_time.h>
namespace Ui {
class FrameGenerator {
public:
virtual ~FrameGenerator() = default;
// 0 means unknown.
[[nodiscard]] virtual int count() = 0;
// 0. means unknown.
[[nodiscard]] virtual double rate() = 0;
struct Frame {
crl::time duration = 0;
QImage image;
bool last = false;
};
[[nodiscard]] virtual Frame renderNext(
QImage storage,
QSize size,
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) = 0;
[[nodiscard]] virtual Frame renderCurrent(
QImage storage,
QSize size,
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) = 0;
virtual void jumpToStart() = 0;
};
class ImageFrameGenerator final : public Ui::FrameGenerator {
public:
explicit ImageFrameGenerator(const QByteArray &bytes);
explicit ImageFrameGenerator(const QImage &image);
int count() override;
double rate() override;
Frame renderNext(
QImage storage,
QSize size,
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override;
Frame renderCurrent(
QImage storage,
QSize size,
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override;
void jumpToStart() override;
private:
QByteArray _bytes;
QImage _image;
};
[[nodiscard]] bool GoodStorageForFrame(const QImage &storage, QSize size);
[[nodiscard]] QImage CreateFrameStorage(QSize size);
} // namespace Ui

View File

@@ -0,0 +1,30 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "ui/effects/gradient.h"
namespace anim {
QColor gradient_color_at(const QGradientStops &stops, float64 ratio) {
for (auto i = 1; i < stops.size(); i++) {
const auto currentPoint = stops[i].first;
const auto previousPoint = stops[i - 1].first;
if ((ratio <= currentPoint) && (ratio >= previousPoint)) {
return anim::color(
stops[i - 1].second,
stops[i].second,
(ratio - previousPoint) / (currentPoint - previousPoint));
}
}
return QColor();
}
QColor gradient_color_at(const QGradient &gradient, float64 ratio) {
return gradient_color_at(gradient.stops(), ratio);
}
} // namespace anim

View File

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

View File

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

View File

@@ -0,0 +1,114 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
namespace style {
struct FlatLabel;
} // namespace style
namespace Ui {
class NumbersAnimation {
public:
NumbersAnimation(
const style::font &font,
Fn<void()> animationCallback);
void setWidthChangedCallback(Fn<void()> callback) {
_widthChangedCallback = std::move(callback);
}
void setText(const QString &text, int value);
void setDuration(int duration);
void setDisabledMonospace(bool value);
void finishAnimating();
void paint(QPainter &p, int x, int y, int outerWidth);
int countWidth() const;
int maxWidth() const;
private:
struct Digit {
QChar from = QChar(0);
QChar to = QChar(0);
int fromWidth = 0;
int toWidth = 0;
};
void animationCallback();
void realSetText(QString text, int value);
const style::font &_font;
int _duration;
QList<Digit> _digits;
int _digitWidth = 0;
int _fromWidth = 0;
int _toWidth = 0;
int _bothWidth = 0;
Ui::Animations::Simple _a_ready;
QString _delayedText;
int _delayedValue = 0;
int _value = 0;
bool _growing = false;
bool _disabledMonospace = false;
Fn<void()> _animationCallback;
Fn<void()> _widthChangedCallback;
};
struct StringWithNumbers {
static StringWithNumbers FromString(const QString &text) {
return { text };
}
QString text;
int offset = -1;
int length = 0;
};
class LabelWithNumbers : public Ui::RpWidget {
public:
LabelWithNumbers(
QWidget *parent,
const style::FlatLabel &st,
int textTop,
const StringWithNumbers &value);
void setValue(const StringWithNumbers &value);
void finishAnimating();
protected:
void paintEvent(QPaintEvent *e) override;
private:
[[nodiscard]] static QString GetBefore(const StringWithNumbers &value);
[[nodiscard]] static QString GetAfter(const StringWithNumbers &value);
[[nodiscard]] static QString GetNumbers(const StringWithNumbers &value);
void updateNaturalWidth();
const style::FlatLabel &_st;
int _textTop;
QString _before;
QString _after;
NumbersAnimation _numbers;
int _beforeWidth = 0;
int _afterWidth = 0;
Ui::Animations::Simple _beforeWidthAnimation;
};
} // namespace Ui

View File

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

View File

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

View File

@@ -0,0 +1,180 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "ui/effects/path_shift_gradient.h"
#include "base/call_delayed.h"
#include "ui/effects/animations.h"
namespace Ui {
namespace {
constexpr auto kSlideDuration = crl::time(1000);
constexpr auto kWaitDuration = crl::time(1000);
constexpr auto kFullDuration = kSlideDuration + kWaitDuration;
} // namespace
struct PathShiftGradient::AnimationData {
Ui::Animations::Simple animation;
base::flat_set<not_null<PathShiftGradient*>> active;
bool scheduled = false;
};
std::weak_ptr<PathShiftGradient::AnimationData> PathShiftGradient::Animation;
PathShiftGradient::PathShiftGradient(
const style::color &bg,
const style::color &fg,
Fn<void()> animationCallback,
rpl::producer<> paletteUpdated)
: _bg(bg)
, _fg(fg)
, _animationCallback(std::move(animationCallback)) {
refreshColors();
if (!paletteUpdated) {
paletteUpdated = style::PaletteChanged();
}
std::move(
paletteUpdated
) | rpl::on_next([=] {
refreshColors();
}, _lifetime);
}
PathShiftGradient::~PathShiftGradient() {
if (const auto strong = _animation.get()) {
strong->active.erase(this);
}
}
void PathShiftGradient::overrideColors(
const style::color &bg,
const style::color &fg) {
_colorsOverriden = true;
refreshColors(bg, fg);
}
void PathShiftGradient::clearOverridenColors() {
if (!_colorsOverriden) {
return;
}
_colorsOverriden = false;
refreshColors();
}
void PathShiftGradient::startFrame(
int viewportLeft,
int viewportWidth,
int gradientWidth) {
_viewportLeft = viewportLeft;
_viewportWidth = viewportWidth;
_gradientWidth = gradientWidth;
_geometryUpdated = false;
}
void PathShiftGradient::updateGeometry() {
if (_geometryUpdated) {
return;
}
_geometryUpdated = true;
const auto now = crl::now();
const auto period = now % kFullDuration;
if (period >= kSlideDuration) {
_gradientEnabled = false;
return;
}
const auto progress = period / float64(kSlideDuration);
_gradientStart = anim::interpolate(
_viewportLeft - _gradientWidth,
_viewportLeft + _viewportWidth,
progress);
_gradientFinalStop = _gradientStart + _gradientWidth;
_gradientEnabled = true;
}
bool PathShiftGradient::paint(Fn<bool(const Background&)> painter) {
updateGeometry();
if (_gradientEnabled) {
_gradient.setStart(_gradientStart, 0);
_gradient.setFinalStop(_gradientFinalStop, 0);
}
const auto background = _gradientEnabled
? Background(&_gradient)
: _bgOverride
? *_bgOverride
: _bg;
if (!painter(background)) {
return false;
}
activateAnimation();
return true;
}
void PathShiftGradient::activateAnimation() {
if (_animationActive) {
return;
}
_animationActive = true;
if (!_animation) {
_animation = Animation.lock();
if (!_animation) {
_animation = std::make_shared<AnimationData>();
Animation = _animation;
}
}
const auto raw = _animation.get();
if (_animationCallback) {
raw->active.emplace(this);
}
const auto globalCallback = [] {
const auto strong = Animation.lock();
if (!strong) {
return;
}
strong->scheduled = false;
while (!strong->active.empty()) {
const auto entry = strong->active.back();
strong->active.erase(strong->active.end() - 1);
entry->_animationActive = false;
entry->_animationCallback();
}
};
const auto now = crl::now();
const auto period = now % kFullDuration;
if (period >= kSlideDuration) {
const auto tillWaitFinish = kFullDuration - period;
if (!raw->scheduled) {
raw->scheduled = true;
raw->animation.stop();
base::call_delayed(tillWaitFinish, globalCallback);
}
} else {
const auto tillSlideFinish = kSlideDuration - period;
if (!raw->animation.animating()) {
raw->animation.start(globalCallback, 0., 1., tillSlideFinish);
}
}
}
void PathShiftGradient::refreshColors() {
refreshColors(_bg, _fg);
}
void PathShiftGradient::refreshColors(
const style::color &bg,
const style::color &fg) {
_gradient.setStops({
{ 0., bg->c },
{ 0.5, fg->c },
{ 1., bg->c },
});
_bgOverride = _colorsOverriden ? &bg : nullptr;
}
} // namespace Ui

View File

@@ -0,0 +1,65 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "ui/style/style_core_types.h"
#include <QtGui/QLinearGradient>
namespace Ui {
class PathShiftGradient final {
public:
PathShiftGradient(
const style::color &bg,
const style::color &fg,
Fn<void()> animationCallback,
rpl::producer<> paletteUpdated = nullptr);
~PathShiftGradient();
void startFrame(
int viewportLeft,
int viewportWidth,
int gradientWidth);
void overrideColors(const style::color &bg, const style::color &fg);
void clearOverridenColors();
using Background = std::variant<QLinearGradient*, style::color>;
bool paint(Fn<bool(const Background&)> painter);
private:
struct AnimationData;
void refreshColors();
void refreshColors(const style::color &bg, const style::color &fg);
void updateGeometry();
void activateAnimation();
static std::weak_ptr<AnimationData> Animation;
const style::color &_bg;
const style::color &_fg;
const style::color *_bgOverride = nullptr;
QLinearGradient _gradient;
std::shared_ptr<AnimationData> _animation;
const Fn<void()> _animationCallback;
int _viewportLeft = 0;
int _viewportWidth = 0;
int _gradientWidth = 0;
int _gradientStart = 0;
int _gradientFinalStop = 0;
bool _gradientEnabled = false;
bool _geometryUpdated = false;
bool _animationActive = false;
bool _colorsOverriden = false;
rpl::lifetime _lifetime;
};
} // namespace Ui

View File

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

View File

@@ -0,0 +1,123 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "ui/effects/animations.h"
namespace style {
struct InfiniteRadialAnimation;
} // namespace style
namespace Ui {
struct RadialState {
static const int kFull;
float64 shown = 0.;
int arcFrom = 0;
int arcLength = kFull;
};
class RadialAnimation {
public:
template <typename Callback>
RadialAnimation(Callback &&callback);
[[nodiscard]] float64 opacity() const {
return _opacity;
}
[[nodiscard]] bool animating() const {
return _animation.animating();
}
void start(float64 prg);
bool update(float64 prg, bool finished, crl::time ms);
void stop();
void draw(
QPainter &p,
const QRectF &inner,
float64 thickness,
style::color color) const;
[[nodiscard]] RadialState computeState() const;
private:
crl::time _firstStart = 0;
crl::time _lastStart = 0;
crl::time _lastTime = 0;
float64 _opacity = 0.;
anim::value _arcEnd;
anim::value _arcStart;
Ui::Animations::Basic _animation;
bool _finished = false;
};
template <typename Callback>
inline RadialAnimation::RadialAnimation(Callback &&callback)
: _arcStart(0, RadialState::kFull)
, _animation(std::forward<Callback>(callback)) {
}
class InfiniteRadialAnimation {
public:
template <typename Callback>
InfiniteRadialAnimation(
Callback &&callback,
const style::InfiniteRadialAnimation &st);
[[nodiscard]] bool animating() const {
return _workStarted && (!_workFinished || _workFinished > crl::now());
}
void start(crl::time skip = 0);
void stop(anim::type animated = anim::type::normal);
void draw(
QPainter &p,
QPoint position,
int outerWidth);
void draw(
QPainter &p,
QPoint position,
QSize size,
int outerWidth);
static void Draw(
QPainter &p,
const RadialState &state,
QPoint position,
QSize size,
int outerWidth,
QPen pen,
int thickness);
[[nodiscard]] RadialState computeState();
private:
void init();
const style::InfiniteRadialAnimation &_st;
crl::time _workStarted = 0;
crl::time _workFinished = 0;
Ui::Animations::Basic _animation;
rpl::lifetime _lifetime;
};
template <typename Callback>
inline InfiniteRadialAnimation::InfiniteRadialAnimation(
Callback &&callback,
const style::InfiniteRadialAnimation &st)
: _st(st)
, _animation(std::forward<Callback>(callback)) {
init();
}
} // namespace Ui

View File

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

View File

@@ -0,0 +1,72 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include <deque>
namespace Images {
struct CornersMaskRef;
} // namespace Images
namespace style {
struct RippleAnimation;
} // namespace style
namespace Ui {
class RippleAnimation {
public:
// White upon transparent mask,
// like colorizeImage(black-white-mask, white).
RippleAnimation(
const style::RippleAnimation &st,
QImage mask,
Fn<void()> update);
void add(QPoint origin, int startRadius = 0);
void addFading();
void lastStop();
void lastUnstop();
void lastFinish();
void forceRepaint();
void paint(
QPainter &p,
int x,
int y,
int outerWidth,
const QColor *colorOverride = nullptr);
bool empty() const {
return _ripples.empty();
}
static QImage MaskByDrawer(
QSize size,
bool filled,
Fn<void(QPainter &p)> drawer);
static QImage RectMask(QSize size);
static QImage RoundRectMask(QSize size, int radius);
static QImage RoundRectMask(QSize size, Images::CornersMaskRef corners);
static QImage EllipseMask(QSize size);
~RippleAnimation();
private:
void clear();
void clearFinished();
const style::RippleAnimation &_st;
QPixmap _mask;
Fn<void()> _update;
class Ripple;
std::deque<std::unique_ptr<Ripple>> _ripples;
};
} // namespace Ui

View File

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

View File

@@ -0,0 +1,92 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
namespace Ui {
struct ImageSubrect {
not_null<QImage*> image;
QRect rect;
};
class RoundAreaWithShadow final {
public:
static constexpr auto kFramesCount = 32;
[[nodiscard]] static QImage PrepareImage(QSize size);
[[nodiscard]] static QImage PrepareFramesCache(
QSize frame,
int columns = 1);
[[nodiscard]] static QRect FrameCacheRect(
int frameIndex,
int column,
QSize frame);
// Returns center area which could be just filled with a solid color.
static QRect FillWithImage(
QPainter &p,
QRect geometry,
const ImageSubrect &pattern);
RoundAreaWithShadow(QSize inner, QMargins shadow, int twiceRadiusMax);
void setBackgroundColor(const QColor &background);
void setShadowColor(const QColor &shadow);
[[nodiscard]] ImageSubrect validateFrame(
int frameIndex,
float64 scale,
float64 radius);
[[nodiscard]] ImageSubrect validateOverlayMask(
int frameIndex,
QSize innerSize,
float64 radius,
int twiceRadius,
float64 scale);
[[nodiscard]] ImageSubrect validateOverlayShadow(
int frameIndex,
QSize innerSize,
float64 radius,
int twiceRadius,
float64 scale,
const ImageSubrect &mask);
void overlayExpandedBorder(
QPainter &p,
QSize size,
float64 expandRatio,
float64 radiusFrom,
float64 radiusTill,
float64 scale);
private:
[[nodiscard]] QRect validateShadow(
int frameIndex,
float64 scale,
float64 radius);
QRect _inner;
QSize _outer;
QSize _overlay;
std::array<bool, kFramesCount> _validBg = { { false } };
std::array<bool, kFramesCount> _validShadow = { { false } };
std::array<bool, kFramesCount> _validOverlayMask = { { false } };
std::array<bool, kFramesCount> _validOverlayShadow = { { false } };
QColor _background;
QColor _gradient;
QColor _shadow;
QImage _cacheBg;
QImage _shadowParts;
QImage _overlayCacheParts;
QImage _overlayMaskScaled;
QImage _overlayShadowScaled;
QImage _shadowBuffer;
};
} // namespace Ui

View File

@@ -0,0 +1,113 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "ui/effects/show_animation.h"
#include "ui/effects/animations.h"
#include "ui/rp_widget.h"
#include "ui/ui_utility.h"
#include "styles/style_widgets.h"
namespace Ui::Animations {
namespace {
void AnimateWidgets(const Widgets &targets, bool show) {
enum class Finish {
Bad,
Good,
};
struct Object {
base::unique_qptr<Ui::RpWidget> container;
base::weak_qptr<Ui::RpWidget> weakTarget;
};
struct State {
rpl::event_stream<Finish> destroy;
Ui::Animations::Simple animation;
std::vector<Object> objects;
};
auto lifetime = std::make_shared<rpl::lifetime>();
const auto state = lifetime->make_state<State>();
const auto from = show ? 0. : 1.;
const auto to = show ? 1. : 0.;
for (const auto &target : targets) {
state->objects.push_back({
base::make_unique_q<Ui::RpWidget>(target->parentWidget()),
base::make_weak(target),
});
const auto pixmap = Ui::GrabWidget(target);
const auto raw = state->objects.back().container.get();
raw->paintRequest(
) | rpl::on_next([=] {
QPainter p(raw);
p.setOpacity(state->animation.value(to));
p.drawPixmap(QPoint(), pixmap);
}, raw->lifetime());
target->geometryValue(
) | rpl::on_next([=](const QRect &r) {
raw->setGeometry(r);
}, raw->lifetime());
raw->show();
if (!show) {
target->hide();
}
}
state->destroy.events(
) | rpl::take(
1
) | rpl::on_next([=](Finish type) mutable {
if (type == Finish::Good && show) {
for (const auto &object : state->objects) {
if (object.weakTarget) {
object.weakTarget->show();
}
}
}
if (lifetime) {
base::take(lifetime)->destroy();
}
}, *lifetime);
state->animation.start(
[=](auto value) {
for (const auto &object : state->objects) {
if (object.container) {
object.container->update();
}
if (!object.weakTarget && show) {
state->destroy.fire(Finish::Bad);
return;
}
}
if (value == to) {
state->destroy.fire(Finish::Good);
}
},
from,
to,
st::defaultToggle.duration);
}
} // namespace
void ShowWidgets(const Widgets &targets) {
AnimateWidgets(targets, true);
}
void HideWidgets(const Widgets &targets) {
AnimateWidgets(targets, false);
}
} // namespace Ui::Animations

View File

@@ -0,0 +1,20 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Ui::Animations {
using Widgets = std::vector<not_null<Ui::RpWidget*>>;
void ShowWidgets(const Widgets &targets);
void HideWidgets(const Widgets &targets);
} // namespace Ui::Animations

View File

@@ -0,0 +1,89 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "ui/effects/slide_animation.h"
#include <QtGui/QPainter>
namespace Ui {
void SlideAnimation::setSnapshots(
QPixmap leftSnapshot,
QPixmap rightSnapshot) {
Expects(!leftSnapshot.isNull());
Expects(!rightSnapshot.isNull());
_leftSnapshot = std::move(leftSnapshot);
_rightSnapshot = std::move(rightSnapshot);
_leftSnapshot.setDevicePixelRatio(style::DevicePixelRatio());
_rightSnapshot.setDevicePixelRatio(style::DevicePixelRatio());
}
void SlideAnimation::paintFrame(QPainter &p, int x, int y, int outerWidth) {
if (!animating()) {
return;
}
const auto pixelRatio = style::DevicePixelRatio();
const auto state = this->state();
const auto leftCoord = _slideLeft
? anim::interpolate(-_leftSnapshotWidth, 0, state.leftProgress)
: anim::interpolate(0, -_leftSnapshotWidth, state.leftProgress);
const auto rightCoord = _slideLeft
? anim::interpolate(0, _rightSnapshotWidth, state.rightProgress)
: anim::interpolate(_rightSnapshotWidth, 0, state.rightProgress);
if (_overflowHidden) {
const auto leftWidth = (_leftSnapshotWidth + leftCoord);
if (leftWidth > 0) {
p.setOpacity(state.leftAlpha);
p.drawPixmap(
x,
y,
leftWidth,
_leftSnapshotHeight,
_leftSnapshot,
(_leftSnapshot.width() - leftWidth * pixelRatio),
0,
leftWidth * pixelRatio,
_leftSnapshot.height());
}
const auto rightWidth = _rightSnapshotWidth - rightCoord;
if (rightWidth > 0) {
p.setOpacity(state.rightAlpha);
p.drawPixmap(
x + rightCoord,
y,
_rightSnapshot,
0,
0,
rightWidth * pixelRatio,
_rightSnapshot.height());
}
} else {
p.setOpacity(state.leftAlpha);
p.drawPixmap(x + leftCoord, y, _leftSnapshot);
p.setOpacity(state.rightAlpha);
p.drawPixmap(x + rightCoord, y, _rightSnapshot);
}
}
SlideAnimation::State SlideAnimation::state() const {
const auto dt = _animation.value(1.);
const auto easeOut = anim::easeOutCirc(1., dt);
const auto easeIn = anim::easeInCirc(1., dt);
const auto arrivingAlpha = easeIn;
const auto departingAlpha = 1. - easeOut;
auto result = State();
result.leftProgress = _slideLeft ? easeOut : easeIn;
result.leftAlpha = _slideLeft ? arrivingAlpha : departingAlpha;
result.rightProgress = _slideLeft ? easeIn : easeOut;
result.rightAlpha = _slideLeft ? departingAlpha : arrivingAlpha;
return result;
}
} // namespace Ui

View File

@@ -0,0 +1,66 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "ui/effects/animations.h"
namespace Ui {
class SlideAnimation {
public:
struct State {
float64 leftProgress = 0.;
float64 leftAlpha = 0.;
float64 rightProgress = 0.;
float64 rightAlpha = 0.;
};
void setSnapshots(QPixmap leftSnapshot, QPixmap rightSnapshot);
void setOverflowHidden(bool hidden) {
_overflowHidden = hidden;
}
template <typename Lambda>
void start(bool slideLeft, Lambda &&updateCallback, float64 duration);
void paintFrame(QPainter &p, int x, int y, int outerWidth);
[[nodiscard]] State state() const;
[[nodiscard]] bool animating() const {
return _animation.animating();
}
private:
Ui::Animations::Simple _animation;
QPixmap _leftSnapshot;
QPixmap _rightSnapshot;
bool _slideLeft = false;
bool _overflowHidden = true;
int _leftSnapshotWidth = 0;
int _leftSnapshotHeight = 0;
int _rightSnapshotWidth = 0;
};
template <typename Lambda>
void SlideAnimation::start(
bool slideLeft,
Lambda &&updateCallback,
float64 duration) {
_slideLeft = slideLeft;
if (_slideLeft) {
std::swap(_leftSnapshot, _rightSnapshot);
}
const auto pixelRatio = style::DevicePixelRatio();
_leftSnapshotWidth = _leftSnapshot.width() / pixelRatio;
_leftSnapshotHeight = _leftSnapshot.height() / pixelRatio;
_rightSnapshotWidth = _rightSnapshot.width() / pixelRatio;
_animation.start(std::forward<Lambda>(updateCallback), 0., 1., duration);
}
} // namespace Ui

View File

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

View File

@@ -0,0 +1,117 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include <crl/crl_time.h>
namespace Images {
struct CornersMaskRef;
} // namespace Images
namespace Ui {
struct SpoilerMessDescriptor {
crl::time particleFadeInDuration = 0;
crl::time particleShownDuration = 0;
crl::time particleFadeOutDuration = 0;
float64 particleSizeMin = 0.;
float64 particleSizeMax = 0.;
float64 particleSpeedMin = 0.;
float64 particleSpeedMax = 0.;
int particleSpritesCount = 0;
int particlesCount = 0;
int canvasSize = 0;
int framesCount = 0;
crl::time frameDuration = 0;
};
struct SpoilerMessFrame {
not_null<const QImage*> image;
QRect source;
};
void FillSpoilerRect(
QPainter &p,
QRect rect,
const SpoilerMessFrame &frame,
QPoint originShift = {});
void FillSpoilerRect(
QPainter &p,
QRect rect,
Images::CornersMaskRef mask,
const SpoilerMessFrame &frame,
QImage &cornerCache,
QPoint originShift = {});
class SpoilerMessCached final {
public:
SpoilerMessCached(
QImage image,
int framesCount,
crl::time frameDuration,
int canvasSize);
SpoilerMessCached(const SpoilerMessCached &mask, const QColor &color);
[[nodiscard]] SpoilerMessFrame frame(int index) const;
[[nodiscard]] SpoilerMessFrame frame() const; // Current by time.
[[nodiscard]] crl::time frameDuration() const;
[[nodiscard]] int framesCount() const;
[[nodiscard]] int canvasSize() const;
struct Validator {
crl::time frameDuration = 0;
int framesCount = 0;
int canvasSize = 0;
};
[[nodiscard]] QByteArray serialize() const;
[[nodiscard]] static std::optional<SpoilerMessCached> FromSerialized(
QByteArray data,
std::optional<Validator> validator = {});
private:
QImage _image;
crl::time _frameDuration = 0;
int _framesCount = 0;
int _canvasSize = 0;
};
// Works with default frame duration and default frame count.
class SpoilerAnimationManager;
class SpoilerAnimation final {
public:
explicit SpoilerAnimation(Fn<void()> repaint);
~SpoilerAnimation();
[[nodiscard]] int index(crl::time now, bool paused);
[[nodiscard]] Fn<void()> repaintCallback() const;
private:
friend class SpoilerAnimationManager;
[[nodiscard]] bool repaint(crl::time now);
const Fn<void()> _repaint;
crl::time _accumulated = 0;
crl::time _last = 0;
bool _animating : 1 = false;
bool _scheduled : 1 = false;
};
[[nodiscard]] SpoilerMessCached GenerateSpoilerMess(
const SpoilerMessDescriptor &descriptor);
void PreloadTextSpoilerMask();
[[nodiscard]] const SpoilerMessCached &DefaultTextSpoilerMask();
void PreloadImageSpoiler();
[[nodiscard]] const SpoilerMessCached &DefaultImageSpoiler();
} // namespace Ui

View File

@@ -0,0 +1,917 @@
// 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 "emoji_config.h"
#include "emoji_suggestions_helper.h"
#include "base/bytes.h"
#include "base/openssl_help.h"
#include "base/parse_helper.h"
#include "base/debug_log.h"
#include "ui/style/style_core.h"
#include "ui/integration.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "styles/style_basic.h"
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QFile>
#include <QtCore/QDir>
#include <crl/crl_async.h>
namespace Ui {
namespace Emoji {
namespace {
constexpr auto kUniversalSize = 72;
constexpr auto kImagesPerRow = 32;
constexpr auto kImageRowsPerSprite = 16;
constexpr auto kSetVersion = uint32(7);
constexpr auto kCacheVersion = uint32(9);
constexpr auto kMaxId = uint32(1 << 8);
#ifdef Q_OS_MAC
constexpr auto kScaleForTouchBar = 150;
#endif
enum class ConfigResult {
Invalid,
BadVersion,
Good,
};
// Right now we can't allow users of Ui::Emoji to create custom sizes.
// Any Instance::Instance() can invalidate Universal.id() and sprites.
// So all Instance::Instance() should happen before async generations.
class Instance {
public:
explicit Instance(int size);
bool cached() const;
void draw(QPainter &p, EmojiPtr emoji, int x, int y);
private:
void readCache();
void generateCache();
void checkUniversalImages();
void pushSprite(QImage &&data);
int _id = 0;
int _size = 0;
std::vector<QPixmap> _sprites;
base::binary_guard _generating;
bool _unsupported = false;
};
auto SizeNormal = -1;
auto SizeLarge = -1;
auto SpritesCount = -1;
auto InstanceNormal = std::unique_ptr<Instance>();
auto InstanceLarge = std::unique_ptr<Instance>();
auto Universal = std::shared_ptr<UniversalImages>();
auto CanClearUniversal = false;
auto WaitingToSwitchBackToId = 0;
auto Updates = rpl::event_stream<>();
#ifdef Q_OS_MAC
auto TouchbarSize = -1;
auto TouchbarInstance = std::unique_ptr<Instance>();
auto TouchbarEmoji = (Instance*)nullptr;
#endif
auto MainEmojiMap = std::map<int, QPixmap>();
auto OtherEmojiMap = base::flat_map<int, std::map<int, QPixmap>>();
int RowsCount(int index) {
if (index + 1 < SpritesCount) {
return kImageRowsPerSprite;
}
const auto count = internal::FullCount()
- (index * kImagesPerRow * kImageRowsPerSprite);
return (count / kImagesPerRow)
+ ((count % kImagesPerRow) ? 1 : 0);
}
QString CacheFileNameMask(int size) {
return "cache_" + QString::number(size) + '_';
}
QString CacheFilePath(int size, int index) {
return internal::CacheFileFolder()
+ '/'
+ CacheFileNameMask(size)
+ QString::number(index);
}
QString CurrentSettingPath() {
return internal::CacheFileFolder() + "/current";
}
bool IsValidSetId(int id) {
return (id == 0) || (id > 0 && id < kMaxId);
}
uint32 ComputeVersion(int id) {
Expects(IsValidSetId(id));
static_assert(kCacheVersion > 0 && kCacheVersion < (1 << 16));
static_assert(kSetVersion > 0 && kSetVersion < (1 << 8));
return uint32(kCacheVersion)
| (uint32(kSetVersion) << 16)
| (uint32(id) << 24);
}
int ReadCurrentSetId() {
const auto path = CurrentSettingPath();
auto file = QFile(path);
if (!file.open(QIODevice::ReadOnly)) {
return 0;
}
auto stream = QDataStream(&file);
stream.setVersion(QDataStream::Qt_5_1);
auto id = qint32(0);
stream >> id;
return (stream.status() == QDataStream::Ok && IsValidSetId(id))
? id
: 0;
}
void ApplyUniversalImages(std::shared_ptr<UniversalImages> images) {
Universal = std::move(images);
CanClearUniversal = false;
MainEmojiMap.clear();
OtherEmojiMap.clear();
Updates.fire({});
}
void SwitchToSetPrepared(int id, std::shared_ptr<UniversalImages> images) {
WaitingToSwitchBackToId = 0;
auto setting = QFile(CurrentSettingPath());
if (!id) {
setting.remove();
} else if (setting.open(QIODevice::WriteOnly)) {
auto stream = QDataStream(&setting);
stream.setVersion(QDataStream::Qt_5_1);
stream << qint32(id);
}
ApplyUniversalImages(std::move(images));
}
[[nodiscard]] ConfigResult ValidateConfig(int id) {
Expects(IsValidSetId(id));
if (!id) {
return ConfigResult::Good;
}
constexpr auto kSizeLimit = 65536;
auto config = QFile(internal::SetDataPath(id) + "/config.json");
if (!config.open(QIODevice::ReadOnly) || config.size() > kSizeLimit) {
return ConfigResult::Invalid;
}
auto error = QJsonParseError{ 0, QJsonParseError::NoError };
const auto document = QJsonDocument::fromJson(
base::parse::stripComments(config.readAll()),
&error);
config.close();
if (error.error != QJsonParseError::NoError) {
return ConfigResult::Invalid;
}
if (document.object()["id"].toInt() != id) {
return ConfigResult::Invalid;
} else if (document.object()["version"].toInt() != kSetVersion) {
return ConfigResult::BadVersion;
}
return ConfigResult::Good;
}
void ClearCurrentSetIdSync() {
Expects(Universal != nullptr);
const auto id = Universal->id();
if (!id) {
return;
}
const auto newId = 0;
auto universal = std::make_shared<UniversalImages>(newId);
universal->ensureLoaded();
// Start loading the set when possible.
ApplyUniversalImages(std::move(universal));
WaitingToSwitchBackToId = id;
}
void SaveToFile(int id, const QImage &image, int size, int index) {
Expects(image.bytesPerLine() == image.width() * 4);
QFile f(CacheFilePath(size, index));
if (!f.open(QIODevice::WriteOnly)) {
if (!QDir::current().mkpath(internal::CacheFileFolder())
|| !f.open(QIODevice::WriteOnly)) {
LOG(("App Error: Could not open emoji cache '%1' for size %2_%3"
).arg(f.fileName()
).arg(size
).arg(index));
return;
}
}
const auto write = [&](bytes::const_span data) {
return f.write(
reinterpret_cast<const char*>(data.data()),
data.size()
) == data.size();
};
const uint32 header[] = {
uint32(ComputeVersion(id)),
uint32(size),
uint32(image.width()),
uint32(image.height()),
};
const auto data = bytes::const_span(
reinterpret_cast<const bytes::type*>(image.bits()),
image.width() * image.height() * 4);
if (!write(bytes::make_span(header))
|| !write(data)
|| !write(openssl::Sha256(bytes::make_span(header), data))
|| false) {
LOG(("App Error: Could not write emoji cache '%1' for size %2"
).arg(f.fileName()
).arg(size));
}
}
QImage LoadFromFile(int id, int size, int index) {
const auto rows = RowsCount(index);
const auto width = kImagesPerRow * size;
const auto height = rows * size;
const auto fileSize = 4 * sizeof(uint32)
+ (width * height * 4)
+ openssl::kSha256Size;
QFile f(CacheFilePath(size, index));
if (!f.exists()
|| f.size() != fileSize
|| !f.open(QIODevice::ReadOnly)) {
return QImage();
}
const auto read = [&](bytes::span data) {
return f.read(
reinterpret_cast<char*>(data.data()),
data.size()
) == data.size();
};
uint32 header[4] = { 0 };
if (!read(bytes::make_span(header))
|| header[0] != ComputeVersion(id)
|| header[1] != size
|| header[2] != width
|| header[3] != height) {
return QImage();
}
auto result = QImage(
width,
height,
QImage::Format_ARGB32_Premultiplied);
Assert(result.bytesPerLine() == width * 4);
const auto data = bytes::make_span(
reinterpret_cast<bytes::type*>(result.bits()),
width * height * 4);
auto signature = bytes::vector(openssl::kSha256Size);
if (!read(data)
|| !read(signature)
//|| (bytes::compare(
// signature,
// openssl::Sha256(bytes::make_span(header), data)) != 0)
|| false) {
return QImage();
}
// This should remove a non necessary detach on Retina screens later.
result.setDevicePixelRatio(style::DevicePixelRatio());
crl::async([=, signature = std::move(signature)] {
// This should not happen (invalid signature),
// so we delay this check and fix only the next launch.
const auto data = bytes::make_span(
reinterpret_cast<const bytes::type*>(result.bits()),
width * height * 4);
const auto result = bytes::compare(
signature,
openssl::Sha256(bytes::make_span(header), data));
if (result != 0) {
QFile(CacheFilePath(size, index)).remove();
}
});
return result;
}
std::vector<QImage> LoadSprites(int id) {
Expects(IsValidSetId(id));
Expects(SpritesCount > 0);
auto result = std::vector<QImage>();
const auto folder = (id != 0)
? internal::SetDataPath(id) + '/'
: QStringLiteral(":/gui/emoji/");
const auto base = folder + "emoji_";
return ranges::views::ints(
0,
SpritesCount
) | ranges::views::transform([&](int index) {
return base + QString::number(index + 1) + ".webp";
}) | ranges::views::transform([](const QString &path) {
return QImage(path, "WEBP").convertToFormat(
QImage::Format_ARGB32_Premultiplied);
}) | ranges::to_vector;
}
std::vector<QImage> LoadAndValidateSprites(int id) {
Expects(IsValidSetId(id));
Expects(SpritesCount > 0);
const auto config = ValidateConfig(id);
if (config != ConfigResult::Good) {
return {};
}
auto result = LoadSprites(id);
const auto sizes = ranges::views::ints(
0,
SpritesCount
) | ranges::views::transform([](int index) {
return QSize(
kImagesPerRow * kUniversalSize,
RowsCount(index) * kUniversalSize);
});
const auto good = ranges::views::zip_with(
[](const QImage &data, QSize size) { return data.size() == size; },
result,
sizes);
if (ranges::find(good, false) != end(good)) {
return {};
}
return result;
}
void ClearUniversalChecked() {
Expects(InstanceNormal != nullptr && InstanceLarge != nullptr);
if (Universal
&& InstanceNormal->cached()
&& InstanceLarge->cached()) {
if (CanClearUniversal) {
Universal->clear();
}
ClearIrrelevantCache();
}
}
} // namespace
namespace internal {
QString CacheFileFolder() {
return Integration::Instance().emojiCacheFolder();
}
QString SetDataPath(int id) {
Expects(IsValidSetId(id) && id != 0);
return CacheFileFolder() + "/set" + QString::number(id);
}
} // namespace internal
UniversalImages::UniversalImages(int id) : _id(id) {
Expects(IsValidSetId(id));
}
int UniversalImages::id() const {
return _id;
}
bool UniversalImages::ensureLoaded() {
Expects(SpritesCount > 0);
if (!_sprites.empty()) {
return true;
}
_sprites = LoadAndValidateSprites(_id);
return !_sprites.empty();
}
void UniversalImages::clear() {
_sprites.clear();
}
void UniversalImages::draw(
QPainter &p,
EmojiPtr emoji,
int size,
int x,
int y) const {
Expects(emoji->sprite() < _sprites.size());
const auto large = kUniversalSize;
const auto &original = _sprites[emoji->sprite()];
const auto data = original.bits();
const auto stride = original.bytesPerLine();
const auto format = original.format();
const auto row = emoji->row();
const auto column = emoji->column();
auto single = QImage(
data + (row * kImagesPerRow * large + column) * large * 4,
large,
large,
stride,
format
).scaled(
size,
size,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
single.setDevicePixelRatio(p.device()->devicePixelRatio());
p.drawImage(x, y, single);
}
QImage UniversalImages::generate(int size, int index) const {
Expects(size > 0);
Expects(index < _sprites.size());
const auto rows = RowsCount(index);
const auto large = kUniversalSize;
const auto &original = _sprites[index];
const auto data = original.bits();
const auto stride = original.bytesPerLine();
const auto format = original.format();
auto result = QImage(
size * kImagesPerRow,
size * rows,
QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
{
QPainter p(&result);
for (auto y = 0; y != rows; ++y) {
for (auto x = 0; x != kImagesPerRow; ++x) {
const auto single = QImage(
data + (y * kImagesPerRow * large + x) * large * 4,
large,
large,
stride,
format
).scaled(
size,
size,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
p.drawImage(
x * size,
y * size,
single);
}
}
}
SaveToFile(_id, result, size, index);
return result;
}
void Init() {
internal::Init();
const auto count = internal::FullCount();
const auto persprite = kImagesPerRow * kImageRowsPerSprite;
SpritesCount = (count / persprite) + ((count % persprite) ? 1 : 0);
SizeNormal = st::emojiSize * style::DevicePixelRatio();
SizeLarge = int(style::ConvertScale(18 * 4 / 3., style::Scale())) * style::DevicePixelRatio();
Universal = std::make_shared<UniversalImages>(ReadCurrentSetId());
CanClearUniversal = false;
InstanceNormal = std::make_unique<Instance>(SizeNormal);
InstanceLarge = std::make_unique<Instance>(SizeLarge);
#ifdef Q_OS_MAC
if (style::Scale() != kScaleForTouchBar) {
TouchbarSize = int(style::ConvertScale(18 * 4 / 3.,
kScaleForTouchBar * style::DevicePixelRatio()));
TouchbarInstance = std::make_unique<Instance>(TouchbarSize);
TouchbarEmoji = TouchbarInstance.get();
} else {
TouchbarEmoji = InstanceLarge.get();
}
#endif
}
void Clear() {
MainEmojiMap.clear();
OtherEmojiMap.clear();
InstanceNormal = nullptr;
InstanceLarge = nullptr;
#ifdef Q_OS_MAC
TouchbarInstance = nullptr;
TouchbarEmoji = nullptr;
#endif
}
void ClearIrrelevantCache() {
Expects(SizeNormal > 0);
Expects(SizeLarge > 0);
crl::async([] {
const auto folder = internal::CacheFileFolder();
const auto list = QDir(folder).entryList(QDir::Files);
const auto good1 = CacheFileNameMask(SizeNormal);
const auto good2 = CacheFileNameMask(SizeLarge);
const auto good3full = CurrentSettingPath();
for (const auto &name : list) {
if (!name.startsWith(good1) && !name.startsWith(good2)) {
const auto full = folder + '/' + name;
if (full != good3full) {
QFile(full).remove();
}
}
}
});
}
int CurrentSetId() {
Expects(Universal != nullptr);
return Universal->id();
}
int NeedToSwitchBackToId() {
return WaitingToSwitchBackToId;
}
void ClearNeedSwitchToId() {
if (!WaitingToSwitchBackToId) {
return;
}
WaitingToSwitchBackToId = 0;
QFile(CurrentSettingPath()).remove();
}
void SwitchToSet(int id, Fn<void(bool)> callback) {
Expects(IsValidSetId(id));
if (Universal && Universal->id() == id) {
callback(true);
return;
}
crl::async([=] {
auto universal = std::make_shared<UniversalImages>(id);
if (!universal->ensureLoaded()) {
crl::on_main([=] {
callback(false);
});
} else {
crl::on_main([=, universal = std::move(universal)]() mutable {
SwitchToSetPrepared(id, std::move(universal));
callback(true);
});
}
});
}
bool SetIsReady(int id) {
Expects(IsValidSetId(id));
if (!id) {
return true;
}
const auto folder = internal::SetDataPath(id) + '/';
auto names = ranges::views::ints(
0,
SpritesCount + 1
) | ranges::views::transform([](int index) {
return index
? "emoji_" + QString::number(index) + ".webp"
: QString("config.json");
});
const auto bad = ranges::find_if(names, [&](const QString &name) {
return !QFile(folder + name).exists();
});
return (bad == names.end());
}
rpl::producer<> Updated() {
return Updates.events();
}
int GetSizeNormal() {
Expects(SizeNormal > 0);
return SizeNormal;
}
int GetSizeLarge() {
Expects(SizeLarge > 0);
return SizeLarge;
}
#ifdef Q_OS_MAC
int GetSizeTouchbar() {
return (style::Scale() == kScaleForTouchBar)
? GetSizeLarge()
: TouchbarSize;
}
#endif
One::One(
const QString &id,
EmojiPtr original,
uint32 index,
bool hasPostfix,
bool colorizable,
const CreationTag &)
: _id(id)
, _original(original)
, _index(index)
, _hasPostfix(hasPostfix)
, _colorizable(colorizable) {
Expects(!_colorizable || !colored());
}
int One::variantsCount() const {
return hasVariants() ? 5 : 0;
}
int One::variantIndex(EmojiPtr variant) const {
return (variant - original());
}
EmojiPtr One::variant(int index) const {
return (index >= 0 && index <= variantsCount()) ? (original() + index) : this;
}
QString IdFromOldKey(uint64 oldKey) {
auto code = uint32(oldKey >> 32);
auto code2 = uint32(oldKey & 0xFFFFFFFFLLU);
if (!code && code2) {
code = base::take(code2);
}
if ((code & 0xFFFF0000U) != 0xFFFF0000U) { // code and code2 contain the whole id
auto result = QString();
result.reserve(4);
auto addCode = [&result](uint32 code) {
if (auto high = (code >> 16)) {
result.append(QChar(static_cast<ushort>(high & 0xFFFFU)));
}
result.append(QChar(static_cast<ushort>(code & 0xFFFFU)));
};
addCode(code);
if (code2) addCode(code2);
return result;
}
// old sequence
auto sequenceIndex = int(code & 0xFFFFU);
switch (sequenceIndex) {
case 0: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7");
case 1: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6");
case 2: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6\xe2\x80\x8d\xf0\x9f\x91\xa6");
case 3: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7");
case 4: return QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6");
case 5: return QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7");
case 6: return QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6");
case 7: return QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6\xe2\x80\x8d\xf0\x9f\x91\xa6");
case 8: return QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7");
case 9: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6");
case 10: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7");
case 11: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6");
case 12: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6\xe2\x80\x8d\xf0\x9f\x91\xa6");
case 13: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7");
case 14: return QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9");
case 15: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8");
case 16: return QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9");
case 17: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8");
case 18: return QString::fromUtf8("\xf0\x9f\x91\x81\xe2\x80\x8d\xf0\x9f\x97\xa8");
}
return QString();
}
QVector<EmojiPtr> GetDefaultRecent() {
const auto defaultRecent = {
0xD83DDE02LLU,
0xD83DDE18LLU,
0x2764LLU,
0xD83DDE0DLLU,
0xD83DDE0ALLU,
0xD83DDE01LLU,
0xD83DDC4DLLU,
0x263ALLU,
0xD83DDE14LLU,
0xD83DDE04LLU,
0xD83DDE2DLLU,
0xD83DDC8BLLU,
0xD83DDE12LLU,
0xD83DDE33LLU,
0xD83DDE1CLLU,
0xD83DDE48LLU,
0xD83DDE09LLU,
0xD83DDE03LLU,
0xD83DDE22LLU,
0xD83DDE1DLLU,
0xD83DDE31LLU,
0xD83DDE21LLU,
0xD83DDE0FLLU,
0xD83DDE1ELLU,
0xD83DDE05LLU,
0xD83DDE1ALLU,
0xD83DDE4ALLU,
0xD83DDE0CLLU,
0xD83DDE00LLU,
0xD83DDE0BLLU,
0xD83DDE06LLU,
0xD83DDC4CLLU,
0xD83DDE10LLU,
0xD83DDE15LLU,
};
auto result = QVector<EmojiPtr>();
for (const auto oldKey : defaultRecent) {
if (const auto emoji = FromOldKey(oldKey)) {
result.push_back(emoji);
}
}
return result;
}
const QPixmap &SinglePixmap(EmojiPtr emoji, int fontHeight) {
const auto factor = style::DevicePixelRatio();
auto &map = (fontHeight == st::normalFont->height * factor)
? MainEmojiMap
: OtherEmojiMap[fontHeight];
auto i = map.find(emoji->index());
if (i != end(map)) {
return i->second;
}
auto image = QImage(
SizeNormal + st::emojiPadding * factor * 2,
fontHeight,
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(factor);
image.fill(Qt::transparent);
{
QPainter p(&image);
PainterHighQualityEnabler hq(p);
Draw(
p,
emoji,
SizeNormal,
st::emojiPadding,
(fontHeight - SizeNormal) / (2 * factor));
}
return map.emplace(
emoji->index(),
PixmapFromImage(std::move(image))
).first->second;
}
void Draw(QPainter &p, EmojiPtr emoji, int size, int x, int y) {
#ifdef Q_OS_MAC
const auto s = (style::Scale() == kScaleForTouchBar)
? SizeLarge
: TouchbarSize;
if (size == s) {
TouchbarEmoji->draw(p, emoji, x, y);
return;
}
#endif
if (size == SizeNormal) {
InstanceNormal->draw(p, emoji, x, y);
} else if (size == SizeLarge) {
InstanceLarge->draw(p, emoji, x, y);
} else {
Unexpected("Size in Ui::Emoji::Draw.");
}
}
Instance::Instance(int size) : _id(Universal->id()), _size(size) {
Expects(Universal != nullptr);
readCache();
if (!cached()) {
generateCache();
}
}
bool Instance::cached() const {
Expects(Universal != nullptr);
return (Universal->id() == _id) && (_sprites.size() == SpritesCount);
}
void Instance::draw(QPainter &p, EmojiPtr emoji, int x, int y) {
if (_unsupported) {
return;
} else if (Universal && Universal->id() != _id) {
generateCache();
}
const auto sprite = emoji->sprite();
if (sprite >= _sprites.size()) {
Assert(Universal != nullptr);
Universal->draw(p, emoji, _size, x, y);
return;
}
p.drawPixmap(
QPoint(x, y),
_sprites[sprite],
QRect(emoji->column() * _size, emoji->row() * _size, _size, _size));
}
void Instance::readCache() {
for (auto i = 0; i != SpritesCount; ++i) {
auto image = LoadFromFile(_id, _size, i);
if (image.isNull()) {
return;
}
pushSprite(std::move(image));
}
}
void Instance::checkUniversalImages() {
Expects(Universal != nullptr);
if (_id != Universal->id()) {
_id = Universal->id();
_generating = nullptr;
_sprites.clear();
}
if (!Universal->ensureLoaded()) {
if (Universal->id() != 0) {
ClearCurrentSetIdSync();
} else {
_unsupported = true;
}
}
}
void Instance::generateCache() {
checkUniversalImages();
const auto cachePath = internal::CacheFileFolder();
if (cachePath.isEmpty()) {
return;
}
const auto size = _size;
const auto index = _sprites.size();
crl::async([
=,
universal = Universal,
guard = _generating.make_guard()
]() mutable {
auto image = universal->generate(size, index);
crl::on_main(std::move(guard), [
=,
image = std::move(image)
]() mutable {
if (universal != Universal) {
return;
}
pushSprite(std::move(image));
if (cached()) {
ClearUniversalChecked();
} else {
generateCache();
}
});
});
}
void Instance::pushSprite(QImage &&data) {
_sprites.push_back(PixmapFromImage(std::move(data)));
_sprites.back().setDevicePixelRatio(style::DevicePixelRatio());
}
const std::shared_ptr<UniversalImages> &SourceImages() {
return Universal;
}
void ClearSourceImages(const std::shared_ptr<UniversalImages> &images) {
if (images == Universal) {
CanClearUniversal = true;
ClearUniversalChecked();
}
}
void ReplaceSourceImages(std::shared_ptr<UniversalImages> images) {
Expects(images != nullptr);
if (Universal->id() == images->id()) {
Universal = std::move(images);
}
}
} // namespace Emoji
} // namespace Ui

View File

@@ -0,0 +1,184 @@
// 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/basic_types.h"
#include "base/binary_guard.h"
#include "base/qt/qt_string_view.h"
#include "emoji.h"
#include <QtGui/QPainter>
#include <QtGui/QPixmap>
#include <rpl/producer.h>
namespace Ui {
namespace Emoji {
namespace internal {
[[nodiscard]] QString CacheFileFolder();
[[nodiscard]] QString SetDataPath(int id);
} // namespace internal
void Init();
void Clear();
void ClearIrrelevantCache();
// Thread safe, callback is called on main thread.
void SwitchToSet(int id, Fn<void(bool)> callback);
[[nodiscard]] int CurrentSetId();
[[nodiscard]] int NeedToSwitchBackToId();
void ClearNeedSwitchToId();
[[nodiscard]] bool SetIsReady(int id);
[[nodiscard]] rpl::producer<> Updated();
[[nodiscard]] int GetSizeNormal();
[[nodiscard]] int GetSizeLarge();
#ifdef Q_OS_MAC
[[nodiscard]] int GetSizeTouchbar();
#endif
class One {
struct CreationTag {
};
public:
One(One &&other) = default;
One(
const QString &id,
EmojiPtr original,
uint32 index,
bool hasPostfix,
bool colorizable,
const CreationTag &);
[[nodiscard]] QString id() const {
return _id;
}
[[nodiscard]] QString text() const {
return hasPostfix() ? (_id + QChar(kPostfix)) : _id;
}
[[nodiscard]] bool colored() const {
return (_original != nullptr);
}
[[nodiscard]] EmojiPtr original() const {
return _original ? _original : this;
}
[[nodiscard]] QString nonColoredId() const {
return original()->id();
}
[[nodiscard]] bool hasPostfix() const {
return _hasPostfix;
}
[[nodiscard]] bool hasVariants() const {
return _colorizable || colored();
}
[[nodiscard]] int variantsCount() const;
[[nodiscard]] int variantIndex(EmojiPtr variant) const;
[[nodiscard]] EmojiPtr variant(int index) const;
[[nodiscard]] int index() const {
return _index;
}
[[nodiscard]] int sprite() const {
return int(_index >> 9);
}
[[nodiscard]] int row() const {
return int((_index >> 5) & 0x0FU);
}
[[nodiscard]] int column() const {
return int(_index & 0x1FU);
}
[[nodiscard]] QString toUrl() const {
return "emoji://e." + QString::number(index());
}
private:
const QString _id;
const EmojiPtr _original = nullptr;
const uint32 _index = 0;
const bool _hasPostfix = false;
const bool _colorizable = false;
friend void internal::Init();
};
[[nodiscard]] inline EmojiPtr FromUrl(const QString &url) {
auto start = qstr("emoji://e.");
if (url.startsWith(start)) {
return internal::ByIndex(base::StringViewMid(url, start.size()).toInt()); // skip emoji://e.
}
return nullptr;
}
[[nodiscard]] inline EmojiPtr Find(const QChar *start, const QChar *end, int *outLength = nullptr) {
return internal::Find(start, end, outLength);
}
[[nodiscard]] inline EmojiPtr Find(QStringView text, int *outLength = nullptr) {
return Find(text.begin(), text.end(), outLength);
}
[[nodiscard]] QString IdFromOldKey(uint64 oldKey);
[[nodiscard]] inline EmojiPtr FromOldKey(uint64 oldKey) {
return Find(IdFromOldKey(oldKey));
}
[[nodiscard]] inline int ColorIndexFromCode(uint32 code) {
switch (code) {
case 0xD83CDFFBU: return 1;
case 0xD83CDFFCU: return 2;
case 0xD83CDFFDU: return 3;
case 0xD83CDFFEU: return 4;
case 0xD83CDFFFU: return 5;
}
return 0;
}
[[nodiscard]] inline int ColorIndexFromOldKey(uint64 oldKey) {
return ColorIndexFromCode(uint32(oldKey & 0xFFFFFFFFLLU));
}
QVector<EmojiPtr> GetDefaultRecent();
const QPixmap &SinglePixmap(EmojiPtr emoji, int fontHeight);
void Draw(QPainter &p, EmojiPtr emoji, int size, int x, int y);
class UniversalImages {
public:
explicit UniversalImages(int id);
int id() const;
bool ensureLoaded();
void clear();
void draw(QPainter &p, EmojiPtr emoji, int size, int x, int y) const;
// This method must be thread safe and so it is called after
// the _id value is fixed and all _sprites are loaded.
QImage generate(int size, int index) const;
private:
const int _id = 0;
std::vector<QImage> _sprites;
};
[[nodiscard]] const std::shared_ptr<UniversalImages> &SourceImages();
void ClearSourceImages(const std::shared_ptr<UniversalImages> &images);
} // namespace Emoji
} // namespace Ui

View File

@@ -0,0 +1,41 @@
// 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 FocusPersister {
public:
FocusPersister(QWidget *parent, QWidget *steal = nullptr)
: _weak(GrabFocused(parent)) {
if (steal) {
steal->setFocus();
}
}
~FocusPersister() {
if (auto strong = _weak.data()) {
if (auto window = strong->window()) {
if (window->focusWidget() != strong) {
strong->setFocus();
}
}
}
}
private:
static QWidget *GrabFocused(QWidget *parent) {
if (auto window = parent ? parent->window() : nullptr) {
return window->focusWidget();
}
return nullptr;
}
QPointer<QWidget> _weak;
};
} // namespace Ui

View File

@@ -0,0 +1,290 @@
// 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/gl/gl_detection.h"
#include "ui/gl/gl_shader.h"
#include "ui/integration.h"
#include "base/debug_log.h"
#include "base/platform/base_platform_info.h"
#include <QtCore/QSet>
#include <QtCore/QFile>
#include <QtGui/QWindow>
#include <QtGui/QOpenGLContext>
#include <QtGui/QOpenGLFunctions>
#include <QOpenGLWidget>
#ifdef DESKTOP_APP_USE_ANGLE
#include <QtGui/QGuiApplication>
#include <qpa/qplatformnativeinterface.h>
#include <EGL/egl.h>
#endif // DESKTOP_APP_USE_ANGLE
#define LOG_ONCE(x) [[maybe_unused]] static auto logged = [&] { LOG(x); return true; }();
namespace Ui::GL {
namespace {
bool ForceDisabled/* = false*/;
bool LastCheckCrashed/* = false*/;
#ifdef DESKTOP_APP_USE_ANGLE
ANGLE ResolvedANGLE/* = ANGLE::Auto*/;
QList<QByteArray> EGLExtensions(not_null<QOpenGLContext*> context) {
const auto native = QGuiApplication::platformNativeInterface();
Assert(native != nullptr);
const auto display = static_cast<EGLDisplay>(
native->nativeResourceForContext(
QByteArrayLiteral("egldisplay"),
context));
return display
? QByteArray(eglQueryString(display, EGL_EXTENSIONS)).split(' ')
: QList<QByteArray>();
}
#endif // DESKTOP_APP_USE_ANGLE
void CrashCheckStart() {
auto f = QFile(Integration::Instance().openglCheckFilePath());
if (f.open(QIODevice::WriteOnly)) {
f.write("1", 1);
f.close();
}
}
} // namespace
Capabilities CheckCapabilities(QWidget *widget) {
if (!Platform::IsMac()) {
if (ForceDisabled) {
LOG_ONCE(("OpenGL: Force-disabled."));
return {};
} else if (LastCheckCrashed) {
LOG_ONCE(("OpenGL: Last-crashed."));
return {};
}
}
[[maybe_unused]] static const auto BugListInited = [] {
if (!QFile::exists(":/misc/gpu_driver_bug_list.json")) {
return false;
}
LOG(("OpenGL: Using custom 'gpu_driver_bug_list.json'."));
qputenv("QT_OPENGL_BUGLIST", ":/misc/gpu_driver_bug_list.json");
return true;
}();
CrashCheckStart();
const auto guard = gsl::finally([=] {
CrashCheckFinish();
});
auto tester = QOpenGLWidget(widget);
tester.setAttribute(Qt::WA_TranslucentBackground);
if (tester.window()->testAttribute(Qt::WA_TranslucentBackground)) {
auto format = tester.format();
format.setAlphaBufferSize(8);
tester.setFormat(format);
}
const auto guard2 = [&]() -> std::optional<gsl::final_action<Fn<void()>>> {
if (!tester.window()->windowHandle()) {
tester.window()->createWinId();
return gsl::finally(Fn<void()>([&] {
tester.window()->windowHandle()->destroy();
tester.window()->setAttribute(Qt::WA_OutsideWSRange, false);
}));
}
return std::nullopt;
}();
tester.grabFramebuffer(); // Force initialize().
const auto context = tester.context();
if (!context
|| !context->isValid()
|| !context->makeCurrent(tester.window()->windowHandle())) {
LOG_ONCE(("OpenGL: Could not create widget in a window."));
return {};
}
const auto functions = context->functions();
using Feature = QOpenGLFunctions;
if (!functions->hasOpenGLFeature(Feature::NPOTTextures)) {
LOG_ONCE(("OpenGL: NPOT textures not supported."));
return {};
} else if (!functions->hasOpenGLFeature(Feature::Framebuffers)) {
LOG_ONCE(("OpenGL: Framebuffers not supported."));
return {};
} else if (!functions->hasOpenGLFeature(Feature::Shaders)) {
LOG_ONCE(("OpenGL: Shaders not supported."));
return {};
}
{
auto program = QOpenGLShaderProgram();
LinkProgram(
&program,
VertexShader({
VertexViewportTransform(),
VertexPassTextureCoord(),
}),
FragmentShader({
FragmentSampleARGB32Texture(),
}));
if (!program.isLinked()) {
LOG_ONCE(("OpenGL: Could not link simple shader."));
return {};
}
}
const auto supported = context->format();
switch (supported.profile()) {
case QSurfaceFormat::NoProfile: {
if (supported.renderableType() == QSurfaceFormat::OpenGLES) {
LOG_ONCE(("OpenGL Profile: OpenGLES."));
} else {
LOG_ONCE(("OpenGL Profile: NoProfile."));
}
} break;
case QSurfaceFormat::CoreProfile: {
LOG_ONCE(("OpenGL Profile: Core."));
} break;
case QSurfaceFormat::CompatibilityProfile: {
LOG_ONCE(("OpenGL Profile: Compatibility."));
} break;
}
static const auto checkVendor = [&] {
const auto renderer = reinterpret_cast<const char*>(
functions->glGetString(GL_RENDERER));
LOG(("OpenGL Renderer: %1").arg(renderer ? renderer : "[nullptr]"));
const auto vendor = reinterpret_cast<const char*>(
functions->glGetString(GL_VENDOR));
LOG(("OpenGL Vendor: %1").arg(vendor ? vendor : "[nullptr]"));
const auto version = reinterpret_cast<const char*>(
functions->glGetString(GL_VERSION));
LOG(("OpenGL Version: %1").arg(version ? version : "[nullptr]"));
const auto extensions = context->extensions();
auto list = QStringList();
for (const auto &extension : extensions) {
list.append(QString::fromLatin1(extension));
}
LOG(("OpenGL Extensions: %1").arg(list.join(", ")));
#ifdef DESKTOP_APP_USE_ANGLE
auto egllist = QStringList();
for (const auto &extension : EGLExtensions(context)) {
egllist.append(QString::fromLatin1(extension));
}
LOG(("EGL Extensions: %1").arg(egllist.join(", ")));
#endif // DESKTOP_APP_USE_ANGLE
return true;
}();
if (!checkVendor) {
return {};
}
const auto version = u"%1.%2"_q
.arg(supported.majorVersion())
.arg(supported.majorVersion());
auto result = Capabilities{ .supported = true };
if (supported.alphaBufferSize() >= 8) {
result.transparency = true;
LOG_ONCE(("OpenGL: QOpenGLContext created, version: %1."
).arg(version));
} else {
LOG_ONCE(("OpenGL: QOpenGLContext without alpha created, version: %1"
).arg(version));
}
return result;
}
Backend ChooseBackendDefault(Capabilities capabilities) {
const auto use = ::Platform::IsMac()
? true
: ::Platform::IsWindows()
? capabilities.supported
: capabilities.transparency;
return use ? Backend::OpenGL : Backend::Raster;
}
void DetectLastCheckCrash() {
[[maybe_unused]] static const auto Once = [] {
LastCheckCrashed = !Platform::IsMac()
&& QFile::exists(Integration::Instance().openglCheckFilePath());
return false;
}();
}
bool LastCrashCheckFailed() {
DetectLastCheckCrash();
return LastCheckCrashed;
}
void CrashCheckFinish() {
QFile::remove(Integration::Instance().openglCheckFilePath());
}
void ForceDisable(bool disable) {
if (!Platform::IsMac()) {
ForceDisabled = disable;
}
}
#ifdef DESKTOP_APP_USE_ANGLE
void ConfigureANGLE() {
qunsetenv("DESKTOP_APP_QT_ANGLE_PLATFORM");
const auto path = Ui::Integration::Instance().angleBackendFilePath();
if (path.isEmpty()) {
return;
}
auto f = QFile(path);
if (!f.open(QIODevice::ReadOnly)) {
return;
}
auto bytes = f.read(32);
const auto check = [&](const char *backend, ANGLE angle) {
if (bytes.startsWith(backend)) {
ResolvedANGLE = angle;
qputenv("DESKTOP_APP_QT_ANGLE_PLATFORM", backend);
}
};
//check("gl", ANGLE::OpenGL);
check("d3d9", ANGLE::D3D9);
check("d3d11", ANGLE::D3D11);
check("d3d11on12", ANGLE::D3D11on12);
if (ResolvedANGLE == ANGLE::Auto) {
LOG(("ANGLE Warning: Unknown backend: %1"
).arg(QString::fromUtf8(bytes)));
}
}
void ChangeANGLE(ANGLE backend) {
const auto path = Ui::Integration::Instance().angleBackendFilePath();
const auto write = [&](QByteArray backend) {
auto f = QFile(path);
if (!f.open(QIODevice::WriteOnly)) {
LOG(("ANGLE Warning: Could not write to %1.").arg(path));
return;
}
f.write(backend);
};
switch (backend) {
case ANGLE::Auto: QFile(path).remove(); break;
case ANGLE::D3D9: write("d3d9"); break;
case ANGLE::D3D11: write("d3d11"); break;
case ANGLE::D3D11on12: write("d3d11on12"); break;
//case ANGLE::OpenGL: write("gl"); break;
default: Unexpected("ANGLE backend value.");
}
}
ANGLE CurrentANGLE() {
return ResolvedANGLE;
}
#endif // DESKTOP_APP_USE_ANGLE
} // namespace Ui::GL

View File

@@ -0,0 +1,53 @@
// 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/flags.h"
// ANGLE is used only on Windows with Qt < 6.
#if defined Q_OS_WIN && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#define DESKTOP_APP_USE_ANGLE
#endif // Q_OS_WIN && Qt < 6
class QOpenGLContext;
namespace Ui::GL {
enum class Backend {
Raster,
OpenGL,
};
struct Capabilities {
bool supported = false;
bool transparency = false;
};
[[nodiscard]] Capabilities CheckCapabilities(QWidget *widget = nullptr);
[[nodiscard]] Backend ChooseBackendDefault(Capabilities capabilities);
void ForceDisable(bool disable);
void DetectLastCheckCrash();
[[nodiscard]] bool LastCrashCheckFailed();
void CrashCheckFinish();
#ifdef DESKTOP_APP_USE_ANGLE
enum class ANGLE {
Auto,
D3D9,
D3D11,
D3D11on12,
//OpenGL,
};
void ConfigureANGLE(); // Requires Ui::Integration being set.
void ChangeANGLE(ANGLE backend);
[[nodiscard]] ANGLE CurrentANGLE();
#endif // DESKTOP_APP_USE_ANGLE
} // namespace Ui::GL

View File

@@ -0,0 +1,165 @@
// 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/gl/gl_image.h"
#include <QtGui/QOpenGLFunctions>
namespace Ui::GL {
namespace details {
void GenerateTextures(
QOpenGLFunctions &f,
gsl::span<GLuint> values,
GLint filter,
GLint clamp) {
Expects(!values.empty());
f.glGenTextures(values.size(), values.data());
for (const auto texture : values) {
f.glBindTexture(GL_TEXTURE_2D, texture);
f.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, clamp);
f.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, clamp);
f.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
f.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
}
}
void DestroyTextures(QOpenGLFunctions *f, gsl::span<GLuint> values) {
Expects(!values.empty());
if (f) {
f->glDeleteTextures(values.size(), values.data());
}
ranges::fill(values, 0);
}
void GenerateFramebuffers(QOpenGLFunctions &f, gsl::span<GLuint> values) {
Expects(!values.empty());
f.glGenFramebuffers(values.size(), values.data());
}
void DestroyFramebuffers(QOpenGLFunctions *f, gsl::span<GLuint> values) {
Expects(!values.empty());
if (f) {
f->glDeleteFramebuffers(values.size(), values.data());
}
ranges::fill(values, 0);
}
} // namespace details
void Image::setImage(QImage image, QSize subimage) {
Expects(subimage.width() <= image.width()
&& subimage.height() <= image.height());
_image = std::move(image);
_subimage = subimage.isValid() ? subimage : _image.size();
}
const QImage &Image::image() const {
return _image;
}
QImage Image::takeImage() {
return _image.isNull() ? base::take(_storage) : base::take(_image);
}
void Image::invalidate() {
_storage = base::take(_image);
_subimage = QSize();
}
void Image::bind(QOpenGLFunctions &f) {
_textures.ensureCreated(f, GL_NEAREST);
if (_subimage.isEmpty()) {
_textureSize = _subimage;
return;
}
const auto cacheKey = _image.cacheKey();
const auto upload = (_cacheKey != cacheKey);
if (upload) {
_cacheKey = cacheKey;
}
_textures.bind(f, 0);
if (upload) {
f.glPixelStorei(GL_UNPACK_ROW_LENGTH, _image.bytesPerLine() / 4);
if (_textureSize.width() < _subimage.width()
|| _textureSize.height() < _subimage.height()) {
_textureSize = _subimage;
f.glTexImage2D(
GL_TEXTURE_2D,
0,
kFormatRGBA,
_subimage.width(),
_subimage.height(),
0,
kFormatRGBA,
GL_UNSIGNED_BYTE,
_image.constBits());
} else {
f.glTexSubImage2D(
GL_TEXTURE_2D,
0,
0,
0,
_subimage.width(),
_subimage.height(),
kFormatRGBA,
GL_UNSIGNED_BYTE,
_image.constBits());
}
f.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
}
}
void Image::destroy(QOpenGLFunctions *f) {
invalidate();
_textures.destroy(f);
_cacheKey = 0;
_textureSize = QSize();
}
TexturedRect Image::texturedRect(
const QRect &geometry,
const QRect &texture,
const QRect &clip) {
Expects(!_image.isNull());
const auto visible = clip.isNull()
? geometry
: clip.intersected(geometry);
if (visible.isEmpty()) {
return TexturedRect{
.geometry = Rect(visible),
.texture = Rect(0., 0., 0., 0.),
};
}
const auto xFactor = texture.width() / geometry.width();
const auto yFactor = texture.height() / geometry.height();
const auto usedTexture = QRect(
texture.x() + (visible.x() - geometry.x()) * xFactor,
texture.y() + (visible.y() - geometry.y()) * yFactor,
visible.width() * xFactor,
visible.height() * yFactor);
const auto dimensions = QSizeF((_textureSize.width() < _subimage.width()
|| _textureSize.height() < _subimage.height())
? _subimage
: _textureSize);
return {
.geometry = Rect(visible),
.texture = Rect(QRectF(
usedTexture.x() / dimensions.width(),
usedTexture.y() / dimensions.height(),
usedTexture.width() / dimensions.width(),
usedTexture.height() / dimensions.height())),
};
}
} // namespace Ui::GL

View File

@@ -0,0 +1,145 @@
// 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/gl/gl_math.h"
#include <QtGui/QOpenGLFunctions>
namespace Ui::GL {
namespace details {
void GenerateTextures(
QOpenGLFunctions &f,
gsl::span<GLuint> values,
GLint filter,
GLint clamp);
void DestroyTextures(QOpenGLFunctions *f, gsl::span<GLuint> values);
void GenerateFramebuffers(QOpenGLFunctions &f, gsl::span<GLuint> values);
void DestroyFramebuffers(QOpenGLFunctions *f, gsl::span<GLuint> values);
} // namespace details
template <size_t Count>
class Textures final {
public:
static_assert(Count > 0);
void ensureCreated(
QOpenGLFunctions &f,
GLint filter = GL_LINEAR,
GLint clamp = GL_CLAMP_TO_EDGE) {
if (!created()) {
details::GenerateTextures(
f,
gsl::make_span(_values),
filter,
clamp);
}
}
void destroy(QOpenGLFunctions *f) {
if (created()) {
details::DestroyTextures(f, gsl::make_span(_values));
}
}
void bind(QOpenGLFunctions &f, int index) const {
Expects(index >= 0 && index < Count);
f.glBindTexture(GL_TEXTURE_2D, _values[index]);
}
[[nodiscard]] GLuint id(int index) const {
Expects(index >= 0 && index < Count);
return _values[index];
}
[[nodiscard]] bool created() const {
return (_values[0] != 0);
}
private:
std::array<GLuint, Count> _values = { { 0 } };
};
template <size_t Count>
class Framebuffers final {
public:
static_assert(Count > 0);
void ensureCreated(QOpenGLFunctions &f) {
if (!created()) {
details::GenerateFramebuffers(f, gsl::make_span(_values));
}
}
void destroy(QOpenGLFunctions *f) {
if (created()) {
details::DestroyFramebuffers(f, gsl::make_span(_values));
}
}
void bind(QOpenGLFunctions &f, int index) const {
Expects(index >= 0 && index < Count);
f.glBindFramebuffer(GL_FRAMEBUFFER, _values[index]);
}
[[nodiscard]] bool created() const {
return (_values[0] != 0);
}
private:
std::array<GLuint, Count> _values = { { 0 } };
};
struct TexturedRect {
Rect geometry;
Rect texture;
};
class Image final {
public:
void setImage(QImage image, QSize subimage = QSize());
[[nodiscard]] const QImage &image() const;
[[nodiscard]] QImage takeImage();
void invalidate();
void bind(QOpenGLFunctions &f);
void destroy(QOpenGLFunctions *f);
[[nodiscard]] TexturedRect texturedRect(
const QRect &geometry,
const QRect &texture,
const QRect &clip = QRect());
explicit operator bool() const {
return !_image.isNull();
}
private:
QImage _image;
QImage _storage;
Textures<1> _textures;
qint64 _cacheKey = 0;
QSize _subimage;
QSize _textureSize;
};
#if defined Q_OS_WIN && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
inline constexpr auto kFormatRGBA = GL_BGRA_EXT;
inline constexpr auto kSwizzleRedBlue = false;
#else // Q_OS_WIN
inline constexpr auto kFormatRGBA = GL_RGBA;
inline constexpr auto kSwizzleRedBlue = true;
#endif // Q_OS_WIN
} // namespace Ui::GL

View File

@@ -0,0 +1,36 @@
// 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/gl/gl_math.h"
namespace Ui::GL {
QVector4D Uniform(const QRect &rect, float factor) {
return QVector4D(
rect.x() * factor,
rect.y() * factor,
rect.width() * factor,
rect.height() * factor);
}
QVector4D Uniform(const Rect &rect) {
return QVector4D(rect.x(), rect.y(), rect.width(), rect.height());
}
QSizeF Uniform(QSize size) {
return size;
}
Rect TransformRect(const Rect &raster, QSize viewport, float factor) {
return {
raster.left() * factor,
float(viewport.height() - raster.bottom()) * factor,
raster.width() * factor,
raster.height() * factor,
};
}
} // namespace Ui::GL

View File

@@ -0,0 +1,83 @@
// 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/QVector4D>
#include <QtCore/QSizeF>
namespace Ui::GL {
class Rect final {
public:
Rect(QRect rect)
: _x(rect.x())
, _y(rect.y())
, _width(rect.width())
, _height(rect.height()) {
}
Rect(QRectF rect)
: _x(rect.x())
, _y(rect.y())
, _width(rect.width())
, _height(rect.height()) {
}
Rect(float x, float y, float width, float height)
: _x(x)
, _y(y)
, _width(width)
, _height(height) {
}
[[nodiscard]] float x() const {
return _x;
}
[[nodiscard]] float y() const {
return _y;
}
[[nodiscard]] float width() const {
return _width;
}
[[nodiscard]] float height() const {
return _height;
}
[[nodiscard]] float left() const {
return _x;
}
[[nodiscard]] float top() const {
return _y;
}
[[nodiscard]] float right() const {
return _x + _width;
}
[[nodiscard]] float bottom() const {
return _y + _height;
}
[[nodiscard]] bool empty() const {
return (_width <= 0) || (_height <= 0);
}
private:
float _x = 0;
float _y = 0;
float _width = 0;
float _height = 0;
};
[[nodiscard]] QVector4D Uniform(const QRect &rect, float factor);
[[nodiscard]] QVector4D Uniform(const Rect &rect);
[[nodiscard]] QSizeF Uniform(QSize size);
[[nodiscard]] Rect TransformRect(
const Rect &raster,
QSize viewport,
float factor);
} // namespace Ui::GL

View File

@@ -0,0 +1,125 @@
// 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/gl/gl_primitives.h"
#include "ui/gl/gl_shader.h"
#include "ui/style/style_core.h"
#include <QtGui/QOpenGLFunctions>
namespace Ui::GL {
static_assert(std::is_same_v<float, GLfloat>);
void FillRectTriangleVertices(float *coords, Rect rect) {
coords[0] = coords[10] = rect.left();
coords[1] = coords[11] = rect.top();
coords[2] = rect.right();
coords[3] = rect.top();
coords[4] = coords[6] = rect.right();
coords[5] = coords[7] = rect.bottom();
coords[8] = rect.left();
coords[9] = rect.bottom();
}
void FillTriangles(
QOpenGLFunctions &f,
gsl::span<const float> coords,
not_null<QOpenGLBuffer*> buffer,
not_null<QOpenGLShaderProgram*> program,
const QColor &color,
Fn<void()> additional) {
Expects(coords.size() % 6 == 0);
if (coords.empty()) {
return;
}
buffer->bind();
buffer->allocate(coords.data(), coords.size() * sizeof(GLfloat));
program->setUniformValue("s_color", color);
GLint position = program->attributeLocation("position");
f.glVertexAttribPointer(
position,
2,
GL_FLOAT,
GL_FALSE,
2 * sizeof(GLfloat),
nullptr);
f.glEnableVertexAttribArray(position);
if (additional) {
additional();
}
f.glDrawArrays(GL_TRIANGLES, 0, coords.size() / 2);
f.glDisableVertexAttribArray(position);
}
void FillRectangle(
QOpenGLFunctions &f,
not_null<QOpenGLShaderProgram*> program,
int skipVertices,
const QColor &color) {
const auto shift = [&](int elements) {
return reinterpret_cast<const void*>(
(skipVertices * 4 + elements) * sizeof(GLfloat));
};
program->setUniformValue("s_color", color);
GLint position = program->attributeLocation("position");
f.glVertexAttribPointer(
position,
2,
GL_FLOAT,
GL_FALSE,
2 * sizeof(GLfloat),
shift(0));
f.glEnableVertexAttribArray(position);
f.glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
f.glDisableVertexAttribArray(position);
}
void FillTexturedRectangle(
QOpenGLFunctions &f,
not_null<QOpenGLShaderProgram*> program,
int skipVertices) {
const auto shift = [&](int elements) {
return reinterpret_cast<const void*>(
(skipVertices * 4 + elements) * sizeof(GLfloat));
};
GLint position = program->attributeLocation("position");
f.glVertexAttribPointer(
position,
2,
GL_FLOAT,
GL_FALSE,
4 * sizeof(GLfloat),
shift(0));
f.glEnableVertexAttribArray(position);
GLint texcoord = program->attributeLocation("v_texcoordIn");
f.glVertexAttribPointer(
texcoord,
2,
GL_FLOAT,
GL_FALSE,
4 * sizeof(GLfloat),
shift(2));
f.glEnableVertexAttribArray(texcoord);
f.glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
f.glDisableVertexAttribArray(position);
f.glDisableVertexAttribArray(texcoord);
}
} // namespace Ui::GL

View File

@@ -0,0 +1,39 @@
// 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/gl/gl_math.h"
#include "ui/style/style_core.h"
#include <QOpenGLBuffer>
#include <QOpenGLShaderProgram>
class QOpenGLFunctions;
namespace Ui::GL {
void FillRectTriangleVertices(float *coords, Rect rect);
void FillTriangles(
QOpenGLFunctions &f,
gsl::span<const float> coords,
not_null<QOpenGLBuffer*> buffer,
not_null<QOpenGLShaderProgram*> program,
const QColor &color,
Fn<void()> additional = nullptr);
void FillRectangle(
QOpenGLFunctions &f,
not_null<QOpenGLShaderProgram*> program,
int skipVertices,
const QColor &color);
void FillTexturedRectangle(
QOpenGLFunctions &f,
not_null<QOpenGLShaderProgram*> program,
int skipVertices = 0);
} // namespace Ui::GL

View File

@@ -0,0 +1,240 @@
// 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/gl/gl_shader.h"
#include "ui/gl/gl_image.h"
#include "base/debug_log.h"
#include <QtGui/QOpenGLContext>
namespace Ui::GL {
[[nodiscard]] bool IsOpenGLES() {
const auto current = QOpenGLContext::currentContext();
Assert(current != nullptr);
return (current->format().renderableType() == QSurfaceFormat::OpenGLES);
}
QString VertexShader(const std::vector<ShaderPart> &parts) {
const auto version = IsOpenGLES()
? QString("#version 100\nprecision highp float;\n")
: QString("#version 120\n");
const auto accumulate = [&](auto proj) {
return ranges::accumulate(parts, QString(), std::plus<>(), proj);
};
return version + R"(
attribute vec2 position;
)" + accumulate(&ShaderPart::header) + R"(
void main() {
vec4 result = vec4(position, 0., 1.);
)" + accumulate(&ShaderPart::body) + R"(
gl_Position = result;
}
)";
}
QString FragmentShader(const std::vector<ShaderPart> &parts) {
const auto version = IsOpenGLES()
? QString("#version 100\nprecision highp float;\n")
: QString("#version 120\n");
const auto accumulate = [&](auto proj) {
return ranges::accumulate(parts, QString(), std::plus<>(), proj);
};
return version + accumulate(&ShaderPart::header) + R"(
void main() {
vec4 result = vec4(0., 0., 0., 0.);
)" + accumulate(&ShaderPart::body) + R"(
gl_FragColor = result;
}
)";
}
ShaderPart VertexPassTextureCoord(char prefix) {
const auto name = prefix + QString("_texcoord");
return {
.header = R"(
attribute vec2 )" + name + R"(In;
varying vec2 )" + name + ";\n",
.body = R"(
)" + name + " = " + name + "In;\n",
};
}
ShaderPart FragmentSampleARGB32Texture() {
return {
.header = R"(
varying vec2 v_texcoord;
uniform sampler2D s_texture;
)",
.body = R"(
result = texture2D(s_texture, v_texcoord);
)" + (kSwizzleRedBlue
? R"(
result = vec4(result.b, result.g, result.r, result.a);
)" : QString()),
};
}
QString FragmentYUV2RGB() {
return R"(
result = vec4(
1.164 * y + 1.596 * v,
1.164 * y - 0.392 * u - 0.813 * v,
1.164 * y + 2.017 * u,
1.);
)";
}
ShaderPart FragmentSampleYUV420Texture() {
return {
.header = R"(
varying vec2 v_texcoord;
uniform sampler2D y_texture;
uniform sampler2D u_texture;
uniform sampler2D v_texture;
)",
.body = R"(
float y = texture2D(y_texture, v_texcoord).a - 0.0625;
float u = texture2D(u_texture, v_texcoord).a - 0.5;
float v = texture2D(v_texture, v_texcoord).a - 0.5;
)" + FragmentYUV2RGB(),
};
}
ShaderPart FragmentSampleNV12Texture() {
return {
.header = R"(
varying vec2 v_texcoord;
uniform sampler2D y_texture;
uniform sampler2D uv_texture;
)",
.body = R"(
float y = texture2D(y_texture, v_texcoord).a - 0.0625;
vec2 uv = texture2D(uv_texture, v_texcoord).rg - vec2(0.5, 0.5);
float u = uv.x;
float v = uv.y;
)" + FragmentYUV2RGB(),
};
}
ShaderPart FragmentGlobalOpacity() {
return {
.header = R"(
uniform float g_opacity;
)",
.body = R"(
result *= g_opacity;
)",
};
}
ShaderPart VertexViewportTransform() {
return {
.header = R"(
uniform vec2 viewport;
vec4 transform(vec4 position) {
return vec4(
vec2(-1, -1) + 2. * position.xy / viewport,
position.z,
position.w);
}
)",
.body = R"(
result = transform(result);
)",
};
}
ShaderPart FragmentRoundCorners() {
return {
.header = R"(
uniform vec4 roundRect;
uniform vec2 radiusOutline;
uniform vec4 roundBg;
uniform vec4 outlineFg;
vec2 roundedCorner() {
vec2 rectHalf = roundRect.zw / 2.;
vec2 rectCenter = roundRect.xy + rectHalf;
vec2 fromRectCenter = abs(gl_FragCoord.xy - rectCenter);
vec2 vectorRadius = radiusOutline.xx + vec2(0.5, 0.5);
vec2 fromCenterWithRadius = fromRectCenter + vectorRadius;
vec2 fromRoundingCenter = max(fromCenterWithRadius, rectHalf)
- rectHalf;
float rounded = length(fromRoundingCenter) - radiusOutline.x;
float outline = rounded + radiusOutline.y;
return vec2(
1. - smoothstep(0., 1., rounded),
1. - (smoothstep(0., 1., outline) * outlineFg.a));
}
)",
.body = R"(
vec2 roundOutline = roundedCorner();
result = result * roundOutline.y
+ vec4(outlineFg.rgb, 1) * (1. - roundOutline.y);
result = result * roundOutline.x + roundBg * (1. - roundOutline.x);
)",
};
}
ShaderPart FragmentStaticColor() {
return {
.header = R"(
uniform vec4 s_color;
)",
.body = R"(
result = s_color;
)",
};
}
not_null<QOpenGLShader*> MakeShader(
not_null<QOpenGLShaderProgram*> program,
QOpenGLShader::ShaderType type,
const QString &source) {
const auto result = new QOpenGLShader(type, program);
if (!result->compileSourceCode(source)) {
LOG(("Shader Compilation Failed: %1, error %2.").arg(
source,
result->log()));
}
program->addShader(result);
return result;
}
Program LinkProgram(
not_null<QOpenGLShaderProgram*> program,
std::variant<QString, not_null<QOpenGLShader*>> vertex,
std::variant<QString, not_null<QOpenGLShader*>> fragment) {
const auto vertexAsSource = v::is<QString>(vertex);
const auto v = vertexAsSource
? MakeShader(
program,
QOpenGLShader::Vertex,
v::get<QString>(vertex))
: v::get<not_null<QOpenGLShader*>>(vertex);
if (!vertexAsSource) {
program->addShader(v);
}
const auto fragmentAsSource = v::is<QString>(fragment);
const auto f = fragmentAsSource
? MakeShader(
program,
QOpenGLShader::Fragment,
v::get<QString>(fragment))
: v::get<not_null<QOpenGLShader*>>(fragment);
if (!fragmentAsSource) {
program->addShader(f);
}
if (!program->link()) {
LOG(("Shader Link Failed: %1.").arg(program->log()));
}
return { v, f };
}
} // namespace Ui::GL

View File

@@ -0,0 +1,48 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include <QtCore/QString>
#include <QOpenGLShader>
class OpenGLShaderProgram;
namespace Ui::GL {
struct ShaderPart {
QString header;
QString body;
};
[[nodiscard]] QString VertexShader(const std::vector<ShaderPart> &parts);
[[nodiscard]] QString FragmentShader(const std::vector<ShaderPart> &parts);
[[nodiscard]] ShaderPart VertexPassTextureCoord(char prefix = 'v');
[[nodiscard]] ShaderPart FragmentSampleARGB32Texture();
[[nodiscard]] ShaderPart FragmentSampleYUV420Texture();
[[nodiscard]] ShaderPart FragmentSampleNV12Texture();
[[nodiscard]] ShaderPart FragmentGlobalOpacity();
[[nodiscard]] ShaderPart VertexViewportTransform();
[[nodiscard]] ShaderPart FragmentRoundCorners();
[[nodiscard]] ShaderPart FragmentStaticColor();
not_null<QOpenGLShader*> MakeShader(
not_null<QOpenGLShaderProgram*> program,
QOpenGLShader::ShaderType type,
const QString &source);
struct Program {
not_null<QOpenGLShader*> vertex;
not_null<QOpenGLShader*> fragment;
};
Program LinkProgram(
not_null<QOpenGLShaderProgram*> program,
std::variant<QString, not_null<QOpenGLShader*>> vertex,
std::variant<QString, not_null<QOpenGLShader*>> fragment);
} // namespace Ui::GL

View File

@@ -0,0 +1,187 @@
// 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/gl/gl_surface.h"
#include "ui/rp_widget.h"
#include "ui/painter.h"
#include <QtCore/QCoreApplication>
#include <QtGui/QtEvents>
#include <QtGui/QOpenGLContext>
#include <QtGui/QWindow>
#include <QOpenGLWidget>
namespace Ui::GL {
namespace {
struct SurfaceTraits : RpWidgetDefaultTraits {
static constexpr bool kSetZeroGeometry = false;
};
class SurfaceOpenGL final
: public RpWidgetBase<QOpenGLWidget, SurfaceTraits> {
public:
SurfaceOpenGL(QWidget *parent, std::unique_ptr<Renderer> renderer);
~SurfaceOpenGL();
private:
void initializeGL() override;
void resizeEvent(QResizeEvent *e) override;
void resizeGL(int w, int h) override;
void paintEvent(QPaintEvent *e) override;
void paintGL() override;
bool eventHook(QEvent *e) override;
void callDeInit();
const std::unique_ptr<Renderer> _renderer;
QMetaObject::Connection _connection;
QSize _deviceSize;
};
class SurfaceRaster final : public RpWidgetBase<QWidget, SurfaceTraits> {
public:
SurfaceRaster(QWidget *parent, std::unique_ptr<Renderer> renderer);
private:
void paintEvent(QPaintEvent *e) override;
const std::unique_ptr<Renderer> _renderer;
};
SurfaceOpenGL::SurfaceOpenGL(
QWidget *parent,
std::unique_ptr<Renderer> renderer)
: RpWidgetBase<QOpenGLWidget, SurfaceTraits>(parent)
, _renderer(std::move(renderer)) {
setUpdateBehavior(QOpenGLWidget::PartialUpdate);
}
SurfaceOpenGL::~SurfaceOpenGL() {
callDeInit();
}
void SurfaceOpenGL::initializeGL() {
if (_connection) {
QObject::disconnect(base::take(_connection));
}
const auto context = this->context();
_connection = QObject::connect(
context,
&QOpenGLContext::aboutToBeDestroyed,
[=] { callDeInit(); });
_renderer->init(*context->functions());
}
void SurfaceOpenGL::resizeEvent(QResizeEvent *e) {
if (!window()->windowHandle()) {
return;
}
QOpenGLWidget::resizeEvent(e);
}
void SurfaceOpenGL::resizeGL(int w, int h) {
_deviceSize = QSize(w, h) * devicePixelRatio();
_renderer->resize(this, *context()->functions(), w, h);
}
void SurfaceOpenGL::paintEvent(QPaintEvent *e) {
if (_deviceSize != size() * devicePixelRatio()) {
QCoreApplication::postEvent(this, new QResizeEvent(size(), size()));
}
QOpenGLWidget::paintEvent(e);
}
void SurfaceOpenGL::paintGL() {
if (!updatesEnabled() || size().isEmpty() || !isValid()) {
return;
}
const auto f = context()->functions();
if (const auto bg = _renderer->clearColor()) {
f->glClearColor(bg->redF(), bg->greenF(), bg->blueF(), bg->alphaF());
f->glClear(GL_COLOR_BUFFER_BIT);
}
f->glDisable(GL_BLEND);
_renderer->paint(this, *f);
}
bool SurfaceOpenGL::eventHook(QEvent *e) {
const auto result = RpWidgetBase::eventHook(e);
if (e->type() == QEvent::ScreenChangeInternal) {
_deviceSize = size() * devicePixelRatio();
}
return result;
}
void SurfaceOpenGL::callDeInit() {
if (!_connection) {
return;
}
QObject::disconnect(base::take(_connection));
makeCurrent();
const auto context = this->context();
_renderer->deinit(
(isValid() && context && QOpenGLContext::currentContext() == context)
? context->functions()
: nullptr);
}
SurfaceRaster::SurfaceRaster(
QWidget *parent,
std::unique_ptr<Renderer> renderer)
: RpWidgetBase<QWidget, SurfaceTraits>(parent)
, _renderer(std::move(renderer)) {
}
void SurfaceRaster::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
_renderer->paintFallback(p, e->region(), Backend::Raster);
}
} // namespace
void Renderer::paint(
not_null<QOpenGLWidget*> widget,
QOpenGLFunctions &f) {
auto p = Painter(widget.get());
paintFallback(p, widget->rect(), Backend::OpenGL);
}
std::unique_ptr<RpWidgetWrap> CreateSurface(
Fn<ChosenRenderer(Capabilities)> chooseRenderer) {
auto chosen = chooseRenderer(CheckCapabilities(nullptr));
switch (chosen.backend) {
case Backend::OpenGL:
return std::make_unique<SurfaceOpenGL>(
nullptr,
std::move(chosen.renderer));
case Backend::Raster:
return std::make_unique<SurfaceRaster>(
nullptr,
std::move(chosen.renderer));
}
Unexpected("Backend value in Ui::GL::CreateSurface.");
}
std::unique_ptr<RpWidgetWrap> CreateSurface(
QWidget *parent,
ChosenRenderer chosen) {
switch (chosen.backend) {
case Backend::OpenGL:
return std::make_unique<SurfaceOpenGL>(
parent,
std::move(chosen.renderer));
case Backend::Raster:
return std::make_unique<SurfaceRaster>(
parent,
std::move(chosen.renderer));
}
Unexpected("Backend value in Ui::GL::CreateSurface.");
}
} // namespace Ui::GL

View File

@@ -0,0 +1,66 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "ui/gl/gl_detection.h"
#include <QtGui/QOpenGLFunctions>
class Painter;
class QOpenGLWidget;
namespace Ui {
class RpWidgetWrap;
} // namespace Ui
namespace Ui::GL {
class Renderer {
public:
virtual void init(QOpenGLFunctions &f) {
}
virtual void deinit(QOpenGLFunctions *f) {
}
virtual void resize(
not_null<QOpenGLWidget*> widget,
QOpenGLFunctions &f,
int w,
int h) {
}
virtual void paint(
not_null<QOpenGLWidget*> widget,
QOpenGLFunctions &f);
[[nodiscard]] virtual std::optional<QColor> clearColor() {
return std::nullopt;
}
virtual void paintFallback(
Painter &p,
const QRegion &clip,
Backend backend) {
}
virtual ~Renderer() = default;
};
struct ChosenRenderer {
std::unique_ptr<Renderer> renderer;
Backend backend = Backend::Raster;
};
[[nodiscard]] std::unique_ptr<RpWidgetWrap> CreateSurface(
Fn<ChosenRenderer(Capabilities)> chooseRenderer);
[[nodiscard]] std::unique_ptr<RpWidgetWrap> CreateSurface(
QWidget *parent,
ChosenRenderer chosen);
} // namespace Ui::GL

View File

@@ -0,0 +1,55 @@
// 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/gl/gl_window.h"
#include "ui/gl/gl_detection.h"
#include "ui/widgets/rp_window.h"
#include "base/debug_log.h"
namespace Ui::GL {
namespace {
[[nodiscard]] Fn<Backend(Capabilities)> ChooseBackendWrap(
Fn<Backend(Capabilities)> chooseBackend) {
return [=](Capabilities capabilities) {
const auto backend = chooseBackend(capabilities);
const auto use = backend == Backend::OpenGL;
LOG(("OpenGL: %1 (Window)").arg(use ? "[TRUE]" : "[FALSE]"));
return backend;
};
}
} // namespace
Window::Window() : Window(ChooseBackendDefault) {
}
Window::Window(Fn<Backend(Capabilities)> chooseBackend)
: _window(createWindow(ChooseBackendWrap(chooseBackend))) {
}
Window::~Window() = default;
Backend Window::backend() const {
return _backend;
}
not_null<RpWindow*> Window::window() const {
return _window.get();
}
not_null<RpWidget*> Window::widget() const {
return _window->body().get();
}
std::unique_ptr<RpWindow> Window::createWindow(
const Fn<Backend(Capabilities)> &chooseBackend) {
_backend = chooseBackend(CheckCapabilities());
return std::make_unique<RpWindow>();
}
} // namespace Ui::GL

View File

@@ -0,0 +1,38 @@
// 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 RpWindow;
class RpWidget;
} // namespace Ui
namespace Ui::GL {
enum class Backend;
struct Capabilities;
class Window final {
public:
Window();
explicit Window(Fn<Backend(Capabilities)> chooseBackend);
~Window();
[[nodiscard]] Backend backend() const;
[[nodiscard]] not_null<RpWindow*> window() const;
[[nodiscard]] not_null<RpWidget*> widget() const;
private:
[[nodiscard]] std::unique_ptr<RpWindow> createWindow(
const Fn<Backend(Capabilities)> &chooseBackend);
Backend _backend = Backend();
const std::unique_ptr<RpWindow> _window;
};
} // namespace Ui::GL

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,216 @@
// 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/flags.h"
#include "ui/rect_part.h"
#include "ui/style/style_core.h"
namespace Storage {
namespace Cache {
struct Key;
} // namespace Cache
} // namespace Storage
enum class ImageRoundRadius {
None,
Large,
Small,
Ellipse,
};
namespace Images {
[[nodiscard]] QPixmap PixmapFast(QImage &&image);
[[nodiscard]] QImage BlurLargeImage(QImage &&image, int radius);
[[nodiscard]] QImage DitherImage(const QImage &image);
[[nodiscard]] QImage GenerateGradient(
QSize size,
const std::vector<QColor> &colors, // colors.size() <= 4.
int rotation = 0,
float progress = 1.f);
[[nodiscard]] QImage GenerateLinearGradient(
QSize size,
const std::vector<QColor> &colors,
int rotation = 0);
[[nodiscard]] QImage GenerateShadow(
int height,
int topAlpha,
int bottomAlpha,
QColor color = QColor(0, 0, 0));
inline constexpr auto kTopLeft = 0;
inline constexpr auto kTopRight = 1;
inline constexpr auto kBottomLeft = 2;
inline constexpr auto kBottomRight = 3;
struct CornersMaskRef {
CornersMaskRef() = default;
explicit CornersMaskRef(gsl::span<const QImage, 4> masks)
: p{ &masks[0], &masks[1], &masks[2], &masks[3] } {
}
explicit CornersMaskRef(std::array<const QImage, 4> masks)
: p{ &masks[0], &masks[1], &masks[2], &masks[3] } {
}
explicit CornersMaskRef(gsl::span<const QImage*, 4> masks)
: p{ masks[0], masks[1], masks[2], masks[3] } {
}
explicit CornersMaskRef(std::array<const QImage*, 4> masks)
: p{ masks[0], masks[1], masks[2], masks[3] } {
}
[[nodiscard]] bool empty() const {
return !p[0] && !p[1] && !p[2] && !p[3];
}
std::array<const QImage*, 4> p{};
friend inline constexpr std::strong_ordering operator<=>(
CornersMaskRef a,
CornersMaskRef b) noexcept {
for (auto i = 0; i != 4; ++i) {
if (a.p[i] < b.p[i]) {
return std::strong_ordering::less;
} else if (a.p[i] > b.p[i]) {
return std::strong_ordering::greater;
}
}
return std::strong_ordering::equal;
}
friend inline constexpr bool operator==(
CornersMaskRef a,
CornersMaskRef b) noexcept = default;
};
[[nodiscard]] const std::array<QImage, 4> &CornersMask(
ImageRoundRadius radius);
[[nodiscard]] std::array<QImage, 4> PrepareCorners(
ImageRoundRadius radius,
const style::color &color);
[[nodiscard]] std::array<QImage, 4> CornersMask(int radius);
[[nodiscard]] QImage EllipseMask(QSize size, double ratio = style::DevicePixelRatio());
[[nodiscard]] std::array<QImage, 4> PrepareCorners(
int radius,
const style::color &color);
[[nodiscard]] QByteArray UnpackGzip(const QByteArray &bytes);
// Try to read images up to 64MB.
inline constexpr auto kReadBytesLimit = 64 * 1024 * 1024;
inline constexpr auto kReadMaxArea = 12'032 * 9'024;
struct ReadArgs {
QString path;
QByteArray content;
QByteArray svgCutOutId;
QSize maxSize;
bool gzipSvg = false;
bool forceOpaque = false;
bool returnContent = false;
};
struct ReadResult {
QImage image;
QByteArray content;
QByteArray svgCutOutContent;
QByteArray format;
float64 scale = 1.;
bool animated = false;
};
[[nodiscard]] ReadResult Read(ReadArgs &&args);
enum class Option {
None = 0,
FastTransform = (1 << 0),
Blur = (1 << 1),
RoundCircle = (1 << 2),
RoundLarge = (1 << 3),
RoundSmall = (1 << 4),
RoundSkipTopLeft = (1 << 5),
RoundSkipTopRight = (1 << 6),
RoundSkipBottomLeft = (1 << 7),
RoundSkipBottomRight = (1 << 8),
Colorize = (1 << 9),
TransparentBackground = (1 << 10),
};
using Options = base::flags<Option>;
inline constexpr auto is_flag_type(Option) { return true; };
[[nodiscard]] Options RoundOptions(
ImageRoundRadius radius,
RectParts corners = RectPart::AllCorners);
[[nodiscard]] QImage Round(
QImage &&image,
CornersMaskRef mask,
QRect target = QRect());
[[nodiscard]] QImage Blur(QImage &&image, bool ignoreAlpha = false);
[[nodiscard]] QImage Round(
QImage &&image,
ImageRoundRadius radius,
RectParts corners = RectPart::AllCorners,
QRect target = QRect());
[[nodiscard]] QImage Round(
QImage &&image,
gsl::span<const QImage, 4> cornerMasks,
RectParts corners = RectPart::AllCorners,
QRect target = QRect());
[[nodiscard]] QImage Round(
QImage &&image,
Options options,
QRect target = QRect());
[[nodiscard]] QImage Circle(QImage &&image, QRect target = QRect());
[[nodiscard]] QImage Colored(QImage &&image, style::color add);
[[nodiscard]] QImage Colored(QImage &&image, QColor add);
[[nodiscard]] QImage Opaque(QImage &&image);
struct PrepareArgs {
const style::color *colored = nullptr;
Options options;
QSize outer;
[[nodiscard]] PrepareArgs blurred() const {
auto result = *this;
result.options |= Option::Blur;
return result;
}
};
[[nodiscard]] QImage Prepare(
QImage image,
int w,
int h,
const PrepareArgs &args);
[[nodiscard]] inline QImage Prepare(
QImage image,
int w,
const PrepareArgs &args) {
return Prepare(std::move(image), w, 0, args);
}
[[nodiscard]] inline QImage Prepare(
QImage image,
QSize size,
const PrepareArgs &args) {
return Prepare(std::move(image), size.width(), size.height(), args);
}
[[nodiscard]] bool IsProgressiveJpeg(const QByteArray &bytes);
[[nodiscard]] QByteArray MakeProgressiveJpeg(const QByteArray &bytes);
[[nodiscard]] QByteArray ExpandInlineBytes(const QByteArray &bytes);
[[nodiscard]] QImage FromInlineBytes(const QByteArray &bytes);
[[nodiscard]] QPainterPath PathFromInlineBytes(const QByteArray &bytes);
} // namespace Images

View File

@@ -0,0 +1,53 @@
// 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/inactive_press.h"
#include "base/timer.h"
#include "base/qt_connection.h"
#include <QtCore/QPointer>
namespace Ui {
namespace {
constexpr auto kInactivePressTimeout = crl::time(200);
struct InactivePressedWidget {
QWidget *widget = nullptr;
base::qt_connection connection;
base::Timer timer;
};
std::unique_ptr<InactivePressedWidget> Tracker;
} // namespace
void MarkInactivePress(not_null<QWidget*> widget, bool was) {
if (!was) {
if (WasInactivePress(widget)) {
Tracker = nullptr;
}
return;
}
Tracker = std::make_unique<InactivePressedWidget>();
Tracker->widget = widget;
Tracker->connection = QObject::connect(widget, &QWidget::destroyed, [=] {
Tracker->connection.release();
Tracker = nullptr;
});
Tracker->timer.setCallback([=] {
Tracker = nullptr;
});
Tracker->timer.callOnce(kInactivePressTimeout);
}
[[nodiscard]] bool WasInactivePress(not_null<QWidget*> widget) {
return Tracker && (Tracker->widget == widget);
}
} // namespace Ui

View File

@@ -0,0 +1,14 @@
// 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 {
void MarkInactivePress(not_null<QWidget*> widget, bool was);
[[nodiscard]] bool WasInactivePress(not_null<QWidget*> widget);
} // namespace Ui

View File

@@ -0,0 +1,223 @@
// 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/integration.h"
#include "ui/gl/gl_detection.h"
#include "ui/text/text_custom_emoji.h"
#include "ui/text/text_entity.h"
#include "ui/toast/toast.h"
#include "ui/basic_click_handlers.h"
#include "base/platform/base_platform_info.h"
namespace Ui {
namespace {
Integration *IntegrationInstance = nullptr;
} // namespace
void Integration::Set(not_null<Integration*> instance) {
IntegrationInstance = instance;
#ifdef DESKTOP_APP_USE_ANGLE
GL::ConfigureANGLE();
#endif
}
Integration &Integration::Instance() {
Expects(IntegrationInstance != nullptr);
return *IntegrationInstance;
}
bool Integration::Exists() {
return (IntegrationInstance != nullptr);
}
void Integration::textActionsUpdated() {
}
void Integration::activationFromTopPanel() {
}
bool Integration::screenIsLocked() {
return false;
}
std::shared_ptr<ClickHandler> Integration::createLinkHandler(
const EntityLinkData &data,
const Text::MarkedContext &context) {
switch (data.type) {
case EntityType::CustomUrl:
return !data.data.isEmpty()
? std::make_shared<UrlClickHandler>(data.data, false)
: nullptr;
case EntityType::Email:
case EntityType::Url:
return !data.data.isEmpty()
? std::make_shared<UrlClickHandler>(
data.data,
data.shown == EntityLinkShown::Full)
: nullptr;
}
return nullptr;
}
// bool Integration::allowClickHandlerActivation(
// const std::shared_ptr<ClickHandler> &handler,
// const ClickContext &context) {
// return true;
// }
bool Integration::handleUrlClick(
const QString &url,
const QVariant &context) {
return false;
}
bool Integration::copyPreOnClick(const QVariant &context) {
Toast::Show(u"Code copied to clipboard."_q);
return true;
}
QString Integration::convertTagToMimeTag(const QString &tagId) {
return tagId;
}
const Emoji::One *Integration::defaultEmojiVariant(const Emoji::One *emoji) {
return emoji;
}
rpl::producer<> Integration::forcePopupMenuHideRequests() {
return rpl::never<rpl::empty_value>();
}
QString Integration::phraseContextCopyText() {
return "Copy text";
}
QString Integration::phraseContextCopyEmail() {
return "Copy email";
}
QString Integration::phraseContextCopyLink() {
return "Copy link";
}
QString Integration::phraseContextCopySelected() {
return "Copy to clipboard";
}
QString Integration::phraseFormattingTitle() {
return "Formatting";
}
QString Integration::phraseFormattingLinkCreate() {
return "Create link";
}
QString Integration::phraseFormattingLinkEdit() {
return "Edit link";
}
QString Integration::phraseFormattingClear() {
return "Plain text";
}
QString Integration::phraseFormattingBold() {
return "Bold";
}
QString Integration::phraseFormattingItalic() {
return "Italic";
}
QString Integration::phraseFormattingUnderline() {
return "Underline";
}
QString Integration::phraseFormattingStrikeOut() {
return "Strike-through";
}
QString Integration::phraseFormattingBlockquote() {
return "Quote";
}
QString Integration::phraseFormattingMonospace() {
return "Monospace";
}
QString Integration::phraseFormattingSpoiler() {
return "Spoiler";
}
QString Integration::phraseButtonOk() {
return "OK";
}
QString Integration::phraseButtonClose() {
return "Close";
}
QString Integration::phraseButtonCancel() {
return "Cancel";
}
QString Integration::phrasePanelCloseWarning() {
return "Warning";
}
QString Integration::phrasePanelCloseUnsaved() {
return "Changes that you made may not be saved.";
}
QString Integration::phrasePanelCloseAnyway() {
return "Close anyway";
}
QString Integration::phraseBotSharePhone() {
return "Do you want to share your phone number with this bot?";
}
QString Integration::phraseBotSharePhoneTitle() {
return "Phone number";
}
QString Integration::phraseBotSharePhoneConfirm() {
return "Share";
}
QString Integration::phraseBotAllowWrite() {
return "Do you want to allow this bot to write you?";
}
QString Integration::phraseBotAllowWriteTitle() {
return "Allow write";
}
QString Integration::phraseBotAllowWriteConfirm() {
return "Allow";
}
QString Integration::phraseQuoteHeaderCopy() {
return "copy";
}
QString Integration::phraseMinimize() {
return "Minimize";
}
QString Integration::phraseMaximize() {
return "Maximize";
}
QString Integration::phraseRestore() {
return "Restore";
}
} // namespace Ui

View File

@@ -0,0 +1,105 @@
// 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/basic_types.h"
#include <rpl/producer.h>
#include <any>
// Methods that must be implemented outside lib_ui.
class QString;
class QWidget;
class QVariant;
struct TextParseOptions;
class ClickHandler;
struct ClickContext;
struct EntityLinkData;
namespace Ui {
namespace Emoji {
class One;
} // namespace Emoji
namespace Text {
class CustomEmoji;
struct MarkedContext;
} // namespace Text
class Integration {
public:
static void Set(not_null<Integration*> instance);
static Integration &Instance();
static bool Exists();
virtual void postponeCall(FnMut<void()> &&callable) = 0;
virtual void registerLeaveSubscription(not_null<QWidget*> widget) = 0;
virtual void unregisterLeaveSubscription(not_null<QWidget*> widget) = 0;
[[nodiscard]] virtual QString emojiCacheFolder() = 0;
[[nodiscard]] virtual QString openglCheckFilePath() = 0;
[[nodiscard]] virtual QString angleBackendFilePath() = 0;
virtual void textActionsUpdated();
virtual void activationFromTopPanel();
[[nodiscard]] virtual bool screenIsLocked();
[[nodiscard]] virtual std::shared_ptr<ClickHandler> createLinkHandler(
const EntityLinkData &data,
const Text::MarkedContext &context);
[[nodiscard]] virtual bool handleUrlClick(
const QString &url,
const QVariant &context);
[[nodiscard]] virtual bool copyPreOnClick(const QVariant &context);
[[nodiscard]] virtual QString convertTagToMimeTag(const QString &tagId);
[[nodiscard]] virtual const Emoji::One *defaultEmojiVariant(
const Emoji::One *emoji);
// [[nodiscard]] virtual bool allowClickHandlerActivation(
// const std::shared_ptr<ClickHandler> &handler,
// const ClickContext &context);
[[nodiscard]] virtual rpl::producer<> forcePopupMenuHideRequests();
[[nodiscard]] virtual QString phraseContextCopyText();
[[nodiscard]] virtual QString phraseContextCopyEmail();
[[nodiscard]] virtual QString phraseContextCopyLink();
[[nodiscard]] virtual QString phraseContextCopySelected();
[[nodiscard]] virtual QString phraseFormattingTitle();
[[nodiscard]] virtual QString phraseFormattingLinkCreate();
[[nodiscard]] virtual QString phraseFormattingLinkEdit();
[[nodiscard]] virtual QString phraseFormattingClear();
[[nodiscard]] virtual QString phraseFormattingBold();
[[nodiscard]] virtual QString phraseFormattingItalic();
[[nodiscard]] virtual QString phraseFormattingUnderline();
[[nodiscard]] virtual QString phraseFormattingStrikeOut();
[[nodiscard]] virtual QString phraseFormattingBlockquote();
[[nodiscard]] virtual QString phraseFormattingMonospace();
[[nodiscard]] virtual QString phraseFormattingSpoiler();
[[nodiscard]] virtual QString phraseButtonOk();
[[nodiscard]] virtual QString phraseButtonClose();
[[nodiscard]] virtual QString phraseButtonCancel();
[[nodiscard]] virtual QString phrasePanelCloseWarning();
[[nodiscard]] virtual QString phrasePanelCloseUnsaved();
[[nodiscard]] virtual QString phrasePanelCloseAnyway();
[[nodiscard]] virtual QString phraseBotSharePhone();
[[nodiscard]] virtual QString phraseBotSharePhoneTitle();
[[nodiscard]] virtual QString phraseBotSharePhoneConfirm();
[[nodiscard]] virtual QString phraseBotAllowWrite();
[[nodiscard]] virtual QString phraseBotAllowWriteTitle();
[[nodiscard]] virtual QString phraseBotAllowWriteConfirm();
[[nodiscard]] virtual QString phraseQuoteHeaderCopy();
[[nodiscard]] virtual QString phraseMinimize();
[[nodiscard]] virtual QString phraseMaximize();
[[nodiscard]] virtual QString phraseRestore();
};
} // namespace Ui

View File

@@ -0,0 +1,455 @@
// 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/layers/box_content.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/shadow.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/text/text_utilities.h"
#include "ui/rect_part.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "base/timer.h"
#include "styles/style_layers.h"
#include "styles/palette.h"
namespace Ui {
namespace {
class BoxShow final : public Show {
public:
explicit BoxShow(not_null<Ui::BoxContent*> box);
~BoxShow();
void showOrHideBoxOrLayer(
std::variant<
v::null_t,
object_ptr<BoxContent>,
std::unique_ptr<LayerWidget>> &&layer,
LayerOptions options,
anim::type animated) const override;
[[nodiscard]] not_null<QWidget*> toastParent() const override;
[[nodiscard]] bool valid() const override;
operator bool() const override;
private:
BoxShow(base::weak_qptr<BoxContent> weak, ShowPtr wrapped);
bool resolve() const;
const base::weak_qptr<Ui::BoxContent> _weak;
mutable std::shared_ptr<Show> _wrapped;
rpl::lifetime _lifetime;
};
BoxShow::BoxShow(not_null<BoxContent*> box)
: BoxShow(base::make_weak(box.get()), nullptr) {
}
BoxShow::BoxShow(base::weak_qptr<BoxContent> weak, ShowPtr wrapped)
: _weak(weak)
, _wrapped(std::move(wrapped)) {
if (!resolve()) {
if (const auto box = _weak.get()) {
box->boxClosing(
) | rpl::on_next([=] {
resolve();
_lifetime.destroy();
}, _lifetime);
}
}
}
BoxShow::~BoxShow() = default;
bool BoxShow::resolve() const {
if (_wrapped) {
return true;
} else if (const auto strong = _weak.get()) {
if (strong->hasDelegate()) {
_wrapped = strong->getDelegate()->showFactory()();
return true;
}
}
return false;
}
void BoxShow::showOrHideBoxOrLayer(
std::variant<
v::null_t,
object_ptr<BoxContent>,
std::unique_ptr<LayerWidget>> &&layer,
LayerOptions options,
anim::type animated) const {
if (resolve()) {
_wrapped->showOrHideBoxOrLayer(std::move(layer), options, animated);
}
}
not_null<QWidget*> BoxShow::toastParent() const {
if (resolve()) {
return _wrapped->toastParent();
}
Unexpected("Stale BoxShow::toastParent call.");
}
bool BoxShow::valid() const {
return resolve() && _wrapped->valid();
}
BoxShow::operator bool() const {
return valid();
}
} // namespace
void BoxContent::setTitle(rpl::producer<QString> title) {
getDelegate()->setTitle(std::move(title) | rpl::map(Text::WithEntities));
}
QPointer<AbstractButton> BoxContent::addButton(
object_ptr<AbstractButton> button) {
auto result = QPointer<AbstractButton>(button.data());
getDelegate()->addButton(std::move(button));
return result;
}
QPointer<RoundButton> BoxContent::addButton(
rpl::producer<QString> text,
Fn<void()> clickCallback) {
return addButton(
std::move(text),
std::move(clickCallback),
getDelegate()->style().button);
}
QPointer<RoundButton> BoxContent::addButton(
rpl::producer<QString> text,
const style::RoundButton &st) {
return addButton(std::move(text), nullptr, st);
}
QPointer<RoundButton> BoxContent::addButton(
rpl::producer<QString> text,
Fn<void()> clickCallback,
const style::RoundButton &st) {
auto button = object_ptr<RoundButton>(this, std::move(text), st);
auto result = QPointer<RoundButton>(button.data());
result->setTextTransform(RoundButton::TextTransform::NoTransform);
result->setClickedCallback(std::move(clickCallback));
getDelegate()->addButton(std::move(button));
return result;
}
QPointer<AbstractButton> BoxContent::addLeftButton(
object_ptr<AbstractButton> button) {
auto result = QPointer<AbstractButton>(button.data());
getDelegate()->addLeftButton(std::move(button));
return result;
}
QPointer<RoundButton> BoxContent::addLeftButton(
rpl::producer<QString> text,
Fn<void()> clickCallback) {
return addLeftButton(
std::move(text),
std::move(clickCallback),
getDelegate()->style().button);
}
QPointer<RoundButton> BoxContent::addLeftButton(
rpl::producer<QString> text,
Fn<void()> clickCallback,
const style::RoundButton &st) {
auto button = object_ptr<RoundButton>(this, std::move(text), st);
const auto result = QPointer<RoundButton>(button.data());
result->setTextTransform(RoundButton::TextTransform::NoTransform);
result->setClickedCallback(std::move(clickCallback));
getDelegate()->addLeftButton(std::move(button));
return result;
}
QPointer<AbstractButton> BoxContent::addTopButton(
object_ptr<AbstractButton> button) {
auto result = QPointer<AbstractButton>(button.data());
getDelegate()->addTopButton(std::move(button));
return result;
}
QPointer<IconButton> BoxContent::addTopButton(
const style::IconButton &st,
Fn<void()> clickCallback) {
auto button = object_ptr<IconButton>(this, st);
const auto result = QPointer<IconButton>(button.data());
result->setClickedCallback(std::move(clickCallback));
getDelegate()->addTopButton(std::move(button));
return result;
}
void BoxContent::setInner(
object_ptr<RpWidget> inner,
const style::ScrollArea &st) {
if (inner) {
getDelegate()->setLayerType(true);
_scroll.create(this, st);
_scroll->setGeometryToLeft(0, _innerTopSkip, width(), 0);
_scroll->setOwnedWidget(std::move(inner));
if (_topShadow) {
_topShadow->raise();
_bottomShadow->raise();
} else {
_topShadow.create(this);
_bottomShadow.create(this);
}
if (!_preparing) {
// We didn't set dimensions yet, this will be called from finishPrepare();
finishScrollCreate();
}
} else {
getDelegate()->setLayerType(false);
_scroll.destroyDelayed();
_topShadow.destroyDelayed();
_bottomShadow.destroyDelayed();
}
}
void BoxContent::finishPrepare() {
_preparing = false;
if (_scroll) {
finishScrollCreate();
}
setInnerFocus();
}
void BoxContent::finishScrollCreate() {
Expects(_scroll != nullptr);
if (!_scroll->isHidden()) {
_scroll->show();
}
updateScrollAreaGeometry();
_scroll->scrolls(
) | rpl::on_next([=] {
updateInnerVisibleTopBottom();
updateShadowsVisibility();
}, lifetime());
_scroll->innerResizes(
) | rpl::on_next([=] {
updateInnerVisibleTopBottom();
updateShadowsVisibility();
}, lifetime());
_draggingScroll.scrolls(
) | rpl::on_next([=](int delta) {
if (_scroll) {
_scroll->scrollToY(_scroll->scrollTop() + delta);
}
}, lifetime());
}
void BoxContent::scrollToWidget(not_null<QWidget*> widget) {
if (_scroll) {
_scroll->scrollToWidget(widget);
}
}
void BoxContent::scrollToY(int top, int bottom) {
scrollTo({ top, bottom });
}
void BoxContent::scrollTo(ScrollToRequest request, anim::type animated) {
if (_scroll) {
const auto v = _scroll->computeScrollToY(request.ymin, request.ymax);
const auto now = _scroll->scrollTop();
if (animated == anim::type::instant || v == now) {
_scrollAnimation.stop();
_scroll->scrollToY(v);
} else {
_scrollAnimation.start([=] {
_scroll->scrollToY(_scrollAnimation.value(v));
}, now, v, st::slideWrapDuration, anim::sineInOut);
}
}
}
void BoxContent::sendScrollViewportEvent(not_null<QEvent*> event) {
if (_scroll) {
_scroll->viewportEvent(event);
}
}
rpl::producer<> BoxContent::scrolls() const {
return _scroll ? _scroll->scrolls() : rpl::never<>();
}
int BoxContent::scrollTop() const {
return _scroll ? _scroll->scrollTop() : 0;
}
int BoxContent::scrollHeight() const {
return _scroll ? _scroll->height() : 0;
}
base::weak_ptr<Toast::Instance> BoxContent::showToast(
Toast::Config &&config) {
return BoxShow(this).showToast(std::move(config));
}
base::weak_ptr<Toast::Instance> BoxContent::showToast(
TextWithEntities &&text,
crl::time duration) {
return BoxShow(this).showToast(std::move(text), duration);
}
base::weak_ptr<Toast::Instance> BoxContent::showToast(
const QString &text,
crl::time duration) {
return BoxShow(this).showToast(text, duration);
}
std::shared_ptr<Show> BoxContent::uiShow() {
return std::make_shared<BoxShow>(this);
}
void BoxContent::scrollByDraggingDelta(int delta) {
_draggingScroll.checkDeltaScroll(_scroll ? delta : 0);
}
void BoxContent::updateInnerVisibleTopBottom() {
const auto widget = static_cast<RpWidget*>(_scroll
? _scroll->widget()
: nullptr);
if (widget) {
const auto top = _scroll->scrollTop();
widget->setVisibleTopBottom(top, top + _scroll->height());
}
}
void BoxContent::updateShadowsVisibility(anim::type animated) {
if (!_scroll) {
return;
}
const auto top = _scroll->scrollTop();
_topShadow->toggle(
((top > 0)
|| (_innerTopSkip > 0
&& !getDelegate()->style().shadowIgnoreTopSkip)),
animated);
_bottomShadow->toggle(
(top < _scroll->scrollTopMax())
|| (_innerBottomSkip > 0
&& !getDelegate()->style().shadowIgnoreBottomSkip),
animated);
}
void BoxContent::setDimensionsToContent(
int newWidth,
not_null<RpWidget*> content) {
content->resizeToWidth(newWidth);
content->heightValue(
) | rpl::on_next([=](int height) {
setDimensions(newWidth, height);
}, content->lifetime());
}
void BoxContent::setInnerTopSkip(int innerTopSkip, bool scrollBottomFixed) {
if (_innerTopSkip != innerTopSkip) {
const auto delta = innerTopSkip - _innerTopSkip;
_innerTopSkip = innerTopSkip;
if (_scroll && width() > 0) {
auto scrollTopWas = _scroll->scrollTop();
updateScrollAreaGeometry();
if (scrollBottomFixed) {
_scroll->scrollToY(scrollTopWas + delta);
}
}
}
}
void BoxContent::setInnerBottomSkip(int innerBottomSkip) {
if (_innerBottomSkip != innerBottomSkip) {
_innerBottomSkip = innerBottomSkip;
if (_scroll && width() > 0) {
updateScrollAreaGeometry();
}
}
}
void BoxContent::setInnerVisible(bool scrollAreaVisible) {
if (_scroll) {
_scroll->setVisible(scrollAreaVisible);
}
}
QPixmap BoxContent::grabInnerCache() {
const auto isTopShadowVisible = !_topShadow->isHidden();
const auto isBottomShadowVisible = !_bottomShadow->isHidden();
if (isTopShadowVisible) {
_topShadow->setVisible(false);
}
if (isBottomShadowVisible) {
_bottomShadow->setVisible(false);
}
const auto result = GrabWidget(this, _scroll->geometry());
if (isTopShadowVisible) {
_topShadow->setVisible(true);
}
if (isBottomShadowVisible) {
_bottomShadow->setVisible(true);
}
return result;
}
void BoxContent::resizeEvent(QResizeEvent *e) {
if (_scroll) {
updateScrollAreaGeometry();
}
}
void BoxContent::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Escape && !_closeByEscape) {
e->accept();
} else {
RpWidget::keyPressEvent(e);
}
}
void BoxContent::updateScrollAreaGeometry() {
const auto newScrollHeight = height() - _innerTopSkip - _innerBottomSkip;
const auto changed = (_scroll->height() != newScrollHeight);
_scroll->setGeometryToLeft(0, _innerTopSkip, width(), newScrollHeight);
_topShadow->entity()->resize(width(), st::lineWidth);
_topShadow->moveToLeft(0, _innerTopSkip);
_bottomShadow->entity()->resize(width(), st::lineWidth);
_bottomShadow->moveToLeft(
0,
height() - _innerBottomSkip - st::lineWidth);
if (changed) {
updateInnerVisibleTopBottom();
updateShadowsVisibility(anim::type::instant);
}
}
object_ptr<RpWidget> BoxContent::doTakeInnerWidget() {
return _scroll->takeWidget<RpWidget>();
}
void BoxContent::paintEvent(QPaintEvent *e) {
Painter p(this);
if (testAttribute(Qt::WA_OpaquePaintEvent)) {
const auto &color = getDelegate()->style().bg;
for (const auto &rect : e->region()) {
p.fillRect(rect, color);
}
}
}
} // namespace Ui

View File

@@ -0,0 +1,396 @@
// 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/unique_qptr.h"
#include "base/flags.h"
#include "ui/dragging_scroll_manager.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/widgets/labels.h"
#include "ui/layers/layer_widget.h"
#include "ui/layers/show.h"
#include "ui/effects/animations.h"
#include "ui/effects/animation_value.h"
#include "ui/text/text_entity.h"
#include "ui/rp_widget.h"
enum class RectPart;
using RectParts = base::flags<RectPart>;
namespace base {
class Timer;
} // namespace base
namespace style {
struct RoundButton;
struct IconButton;
struct ScrollArea;
struct Box;
} // namespace style
namespace st {
extern const style::ScrollArea &boxScroll;
} // namespace st
namespace Ui::Toast {
struct Config;
class Instance;
} // namespace Ui::Toast
namespace Ui {
class GenericBox;
} // namespace Ui
template <typename BoxType = Ui::GenericBox, typename ...Args>
inline object_ptr<BoxType> Box(Args &&...args) {
const auto parent = static_cast<QWidget*>(nullptr);
return object_ptr<BoxType>(parent, std::forward<Args>(args)...);
}
namespace Ui {
class AbstractButton;
class RoundButton;
class IconButton;
class ScrollArea;
class FlatLabel;
class FadeShadow;
class BoxContent;
struct ScrollToRequest;
class BoxContentDelegate {
public:
virtual void setLayerType(bool layerType) = 0;
virtual void setStyle(const style::Box &st) = 0;
virtual const style::Box &style() = 0;
virtual void setTitle(rpl::producer<TextWithEntities> title) = 0;
virtual void setAdditionalTitle(rpl::producer<QString> additional) = 0;
virtual void setCloseByOutsideClick(bool close) = 0;
virtual void setCustomCornersFilling(RectParts corners) = 0;
virtual void clearButtons() = 0;
virtual void addButton(object_ptr<AbstractButton> button) = 0;
virtual void addLeftButton(object_ptr<AbstractButton> button) = 0;
virtual void addTopButton(object_ptr<AbstractButton> button) = 0;
virtual void showLoading(bool show) = 0;
virtual void updateButtonsPositions() = 0;
virtual void showBox(
object_ptr<BoxContent> box,
LayerOptions options,
anim::type animated) = 0;
virtual void setDimensions(
int newWidth,
int maxHeight,
bool forceCenterPosition = false) = 0;
virtual void setNoContentMargin(bool noContentMargin) = 0;
virtual bool isBoxShown() const = 0;
virtual void closeBox() = 0;
virtual void hideLayer() = 0;
virtual void triggerButton(int index) = 0;
template <typename BoxType>
base::weak_qptr<BoxType> show(
object_ptr<BoxType> content,
LayerOptions options = LayerOption::KeepOther,
anim::type animated = anim::type::normal) {
auto result = base::weak_qptr<BoxType>(content.data());
showBox(std::move(content), options, animated);
return result;
}
virtual ShowFactory showFactory() = 0;
virtual QPointer<QWidget> outerContainer() = 0;
};
class BoxContent : public RpWidget {
public:
BoxContent() {
setAttribute(Qt::WA_OpaquePaintEvent);
}
QAccessible::Role accessibilityRole() override {
return QAccessible::Role::Dialog;
}
bool isBoxShown() const {
return getDelegate()->isBoxShown();
}
void closeBox() {
getDelegate()->closeBox();
}
void triggerButton(int index) {
getDelegate()->triggerButton(index);
}
void setTitle(rpl::producer<QString> title);
void setTitle(rpl::producer<TextWithEntities> title) {
getDelegate()->setTitle(std::move(title));
}
void setAdditionalTitle(rpl::producer<QString> additional) {
getDelegate()->setAdditionalTitle(std::move(additional));
}
void setCloseByEscape(bool close) {
_closeByEscape = close;
}
void setCloseByOutsideClick(bool close) {
getDelegate()->setCloseByOutsideClick(close);
}
void scrollToWidget(not_null<QWidget*> widget);
virtual void showFinished() {
}
void setCustomCornersFilling(RectParts corners) {
getDelegate()->setCustomCornersFilling(corners);
}
void clearButtons() {
getDelegate()->clearButtons();
}
QPointer<AbstractButton> addButton(object_ptr<AbstractButton> button);
QPointer<RoundButton> addButton(
rpl::producer<QString> text,
Fn<void()> clickCallback = nullptr);
QPointer<RoundButton> addButton(
rpl::producer<QString> text,
const style::RoundButton &st);
QPointer<RoundButton> addButton(
rpl::producer<QString> text,
Fn<void()> clickCallback,
const style::RoundButton &st);
QPointer<AbstractButton> addLeftButton(
object_ptr<AbstractButton> button);
QPointer<RoundButton> addLeftButton(
rpl::producer<QString> text,
Fn<void()> clickCallback = nullptr);
QPointer<RoundButton> addLeftButton(
rpl::producer<QString> text,
Fn<void()> clickCallback,
const style::RoundButton& st);
QPointer<AbstractButton> addTopButton(
object_ptr<AbstractButton> button);
QPointer<IconButton> addTopButton(
const style::IconButton &st,
Fn<void()> clickCallback = nullptr);
void showLoading(bool show) {
getDelegate()->showLoading(show);
}
void updateButtonsGeometry() {
getDelegate()->updateButtonsPositions();
}
void setStyle(const style::Box &st) {
getDelegate()->setStyle(st);
}
virtual void setInnerFocus() {
setFocus();
}
[[nodiscard]] rpl::producer<> boxClosing() const {
return _boxClosingStream.events();
}
void notifyBoxClosing() {
_boxClosingStream.fire({});
}
void setDelegate(not_null<BoxContentDelegate*> newDelegate) {
_delegate = newDelegate;
_preparing = true;
prepare();
finishPrepare();
}
[[nodiscard]] bool hasDelegate() const {
return _delegate != nullptr;
}
[[nodiscard]] not_null<BoxContentDelegate*> getDelegate() const {
return _delegate;
}
void setNoContentMargin(bool noContentMargin) {
if (_noContentMargin != noContentMargin) {
_noContentMargin = noContentMargin;
setAttribute(Qt::WA_OpaquePaintEvent, !_noContentMargin);
}
getDelegate()->setNoContentMargin(noContentMargin);
}
void scrollByDraggingDelta(int delta);
void scrollToY(int top, int bottom = -1);
void scrollTo(
ScrollToRequest request,
anim::type animated = anim::type::instant);
void sendScrollViewportEvent(not_null<QEvent*> event);
[[nodiscard]] rpl::producer<> scrolls() const;
[[nodiscard]] int scrollTop() const;
[[nodiscard]] int scrollHeight() const;
base::weak_ptr<Toast::Instance> showToast(Toast::Config &&config);
base::weak_ptr<Toast::Instance> showToast(
TextWithEntities &&text,
crl::time duration = 0);
base::weak_ptr<Toast::Instance> showToast(
const QString &text,
crl::time duration = 0);
[[nodiscard]] std::shared_ptr<Show> uiShow();
protected:
virtual void prepare() = 0;
void setLayerType(bool layerType) {
getDelegate()->setLayerType(layerType);
}
void setDimensions(
int newWidth,
int maxHeight,
bool forceCenterPosition = false) {
getDelegate()->setDimensions(
newWidth,
maxHeight,
forceCenterPosition);
}
void setDimensionsToContent(
int newWidth,
not_null<RpWidget*> content);
void setInnerTopSkip(int topSkip, bool scrollBottomFixed = false);
void setInnerBottomSkip(int bottomSkip);
template <typename Widget>
QPointer<Widget> setInnerWidget(
object_ptr<Widget> inner,
const style::ScrollArea &st,
int topSkip = 0,
int bottomSkip = 0) {
auto result = QPointer<Widget>(inner.data());
setInnerTopSkip(topSkip);
setInnerBottomSkip(bottomSkip);
setInner(std::move(inner), st);
return result;
}
template <typename Widget>
QPointer<Widget> setInnerWidget(
object_ptr<Widget> inner,
int topSkip = 0,
int bottomSkip = 0) {
return setInnerWidget(
std::move(inner),
st::boxScroll,
topSkip,
bottomSkip);
}
template <typename Widget>
object_ptr<Widget> takeInnerWidget() {
return object_ptr<Widget>::fromRaw(
static_cast<Widget*>(doTakeInnerWidget().release()));
}
void setInnerVisible(bool scrollAreaVisible);
QPixmap grabInnerCache();
void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
private:
void finishPrepare();
void finishScrollCreate();
void setInner(object_ptr<RpWidget> inner, const style::ScrollArea &st);
void updateScrollAreaGeometry();
void updateInnerVisibleTopBottom();
void updateShadowsVisibility(anim::type animated = anim::type::normal);
object_ptr<RpWidget> doTakeInnerWidget();
BoxContentDelegate *_delegate = nullptr;
bool _preparing = false;
bool _noContentMargin = false;
bool _closeByEscape = true;
int _innerTopSkip = 0;
int _innerBottomSkip = 0;
object_ptr<ScrollArea> _scroll = { nullptr };
object_ptr<FadeShadow> _topShadow = { nullptr };
object_ptr<FadeShadow> _bottomShadow = { nullptr };
Ui::DraggingScrollManager _draggingScroll;
Ui::Animations::Simple _scrollAnimation;
rpl::event_stream<> _boxClosingStream;
};
class BoxPointer {
public:
BoxPointer() = default;
BoxPointer(const BoxPointer &other) = default;
BoxPointer(BoxPointer &&other) : _value(base::take(other._value)) {
}
BoxPointer(BoxContent *value) : _value(value) {
}
BoxPointer(base::weak_qptr<BoxContent> value) : _value(value) {
}
BoxPointer &operator=(const BoxPointer &other) {
if (_value != other._value) {
destroy();
_value = other._value;
}
return *this;
}
BoxPointer &operator=(BoxPointer &&other) {
if (_value != other._value) {
destroy();
_value = base::take(other._value);
}
return *this;
}
BoxPointer &operator=(BoxContent *other) {
if (_value != other) {
destroy();
_value = other;
}
return *this;
}
BoxPointer &operator=(base::weak_qptr<BoxContent> other) {
if (_value != other) {
destroy();
_value = other;
}
return *this;
}
~BoxPointer() {
destroy();
}
BoxContent *get() const {
return _value.get();
}
operator BoxContent*() const {
return get();
}
explicit operator bool() const {
return get();
}
BoxContent *operator->() const {
return get();
}
private:
void destroy() {
if (const auto value = base::take(_value)) {
value->closeBox();
}
}
base::weak_qptr<BoxContent> _value;
};
} // namespace Ui

View File

@@ -0,0 +1,427 @@
// 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/layers/box_layer_widget.h"
#include "ui/effects/radial_animation.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/painter.h"
#include "base/timer.h"
#include "styles/style_layers.h"
#include "styles/palette.h"
namespace Ui {
struct BoxLayerWidget::LoadingProgress {
LoadingProgress(
Fn<void()> &&callback,
const style::InfiniteRadialAnimation &st);
InfiniteRadialAnimation animation;
base::Timer removeTimer;
};
BoxLayerWidget::LoadingProgress::LoadingProgress(
Fn<void()> &&callback,
const style::InfiniteRadialAnimation &st)
: animation(std::move(callback), st) {
}
BoxLayerWidget::BoxLayerWidget(
not_null<LayerStackWidget*> layer,
object_ptr<BoxContent> content)
: LayerWidget(layer)
, _layer(layer)
, _content(std::move(content))
, _roundRect(st::boxRadius, st().bg) {
_content->setParent(this);
_content->setDelegate(this);
_additionalTitle.changes(
) | rpl::on_next([=] {
updateSize();
update();
}, lifetime());
}
BoxLayerWidget::~BoxLayerWidget() = default;
void BoxLayerWidget::setLayerType(bool layerType) {
if (_layerType == layerType) {
return;
}
_layerType = layerType;
updateTitlePosition();
if (_maxContentHeight) {
setDimensions(width(), _maxContentHeight);
}
}
int BoxLayerWidget::titleHeight() const {
return st::boxTitleHeight;
}
const style::Box &BoxLayerWidget::st() const {
return _st
? *_st
: _layerType
? (_layer->boxStyleOverrideLayer()
? *_layer->boxStyleOverrideLayer()
: st::layerBox)
: (_layer->boxStyleOverride()
? *_layer->boxStyleOverride()
: st::defaultBox);
}
void BoxLayerWidget::setStyle(const style::Box &st) {
_st = &st;
_roundRect.setColor(st.bg);
}
const style::Box &BoxLayerWidget::style() {
return st();
}
int BoxLayerWidget::buttonsHeight() const {
const auto padding = st().buttonPadding;
return padding.top() + st().buttonHeight + padding.bottom();
}
int BoxLayerWidget::buttonsTop() const {
const auto padding = st().buttonPadding;
return height() - padding.bottom() - st().buttonHeight;
}
QRect BoxLayerWidget::loadingRect() const {
const auto padding = st().buttonPadding;
const auto size = st::boxLoadingSize;
const auto skipx = st::boxTitlePosition.x();
const auto skipy = (st().buttonHeight - size) / 2;
return QRect(
skipx,
height() - padding.bottom() - skipy - size,
size,
size);
}
void BoxLayerWidget::paintEvent(QPaintEvent *e) {
Painter p(this);
const auto clip = e->rect();
const auto paintTopRounded = !(_customCornersFilling & RectPart::FullTop)
&& clip.intersects(QRect(0, 0, width(), st::boxRadius));
const auto paintBottomRounded = !(_customCornersFilling
& RectPart::FullBottom)
&& clip.intersects(
QRect(0, height() - st::boxRadius, width(), st::boxRadius));
if (paintTopRounded || paintBottomRounded) {
_roundRect.paint(p, rect(), RectPart::None
| (paintTopRounded ? RectPart::FullTop : RectPart::None)
| (paintBottomRounded ? RectPart::FullBottom : RectPart::None));
}
const auto other = e->region().intersected(
QRect(0, st::boxRadius, width(), height() - 2 * st::boxRadius));
if (!other.isEmpty()) {
for (const auto &rect : other) {
p.fillRect(rect, st().bg);
}
}
if (!_additionalTitle.current().isEmpty()
&& clip.intersects(QRect(0, 0, width(), titleHeight()))) {
paintAdditionalTitle(p);
}
if (_loadingProgress) {
const auto rect = loadingRect();
_loadingProgress->animation.draw(
p,
rect.topLeft(),
rect.size(),
width());
}
}
void BoxLayerWidget::paintAdditionalTitle(Painter &p) {
p.setFont(st::boxTitleAdditionalFont);
p.setPen(st().titleAdditionalFg);
p.drawTextLeft(
_titleLeft + (_title ? _title->width() : 0) + st::boxTitleAdditionalSkip,
_titleTop + st::boxTitleFont->ascent - st::boxTitleAdditionalFont->ascent,
width(),
_additionalTitle.current());
}
void BoxLayerWidget::parentResized() {
auto newHeight = countRealHeight();
auto parentSize = parentWidget()->size();
setGeometry(
(parentSize.width() - width()) / 2,
(parentSize.height() - newHeight) / 2,
width(),
newHeight);
update();
}
void BoxLayerWidget::setTitle(rpl::producer<TextWithEntities> title) {
const auto wasTitle = hasTitle();
if (title) {
_title.create(this, rpl::duplicate(title), st().title);
_title->show();
std::move(
title
) | rpl::on_next([=] {
updateTitlePosition();
}, _title->lifetime());
} else {
_title.destroy();
}
if (wasTitle != hasTitle()) {
updateSize();
}
}
void BoxLayerWidget::setAdditionalTitle(rpl::producer<QString> additional) {
_additionalTitle = std::move(additional);
}
void BoxLayerWidget::triggerButton(int index) {
if (index < _buttons.size()) {
_buttons[index]->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);
}
}
void BoxLayerWidget::setCloseByOutsideClick(bool close) {
_closeByOutsideClick = close;
}
bool BoxLayerWidget::closeByOutsideClick() const {
return _closeByOutsideClick;
}
bool BoxLayerWidget::hasTitle() const {
return (_title != nullptr) || !_additionalTitle.current().isEmpty();
}
void BoxLayerWidget::showBox(
object_ptr<BoxContent> box,
LayerOptions options,
anim::type animated) {
_layer->showBox(std::move(box), options, animated);
}
void BoxLayerWidget::hideLayer() {
_layer->hideLayers(anim::type::normal);
}
void BoxLayerWidget::updateSize() {
setDimensions(width(), _maxContentHeight);
}
void BoxLayerWidget::updateButtonsPositions() {
if (!_buttons.empty() || _leftButton) {
auto padding = st().buttonPadding;
auto right = padding.right();
auto top = buttonsTop();
if (_leftButton) {
_leftButton->moveToLeft(right, top);
}
for (const auto &button : _buttons) {
button->moveToRight(right, top);
right += button->width() + padding.left();
}
}
auto right = 0;
for (const auto &button : _topButtons) {
button->moveToRight(right, 0);
right += button->width();
}
}
ShowFactory BoxLayerWidget::showFactory() {
return _layer->showFactory();
}
QPointer<QWidget> BoxLayerWidget::outerContainer() {
return parentWidget();
}
void BoxLayerWidget::updateTitlePosition() {
_titleLeft = st::boxTitlePosition.x();
_titleTop = st::boxTitlePosition.y();
if (_title) {
auto topButtonsSkip = 0;
for (const auto &button : _topButtons) {
topButtonsSkip += button->width();
}
_title->resizeToNaturalWidth(
width() - _titleLeft * 2 - topButtonsSkip);
_title->moveToLeft(_titleLeft, _titleTop);
}
}
void BoxLayerWidget::setCustomCornersFilling(RectParts corners) {
_customCornersFilling = corners;
update();
}
void BoxLayerWidget::clearButtons() {
for (auto &button : base::take(_buttons)) {
button.destroy();
}
_leftButton.destroy();
base::take(_topButtons);
}
void BoxLayerWidget::addButton(object_ptr<AbstractButton> button) {
_buttons.push_back(std::move(button));
const auto raw = _buttons.back().data();
raw->setParent(this);
raw->show();
if (st().buttonWide) {
widthValue() | rpl::on_next([=](int width) {
const auto buttonWidth = width
- st().buttonPadding.left()
- st().buttonPadding.right();
if (buttonWidth > 0) {
raw->resizeToWidth(buttonWidth);
}
}, raw->lifetime());
}
raw->widthValue(
) | rpl::on_next([=] {
if (st().buttonWide) {
const auto buttonWidth = width()
- st().buttonPadding.left()
- st().buttonPadding.right();
if (buttonWidth > 0 && raw->width() != buttonWidth) {
raw->resizeToWidth(buttonWidth);
}
}
updateButtonsPositions();
}, raw->lifetime());
}
void BoxLayerWidget::addLeftButton(object_ptr<AbstractButton> button) {
_leftButton = std::move(button);
const auto raw = _leftButton.data();
raw->setParent(this);
raw->show();
raw->widthValue(
) | rpl::on_next([=] {
updateButtonsPositions();
}, raw->lifetime());
}
void BoxLayerWidget::addTopButton(object_ptr<AbstractButton> button) {
_topButtons.push_back(base::unique_qptr<AbstractButton>(button.release()));
const auto raw = _topButtons.back().get();
raw->setParent(this);
raw->show();
updateButtonsPositions();
updateTitlePosition();
}
void BoxLayerWidget::showLoading(bool show) {
const auto &st = st::boxLoadingAnimation;
if (!show) {
if (_loadingProgress && !_loadingProgress->removeTimer.isActive()) {
_loadingProgress->removeTimer.callOnce(
st.sineDuration + st.sinePeriod);
_loadingProgress->animation.stop();
}
return;
}
if (!_loadingProgress) {
const auto callback = [=] {
if (!anim::Disabled()) {
const auto t = st::boxLoadingAnimation.thickness;
update(loadingRect().marginsAdded({ t, t, t, t }));
}
};
_loadingProgress = std::make_unique<LoadingProgress>(
callback,
st::boxLoadingAnimation);
_loadingProgress->removeTimer.setCallback([=] {
_loadingProgress = nullptr;
});
} else {
_loadingProgress->removeTimer.cancel();
}
_loadingProgress->animation.start();
}
void BoxLayerWidget::setDimensions(int newWidth, int maxHeight, bool forceCenterPosition) {
_maxContentHeight = maxHeight;
auto fullHeight = countFullHeight();
if (width() != newWidth || _fullHeight != fullHeight) {
_fullHeight = fullHeight;
if (parentWidget()) {
auto oldGeometry = geometry();
resize(newWidth, countRealHeight());
auto newGeometry = geometry();
auto parentHeight = parentWidget()->height();
const auto bottomMargin = st().margin.bottom();
if (newGeometry.top() + newGeometry.height() + bottomMargin > parentHeight
|| forceCenterPosition) {
const auto top1 = parentHeight - bottomMargin - newGeometry.height();
const auto top2 = (parentHeight - newGeometry.height()) / 2;
const auto newTop = forceCenterPosition
? std::min(top1, top2)
: std::max(top1, top2);
if (newTop != newGeometry.top()) {
move(newGeometry.left(), newTop);
resizeEvent(0);
}
}
parentWidget()->update(oldGeometry.united(geometry()).marginsAdded(st::boxRoundShadow.extend));
} else {
resize(newWidth, 0);
}
}
}
int BoxLayerWidget::countRealHeight() const {
const auto &margin = st().margin;
return std::min(
_fullHeight,
parentWidget()->height() - margin.top() - margin.bottom());
}
int BoxLayerWidget::countFullHeight() const {
return contentTop() + _maxContentHeight + buttonsHeight();
}
int BoxLayerWidget::contentTop() const {
return hasTitle()
? titleHeight()
: _noContentMargin
?
0
: st::boxTopMargin;
}
void BoxLayerWidget::resizeEvent(QResizeEvent *e) {
updateButtonsPositions();
updateTitlePosition();
const auto top = contentTop();
_content->resize(width(), height() - top - buttonsHeight());
_content->moveToLeft(0, top);
LayerWidget::resizeEvent(e);
}
void BoxLayerWidget::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Escape) {
closeBox();
} else {
LayerWidget::keyPressEvent(e);
}
}
} // namespace Ui

View File

@@ -0,0 +1,143 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/unique_qptr.h"
#include "base/flags.h"
#include "ui/layers/layer_widget.h"
#include "ui/layers/box_content.h"
#include "ui/round_rect.h"
#include "ui/rp_widget.h"
class Painter;
struct TextWithEntities;
namespace anim {
enum class type : uchar;
} // namespace anim
namespace style {
struct Box;
} // namespace style
namespace Ui {
class AbstractButton;
class FlatLabel;
class BoxLayerWidget : public LayerWidget, public BoxContentDelegate {
public:
BoxLayerWidget(
not_null<LayerStackWidget*> layer,
object_ptr<BoxContent> content);
~BoxLayerWidget();
void parentResized() override;
void setLayerType(bool layerType) override;
void setStyle(const style::Box &st) override;
const style::Box &style() override;
void setTitle(rpl::producer<TextWithEntities> title) override;
void setAdditionalTitle(rpl::producer<QString> additional) override;
void showBox(
object_ptr<BoxContent> box,
LayerOptions options,
anim::type animated) override;
void showFinished() override {
_content->showFinished();
}
void setCustomCornersFilling(RectParts corners) override;
void clearButtons() override;
void addButton(object_ptr<AbstractButton> button) override;
void addLeftButton(object_ptr<AbstractButton> button) override;
void addTopButton(object_ptr<AbstractButton> button) override;
void showLoading(bool show) override;
void updateButtonsPositions() override;
ShowFactory showFactory() override;
QPointer<QWidget> outerContainer() override;
void setDimensions(
int newWidth,
int maxHeight,
bool forceCenterPosition = false) override;
void setNoContentMargin(bool noContentMargin) override {
if (_noContentMargin != noContentMargin) {
_noContentMargin = noContentMargin;
updateSize();
}
}
bool isBoxShown() const override {
return !isHidden();
}
void closeBox() override {
closeLayer();
}
void hideLayer() override;
void triggerButton(int index) override;
void setCloseByOutsideClick(bool close) override;
bool closeByOutsideClick() const override;
protected:
void keyPressEvent(QKeyEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void doSetInnerFocus() override {
_content->setInnerFocus();
}
void closeHook() override {
_content->notifyBoxClosing();
}
private:
struct LoadingProgress;
void paintAdditionalTitle(Painter &p);
void updateTitlePosition();
[[nodiscard]] const style::Box &st() const;
[[nodiscard]] bool hasTitle() const;
[[nodiscard]] int titleHeight() const;
[[nodiscard]] int buttonsHeight() const;
[[nodiscard]] int buttonsTop() const;
[[nodiscard]] int contentTop() const;
[[nodiscard]] int countFullHeight() const;
[[nodiscard]] int countRealHeight() const;
[[nodiscard]] QRect loadingRect() const;
void updateSize();
const style::Box *_st = nullptr;
not_null<LayerStackWidget*> _layer;
bool _layerType = false;
int _fullHeight = 0;
bool _noContentMargin = false;
int _maxContentHeight = 0;
object_ptr<BoxContent> _content;
RoundRect _roundRect;
object_ptr<FlatLabel> _title = { nullptr };
Fn<TextWithEntities()> _titleFactory;
rpl::variable<QString> _additionalTitle;
RectParts _customCornersFilling;
int _titleLeft = 0;
int _titleTop = 0;
bool _closeByOutsideClick = true;
std::vector<object_ptr<AbstractButton>> _buttons;
object_ptr<AbstractButton> _leftButton = { nullptr };
std::vector<base::unique_qptr<AbstractButton>> _topButtons;
std::unique_ptr<LoadingProgress> _loadingProgress;
};
} // namespace Ui

View File

@@ -0,0 +1,131 @@
// 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/layers/generic_box.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/wrap/wrap.h"
#include "styles/style_layers.h"
namespace Ui {
void GenericBox::prepare() {
_init(this);
const auto currentWidth = width();
const auto pinnedToTop = _pinnedToTopContent.data();
const auto pinnedToBottom = _pinnedToBottomContent.data();
if (pinnedToTop) {
pinnedToTop->resizeToWidth(currentWidth);
}
if (pinnedToBottom) {
pinnedToBottom->resizeToWidth(currentWidth);
}
auto wrap = object_ptr<Ui::OverrideMargins>(this, std::move(_owned));
wrap->resizeToWidth(currentWidth);
rpl::combine(
pinnedToTop ? pinnedToTop->heightValue() : rpl::single(0),
wrap->heightValue(),
pinnedToBottom ? pinnedToBottom->heightValue() : rpl::single(0)
) | rpl::on_next([=](int top, int height, int bottom) {
Expects(_minHeight >= 0);
Expects(!_maxHeight || _minHeight <= _maxHeight);
setInnerTopSkip(top);
setInnerBottomSkip(bottom);
const auto desired = top + height + bottom;
setDimensions(
currentWidth,
std::clamp(
desired,
_minHeight,
_maxHeight ? _maxHeight : std::max(_minHeight, desired)),
true);
}, wrap->lifetime());
setInnerWidget(
std::move(wrap),
_scrollSt ? *_scrollSt : st::boxScroll,
pinnedToTop ? pinnedToTop->height() : 0,
pinnedToBottom ? pinnedToBottom->height() : 0);
if (pinnedToBottom) {
rpl::combine(
heightValue(),
pinnedToBottom->heightValue()
) | rpl::on_next([=](int outer, int height) {
pinnedToBottom->move(0, outer - height);
}, pinnedToBottom->lifetime());
}
if (const auto onstack = _initScroll) {
onstack();
}
}
void GenericBox::addSkip(int height) {
addRow(object_ptr<Ui::FixedHeightWidget>(this, height));
}
void GenericBox::setInnerFocus() {
if (_focus) {
_focus();
} else {
BoxContent::setInnerFocus();
}
}
void GenericBox::showFinished() {
const auto guard = QPointer(this);
if (const auto onstack = _showFinished) {
onstack();
if (!guard) {
return;
}
}
_showFinishes.fire({});
}
not_null<Ui::RpWidget*> GenericBox::doSetPinnedToTopContent(
object_ptr<Ui::RpWidget> content) {
_pinnedToTopContent = std::move(content);
return _pinnedToTopContent.data();
}
not_null<Ui::RpWidget*> GenericBox::doSetPinnedToBottomContent(
object_ptr<Ui::RpWidget> content) {
_pinnedToBottomContent = std::move(content);
return _pinnedToBottomContent.data();
}
int GenericBox::rowsCount() const {
return _content->count();
}
int GenericBox::width() const {
return _width ? _width : st::boxWidth;
}
not_null<Ui::VerticalLayout*> GenericBox::verticalLayout() {
return _content;
}
rpl::producer<> BoxShowFinishes(not_null<GenericBox*> box) {
const auto singleShot = box->lifetime().make_state<rpl::lifetime>();
const auto showFinishes = singleShot->make_state<rpl::event_stream<>>();
box->setShowFinishedCallback([=] {
showFinishes->fire({});
singleShot->destroy();
box->setShowFinishedCallback(nullptr);
});
return showFinishes->events();
}
} // namespace Ui

View File

@@ -0,0 +1,230 @@
// 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/layers/box_content.h"
#include "ui/wrap/vertical_layout.h"
#include <tuple>
namespace st {
extern const style::margins &boxRowPadding;
} // namespace st
namespace Ui {
class GenericBox final : public BoxContent {
public:
// InitMethod::operator()(not_null<GenericBox*> box, InitArgs...)
// init(box, args...)
template <
typename InitMethod,
typename ...InitArgs,
typename = decltype(std::declval<std::decay_t<InitMethod>>()(
std::declval<not_null<GenericBox*>>(),
std::declval<std::decay_t<InitArgs>>()...))>
GenericBox(
QWidget*,
InitMethod &&init,
InitArgs &&...args);
void setWidth(int width) {
_width = width;
}
void setFocusCallback(Fn<void()> callback) {
_focus = callback;
}
void setInitScrollCallback(Fn<void()> callback) {
_initScroll = callback;
}
void setShowFinishedCallback(Fn<void()> callback) {
_showFinished = callback;
}
[[nodiscard]] rpl::producer<> showFinishes() const {
return _showFinishes.events();
}
[[nodiscard]] int rowsCount() const;
[[nodiscard]] int width() const;
template <
typename Widget,
typename = std::enable_if_t<
std::is_base_of_v<RpWidget, Widget>>>
Widget *insertRow(
int atPosition,
object_ptr<Widget> &&child,
const style::margins &margin = st::boxRowPadding,
style::align align = style::al_left) {
return _content->insert(
atPosition,
std::move(child),
margin,
align);
}
template <
typename Widget,
typename = std::enable_if_t<
std::is_base_of_v<RpWidget, Widget>>>
Widget *insertRow(
int atPosition,
object_ptr<Widget> &&child,
style::align align) {
return _content->insert(
atPosition,
std::move(child),
st::boxRowPadding,
align);
}
template <
typename Widget,
typename = std::enable_if_t<
std::is_base_of_v<RpWidget, Widget>>>
Widget *addRow(
object_ptr<Widget> &&child,
const style::margins &margin = st::boxRowPadding,
style::align align = style::al_left) {
return _content->add(std::move(child), margin, align);
}
template <
typename Widget,
typename = std::enable_if_t<
std::is_base_of_v<RpWidget, Widget>>>
Widget *addRow(object_ptr<Widget> &&child, style::align align) {
return _content->add(std::move(child), st::boxRowPadding, align);
}
void addSkip(int height);
void setMaxHeight(int maxHeight) {
_maxHeight = maxHeight;
}
void setMinHeight(int minHeight) {
_minHeight = minHeight;
}
void setScrollStyle(const style::ScrollArea &st) {
_scrollSt = &st;
}
void setInnerFocus() override;
void showFinished() override;
template <typename Widget>
not_null<Widget*> setPinnedToTopContent(object_ptr<Widget> content) {
return static_cast<Widget*>(
doSetPinnedToTopContent(std::move(content)).get());
}
template <typename Widget>
not_null<Widget*> setPinnedToBottomContent(object_ptr<Widget> content) {
return static_cast<Widget*>(
doSetPinnedToBottomContent(std::move(content)).get());
}
[[nodiscard]] not_null<Ui::VerticalLayout*> verticalLayout();
using BoxContent::setNoContentMargin;
private:
template <typename InitMethod, typename ...InitArgs>
struct Initer {
template <
typename OtherMethod,
typename ...OtherArgs,
typename = std::enable_if_t<
std::is_constructible_v<InitMethod, OtherMethod&&>>>
Initer(OtherMethod &&method, OtherArgs &&...args);
void operator()(not_null<GenericBox*> box);
template <std::size_t... I>
void call(
not_null<GenericBox*> box,
std::index_sequence<I...>);
InitMethod method;
std::tuple<InitArgs...> args;
};
template <typename InitMethod, typename ...InitArgs>
auto MakeIniter(InitMethod &&method, InitArgs &&...args)
-> Initer<std::decay_t<InitMethod>, std::decay_t<InitArgs>...>;
void prepare() override;
not_null<Ui::RpWidget*> doSetPinnedToTopContent(
object_ptr<Ui::RpWidget> content);
not_null<Ui::RpWidget*> doSetPinnedToBottomContent(
object_ptr<Ui::RpWidget> content);
FnMut<void(not_null<GenericBox*>)> _init;
Fn<void()> _focus;
Fn<void()> _initScroll;
Fn<void()> _showFinished;
rpl::event_stream<> _showFinishes;
object_ptr<Ui::VerticalLayout> _owned;
not_null<Ui::VerticalLayout*> _content;
const style::ScrollArea *_scrollSt = nullptr;
int _width = 0;
int _minHeight = 0;
int _maxHeight = 0;
object_ptr<Ui::RpWidget> _pinnedToTopContent = { nullptr };
object_ptr<Ui::RpWidget> _pinnedToBottomContent = { nullptr };
};
template <typename InitMethod, typename ...InitArgs>
template <typename OtherMethod, typename ...OtherArgs, typename>
GenericBox::Initer<InitMethod, InitArgs...>::Initer(
OtherMethod &&method,
OtherArgs &&...args)
: method(std::forward<OtherMethod>(method))
, args(std::forward<OtherArgs>(args)...) {
}
template <typename InitMethod, typename ...InitArgs>
inline void GenericBox::Initer<InitMethod, InitArgs...>::operator()(
not_null<GenericBox*> box) {
call(box, std::make_index_sequence<sizeof...(InitArgs)>());
}
template <typename InitMethod, typename ...InitArgs>
template <std::size_t... I>
inline void GenericBox::Initer<InitMethod, InitArgs...>::call(
not_null<GenericBox*> box,
std::index_sequence<I...>) {
std::invoke(method, box, std::get<I>(std::move(args))...);
}
template <typename InitMethod, typename ...InitArgs>
inline auto GenericBox::MakeIniter(InitMethod &&method, InitArgs &&...args)
-> Initer<std::decay_t<InitMethod>, std::decay_t<InitArgs>...> {
return {
std::forward<InitMethod>(method),
std::forward<InitArgs>(args)...
};
}
template <typename InitMethod, typename ...InitArgs, typename>
inline GenericBox::GenericBox(
QWidget*,
InitMethod &&init,
InitArgs &&...args)
: _init(
MakeIniter(
std::forward<InitMethod>(init),
std::forward<InitArgs>(args)...))
, _owned(this)
, _content(_owned.data()) {
}
[[nodiscard]] rpl::producer<> BoxShowFinishes(not_null<GenericBox*> box);
} // namespace Ui

View File

@@ -0,0 +1,191 @@
// 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/layers/layer_manager.h"
#include "ui/layers/show.h"
#include "ui/ui_utility.h"
namespace Ui {
class LayerManager::ManagerShow final : public Show {
public:
explicit ManagerShow(not_null<LayerManager*> manager);
~ManagerShow();
void showOrHideBoxOrLayer(
std::variant<
v::null_t,
object_ptr<BoxContent>,
std::unique_ptr<LayerWidget>> &&layer,
LayerOptions options,
anim::type animated) const override;
[[nodiscard]] not_null<QWidget*> toastParent() const override;
[[nodiscard]] bool valid() const override;
operator bool() const override;
private:
const base::weak_ptr<LayerManager> _manager;
};
LayerManager::ManagerShow::ManagerShow(not_null<LayerManager*> manager)
: _manager(manager.get()) {
}
LayerManager::ManagerShow::~ManagerShow() = default;
void LayerManager::ManagerShow::showOrHideBoxOrLayer(
std::variant<
v::null_t,
object_ptr<BoxContent>,
std::unique_ptr<LayerWidget>> &&layer,
LayerOptions options,
anim::type animated) const {
using UniqueLayer = std::unique_ptr<Ui::LayerWidget>;
using ObjectBox = object_ptr<Ui::BoxContent>;
if (auto layerWidget = std::get_if<UniqueLayer>(&layer)) {
if (const auto manager = _manager.get()) {
manager->showLayer(std::move(*layerWidget), options, animated);
}
} else if (auto box = std::get_if<ObjectBox>(&layer)) {
if (const auto manager = _manager.get()) {
manager->showBox(std::move(*box), options, animated);
}
} else if (const auto manager = _manager.get()) {
manager->hideAll(animated);
}
}
not_null<QWidget*> LayerManager::ManagerShow::toastParent() const {
const auto manager = _manager.get();
Ensures(manager != nullptr);
return manager->toastParent();
}
bool LayerManager::ManagerShow::valid() const {
return (_manager.get() != nullptr);
}
LayerManager::ManagerShow::operator bool() const {
return valid();
}
LayerManager::LayerManager(not_null<RpWidget*> widget)
: _widget(widget) {
}
void LayerManager::setStyleOverrides(
const style::Box *boxSt,
const style::Box *layerSt) {
_boxSt = boxSt;
_layerSt = layerSt;
if (_layer) {
_layer->setStyleOverrides(_boxSt, _layerSt);
}
}
void LayerManager::setHideByBackgroundClick(bool hide) {
_hideByBackgroundClick = hide;
if (_layer) {
_layer->setHideByBackgroundClick(hide);
}
}
void LayerManager::showBox(
object_ptr<BoxContent> box,
LayerOptions options,
anim::type animated) {
ensureLayerCreated();
_layer->showBox(std::move(box), options, animated);
setFocus();
}
void LayerManager::showLayer(
std::unique_ptr<LayerWidget> layer,
LayerOptions options,
anim::type animated) {
ensureLayerCreated();
_layer->showLayer(std::move(layer), options, animated);
setFocus();
}
void LayerManager::hideAll(anim::type animated) {
if (animated == anim::type::instant) {
destroyLayer();
} else if (_layer) {
_layer->hideAll(animated);
}
}
void LayerManager::raise() {
if (_layer) {
_layer->raise();
}
}
bool LayerManager::setFocus() {
if (!_layer) {
return false;
}
_layer->setInnerFocus();
return true;
}
std::shared_ptr<Show> LayerManager::uiShow() {
if (!_cachedShow) {
_cachedShow = std::make_shared<ManagerShow>(this);
}
return _cachedShow;
}
const LayerWidget *LayerManager::topShownLayer() const {
return _layer ? _layer->topShownLayer() : nullptr;
}
void LayerManager::ensureLayerCreated() {
if (_layer) {
return;
}
_layer.emplace(_widget, crl::guard(this, [=] {
return uiShow();
}));
_layer->setHideByBackgroundClick(_hideByBackgroundClick);
_layer->setStyleOverrides(_boxSt, _layerSt);
_layer->hideFinishEvents(
) | rpl::filter([=] {
return _layer != nullptr; // Last hide finish is sent from destructor.
}) | rpl::on_next([=] {
destroyLayer();
}, _layer->lifetime());
_layer->move(0, 0);
_widget->sizeValue(
) | rpl::on_next([=](QSize size) {
_layer->resize(size);
}, _layer->lifetime());
_layerShown = true;
}
void LayerManager::destroyLayer() {
if (!_layer) {
return;
}
auto layer = base::take(_layer);
_layerShown = false;
const auto resetFocus = Ui::InFocusChain(layer);
if (resetFocus) {
_widget->setFocus();
}
layer = nullptr;
}
} // namespace Ui

View File

@@ -0,0 +1,73 @@
// 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/weak_ptr.h"
#include "ui/layers/layer_widget.h"
#include <QtCore/QMargins>
namespace style {
struct Box;
} // namespace style
namespace Ui {
class BoxContent;
class RpWidget;
class Show;
class LayerManager final : public base::has_weak_ptr {
public:
explicit LayerManager(not_null<RpWidget*> widget);
void setStyleOverrides(
const style::Box *boxSt,
const style::Box *layerSt);
void setHideByBackgroundClick(bool hide);
void showBox(
object_ptr<BoxContent> box,
LayerOptions options = LayerOption::KeepOther,
anim::type animated = anim::type::normal);
void showLayer(
std::unique_ptr<LayerWidget> layer,
LayerOptions options = LayerOption::KeepOther,
anim::type animated = anim::type::normal);
void hideAll(anim::type animated = anim::type::normal);
void raise();
bool setFocus();
[[nodiscard]] rpl::producer<bool> layerShownValue() const {
return _layerShown.value();
}
[[nodiscard]] not_null<Ui::RpWidget*> toastParent() const {
return _widget;
}
[[nodiscard]] const LayerWidget *topShownLayer() const;
[[nodiscard]] std::shared_ptr<Show> uiShow();
private:
class ManagerShow;
void ensureLayerCreated();
void destroyLayer();
const not_null<RpWidget*> _widget;
base::unique_qptr<LayerStackWidget> _layer;
std::shared_ptr<ManagerShow> _cachedShow;
rpl::variable<bool> _layerShown;
const style::Box *_boxSt = nullptr;
const style::Box *_layerSt = nullptr;
bool _hideByBackgroundClick = false;
};
} // namespace Ui

View File

@@ -0,0 +1,955 @@
// 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/layers/layer_widget.h"
#include "ui/cached_special_layer_shadow_corners.h"
#include "ui/layers/box_layer_widget.h"
#include "ui/widgets/shadow.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "ui/round_rect.h"
#include "base/qt/qt_tab_key.h"
#include "base/integration.h"
#include "styles/style_layers.h"
#include "styles/style_widgets.h"
#include "styles/palette.h"
#include <QtGui/QtEvents>
namespace Ui {
class LayerStackWidget::BackgroundWidget : public RpWidget {
public:
using RpWidget::RpWidget;
void setDoneCallback(Fn<void()> callback) {
_doneCallback = std::move(callback);
}
void setLayerBoxes(const QRect &specialLayerBox, const QRect &layerBox);
void setCacheImages(
QPixmap &&bodyCache,
QPixmap &&mainMenuCache,
QPixmap &&specialLayerCache,
QPixmap &&layerCache);
void removeBodyCache();
[[nodiscard]] bool hasBodyCache() const;
void refreshBodyCache(QPixmap &&bodyCache);
void startAnimation(Action action);
void skipAnimation(Action action);
void finishAnimating();
bool animating() const {
return _a_mainMenuShown.animating() || _a_specialLayerShown.animating() || _a_layerShown.animating();
}
protected:
void paintEvent(QPaintEvent *e) override;
private:
bool isShown() const {
return _mainMenuShown || _specialLayerShown || _layerShown;
}
void checkIfDone();
void setMainMenuShown(bool shown);
void setSpecialLayerShown(bool shown);
void setLayerShown(bool shown);
void checkWasShown(bool wasShown);
void animationCallback();
QPixmap _bodyCache;
QPixmap _mainMenuCache;
int _mainMenuCacheWidth = 0;
QPixmap _specialLayerCache;
QPixmap _layerCache;
Fn<void()> _doneCallback;
bool _wasAnimating = false;
bool _inPaintEvent = false;
bool _repaintIssued = false;
Ui::Animations::Simple _a_shown;
Ui::Animations::Simple _a_mainMenuShown;
Ui::Animations::Simple _a_specialLayerShown;
Ui::Animations::Simple _a_layerShown;
QRect _specialLayerBox, _specialLayerCacheBox;
QRect _layerBox, _layerCacheBox;
int _mainMenuRight = 0;
bool _mainMenuShown = false;
bool _specialLayerShown = false;
bool _layerShown = false;
};
void LayerStackWidget::BackgroundWidget::setCacheImages(
QPixmap &&bodyCache,
QPixmap &&mainMenuCache,
QPixmap &&specialLayerCache,
QPixmap &&layerCache) {
_bodyCache = std::move(bodyCache);
_mainMenuCache = std::move(mainMenuCache);
_specialLayerCache = std::move(specialLayerCache);
_layerCache = std::move(layerCache);
_specialLayerCacheBox = _specialLayerBox;
_layerCacheBox = _layerBox;
_repaintIssued = false;
setAttribute(Qt::WA_OpaquePaintEvent, !_bodyCache.isNull());
}
void LayerStackWidget::BackgroundWidget::removeBodyCache() {
if (hasBodyCache()) {
_bodyCache = {};
setAttribute(Qt::WA_OpaquePaintEvent, false);
}
}
bool LayerStackWidget::BackgroundWidget::hasBodyCache() const {
return !_bodyCache.isNull();
}
void LayerStackWidget::BackgroundWidget::refreshBodyCache(
QPixmap &&bodyCache) {
_bodyCache = std::move(bodyCache);
setAttribute(Qt::WA_OpaquePaintEvent, !_bodyCache.isNull());
}
void LayerStackWidget::BackgroundWidget::startAnimation(Action action) {
if (action == Action::ShowMainMenu) {
setMainMenuShown(true);
} else if (action != Action::HideLayer
&& action != Action::HideSpecialLayer) {
setMainMenuShown(false);
}
if (action == Action::ShowSpecialLayer) {
setSpecialLayerShown(true);
} else if (action == Action::ShowMainMenu
|| action == Action::HideAll
|| action == Action::HideSpecialLayer) {
setSpecialLayerShown(false);
}
if (action == Action::ShowLayer) {
setLayerShown(true);
} else if (action != Action::ShowSpecialLayer
&& action != Action::HideSpecialLayer) {
setLayerShown(false);
}
_wasAnimating = true;
checkIfDone();
}
void LayerStackWidget::BackgroundWidget::skipAnimation(Action action) {
_repaintIssued = false;
startAnimation(action);
finishAnimating();
}
void LayerStackWidget::BackgroundWidget::checkIfDone() {
if (!_wasAnimating || _inPaintEvent || animating()) {
return;
}
_wasAnimating = false;
_mainMenuCache = _specialLayerCache = _layerCache = QPixmap();
removeBodyCache();
if (_doneCallback) {
_doneCallback();
}
}
void LayerStackWidget::BackgroundWidget::setMainMenuShown(bool shown) {
auto wasShown = isShown();
if (_mainMenuShown != shown) {
_mainMenuShown = shown;
_a_mainMenuShown.start([this] { animationCallback(); }, _mainMenuShown ? 0. : 1., _mainMenuShown ? 1. : 0., st::boxDuration, anim::easeOutCirc);
}
_mainMenuCacheWidth = (_mainMenuCache.width() / style::DevicePixelRatio())
- st::boxRoundShadow.extend.right();
_mainMenuRight = _mainMenuShown ? _mainMenuCacheWidth : 0;
checkWasShown(wasShown);
}
void LayerStackWidget::BackgroundWidget::setSpecialLayerShown(bool shown) {
auto wasShown = isShown();
if (_specialLayerShown != shown) {
_specialLayerShown = shown;
_a_specialLayerShown.start([this] { animationCallback(); }, _specialLayerShown ? 0. : 1., _specialLayerShown ? 1. : 0., st::boxDuration);
}
checkWasShown(wasShown);
}
void LayerStackWidget::BackgroundWidget::setLayerShown(bool shown) {
auto wasShown = isShown();
if (_layerShown != shown) {
_layerShown = shown;
_a_layerShown.start([this] { animationCallback(); }, _layerShown ? 0. : 1., _layerShown ? 1. : 0., st::boxDuration);
}
checkWasShown(wasShown);
}
void LayerStackWidget::BackgroundWidget::checkWasShown(bool wasShown) {
if (isShown() != wasShown) {
_a_shown.start([this] { animationCallback(); }, wasShown ? 1. : 0., wasShown ? 0. : 1., st::boxDuration, anim::easeOutCirc);
}
}
void LayerStackWidget::BackgroundWidget::setLayerBoxes(const QRect &specialLayerBox, const QRect &layerBox) {
_specialLayerBox = specialLayerBox;
_layerBox = layerBox;
update();
}
void LayerStackWidget::BackgroundWidget::paintEvent(QPaintEvent *e) {
Painter p(this);
_inPaintEvent = true;
auto guard = gsl::finally([this] {
_inPaintEvent = false;
crl::on_main(this, [=] { checkIfDone(); });
});
if (!_bodyCache.isNull()) {
p.drawPixmap(0, 0, _bodyCache);
}
auto specialLayerBox = _specialLayerCache.isNull() ? _specialLayerBox : _specialLayerCacheBox;
auto layerBox = _layerCache.isNull() ? _layerBox : _layerCacheBox;
auto mainMenuProgress = _a_mainMenuShown.value(-1);
auto mainMenuRight = (_mainMenuCache.isNull() || mainMenuProgress < 0) ? _mainMenuRight : (mainMenuProgress < 0) ? _mainMenuRight : anim::interpolate(0, _mainMenuCacheWidth, mainMenuProgress);
if (mainMenuRight) {
// Move showing boxes to the right while main menu is hiding.
if (!_specialLayerCache.isNull()) {
specialLayerBox.moveLeft(specialLayerBox.left() + mainMenuRight / 2);
}
if (!_layerCache.isNull()) {
layerBox.moveLeft(layerBox.left() + mainMenuRight / 2);
}
}
auto bgOpacity = _a_shown.value(isShown() ? 1. : 0.);
auto specialLayerOpacity = _a_specialLayerShown.value(_specialLayerShown ? 1. : 0.);
auto layerOpacity = _a_layerShown.value(_layerShown ? 1. : 0.);
if (bgOpacity == 0.) {
return;
}
p.setOpacity(bgOpacity);
auto overSpecialOpacity = (layerOpacity * specialLayerOpacity);
auto bg = myrtlrect(mainMenuRight, 0, width() - mainMenuRight, height());
if (_mainMenuCache.isNull() && mainMenuRight > 0) {
// All cache images are taken together with their shadows,
// so we paint shadow only when there is no cache.
Ui::Shadow::paint(p, myrtlrect(0, 0, mainMenuRight, height()), width(), st::boxRoundShadow, RectPart::Right);
}
if (_specialLayerCache.isNull() && !specialLayerBox.isEmpty()) {
// All cache images are taken together with their shadows,
// so we paint shadow only when there is no cache.
auto sides = RectPart::Left | RectPart::Right;
auto topCorners = (specialLayerBox.y() > 0);
auto bottomCorners = (specialLayerBox.y() + specialLayerBox.height() < height());
if (topCorners) {
sides |= RectPart::Top;
}
if (bottomCorners) {
sides |= RectPart::Bottom;
}
if (topCorners || bottomCorners) {
p.setClipRegion(QRegion(rect()) - specialLayerBox.marginsRemoved(QMargins(st::boxRadius, 0, st::boxRadius, 0)) - specialLayerBox.marginsRemoved(QMargins(0, st::boxRadius, 0, st::boxRadius)));
}
Ui::Shadow::paint(p, specialLayerBox, width(), st::boxRoundShadow, Ui::SpecialLayerShadowCorners(), sides);
if (topCorners || bottomCorners) {
p.setClipping(false);
}
}
if (!layerBox.isEmpty() && !_specialLayerCache.isNull() && overSpecialOpacity < bgOpacity) {
// In case of moving special layer below the background while showing a box
// we need to fill special layer rect below its cache with a complex opacity
// (alpha_final - alpha_current) / (1 - alpha_current) so we won't get glitches
// in the transparent special layer cache corners after filling special layer
// rect above its cache with alpha_current opacity.
const auto region = QRegion(bg) - specialLayerBox;
for (const auto &rect : region) {
p.fillRect(rect, st::layerBg);
}
p.setOpacity((bgOpacity - overSpecialOpacity) / (1. - (overSpecialOpacity * st::layerBg->c.alphaF())));
p.fillRect(specialLayerBox, st::layerBg);
p.setOpacity(bgOpacity);
} else {
p.fillRect(bg, st::layerBg);
}
if (!_specialLayerCache.isNull() && specialLayerOpacity > 0) {
p.setOpacity(specialLayerOpacity);
auto cacheLeft = specialLayerBox.x() - st::boxRoundShadow.extend.left();
auto cacheTop = specialLayerBox.y() - (specialLayerBox.y() > 0 ? st::boxRoundShadow.extend.top() : 0);
p.drawPixmapLeft(cacheLeft, cacheTop, width(), _specialLayerCache);
}
if (!layerBox.isEmpty()) {
if (!_specialLayerCache.isNull()) {
p.setOpacity(overSpecialOpacity);
p.fillRect(specialLayerBox, st::layerBg);
}
if (_layerCache.isNull()) {
p.setOpacity(layerOpacity);
Ui::Shadow::paint(p, layerBox, width(), st::boxRoundShadow);
}
}
if (!_layerCache.isNull() && layerOpacity > 0) {
p.setOpacity(layerOpacity);
p.drawPixmapLeft(layerBox.topLeft() - QPoint(st::boxRoundShadow.extend.left(), st::boxRoundShadow.extend.top()), width(), _layerCache);
}
if (!_mainMenuCache.isNull() && mainMenuRight > 0) {
p.setOpacity(1.);
auto shownWidth = mainMenuRight + st::boxRoundShadow.extend.right();
auto sourceWidth = shownWidth * style::DevicePixelRatio();
auto sourceRect = style::rtlrect(_mainMenuCache.width() - sourceWidth, 0, sourceWidth, _mainMenuCache.height(), _mainMenuCache.width());
p.drawPixmapLeft(0, 0, shownWidth, height(), width(), _mainMenuCache, sourceRect);
}
if (!_repaintIssued && !_a_shown.animating()) {
_repaintIssued = true;
update();
}
}
void LayerStackWidget::BackgroundWidget::finishAnimating() {
_a_shown.stop();
_a_mainMenuShown.stop();
_a_specialLayerShown.stop();
_a_layerShown.stop();
checkIfDone();
}
void LayerStackWidget::BackgroundWidget::animationCallback() {
update();
checkIfDone();
}
LayerStackWidget::LayerStackWidget(QWidget *parent, ShowFactory showFactory)
: RpWidget(parent)
, _background(this)
, _showFactory(std::move(showFactory)) {
setGeometry(parentWidget()->rect());
hide();
_background->setDoneCallback([this] { animationDone(); });
}
void LayerWidget::setInnerFocus() {
if (!isAncestorOf(window()->focusWidget())) {
doSetInnerFocus();
}
}
bool LayerWidget::overlaps(const QRect &globalRect) {
if (isHidden()) {
return false;
}
auto testRect = QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size());
if (testAttribute(Qt::WA_OpaquePaintEvent)) {
return rect().contains(testRect);
}
if (QRect(0, st::boxRadius, width(), height() - 2 * st::boxRadius).contains(testRect)) {
return true;
}
if (QRect(st::boxRadius, 0, width() - 2 * st::boxRadius, height()).contains(testRect)) {
return true;
}
return false;
}
void LayerWidget::mousePressEvent(QMouseEvent *e) {
e->accept();
}
void LayerWidget::resizeEvent(QResizeEvent *e) {
if (_resizedCallback) {
_resizedCallback();
}
}
bool LayerWidget::focusNextPrevChild(bool next) {
return base::FocusNextPrevChildBlocked(this, next);
}
void LayerStackWidget::setHideByBackgroundClick(bool hide) {
_hideByBackgroundClick = hide;
}
void LayerStackWidget::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Escape) {
hideCurrent(anim::type::normal);
}
}
void LayerStackWidget::mousePressEvent(QMouseEvent *e) {
Ui::PostponeCall(this, [=] { backgroundClicked(); });
}
void LayerStackWidget::backgroundClicked() {
if (!_hideByBackgroundClick) {
return;
}
if (const auto layer = currentLayer()) {
if (!layer->closeByOutsideClick()) {
return;
}
} else if (const auto special = _specialLayer.data()) {
if (!special->closeByOutsideClick()) {
return;
}
}
hideCurrent(anim::type::normal);
}
void LayerStackWidget::hideCurrent(anim::type animated) {
return currentLayer() ? hideLayers(animated) : hideAll(animated);
}
void LayerStackWidget::hideLayers(anim::type animated) {
startAnimation([] {}, [&] {
clearLayers();
}, Action::HideLayer, animated);
}
void LayerStackWidget::hideAll(anim::type animated) {
startAnimation([] {}, [&] {
clearLayers();
clearSpecialLayer();
_mainMenu.destroy();
}, Action::HideAll, animated);
}
void LayerStackWidget::hideAllAnimatedPrepare() {
prepareAnimation([] {}, [&] {
clearLayers();
clearSpecialLayer();
_mainMenu.destroy();
}, Action::HideAll, anim::type::normal);
}
void LayerStackWidget::hideAllAnimatedRun() {
if (_background->hasBodyCache()) {
removeBodyCache();
hideChildren();
auto bodyCache = Ui::GrabWidget(parentWidget());
showChildren();
_background->refreshBodyCache(std::move(bodyCache));
}
_background->startAnimation(Action::HideAll);
}
void LayerStackWidget::hideTopLayer(anim::type animated) {
if (_specialLayer || _mainMenu) {
hideLayers(animated);
} else {
hideAll(animated);
}
}
void LayerStackWidget::removeBodyCache() {
_background->removeBodyCache();
setAttribute(Qt::WA_OpaquePaintEvent, false);
}
bool LayerStackWidget::layerShown() const {
return _specialLayer || currentLayer() || _mainMenu;
}
const LayerWidget *LayerStackWidget::topShownLayer() const {
if (const auto result = currentLayer()) {
return result;
} else if (const auto special = _specialLayer.data()) {
return special;
} else if (const auto menu = _mainMenu.data()) {
return menu;
}
return nullptr;
}
void LayerStackWidget::setStyleOverrides(
const style::Box *boxSt,
const style::Box *layerSt) {
_boxSt = boxSt;
_layerSt = layerSt;
}
void LayerStackWidget::setCacheImages() {
auto bodyCache = QPixmap(), mainMenuCache = QPixmap();
auto specialLayerCache = QPixmap();
if (_specialLayer) {
Ui::SendPendingMoveResizeEvents(_specialLayer);
auto sides = RectPart::Left | RectPart::Right;
if (_specialLayer->y() > 0) {
sides |= RectPart::Top;
}
if (_specialLayer->y() + _specialLayer->height() < height()) {
sides |= RectPart::Bottom;
}
specialLayerCache = Ui::Shadow::grab(_specialLayer, st::boxRoundShadow, sides);
}
auto layerCache = QPixmap();
if (auto layer = currentLayer()) {
layerCache = Ui::Shadow::grab(layer, st::boxRoundShadow);
}
if (isAncestorOf(window()->focusWidget())) {
setFocus();
}
if (_mainMenu) {
removeBodyCache();
hideChildren();
bodyCache = Ui::GrabWidget(parentWidget());
showChildren();
mainMenuCache = Ui::Shadow::grab(_mainMenu, st::boxRoundShadow, RectPart::Right);
}
setAttribute(Qt::WA_OpaquePaintEvent, !bodyCache.isNull());
updateLayerBoxes();
_background->setCacheImages(std::move(bodyCache), std::move(mainMenuCache), std::move(specialLayerCache), std::move(layerCache));
}
void LayerStackWidget::closeLayer(not_null<LayerWidget*> layer) {
const auto weak = base::make_weak(layer.get());
if (Ui::InFocusChain(layer)) {
setFocus();
}
if (!layer->setClosing()) {
// This layer is already closing.
return;
} else if (!weak) {
// setClosing() could've killed the layer.
return;
}
if (layer == _specialLayer || layer == _mainMenu) {
hideAll(anim::type::normal);
} else if (layer == currentLayer()) {
if (_layers.size() == 1) {
hideCurrent(anim::type::normal);
} else {
const auto taken = std::move(_layers.back());
_layers.pop_back();
layer = currentLayer();
layer->parentResized();
if (!_background->animating()) {
layer->show();
showFinished();
}
}
} else {
for (auto i = _layers.begin(), e = _layers.end(); i != e; ++i) {
if (layer == i->get()) {
const auto taken = std::move(*i);
_layers.erase(i);
break;
}
}
}
}
void LayerStackWidget::updateLayerBoxes() {
const auto layerBox = [&] {
if (const auto layer = currentLayer()) {
return layer->geometry();
}
return QRect();
}();
const auto specialLayerBox = _specialLayer
? _specialLayer->geometry()
: QRect();
_background->setLayerBoxes(specialLayerBox, layerBox);
update();
}
void LayerStackWidget::finishAnimating() {
_background->finishAnimating();
}
bool LayerStackWidget::canSetFocus() const {
return (currentLayer() || _specialLayer || _mainMenu);
}
void LayerStackWidget::setInnerFocus() {
if (_background->animating()) {
setFocus();
} else if (auto l = currentLayer()) {
l->setInnerFocus();
} else if (_specialLayer) {
_specialLayer->setInnerFocus();
} else if (_mainMenu) {
_mainMenu->setInnerFocus();
}
}
bool LayerStackWidget::contentOverlapped(const QRect &globalRect) {
if (isHidden()) {
return false;
}
if (_specialLayer && _specialLayer->overlaps(globalRect)) {
return true;
}
if (auto layer = currentLayer()) {
return layer->overlaps(globalRect);
}
return false;
}
template <typename SetupNew, typename ClearOld>
bool LayerStackWidget::prepareAnimation(
SetupNew &&setupNewWidgets,
ClearOld &&clearOldWidgets,
Action action,
anim::type animated) {
if (animated == anim::type::instant) {
setupNewWidgets();
clearOldWidgets();
prepareForAnimation();
_background->skipAnimation(action);
} else {
setupNewWidgets();
setCacheImages();
const auto weak = base::make_weak(this);
clearOldWidgets();
if (weak) {
prepareForAnimation();
return true;
}
}
return false;
}
template <typename SetupNew, typename ClearOld>
void LayerStackWidget::startAnimation(
SetupNew &&setupNewWidgets,
ClearOld &&clearOldWidgets,
Action action,
anim::type animated) {
const auto alive = prepareAnimation(
std::forward<SetupNew>(setupNewWidgets),
std::forward<ClearOld>(clearOldWidgets),
action,
animated);
if (alive) {
_background->startAnimation(action);
}
}
void LayerStackWidget::resizeEvent(QResizeEvent *e) {
const auto weak = base::make_weak(this);
_background->setGeometry(rect());
if (!weak) {
return;
}
if (_specialLayer) {
_specialLayer->parentResized();
if (!weak) {
return;
}
}
if (const auto layer = currentLayer()) {
layer->parentResized();
if (!weak) {
return;
}
}
if (_mainMenu) {
_mainMenu->parentResized();
if (!weak) {
return;
}
}
updateLayerBoxes();
}
void LayerStackWidget::prepareForAnimation() {
if (isHidden()) {
show();
}
if (_mainMenu) {
if (Ui::InFocusChain(_mainMenu)) {
setFocus();
}
_mainMenu->hide();
}
if (_specialLayer) {
if (Ui::InFocusChain(_specialLayer)) {
setFocus();
}
_specialLayer->hide();
}
if (const auto layer = currentLayer()) {
if (Ui::InFocusChain(layer)) {
setFocus();
}
layer->hide();
}
}
void LayerStackWidget::animationDone() {
auto &integration = base::Integration::Instance();
bool hidden = true;
if (_mainMenu) {
integration.setCrashAnnotation("ShowingWidget", u"MainMenu"_q);
_mainMenu->show();
hidden = false;
}
if (_specialLayer) {
integration.setCrashAnnotation("ShowingWidget", u"SpecialLayer"_q);
_specialLayer->show();
hidden = false;
}
if (auto layer = currentLayer()) {
integration.setCrashAnnotation("ShowingWidget", u"Box"_q);
layer->show();
hidden = false;
}
setAttribute(Qt::WA_OpaquePaintEvent, false);
if (hidden) {
_hideFinishStream.fire({});
} else {
integration.setCrashAnnotation("ShowingWidget", u"Finished"_q);
showFinished();
integration.setCrashAnnotation("ShowingWidget", QString());
}
}
rpl::producer<> LayerStackWidget::hideFinishEvents() const {
return _hideFinishStream.events();
}
void LayerStackWidget::showFinished() {
fixOrder();
sendFakeMouseEvent();
updateLayerBoxes();
if (_specialLayer) {
_specialLayer->showFinished();
}
if (_mainMenu) {
_mainMenu->showFinished();
}
if (auto layer = currentLayer()) {
layer->showFinished();
}
if (canSetFocus()) {
setInnerFocus();
}
}
void LayerStackWidget::showSpecialLayer(
object_ptr<LayerWidget> layer,
anim::type animated) {
startAnimation([&] {
_specialLayer.destroy();
_specialLayer = std::move(layer);
initChildLayer(_specialLayer);
}, [&] {
_mainMenu.destroy();
}, Action::ShowSpecialLayer, animated);
}
bool LayerStackWidget::showSectionInternal(
not_null<::Window::SectionMemento*> memento,
const ::Window::SectionShow &params) {
if (_specialLayer) {
return _specialLayer->showSectionInternal(memento, params);
}
return false;
}
void LayerStackWidget::hideSpecialLayer(anim::type animated) {
startAnimation([] {}, [&] {
clearSpecialLayer();
_mainMenu.destroy();
}, Action::HideSpecialLayer, animated);
}
void LayerStackWidget::showMainMenu(
object_ptr<LayerWidget> layer,
anim::type animated) {
startAnimation([&] {
_mainMenu = std::move(layer);
initChildLayer(_mainMenu);
_mainMenu->moveToLeft(0, 0);
}, [&] {
clearLayers();
_specialLayer.destroy();
}, Action::ShowMainMenu, animated);
}
void LayerStackWidget::showBox(
object_ptr<BoxContent> box,
LayerOptions options,
anim::type animated) {
showLayer(
std::make_unique<BoxLayerWidget>(this, std::move(box)),
options,
animated);
}
void LayerStackWidget::showLayer(
std::unique_ptr<LayerWidget> layer,
LayerOptions options,
anim::type animated) {
if (options & LayerOption::KeepOther) {
if (options & LayerOption::ShowAfterOther) {
prependLayer(std::move(layer), animated);
} else {
appendLayer(std::move(layer), animated);
}
} else {
replaceLayer(std::move(layer), animated);
}
}
LayerWidget *LayerStackWidget::pushLayer(
std::unique_ptr<LayerWidget> layer,
anim::type animated) {
const auto oldLayer = currentLayer();
if (oldLayer) {
if (Ui::InFocusChain(oldLayer)) {
setFocus();
}
oldLayer->hide();
}
_layers.push_back(std::move(layer));
const auto raw = _layers.back().get();
initChildLayer(raw);
if (_layers.size() > 1) {
if (!_background->animating()) {
raw->setVisible(true);
showFinished();
}
} else {
startAnimation([] {}, [&] {
_mainMenu.destroy();
}, Action::ShowLayer, animated);
}
return raw;
}
void LayerStackWidget::appendLayer(
std::unique_ptr<LayerWidget> layer,
anim::type animated) {
pushLayer(std::move(layer), animated);
}
void LayerStackWidget::prependLayer(
std::unique_ptr<LayerWidget> layer,
anim::type animated) {
if (_layers.empty()) {
replaceLayer(std::move(layer), animated);
return;
}
_layers.insert(
begin(_layers),
std::move(layer));
const auto raw = _layers.front().get();
raw->hide();
initChildLayer(raw);
}
void LayerStackWidget::replaceLayer(
std::unique_ptr<LayerWidget> layer,
anim::type animated) {
const auto pointer = pushLayer(std::move(layer), animated);
const auto removeTill = ranges::find(
_layers,
pointer,
&std::unique_ptr<LayerWidget>::get);
_closingLayers.insert(
end(_closingLayers),
std::make_move_iterator(begin(_layers)),
std::make_move_iterator(removeTill));
_layers.erase(begin(_layers), removeTill);
clearClosingLayers();
}
bool LayerStackWidget::takeToThirdSection() {
return _specialLayer
? _specialLayer->takeToThirdSection()
: false;
}
void LayerStackWidget::clearLayers() {
_closingLayers.insert(
end(_closingLayers),
std::make_move_iterator(begin(_layers)),
std::make_move_iterator(end(_layers)));
_layers.clear();
clearClosingLayers();
}
void LayerStackWidget::clearClosingLayers() {
const auto weak = base::make_weak(this);
while (!_closingLayers.empty()) {
const auto index = _closingLayers.size() - 1;
const auto layer = _closingLayers.back().get();
if (Ui::InFocusChain(layer)) {
setFocus();
}
// This may destroy LayerStackWidget (by calling Ui::hideLayer).
// So each time we check a weak pointer (if we are still alive).
layer->setClosing();
// setClosing() could destroy 'this' or could call clearLayers().
if (weak && !_closingLayers.empty()) {
// We could enqueue more closing layers, so we remove by index.
Assert(index < _closingLayers.size());
Assert(_closingLayers[index].get() == layer);
_closingLayers.erase(begin(_closingLayers) + index);
} else {
// Everything was destroyed in clearLayers or ~LayerStackWidget.
break;
}
}
}
void LayerStackWidget::clearSpecialLayer() {
if (_specialLayer) {
_specialLayer->setClosing();
_specialLayer.destroy();
}
}
void LayerStackWidget::initChildLayer(LayerWidget *layer) {
layer->setParent(this);
layer->setClosedCallback([=] { closeLayer(layer); });
layer->setResizedCallback([=] { updateLayerBoxes(); });
Ui::SendPendingMoveResizeEvents(layer);
layer->parentResized();
}
void LayerStackWidget::fixOrder() {
if (const auto layer = currentLayer()) {
_background->raise();
layer->raise();
} else if (_specialLayer) {
_specialLayer->raise();
}
if (_mainMenu) {
_mainMenu->raise();
}
}
void LayerStackWidget::sendFakeMouseEvent() {
SendSynteticMouseEvent(this, QEvent::MouseMove, Qt::NoButton);
}
LayerStackWidget::~LayerStackWidget() {
// Some layer destructors call back into LayerStackWidget.
while (!_layers.empty() || !_closingLayers.empty()) {
hideAll(anim::type::instant);
clearClosingLayers();
}
}
} // namespace Ui

View File

@@ -0,0 +1,244 @@
// 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 "base/object_ptr.h"
#include "base/flags.h"
namespace Window {
class SectionMemento;
struct SectionShow;
} // namespace Window
namespace style {
struct Box;
} // namespace style
namespace Ui {
class BoxContent;
enum class LayerOption {
CloseOther = (1 << 0),
KeepOther = (1 << 1),
ShowAfterOther = (1 << 2),
};
using LayerOptions = base::flags<LayerOption>;
inline constexpr auto is_flag_type(LayerOption) { return true; };
class Show;
using ShowPtr = std::shared_ptr<Show>;
using ShowFactory = Fn<ShowPtr()>;
class LayerWidget : public RpWidget {
public:
using RpWidget::RpWidget;
virtual void parentResized() = 0;
virtual void showFinished() {
}
void setInnerFocus();
bool setClosing() {
if (!_closing) {
_closing = true;
closeHook();
return true;
}
return false;
}
bool overlaps(const QRect &globalRect);
void setClosedCallback(Fn<void()> callback) {
_closedCallback = std::move(callback);
}
void setResizedCallback(Fn<void()> callback) {
_resizedCallback = std::move(callback);
}
virtual bool takeToThirdSection() {
return false;
}
virtual bool showSectionInternal(
not_null<::Window::SectionMemento*> memento,
const ::Window::SectionShow &params) {
return false;
}
virtual bool closeByOutsideClick() const {
return true;
}
void closeLayer() {
if (const auto callback = base::take(_closedCallback)) {
callback();
}
}
protected:
void mousePressEvent(QMouseEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
bool focusNextPrevChild(bool next) override;
virtual void doSetInnerFocus() {
setFocus();
}
virtual void closeHook() {
}
private:
bool _closing = false;
Fn<void()> _closedCallback;
Fn<void()> _resizedCallback;
};
class LayerStackWidget : public RpWidget {
public:
LayerStackWidget(QWidget *parent, ShowFactory showFactory);
void finishAnimating();
rpl::producer<> hideFinishEvents() const;
void setStyleOverrides(
const style::Box *boxSt,
const style::Box *layerSt);
[[nodiscard]] const style::Box *boxStyleOverrideLayer() const {
return _layerSt;
}
[[nodiscard]] const style::Box *boxStyleOverride() const {
return _boxSt;
}
[[nodiscard]] ShowFactory showFactory() const {
return _showFactory;
}
void showBox(
object_ptr<BoxContent> box,
LayerOptions options,
anim::type animated);
void showLayer(
std::unique_ptr<LayerWidget> layer,
LayerOptions options,
anim::type animated);
void showSpecialLayer(
object_ptr<LayerWidget> layer,
anim::type animated);
void showMainMenu(
object_ptr<LayerWidget> menu,
anim::type animated);
bool takeToThirdSection();
bool canSetFocus() const;
void setInnerFocus();
bool contentOverlapped(const QRect &globalRect);
void hideSpecialLayer(anim::type animated);
void hideLayers(anim::type animated);
void hideAll(anim::type animated);
void hideTopLayer(anim::type animated);
void setHideByBackgroundClick(bool hide);
void removeBodyCache();
// If you need to divide animated hideAll().
void hideAllAnimatedPrepare();
void hideAllAnimatedRun();
bool showSectionInternal(
not_null<::Window::SectionMemento*> memento,
const ::Window::SectionShow &params);
bool layerShown() const;
const LayerWidget *topShownLayer() const;
~LayerStackWidget();
protected:
void keyPressEvent(QKeyEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
private:
void appendLayer(
std::unique_ptr<LayerWidget> layer,
anim::type animated);
void prependLayer(
std::unique_ptr<LayerWidget> layer,
anim::type animated);
void replaceLayer(
std::unique_ptr<LayerWidget> layer,
anim::type animated);
void backgroundClicked();
LayerWidget *pushLayer(
std::unique_ptr<LayerWidget> layer,
anim::type animated);
void showFinished();
void hideCurrent(anim::type animated);
void closeLayer(not_null<LayerWidget*> layer);
enum class Action {
ShowMainMenu,
ShowSpecialLayer,
ShowLayer,
HideSpecialLayer,
HideLayer,
HideAll,
};
template <typename SetupNew, typename ClearOld>
bool prepareAnimation(
SetupNew &&setupNewWidgets,
ClearOld &&clearOldWidgets,
Action action,
anim::type animated);
template <typename SetupNew, typename ClearOld>
void startAnimation(
SetupNew &&setupNewWidgets,
ClearOld &&clearOldWidgets,
Action action,
anim::type animated);
void prepareForAnimation();
void animationDone();
void setCacheImages();
void clearLayers();
void clearSpecialLayer();
void initChildLayer(LayerWidget *layer);
void updateLayerBoxes();
void fixOrder();
void sendFakeMouseEvent();
void clearClosingLayers();
LayerWidget *currentLayer() {
return _layers.empty() ? nullptr : _layers.back().get();
}
const LayerWidget *currentLayer() const {
return const_cast<LayerStackWidget*>(this)->currentLayer();
}
std::vector<std::unique_ptr<LayerWidget>> _layers;
std::vector<std::unique_ptr<LayerWidget>> _closingLayers;
object_ptr<LayerWidget> _specialLayer = { nullptr };
object_ptr<LayerWidget> _mainMenu = { nullptr };
class BackgroundWidget;
object_ptr<BackgroundWidget> _background;
ShowFactory _showFactory;
const style::Box *_boxSt = nullptr;
const style::Box *_layerSt = nullptr;
bool _hideByBackgroundClick = true;
rpl::event_stream<> _hideFinishStream;
};
} // namespace Ui

View File

@@ -0,0 +1,235 @@
// 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
//
using "ui/basic.style";
using "ui/widgets/widgets.style";
ServiceCheck {
margin: margins;
diameter: pixels;
shift: pixels;
thickness: pixels;
tip: point;
small: pixels;
large: pixels;
stroke: pixels;
color: color;
duration: int;
}
Box {
buttonPadding: margins;
buttonHeight: pixels;
buttonWide: bool;
button: RoundButton;
margin: margins;
title: FlatLabel;
bg: color;
titleAdditionalFg: color;
shadowIgnoreTopSkip: bool;
shadowIgnoreBottomSkip: bool;
}
boxDuration: 200;
boxRadius: 8px;
boxButtonFont: font(boxFontSize semibold);
defaultBoxButtonTextStyle: TextStyle(semiboldTextStyle) {
font: font(14px semibold);
}
defaultBoxButton: RoundButton(defaultLightButton) {
width: -30px;
height: 34px;
textTop: 7px;
style: defaultBoxButtonTextStyle;
}
boxLabelStyle: TextStyle(boxTextStyle) {
lineHeight: 22px;
}
attentionBoxButton: RoundButton(defaultBoxButton) {
textFg: attentionButtonFg;
textFgOver: attentionButtonFgOver;
textBgOver: attentionButtonBgOver;
ripple: RippleAnimation(defaultRippleAnimation) {
color: attentionButtonBgRipple;
}
}
defaultBoxCheckbox: Checkbox(defaultCheckbox) {
width: -46px;
textPosition: point(12px, 1px);
style: boxTextStyle;
}
boxRoundShadow: roundShadowRadius8px;
boxTitleFont: font(16px semibold);
boxTitle: FlatLabel(defaultFlatLabel) {
textFg: boxTitleFg;
maxHeight: 24px;
style: TextStyle(defaultTextStyle) {
font: boxTitleFont;
}
}
boxTitlePosition: point(24px, 13px);
boxTitleHeight: 48px;
boxTitleAdditionalSkip: 9px;
boxTitleAdditionalFont: normalFont;
boxScroll: defaultSolidScroll;
boxRowPadding: margins(24px, 0px, 24px, 0px);
boxTopMargin: 8px;
boxTitleCloseIcon: icon {{ "box_button_close", boxTitleCloseFg }};
boxTitleCloseIconOver: icon {{ "box_button_close", boxTitleCloseFgOver }};
boxTitleClose: IconButton(defaultIconButton) {
width: boxTitleHeight;
height: boxTitleHeight;
icon: boxTitleCloseIcon;
iconOver: boxTitleCloseIconOver;
rippleAreaPosition: point(4px, 4px);
rippleAreaSize: 40px;
ripple: defaultRippleAnimationBgOver;
}
boxTitleMenu: IconButton(boxTitleClose) {
icon: icon {{ "title_menu_dots", boxTitleCloseFg }};
iconOver: icon {{ "title_menu_dots", boxTitleCloseFgOver }};
}
boxLinkButton: LinkButton(defaultLinkButton) {
font: boxTextFont;
overFont: font(boxFontSize underline);
}
boxOptionListPadding: margins(0px, 0px, 0px, 0px);
boxOptionListSkip: 20px;
boxWidth: 320px;
boxWideWidth: 364px;
boxPadding: margins(24px, 14px, 24px, 8px);
boxMaxListHeight: 492px;
boxLittleSkip: 10px;
boxMediumSkip: 20px;
defaultBox: Box {
buttonPadding: margins(6px, 10px, 10px, 10px);
buttonHeight: 34px;
button: defaultBoxButton;
margin: margins(0px, 10px, 0px, 10px);
bg: boxBg;
title: boxTitle;
titleAdditionalFg: boxTitleAdditionalFg;
}
layerBox: Box(defaultBox) {
}
boxLabel: FlatLabel(defaultFlatLabel) {
// Keep minWidth <= boxWidth - boxPadding.left - boxPadding.right.
minWidth: 256px;
align: align(topleft);
style: boxLabelStyle;
}
boxLoadingAnimation: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
color: windowSubTextFg;
thickness: 2px;
}
boxLoadingSize: 20px;
defaultSubsectionTitle: FlatLabel(defaultFlatLabel) {
style: TextStyle(semiboldTextStyle) {
font: font(boxFontSize semibold);
}
textFg: windowActiveTextFg;
minWidth: 240px;
}
defaultSubsectionTitlePadding: margins(22px, 7px, 10px, 9px);
separatePanelBorderCacheSize: 60px;
separatePanelTitleHeight: 62px;
separatePanelNoTitleHeight: 32px;
separatePanelTitleBadgeSkip: 6px;
separatePanelClose: IconButton(boxTitleClose) {
width: 60px;
height: 60px;
rippleAreaPosition: point(8px, 8px);
rippleAreaSize: 44px;
ripple: defaultRippleAnimationBgOver;
}
separatePanelMenu: IconButton(separatePanelClose) {
width: 44px;
icon: icon {{ "title_menu_dots", boxTitleCloseFg }};
iconOver: icon {{ "title_menu_dots", boxTitleCloseFgOver }};
rippleAreaPosition: point(0px, 8px);
}
separatePanelMenuPosition: point(0px, 52px);
separatePanelTitleFont: font(18px semibold);
separatePanelTitle: FlatLabel(defaultFlatLabel) {
textFg: boxTitleFg;
maxHeight: 26px;
style: TextStyle(defaultTextStyle) {
font: separatePanelTitleFont;
}
}
separatePanelTitleTop: 18px;
separatePanelTitleLeft: 22px;
separatePanelTitleSkip: 0px;
separatePanelTitleBadgeTop: 4px;
separatePanelSearch: IconButton(separatePanelClose) {
width: 44px;
icon: icon {{ "box_search", boxTitleCloseFg }};
iconOver: icon {{ "box_search", boxTitleCloseFgOver }};
rippleAreaPosition: point(0px, 8px);
}
separatePanelBack: IconButton(separatePanelClose) {
icon: icon {{ "box_button_back", boxTitleCloseFg }};
iconOver: icon {{ "box_button_back", boxTitleCloseFgOver }};
}
separatePanelDuration: 150;
fullScreenPanelClose: IconButton {
width: 44px;
height: 44px;
icon: icon {{ "box_button_close", radialFg }};
iconOver: icon {{ "box_button_close", radialFg }};
iconPosition: point(-1px, -1px);
rippleAreaPosition: point(0px, 0px);
rippleAreaSize: 44px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: shadowFg;
}
}
fullScreenPanelBack: IconButton(fullScreenPanelClose) {
icon: icon {{ "box_button_back", radialFg }};
iconOver: icon {{ "box_button_back", radialFg }};
}
fullScreenPanelMenu: IconButton(fullScreenPanelClose) {
icon: icon {{ "title_menu_dots", radialFg }};
iconOver: icon {{ "title_menu_dots", radialFg }};
}
webviewDialogButton: defaultBoxButton;
webviewDialogDestructiveButton: attentionBoxButton;
webviewDialogSubmit: RoundButton(defaultActiveButton) {
width: -48px;
height: 34px;
textTop: 7px;
style: defaultBoxButtonTextStyle;
}
webviewDialogPadding: margins(8px, 12px, 15px, 12px);

View File

@@ -0,0 +1,61 @@
// 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/layers/show.h"
#include "ui/toast/toast.h"
namespace Ui {
namespace {
using namespace Toast;
} // namespace
void Show::showBox(
object_ptr<BoxContent> content,
LayerOptions options,
anim::type animated) const {
return showOrHideBoxOrLayer(std::move(content), options, animated);
}
void Show::showLayer(
std::unique_ptr<LayerWidget> layer,
LayerOptions options,
anim::type animated) const {
return showOrHideBoxOrLayer(std::move(layer), options, animated);
}
void Show::hideLayer(anim::type animated) const {
return showOrHideBoxOrLayer(v::null, LayerOptions(), animated);
}
base::weak_ptr<Instance> Show::showToast(Config &&config) const {
if (const auto strong = _lastToast.get()) {
strong->hideAnimated();
}
_lastToast = valid()
? Toast::Show(toastParent(), std::move(config))
: base::weak_ptr<Instance>();
return _lastToast;
}
base::weak_ptr<Instance> Show::showToast(
TextWithEntities &&text,
crl::time duration) const {
return showToast({ .text = std::move(text), .duration = duration });
}
base::weak_ptr<Instance> Show::showToast(
const QString &text,
crl::time duration) const {
return showToast({
.text = TextWithEntities{ text },
.duration = duration,
});
}
} // namespace Ui

View File

@@ -0,0 +1,81 @@
// 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/weak_ptr.h"
#include "ui/layers/layer_widget.h"
struct TextWithEntities;
namespace anim {
enum class type : uchar;
} // namespace anim
namespace Ui::Toast {
struct Config;
class Instance;
} // namespace Ui::Toast
namespace Ui {
class BoxContent;
class LayerWidget;
inline constexpr auto kZOrderBasic = 0;
class Show {
public:
virtual ~Show() = 0;
virtual void showOrHideBoxOrLayer(
std::variant<
v::null_t,
object_ptr<BoxContent>,
std::unique_ptr<LayerWidget>> &&layer,
LayerOptions options,
anim::type animated) const = 0;
[[nodiscard]] virtual not_null<QWidget*> toastParent() const = 0;
[[nodiscard]] virtual bool valid() const = 0;
virtual operator bool() const = 0;
void showBox(
object_ptr<BoxContent> content,
LayerOptions options = LayerOption::KeepOther,
anim::type animated = anim::type()) const;
void showLayer(
std::unique_ptr<LayerWidget> layer,
LayerOptions options = LayerOption::KeepOther,
anim::type animated = anim::type()) const;
void hideLayer(anim::type animated = anim::type()) const;
base::weak_ptr<Toast::Instance> showToast(Toast::Config &&config) const;
base::weak_ptr<Toast::Instance> showToast(
TextWithEntities &&text,
crl::time duration = 0) const;
base::weak_ptr<Toast::Instance> showToast(
const QString &text,
crl::time duration = 0) const;
template <
typename BoxType,
typename = std::enable_if_t<std::is_base_of_v<BoxContent, BoxType>>>
base::weak_qptr<BoxType> show(
object_ptr<BoxType> content,
LayerOptions options = LayerOption::KeepOther,
anim::type animated = anim::type()) const {
auto result = base::weak_qptr<BoxType>(content.data());
showBox(std::move(content), options, animated);
return result;
}
private:
mutable base::weak_ptr<Toast::Instance> _lastToast;
};
inline Show::~Show() = default;
} // namespace Ui

View File

@@ -0,0 +1,129 @@
// 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/main_queue_processor.h"
#include "base/integration.h"
#include "ui/platform/ui_platform_utility.h"
#include <QtCore/QMutex>
#include <QtCore/QCoreApplication>
#include <QtGui/QtEvents>
#include <crl/crl_on_main.h>
namespace Ui {
namespace {
auto ProcessorEventType() {
static const auto Result = QEvent::Type(QEvent::registerEventType());
return Result;
}
QMutex ProcessorMutex;
MainQueueProcessor *ProcessorInstance/* = nullptr*/;
enum class ProcessState : int {
Processed,
FillingUp,
Waiting,
};
std::atomic<ProcessState> MainQueueProcessState/* = ProcessState(0)*/;
void (*MainQueueProcessCallback)(void*)/* = nullptr*/;
void *MainQueueProcessArgument/* = nullptr*/;
void PushToMainQueueGeneric(void (*callable)(void*), void *argument) {
Expects(Platform::UseMainQueueGeneric());
auto expected = ProcessState::Processed;
const auto fill = MainQueueProcessState.compare_exchange_strong(
expected,
ProcessState::FillingUp);
if (fill) {
MainQueueProcessCallback = callable;
MainQueueProcessArgument = argument;
MainQueueProcessState.store(ProcessState::Waiting);
}
auto event = std::make_unique<QEvent>(ProcessorEventType());
QMutexLocker lock(&ProcessorMutex);
if (ProcessorInstance) {
QCoreApplication::postEvent(ProcessorInstance, event.release());
}
}
void DrainMainQueueGeneric() {
Expects(Platform::UseMainQueueGeneric());
if (MainQueueProcessState.load() != ProcessState::Waiting) {
return;
}
const auto callback = MainQueueProcessCallback;
const auto argument = MainQueueProcessArgument;
MainQueueProcessState.store(ProcessState::Processed);
callback(argument);
}
} // namespace
MainQueueProcessor::MainQueueProcessor() {
if constexpr (Platform::UseMainQueueGeneric()) {
acquire();
crl::init_main_queue(PushToMainQueueGeneric);
} else {
crl::wrap_main_queue([](void (*callable)(void*), void *argument) {
base::Integration::Instance().enterFromEventLoop([&] {
callable(argument);
});
});
}
crl::on_main_update_requests(
) | rpl::on_next([] {
if constexpr (Platform::UseMainQueueGeneric()) {
DrainMainQueueGeneric();
} else {
Platform::DrainMainQueue();
}
}, _lifetime);
}
bool MainQueueProcessor::event(QEvent *event) {
if constexpr (Platform::UseMainQueueGeneric()) {
if (event->type() == ProcessorEventType()) {
DrainMainQueueGeneric();
return true;
}
}
return QObject::event(event);
}
void MainQueueProcessor::acquire() {
Expects(Platform::UseMainQueueGeneric());
Expects(ProcessorInstance == nullptr);
QMutexLocker lock(&ProcessorMutex);
ProcessorInstance = this;
}
void MainQueueProcessor::release() {
Expects(Platform::UseMainQueueGeneric());
Expects(ProcessorInstance == this);
QMutexLocker lock(&ProcessorMutex);
ProcessorInstance = nullptr;
}
MainQueueProcessor::~MainQueueProcessor() {
if constexpr (Platform::UseMainQueueGeneric()) {
release();
}
}
} // namespace Ui

View File

@@ -0,0 +1,27 @@
// 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 MainQueueProcessor : public QObject {
public:
MainQueueProcessor();
~MainQueueProcessor();
protected:
bool event(QEvent *event) override;
private:
void acquire();
void release();
rpl::lifetime _lifetime;
};
} // namespace Ui

View File

@@ -0,0 +1,220 @@
// 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/paint/arcs.h"
#include "ui/effects/animation_value.h"
#include "ui/effects/animation_value_f.h"
#include "ui/painter.h"
namespace Ui::Paint {
ArcsAnimation::ArcsAnimation(
const style::ArcsAnimation &st,
std::vector<float> thresholds,
float64 startValue,
Direction direction)
: _st(st)
, _direction(direction)
, _startAngle(16
* (st.deltaAngle
+ ((direction == Direction::Up)
? 90
: (direction == Direction::Down)
? 270
: (direction == Direction::Left)
? 180
: 0)))
, _spanAngle(-st.deltaAngle * 2 * 16)
, _emptyRect(computeArcRect(0))
, _currentValue(startValue) {
initArcs(std::move(thresholds));
}
void ArcsAnimation::initArcs(std::vector<float> thresholds) {
const auto count = thresholds.size();
_arcs.reserve(count);
for (auto i = 0; i < count; i++) {
const auto threshold = thresholds[i];
const auto progress = (threshold > _currentValue) ? 1. : 0.;
auto arc = Arc{
.rect = computeArcRect(i + 1),
.threshold = threshold,
.progress = progress,
};
_arcs.push_back(std::move(arc));
}
}
bool ArcsAnimation::isHorizontal() const {
return _direction == Direction::Left || _direction == Direction::Right;
}
QRectF ArcsAnimation::computeArcRect(int index) const {
const auto w = _st.startWidth + _st.deltaWidth * index;
const auto h = _st.startHeight + _st.deltaHeight * index;
if (isHorizontal()) {
auto rect = QRectF(0, -h / 2.0, w, h);
if (_direction == Direction::Right) {
rect.moveRight(index * _st.space);
} else {
rect.moveLeft(-index * _st.space);
}
return rect;
} else {
auto rect = QRectF(-w / 2.0, 0, w, h);
if (_direction == Direction::Up) {
rect.moveTop(-index * _st.space);
} else {
rect.moveBottom(index * _st.space);
}
return rect;
}
return QRectF();
}
void ArcsAnimation::update(crl::time now) {
for (auto &arc : _arcs) {
if (!isArcFinished(arc)) {
const auto progress = std::clamp(
(now - arc.startTime) / float64(_st.duration),
0.,
1.);
arc.progress = (arc.threshold > _currentValue)
? progress
: (1. - progress);
}
}
if (isFinished()) {
_stopUpdateRequests.fire({});
}
}
void ArcsAnimation::setValue(float64 value) {
if (_currentValue == value) {
return;
}
const auto previousValue = _currentValue;
_currentValue = value;
if (!isFinished()) {
const auto now = crl::now();
_startUpdateRequests.fire({});
for (auto &arc : _arcs) {
updateArcStartTime(arc, previousValue, now);
}
}
}
void ArcsAnimation::updateArcStartTime(
Arc &arc,
float64 previousValue,
crl::time now) {
if ((arc.progress == 0.) || (arc.progress == 1.)) {
arc.startTime = isArcFinished(arc) ? 0 : now;
return;
}
const auto isPreviousToHide = (arc.threshold <= previousValue); // 0 -> 1
const auto isCurrentToHide = (arc.threshold <= _currentValue);
if (isPreviousToHide != isCurrentToHide) {
const auto passedTime = _st.duration * arc.progress;
const auto newDelta = isCurrentToHide
? (_st.duration - passedTime)
: passedTime;
arc.startTime = now - newDelta;
}
}
float ArcsAnimation::width() const {
if (_arcs.empty()) {
return 0;
}
for (const auto &arc : ranges::views::reverse(_arcs)) {
if ((arc.progress != 1.)) {
return arc.rect.x() + arc.rect.width();
}
}
return 0;
}
float ArcsAnimation::finishedWidth() const {
if (_arcs.empty()) {
return 0;
}
for (const auto &arc : ranges::views::reverse(_arcs)) {
if (arc.threshold <= _currentValue) {
return arc.rect.x() + arc.rect.width();
}
}
return 0;
}
float ArcsAnimation::maxWidth() const {
if (_arcs.empty()) {
return 0;
}
const auto &r = _arcs.back().rect;
return r.x() + r.width();
}
float ArcsAnimation::height() const {
return _arcs.empty()
? 0
: _arcs.back().rect.height();
}
rpl::producer<> ArcsAnimation::startUpdateRequests() {
return _startUpdateRequests.events();
}
rpl::producer<> ArcsAnimation::stopUpdateRequests() {
return _stopUpdateRequests.events();
}
bool ArcsAnimation::isFinished() const {
return ranges::all_of(
_arcs,
[=](const Arc &arc) { return isArcFinished(arc); });
}
bool ArcsAnimation::isArcFinished(const Arc &arc) const {
return ((arc.threshold > _currentValue) && (arc.progress == 1.))
|| ((arc.threshold <= _currentValue) && (arc.progress == 0.));
}
void ArcsAnimation::paint(QPainter &p, std::optional<QColor> colorOverride) {
PainterHighQualityEnabler hq(p);
QPen pen;
if (_strokeRatio) {
pen.setWidthF(_st.stroke * _strokeRatio);
} else {
pen.setWidth(_st.stroke);
}
pen.setCapStyle(Qt::RoundCap);
pen.setColor(colorOverride ? (*colorOverride) : _st.fg->c);
p.setPen(pen);
for (auto i = 0; i < _arcs.size(); i++) {
const auto &arc = _arcs[i];
const auto previousRect = (!i) ? _emptyRect : _arcs[i - 1].rect;
const auto progress = arc.progress;
const auto opactity = (1. - progress);
p.setOpacity(opactity * opactity);
const auto rect = (progress == 0.)
? arc.rect
: (progress == 1.)
? previousRect
: anim::interpolatedRectF(arc.rect, previousRect, progress);
p.drawArc(rect, _startAngle, _spanAngle);
}
p.setOpacity(1.);
}
void ArcsAnimation::setStrokeRatio(float ratio) {
_strokeRatio = ratio;
}
} // namespace Ui::Paint

View File

@@ -0,0 +1,85 @@
// 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::Paint {
class ArcsAnimation {
public:
enum class Direction {
Up,
Down,
Left,
Right,
};
ArcsAnimation(
const style::ArcsAnimation &st,
std::vector<float> thresholds,
float64 startValue,
Direction direction);
void paint(
QPainter &p,
std::optional<QColor> colorOverride = std::nullopt);
void setValue(float64 value);
rpl::producer<> startUpdateRequests();
rpl::producer<> stopUpdateRequests();
void update(crl::time now);
bool isFinished() const;
float width() const;
float maxWidth() const;
float finishedWidth() const;
float height() const;
void setStrokeRatio(float ratio);
private:
struct Arc {
QRectF rect;
float threshold;
crl::time startTime = 0;
float64 progress = 0.;
};
void initArcs(std::vector<float> thresholds);
QRectF computeArcRect(int index) const;
bool isHorizontal() const;
bool isArcFinished(const Arc &arc) const;
void updateArcStartTime(
Arc &arc,
float64 previousValue,
crl::time now);
const style::ArcsAnimation &_st;
const Direction _direction;
const int _startAngle;
const int _spanAngle;
const QRectF _emptyRect;
float64 _currentValue = 0.;
float _strokeRatio = 0.;
rpl::event_stream<> _startUpdateRequests;
rpl::event_stream<> _stopUpdateRequests;
std::vector<Arc> _arcs;
};
} // namespace Ui::Paint

View File

@@ -0,0 +1,244 @@
// 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/paint/blob.h"
#include "base/random.h"
#include "ui/painter.h"
#include <QtGui/QPainterPath>
#include <QtCore/QtMath>
namespace Ui::Paint {
namespace {
constexpr auto kMaxSpeed = 8.2;
constexpr auto kMinSpeed = 0.8;
constexpr auto kMinSegmentSpeed = 0.017;
constexpr auto kSegmentSpeedDiff = 0.003;
[[nodiscard]] float64 RandomAdditional() {
return (base::RandomValue<int>() % 100 / 100.);
}
} // namespace
Blob::Blob(int n, float minSpeed, float maxSpeed)
: _segmentsCount(n)
, _minSpeed(minSpeed ? minSpeed : kMinSpeed)
, _maxSpeed(maxSpeed ? maxSpeed : kMaxSpeed)
, _pen(Qt::NoBrush, 0, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin) {
}
void Blob::generateBlob() {
for (auto i = 0; i < _segmentsCount; i++) {
generateSingleValues(i);
// Fill nexts.
generateTwoValues(i);
// Fill currents.
generateTwoValues(i);
}
}
void Blob::generateSingleValues(int i) {
auto &segment = segmentAt(i);
segment.progress = 0.;
segment.speed = kMinSegmentSpeed
+ kSegmentSpeedDiff * std::abs(RandomAdditional());
}
void Blob::update(float level, float speedScale, float64 rate) {
for (auto i = 0; i < _segmentsCount; i++) {
auto &segment = segmentAt(i);
segment.progress += (_minSpeed + level * _maxSpeed * speedScale)
* segment.speed
* rate;
if (segment.progress >= 1) {
generateSingleValues(i);
generateTwoValues(i);
}
}
}
void Blob::setRadiuses(Radiuses values) {
_radiuses = values;
}
Blob::Radiuses Blob::radiuses() const {
return _radiuses;
}
RadialBlob::RadialBlob(int n, float minScale, float minSpeed, float maxSpeed)
: Blob(n, minSpeed, maxSpeed)
, _segmentLength((4.0 / 3.0) * std::tan(M_PI / (2 * n)))
, _minScale(minScale)
, _segmentAngle(360. / n)
, _angleDiff(_segmentAngle * 0.05)
, _segments(n) {
}
void RadialBlob::paint(QPainter &p, const QBrush &brush, float outerScale) {
auto path = QPainterPath();
auto m = QTransform();
const auto scale = (_minScale + (1. - _minScale) * _scale) * outerScale;
if (scale == 0.) {
return;
}
p.save();
if (scale != 1.) {
p.scale(scale, scale);
}
for (auto i = 0; i < _segmentsCount; i++) {
const auto &segment = _segments[i];
const auto nextIndex = i + 1 < _segmentsCount ? (i + 1) : 0;
const auto nextSegment = _segments[nextIndex];
const auto progress = segment.progress;
const auto progressNext = nextSegment.progress;
const auto r1 = segment.radius.current * (1. - progress)
+ segment.radius.next * progress;
const auto r2 = nextSegment.radius.current * (1. - progressNext)
+ nextSegment.radius.next * progressNext;
const auto angle1 = segment.angle.current * (1. - progress)
+ segment.angle.next * progress;
const auto angle2 = nextSegment.angle.current * (1. - progressNext)
+ nextSegment.angle.next * progressNext;
const auto l = _segmentLength * (std::min(r1, r2)
+ (std::max(r1, r2) - std::min(r1, r2)) / 2.);
m.reset();
m.rotate(angle1);
const auto pointStart1 = m.map(QPointF(0, -r1));
const auto pointStart2 = m.map(QPointF(l, -r1));
m.reset();
m.rotate(angle2);
const auto pointEnd1 = m.map(QPointF(0, -r2));
const auto pointEnd2 = m.map(QPointF(-l, -r2));
if (i == 0) {
path.moveTo(pointStart1);
}
path.cubicTo(pointStart2, pointEnd2, pointEnd1);
}
p.setBrush(Qt::NoBrush);
p.setPen(_pen);
p.fillPath(path, brush);
p.drawPath(path);
p.restore();
}
void RadialBlob::generateTwoValues(int i) {
auto &radius = _segments[i].radius;
auto &angle = _segments[i].angle;
const auto radDiff = _radiuses.max - _radiuses.min;
angle.setNext(_segmentAngle * i + RandomAdditional() * _angleDiff);
radius.setNext(_radiuses.min + std::abs(RandomAdditional()) * radDiff);
}
void RadialBlob::update(float level, float speedScale, float64 rate) {
_scale = level;
Blob::update(level, speedScale, rate);
}
Blob::Segment &RadialBlob::segmentAt(int i) {
return _segments[i];
};
LinearBlob::LinearBlob(
int n,
Direction direction,
float minSpeed,
float maxSpeed)
: Blob(n + 1)
, _topDown(direction == Direction::TopDown ? 1 : -1)
, _segments(_segmentsCount) {
}
void LinearBlob::paint(QPainter &p, const QBrush &brush, int width) {
if (!width) {
return;
}
auto path = QPainterPath();
const auto left = 0;
const auto right = width;
path.moveTo(right, 0);
path.lineTo(left, 0);
const auto n = float(_segmentsCount - 1);
p.save();
for (auto i = 0; i < _segmentsCount; i++) {
const auto &segment = _segments[i];
if (!i) {
const auto &progress = segment.progress;
const auto r1 = segment.radius.current * (1. - progress)
+ segment.radius.next * progress;
const auto y = r1 * _topDown;
path.lineTo(left, y);
} else {
const auto &prevSegment = _segments[i - 1];
const auto &progress = prevSegment.progress;
const auto r1 = prevSegment.radius.current * (1. - progress)
+ prevSegment.radius.next * progress;
const auto &progressNext = segment.progress;
const auto r2 = segment.radius.current * (1. - progressNext)
+ segment.radius.next * progressNext;
const auto x1 = (right - left) / n * (i - 1);
const auto x2 = (right - left) / n * i;
const auto cx = x1 + (x2 - x1) / 2;
const auto y1 = r1 * _topDown;
const auto y2 = r2 * _topDown;
path.cubicTo(
QPointF(cx, y1),
QPointF(cx, y2),
QPointF(x2, y2)
);
}
}
path.lineTo(right, 0);
p.setBrush(Qt::NoBrush);
p.setPen(_pen);
p.fillPath(path, brush);
p.drawPath(path);
p.restore();
}
void LinearBlob::generateTwoValues(int i) {
auto &radius = _segments[i].radius;
const auto radDiff = _radiuses.max - _radiuses.min;
radius.setNext(_radiuses.min + std::abs(RandomAdditional()) * radDiff);
}
Blob::Segment &LinearBlob::segmentAt(int i) {
return _segments[i];
};
} // namespace Ui::Paint

View File

@@ -0,0 +1,113 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
class Painter;
namespace Ui::Paint {
class Blob {
public:
struct Radiuses {
float min = 0.;
float max = 0.;
};
Blob(int n, float minSpeed = 0, float maxSpeed = 0);
virtual ~Blob() = default;
void update(float level, float speedScale, float64 rate);
void generateBlob();
void setRadiuses(Radiuses values);
[[nodiscard]] Radiuses radiuses() const;
protected:
struct TwoValues {
float current = 0.;
float next = 0.;
void setNext(float v) {
current = next;
next = v;
}
};
struct Segment {
float progress = 0.;
float speed = 0.;
};
void generateSingleValues(int i);
virtual void generateTwoValues(int i) = 0;
virtual Segment &segmentAt(int i) = 0;
const int _segmentsCount;
const float _minSpeed;
const float _maxSpeed;
const QPen _pen;
Radiuses _radiuses;
};
class RadialBlob final : public Blob {
public:
RadialBlob(int n, float minScale, float minSpeed = 0, float maxSpeed = 0);
void paint(QPainter &p, const QBrush &brush, float outerScale = 1.);
void update(float level, float speedScale, float64 rate);
private:
struct Segment : Blob::Segment {
Blob::TwoValues radius;
Blob::TwoValues angle;
};
void generateTwoValues(int i) override;
Blob::Segment &segmentAt(int i) override;
const float64 _segmentLength;
const float _minScale;
const float _segmentAngle;
const float _angleDiff;
std::vector<Segment> _segments;
float64 _scale = 0;
};
class LinearBlob final : public Blob {
public:
enum class Direction {
TopDown,
BottomUp,
};
LinearBlob(
int n,
Direction direction = Direction::TopDown,
float minSpeed = 0,
float maxSpeed = 0);
void paint(QPainter &p, const QBrush &brush, int width);
private:
struct Segment : Blob::Segment {
Blob::TwoValues radius;
};
void generateTwoValues(int i) override;
Blob::Segment &segmentAt(int i) override;
const int _topDown;
std::vector<Segment> _segments;
};
} // namespace Ui::Paint

Some files were not shown because too many files have changed in this diff Show More