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
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:
233
Telegram/lib_ui/ui/abstract_button.cpp
Normal file
233
Telegram/lib_ui/ui/abstract_button.cpp
Normal 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
|
||||
125
Telegram/lib_ui/ui/abstract_button.h
Normal file
125
Telegram/lib_ui/ui/abstract_button.h
Normal 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
|
||||
27
Telegram/lib_ui/ui/accessible/ui_accessible_factory.cpp
Normal file
27
Telegram/lib_ui/ui/accessible/ui_accessible_factory.cpp
Normal 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
|
||||
13
Telegram/lib_ui/ui/accessible/ui_accessible_factory.h
Normal file
13
Telegram/lib_ui/ui/accessible/ui_accessible_factory.h
Normal 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
|
||||
131
Telegram/lib_ui/ui/accessible/ui_accessible_widget.cpp
Normal file
131
Telegram/lib_ui/ui/accessible/ui_accessible_widget.cpp
Normal 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
|
||||
31
Telegram/lib_ui/ui/accessible/ui_accessible_widget.h
Normal file
31
Telegram/lib_ui/ui/accessible/ui_accessible_widget.h
Normal 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
|
||||
429
Telegram/lib_ui/ui/animated_icon.cpp
Normal file
429
Telegram/lib_ui/ui/animated_icon.cpp
Normal 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
|
||||
102
Telegram/lib_ui/ui/animated_icon.h
Normal file
102
Telegram/lib_ui/ui/animated_icon.h
Normal 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
|
||||
18
Telegram/lib_ui/ui/arc_angles.h
Normal file
18
Telegram/lib_ui/ui/arc_angles.h
Normal 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
|
||||
135
Telegram/lib_ui/ui/basic.style
Normal file
135
Telegram/lib_ui/ui/basic.style
Normal 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
|
||||
134
Telegram/lib_ui/ui/basic_click_handlers.cpp
Normal file
134
Telegram/lib_ui/ui/basic_click_handlers.cpp
Normal 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 };
|
||||
}
|
||||
84
Telegram/lib_ui/ui/basic_click_handlers.h
Normal file
84
Telegram/lib_ui/ui/basic_click_handlers.h
Normal 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;
|
||||
|
||||
};
|
||||
71
Telegram/lib_ui/ui/cached_special_layer_shadow_corners.cpp
Normal file
71
Telegram/lib_ui/ui/cached_special_layer_shadow_corners.cpp
Normal 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
|
||||
13
Telegram/lib_ui/ui/cached_special_layer_shadow_corners.h
Normal file
13
Telegram/lib_ui/ui/cached_special_layer_shadow_corners.h
Normal 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
|
||||
186
Telegram/lib_ui/ui/click_handler.cpp
Normal file
186
Telegram/lib_ui/ui/click_handler.cpp
Normal 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 });
|
||||
}
|
||||
155
Telegram/lib_ui/ui/click_handler.h
Normal file
155
Telegram/lib_ui/ui/click_handler.h
Normal 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);
|
||||
678
Telegram/lib_ui/ui/colors.palette
Normal file
678
Telegram/lib_ui/ui/colors.palette
Normal 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
|
||||
127
Telegram/lib_ui/ui/delayed_activation.cpp
Normal file
127
Telegram/lib_ui/ui/delayed_activation.cpp
Normal 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
|
||||
16
Telegram/lib_ui/ui/delayed_activation.h
Normal file
16
Telegram/lib_ui/ui/delayed_activation.h
Normal 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
|
||||
32
Telegram/lib_ui/ui/dpr/dpr_icon.cpp
Normal file
32
Telegram/lib_ui/ui/dpr/dpr_icon.cpp
Normal 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
|
||||
20
Telegram/lib_ui/ui/dpr/dpr_icon.h
Normal file
20
Telegram/lib_ui/ui/dpr/dpr_icon.h
Normal file
@@ -0,0 +1,20 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/style/style_core.h"
|
||||
|
||||
class QImage;
|
||||
|
||||
namespace dpr {
|
||||
|
||||
[[nodiscard]] QImage IconFrame(
|
||||
const style::icon &icon,
|
||||
const QColor &color,
|
||||
double ratio);
|
||||
|
||||
} // namespace dpr
|
||||
50
Telegram/lib_ui/ui/dpr/dpr_image.h
Normal file
50
Telegram/lib_ui/ui/dpr/dpr_image.h
Normal 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
|
||||
63
Telegram/lib_ui/ui/dragging_scroll_manager.cpp
Normal file
63
Telegram/lib_ui/ui/dragging_scroll_manager.cpp
Normal 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
|
||||
34
Telegram/lib_ui/ui/dragging_scroll_manager.h
Normal file
34
Telegram/lib_ui/ui/dragging_scroll_manager.h
Normal 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
|
||||
21
Telegram/lib_ui/ui/dynamic_image.h
Normal file
21
Telegram/lib_ui/ui/dynamic_image.h
Normal 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
|
||||
181
Telegram/lib_ui/ui/effects/animation_value.cpp
Normal file
181
Telegram/lib_ui/ui/effects/animation_value.cpp
Normal file
@@ -0,0 +1,181 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/animation_value.h"
|
||||
|
||||
#include "ui/painter.h"
|
||||
|
||||
#include <QtCore/QtMath> // M_PI
|
||||
|
||||
namespace anim {
|
||||
namespace {
|
||||
|
||||
rpl::variable<bool> AnimationsDisabled = false;
|
||||
int SlowMultiplierMinusOne/* = 0*/;
|
||||
|
||||
} // namespace
|
||||
|
||||
transition linear = [](const float64 &delta, const float64 &dt) {
|
||||
Expects(!std::isnan(delta));
|
||||
Expects(!std::isnan(dt));
|
||||
|
||||
const auto result = delta * dt;
|
||||
|
||||
Ensures(!std::isnan(result));
|
||||
return result;
|
||||
};
|
||||
|
||||
transition sineInOut = [](const float64 &delta, const float64 &dt) {
|
||||
Expects(!std::isnan(delta));
|
||||
Expects(!std::isnan(dt));
|
||||
|
||||
const auto result = -(delta / 2) * (cos(M_PI * dt) - 1);
|
||||
|
||||
Ensures(!std::isnan(result));
|
||||
return result;
|
||||
};
|
||||
|
||||
transition halfSine = [](const float64 &delta, const float64 &dt) {
|
||||
Expects(!std::isnan(delta));
|
||||
Expects(!std::isnan(dt));
|
||||
|
||||
const auto result = delta * sin(M_PI * dt / 2);
|
||||
|
||||
Ensures(!std::isnan(result));
|
||||
return result;
|
||||
};
|
||||
|
||||
transition easeOutBack = [](const float64 &delta, const float64 &dt) {
|
||||
Expects(!std::isnan(delta));
|
||||
Expects(!std::isnan(dt));
|
||||
|
||||
static constexpr auto s = 1.70158;
|
||||
|
||||
const auto t = dt - 1;
|
||||
Assert(!std::isnan(t));
|
||||
const auto result = delta * (t * t * ((s + 1) * t + s) + 1);
|
||||
|
||||
Ensures(!std::isnan(result));
|
||||
return result;
|
||||
};
|
||||
|
||||
transition easeInCirc = [](const float64 &delta, const float64 &dt) {
|
||||
Expects(!std::isnan(delta));
|
||||
Expects(!std::isnan(dt));
|
||||
|
||||
const auto result = -delta * (sqrt(1 - dt * dt) - 1);
|
||||
|
||||
Ensures(!std::isnan(result));
|
||||
return result;
|
||||
};
|
||||
|
||||
transition easeOutCirc = [](const float64 &delta, const float64 &dt) {
|
||||
Expects(!std::isnan(delta));
|
||||
Expects(!std::isnan(dt));
|
||||
|
||||
const auto t = dt - 1;
|
||||
Assert(!std::isnan(t));
|
||||
const auto result = delta * sqrt(1 - t * t);
|
||||
|
||||
Ensures(!std::isnan(result));
|
||||
return result;
|
||||
};
|
||||
|
||||
transition easeInCubic = [](const float64 &delta, const float64 &dt) {
|
||||
const auto result = delta * dt * dt * dt;
|
||||
|
||||
Ensures(!std::isnan(result));
|
||||
return result;
|
||||
};
|
||||
|
||||
transition easeOutCubic = [](const float64 &delta, const float64 &dt) {
|
||||
Expects(!std::isnan(delta));
|
||||
Expects(!std::isnan(dt));
|
||||
|
||||
const auto t = dt - 1;
|
||||
Assert(!std::isnan(t));
|
||||
const auto result = delta * (t * t * t + 1);
|
||||
|
||||
Ensures(!std::isnan(result));
|
||||
return result;
|
||||
};
|
||||
|
||||
transition easeInQuint = [](const float64 &delta, const float64 &dt) {
|
||||
Expects(!std::isnan(delta));
|
||||
Expects(!std::isnan(dt));
|
||||
|
||||
const auto t2 = dt * dt;
|
||||
Assert(!std::isnan(t2));
|
||||
const auto result = delta * t2 * t2 * dt;
|
||||
|
||||
Ensures(!std::isnan(result));
|
||||
return result;
|
||||
};
|
||||
|
||||
transition easeOutQuint = [](const float64 &delta, const float64 &dt) {
|
||||
Expects(!std::isnan(delta));
|
||||
Expects(!std::isnan(dt));
|
||||
|
||||
const auto t = dt - 1, t2 = t * t;
|
||||
Assert(!std::isnan(t));
|
||||
Assert(!std::isnan(t2));
|
||||
const auto result = delta * (t2 * t2 * t + 1);
|
||||
|
||||
Ensures(!std::isnan(result));
|
||||
return result;
|
||||
};
|
||||
|
||||
rpl::producer<bool> Disables() {
|
||||
return AnimationsDisabled.value();
|
||||
};
|
||||
|
||||
bool Disabled() {
|
||||
return AnimationsDisabled.current();
|
||||
}
|
||||
|
||||
void SetDisabled(bool disabled) {
|
||||
AnimationsDisabled = disabled;
|
||||
}
|
||||
|
||||
int SlowMultiplier() {
|
||||
return (SlowMultiplierMinusOne + 1);
|
||||
}
|
||||
|
||||
void SetSlowMultiplier(int multiplier) {
|
||||
Expects(multiplier > 0);
|
||||
|
||||
SlowMultiplierMinusOne = multiplier - 1;
|
||||
}
|
||||
|
||||
void DrawStaticLoading(
|
||||
QPainter &p,
|
||||
QRectF rect,
|
||||
float64 stroke,
|
||||
QPen pen,
|
||||
QBrush brush) {
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
p.setBrush(brush);
|
||||
pen.setWidthF(stroke);
|
||||
pen.setCapStyle(Qt::RoundCap);
|
||||
pen.setJoinStyle(Qt::RoundJoin);
|
||||
p.setPen(pen);
|
||||
p.drawEllipse(rect);
|
||||
|
||||
const auto center = rect.center();
|
||||
const auto first = QPointF(center.x(), rect.y() + 1.5 * stroke);
|
||||
const auto delta = center.y() - first.y();
|
||||
const auto second = QPointF(center.x() + delta * 2 / 3., center.y());
|
||||
if (delta > 0) {
|
||||
QPainterPath path;
|
||||
path.moveTo(first);
|
||||
path.lineTo(center);
|
||||
path.lineTo(second);
|
||||
p.drawPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
} // anim
|
||||
420
Telegram/lib_ui/ui/effects/animation_value.h
Normal file
420
Telegram/lib_ui/ui/effects/animation_value.h
Normal file
@@ -0,0 +1,420 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/style/style_core.h"
|
||||
#include "base/basic_types.h"
|
||||
|
||||
#include <QtGui/QPainterPath>
|
||||
#include <crl/crl_time.h>
|
||||
|
||||
namespace anim {
|
||||
|
||||
enum class type : uchar {
|
||||
normal,
|
||||
instant,
|
||||
};
|
||||
|
||||
enum class activation : uchar {
|
||||
normal,
|
||||
background,
|
||||
};
|
||||
|
||||
enum class repeat : uchar {
|
||||
loop,
|
||||
once,
|
||||
};
|
||||
|
||||
using transition = Fn<float64(float64 delta, float64 dt)>;
|
||||
|
||||
extern transition linear;
|
||||
extern transition sineInOut;
|
||||
extern transition halfSine;
|
||||
extern transition easeOutBack;
|
||||
extern transition easeInCirc;
|
||||
extern transition easeOutCirc;
|
||||
extern transition easeInCubic;
|
||||
extern transition easeOutCubic;
|
||||
extern transition easeInQuint;
|
||||
extern transition easeOutQuint;
|
||||
|
||||
inline transition bumpy(float64 bump) {
|
||||
auto dt0 = (bump - sqrt(bump * (bump - 1.)));
|
||||
auto k = (1 / (2 * dt0 - 1));
|
||||
return [bump, dt0, k](float64 delta, float64 dt) {
|
||||
return delta * (bump - k * (dt - dt0) * (dt - dt0));
|
||||
};
|
||||
}
|
||||
|
||||
// Basic animated value.
|
||||
class value {
|
||||
public:
|
||||
using ValueType = float64;
|
||||
|
||||
value() = default;
|
||||
value(float64 from) : _cur(from), _from(from) {
|
||||
}
|
||||
value(float64 from, float64 to) : _cur(from), _from(from), _delta(to - from) {
|
||||
}
|
||||
void start(float64 to) {
|
||||
_from = _cur;
|
||||
_delta = to - _from;
|
||||
}
|
||||
void restart() {
|
||||
_delta = _from + _delta - _cur;
|
||||
_from = _cur;
|
||||
}
|
||||
|
||||
float64 from() const {
|
||||
return _from;
|
||||
}
|
||||
float64 current() const {
|
||||
return _cur;
|
||||
}
|
||||
float64 to() const {
|
||||
return _from + _delta;
|
||||
}
|
||||
void add(float64 delta) {
|
||||
_from += delta;
|
||||
_cur += delta;
|
||||
}
|
||||
value &update(float64 dt, transition func) {
|
||||
_cur = _from + func(_delta, dt);
|
||||
return *this;
|
||||
}
|
||||
void finish() {
|
||||
_cur = _from + _delta;
|
||||
_from = _cur;
|
||||
_delta = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
float64 _cur = 0.;
|
||||
float64 _from = 0.;
|
||||
float64 _delta = 0.;
|
||||
|
||||
};
|
||||
|
||||
TG_FORCE_INLINE float64 interpolateToF(int a, int b, float64 b_ratio) {
|
||||
return a + float64(b - a) * b_ratio;
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE int interpolate(int a, int b, float64 b_ratio) {
|
||||
return base::SafeRound(interpolateToF(a, b, b_ratio));
|
||||
}
|
||||
|
||||
#ifdef ARCH_CPU_32_BITS
|
||||
#define SHIFTED_USE_32BIT
|
||||
#endif // ARCH_CPU_32_BITS
|
||||
|
||||
#ifdef SHIFTED_USE_32BIT
|
||||
|
||||
using ShiftedMultiplier = uint32;
|
||||
|
||||
struct Shifted {
|
||||
Shifted() = default;
|
||||
Shifted(uint32 low, uint32 high) : low(low), high(high) {
|
||||
}
|
||||
uint32 low = 0;
|
||||
uint32 high = 0;
|
||||
};
|
||||
|
||||
TG_FORCE_INLINE Shifted operator+(Shifted a, Shifted b) {
|
||||
return Shifted(a.low + b.low, a.high + b.high);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE Shifted operator*(Shifted shifted, ShiftedMultiplier multiplier) {
|
||||
return Shifted(shifted.low * multiplier, shifted.high * multiplier);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE Shifted operator*(ShiftedMultiplier multiplier, Shifted shifted) {
|
||||
return Shifted(shifted.low * multiplier, shifted.high * multiplier);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE Shifted shifted(uint32 components) {
|
||||
return Shifted(
|
||||
(components & 0x000000FFU) | ((components & 0x0000FF00U) << 8),
|
||||
((components & 0x00FF0000U) >> 16) | ((components & 0xFF000000U) >> 8));
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE uint32 unshifted(Shifted components) {
|
||||
return ((components.low & 0x0000FF00U) >> 8)
|
||||
| ((components.low & 0xFF000000U) >> 16)
|
||||
| ((components.high & 0x0000FF00U) << 8)
|
||||
| (components.high & 0xFF000000U);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE Shifted reshifted(Shifted components) {
|
||||
return Shifted((components.low >> 8) & 0x00FF00FFU, (components.high >> 8) & 0x00FF00FFU);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE Shifted shifted(QColor color) {
|
||||
// Make it premultiplied.
|
||||
auto alpha = static_cast<uint32>((color.alpha() & 0xFF) + 1);
|
||||
auto components = Shifted(static_cast<uint32>(color.blue() & 0xFF) | (static_cast<uint32>(color.green() & 0xFF) << 16),
|
||||
static_cast<uint32>(color.red() & 0xFF) | (static_cast<uint32>(255) << 16));
|
||||
return reshifted(components * alpha);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE uint32 getPremultiplied(QColor color) {
|
||||
// Make it premultiplied.
|
||||
auto alpha = static_cast<uint32>((color.alpha() & 0xFF) + 1);
|
||||
auto components = Shifted(static_cast<uint32>(color.blue() & 0xFF) | (static_cast<uint32>(color.green() & 0xFF) << 16),
|
||||
static_cast<uint32>(color.red() & 0xFF) | (static_cast<uint32>(255) << 16));
|
||||
return unshifted(components * alpha);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE uint32 getAlpha(Shifted components) {
|
||||
return (components.high & 0x00FF0000U) >> 16;
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE Shifted non_premultiplied(QColor color) {
|
||||
return Shifted(static_cast<uint32>(color.blue() & 0xFF) | (static_cast<uint32>(color.green() & 0xFF) << 16),
|
||||
static_cast<uint32>(color.red() & 0xFF) | (static_cast<uint32>(color.alpha() & 0xFF) << 16));
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE QColor color(QColor a, QColor b, float64 b_ratio) {
|
||||
auto bOpacity = std::clamp(interpolate(0, 255, b_ratio), 0, 255) + 1;
|
||||
auto aOpacity = (256 - bOpacity);
|
||||
auto components = (non_premultiplied(a) * aOpacity + non_premultiplied(b) * bOpacity);
|
||||
return {
|
||||
static_cast<int>((components.high >> 8) & 0xFF),
|
||||
static_cast<int>((components.low >> 24) & 0xFF),
|
||||
static_cast<int>((components.low >> 8) & 0xFF),
|
||||
static_cast<int>((components.high >> 24) & 0xFF),
|
||||
};
|
||||
}
|
||||
|
||||
#else // SHIFTED_USE_32BIT
|
||||
|
||||
using ShiftedMultiplier = uint64;
|
||||
|
||||
struct Shifted {
|
||||
Shifted() = default;
|
||||
Shifted(uint32 value) : value(value) {
|
||||
}
|
||||
Shifted(uint64 value) : value(value) {
|
||||
}
|
||||
uint64 value = 0;
|
||||
};
|
||||
|
||||
TG_FORCE_INLINE Shifted operator+(Shifted a, Shifted b) {
|
||||
return Shifted(a.value + b.value);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE Shifted operator*(Shifted shifted, ShiftedMultiplier multiplier) {
|
||||
return Shifted(shifted.value * multiplier);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE Shifted operator*(ShiftedMultiplier multiplier, Shifted shifted) {
|
||||
return Shifted(shifted.value * multiplier);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE Shifted shifted(uint32 components) {
|
||||
auto wide = static_cast<uint64>(components);
|
||||
return (wide & 0x00000000000000FFULL)
|
||||
| ((wide & 0x000000000000FF00ULL) << 8)
|
||||
| ((wide & 0x0000000000FF0000ULL) << 16)
|
||||
| ((wide & 0x00000000FF000000ULL) << 24);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE uint32 unshifted(Shifted components) {
|
||||
return static_cast<uint32>((components.value & 0x000000000000FF00ULL) >> 8)
|
||||
| static_cast<uint32>((components.value & 0x00000000FF000000ULL) >> 16)
|
||||
| static_cast<uint32>((components.value & 0x0000FF0000000000ULL) >> 24)
|
||||
| static_cast<uint32>((components.value & 0xFF00000000000000ULL) >> 32);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE Shifted reshifted(Shifted components) {
|
||||
return (components.value >> 8) & 0x00FF00FF00FF00FFULL;
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE Shifted shifted(QColor color) {
|
||||
// Make it premultiplied.
|
||||
auto alpha = static_cast<uint64>((color.alpha() & 0xFF) + 1);
|
||||
auto components = static_cast<uint64>(color.blue() & 0xFF)
|
||||
| (static_cast<uint64>(color.green() & 0xFF) << 16)
|
||||
| (static_cast<uint64>(color.red() & 0xFF) << 32)
|
||||
| (static_cast<uint64>(255) << 48);
|
||||
return reshifted(components * alpha);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE uint32 getPremultiplied(QColor color) {
|
||||
// Make it premultiplied.
|
||||
auto alpha = static_cast<uint64>((color.alpha() & 0xFF) + 1);
|
||||
auto components = static_cast<uint64>(color.blue() & 0xFF)
|
||||
| (static_cast<uint64>(color.green() & 0xFF) << 16)
|
||||
| (static_cast<uint64>(color.red() & 0xFF) << 32)
|
||||
| (static_cast<uint64>(255) << 48);
|
||||
return unshifted(components * alpha);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE uint32 getAlpha(Shifted components) {
|
||||
return (components.value & 0x00FF000000000000ULL) >> 48;
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE Shifted non_premultiplied(QColor color) {
|
||||
return static_cast<uint64>(color.blue() & 0xFF)
|
||||
| (static_cast<uint64>(color.green() & 0xFF) << 16)
|
||||
| (static_cast<uint64>(color.red() & 0xFF) << 32)
|
||||
| (static_cast<uint64>(color.alpha() & 0xFF) << 48);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE QColor color(QColor a, QColor b, float64 b_ratio) {
|
||||
auto bOpacity = std::clamp(interpolate(0, 255, b_ratio), 0, 255) + 1;
|
||||
auto aOpacity = (256 - bOpacity);
|
||||
auto components = (non_premultiplied(a) * aOpacity + non_premultiplied(b) * bOpacity);
|
||||
return {
|
||||
static_cast<int>((components.value >> 40) & 0xFF),
|
||||
static_cast<int>((components.value >> 24) & 0xFF),
|
||||
static_cast<int>((components.value >> 8) & 0xFF),
|
||||
static_cast<int>((components.value >> 56) & 0xFF),
|
||||
};
|
||||
}
|
||||
|
||||
#endif // SHIFTED_USE_32BIT
|
||||
|
||||
TG_FORCE_INLINE QColor color(style::color a, QColor b, float64 b_ratio) {
|
||||
return color(a->c, b, b_ratio);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE QColor color(QColor a, style::color b, float64 b_ratio) {
|
||||
return color(a, b->c, b_ratio);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE QColor color(style::color a, style::color b, float64 b_ratio) {
|
||||
return color(a->c, b->c, b_ratio);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE QPen pen(QColor a, QColor b, float64 b_ratio) {
|
||||
return color(a, b, b_ratio);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE QPen pen(style::color a, QColor b, float64 b_ratio) {
|
||||
return (b_ratio > 0) ? pen(a->c, b, b_ratio) : a;
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE QPen pen(QColor a, style::color b, float64 b_ratio) {
|
||||
return (b_ratio < 1) ? pen(a, b->c, b_ratio) : b;
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE QPen pen(style::color a, style::color b, float64 b_ratio) {
|
||||
return (b_ratio > 0) ? ((b_ratio < 1) ? pen(a->c, b->c, b_ratio) : b) : a;
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE QBrush brush(QColor a, QColor b, float64 b_ratio) {
|
||||
return color(a, b, b_ratio);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE QBrush brush(style::color a, QColor b, float64 b_ratio) {
|
||||
return (b_ratio > 0) ? brush(a->c, b, b_ratio) : a;
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE QBrush brush(QColor a, style::color b, float64 b_ratio) {
|
||||
return (b_ratio < 1) ? brush(a, b->c, b_ratio) : b;
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE QBrush brush(style::color a, style::color b, float64 b_ratio) {
|
||||
return (b_ratio > 0) ? ((b_ratio < 1) ? brush(a->c, b->c, b_ratio) : b) : a;
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE QColor with_alpha(QColor color, float64 alpha) {
|
||||
color.setAlphaF(color.alphaF() * alpha);
|
||||
return color;
|
||||
}
|
||||
|
||||
template <int N>
|
||||
QPainterPath interpolate(QPointF (&from)[N], QPointF (&to)[N], float64 k) {
|
||||
static_assert(N > 1, "Wrong points count in path!");
|
||||
|
||||
auto from_coef = 1. - k, to_coef = k;
|
||||
QPainterPath result;
|
||||
auto x = from[0].x() * from_coef + to[0].x() * to_coef;
|
||||
auto y = from[0].y() * from_coef + to[0].y() * to_coef;
|
||||
result.moveTo(x, y);
|
||||
for (int i = 1; i != N; ++i) {
|
||||
result.lineTo(from[i].x() * from_coef + to[i].x() * to_coef, from[i].y() * from_coef + to[i].y() * to_coef);
|
||||
}
|
||||
result.lineTo(x, y);
|
||||
return result;
|
||||
}
|
||||
|
||||
template <int N>
|
||||
QPainterPath path(QPointF (&from)[N]) {
|
||||
static_assert(N > 1, "Wrong points count in path!");
|
||||
|
||||
QPainterPath result;
|
||||
auto x = from[0].x();
|
||||
auto y = from[0].y();
|
||||
result.moveTo(x, y);
|
||||
for (int i = 1; i != N; ++i) {
|
||||
result.lineTo(from[i].x(), from[i].y());
|
||||
}
|
||||
result.lineTo(x, y);
|
||||
return result;
|
||||
}
|
||||
|
||||
rpl::producer<bool> Disables();
|
||||
bool Disabled();
|
||||
void SetDisabled(bool disabled);
|
||||
int SlowMultiplier();
|
||||
void SetSlowMultiplier(int multiplier); // 1 - default, 10 - slow x10.
|
||||
|
||||
void DrawStaticLoading(
|
||||
QPainter &p,
|
||||
QRectF rect,
|
||||
float64 stroke,
|
||||
QPen pen,
|
||||
QBrush brush = Qt::NoBrush);
|
||||
|
||||
class continuous_value {
|
||||
public:
|
||||
continuous_value() = default;
|
||||
continuous_value(float64 duration) : _duration(duration) {
|
||||
}
|
||||
void start(float64 to, float64 duration) {
|
||||
_to = to;
|
||||
_delta = (_to - _cur) / duration;
|
||||
}
|
||||
void start(float64 to) {
|
||||
start(to, _duration);
|
||||
}
|
||||
void reset() {
|
||||
_to = _cur = _delta = 0.;
|
||||
}
|
||||
|
||||
float64 current() const {
|
||||
return _cur;
|
||||
}
|
||||
float64 to() const {
|
||||
return _to;
|
||||
}
|
||||
float64 delta() const {
|
||||
return _delta;
|
||||
}
|
||||
void update(crl::time dt, Fn<void(float64 &)> &&callback = nullptr) {
|
||||
if (_to != _cur) {
|
||||
_cur += _delta * dt;
|
||||
if ((_to != _cur) && ((_delta > 0) == (_cur > _to))) {
|
||||
_cur = _to;
|
||||
}
|
||||
if (callback) {
|
||||
callback(_cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
float64 _duration = 0.;
|
||||
float64 _to = 0.;
|
||||
|
||||
float64 _cur = 0.;
|
||||
float64 _delta = 0.;
|
||||
|
||||
};
|
||||
|
||||
} // namespace anim
|
||||
26
Telegram/lib_ui/ui/effects/animation_value_f.h
Normal file
26
Telegram/lib_ui/ui/effects/animation_value_f.h
Normal file
@@ -0,0 +1,26 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
namespace anim {
|
||||
|
||||
TG_FORCE_INLINE float64 interpolateF(float a, float b, float64 b_ratio) {
|
||||
return a + float64(b - a) * b_ratio;
|
||||
};
|
||||
|
||||
TG_FORCE_INLINE QRectF interpolatedRectF(
|
||||
const QRectF &r1,
|
||||
const QRectF &r2,
|
||||
float64 ratio) {
|
||||
return QRectF(
|
||||
interpolateF(r1.x(), r2.x(), ratio),
|
||||
interpolateF(r1.y(), r2.y(), ratio),
|
||||
interpolateF(r1.width(), r2.width(), ratio),
|
||||
interpolateF(r1.height(), r2.height(), ratio));
|
||||
}
|
||||
|
||||
} // namespace anim
|
||||
212
Telegram/lib_ui/ui/effects/animations.cpp
Normal file
212
Telegram/lib_ui/ui/effects/animations.cpp
Normal file
@@ -0,0 +1,212 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
#include "base/invoke_queued.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_basic.h"
|
||||
|
||||
#include <QtCore/QPointer>
|
||||
|
||||
#include <crl/crl_on_main.h>
|
||||
#include <crl/crl.h>
|
||||
#include <rpl/filter.h>
|
||||
#include <range/v3/algorithm/remove_if.hpp>
|
||||
#include <range/v3/algorithm/remove.hpp>
|
||||
#include <range/v3/algorithm/find.hpp>
|
||||
|
||||
namespace Ui {
|
||||
namespace Animations {
|
||||
namespace {
|
||||
|
||||
constexpr auto kAnimationTick = crl::time(1000) / st::universalDuration;
|
||||
constexpr auto kIgnoreUpdatesTimeout = crl::time(4);
|
||||
|
||||
Manager *ManagerInstance = nullptr;
|
||||
|
||||
} // namespace
|
||||
|
||||
void Basic::start() {
|
||||
Expects(ManagerInstance != nullptr);
|
||||
|
||||
if (animating()) {
|
||||
restart();
|
||||
} else {
|
||||
ManagerInstance->start(this);
|
||||
}
|
||||
}
|
||||
|
||||
void Basic::stop() {
|
||||
Expects(ManagerInstance != nullptr);
|
||||
|
||||
if (animating()) {
|
||||
ManagerInstance->stop(this);
|
||||
}
|
||||
}
|
||||
|
||||
void Basic::restart() {
|
||||
Expects(_started >= 0);
|
||||
|
||||
_started = crl::now();
|
||||
|
||||
Ensures(_started >= 0);
|
||||
}
|
||||
|
||||
void Basic::markStarted() {
|
||||
Expects(_started < 0);
|
||||
|
||||
_started = crl::now();
|
||||
|
||||
Ensures(_started >= 0);
|
||||
}
|
||||
|
||||
void Basic::markStopped() {
|
||||
Expects(_started >= 0);
|
||||
|
||||
_started = -1;
|
||||
}
|
||||
|
||||
Manager::Manager() {
|
||||
Expects(ManagerInstance == nullptr);
|
||||
|
||||
ManagerInstance = this;
|
||||
|
||||
crl::on_main_update_requests(
|
||||
) | rpl::filter([=] {
|
||||
return (_lastUpdateTime + kIgnoreUpdatesTimeout < crl::now());
|
||||
}) | rpl::on_next([=] {
|
||||
update();
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
Manager::~Manager() {
|
||||
Expects(ManagerInstance == this);
|
||||
Expects(_active.empty());
|
||||
Expects(_starting.empty());
|
||||
|
||||
ManagerInstance = nullptr;
|
||||
}
|
||||
|
||||
void Manager::start(not_null<Basic*> animation) {
|
||||
_forceImmediateUpdate = true;
|
||||
if (_updating) {
|
||||
_starting.emplace_back(animation.get());
|
||||
} else {
|
||||
schedule();
|
||||
_active.emplace_back(animation.get());
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::stop(not_null<Basic*> animation) {
|
||||
if (empty(_active) && empty(_starting)) {
|
||||
return;
|
||||
}
|
||||
const auto value = animation.get();
|
||||
const auto proj = &ActiveBasicPointer::get;
|
||||
auto &list = _updating ? _starting : _active;
|
||||
list.erase(ranges::remove(list, value, proj), end(list));
|
||||
|
||||
if (_updating) {
|
||||
const auto i = ranges::find(_active, value, proj);
|
||||
if (i != end(_active)) {
|
||||
*i = nullptr;
|
||||
_removedWhileUpdating = true;
|
||||
}
|
||||
} else if (empty(_active)) {
|
||||
stopTimer();
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::update() {
|
||||
if (_active.empty() || _updating || _scheduled) {
|
||||
return;
|
||||
}
|
||||
const auto now = crl::now();
|
||||
if (_forceImmediateUpdate) {
|
||||
_forceImmediateUpdate = false;
|
||||
}
|
||||
schedule();
|
||||
|
||||
_updating = true;
|
||||
const auto guard = gsl::finally([&] { _updating = false; });
|
||||
|
||||
_lastUpdateTime = now;
|
||||
const auto isFinished = [&](const ActiveBasicPointer &element) {
|
||||
return !element.call(now);
|
||||
};
|
||||
_active.erase(ranges::remove_if(_active, isFinished), end(_active));
|
||||
|
||||
if (_removedWhileUpdating) {
|
||||
_removedWhileUpdating = false;
|
||||
const auto proj = &ActiveBasicPointer::get;
|
||||
_active.erase(ranges::remove(_active, nullptr, proj), end(_active));
|
||||
}
|
||||
|
||||
if (!empty(_starting)) {
|
||||
_active.insert(
|
||||
end(_active),
|
||||
std::make_move_iterator(begin(_starting)),
|
||||
std::make_move_iterator(end(_starting)));
|
||||
_starting.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::updateQueued() {
|
||||
Expects(_timerId == 0);
|
||||
|
||||
_timerId = -1;
|
||||
InvokeQueued(delayedCallGuard(), [=] {
|
||||
Expects(_timerId < 0);
|
||||
|
||||
_timerId = 0;
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
void Manager::schedule() {
|
||||
if (_scheduled || _timerId < 0) {
|
||||
return;
|
||||
}
|
||||
stopTimer();
|
||||
|
||||
_scheduled = true;
|
||||
PostponeCall(delayedCallGuard(), [=] {
|
||||
_scheduled = false;
|
||||
if (_active.empty()) {
|
||||
return;
|
||||
}
|
||||
if (_forceImmediateUpdate) {
|
||||
_forceImmediateUpdate = false;
|
||||
updateQueued();
|
||||
} else {
|
||||
const auto next = _lastUpdateTime + kAnimationTick;
|
||||
const auto now = crl::now();
|
||||
if (now < next) {
|
||||
_timerId = startTimer(next - now, Qt::PreciseTimer);
|
||||
} else {
|
||||
updateQueued();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
not_null<const QObject*> Manager::delayedCallGuard() const {
|
||||
return static_cast<const QObject*>(this);
|
||||
}
|
||||
|
||||
void Manager::stopTimer() {
|
||||
if (_timerId > 0) {
|
||||
killTimer(base::take(_timerId));
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::timerEvent(QTimerEvent *e) {
|
||||
update();
|
||||
}
|
||||
|
||||
} // namespace Animations
|
||||
} // namespace Ui
|
||||
460
Telegram/lib_ui/ui/effects/animations.h
Normal file
460
Telegram/lib_ui/ui/effects/animations.h
Normal file
@@ -0,0 +1,460 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/effects/animation_value.h"
|
||||
|
||||
#include <crl/crl_time.h>
|
||||
#include <rpl/lifetime.h>
|
||||
#include <QtCore/QObject>
|
||||
|
||||
namespace Ui {
|
||||
namespace Animations {
|
||||
|
||||
class Manager;
|
||||
|
||||
class Basic final {
|
||||
public:
|
||||
Basic() = default;
|
||||
Basic(const Basic &other) = delete;
|
||||
Basic &operator=(const Basic &other) = delete;
|
||||
Basic(Basic &&other);
|
||||
Basic &operator=(Basic &&other);
|
||||
|
||||
template <typename Callback>
|
||||
explicit Basic(Callback &&callback);
|
||||
|
||||
template <typename Callback>
|
||||
void init(Callback &&callback);
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
[[nodiscard]] crl::time started() const;
|
||||
[[nodiscard]] bool animating() const;
|
||||
|
||||
~Basic();
|
||||
|
||||
private:
|
||||
friend class Manager;
|
||||
|
||||
template <typename Callback>
|
||||
[[nodiscard]] static Fn<bool(crl::time)> Prepare(Callback &&callback);
|
||||
|
||||
[[nodiscard]] bool call(crl::time now) const;
|
||||
void restart();
|
||||
|
||||
void markStarted();
|
||||
void markStopped();
|
||||
|
||||
crl::time _started = -1;
|
||||
Fn<bool(crl::time)> _callback;
|
||||
|
||||
};
|
||||
|
||||
class Simple final {
|
||||
public:
|
||||
template <typename Callback>
|
||||
void start(
|
||||
Callback &&callback,
|
||||
float64 from,
|
||||
float64 to,
|
||||
crl::time duration,
|
||||
anim::transition transition = anim::linear);
|
||||
void change(
|
||||
float64 to,
|
||||
crl::time duration,
|
||||
anim::transition transition = anim::linear);
|
||||
void stop();
|
||||
[[nodiscard]] bool animating() const;
|
||||
[[nodiscard]] float64 value(float64 final) const;
|
||||
|
||||
private:
|
||||
class ShortTracker {
|
||||
public:
|
||||
ShortTracker() {
|
||||
restart();
|
||||
}
|
||||
ShortTracker(const ShortTracker &other) = delete;
|
||||
ShortTracker &operator=(const ShortTracker &other) = delete;
|
||||
~ShortTracker() {
|
||||
release();
|
||||
}
|
||||
void restart() {
|
||||
if (!std::exchange(_paused, true)) {
|
||||
style::internal::StartShortAnimation();
|
||||
}
|
||||
}
|
||||
void release() {
|
||||
if (std::exchange(_paused, false)) {
|
||||
style::internal::StopShortAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
bool _paused = false;
|
||||
|
||||
};
|
||||
|
||||
struct Data {
|
||||
explicit Data(float64 initial) : value(initial) {
|
||||
}
|
||||
~Data() {
|
||||
if (markOnDelete) {
|
||||
*markOnDelete = true;
|
||||
}
|
||||
}
|
||||
|
||||
Basic animation;
|
||||
anim::transition transition;
|
||||
float64 from = 0.;
|
||||
float64 delta = 0.;
|
||||
float64 value = 0.;
|
||||
float64 duration = 0.;
|
||||
bool *markOnDelete = nullptr;
|
||||
ShortTracker tracker;
|
||||
};
|
||||
|
||||
template <typename Callback>
|
||||
[[nodiscard]] static decltype(auto) Prepare(Callback &&callback);
|
||||
|
||||
void prepare(float64 from, crl::time duration);
|
||||
void startPrepared(
|
||||
float64 to,
|
||||
crl::time duration,
|
||||
anim::transition transition);
|
||||
|
||||
static constexpr auto kLongAnimationDuration = crl::time(1000);
|
||||
|
||||
mutable std::unique_ptr<Data> _data;
|
||||
|
||||
};
|
||||
|
||||
class Manager final : private QObject {
|
||||
public:
|
||||
Manager();
|
||||
~Manager();
|
||||
|
||||
void update();
|
||||
|
||||
private:
|
||||
class ActiveBasicPointer {
|
||||
public:
|
||||
ActiveBasicPointer(Basic *value = nullptr) : _value(value) {
|
||||
if (_value) {
|
||||
_value->markStarted();
|
||||
}
|
||||
}
|
||||
ActiveBasicPointer(ActiveBasicPointer &&other)
|
||||
: _value(base::take(other._value)) {
|
||||
}
|
||||
ActiveBasicPointer &operator=(ActiveBasicPointer &&other) {
|
||||
if (_value != other._value) {
|
||||
if (_value) {
|
||||
_value->markStopped();
|
||||
}
|
||||
_value = base::take(other._value);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
~ActiveBasicPointer() {
|
||||
if (_value) {
|
||||
_value->markStopped();
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool call(crl::time now) const {
|
||||
return _value && _value->call(now);
|
||||
}
|
||||
|
||||
friend inline bool operator==(
|
||||
const ActiveBasicPointer &a,
|
||||
const ActiveBasicPointer &b) {
|
||||
return a._value == b._value;
|
||||
}
|
||||
|
||||
Basic *get() const {
|
||||
return _value;
|
||||
}
|
||||
|
||||
private:
|
||||
Basic *_value = nullptr;
|
||||
|
||||
};
|
||||
|
||||
friend class Basic;
|
||||
|
||||
void timerEvent(QTimerEvent *e) override;
|
||||
|
||||
void start(not_null<Basic*> animation);
|
||||
void stop(not_null<Basic*> animation);
|
||||
|
||||
void schedule();
|
||||
void updateQueued();
|
||||
void stopTimer();
|
||||
not_null<const QObject*> delayedCallGuard() const;
|
||||
|
||||
crl::time _lastUpdateTime = 0;
|
||||
int _timerId = 0;
|
||||
bool _updating = false;
|
||||
bool _removedWhileUpdating = false;
|
||||
bool _scheduled = false;
|
||||
bool _forceImmediateUpdate = false;
|
||||
std::vector<ActiveBasicPointer> _active;
|
||||
std::vector<ActiveBasicPointer> _starting;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
template <typename Callback>
|
||||
Fn<bool(crl::time)> Basic__PrepareCrlTime(Callback &&callback) {
|
||||
using Return = decltype(callback(crl::time(0)));
|
||||
if constexpr (std::is_convertible_v<Return, bool>) {
|
||||
return std::forward<Callback>(callback);
|
||||
} else if constexpr (std::is_same_v<Return, void>) {
|
||||
return [callback = std::forward<Callback>(callback)](
|
||||
crl::time time) {
|
||||
callback(time);
|
||||
return true;
|
||||
};
|
||||
} else {
|
||||
static_assert(false_t(callback), "Expected void or bool.");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
Fn<bool(crl::time)> Basic__PreparePlain(Callback &&callback) {
|
||||
using Return = decltype(callback());
|
||||
if constexpr (std::is_convertible_v<Return, bool>) {
|
||||
return [callback = std::forward<Callback>(callback)](crl::time) {
|
||||
return callback();
|
||||
};
|
||||
} else if constexpr (std::is_same_v<Return, void>) {
|
||||
return [callback = std::forward<Callback>(callback)](crl::time) {
|
||||
callback();
|
||||
return true;
|
||||
};
|
||||
} else {
|
||||
static_assert(false_t(callback), "Expected void or bool.");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
inline Fn<bool(crl::time)> Basic::Prepare(Callback &&callback) {
|
||||
if constexpr (rpl::details::is_callable_plain_v<Callback, crl::time>) {
|
||||
return Basic__PrepareCrlTime(std::forward<Callback>(callback));
|
||||
} else if constexpr (rpl::details::is_callable_plain_v<Callback>) {
|
||||
return Basic__PreparePlain(std::forward<Callback>(callback));
|
||||
} else {
|
||||
static_assert(false_t(callback), "Expected crl::time or no args.");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
inline Basic::Basic(Callback &&callback)
|
||||
: _callback(Prepare(std::forward<Callback>(callback))) {
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
inline void Basic::init(Callback &&callback) {
|
||||
_callback = Prepare(std::forward<Callback>(callback));
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE crl::time Basic::started() const {
|
||||
return _started;
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE bool Basic::animating() const {
|
||||
return (_started >= 0);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE bool Basic::call(crl::time now) const {
|
||||
Expects(_started >= 0);
|
||||
|
||||
// _started may be greater than now if we called restart while iterating.
|
||||
const auto onstack = _callback;
|
||||
return onstack(std::max(_started, now));
|
||||
}
|
||||
|
||||
inline Basic::Basic(Basic &&other) : _callback(base::take(other._callback)) {
|
||||
if (other.animating()) {
|
||||
const auto started = other._started;
|
||||
other.stop();
|
||||
start();
|
||||
other._started = started;
|
||||
}
|
||||
}
|
||||
|
||||
inline Basic &Basic::operator=(Basic &&other) {
|
||||
_callback = base::take(other._callback);
|
||||
if (animating()) {
|
||||
stop();
|
||||
}
|
||||
if (other.animating()) {
|
||||
const auto started = other._started;
|
||||
other.stop();
|
||||
start();
|
||||
other._started = started;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline Basic::~Basic() {
|
||||
stop();
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
decltype(auto) Simple__PrepareFloat64(Callback &&callback) {
|
||||
using Return = decltype(callback(float64(0.)));
|
||||
if constexpr (std::is_convertible_v<Return, bool>) {
|
||||
return std::forward<Callback>(callback);
|
||||
} else if constexpr (std::is_same_v<Return, void>) {
|
||||
return [callback = std::forward<Callback>(callback)](
|
||||
float64 value) {
|
||||
callback(value);
|
||||
return true;
|
||||
};
|
||||
} else {
|
||||
static_assert(false_t(callback), "Expected void or float64.");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
decltype(auto) Simple__PreparePlain(Callback &&callback) {
|
||||
using Return = decltype(callback());
|
||||
if constexpr (std::is_convertible_v<Return, bool>) {
|
||||
return [callback = std::forward<Callback>(callback)](float64) {
|
||||
return callback();
|
||||
};
|
||||
} else if constexpr (std::is_same_v<Return, void>) {
|
||||
return [callback = std::forward<Callback>(callback)](float64) {
|
||||
callback();
|
||||
return true;
|
||||
};
|
||||
} else {
|
||||
static_assert(false_t(callback), "Expected void or bool.");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
decltype(auto) Simple::Prepare(Callback &&callback) {
|
||||
if constexpr (rpl::details::is_callable_plain_v<Callback, float64>) {
|
||||
return Simple__PrepareFloat64(std::forward<Callback>(callback));
|
||||
} else if constexpr (rpl::details::is_callable_plain_v<Callback>) {
|
||||
return Simple__PreparePlain(std::forward<Callback>(callback));
|
||||
} else {
|
||||
static_assert(false_t(callback), "Expected float64 or no args.");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
inline void Simple::start(
|
||||
Callback &&callback,
|
||||
float64 from,
|
||||
float64 to,
|
||||
crl::time duration,
|
||||
anim::transition transition) {
|
||||
prepare(from, duration);
|
||||
_data->animation.init([
|
||||
that = _data.get(),
|
||||
callback = Prepare(std::forward<Callback>(callback))
|
||||
](crl::time now) {
|
||||
Assert(!std::isnan(double(now - that->animation.started())));
|
||||
const auto time = anim::Disabled()
|
||||
? that->duration
|
||||
: (now - that->animation.started());
|
||||
Assert(!std::isnan(time));
|
||||
Assert(!std::isnan(that->delta));
|
||||
Assert(!std::isnan(that->duration));
|
||||
const auto finished = (time >= that->duration);
|
||||
Assert(finished || that->duration > 0);
|
||||
const auto progressRatio = finished ? 1. : time / that->duration;
|
||||
Assert(!std::isnan(progressRatio));
|
||||
const auto progress = finished
|
||||
? that->delta
|
||||
: that->transition(that->delta, progressRatio);
|
||||
Assert(!std::isnan(that->from));
|
||||
Assert(!std::isnan(progress));
|
||||
that->value = that->from + progress;
|
||||
Assert(!std::isnan(that->value));
|
||||
|
||||
if (finished) {
|
||||
that->animation.stop();
|
||||
}
|
||||
|
||||
auto deleted = false;
|
||||
that->markOnDelete = &deleted;
|
||||
const auto result = callback(that->value) && !finished;
|
||||
if (!deleted) {
|
||||
that->markOnDelete = nullptr;
|
||||
if (!result) {
|
||||
that->tracker.release();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
startPrepared(to, duration, transition);
|
||||
}
|
||||
|
||||
inline void Simple::change(
|
||||
float64 to,
|
||||
crl::time duration,
|
||||
anim::transition transition) {
|
||||
Expects(_data != nullptr);
|
||||
|
||||
prepare(0. /* ignored */, duration);
|
||||
startPrepared(to, duration, transition);
|
||||
}
|
||||
|
||||
inline void Simple::prepare(float64 from, crl::time duration) {
|
||||
const auto isLong = (duration > kLongAnimationDuration);
|
||||
if (!_data) {
|
||||
_data = std::make_unique<Data>(from);
|
||||
} else if (!isLong) {
|
||||
_data->tracker.restart();
|
||||
}
|
||||
if (isLong) {
|
||||
_data->tracker.release();
|
||||
}
|
||||
}
|
||||
|
||||
inline void Simple::stop() {
|
||||
_data = nullptr;
|
||||
}
|
||||
|
||||
inline bool Simple::animating() const {
|
||||
if (!_data) {
|
||||
return false;
|
||||
} else if (!_data->animation.animating()) {
|
||||
_data = nullptr;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE float64 Simple::value(float64 final) const {
|
||||
if (animating()) {
|
||||
Assert(!std::isnan(_data->value));
|
||||
return _data->value;
|
||||
}
|
||||
Assert(!std::isnan(final));
|
||||
return final;
|
||||
}
|
||||
|
||||
inline void Simple::startPrepared(
|
||||
float64 to,
|
||||
crl::time duration,
|
||||
anim::transition transition) {
|
||||
_data->from = _data->value;
|
||||
_data->delta = to - _data->from;
|
||||
_data->duration = duration * anim::SlowMultiplier();
|
||||
_data->transition = transition;
|
||||
_data->animation.start();
|
||||
}
|
||||
|
||||
} // namespace Animations
|
||||
} // namespace Ui
|
||||
206
Telegram/lib_ui/ui/effects/cross_animation.cpp
Normal file
206
Telegram/lib_ui/ui/effects/cross_animation.cpp
Normal file
@@ -0,0 +1,206 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/cross_animation.h"
|
||||
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/arc_angles.h"
|
||||
#include "ui/painter.h"
|
||||
|
||||
#include <QtCore/QtMath>
|
||||
#include <QtGui/QPainterPath>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kPointCount = 12;
|
||||
constexpr auto kStaticLoadingValue = float64(-666);
|
||||
|
||||
|
||||
//
|
||||
// 1 3
|
||||
// X X X X
|
||||
// X X X X
|
||||
// 0 X X 4
|
||||
// X X X X
|
||||
// X 2 X
|
||||
// X X
|
||||
// X X
|
||||
// 11 5
|
||||
// X X
|
||||
// X X
|
||||
// X 8 X
|
||||
// X X X X
|
||||
// 10 X X 6
|
||||
// X X X X
|
||||
// X X X X
|
||||
// 9 7
|
||||
//
|
||||
|
||||
void transformLoadingCross(float64 loading, std::array<QPointF, kPointCount> &points, int &paintPointsCount) {
|
||||
auto moveTo = [](QPointF &point, QPointF &to, float64 ratio) {
|
||||
point = point * (1. - ratio) + to * ratio;
|
||||
};
|
||||
auto moveFrom = [](QPointF &point, QPointF &from, float64 ratio) {
|
||||
point = from * (1. - ratio) + point * ratio;
|
||||
};
|
||||
auto paintPoints = [&points, &paintPointsCount](std::initializer_list<int> &&indices) {
|
||||
auto index = 0;
|
||||
for (auto paintIndex : indices) {
|
||||
points[index++] = points[paintIndex];
|
||||
}
|
||||
paintPointsCount = indices.size();
|
||||
};
|
||||
|
||||
if (loading < 0.125) {
|
||||
auto ratio = loading / 0.125;
|
||||
moveTo(points[6], points[5], ratio);
|
||||
moveTo(points[7], points[8], ratio);
|
||||
} else if (loading < 0.25) {
|
||||
auto ratio = (loading - 0.125) / 0.125;
|
||||
moveTo(points[9], points[8], ratio);
|
||||
moveTo(points[10], points[11], ratio);
|
||||
paintPoints({ 0, 1, 2, 3, 4, 9, 10, 11 });
|
||||
} else if (loading < 0.375) {
|
||||
auto ratio = (loading - 0.25) / 0.125;
|
||||
moveTo(points[0], points[11], ratio);
|
||||
moveTo(points[1], points[2], ratio);
|
||||
paintPoints({ 0, 1, 2, 3, 4, 8 });
|
||||
} else if (loading < 0.5) {
|
||||
auto ratio = (loading - 0.375) / 0.125;
|
||||
moveTo(points[8], points[4], ratio);
|
||||
moveTo(points[11], points[3], ratio);
|
||||
paintPoints({ 3, 4, 8, 11 });
|
||||
} else if (loading < 0.625) {
|
||||
auto ratio = (loading - 0.5) / 0.125;
|
||||
moveFrom(points[8], points[4], ratio);
|
||||
moveFrom(points[11], points[3], ratio);
|
||||
paintPoints({ 3, 4, 8, 11 });
|
||||
} else if (loading < 0.75) {
|
||||
auto ratio = (loading - 0.625) / 0.125;
|
||||
moveFrom(points[6], points[5], ratio);
|
||||
moveFrom(points[7], points[8], ratio);
|
||||
paintPoints({ 3, 4, 5, 6, 7, 11 });
|
||||
} else if (loading < 0.875) {
|
||||
auto ratio = (loading - 0.75) / 0.125;
|
||||
moveFrom(points[9], points[8], ratio);
|
||||
moveFrom(points[10], points[11], ratio);
|
||||
paintPoints({ 3, 4, 5, 6, 7, 8, 9, 10 });
|
||||
} else {
|
||||
auto ratio = (loading - 0.875) / 0.125;
|
||||
moveFrom(points[0], points[11], ratio);
|
||||
moveFrom(points[1], points[2], ratio);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void CrossAnimation::paintStaticLoading(
|
||||
QPainter &p,
|
||||
const style::CrossAnimation &st,
|
||||
style::color color,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
float64 shown) {
|
||||
paint(p, st, color, x, y, outerWidth, shown, kStaticLoadingValue);
|
||||
}
|
||||
|
||||
void CrossAnimation::paint(
|
||||
QPainter &p,
|
||||
const style::CrossAnimation &st,
|
||||
style::color color,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
float64 shown,
|
||||
float64 loading) {
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
const auto stroke = style::ConvertScaleExact(st.stroke);
|
||||
|
||||
const auto deleteScale = shown + st.minScale * (1. - shown);
|
||||
const auto deleteSkip = (deleteScale * st.skip)
|
||||
+ (1. - deleteScale) * (st.size / 2);
|
||||
const auto deleteLeft = 0.
|
||||
+ style::rtlpoint(x + deleteSkip, 0, outerWidth).x();
|
||||
const auto deleteTop = y + deleteSkip + 0.;
|
||||
const auto deleteWidth = st.size - 2 * deleteSkip;
|
||||
const auto deleteHeight = st.size - 2 * deleteSkip;
|
||||
const auto deleteStroke = stroke / M_SQRT2;
|
||||
std::array<QPointF, kPointCount> pathDelete = { {
|
||||
{ deleteLeft, deleteTop + deleteStroke },
|
||||
{ deleteLeft + deleteStroke, deleteTop },
|
||||
{ deleteLeft + (deleteWidth / 2.), deleteTop + (deleteHeight / 2.) - deleteStroke },
|
||||
{ deleteLeft + deleteWidth - deleteStroke, deleteTop },
|
||||
{ deleteLeft + deleteWidth, deleteTop + deleteStroke },
|
||||
{ deleteLeft + (deleteWidth / 2.) + deleteStroke, deleteTop + (deleteHeight / 2.) },
|
||||
{ deleteLeft + deleteWidth, deleteTop + deleteHeight - deleteStroke },
|
||||
{ deleteLeft + deleteWidth - deleteStroke, deleteTop + deleteHeight },
|
||||
{ deleteLeft + (deleteWidth / 2.), deleteTop + (deleteHeight / 2.) + deleteStroke },
|
||||
{ deleteLeft + deleteStroke, deleteTop + deleteHeight },
|
||||
{ deleteLeft, deleteTop + deleteHeight - deleteStroke },
|
||||
{ deleteLeft + (deleteWidth / 2.) - deleteStroke, deleteTop + (deleteHeight / 2.) },
|
||||
} };
|
||||
auto pathDeleteSize = kPointCount;
|
||||
|
||||
const auto staticLoading = (loading == kStaticLoadingValue);
|
||||
auto loadingArcLength = staticLoading ? arc::kFullLength : 0;
|
||||
if (loading > 0.) {
|
||||
transformLoadingCross(loading, pathDelete, pathDeleteSize);
|
||||
|
||||
auto loadingArc = (loading >= 0.5) ? (loading - 1.) : loading;
|
||||
loadingArcLength = qRound(-loadingArc * 2 * arc::kFullLength);
|
||||
}
|
||||
|
||||
if (!staticLoading) {
|
||||
if (shown < 1.) {
|
||||
auto alpha = -(shown - 1.) * M_PI_2;
|
||||
auto cosalpha = cos(alpha);
|
||||
auto sinalpha = sin(alpha);
|
||||
auto shiftx = deleteLeft + (deleteWidth / 2.);
|
||||
auto shifty = deleteTop + (deleteHeight / 2.);
|
||||
for (auto &point : pathDelete) {
|
||||
auto x = point.x() - shiftx;
|
||||
auto y = point.y() - shifty;
|
||||
point.setX(shiftx + x * cosalpha - y * sinalpha);
|
||||
point.setY(shifty + y * cosalpha + x * sinalpha);
|
||||
}
|
||||
}
|
||||
QPainterPath path;
|
||||
path.moveTo(pathDelete[0]);
|
||||
for (int i = 1; i != pathDeleteSize; ++i) {
|
||||
path.lineTo(pathDelete[i]);
|
||||
}
|
||||
path.lineTo(pathDelete[0]);
|
||||
p.fillPath(path, color);
|
||||
}
|
||||
if (loadingArcLength != 0) {
|
||||
auto roundSkip = (st.size * (1 - M_SQRT2) + 2 * M_SQRT2 * deleteSkip + stroke) / 2;
|
||||
auto roundPart = QRectF(x + roundSkip, y + roundSkip, st.size - 2 * roundSkip, st.size - 2 * roundSkip);
|
||||
if (staticLoading) {
|
||||
anim::DrawStaticLoading(p, roundPart, stroke, color);
|
||||
} else {
|
||||
auto loadingArcStart = arc::kQuarterLength / 2;
|
||||
if (shown < 1.) {
|
||||
loadingArcStart -= qRound(-(shown - 1.) * arc::kQuarterLength);
|
||||
}
|
||||
if (loadingArcLength < 0) {
|
||||
loadingArcStart += loadingArcLength;
|
||||
loadingArcLength = -loadingArcLength;
|
||||
}
|
||||
|
||||
p.setBrush(Qt::NoBrush);
|
||||
auto pen = color->p;
|
||||
pen.setWidthF(stroke);
|
||||
pen.setCapStyle(Qt::RoundCap);
|
||||
p.setPen(pen);
|
||||
p.drawArc(roundPart, loadingArcStart, loadingArcLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
37
Telegram/lib_ui/ui/effects/cross_animation.h
Normal file
37
Telegram/lib_ui/ui/effects/cross_animation.h
Normal file
@@ -0,0 +1,37 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class CrossAnimation {
|
||||
public:
|
||||
static void paint(
|
||||
QPainter &p,
|
||||
const style::CrossAnimation &st,
|
||||
style::color color,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
float64 shown,
|
||||
float64 loading = 0.);
|
||||
static void paintStaticLoading(
|
||||
QPainter &p,
|
||||
const style::CrossAnimation &st,
|
||||
style::color color,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
float64 shown);
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
130
Telegram/lib_ui/ui/effects/cross_line.cpp
Normal file
130
Telegram/lib_ui/ui/effects/cross_line.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/cross_line.h"
|
||||
|
||||
#include "ui/painter.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] float64 StrokeWidth(
|
||||
const style::CrossLineAnimation &st) noexcept {
|
||||
return float64(st.stroke)
|
||||
/ (st.strokeDenominator ? st.strokeDenominator : 1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CrossLineAnimation::CrossLineAnimation(
|
||||
const style::CrossLineAnimation &st,
|
||||
bool reversed,
|
||||
float angle)
|
||||
: _st(st)
|
||||
, _reversed(reversed)
|
||||
, _transparentPen(
|
||||
Qt::transparent,
|
||||
StrokeWidth(st),
|
||||
Qt::SolidLine,
|
||||
Qt::RoundCap)
|
||||
, _strokePen(st.fg, StrokeWidth(st), Qt::SolidLine, Qt::RoundCap)
|
||||
, _line(st.startPosition, st.endPosition) {
|
||||
_line.setAngle(angle);
|
||||
}
|
||||
|
||||
void CrossLineAnimation::paint(
|
||||
QPainter &p,
|
||||
QPoint position,
|
||||
float64 progress,
|
||||
std::optional<QColor> colorOverride) {
|
||||
paint(p, position.x(), position.y(), progress, colorOverride);
|
||||
}
|
||||
|
||||
void CrossLineAnimation::paint(
|
||||
QPainter &p,
|
||||
int left,
|
||||
int top,
|
||||
float64 progress,
|
||||
std::optional<QColor> colorOverride) {
|
||||
if (progress == 0.) {
|
||||
if (colorOverride) {
|
||||
_st.icon.paint(p, left, top, _st.icon.width(), *colorOverride);
|
||||
} else {
|
||||
_st.icon.paint(p, left, top, _st.icon.width());
|
||||
}
|
||||
} else if (progress == 1.) {
|
||||
auto &complete = colorOverride
|
||||
? _completeCrossOverride
|
||||
: _completeCross;
|
||||
if (complete.isNull()) {
|
||||
fillFrame(progress, colorOverride);
|
||||
complete = _frame;
|
||||
}
|
||||
p.drawImage(left, top, complete);
|
||||
} else {
|
||||
fillFrame(progress, colorOverride);
|
||||
p.drawImage(left, top, _frame);
|
||||
}
|
||||
}
|
||||
|
||||
void CrossLineAnimation::fillFrame(
|
||||
float64 progress,
|
||||
std::optional<QColor> colorOverride) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
if (_frame.isNull()) {
|
||||
_frame = QImage(
|
||||
_st.icon.size() * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
_frame.setDevicePixelRatio(ratio);
|
||||
}
|
||||
_frame.fill(Qt::transparent);
|
||||
|
||||
auto topLine = _line;
|
||||
topLine.setLength(topLine.length() * progress);
|
||||
auto bottomLine = topLine.translated(0, _strokePen.widthF() + 1);
|
||||
|
||||
auto q = QPainter(&_frame);
|
||||
PainterHighQualityEnabler hq(q);
|
||||
const auto colorize = ((colorOverride && colorOverride->alpha() != 255)
|
||||
|| (!colorOverride && _st.fg->c.alpha() != 255));
|
||||
const auto color = colorize
|
||||
? QColor(255, 255, 255)
|
||||
: colorOverride;
|
||||
if (color) {
|
||||
_st.icon.paint(q, 0, 0, _st.icon.width(), *color);
|
||||
} else {
|
||||
_st.icon.paint(q, 0, 0, _st.icon.width());
|
||||
}
|
||||
|
||||
if (color) {
|
||||
auto pen = _strokePen;
|
||||
pen.setColor(*color);
|
||||
q.setPen(pen);
|
||||
} else {
|
||||
q.setPen(_strokePen);
|
||||
}
|
||||
q.drawLine(_reversed ? topLine : bottomLine);
|
||||
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
q.setPen(_transparentPen);
|
||||
q.drawLine(_reversed ? bottomLine : topLine);
|
||||
q.end();
|
||||
|
||||
if (colorize) {
|
||||
style::colorizeImage(
|
||||
_frame,
|
||||
colorOverride.value_or(_st.fg->c),
|
||||
&_frame);
|
||||
}
|
||||
}
|
||||
|
||||
void CrossLineAnimation::invalidate() {
|
||||
_completeCross = QImage();
|
||||
_completeCrossOverride = QImage();
|
||||
_strokePen = QPen(_st.fg, StrokeWidth(_st), Qt::SolidLine, Qt::RoundCap);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
48
Telegram/lib_ui/ui/effects/cross_line.h
Normal file
48
Telegram/lib_ui/ui/effects/cross_line.h
Normal file
@@ -0,0 +1,48 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class CrossLineAnimation {
|
||||
public:
|
||||
CrossLineAnimation(
|
||||
const style::CrossLineAnimation &st,
|
||||
bool reversed = false,
|
||||
float angle = 315);
|
||||
|
||||
void paint(
|
||||
QPainter &p,
|
||||
QPoint position,
|
||||
float64 progress,
|
||||
std::optional<QColor> colorOverride = std::nullopt);
|
||||
void paint(
|
||||
QPainter &p,
|
||||
int left,
|
||||
int top,
|
||||
float64 progress,
|
||||
std::optional<QColor> colorOverride = std::nullopt);
|
||||
|
||||
void invalidate();
|
||||
|
||||
private:
|
||||
void fillFrame(float64 progress, std::optional<QColor> colorOverride);
|
||||
|
||||
const style::CrossLineAnimation &_st;
|
||||
const bool _reversed;
|
||||
const QPen _transparentPen;
|
||||
QPen _strokePen;
|
||||
QLineF _line;
|
||||
QImage _frame;
|
||||
QImage _completeCross;
|
||||
QImage _completeCrossOverride;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
161
Telegram/lib_ui/ui/effects/fade_animation.cpp
Normal file
161
Telegram/lib_ui/ui/effects/fade_animation.cpp
Normal file
@@ -0,0 +1,161 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/fade_animation.h"
|
||||
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/painter.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr int kWideScale = 5;
|
||||
|
||||
} // namespace
|
||||
|
||||
FadeAnimation::FadeAnimation(RpWidget *widget, float64 scale)
|
||||
: _widget(widget)
|
||||
, _scale(scale) {
|
||||
}
|
||||
|
||||
bool FadeAnimation::paint(QPainter &p) {
|
||||
if (_cache.isNull()) return false;
|
||||
|
||||
const auto cache = _cache;
|
||||
auto opacity = _animation.value(_visible ? 1. : 0.);
|
||||
p.setOpacity(opacity);
|
||||
if (_scale < 1.) {
|
||||
PainterHighQualityEnabler hq(p);
|
||||
auto targetRect = QRect(
|
||||
(1 - kWideScale) / 2 * _size.width(),
|
||||
(1 - kWideScale) / 2 * _size.height(),
|
||||
kWideScale * _size.width(),
|
||||
kWideScale * _size.height());
|
||||
auto scale = opacity + (1. - opacity) * _scale;
|
||||
auto shownWidth = anim::interpolate(
|
||||
(1 - kWideScale) / 2 * _size.width(),
|
||||
0,
|
||||
scale);
|
||||
auto shownHeight = anim::interpolate(
|
||||
(1 - kWideScale) / 2 * _size.height(),
|
||||
0,
|
||||
scale);
|
||||
auto margins = QMargins(
|
||||
shownWidth,
|
||||
shownHeight,
|
||||
shownWidth,
|
||||
shownHeight);
|
||||
p.drawPixmap(targetRect.marginsAdded(margins), cache);
|
||||
} else {
|
||||
p.drawPixmap(0, 0, cache);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void FadeAnimation::refreshCache() {
|
||||
if (!_cache.isNull()) {
|
||||
_cache = QPixmap();
|
||||
_cache = grabContent();
|
||||
Assert(!_cache.isNull());
|
||||
}
|
||||
}
|
||||
|
||||
QPixmap FadeAnimation::grabContent() {
|
||||
SendPendingMoveResizeEvents(_widget);
|
||||
_size = _widget->size();
|
||||
const auto pixelRatio = style::DevicePixelRatio();
|
||||
if (_size.isEmpty()) {
|
||||
auto image = QImage(
|
||||
pixelRatio,
|
||||
pixelRatio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.fill(Qt::transparent);
|
||||
return PixmapFromImage(std::move(image));
|
||||
}
|
||||
auto widgetContent = GrabWidget(_widget);
|
||||
if (_scale < 1.) {
|
||||
auto result = QImage(kWideScale * _size * pixelRatio, QImage::Format_ARGB32_Premultiplied);
|
||||
result.setDevicePixelRatio(pixelRatio);
|
||||
result.fill(Qt::transparent);
|
||||
{
|
||||
Painter p(&result);
|
||||
p.drawPixmap((kWideScale - 1) / 2 * _size.width(), (kWideScale - 1) / 2 * _size.height(), widgetContent);
|
||||
}
|
||||
return PixmapFromImage(std::move(result));
|
||||
}
|
||||
return widgetContent;
|
||||
}
|
||||
|
||||
void FadeAnimation::setFinishedCallback(FinishedCallback &&callback) {
|
||||
_finishedCallback = std::move(callback);
|
||||
}
|
||||
|
||||
void FadeAnimation::setUpdatedCallback(UpdatedCallback &&callback) {
|
||||
_updatedCallback = std::move(callback);
|
||||
}
|
||||
|
||||
void FadeAnimation::show() {
|
||||
_visible = true;
|
||||
stopAnimation();
|
||||
}
|
||||
|
||||
void FadeAnimation::hide() {
|
||||
_visible = false;
|
||||
stopAnimation();
|
||||
}
|
||||
|
||||
void FadeAnimation::stopAnimation() {
|
||||
_animation.stop();
|
||||
if (!_cache.isNull()) {
|
||||
_cache = QPixmap();
|
||||
if (_finishedCallback) {
|
||||
_finishedCallback();
|
||||
}
|
||||
}
|
||||
if (_visible == _widget->isHidden()) {
|
||||
_widget->setVisible(_visible);
|
||||
}
|
||||
}
|
||||
|
||||
void FadeAnimation::fadeIn(int duration) {
|
||||
if (_visible) return;
|
||||
|
||||
_visible = true;
|
||||
startAnimation(duration);
|
||||
}
|
||||
|
||||
void FadeAnimation::fadeOut(int duration) {
|
||||
if (!_visible) return;
|
||||
|
||||
_visible = false;
|
||||
startAnimation(duration);
|
||||
}
|
||||
|
||||
void FadeAnimation::startAnimation(int duration) {
|
||||
if (_cache.isNull()) {
|
||||
_cache = grabContent();
|
||||
Assert(!_cache.isNull());
|
||||
}
|
||||
auto from = _visible ? 0. : 1.;
|
||||
auto to = _visible ? 1. : 0.;
|
||||
_animation.start([this]() { updateCallback(); }, from, to, duration);
|
||||
updateCallback();
|
||||
if (_widget->isHidden()) {
|
||||
_widget->show();
|
||||
}
|
||||
}
|
||||
|
||||
void FadeAnimation::updateCallback() {
|
||||
_widget->update();
|
||||
if (_updatedCallback) {
|
||||
_updatedCallback(_animation.value(_visible ? 1. : 0.));
|
||||
}
|
||||
if (!_animation.animating()) {
|
||||
stopAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
65
Telegram/lib_ui/ui/effects/fade_animation.h
Normal file
65
Telegram/lib_ui/ui/effects/fade_animation.h
Normal file
@@ -0,0 +1,65 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class FadeAnimation {
|
||||
public:
|
||||
FadeAnimation(RpWidget *widget, float64 scale = 1.);
|
||||
|
||||
bool paint(QPainter &p);
|
||||
void refreshCache();
|
||||
|
||||
using FinishedCallback = Fn<void()>;
|
||||
void setFinishedCallback(FinishedCallback &&callback);
|
||||
|
||||
using UpdatedCallback = Fn<void(float64)>;
|
||||
void setUpdatedCallback(UpdatedCallback &&callback);
|
||||
|
||||
void show();
|
||||
void hide();
|
||||
|
||||
void fadeIn(int duration);
|
||||
void fadeOut(int duration);
|
||||
|
||||
void finish() {
|
||||
stopAnimation();
|
||||
}
|
||||
|
||||
bool animating() const {
|
||||
return _animation.animating();
|
||||
}
|
||||
bool visible() const {
|
||||
return _visible;
|
||||
}
|
||||
|
||||
private:
|
||||
void startAnimation(int duration);
|
||||
void stopAnimation();
|
||||
|
||||
void updateCallback();
|
||||
QPixmap grabContent();
|
||||
|
||||
RpWidget *_widget = nullptr;
|
||||
float64 _scale = 1.;
|
||||
|
||||
Ui::Animations::Simple _animation;
|
||||
QSize _size;
|
||||
QPixmap _cache;
|
||||
bool _visible = false;
|
||||
|
||||
FinishedCallback _finishedCallback;
|
||||
UpdatedCallback _updatedCallback;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
79
Telegram/lib_ui/ui/effects/frame_generator.cpp
Normal file
79
Telegram/lib_ui/ui/effects/frame_generator.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/frame_generator.h"
|
||||
|
||||
#include "ui/image/image_prepare.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
ImageFrameGenerator::ImageFrameGenerator(const QByteArray &bytes)
|
||||
: _bytes(bytes) {
|
||||
}
|
||||
|
||||
ImageFrameGenerator::ImageFrameGenerator(const QImage &image)
|
||||
: _image(image) {
|
||||
}
|
||||
|
||||
int ImageFrameGenerator::count() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
double ImageFrameGenerator::rate() {
|
||||
return 1.;
|
||||
}
|
||||
|
||||
FrameGenerator::Frame ImageFrameGenerator::renderNext(
|
||||
QImage storage,
|
||||
QSize size,
|
||||
Qt::AspectRatioMode mode) {
|
||||
return renderCurrent(std::move(storage), size, mode);
|
||||
}
|
||||
|
||||
FrameGenerator::Frame ImageFrameGenerator::renderCurrent(
|
||||
QImage storage,
|
||||
QSize size,
|
||||
Qt::AspectRatioMode mode) {
|
||||
if (_image.isNull() && !_bytes.isEmpty()) {
|
||||
_image = Images::Read({
|
||||
.content = _bytes,
|
||||
}).image;
|
||||
_bytes = QByteArray();
|
||||
}
|
||||
if (_image.isNull()) {
|
||||
return {};
|
||||
}
|
||||
auto scaled = _image.scaled(
|
||||
size,
|
||||
mode,
|
||||
Qt::SmoothTransformation
|
||||
).convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
if (scaled.size() == size) {
|
||||
return { .image = std::move(scaled) };
|
||||
}
|
||||
auto result = QImage(size, QImage::Format_ARGB32_Premultiplied);
|
||||
result.fill(Qt::transparent);
|
||||
|
||||
const auto skipx = (size.width() - scaled.width()) / 2;
|
||||
const auto skipy = (size.height() - scaled.height()) / 2;
|
||||
const auto srcPerLine = scaled.bytesPerLine();
|
||||
const auto dstPerLine = result.bytesPerLine();
|
||||
const auto lineBytes = scaled.width() * 4;
|
||||
auto src = scaled.constBits();
|
||||
auto dst = result.bits() + (skipx * 4) + (skipy * srcPerLine);
|
||||
for (auto y = 0, height = scaled.height(); y != height; ++y) {
|
||||
memcpy(dst, src, lineBytes);
|
||||
src += srcPerLine;
|
||||
dst += dstPerLine;
|
||||
}
|
||||
|
||||
return { .image = std::move(result), .last = true };
|
||||
}
|
||||
|
||||
void ImageFrameGenerator::jumpToStart() {
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
69
Telegram/lib_ui/ui/effects/frame_generator.h
Normal file
69
Telegram/lib_ui/ui/effects/frame_generator.h
Normal file
@@ -0,0 +1,69 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include <QtGui/QImage>
|
||||
|
||||
#include <crl/crl_time.h>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class FrameGenerator {
|
||||
public:
|
||||
virtual ~FrameGenerator() = default;
|
||||
|
||||
// 0 means unknown.
|
||||
[[nodiscard]] virtual int count() = 0;
|
||||
|
||||
// 0. means unknown.
|
||||
[[nodiscard]] virtual double rate() = 0;
|
||||
|
||||
struct Frame {
|
||||
crl::time duration = 0;
|
||||
QImage image;
|
||||
bool last = false;
|
||||
};
|
||||
[[nodiscard]] virtual Frame renderNext(
|
||||
QImage storage,
|
||||
QSize size,
|
||||
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) = 0;
|
||||
[[nodiscard]] virtual Frame renderCurrent(
|
||||
QImage storage,
|
||||
QSize size,
|
||||
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) = 0;
|
||||
|
||||
virtual void jumpToStart() = 0;
|
||||
|
||||
};
|
||||
|
||||
class ImageFrameGenerator final : public Ui::FrameGenerator {
|
||||
public:
|
||||
explicit ImageFrameGenerator(const QByteArray &bytes);
|
||||
explicit ImageFrameGenerator(const QImage &image);
|
||||
|
||||
int count() override;
|
||||
double rate() override;
|
||||
Frame renderNext(
|
||||
QImage storage,
|
||||
QSize size,
|
||||
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override;
|
||||
Frame renderCurrent(
|
||||
QImage storage,
|
||||
QSize size,
|
||||
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override;
|
||||
void jumpToStart() override;
|
||||
|
||||
private:
|
||||
QByteArray _bytes;
|
||||
QImage _image;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] bool GoodStorageForFrame(const QImage &storage, QSize size);
|
||||
[[nodiscard]] QImage CreateFrameStorage(QSize size);
|
||||
|
||||
} // namespace Ui
|
||||
30
Telegram/lib_ui/ui/effects/gradient.cpp
Normal file
30
Telegram/lib_ui/ui/effects/gradient.cpp
Normal file
@@ -0,0 +1,30 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/gradient.h"
|
||||
|
||||
namespace anim {
|
||||
|
||||
QColor gradient_color_at(const QGradientStops &stops, float64 ratio) {
|
||||
for (auto i = 1; i < stops.size(); i++) {
|
||||
const auto currentPoint = stops[i].first;
|
||||
const auto previousPoint = stops[i - 1].first;
|
||||
|
||||
if ((ratio <= currentPoint) && (ratio >= previousPoint)) {
|
||||
return anim::color(
|
||||
stops[i - 1].second,
|
||||
stops[i].second,
|
||||
(ratio - previousPoint) / (currentPoint - previousPoint));
|
||||
}
|
||||
}
|
||||
return QColor();
|
||||
}
|
||||
|
||||
QColor gradient_color_at(const QGradient &gradient, float64 ratio) {
|
||||
return gradient_color_at(gradient.stops(), ratio);
|
||||
}
|
||||
|
||||
} // namespace anim
|
||||
257
Telegram/lib_ui/ui/effects/gradient.h
Normal file
257
Telegram/lib_ui/ui/effects/gradient.h
Normal file
@@ -0,0 +1,257 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "base/flat_map.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
|
||||
#include <QtGui/QLinearGradient>
|
||||
#include <QtGui/QRadialGradient>
|
||||
|
||||
namespace anim {
|
||||
|
||||
[[nodiscard]] QColor gradient_color_at(
|
||||
const QGradientStops &stops,
|
||||
float64 ratio);
|
||||
|
||||
[[nodiscard]] QColor gradient_color_at(
|
||||
const QGradient &gradient,
|
||||
float64 ratio);
|
||||
|
||||
struct gradient_colors {
|
||||
explicit gradient_colors(QColor color) {
|
||||
stops.push_back({ 0., color });
|
||||
stops.push_back({ 1., color });
|
||||
}
|
||||
explicit gradient_colors(std::vector<QColor> colors) {
|
||||
if (colors.size() == 1) {
|
||||
gradient_colors(colors.front());
|
||||
return;
|
||||
}
|
||||
const auto last = float(colors.size() - 1);
|
||||
for (auto i = 0; i < colors.size(); i++) {
|
||||
stops.push_back({ i / last, std::move(colors[i]) });
|
||||
}
|
||||
}
|
||||
explicit gradient_colors(QGradientStops colors)
|
||||
: stops(std::move(colors)) {
|
||||
}
|
||||
|
||||
QGradientStops stops;
|
||||
};
|
||||
|
||||
namespace details {
|
||||
|
||||
template <typename T, typename Derived>
|
||||
class gradients {
|
||||
public:
|
||||
gradients() = default;
|
||||
gradients(base::flat_map<T, std::vector<QColor>> colors) {
|
||||
Expects(!colors.empty());
|
||||
|
||||
for (const auto &[key, value] : colors) {
|
||||
auto c = gradient_colors(std::move(value));
|
||||
_gradients.emplace(key, gradient_with_stops(std::move(c.stops)));
|
||||
}
|
||||
}
|
||||
gradients(base::flat_map<T, gradient_colors> colors) {
|
||||
Expects(!colors.empty());
|
||||
|
||||
for (const auto &[key, c] : colors) {
|
||||
_gradients.emplace(key, gradient_with_stops(std::move(c.stops)));
|
||||
}
|
||||
}
|
||||
|
||||
QGradient gradient(T state1, T state2, float64 b_ratio) const {
|
||||
Expects(!_gradients.empty());
|
||||
|
||||
if (b_ratio == 0.) {
|
||||
return _gradients.find(state1)->second;
|
||||
} else if (b_ratio == 1.) {
|
||||
return _gradients.find(state2)->second;
|
||||
}
|
||||
|
||||
auto gradient = empty_gradient();
|
||||
const auto gradient1 = _gradients.find(state1);
|
||||
const auto gradient2 = _gradients.find(state2);
|
||||
|
||||
Assert(gradient1 != end(_gradients));
|
||||
Assert(gradient2 != end(_gradients));
|
||||
|
||||
const auto stopsFrom = gradient1->second.stops();
|
||||
const auto stopsTo = gradient2->second.stops();
|
||||
|
||||
if ((stopsFrom.size() == stopsTo.size())
|
||||
&& ranges::equal(
|
||||
stopsFrom,
|
||||
stopsTo,
|
||||
ranges::equal_to(),
|
||||
&QGradientStop::first,
|
||||
&QGradientStop::first)) {
|
||||
|
||||
const auto size = stopsFrom.size();
|
||||
const auto &p = b_ratio;
|
||||
for (auto i = 0; i < size; i++) {
|
||||
auto c = color(stopsFrom[i].second, stopsTo[i].second, p);
|
||||
gradient.setColorAt(stopsTo[i].first, std::move(c));
|
||||
}
|
||||
return gradient;
|
||||
}
|
||||
|
||||
const auto invert = (stopsFrom.size() > stopsTo.size());
|
||||
if (invert) {
|
||||
b_ratio = 1. - b_ratio;
|
||||
}
|
||||
|
||||
const auto &stops1 = invert ? stopsTo : stopsFrom;
|
||||
const auto &stops2 = invert ? stopsFrom : stopsTo;
|
||||
|
||||
const auto size1 = stops1.size();
|
||||
const auto size2 = stops2.size();
|
||||
|
||||
for (auto i = 0; i < size1; i++) {
|
||||
const auto point1 = stops1[i].first;
|
||||
const auto previousPoint1 = i ? stops1[i - 1].first : -1.;
|
||||
|
||||
for (auto n = 0; n < size2; n++) {
|
||||
const auto point2 = stops2[n].first;
|
||||
|
||||
if ((point2 <= previousPoint1) || (point2 > point1)) {
|
||||
continue;
|
||||
}
|
||||
const auto color2 = stops2[n].second;
|
||||
QColor result;
|
||||
if (point2 < point1) {
|
||||
const auto pointRatio2 = (point2 - previousPoint1)
|
||||
/ (point1 - previousPoint1);
|
||||
const auto color1 = color(
|
||||
stops1[i - 1].second,
|
||||
stops1[i].second,
|
||||
pointRatio2);
|
||||
|
||||
result = color(color1, color2, b_ratio);
|
||||
} else {
|
||||
// point2 == point1
|
||||
result = color(stops1[i].second, color2, b_ratio);
|
||||
}
|
||||
gradient.setColorAt(point2, std::move(result));
|
||||
}
|
||||
}
|
||||
return gradient;
|
||||
}
|
||||
|
||||
protected:
|
||||
void cache_gradients() {
|
||||
auto copy = std::move(_gradients);
|
||||
for (const auto &[key, value] : copy) {
|
||||
_gradients.emplace(key, gradient_with_stops(value.stops()));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
QGradient empty_gradient() const {
|
||||
return static_cast<const Derived*>(this)->empty_gradient();
|
||||
}
|
||||
QGradient gradient_with_stops(QGradientStops stops) const {
|
||||
auto gradient = empty_gradient();
|
||||
gradient.setStops(std::move(stops));
|
||||
return gradient;
|
||||
}
|
||||
|
||||
base::flat_map<T, QGradient> _gradients;
|
||||
|
||||
};
|
||||
|
||||
} // namespace details
|
||||
|
||||
template <typename T>
|
||||
class linear_gradients final
|
||||
: public details::gradients<T, linear_gradients<T>> {
|
||||
using parent = details::gradients<T, linear_gradients<T>>;
|
||||
|
||||
public:
|
||||
linear_gradients() = default;
|
||||
linear_gradients(
|
||||
base::flat_map<T, std::vector<QColor>> colors,
|
||||
QPointF point1,
|
||||
QPointF point2)
|
||||
: parent(std::move(colors)) {
|
||||
set_points(point1, point2);
|
||||
}
|
||||
linear_gradients(
|
||||
base::flat_map<T, gradient_colors> colors,
|
||||
QPointF point1,
|
||||
QPointF point2)
|
||||
: parent(std::move(colors)) {
|
||||
set_points(point1, point2);
|
||||
}
|
||||
|
||||
void set_points(QPointF point1, QPointF point2) {
|
||||
if (_point1 == point1 && _point2 == point2) {
|
||||
return;
|
||||
}
|
||||
_point1 = point1;
|
||||
_point2 = point2;
|
||||
parent::cache_gradients();
|
||||
}
|
||||
|
||||
private:
|
||||
friend class details::gradients<T, linear_gradients<T>>;
|
||||
|
||||
QGradient empty_gradient() const {
|
||||
return QLinearGradient(_point1, _point2);
|
||||
}
|
||||
|
||||
QPointF _point1;
|
||||
QPointF _point2;
|
||||
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class radial_gradients final
|
||||
: public details::gradients<T, radial_gradients<T>> {
|
||||
using parent = details::gradients<T, radial_gradients<T>>;
|
||||
|
||||
public:
|
||||
radial_gradients() = default;
|
||||
radial_gradients(
|
||||
base::flat_map<T, std::vector<QColor>> colors,
|
||||
QPointF center,
|
||||
float radius)
|
||||
: parent(std::move(colors)) {
|
||||
set_points(center, radius);
|
||||
}
|
||||
radial_gradients(
|
||||
base::flat_map<T, gradient_colors> colors,
|
||||
QPointF center,
|
||||
float radius)
|
||||
: parent(std::move(colors)) {
|
||||
set_points(center, radius);
|
||||
}
|
||||
|
||||
void set_points(QPointF center, float radius) {
|
||||
if (_center == center && _radius == radius) {
|
||||
return;
|
||||
}
|
||||
_center = center;
|
||||
_radius = radius;
|
||||
parent::cache_gradients();
|
||||
}
|
||||
|
||||
private:
|
||||
friend class details::gradients<T, radial_gradients<T>>;
|
||||
|
||||
QGradient empty_gradient() const {
|
||||
return QRadialGradient(_center, _radius);
|
||||
}
|
||||
|
||||
QPointF _center;
|
||||
float _radius = 0.;
|
||||
|
||||
};
|
||||
|
||||
} // namespace anim
|
||||
272
Telegram/lib_ui/ui/effects/numbers_animation.cpp
Normal file
272
Telegram/lib_ui/ui/effects/numbers_animation.cpp
Normal file
@@ -0,0 +1,272 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/numbers_animation.h"
|
||||
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <QtGui/QPainter>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
NumbersAnimation::NumbersAnimation(
|
||||
const style::font &font,
|
||||
Fn<void()> animationCallback)
|
||||
: _font(font)
|
||||
, _duration(st::slideWrapDuration)
|
||||
, _animationCallback(std::move(animationCallback)) {
|
||||
for (auto ch = '0'; ch != '9'; ++ch) {
|
||||
accumulate_max(_digitWidth, _font->width(ch));
|
||||
}
|
||||
}
|
||||
|
||||
void NumbersAnimation::setDuration(int duration) {
|
||||
_duration = duration;
|
||||
}
|
||||
|
||||
void NumbersAnimation::setDisabledMonospace(bool value) {
|
||||
_disabledMonospace = value;
|
||||
}
|
||||
|
||||
void NumbersAnimation::setText(const QString &text, int value) {
|
||||
if (_a_ready.animating()) {
|
||||
_delayedText = text;
|
||||
_delayedValue = value;
|
||||
} else {
|
||||
realSetText(text, value);
|
||||
}
|
||||
}
|
||||
|
||||
void NumbersAnimation::animationCallback() {
|
||||
if (_animationCallback) {
|
||||
_animationCallback();
|
||||
}
|
||||
if (_widthChangedCallback) {
|
||||
_widthChangedCallback();
|
||||
}
|
||||
if (!_a_ready.animating() && !_delayedText.isEmpty()) {
|
||||
setText(_delayedText, _delayedValue);
|
||||
}
|
||||
}
|
||||
|
||||
void NumbersAnimation::realSetText(QString text, int value) {
|
||||
_delayedText = QString();
|
||||
_delayedValue = 0;
|
||||
|
||||
_growing = (value > _value);
|
||||
_value = value;
|
||||
|
||||
auto newSize = text.size();
|
||||
while (_digits.size() < newSize) {
|
||||
_digits.push_front(Digit());
|
||||
}
|
||||
while (_digits.size() > newSize && !_digits.front().to.unicode()) {
|
||||
_digits.pop_front();
|
||||
}
|
||||
auto animating = false;
|
||||
auto toFullWidth = 0;
|
||||
auto bothFullWidth = 0;
|
||||
for (auto i = 0, size = int(_digits.size()); i != size; ++i) {
|
||||
auto &digit = _digits[i];
|
||||
const auto from = digit.from = digit.to;
|
||||
digit.fromWidth = digit.toWidth;
|
||||
const auto to = digit.to = (newSize + i < size)
|
||||
? QChar(0)
|
||||
: text[newSize + i - size];
|
||||
digit.toWidth = to.unicode() ? _font->width(to) : 0;
|
||||
if (from != to) {
|
||||
animating = true;
|
||||
}
|
||||
const auto toCharWidth = (!_disabledMonospace || to.isDigit())
|
||||
? _digitWidth
|
||||
: digit.toWidth;
|
||||
const auto fromCharWidth = (!_disabledMonospace || from.isDigit())
|
||||
? _digitWidth
|
||||
: digit.fromWidth;
|
||||
const auto charWidth = std::max(toCharWidth, fromCharWidth);
|
||||
bothFullWidth += charWidth;
|
||||
if (to.unicode()) {
|
||||
toFullWidth += charWidth;
|
||||
}
|
||||
}
|
||||
_fromWidth = _toWidth;
|
||||
_toWidth = toFullWidth;
|
||||
_bothWidth = bothFullWidth;
|
||||
if (animating) {
|
||||
_a_ready.start(
|
||||
[this] { animationCallback(); },
|
||||
0.,
|
||||
1.,
|
||||
_duration);
|
||||
}
|
||||
}
|
||||
|
||||
int NumbersAnimation::countWidth() const {
|
||||
return anim::interpolate(
|
||||
_fromWidth,
|
||||
_toWidth,
|
||||
anim::easeOutCirc(1., _a_ready.value(1.)));
|
||||
}
|
||||
|
||||
int NumbersAnimation::maxWidth() const {
|
||||
return std::max(_fromWidth, _toWidth);
|
||||
}
|
||||
|
||||
void NumbersAnimation::finishAnimating() {
|
||||
auto width = countWidth();
|
||||
_a_ready.stop();
|
||||
if (_widthChangedCallback && countWidth() != width) {
|
||||
_widthChangedCallback();
|
||||
}
|
||||
if (!_delayedText.isEmpty()) {
|
||||
setText(_delayedText, _delayedValue);
|
||||
}
|
||||
}
|
||||
|
||||
void NumbersAnimation::paint(QPainter &p, int x, int y, int outerWidth) {
|
||||
auto digitsCount = _digits.size();
|
||||
if (!digitsCount) return;
|
||||
|
||||
auto progress = anim::easeOutCirc(1., _a_ready.value(1.));
|
||||
auto width = anim::interpolate(_fromWidth, _toWidth, progress);
|
||||
|
||||
const auto initial = p.opacity();
|
||||
QString singleChar('0');
|
||||
if (style::RightToLeft()) x = outerWidth - x - width;
|
||||
x += width - _bothWidth;
|
||||
auto fromTop = anim::interpolate(0, _font->height, progress) * (_growing ? 1 : -1);
|
||||
auto toTop = anim::interpolate(_font->height, 0, progress) * (_growing ? -1 : 1);
|
||||
for (auto i = 0; i != digitsCount; ++i) {
|
||||
auto &digit = _digits[i];
|
||||
auto from = digit.from;
|
||||
auto to = digit.to;
|
||||
const auto toCharWidth = (!_disabledMonospace || to.isDigit())
|
||||
? _digitWidth
|
||||
: digit.toWidth;
|
||||
const auto fromCharWidth = (!_disabledMonospace || from.isDigit())
|
||||
? _digitWidth
|
||||
: digit.fromWidth;
|
||||
if (from == to) {
|
||||
p.setOpacity(initial);
|
||||
singleChar[0] = from;
|
||||
p.drawText(x + (toCharWidth - digit.fromWidth) / 2, y + _font->ascent, singleChar);
|
||||
} else {
|
||||
if (from.unicode()) {
|
||||
p.setOpacity(initial * (1. - progress));
|
||||
singleChar[0] = from;
|
||||
p.drawText(x + (fromCharWidth - digit.fromWidth) / 2, y + fromTop + _font->ascent, singleChar);
|
||||
}
|
||||
if (to.unicode()) {
|
||||
p.setOpacity(initial * progress);
|
||||
singleChar[0] = to;
|
||||
p.drawText(x + (toCharWidth - digit.toWidth) / 2, y + toTop + _font->ascent, singleChar);
|
||||
}
|
||||
}
|
||||
x += std::max(toCharWidth, fromCharWidth);
|
||||
}
|
||||
p.setOpacity(initial);
|
||||
}
|
||||
|
||||
LabelWithNumbers::LabelWithNumbers(
|
||||
QWidget *parent,
|
||||
const style::FlatLabel &st,
|
||||
int textTop,
|
||||
const StringWithNumbers &value)
|
||||
: RpWidget(parent)
|
||||
, _st(st)
|
||||
, _textTop(textTop)
|
||||
, _before(GetBefore(value))
|
||||
, _after(GetAfter(value))
|
||||
, _numbers(_st.style.font, [=] { update(); })
|
||||
, _beforeWidth(_st.style.font->width(_before))
|
||||
, _afterWidth(st.style.font->width(_after)) {
|
||||
Expects((value.offset < 0) == (value.length == 0));
|
||||
|
||||
_numbers.setWidthChangedCallback([=] {
|
||||
updateNaturalWidth();
|
||||
});
|
||||
|
||||
const auto numbers = GetNumbers(value);
|
||||
_numbers.setText(numbers, numbers.toInt());
|
||||
_numbers.finishAnimating();
|
||||
}
|
||||
|
||||
QString LabelWithNumbers::GetBefore(const StringWithNumbers &value) {
|
||||
return value.text.mid(0, value.offset);
|
||||
}
|
||||
|
||||
QString LabelWithNumbers::GetAfter(const StringWithNumbers &value) {
|
||||
return (value.offset >= 0)
|
||||
? value.text.mid(value.offset + value.length)
|
||||
: QString();
|
||||
}
|
||||
|
||||
QString LabelWithNumbers::GetNumbers(const StringWithNumbers &value) {
|
||||
return (value.offset >= 0)
|
||||
? value.text.mid(value.offset, value.length)
|
||||
: QString();
|
||||
}
|
||||
|
||||
void LabelWithNumbers::setValue(const StringWithNumbers &value) {
|
||||
_before = GetBefore(value);
|
||||
_after = GetAfter(value);
|
||||
const auto numbers = GetNumbers(value);
|
||||
_numbers.setText(numbers, numbers.toInt());
|
||||
|
||||
const auto oldBeforeWidth = std::exchange(
|
||||
_beforeWidth,
|
||||
_st.style.font->width(_before));
|
||||
_beforeWidthAnimation.start(
|
||||
[this] { update(); },
|
||||
oldBeforeWidth,
|
||||
_beforeWidth,
|
||||
st::slideWrapDuration,
|
||||
anim::easeOutCirc);
|
||||
|
||||
_afterWidth = _st.style.font->width(_after);
|
||||
|
||||
updateNaturalWidth();
|
||||
}
|
||||
|
||||
void LabelWithNumbers::updateNaturalWidth() {
|
||||
setNaturalWidth(_beforeWidth + _numbers.maxWidth() + _afterWidth);
|
||||
}
|
||||
|
||||
void LabelWithNumbers::finishAnimating() {
|
||||
_beforeWidthAnimation.stop();
|
||||
_numbers.finishAnimating();
|
||||
update();
|
||||
}
|
||||
|
||||
void LabelWithNumbers::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
const auto beforeWidth = _beforeWidthAnimation.value(_beforeWidth);
|
||||
|
||||
p.setFont(_st.style.font);
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.setPen(_st.textFg);
|
||||
auto left = 0;
|
||||
const auto outerWidth = width();
|
||||
|
||||
p.setClipRect(0, 0, left + beforeWidth, height());
|
||||
p.drawTextLeft(left, _textTop, outerWidth, _before, _beforeWidth);
|
||||
left += beforeWidth;
|
||||
p.setClipping(false);
|
||||
|
||||
_numbers.paint(p, left, _textTop, outerWidth);
|
||||
left += _numbers.countWidth();
|
||||
|
||||
const auto availableWidth = outerWidth - left;
|
||||
const auto text = (availableWidth < _afterWidth)
|
||||
? _st.style.font->elided(_after, availableWidth)
|
||||
: _after;
|
||||
const auto textWidth = (availableWidth < _afterWidth) ? -1 : _afterWidth;
|
||||
p.drawTextLeft(left, _textTop, outerWidth, text, textWidth);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
114
Telegram/lib_ui/ui/effects/numbers_animation.h
Normal file
114
Telegram/lib_ui/ui/effects/numbers_animation.h
Normal file
@@ -0,0 +1,114 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
namespace style {
|
||||
struct FlatLabel;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class NumbersAnimation {
|
||||
public:
|
||||
NumbersAnimation(
|
||||
const style::font &font,
|
||||
Fn<void()> animationCallback);
|
||||
|
||||
void setWidthChangedCallback(Fn<void()> callback) {
|
||||
_widthChangedCallback = std::move(callback);
|
||||
}
|
||||
void setText(const QString &text, int value);
|
||||
void setDuration(int duration);
|
||||
void setDisabledMonospace(bool value);
|
||||
void finishAnimating();
|
||||
|
||||
void paint(QPainter &p, int x, int y, int outerWidth);
|
||||
int countWidth() const;
|
||||
int maxWidth() const;
|
||||
|
||||
private:
|
||||
struct Digit {
|
||||
QChar from = QChar(0);
|
||||
QChar to = QChar(0);
|
||||
int fromWidth = 0;
|
||||
int toWidth = 0;
|
||||
};
|
||||
|
||||
void animationCallback();
|
||||
void realSetText(QString text, int value);
|
||||
|
||||
const style::font &_font;
|
||||
|
||||
int _duration;
|
||||
|
||||
QList<Digit> _digits;
|
||||
int _digitWidth = 0;
|
||||
|
||||
int _fromWidth = 0;
|
||||
int _toWidth = 0;
|
||||
int _bothWidth = 0;
|
||||
|
||||
Ui::Animations::Simple _a_ready;
|
||||
QString _delayedText;
|
||||
int _delayedValue = 0;
|
||||
|
||||
int _value = 0;
|
||||
bool _growing = false;
|
||||
|
||||
bool _disabledMonospace = false;
|
||||
|
||||
Fn<void()> _animationCallback;
|
||||
Fn<void()> _widthChangedCallback;
|
||||
|
||||
};
|
||||
|
||||
struct StringWithNumbers {
|
||||
static StringWithNumbers FromString(const QString &text) {
|
||||
return { text };
|
||||
}
|
||||
|
||||
QString text;
|
||||
int offset = -1;
|
||||
int length = 0;
|
||||
};
|
||||
|
||||
class LabelWithNumbers : public Ui::RpWidget {
|
||||
public:
|
||||
LabelWithNumbers(
|
||||
QWidget *parent,
|
||||
const style::FlatLabel &st,
|
||||
int textTop,
|
||||
const StringWithNumbers &value);
|
||||
|
||||
void setValue(const StringWithNumbers &value);
|
||||
void finishAnimating();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
[[nodiscard]] static QString GetBefore(const StringWithNumbers &value);
|
||||
[[nodiscard]] static QString GetAfter(const StringWithNumbers &value);
|
||||
[[nodiscard]] static QString GetNumbers(const StringWithNumbers &value);
|
||||
|
||||
void updateNaturalWidth();
|
||||
|
||||
const style::FlatLabel &_st;
|
||||
int _textTop;
|
||||
QString _before;
|
||||
QString _after;
|
||||
NumbersAnimation _numbers;
|
||||
int _beforeWidth = 0;
|
||||
int _afterWidth = 0;
|
||||
Ui::Animations::Simple _beforeWidthAnimation;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
539
Telegram/lib_ui/ui/effects/panel_animation.cpp
Normal file
539
Telegram/lib_ui/ui/effects/panel_animation.cpp
Normal file
@@ -0,0 +1,539 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/panel_animation.h"
|
||||
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/ui_utility.h"
|
||||
|
||||
#include <QtGui/QPainter>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
void RoundShadowAnimation::start(int frameWidth, int frameHeight, float64 devicePixelRatio) {
|
||||
Expects(!started());
|
||||
|
||||
_frameWidth = frameWidth;
|
||||
_frameHeight = frameHeight;
|
||||
_frame = QImage(_frameWidth, _frameHeight, QImage::Format_ARGB32_Premultiplied);
|
||||
_frame.setDevicePixelRatio(devicePixelRatio);
|
||||
_frameIntsPerLine = (_frame.bytesPerLine() >> 2);
|
||||
_frameInts = reinterpret_cast<uint32*>(_frame.bits());
|
||||
_frameIntsPerLineAdded = _frameIntsPerLine - _frameWidth;
|
||||
Assert(_frame.depth() == static_cast<int>(sizeof(uint32) << 3));
|
||||
Assert(_frame.bytesPerLine() == (_frameIntsPerLine << 2));
|
||||
Assert(_frameIntsPerLineAdded >= 0);
|
||||
}
|
||||
|
||||
void RoundShadowAnimation::setShadow(const style::Shadow &st) {
|
||||
_shadow.extend = st.extend * style::DevicePixelRatio();
|
||||
_shadow.left = cloneImage(st.left);
|
||||
if (_shadow.valid()) {
|
||||
_shadow.topLeft = cloneImage(st.topLeft);
|
||||
_shadow.top = cloneImage(st.top);
|
||||
_shadow.topRight = cloneImage(st.topRight);
|
||||
_shadow.right = cloneImage(st.right);
|
||||
_shadow.bottomRight = cloneImage(st.bottomRight);
|
||||
_shadow.bottom = cloneImage(st.bottom);
|
||||
_shadow.bottomLeft = cloneImage(st.bottomLeft);
|
||||
Assert(!_shadow.topLeft.isNull()
|
||||
&& !_shadow.top.isNull()
|
||||
&& !_shadow.topRight.isNull()
|
||||
&& !_shadow.right.isNull()
|
||||
&& !_shadow.bottomRight.isNull()
|
||||
&& !_shadow.bottom.isNull()
|
||||
&& !_shadow.bottomLeft.isNull());
|
||||
} else {
|
||||
_shadow.topLeft =
|
||||
_shadow.top =
|
||||
_shadow.topRight =
|
||||
_shadow.right =
|
||||
_shadow.bottomRight =
|
||||
_shadow.bottom =
|
||||
_shadow.bottomLeft = QImage();
|
||||
}
|
||||
}
|
||||
|
||||
void RoundShadowAnimation::setCornerMasks(
|
||||
const std::array<QImage, 4> &corners) {
|
||||
setCornerMask(_topLeft, corners[0]);
|
||||
setCornerMask(_topRight, corners[1]);
|
||||
setCornerMask(_bottomLeft, corners[2]);
|
||||
setCornerMask(_bottomRight, corners[3]);
|
||||
}
|
||||
|
||||
void RoundShadowAnimation::setCornerMask(Corner &corner, const QImage &image) {
|
||||
Expects(!started());
|
||||
|
||||
corner.image = image;
|
||||
if (corner.valid()) {
|
||||
corner.width = corner.image.width();
|
||||
corner.height = corner.image.height();
|
||||
corner.bytes = corner.image.constBits();
|
||||
corner.bytesPerPixel = (corner.image.depth() >> 3);
|
||||
corner.bytesPerLineAdded = corner.image.bytesPerLine() - corner.width * corner.bytesPerPixel;
|
||||
Assert(corner.image.depth() == (corner.bytesPerPixel << 3));
|
||||
Assert(corner.bytesPerLineAdded >= 0);
|
||||
} else {
|
||||
corner.width = corner.height = 0;
|
||||
}
|
||||
}
|
||||
|
||||
QImage RoundShadowAnimation::cloneImage(const style::icon &source) {
|
||||
if (source.empty()) return QImage();
|
||||
|
||||
auto result = QImage(
|
||||
source.size() * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
result.fill(Qt::transparent);
|
||||
{
|
||||
QPainter p(&result);
|
||||
source.paint(p, 0, 0, source.width());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void RoundShadowAnimation::paintCorner(Corner &corner, int left, int top) {
|
||||
auto mask = corner.bytes;
|
||||
auto bytesPerPixel = corner.bytesPerPixel;
|
||||
auto bytesPerLineAdded = corner.bytesPerLineAdded;
|
||||
auto frameInts = _frameInts + top * _frameIntsPerLine + left;
|
||||
auto frameIntsPerLineAdd = _frameIntsPerLine - corner.width;
|
||||
for (auto y = 0; y != corner.height; ++y) {
|
||||
for (auto x = 0; x != corner.width; ++x) {
|
||||
auto alpha = static_cast<uint32>(*mask) + 1;
|
||||
*frameInts = anim::unshifted(anim::shifted(*frameInts) * alpha);
|
||||
++frameInts;
|
||||
mask += bytesPerPixel;
|
||||
}
|
||||
frameInts += frameIntsPerLineAdd;
|
||||
mask += bytesPerLineAdded;
|
||||
}
|
||||
}
|
||||
|
||||
void RoundShadowAnimation::paintShadow(int left, int top, int right, int bottom) {
|
||||
paintShadowCorner(left, top, _shadow.topLeft);
|
||||
paintShadowCorner(right - _shadow.topRight.width(), top, _shadow.topRight);
|
||||
paintShadowCorner(right - _shadow.bottomRight.width(), bottom - _shadow.bottomRight.height(), _shadow.bottomRight);
|
||||
paintShadowCorner(left, bottom - _shadow.bottomLeft.height(), _shadow.bottomLeft);
|
||||
paintShadowVertical(left, top + _shadow.topLeft.height(), bottom - _shadow.bottomLeft.height(), _shadow.left);
|
||||
paintShadowVertical(right - _shadow.right.width(), top + _shadow.topRight.height(), bottom - _shadow.bottomRight.height(), _shadow.right);
|
||||
paintShadowHorizontal(left + _shadow.topLeft.width(), right - _shadow.topRight.width(), top, _shadow.top);
|
||||
paintShadowHorizontal(left + _shadow.bottomLeft.width(), right - _shadow.bottomRight.width(), bottom - _shadow.bottom.height(), _shadow.bottom);
|
||||
}
|
||||
|
||||
void RoundShadowAnimation::paintShadowCorner(int left, int top, const QImage &image) {
|
||||
auto imageWidth = image.width();
|
||||
auto imageHeight = image.height();
|
||||
auto imageInts = reinterpret_cast<const uint32*>(image.constBits());
|
||||
auto imageIntsPerLine = (image.bytesPerLine() >> 2);
|
||||
auto imageIntsPerLineAdded = imageIntsPerLine - imageWidth;
|
||||
if (left < 0) {
|
||||
auto shift = -base::take(left);
|
||||
imageWidth -= shift;
|
||||
imageInts += shift;
|
||||
}
|
||||
if (top < 0) {
|
||||
auto shift = -base::take(top);
|
||||
imageHeight -= shift;
|
||||
imageInts += shift * imageIntsPerLine;
|
||||
}
|
||||
if (left + imageWidth > _frameWidth) {
|
||||
imageWidth = _frameWidth - left;
|
||||
}
|
||||
if (top + imageHeight > _frameHeight) {
|
||||
imageHeight = _frameHeight - top;
|
||||
}
|
||||
if (imageWidth < 0 || imageHeight < 0) return;
|
||||
|
||||
auto frameInts = _frameInts + top * _frameIntsPerLine + left;
|
||||
auto frameIntsPerLineAdd = _frameIntsPerLine - imageWidth;
|
||||
for (auto y = 0; y != imageHeight; ++y) {
|
||||
for (auto x = 0; x != imageWidth; ++x) {
|
||||
auto source = *frameInts;
|
||||
auto shadowAlpha = qMax(_frameAlpha - int(source >> 24), 0);
|
||||
*frameInts = anim::unshifted(anim::shifted(source) * 256 + anim::shifted(*imageInts) * shadowAlpha);
|
||||
++frameInts;
|
||||
++imageInts;
|
||||
}
|
||||
frameInts += frameIntsPerLineAdd;
|
||||
imageInts += imageIntsPerLineAdded;
|
||||
}
|
||||
}
|
||||
|
||||
void RoundShadowAnimation::paintShadowVertical(int left, int top, int bottom, const QImage &image) {
|
||||
auto imageWidth = image.width();
|
||||
auto imageInts = reinterpret_cast<const uint32*>(image.constBits());
|
||||
if (left < 0) {
|
||||
auto shift = -base::take(left);
|
||||
imageWidth -= shift;
|
||||
imageInts += shift;
|
||||
}
|
||||
if (top < 0) top = 0;
|
||||
accumulate_min(bottom, _frameHeight);
|
||||
accumulate_min(imageWidth, _frameWidth - left);
|
||||
if (imageWidth < 0 || bottom <= top) return;
|
||||
|
||||
auto frameInts = _frameInts + top * _frameIntsPerLine + left;
|
||||
auto frameIntsPerLineAdd = _frameIntsPerLine - imageWidth;
|
||||
for (auto y = top; y != bottom; ++y) {
|
||||
for (auto x = 0; x != imageWidth; ++x) {
|
||||
auto source = *frameInts;
|
||||
auto shadowAlpha = _frameAlpha - (source >> 24);
|
||||
*frameInts = anim::unshifted(anim::shifted(source) * 256 + anim::shifted(*imageInts) * shadowAlpha);
|
||||
++frameInts;
|
||||
++imageInts;
|
||||
}
|
||||
frameInts += frameIntsPerLineAdd;
|
||||
imageInts -= imageWidth;
|
||||
}
|
||||
}
|
||||
|
||||
void RoundShadowAnimation::paintShadowHorizontal(int left, int right, int top, const QImage &image) {
|
||||
auto imageHeight = image.height();
|
||||
auto imageInts = reinterpret_cast<const uint32*>(image.constBits());
|
||||
auto imageIntsPerLine = (image.bytesPerLine() >> 2);
|
||||
if (top < 0) {
|
||||
auto shift = -base::take(top);
|
||||
imageHeight -= shift;
|
||||
imageInts += shift * imageIntsPerLine;
|
||||
}
|
||||
if (left < 0) left = 0;
|
||||
accumulate_min(right, _frameWidth);
|
||||
accumulate_min(imageHeight, _frameHeight - top);
|
||||
if (imageHeight < 0 || right <= left) return;
|
||||
|
||||
auto frameInts = _frameInts + top * _frameIntsPerLine + left;
|
||||
auto frameIntsPerLineAdd = _frameIntsPerLine - (right - left);
|
||||
for (auto y = 0; y != imageHeight; ++y) {
|
||||
auto imagePattern = anim::shifted(*imageInts);
|
||||
for (auto x = left; x != right; ++x) {
|
||||
auto source = *frameInts;
|
||||
auto shadowAlpha = _frameAlpha - (source >> 24);
|
||||
*frameInts = anim::unshifted(anim::shifted(source) * 256 + imagePattern * shadowAlpha);
|
||||
++frameInts;
|
||||
}
|
||||
frameInts += frameIntsPerLineAdd;
|
||||
imageInts += imageIntsPerLine;
|
||||
}
|
||||
}
|
||||
|
||||
void PanelAnimation::setFinalImage(QImage &&finalImage, QRect inner) {
|
||||
Expects(!started());
|
||||
|
||||
const auto pixelRatio = style::DevicePixelRatio();
|
||||
_finalImage = PixmapFromImage(
|
||||
std::move(finalImage).convertToFormat(
|
||||
QImage::Format_ARGB32_Premultiplied));
|
||||
|
||||
Assert(!_finalImage.isNull());
|
||||
_finalWidth = _finalImage.width();
|
||||
_finalHeight = _finalImage.height();
|
||||
Assert(!(_finalWidth % pixelRatio));
|
||||
Assert(!(_finalHeight % pixelRatio));
|
||||
_finalInnerLeft = inner.x();
|
||||
_finalInnerTop = inner.y();
|
||||
_finalInnerWidth = inner.width();
|
||||
_finalInnerHeight = inner.height();
|
||||
Assert(!(_finalInnerLeft % pixelRatio));
|
||||
Assert(!(_finalInnerTop % pixelRatio));
|
||||
Assert(!(_finalInnerWidth % pixelRatio));
|
||||
Assert(!(_finalInnerHeight % pixelRatio));
|
||||
_finalInnerRight = _finalInnerLeft + _finalInnerWidth;
|
||||
_finalInnerBottom = _finalInnerTop + _finalInnerHeight;
|
||||
Assert(QRect(0, 0, _finalWidth, _finalHeight).contains(inner));
|
||||
|
||||
setStartWidth();
|
||||
setStartHeight();
|
||||
setStartAlpha();
|
||||
setStartFadeTop();
|
||||
createFadeMask();
|
||||
setWidthDuration();
|
||||
setHeightDuration();
|
||||
setAlphaDuration();
|
||||
if (!_skipShadow) {
|
||||
setShadow(_st.shadow);
|
||||
}
|
||||
|
||||
auto checkCorner = [this, inner](Corner &corner) {
|
||||
if (!corner.valid()) return;
|
||||
if ((_startWidth >= 0 && _startWidth < _finalWidth)
|
||||
|| (_startHeight >= 0 && _startHeight < _finalHeight)) {
|
||||
Assert(corner.width <= inner.width());
|
||||
Assert(corner.height <= inner.height());
|
||||
}
|
||||
};
|
||||
checkCorner(_topLeft);
|
||||
checkCorner(_topRight);
|
||||
checkCorner(_bottomLeft);
|
||||
checkCorner(_bottomRight);
|
||||
}
|
||||
|
||||
void PanelAnimation::setStartWidth() {
|
||||
_startWidth = qRound(_st.startWidth * _finalInnerWidth);
|
||||
if (_startWidth >= 0) Assert(_startWidth <= _finalInnerWidth);
|
||||
}
|
||||
|
||||
void PanelAnimation::setStartHeight() {
|
||||
_startHeight = qRound(_st.startHeight * _finalInnerHeight);
|
||||
if (_startHeight >= 0) Assert(_startHeight <= _finalInnerHeight);
|
||||
}
|
||||
|
||||
void PanelAnimation::setStartAlpha() {
|
||||
_startAlpha = qRound(_st.startOpacity * 255);
|
||||
Assert(_startAlpha >= 0 && _startAlpha < 256);
|
||||
}
|
||||
|
||||
void PanelAnimation::setStartFadeTop() {
|
||||
_startFadeTop = qRound(_st.startFadeTop * _finalInnerHeight);
|
||||
}
|
||||
|
||||
void PanelAnimation::createFadeMask() {
|
||||
auto resultHeight = qRound(_finalImage.height() * _st.fadeHeight);
|
||||
if (auto remove = (resultHeight % style::DevicePixelRatio())) {
|
||||
resultHeight -= remove;
|
||||
}
|
||||
auto finalAlpha = qRound(_st.fadeOpacity * 255);
|
||||
Assert(finalAlpha >= 0 && finalAlpha < 256);
|
||||
auto result = QImage(style::DevicePixelRatio(), resultHeight, QImage::Format_ARGB32_Premultiplied);
|
||||
auto ints = reinterpret_cast<uint32*>(result.bits());
|
||||
auto intsPerLineAdded = (result.bytesPerLine() >> 2) - style::DevicePixelRatio();
|
||||
auto up = (_origin == PanelAnimation::Origin::BottomLeft || _origin == PanelAnimation::Origin::BottomRight);
|
||||
auto from = up ? resultHeight : 0, to = resultHeight - from, delta = up ? -1 : 1;
|
||||
auto fadeFirstAlpha = up ? (finalAlpha + 1) : 1;
|
||||
auto fadeLastAlpha = up ? 1 : (finalAlpha + 1);
|
||||
_fadeFirst = QBrush(QColor(_st.fadeBg->c.red(), _st.fadeBg->c.green(), _st.fadeBg->c.blue(), (_st.fadeBg->c.alpha() * fadeFirstAlpha) >> 8));
|
||||
_fadeLast = QBrush(QColor(_st.fadeBg->c.red(), _st.fadeBg->c.green(), _st.fadeBg->c.blue(), (_st.fadeBg->c.alpha() * fadeLastAlpha) >> 8));
|
||||
for (auto y = from; y != to; y += delta) {
|
||||
auto alpha = static_cast<uint32>(finalAlpha * y) / resultHeight;
|
||||
auto value = (0xFFU << 24) | (alpha << 16) | (alpha << 8) | alpha;
|
||||
for (auto x = 0; x != style::DevicePixelRatio(); ++x) {
|
||||
*ints++ = value;
|
||||
}
|
||||
ints += intsPerLineAdded;
|
||||
}
|
||||
_fadeMask = PixmapFromImage(style::colorizeImage(result, _st.fadeBg));
|
||||
_fadeHeight = _fadeMask.height();
|
||||
}
|
||||
|
||||
void PanelAnimation::setSkipShadow(bool skipShadow) {
|
||||
Assert(!started());
|
||||
_skipShadow = skipShadow;
|
||||
}
|
||||
|
||||
void PanelAnimation::setWidthDuration() {
|
||||
_widthDuration = _st.widthDuration;
|
||||
Assert(_widthDuration >= 0.);
|
||||
Assert(_widthDuration <= 1.);
|
||||
}
|
||||
|
||||
void PanelAnimation::setHeightDuration() {
|
||||
Assert(!started());
|
||||
_heightDuration = _st.heightDuration;
|
||||
Assert(_heightDuration >= 0.);
|
||||
Assert(_heightDuration <= 1.);
|
||||
}
|
||||
|
||||
void PanelAnimation::setAlphaDuration() {
|
||||
Assert(!started());
|
||||
_alphaDuration = _st.opacityDuration;
|
||||
Assert(_alphaDuration >= 0.);
|
||||
Assert(_alphaDuration <= 1.);
|
||||
}
|
||||
|
||||
void PanelAnimation::start() {
|
||||
Assert(!_finalImage.isNull());
|
||||
RoundShadowAnimation::start(_finalWidth, _finalHeight, _finalImage.devicePixelRatio());
|
||||
auto checkCorner = [this](const Corner &corner) {
|
||||
if (!corner.valid()) return;
|
||||
if (_startWidth >= 0) Assert(corner.width <= _startWidth);
|
||||
if (_startHeight >= 0) Assert(corner.height <= _startHeight);
|
||||
Assert(corner.width <= _finalInnerWidth);
|
||||
Assert(corner.height <= _finalInnerHeight);
|
||||
};
|
||||
checkCorner(_topLeft);
|
||||
checkCorner(_topRight);
|
||||
checkCorner(_bottomLeft);
|
||||
checkCorner(_bottomRight);
|
||||
}
|
||||
|
||||
auto PanelAnimation::computeState(float64 dt, float64 opacity) const
|
||||
-> PaintState {
|
||||
auto &transition = anim::easeOutCirc;
|
||||
if (dt < _alphaDuration) {
|
||||
opacity *= transition(1., dt / _alphaDuration);
|
||||
}
|
||||
const auto widthProgress = (_startWidth < 0 || dt >= _widthDuration)
|
||||
? 1.
|
||||
: transition(1., dt / _widthDuration);
|
||||
const auto heightProgress = (_startHeight < 0 || dt >= _heightDuration)
|
||||
? 1.
|
||||
: transition(1., dt / _heightDuration);
|
||||
const auto frameWidth = (widthProgress < 1.)
|
||||
? anim::interpolate(_startWidth, _finalInnerWidth, widthProgress)
|
||||
: _finalInnerWidth;
|
||||
const auto frameHeight = (heightProgress < 1.)
|
||||
? anim::interpolate(_startHeight, _finalInnerHeight, heightProgress)
|
||||
: _finalInnerHeight;
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
return {
|
||||
.opacity = opacity,
|
||||
.widthProgress = widthProgress,
|
||||
.heightProgress = heightProgress,
|
||||
.fade = transition(1., dt),
|
||||
.width = frameWidth / ratio,
|
||||
.height = frameHeight / ratio,
|
||||
};
|
||||
}
|
||||
|
||||
auto PanelAnimation::paintFrame(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
float64 dt,
|
||||
float64 opacity)
|
||||
-> PaintState {
|
||||
Assert(started());
|
||||
Assert(dt >= 0.);
|
||||
|
||||
const auto pixelRatio = style::DevicePixelRatio();
|
||||
|
||||
const auto state = computeState(dt, opacity);
|
||||
opacity = state.opacity;
|
||||
_frameAlpha = anim::interpolate(1, 256, opacity);
|
||||
const auto frameWidth = state.width * pixelRatio;
|
||||
const auto frameHeight = state.height * pixelRatio;
|
||||
auto frameLeft = (_origin == Origin::TopLeft || _origin == Origin::BottomLeft) ? _finalInnerLeft : (_finalInnerRight - frameWidth);
|
||||
auto frameTop = (_origin == Origin::TopLeft || _origin == Origin::TopRight) ? _finalInnerTop : (_finalInnerBottom - frameHeight);
|
||||
auto frameRight = frameLeft + frameWidth;
|
||||
auto frameBottom = frameTop + frameHeight;
|
||||
|
||||
auto fadeTop = (_fadeHeight > 0) ? std::clamp(anim::interpolate(_startFadeTop, _finalInnerHeight, state.fade), 0, frameHeight) : frameHeight;
|
||||
if (auto decrease = (fadeTop % pixelRatio)) {
|
||||
fadeTop -= decrease;
|
||||
}
|
||||
auto fadeBottom = (fadeTop < frameHeight) ? std::min(fadeTop + _fadeHeight, frameHeight) : frameHeight;
|
||||
auto fadeSkipLines = 0;
|
||||
if (_origin == Origin::BottomLeft || _origin == Origin::BottomRight) {
|
||||
fadeTop = frameHeight - fadeTop;
|
||||
fadeBottom = frameHeight - fadeBottom;
|
||||
qSwap(fadeTop, fadeBottom);
|
||||
fadeSkipLines = fadeTop + _fadeHeight - fadeBottom;
|
||||
}
|
||||
fadeTop += frameTop;
|
||||
fadeBottom += frameTop;
|
||||
|
||||
if (opacity < 1.) {
|
||||
_frame.fill(Qt::transparent);
|
||||
}
|
||||
{
|
||||
QPainter p(&_frame);
|
||||
p.setOpacity(opacity);
|
||||
auto painterFrameLeft = frameLeft / pixelRatio;
|
||||
auto painterFrameTop = frameTop / pixelRatio;
|
||||
auto painterFadeBottom = fadeBottom / pixelRatio;
|
||||
p.drawPixmap(painterFrameLeft, painterFrameTop, _finalImage, frameLeft, frameTop, frameWidth, frameHeight);
|
||||
if (_fadeHeight) {
|
||||
if (frameTop != fadeTop) {
|
||||
p.fillRect(painterFrameLeft, painterFrameTop, frameWidth, fadeTop - frameTop, _fadeFirst);
|
||||
}
|
||||
if (fadeTop != fadeBottom) {
|
||||
auto painterFadeTop = fadeTop / pixelRatio;
|
||||
auto painterFrameWidth = frameWidth / pixelRatio;
|
||||
p.drawPixmap(painterFrameLeft, painterFadeTop, painterFrameWidth, painterFadeBottom - painterFadeTop, _fadeMask, 0, fadeSkipLines, pixelRatio, fadeBottom - fadeTop);
|
||||
}
|
||||
if (fadeBottom != frameBottom) {
|
||||
p.fillRect(painterFrameLeft, painterFadeBottom, frameWidth, frameBottom - fadeBottom, _fadeLast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw corners
|
||||
paintCorner(_topLeft, frameLeft, frameTop);
|
||||
paintCorner(_topRight, frameRight - _topRight.width, frameTop);
|
||||
paintCorner(_bottomLeft, frameLeft, frameBottom - _bottomLeft.height);
|
||||
paintCorner(_bottomRight, frameRight - _bottomRight.width, frameBottom - _bottomRight.height);
|
||||
|
||||
// Draw shadow upon the transparent
|
||||
auto outerLeft = frameLeft;
|
||||
auto outerTop = frameTop;
|
||||
auto outerRight = frameRight;
|
||||
auto outerBottom = frameBottom;
|
||||
if (_shadow.valid()) {
|
||||
outerLeft -= _shadow.extend.left();
|
||||
outerTop -= _shadow.extend.top();
|
||||
outerRight += _shadow.extend.right();
|
||||
outerBottom += _shadow.extend.bottom();
|
||||
}
|
||||
if (pixelRatio > 1) {
|
||||
if (auto skipLeft = (outerLeft % pixelRatio)) {
|
||||
outerLeft -= skipLeft;
|
||||
}
|
||||
if (auto skipTop = (outerTop % pixelRatio)) {
|
||||
outerTop -= skipTop;
|
||||
}
|
||||
if (auto skipRight = (outerRight % pixelRatio)) {
|
||||
outerRight += (pixelRatio - skipRight);
|
||||
}
|
||||
if (auto skipBottom = (outerBottom % pixelRatio)) {
|
||||
outerBottom += (pixelRatio - skipBottom);
|
||||
}
|
||||
}
|
||||
|
||||
if (opacity == 1.) {
|
||||
// Fill above the frame top with transparent.
|
||||
auto fillTopInts = (_frameInts + outerTop * _frameIntsPerLine + outerLeft);
|
||||
auto fillWidth = (outerRight - outerLeft) * sizeof(uint32);
|
||||
for (auto fillTop = frameTop - outerTop; fillTop != 0; --fillTop) {
|
||||
memset(fillTopInts, 0, fillWidth);
|
||||
fillTopInts += _frameIntsPerLine;
|
||||
}
|
||||
|
||||
// Fill to the left and to the right of the frame with transparent.
|
||||
auto fillLeft = (frameLeft - outerLeft) * sizeof(uint32);
|
||||
auto fillRight = (outerRight - frameRight) * sizeof(uint32);
|
||||
if (fillLeft || fillRight) {
|
||||
auto fillInts = _frameInts + frameTop * _frameIntsPerLine;
|
||||
for (auto y = frameTop; y != frameBottom; ++y) {
|
||||
memset(fillInts + outerLeft, 0, fillLeft);
|
||||
memset(fillInts + frameRight, 0, fillRight);
|
||||
fillInts += _frameIntsPerLine;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill below the frame bottom with transparent.
|
||||
auto fillBottomInts = (_frameInts + frameBottom * _frameIntsPerLine + outerLeft);
|
||||
for (auto fillBottom = outerBottom - frameBottom; fillBottom != 0; --fillBottom) {
|
||||
memset(fillBottomInts, 0, fillWidth);
|
||||
fillBottomInts += _frameIntsPerLine;
|
||||
}
|
||||
}
|
||||
|
||||
if (_shadow.valid()) {
|
||||
paintShadow(outerLeft, outerTop, outerRight, outerBottom);
|
||||
}
|
||||
|
||||
// Debug
|
||||
//frameInts = _frameInts;
|
||||
//auto pattern = anim::shifted((static_cast<uint32>(0xFF) << 24) | (static_cast<uint32>(0xFF) << 16) | (static_cast<uint32>(0xFF) << 8) | static_cast<uint32>(0xFF));
|
||||
//for (auto y = 0; y != _finalHeight; ++y) {
|
||||
// for (auto x = 0; x != _finalWidth; ++x) {
|
||||
// auto source = *frameInts;
|
||||
// auto sourceAlpha = (source >> 24);
|
||||
// *frameInts = anim::unshifted(anim::shifted(source) * 256 + pattern * (256 - sourceAlpha));
|
||||
// ++frameInts;
|
||||
// }
|
||||
// frameInts += _frameIntsPerLineAdded;
|
||||
//}
|
||||
|
||||
p.drawImage(style::rtlpoint(x + (outerLeft / pixelRatio), y + (outerTop / pixelRatio), outerWidth), _frame, QRect(outerLeft, outerTop, outerRight - outerLeft, outerBottom - outerTop));
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
143
Telegram/lib_ui/ui/effects/panel_animation.h
Normal file
143
Telegram/lib_ui/ui/effects/panel_animation.h
Normal file
@@ -0,0 +1,143 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RoundShadowAnimation {
|
||||
public:
|
||||
void setCornerMasks(const std::array<QImage, 4> &corners);
|
||||
|
||||
protected:
|
||||
void start(int frameWidth, int frameHeight, float64 devicePixelRatio);
|
||||
void setShadow(const style::Shadow &st);
|
||||
|
||||
bool started() const {
|
||||
return !_frame.isNull();
|
||||
}
|
||||
|
||||
struct Corner {
|
||||
QImage image;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
const uchar *bytes = nullptr;
|
||||
int bytesPerPixel = 0;
|
||||
int bytesPerLineAdded = 0;
|
||||
|
||||
bool valid() const {
|
||||
return !image.isNull();
|
||||
}
|
||||
};
|
||||
void setCornerMask(Corner &corner, const QImage &image);
|
||||
void paintCorner(Corner &corner, int left, int top);
|
||||
|
||||
struct Shadow {
|
||||
style::margins extend;
|
||||
QImage left, topLeft, top, topRight, right, bottomRight, bottom, bottomLeft;
|
||||
|
||||
bool valid() const {
|
||||
return !left.isNull();
|
||||
}
|
||||
};
|
||||
QImage cloneImage(const style::icon &source);
|
||||
void paintShadow(int left, int top, int right, int bottom);
|
||||
void paintShadowCorner(int left, int top, const QImage &image);
|
||||
void paintShadowVertical(int left, int top, int bottom, const QImage &image);
|
||||
void paintShadowHorizontal(int left, int right, int top, const QImage &image);
|
||||
|
||||
Shadow _shadow;
|
||||
|
||||
Corner _topLeft;
|
||||
Corner _topRight;
|
||||
Corner _bottomLeft;
|
||||
Corner _bottomRight;
|
||||
|
||||
QImage _frame;
|
||||
uint32 *_frameInts = nullptr;
|
||||
int _frameWidth = 0;
|
||||
int _frameHeight = 0;
|
||||
int _frameAlpha = 0; // recounted each getFrame()
|
||||
int _frameIntsPerLine = 0;
|
||||
int _frameIntsPerLineAdded = 0;
|
||||
|
||||
};
|
||||
|
||||
class PanelAnimation : public RoundShadowAnimation {
|
||||
public:
|
||||
enum class Origin {
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
};
|
||||
PanelAnimation(const style::PanelAnimation &st, Origin origin) : _st(st), _origin(origin) {
|
||||
}
|
||||
|
||||
struct PaintState {
|
||||
float64 opacity = 0.;
|
||||
float64 widthProgress = 0.;
|
||||
float64 heightProgress = 0.;
|
||||
float64 fade = 0.;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
void setFinalImage(QImage &&finalImage, QRect inner);
|
||||
void setSkipShadow(bool skipShadow);
|
||||
|
||||
void start();
|
||||
[[nodiscard]] PaintState computeState(float64 dt, float64 opacity) const;
|
||||
PaintState paintFrame(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
float64 dt,
|
||||
float64 opacity);
|
||||
|
||||
private:
|
||||
void setStartWidth();
|
||||
void setStartHeight();
|
||||
void setStartAlpha();
|
||||
void setStartFadeTop();
|
||||
void createFadeMask();
|
||||
void setWidthDuration();
|
||||
void setHeightDuration();
|
||||
void setAlphaDuration();
|
||||
|
||||
const style::PanelAnimation &_st;
|
||||
Origin _origin = Origin::TopLeft;
|
||||
|
||||
QPixmap _finalImage;
|
||||
int _finalWidth = 0;
|
||||
int _finalHeight = 0;
|
||||
int _finalInnerLeft = 0;
|
||||
int _finalInnerTop = 0;
|
||||
int _finalInnerRight = 0;
|
||||
int _finalInnerBottom = 0;
|
||||
int _finalInnerWidth = 0;
|
||||
int _finalInnerHeight = 0;
|
||||
|
||||
bool _skipShadow = false;
|
||||
int _startWidth = -1;
|
||||
int _startHeight = -1;
|
||||
int _startAlpha = 0;
|
||||
|
||||
int _startFadeTop = 0;
|
||||
QPixmap _fadeMask;
|
||||
int _fadeHeight = 0;
|
||||
QBrush _fadeFirst, _fadeLast;
|
||||
|
||||
float64 _widthDuration = 1.;
|
||||
float64 _heightDuration = 1.;
|
||||
float64 _alphaDuration = 1.;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
180
Telegram/lib_ui/ui/effects/path_shift_gradient.cpp
Normal file
180
Telegram/lib_ui/ui/effects/path_shift_gradient.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/path_shift_gradient.h"
|
||||
|
||||
#include "base/call_delayed.h"
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kSlideDuration = crl::time(1000);
|
||||
constexpr auto kWaitDuration = crl::time(1000);
|
||||
constexpr auto kFullDuration = kSlideDuration + kWaitDuration;
|
||||
|
||||
} // namespace
|
||||
|
||||
struct PathShiftGradient::AnimationData {
|
||||
Ui::Animations::Simple animation;
|
||||
base::flat_set<not_null<PathShiftGradient*>> active;
|
||||
bool scheduled = false;
|
||||
};
|
||||
|
||||
std::weak_ptr<PathShiftGradient::AnimationData> PathShiftGradient::Animation;
|
||||
|
||||
PathShiftGradient::PathShiftGradient(
|
||||
const style::color &bg,
|
||||
const style::color &fg,
|
||||
Fn<void()> animationCallback,
|
||||
rpl::producer<> paletteUpdated)
|
||||
: _bg(bg)
|
||||
, _fg(fg)
|
||||
, _animationCallback(std::move(animationCallback)) {
|
||||
refreshColors();
|
||||
if (!paletteUpdated) {
|
||||
paletteUpdated = style::PaletteChanged();
|
||||
}
|
||||
std::move(
|
||||
paletteUpdated
|
||||
) | rpl::on_next([=] {
|
||||
refreshColors();
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
PathShiftGradient::~PathShiftGradient() {
|
||||
if (const auto strong = _animation.get()) {
|
||||
strong->active.erase(this);
|
||||
}
|
||||
}
|
||||
|
||||
void PathShiftGradient::overrideColors(
|
||||
const style::color &bg,
|
||||
const style::color &fg) {
|
||||
_colorsOverriden = true;
|
||||
refreshColors(bg, fg);
|
||||
}
|
||||
|
||||
void PathShiftGradient::clearOverridenColors() {
|
||||
if (!_colorsOverriden) {
|
||||
return;
|
||||
}
|
||||
_colorsOverriden = false;
|
||||
refreshColors();
|
||||
}
|
||||
|
||||
void PathShiftGradient::startFrame(
|
||||
int viewportLeft,
|
||||
int viewportWidth,
|
||||
int gradientWidth) {
|
||||
_viewportLeft = viewportLeft;
|
||||
_viewportWidth = viewportWidth;
|
||||
_gradientWidth = gradientWidth;
|
||||
_geometryUpdated = false;
|
||||
}
|
||||
|
||||
void PathShiftGradient::updateGeometry() {
|
||||
if (_geometryUpdated) {
|
||||
return;
|
||||
}
|
||||
_geometryUpdated = true;
|
||||
const auto now = crl::now();
|
||||
const auto period = now % kFullDuration;
|
||||
if (period >= kSlideDuration) {
|
||||
_gradientEnabled = false;
|
||||
return;
|
||||
}
|
||||
const auto progress = period / float64(kSlideDuration);
|
||||
_gradientStart = anim::interpolate(
|
||||
_viewportLeft - _gradientWidth,
|
||||
_viewportLeft + _viewportWidth,
|
||||
progress);
|
||||
_gradientFinalStop = _gradientStart + _gradientWidth;
|
||||
_gradientEnabled = true;
|
||||
}
|
||||
|
||||
bool PathShiftGradient::paint(Fn<bool(const Background&)> painter) {
|
||||
updateGeometry();
|
||||
if (_gradientEnabled) {
|
||||
_gradient.setStart(_gradientStart, 0);
|
||||
_gradient.setFinalStop(_gradientFinalStop, 0);
|
||||
}
|
||||
const auto background = _gradientEnabled
|
||||
? Background(&_gradient)
|
||||
: _bgOverride
|
||||
? *_bgOverride
|
||||
: _bg;
|
||||
if (!painter(background)) {
|
||||
return false;
|
||||
}
|
||||
activateAnimation();
|
||||
return true;
|
||||
}
|
||||
|
||||
void PathShiftGradient::activateAnimation() {
|
||||
if (_animationActive) {
|
||||
return;
|
||||
}
|
||||
_animationActive = true;
|
||||
if (!_animation) {
|
||||
_animation = Animation.lock();
|
||||
if (!_animation) {
|
||||
_animation = std::make_shared<AnimationData>();
|
||||
Animation = _animation;
|
||||
}
|
||||
}
|
||||
const auto raw = _animation.get();
|
||||
if (_animationCallback) {
|
||||
raw->active.emplace(this);
|
||||
}
|
||||
|
||||
const auto globalCallback = [] {
|
||||
const auto strong = Animation.lock();
|
||||
if (!strong) {
|
||||
return;
|
||||
}
|
||||
strong->scheduled = false;
|
||||
while (!strong->active.empty()) {
|
||||
const auto entry = strong->active.back();
|
||||
strong->active.erase(strong->active.end() - 1);
|
||||
entry->_animationActive = false;
|
||||
entry->_animationCallback();
|
||||
}
|
||||
};
|
||||
|
||||
const auto now = crl::now();
|
||||
const auto period = now % kFullDuration;
|
||||
if (period >= kSlideDuration) {
|
||||
const auto tillWaitFinish = kFullDuration - period;
|
||||
if (!raw->scheduled) {
|
||||
raw->scheduled = true;
|
||||
raw->animation.stop();
|
||||
base::call_delayed(tillWaitFinish, globalCallback);
|
||||
}
|
||||
} else {
|
||||
const auto tillSlideFinish = kSlideDuration - period;
|
||||
if (!raw->animation.animating()) {
|
||||
raw->animation.start(globalCallback, 0., 1., tillSlideFinish);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PathShiftGradient::refreshColors() {
|
||||
refreshColors(_bg, _fg);
|
||||
}
|
||||
|
||||
void PathShiftGradient::refreshColors(
|
||||
const style::color &bg,
|
||||
const style::color &fg) {
|
||||
_gradient.setStops({
|
||||
{ 0., bg->c },
|
||||
{ 0.5, fg->c },
|
||||
{ 1., bg->c },
|
||||
});
|
||||
_bgOverride = _colorsOverriden ? &bg : nullptr;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
65
Telegram/lib_ui/ui/effects/path_shift_gradient.h
Normal file
65
Telegram/lib_ui/ui/effects/path_shift_gradient.h
Normal file
@@ -0,0 +1,65 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/style/style_core_types.h"
|
||||
|
||||
#include <QtGui/QLinearGradient>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class PathShiftGradient final {
|
||||
public:
|
||||
PathShiftGradient(
|
||||
const style::color &bg,
|
||||
const style::color &fg,
|
||||
Fn<void()> animationCallback,
|
||||
rpl::producer<> paletteUpdated = nullptr);
|
||||
~PathShiftGradient();
|
||||
|
||||
void startFrame(
|
||||
int viewportLeft,
|
||||
int viewportWidth,
|
||||
int gradientWidth);
|
||||
|
||||
void overrideColors(const style::color &bg, const style::color &fg);
|
||||
void clearOverridenColors();
|
||||
|
||||
using Background = std::variant<QLinearGradient*, style::color>;
|
||||
bool paint(Fn<bool(const Background&)> painter);
|
||||
|
||||
private:
|
||||
struct AnimationData;
|
||||
|
||||
void refreshColors();
|
||||
void refreshColors(const style::color &bg, const style::color &fg);
|
||||
void updateGeometry();
|
||||
void activateAnimation();
|
||||
|
||||
static std::weak_ptr<AnimationData> Animation;
|
||||
|
||||
const style::color &_bg;
|
||||
const style::color &_fg;
|
||||
const style::color *_bgOverride = nullptr;
|
||||
QLinearGradient _gradient;
|
||||
std::shared_ptr<AnimationData> _animation;
|
||||
const Fn<void()> _animationCallback;
|
||||
int _viewportLeft = 0;
|
||||
int _viewportWidth = 0;
|
||||
int _gradientWidth = 0;
|
||||
int _gradientStart = 0;
|
||||
int _gradientFinalStop = 0;
|
||||
bool _gradientEnabled = false;
|
||||
bool _geometryUpdated = false;
|
||||
bool _animationActive = false;
|
||||
bool _colorsOverriden = false;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
338
Telegram/lib_ui/ui/effects/radial_animation.cpp
Normal file
338
Telegram/lib_ui/ui/effects/radial_animation.cpp
Normal file
@@ -0,0 +1,338 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/radial_animation.h"
|
||||
|
||||
#include "ui/arc_angles.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kFullArcLength = arc::kFullLength;
|
||||
|
||||
} // namespace
|
||||
|
||||
const int RadialState::kFull = kFullArcLength;
|
||||
|
||||
void RadialAnimation::start(float64 prg) {
|
||||
_firstStart = _lastStart = _lastTime = crl::now();
|
||||
const auto iprg = qRound(qMax(prg, 0.0001) * arc::kAlmostFullLength);
|
||||
const auto iprgstrict = qRound(prg * arc::kAlmostFullLength);
|
||||
_arcEnd = anim::value(iprgstrict, iprg);
|
||||
_animation.start();
|
||||
}
|
||||
|
||||
bool RadialAnimation::update(float64 prg, bool finished, crl::time ms) {
|
||||
const auto iprg = qRound(qMax(prg, 0.0001) * arc::kAlmostFullLength);
|
||||
const auto result = (iprg != qRound(_arcEnd.to()))
|
||||
|| (_finished != finished);
|
||||
if (_finished != finished) {
|
||||
_arcEnd.start(iprg);
|
||||
_finished = finished;
|
||||
_lastStart = _lastTime;
|
||||
} else if (result) {
|
||||
_arcEnd.start(iprg);
|
||||
_lastStart = _lastTime;
|
||||
}
|
||||
_lastTime = ms;
|
||||
|
||||
const auto dt = float64(ms - _lastStart);
|
||||
const auto fulldt = float64(ms - _firstStart);
|
||||
const auto opacitydt = _finished
|
||||
? (_lastStart - _firstStart)
|
||||
: fulldt;
|
||||
_opacity = qMin(opacitydt / st::radialDuration, 1.);
|
||||
if (anim::Disabled()) {
|
||||
_arcEnd.update(1., anim::linear);
|
||||
if (finished) {
|
||||
stop();
|
||||
}
|
||||
} else if (!finished) {
|
||||
_arcEnd.update(1. - (st::radialDuration / (st::radialDuration + dt)), anim::linear);
|
||||
} else if (dt >= st::radialDuration) {
|
||||
_arcEnd.update(1., anim::linear);
|
||||
stop();
|
||||
} else {
|
||||
auto r = dt / st::radialDuration;
|
||||
_arcEnd.update(r, anim::linear);
|
||||
_opacity *= 1 - r;
|
||||
}
|
||||
auto fromstart = fulldt / st::radialPeriod;
|
||||
_arcStart.update(fromstart - std::floor(fromstart), anim::linear);
|
||||
return result;
|
||||
}
|
||||
|
||||
void RadialAnimation::stop() {
|
||||
_firstStart = _lastStart = _lastTime = 0;
|
||||
_arcEnd = anim::value();
|
||||
_animation.stop();
|
||||
}
|
||||
|
||||
void RadialAnimation::draw(
|
||||
QPainter &p,
|
||||
const QRectF &inner,
|
||||
float64 thickness,
|
||||
style::color color) const {
|
||||
const auto state = computeState();
|
||||
|
||||
auto o = p.opacity();
|
||||
p.setOpacity(o * state.shown);
|
||||
|
||||
auto pen = color->p;
|
||||
auto was = p.pen();
|
||||
pen.setWidthF(thickness);
|
||||
pen.setCapStyle(Qt::RoundCap);
|
||||
p.setPen(pen);
|
||||
|
||||
{
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.drawArc(inner, state.arcFrom, state.arcLength);
|
||||
}
|
||||
|
||||
p.setPen(was);
|
||||
p.setOpacity(o);
|
||||
}
|
||||
|
||||
RadialState RadialAnimation::computeState() const {
|
||||
auto length = arc::kMinLength + qRound(_arcEnd.current());
|
||||
auto from = arc::kQuarterLength
|
||||
- length
|
||||
- (anim::Disabled() ? 0 : qRound(_arcStart.current()));
|
||||
if (style::RightToLeft()) {
|
||||
from = arc::kQuarterLength - (from - arc::kQuarterLength) - length;
|
||||
if (from < 0) from += arc::kFullLength;
|
||||
}
|
||||
return { _opacity, from, length };
|
||||
}
|
||||
|
||||
void InfiniteRadialAnimation::init() {
|
||||
anim::Disables() | rpl::filter([=] {
|
||||
return animating();
|
||||
}) | rpl::on_next([=](bool disabled) {
|
||||
if (!disabled && !_animation.animating()) {
|
||||
_animation.start();
|
||||
} else if (disabled && _animation.animating()) {
|
||||
_animation.stop();
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void InfiniteRadialAnimation::start(crl::time skip) {
|
||||
if (!animating()) {
|
||||
const auto now = crl::now();
|
||||
_workStarted = std::max(now + _st.sineDuration - skip, crl::time(1));
|
||||
_workFinished = 0;
|
||||
}
|
||||
if (!anim::Disabled() && !_animation.animating()) {
|
||||
_animation.start();
|
||||
}
|
||||
}
|
||||
|
||||
void InfiniteRadialAnimation::stop(anim::type animated) {
|
||||
const auto now = crl::now();
|
||||
if (anim::Disabled() || animated == anim::type::instant) {
|
||||
_workFinished = now;
|
||||
}
|
||||
if (!_workFinished) {
|
||||
const auto zero = _workStarted - _st.sineDuration;
|
||||
const auto index = (now - zero + _st.sinePeriod - _st.sineShift)
|
||||
/ _st.sinePeriod;
|
||||
_workFinished = zero
|
||||
+ _st.sineShift
|
||||
+ (index * _st.sinePeriod)
|
||||
+ _st.sineDuration;
|
||||
} else if (_workFinished <= now) {
|
||||
_animation.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void InfiniteRadialAnimation::draw(
|
||||
QPainter &p,
|
||||
QPoint position,
|
||||
int outerWidth) {
|
||||
Draw(
|
||||
p,
|
||||
computeState(),
|
||||
position,
|
||||
_st.size,
|
||||
outerWidth,
|
||||
_st.color,
|
||||
_st.thickness);
|
||||
}
|
||||
|
||||
void InfiniteRadialAnimation::draw(
|
||||
QPainter &p,
|
||||
QPoint position,
|
||||
QSize size,
|
||||
int outerWidth) {
|
||||
Draw(
|
||||
p,
|
||||
computeState(),
|
||||
position,
|
||||
size,
|
||||
outerWidth,
|
||||
_st.color,
|
||||
_st.thickness);
|
||||
}
|
||||
|
||||
void InfiniteRadialAnimation::Draw(
|
||||
QPainter &p,
|
||||
const RadialState &state,
|
||||
QPoint position,
|
||||
QSize size,
|
||||
int outerWidth,
|
||||
QPen pen,
|
||||
int thickness) {
|
||||
auto o = p.opacity();
|
||||
p.setOpacity(o * state.shown);
|
||||
|
||||
const auto rect = style::rtlrect(
|
||||
position.x(),
|
||||
position.y(),
|
||||
size.width(),
|
||||
size.height(),
|
||||
outerWidth);
|
||||
const auto was = p.pen();
|
||||
const auto brush = p.brush();
|
||||
if (anim::Disabled()) {
|
||||
anim::DrawStaticLoading(p, rect, thickness, pen);
|
||||
} else {
|
||||
pen.setWidth(thickness);
|
||||
pen.setCapStyle(Qt::RoundCap);
|
||||
p.setPen(pen);
|
||||
|
||||
{
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.drawArc(
|
||||
rect,
|
||||
state.arcFrom,
|
||||
state.arcLength);
|
||||
}
|
||||
}
|
||||
p.setPen(was);
|
||||
p.setBrush(brush);
|
||||
p.setOpacity(o);
|
||||
}
|
||||
|
||||
RadialState InfiniteRadialAnimation::computeState() {
|
||||
const auto now = crl::now();
|
||||
const auto linear = kFullArcLength
|
||||
- int(((now * kFullArcLength) / _st.linearPeriod) % kFullArcLength);
|
||||
if (!animating()) {
|
||||
const auto shown = 0.;
|
||||
_animation.stop();
|
||||
return {
|
||||
shown,
|
||||
linear,
|
||||
kFullArcLength };
|
||||
}
|
||||
if (anim::Disabled()) {
|
||||
return { 1., 0, kFullArcLength };
|
||||
}
|
||||
const auto min = int(base::SafeRound(kFullArcLength * _st.arcMin));
|
||||
const auto max = int(base::SafeRound(kFullArcLength * _st.arcMax));
|
||||
if (now <= _workStarted) {
|
||||
// zero .. _workStarted
|
||||
const auto zero = _workStarted - _st.sineDuration;
|
||||
const auto shown = (now - zero) / float64(_st.sineDuration);
|
||||
const auto length = anim::interpolate(
|
||||
kFullArcLength,
|
||||
min,
|
||||
anim::sineInOut(1., std::clamp(shown, 0., 1.)));
|
||||
return {
|
||||
shown,
|
||||
linear,
|
||||
length };
|
||||
} else if (!_workFinished || now <= _workFinished - _st.sineDuration) {
|
||||
// _workStared .. _workFinished - _st.sineDuration
|
||||
const auto shown = 1.;
|
||||
const auto cycles = (now - _workStarted) / _st.sinePeriod;
|
||||
const auto relative = (now - _workStarted) % _st.sinePeriod;
|
||||
const auto smallDuration = _st.sineShift - _st.sineDuration;
|
||||
const auto basic = int((linear
|
||||
+ min
|
||||
+ (cycles * (kFullArcLength + min - max))) % kFullArcLength);
|
||||
if (relative <= smallDuration) {
|
||||
// localZero .. growStart
|
||||
return {
|
||||
shown,
|
||||
basic - min,
|
||||
min };
|
||||
} else if (relative <= smallDuration + _st.sineDuration) {
|
||||
// growStart .. growEnd
|
||||
const auto growLinear = (relative - smallDuration) /
|
||||
float64(_st.sineDuration);
|
||||
const auto growProgress = anim::sineInOut(1., growLinear);
|
||||
const auto length = anim::interpolate(min, max, growProgress);
|
||||
return {
|
||||
shown,
|
||||
basic - length,
|
||||
length };
|
||||
} else if (relative <= _st.sinePeriod - _st.sineDuration) {
|
||||
// growEnd .. shrinkStart
|
||||
return {
|
||||
shown,
|
||||
basic - max,
|
||||
max };
|
||||
} else {
|
||||
// shrinkStart .. shrinkEnd
|
||||
const auto shrinkLinear = (relative
|
||||
- (_st.sinePeriod - _st.sineDuration))
|
||||
/ float64(_st.sineDuration);
|
||||
const auto shrinkProgress = anim::sineInOut(1., shrinkLinear);
|
||||
const auto shrink = anim::interpolate(
|
||||
0,
|
||||
max - min,
|
||||
shrinkProgress);
|
||||
return {
|
||||
shown,
|
||||
basic - max,
|
||||
max - shrink }; // interpolate(max, min, shrinkProgress)
|
||||
}
|
||||
} else {
|
||||
// _workFinished - _st.sineDuration .. _workFinished
|
||||
const auto hidden = (now - (_workFinished - _st.sineDuration))
|
||||
/ float64(_st.sineDuration);
|
||||
const auto cycles = (_workFinished - _workStarted) / _st.sinePeriod;
|
||||
const auto basic = int((linear
|
||||
+ min
|
||||
+ cycles * (kFullArcLength + min - max)) % kFullArcLength);
|
||||
const auto length = anim::interpolate(
|
||||
min,
|
||||
kFullArcLength,
|
||||
anim::sineInOut(1., std::clamp(hidden, 0., 1.)));
|
||||
return {
|
||||
1. - hidden,
|
||||
basic - length,
|
||||
length };
|
||||
}
|
||||
//const auto frontPeriods = time / st.sinePeriod;
|
||||
//const auto frontCurrent = time % st.sinePeriod;
|
||||
//const auto frontProgress = anim::sineInOut(
|
||||
// st.arcMax - st.arcMin,
|
||||
// std::min(frontCurrent, crl::time(st.sineDuration))
|
||||
// / float64(st.sineDuration));
|
||||
//const auto backTime = std::max(time - st.sineShift, 0LL);
|
||||
//const auto backPeriods = backTime / st.sinePeriod;
|
||||
//const auto backCurrent = backTime % st.sinePeriod;
|
||||
//const auto backProgress = anim::sineInOut(
|
||||
// st.arcMax - st.arcMin,
|
||||
// std::min(backCurrent, crl::time(st.sineDuration))
|
||||
// / float64(st.sineDuration));
|
||||
//const auto front = linear + base::SafeRound((st.arcMin + frontProgress + frontPeriods * (st.arcMax - st.arcMin)) * kFullArcLength);
|
||||
//const auto from = linear + base::SafeRound((backProgress + backPeriods * (st.arcMax - st.arcMin)) * kFullArcLength);
|
||||
//const auto length = (front - from);
|
||||
|
||||
//return {
|
||||
// _opacity,
|
||||
// from,
|
||||
// length
|
||||
//};
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
123
Telegram/lib_ui/ui/effects/radial_animation.h
Normal file
123
Telegram/lib_ui/ui/effects/radial_animation.h
Normal file
@@ -0,0 +1,123 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
namespace style {
|
||||
struct InfiniteRadialAnimation;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct RadialState {
|
||||
static const int kFull;
|
||||
|
||||
float64 shown = 0.;
|
||||
int arcFrom = 0;
|
||||
int arcLength = kFull;
|
||||
};
|
||||
|
||||
class RadialAnimation {
|
||||
public:
|
||||
template <typename Callback>
|
||||
RadialAnimation(Callback &&callback);
|
||||
|
||||
[[nodiscard]] float64 opacity() const {
|
||||
return _opacity;
|
||||
}
|
||||
[[nodiscard]] bool animating() const {
|
||||
return _animation.animating();
|
||||
}
|
||||
|
||||
void start(float64 prg);
|
||||
bool update(float64 prg, bool finished, crl::time ms);
|
||||
void stop();
|
||||
|
||||
void draw(
|
||||
QPainter &p,
|
||||
const QRectF &inner,
|
||||
float64 thickness,
|
||||
style::color color) const;
|
||||
|
||||
[[nodiscard]] RadialState computeState() const;
|
||||
|
||||
private:
|
||||
crl::time _firstStart = 0;
|
||||
crl::time _lastStart = 0;
|
||||
crl::time _lastTime = 0;
|
||||
float64 _opacity = 0.;
|
||||
anim::value _arcEnd;
|
||||
anim::value _arcStart;
|
||||
Ui::Animations::Basic _animation;
|
||||
bool _finished = false;
|
||||
|
||||
};
|
||||
|
||||
template <typename Callback>
|
||||
inline RadialAnimation::RadialAnimation(Callback &&callback)
|
||||
: _arcStart(0, RadialState::kFull)
|
||||
, _animation(std::forward<Callback>(callback)) {
|
||||
}
|
||||
|
||||
|
||||
class InfiniteRadialAnimation {
|
||||
public:
|
||||
template <typename Callback>
|
||||
InfiniteRadialAnimation(
|
||||
Callback &&callback,
|
||||
const style::InfiniteRadialAnimation &st);
|
||||
|
||||
[[nodiscard]] bool animating() const {
|
||||
return _workStarted && (!_workFinished || _workFinished > crl::now());
|
||||
}
|
||||
|
||||
void start(crl::time skip = 0);
|
||||
void stop(anim::type animated = anim::type::normal);
|
||||
|
||||
void draw(
|
||||
QPainter &p,
|
||||
QPoint position,
|
||||
int outerWidth);
|
||||
void draw(
|
||||
QPainter &p,
|
||||
QPoint position,
|
||||
QSize size,
|
||||
int outerWidth);
|
||||
|
||||
static void Draw(
|
||||
QPainter &p,
|
||||
const RadialState &state,
|
||||
QPoint position,
|
||||
QSize size,
|
||||
int outerWidth,
|
||||
QPen pen,
|
||||
int thickness);
|
||||
|
||||
[[nodiscard]] RadialState computeState();
|
||||
|
||||
private:
|
||||
void init();
|
||||
|
||||
const style::InfiniteRadialAnimation &_st;
|
||||
crl::time _workStarted = 0;
|
||||
crl::time _workFinished = 0;
|
||||
Ui::Animations::Basic _animation;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
template <typename Callback>
|
||||
inline InfiniteRadialAnimation::InfiniteRadialAnimation(
|
||||
Callback &&callback,
|
||||
const style::InfiniteRadialAnimation &st)
|
||||
: _st(st)
|
||||
, _animation(std::forward<Callback>(callback)) {
|
||||
init();
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
324
Telegram/lib_ui/ui/effects/ripple_animation.cpp
Normal file
324
Telegram/lib_ui/ui/effects/ripple_animation.cpp
Normal file
@@ -0,0 +1,324 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RippleAnimation::Ripple {
|
||||
public:
|
||||
Ripple(
|
||||
const style::RippleAnimation &st,
|
||||
QPoint origin,
|
||||
int startRadius,
|
||||
const QPixmap &mask,
|
||||
Fn<void()> update);
|
||||
Ripple(
|
||||
const style::RippleAnimation &st,
|
||||
const QPixmap &mask,
|
||||
Fn<void()> update);
|
||||
|
||||
void paint(
|
||||
QPainter &p,
|
||||
const QPixmap &mask,
|
||||
const QColor *colorOverride);
|
||||
|
||||
void stop();
|
||||
void unstop();
|
||||
void finish();
|
||||
void clearCache();
|
||||
bool finished() const {
|
||||
return _hiding && !_hide.animating();
|
||||
}
|
||||
|
||||
private:
|
||||
const style::RippleAnimation &_st;
|
||||
Fn<void()> _update;
|
||||
|
||||
QPoint _origin;
|
||||
int _radiusFrom = 0;
|
||||
int _radiusTo = 0;
|
||||
|
||||
bool _hiding = false;
|
||||
Ui::Animations::Simple _show;
|
||||
Ui::Animations::Simple _hide;
|
||||
QPixmap _cache;
|
||||
QImage _frame;
|
||||
|
||||
};
|
||||
|
||||
RippleAnimation::Ripple::Ripple(
|
||||
const style::RippleAnimation &st,
|
||||
QPoint origin,
|
||||
int startRadius,
|
||||
const QPixmap &mask,
|
||||
Fn<void()> update)
|
||||
: _st(st)
|
||||
, _update(std::move(update))
|
||||
, _origin(origin)
|
||||
, _radiusFrom(startRadius)
|
||||
, _frame(mask.size(), QImage::Format_ARGB32_Premultiplied) {
|
||||
_frame.setDevicePixelRatio(mask.devicePixelRatio());
|
||||
|
||||
const auto pixelRatio = style::DevicePixelRatio();
|
||||
QPoint points[] = {
|
||||
{ 0, 0 },
|
||||
{ _frame.width() / pixelRatio, 0 },
|
||||
{ _frame.width() / pixelRatio, _frame.height() / pixelRatio },
|
||||
{ 0, _frame.height() / pixelRatio },
|
||||
};
|
||||
for (auto point : points) {
|
||||
accumulate_max(
|
||||
_radiusTo,
|
||||
style::point::dotProduct(_origin - point, _origin - point));
|
||||
}
|
||||
_radiusTo = qRound(sqrt(_radiusTo));
|
||||
|
||||
_show.start(_update, 0., 1., _st.showDuration, anim::easeOutQuint);
|
||||
}
|
||||
|
||||
RippleAnimation::Ripple::Ripple(const style::RippleAnimation &st, const QPixmap &mask, Fn<void()> update)
|
||||
: _st(st)
|
||||
, _update(std::move(update))
|
||||
, _origin(
|
||||
mask.width() / (2 * style::DevicePixelRatio()),
|
||||
mask.height() / (2 * style::DevicePixelRatio()))
|
||||
, _radiusFrom(mask.width() + mask.height())
|
||||
, _frame(mask.size(), QImage::Format_ARGB32_Premultiplied) {
|
||||
_frame.setDevicePixelRatio(mask.devicePixelRatio());
|
||||
_radiusTo = _radiusFrom;
|
||||
_hide.start(_update, 0., 1., _st.hideDuration);
|
||||
}
|
||||
|
||||
void RippleAnimation::Ripple::paint(
|
||||
QPainter &p,
|
||||
const QPixmap &mask,
|
||||
const QColor *colorOverride) {
|
||||
auto opacity = _hide.value(_hiding ? 0. : 1.);
|
||||
if (opacity == 0.) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cache.isNull() || colorOverride != nullptr) {
|
||||
const auto shown = _show.value(1.);
|
||||
Assert(!std::isnan(shown));
|
||||
const auto diff = float64(_radiusTo - _radiusFrom);
|
||||
Assert(!std::isnan(diff));
|
||||
const auto mult = diff * shown;
|
||||
Assert(!std::isnan(mult));
|
||||
const auto interpolated = _radiusFrom + mult;
|
||||
//anim::interpolateF(_radiusFrom, _radiusTo, shown);
|
||||
Assert(!std::isnan(interpolated));
|
||||
auto radius = int(base::SafeRound(interpolated));
|
||||
//anim::interpolate(_radiusFrom, _radiusTo, _show.value(1.));
|
||||
_frame.fill(Qt::transparent);
|
||||
{
|
||||
QPainter p(&_frame);
|
||||
p.setPen(Qt::NoPen);
|
||||
if (colorOverride) {
|
||||
p.setBrush(*colorOverride);
|
||||
} else {
|
||||
p.setBrush(_st.color);
|
||||
}
|
||||
{
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.drawEllipse(_origin, radius, radius);
|
||||
}
|
||||
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
|
||||
p.drawPixmap(0, 0, mask);
|
||||
}
|
||||
if (radius == _radiusTo && colorOverride == nullptr) {
|
||||
_cache = PixmapFromImage(std::move(_frame));
|
||||
}
|
||||
}
|
||||
auto saved = p.opacity();
|
||||
if (opacity != 1.) p.setOpacity(saved * opacity);
|
||||
if (_cache.isNull()) {
|
||||
p.drawImage(0, 0, _frame);
|
||||
} else {
|
||||
p.drawPixmap(0, 0, _cache);
|
||||
}
|
||||
if (opacity != 1.) p.setOpacity(saved);
|
||||
}
|
||||
|
||||
void RippleAnimation::Ripple::stop() {
|
||||
_hiding = true;
|
||||
_hide.start(_update, 1., 0., _st.hideDuration);
|
||||
}
|
||||
|
||||
void RippleAnimation::Ripple::unstop() {
|
||||
if (_hiding) {
|
||||
if (_hide.animating()) {
|
||||
_hide.start(_update, 0., 1., _st.hideDuration);
|
||||
}
|
||||
_hiding = false;
|
||||
}
|
||||
}
|
||||
|
||||
void RippleAnimation::Ripple::finish() {
|
||||
if (_update) {
|
||||
_update();
|
||||
}
|
||||
_show.stop();
|
||||
_hide.stop();
|
||||
}
|
||||
|
||||
void RippleAnimation::Ripple::clearCache() {
|
||||
_cache = QPixmap();
|
||||
}
|
||||
|
||||
RippleAnimation::RippleAnimation(
|
||||
const style::RippleAnimation &st,
|
||||
QImage mask,
|
||||
Fn<void()> callback)
|
||||
: _st(st)
|
||||
, _mask(PixmapFromImage(std::move(mask)))
|
||||
, _update(std::move(callback)) {
|
||||
}
|
||||
|
||||
|
||||
void RippleAnimation::add(QPoint origin, int startRadius) {
|
||||
lastStop();
|
||||
_ripples.push_back(
|
||||
std::make_unique<Ripple>(_st, origin, startRadius, _mask, _update));
|
||||
}
|
||||
|
||||
void RippleAnimation::addFading() {
|
||||
lastStop();
|
||||
_ripples.push_back(std::make_unique<Ripple>(_st, _mask, _update));
|
||||
}
|
||||
|
||||
void RippleAnimation::lastStop() {
|
||||
if (!_ripples.empty()) {
|
||||
_ripples.back()->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void RippleAnimation::lastUnstop() {
|
||||
if (!_ripples.empty()) {
|
||||
_ripples.back()->unstop();
|
||||
}
|
||||
}
|
||||
|
||||
void RippleAnimation::lastFinish() {
|
||||
if (!_ripples.empty()) {
|
||||
_ripples.back()->finish();
|
||||
}
|
||||
}
|
||||
|
||||
void RippleAnimation::forceRepaint() {
|
||||
for (const auto &ripple : _ripples) {
|
||||
ripple->clearCache();
|
||||
}
|
||||
if (_update) {
|
||||
_update();
|
||||
}
|
||||
}
|
||||
|
||||
void RippleAnimation::paint(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
const QColor *colorOverride) {
|
||||
if (_ripples.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (style::RightToLeft()) {
|
||||
x = outerWidth - x - (_mask.width() / style::DevicePixelRatio());
|
||||
}
|
||||
p.translate(x, y);
|
||||
for (const auto &ripple : _ripples) {
|
||||
ripple->paint(p, _mask, colorOverride);
|
||||
}
|
||||
p.translate(-x, -y);
|
||||
clearFinished();
|
||||
}
|
||||
|
||||
QImage RippleAnimation::MaskByDrawer(
|
||||
QSize size,
|
||||
bool filled,
|
||||
Fn<void(QPainter &p)> drawer) {
|
||||
auto result = QImage(
|
||||
size * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
result.fill(filled ? QColor(255, 255, 255) : Qt::transparent);
|
||||
if (drawer) {
|
||||
Painter p(&result);
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(QColor(255, 255, 255));
|
||||
drawer(p);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QImage RippleAnimation::RectMask(QSize size) {
|
||||
return MaskByDrawer(size, true, nullptr);
|
||||
}
|
||||
|
||||
QImage RippleAnimation::RoundRectMask(QSize size, int radius) {
|
||||
return MaskByDrawer(size, false, [&](QPainter &p) {
|
||||
p.drawRoundedRect(0, 0, size.width(), size.height(), radius, radius);
|
||||
});
|
||||
}
|
||||
|
||||
QImage RippleAnimation::RoundRectMask(
|
||||
QSize size,
|
||||
Images::CornersMaskRef corners) {
|
||||
return MaskByDrawer(size, true, [&](QPainter &p) {
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto corner = [&](int index, bool right, bool bottom) {
|
||||
if (const auto image = corners.p[index]) {
|
||||
if (!image->isNull()) {
|
||||
const auto width = image->width() / ratio;
|
||||
const auto height = image->height() / ratio;
|
||||
p.drawImage(
|
||||
QRect(
|
||||
right ? (size.width() - width) : 0,
|
||||
bottom ? (size.height() - height) : 0,
|
||||
width,
|
||||
height),
|
||||
*image);
|
||||
}
|
||||
}
|
||||
};
|
||||
corner(0, false, false);
|
||||
corner(1, true, false);
|
||||
corner(2, false, true);
|
||||
corner(3, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
QImage RippleAnimation::EllipseMask(QSize size) {
|
||||
return MaskByDrawer(size, false, [&](QPainter &p) {
|
||||
p.drawEllipse(0, 0, size.width(), size.height());
|
||||
});
|
||||
}
|
||||
|
||||
void RippleAnimation::clearFinished() {
|
||||
while (!_ripples.empty() && _ripples.front()->finished()) {
|
||||
_ripples.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
void RippleAnimation::clear() {
|
||||
_ripples.clear();
|
||||
}
|
||||
|
||||
RippleAnimation::~RippleAnimation() = default;
|
||||
|
||||
} // namespace Ui
|
||||
72
Telegram/lib_ui/ui/effects/ripple_animation.h
Normal file
72
Telegram/lib_ui/ui/effects/ripple_animation.h
Normal file
@@ -0,0 +1,72 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
|
||||
namespace Images {
|
||||
struct CornersMaskRef;
|
||||
} // namespace Images
|
||||
|
||||
namespace style {
|
||||
struct RippleAnimation;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RippleAnimation {
|
||||
public:
|
||||
// White upon transparent mask,
|
||||
// like colorizeImage(black-white-mask, white).
|
||||
RippleAnimation(
|
||||
const style::RippleAnimation &st,
|
||||
QImage mask,
|
||||
Fn<void()> update);
|
||||
|
||||
void add(QPoint origin, int startRadius = 0);
|
||||
void addFading();
|
||||
void lastStop();
|
||||
void lastUnstop();
|
||||
void lastFinish();
|
||||
void forceRepaint();
|
||||
|
||||
void paint(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
const QColor *colorOverride = nullptr);
|
||||
|
||||
bool empty() const {
|
||||
return _ripples.empty();
|
||||
}
|
||||
|
||||
static QImage MaskByDrawer(
|
||||
QSize size,
|
||||
bool filled,
|
||||
Fn<void(QPainter &p)> drawer);
|
||||
static QImage RectMask(QSize size);
|
||||
static QImage RoundRectMask(QSize size, int radius);
|
||||
static QImage RoundRectMask(QSize size, Images::CornersMaskRef corners);
|
||||
static QImage EllipseMask(QSize size);
|
||||
|
||||
~RippleAnimation();
|
||||
|
||||
private:
|
||||
void clear();
|
||||
void clearFinished();
|
||||
|
||||
const style::RippleAnimation &_st;
|
||||
QPixmap _mask;
|
||||
Fn<void()> _update;
|
||||
|
||||
class Ripple;
|
||||
std::deque<std::unique_ptr<Ripple>> _ripples;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
471
Telegram/lib_ui/ui/effects/round_area_with_shadow.cpp
Normal file
471
Telegram/lib_ui/ui/effects/round_area_with_shadow.cpp
Normal file
@@ -0,0 +1,471 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/round_area_with_shadow.h"
|
||||
|
||||
#include "ui/style/style_core.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/painter.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kBgCacheIndex = 0;
|
||||
constexpr auto kShadowCacheIndex = 0;
|
||||
constexpr auto kOverlayMaskCacheIndex = 0;
|
||||
constexpr auto kOverlayShadowCacheIndex = 1;
|
||||
constexpr auto kOverlayCacheColumsCount = 2;
|
||||
constexpr auto kDivider = 4;
|
||||
|
||||
} // namespace
|
||||
|
||||
[[nodiscard]] QImage RoundAreaWithShadow::PrepareImage(QSize size) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
auto result = QImage(
|
||||
size * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result.setDevicePixelRatio(ratio);
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage RoundAreaWithShadow::PrepareFramesCache(
|
||||
QSize frame,
|
||||
int columns) {
|
||||
static_assert(!(kFramesCount % kDivider));
|
||||
|
||||
return PrepareImage(QSize(
|
||||
frame.width() * kDivider * columns,
|
||||
frame.height() * kFramesCount / kDivider));
|
||||
}
|
||||
|
||||
[[nodiscard]] QRect RoundAreaWithShadow::FrameCacheRect(
|
||||
int frameIndex,
|
||||
int column,
|
||||
QSize frame) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto origin = QPoint(
|
||||
frame.width() * (kDivider * column + (frameIndex % kDivider)),
|
||||
frame.height() * (frameIndex / kDivider));
|
||||
return QRect(ratio * origin, ratio * frame);
|
||||
}
|
||||
|
||||
RoundAreaWithShadow::RoundAreaWithShadow(
|
||||
QSize inner,
|
||||
QMargins shadow,
|
||||
int twiceRadiusMax)
|
||||
: _inner({}, inner)
|
||||
, _outer(_inner.marginsAdded(shadow).size())
|
||||
, _overlay(QRect(
|
||||
0,
|
||||
0,
|
||||
std::max(inner.width(), twiceRadiusMax),
|
||||
std::max(inner.height(), twiceRadiusMax)).marginsAdded(shadow).size())
|
||||
, _cacheBg(PrepareFramesCache(_outer))
|
||||
, _shadowParts(PrepareFramesCache(_outer))
|
||||
, _overlayCacheParts(PrepareFramesCache(_overlay, kOverlayCacheColumsCount))
|
||||
, _overlayMaskScaled(PrepareImage(_overlay))
|
||||
, _overlayShadowScaled(PrepareImage(_overlay))
|
||||
, _shadowBuffer(PrepareImage(_outer)) {
|
||||
_inner.translate(QRect({}, _outer).center() - _inner.center());
|
||||
}
|
||||
|
||||
ImageSubrect RoundAreaWithShadow::validateOverlayMask(
|
||||
int frameIndex,
|
||||
QSize innerSize,
|
||||
float64 radius,
|
||||
int twiceRadius,
|
||||
float64 scale) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto cached = (scale == 1.);
|
||||
const auto full = cached
|
||||
? FrameCacheRect(frameIndex, kOverlayMaskCacheIndex, _overlay)
|
||||
: QRect(QPoint(), _overlay * ratio);
|
||||
|
||||
const auto minWidth = twiceRadius + _outer.width() - _inner.width();
|
||||
const auto minHeight = twiceRadius + _outer.height() - _inner.height();
|
||||
const auto maskSize = QSize(
|
||||
std::max(_outer.width(), minWidth),
|
||||
std::max(_outer.height(), minHeight));
|
||||
|
||||
const auto result = ImageSubrect{
|
||||
cached ? &_overlayCacheParts : &_overlayMaskScaled,
|
||||
QRect(full.topLeft(), maskSize * ratio),
|
||||
};
|
||||
if (cached && _validOverlayMask[frameIndex]) {
|
||||
return result;
|
||||
}
|
||||
|
||||
auto p = QPainter(result.image.get());
|
||||
const auto position = full.topLeft() / ratio;
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
p.fillRect(QRect(position, maskSize), Qt::transparent);
|
||||
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto inner = QRect(position + _inner.topLeft(), innerSize);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(Qt::white);
|
||||
if (scale != 1.) {
|
||||
const auto center = inner.center();
|
||||
p.save();
|
||||
p.translate(center);
|
||||
p.scale(scale, scale);
|
||||
p.translate(-center);
|
||||
}
|
||||
p.drawRoundedRect(inner, radius, radius);
|
||||
if (scale != 1.) {
|
||||
p.restore();
|
||||
}
|
||||
|
||||
if (cached) {
|
||||
_validOverlayMask[frameIndex] = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ImageSubrect RoundAreaWithShadow::validateOverlayShadow(
|
||||
int frameIndex,
|
||||
QSize innerSize,
|
||||
float64 radius,
|
||||
int twiceRadius,
|
||||
float64 scale,
|
||||
const ImageSubrect &mask) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto cached = (scale == 1.);
|
||||
const auto full = cached
|
||||
? FrameCacheRect(frameIndex, kOverlayShadowCacheIndex, _overlay)
|
||||
: QRect(QPoint(), _overlay * ratio);
|
||||
|
||||
const auto minWidth = twiceRadius + _outer.width() - _inner.width();
|
||||
const auto minHeight = twiceRadius + _outer.height() - _inner.height();
|
||||
const auto maskSize = QSize(
|
||||
std::max(_outer.width(), minWidth),
|
||||
std::max(_outer.height(), minHeight));
|
||||
|
||||
const auto result = ImageSubrect{
|
||||
cached ? &_overlayCacheParts : &_overlayShadowScaled,
|
||||
QRect(full.topLeft(), maskSize * ratio),
|
||||
};
|
||||
if (cached && _validOverlayShadow[frameIndex]) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto position = full.topLeft() / ratio;
|
||||
|
||||
_overlayShadowScaled.fill(Qt::transparent);
|
||||
const auto inner = QRect(_inner.topLeft(), innerSize);
|
||||
const auto add = style::ConvertScale(2.5);
|
||||
const auto shift = style::ConvertScale(0.5);
|
||||
const auto extended = QRectF(inner).marginsAdded({ add, add, add, add });
|
||||
{
|
||||
auto p = QPainter(&_overlayShadowScaled);
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(_shadow);
|
||||
if (scale != 1.) {
|
||||
const auto center = inner.center();
|
||||
p.translate(center);
|
||||
p.scale(scale, scale);
|
||||
p.translate(-center);
|
||||
}
|
||||
p.drawRoundedRect(extended.translated(0, shift), radius, radius);
|
||||
p.end();
|
||||
}
|
||||
|
||||
_overlayShadowScaled = Images::Blur(std::move(_overlayShadowScaled));
|
||||
|
||||
auto q = Painter(result.image);
|
||||
if (result.image != &_overlayShadowScaled) {
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
q.drawImage(
|
||||
QRect(position, maskSize),
|
||||
_overlayShadowScaled,
|
||||
QRect(QPoint(), maskSize * ratio));
|
||||
}
|
||||
q.setCompositionMode(QPainter::CompositionMode_DestinationOut);
|
||||
q.drawImage(QRect(position, maskSize), *mask.image, mask.rect);
|
||||
|
||||
if (cached) {
|
||||
_validOverlayShadow[frameIndex] = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void RoundAreaWithShadow::overlayExpandedBorder(
|
||||
QPainter &p,
|
||||
QSize size,
|
||||
float64 expandRatio,
|
||||
float64 radiusFrom,
|
||||
float64 radiusTill,
|
||||
float64 scale) {
|
||||
const auto progress = expandRatio;
|
||||
const auto frame = int(base::SafeRound(progress * (kFramesCount - 1)));
|
||||
const auto cacheRatio = frame / float64(kFramesCount - 1);
|
||||
const auto radius = radiusFrom + (radiusTill - radiusFrom) * cacheRatio;
|
||||
const auto twiceRadius = int(base::SafeRound(radius * 2));
|
||||
const auto innerSize = QSize(
|
||||
std::max(_inner.width(), twiceRadius),
|
||||
std::max(_inner.height(), twiceRadius));
|
||||
|
||||
const auto overlayMask = validateOverlayMask(
|
||||
frame,
|
||||
innerSize,
|
||||
radius,
|
||||
twiceRadius,
|
||||
scale);
|
||||
const auto overlayShadow = validateOverlayShadow(
|
||||
frame,
|
||||
innerSize,
|
||||
radius,
|
||||
twiceRadius,
|
||||
scale,
|
||||
overlayMask);
|
||||
|
||||
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
|
||||
FillWithImage(p, QRect(QPoint(), size), overlayMask);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
FillWithImage(p, QRect(QPoint(), size), overlayShadow);
|
||||
}
|
||||
|
||||
QRect RoundAreaWithShadow::FillWithImage(
|
||||
QPainter &p,
|
||||
QRect geometry,
|
||||
const ImageSubrect &pattern) {
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto &image = *pattern.image;
|
||||
const auto source = pattern.rect;
|
||||
const auto sourceWidth = (source.width() / factor);
|
||||
const auto sourceHeight = (source.height() / factor);
|
||||
if (geometry.width() == sourceWidth) {
|
||||
const auto part = (sourceHeight / 2) - 1;
|
||||
const auto fill = geometry.height() - 2 * part;
|
||||
const auto half = part * factor;
|
||||
const auto top = source.height() - half;
|
||||
p.drawImage(
|
||||
geometry.topLeft(),
|
||||
image,
|
||||
QRect(source.x(), source.y(), source.width(), half));
|
||||
if (fill > 0) {
|
||||
p.drawImage(
|
||||
QRect(
|
||||
geometry.topLeft() + QPoint(0, part),
|
||||
QSize(sourceWidth, fill)),
|
||||
image,
|
||||
QRect(
|
||||
source.x(),
|
||||
source.y() + half,
|
||||
source.width(),
|
||||
top - half));
|
||||
}
|
||||
p.drawImage(
|
||||
geometry.topLeft() + QPoint(0, part + fill),
|
||||
image,
|
||||
QRect(source.x(), source.y() + top, source.width(), half));
|
||||
return QRect();
|
||||
} else if (geometry.height() == sourceHeight) {
|
||||
const auto part = (sourceWidth / 2) - 1;
|
||||
const auto fill = geometry.width() - 2 * part;
|
||||
const auto half = part * factor;
|
||||
const auto left = source.width() - half;
|
||||
p.drawImage(
|
||||
geometry.topLeft(),
|
||||
image,
|
||||
QRect(source.x(), source.y(), half, source.height()));
|
||||
if (fill > 0) {
|
||||
p.drawImage(
|
||||
QRect(
|
||||
geometry.topLeft() + QPoint(part, 0),
|
||||
QSize(fill, sourceHeight)),
|
||||
image,
|
||||
QRect(
|
||||
source.x() + half,
|
||||
source.y(),
|
||||
left - half,
|
||||
source.height()));
|
||||
}
|
||||
p.drawImage(
|
||||
geometry.topLeft() + QPoint(part + fill, 0),
|
||||
image,
|
||||
QRect(source.x() + left, source.y(), half, source.height()));
|
||||
return QRect();
|
||||
} else if (geometry.width() > sourceWidth
|
||||
&& geometry.height() > sourceHeight) {
|
||||
const auto xpart = (sourceWidth / 2) - 1;
|
||||
const auto xfill = geometry.width() - 2 * xpart;
|
||||
const auto xhalf = xpart * factor;
|
||||
const auto left = source.width() - xhalf;
|
||||
const auto ypart = (sourceHeight / 2) - 1;
|
||||
const auto yfill = geometry.height() - 2 * ypart;
|
||||
const auto yhalf = ypart * factor;
|
||||
const auto top = source.height() - yhalf;
|
||||
p.drawImage(
|
||||
geometry.topLeft(),
|
||||
image,
|
||||
QRect(source.x(), source.y(), xhalf, yhalf));
|
||||
if (xfill > 0) {
|
||||
p.drawImage(
|
||||
QRect(
|
||||
geometry.topLeft() + QPoint(xpart, 0),
|
||||
QSize(xfill, ypart)),
|
||||
image,
|
||||
QRect(
|
||||
source.x() + xhalf,
|
||||
source.y(),
|
||||
left - xhalf,
|
||||
yhalf));
|
||||
}
|
||||
p.drawImage(
|
||||
geometry.topLeft() + QPoint(xpart + xfill, 0),
|
||||
image,
|
||||
QRect(source.x() + left, source.y(), xhalf, yhalf));
|
||||
|
||||
if (yfill > 0) {
|
||||
p.drawImage(
|
||||
QRect(
|
||||
geometry.topLeft() + QPoint(0, ypart),
|
||||
QSize(xpart, yfill)),
|
||||
image,
|
||||
QRect(
|
||||
source.x(),
|
||||
source.y() + yhalf,
|
||||
xhalf,
|
||||
top - yhalf));
|
||||
p.drawImage(
|
||||
QRect(
|
||||
geometry.topLeft() + QPoint(xpart + xfill, ypart),
|
||||
QSize(xpart, yfill)),
|
||||
image,
|
||||
QRect(
|
||||
source.x() + left,
|
||||
source.y() + yhalf,
|
||||
xhalf,
|
||||
top - yhalf));
|
||||
}
|
||||
|
||||
p.drawImage(
|
||||
geometry.topLeft() + QPoint(0, ypart + yfill),
|
||||
image,
|
||||
QRect(source.x(), source.y() + top, xhalf, yhalf));
|
||||
if (xfill > 0) {
|
||||
p.drawImage(
|
||||
QRect(
|
||||
geometry.topLeft() + QPoint(xpart, ypart + yfill),
|
||||
QSize(xfill, ypart)),
|
||||
image,
|
||||
QRect(
|
||||
source.x() + xhalf,
|
||||
source.y() + top,
|
||||
left - xhalf,
|
||||
yhalf));
|
||||
}
|
||||
p.drawImage(
|
||||
geometry.topLeft() + QPoint(xpart + xfill, ypart + yfill),
|
||||
image,
|
||||
QRect(source.x() + left, source.y() + top, xhalf, yhalf));
|
||||
|
||||
return QRect(
|
||||
geometry.topLeft() + QPoint(xpart, ypart),
|
||||
QSize(xfill, yfill));
|
||||
} else {
|
||||
Unexpected("Values in RoundAreaWithShadow::fillWithImage.");
|
||||
}
|
||||
}
|
||||
|
||||
void RoundAreaWithShadow::setShadowColor(const QColor &shadow) {
|
||||
if (_shadow == shadow) {
|
||||
return;
|
||||
}
|
||||
_shadow = shadow;
|
||||
ranges::fill(_validBg, false);
|
||||
ranges::fill(_validShadow, false);
|
||||
ranges::fill(_validOverlayShadow, false);
|
||||
}
|
||||
|
||||
QRect RoundAreaWithShadow::validateShadow(
|
||||
int frameIndex,
|
||||
float64 scale,
|
||||
float64 radius) {
|
||||
const auto rect = FrameCacheRect(frameIndex, kShadowCacheIndex, _outer);
|
||||
if (_validShadow[frameIndex]) {
|
||||
return rect;
|
||||
}
|
||||
|
||||
_shadowBuffer.fill(Qt::transparent);
|
||||
auto p = QPainter(&_shadowBuffer);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto center = _inner.center();
|
||||
const auto add = style::ConvertScale(2.5);
|
||||
const auto shift = style::ConvertScale(0.5);
|
||||
const auto big = QRectF(_inner).marginsAdded({ add, add, add, add });
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(_shadow);
|
||||
if (scale != 1.) {
|
||||
p.translate(center);
|
||||
p.scale(scale, scale);
|
||||
p.translate(-center);
|
||||
}
|
||||
p.drawRoundedRect(big.translated(0, shift), radius, radius);
|
||||
p.end();
|
||||
_shadowBuffer = Images::Blur(std::move(_shadowBuffer));
|
||||
|
||||
auto q = QPainter(&_shadowParts);
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
q.drawImage(rect.topLeft() / style::DevicePixelRatio(), _shadowBuffer);
|
||||
|
||||
_validShadow[frameIndex] = true;
|
||||
return rect;
|
||||
}
|
||||
|
||||
void RoundAreaWithShadow::setBackgroundColor(const QColor &background) {
|
||||
if (_background == background) {
|
||||
return;
|
||||
}
|
||||
_background = background;
|
||||
ranges::fill(_validBg, false);
|
||||
}
|
||||
|
||||
ImageSubrect RoundAreaWithShadow::validateFrame(
|
||||
int frameIndex,
|
||||
float64 scale,
|
||||
float64 radius) {
|
||||
const auto result = ImageSubrect{
|
||||
&_cacheBg,
|
||||
FrameCacheRect(frameIndex, kBgCacheIndex, _outer)
|
||||
};
|
||||
if (_validBg[frameIndex]) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto position = result.rect.topLeft() / style::DevicePixelRatio();
|
||||
const auto inner = _inner.translated(position);
|
||||
const auto shadowSource = validateShadow(frameIndex, scale, radius);
|
||||
|
||||
auto p = QPainter(&_cacheBg);
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
p.drawImage(position, _shadowParts, shadowSource);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(_background);
|
||||
if (scale != 1.) {
|
||||
const auto center = inner.center();
|
||||
p.save();
|
||||
p.translate(center);
|
||||
p.scale(scale, scale);
|
||||
p.translate(-center);
|
||||
}
|
||||
p.drawRoundedRect(inner, radius, radius);
|
||||
if (scale != 1.) {
|
||||
p.restore();
|
||||
}
|
||||
|
||||
_validBg[frameIndex] = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
92
Telegram/lib_ui/ui/effects/round_area_with_shadow.h
Normal file
92
Telegram/lib_ui/ui/effects/round_area_with_shadow.h
Normal file
@@ -0,0 +1,92 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct ImageSubrect {
|
||||
not_null<QImage*> image;
|
||||
QRect rect;
|
||||
};
|
||||
|
||||
class RoundAreaWithShadow final {
|
||||
public:
|
||||
static constexpr auto kFramesCount = 32;
|
||||
|
||||
[[nodiscard]] static QImage PrepareImage(QSize size);
|
||||
[[nodiscard]] static QImage PrepareFramesCache(
|
||||
QSize frame,
|
||||
int columns = 1);
|
||||
[[nodiscard]] static QRect FrameCacheRect(
|
||||
int frameIndex,
|
||||
int column,
|
||||
QSize frame);
|
||||
|
||||
// Returns center area which could be just filled with a solid color.
|
||||
static QRect FillWithImage(
|
||||
QPainter &p,
|
||||
QRect geometry,
|
||||
const ImageSubrect &pattern);
|
||||
|
||||
RoundAreaWithShadow(QSize inner, QMargins shadow, int twiceRadiusMax);
|
||||
|
||||
void setBackgroundColor(const QColor &background);
|
||||
void setShadowColor(const QColor &shadow);
|
||||
|
||||
[[nodiscard]] ImageSubrect validateFrame(
|
||||
int frameIndex,
|
||||
float64 scale,
|
||||
float64 radius);
|
||||
[[nodiscard]] ImageSubrect validateOverlayMask(
|
||||
int frameIndex,
|
||||
QSize innerSize,
|
||||
float64 radius,
|
||||
int twiceRadius,
|
||||
float64 scale);
|
||||
[[nodiscard]] ImageSubrect validateOverlayShadow(
|
||||
int frameIndex,
|
||||
QSize innerSize,
|
||||
float64 radius,
|
||||
int twiceRadius,
|
||||
float64 scale,
|
||||
const ImageSubrect &mask);
|
||||
|
||||
void overlayExpandedBorder(
|
||||
QPainter &p,
|
||||
QSize size,
|
||||
float64 expandRatio,
|
||||
float64 radiusFrom,
|
||||
float64 radiusTill,
|
||||
float64 scale);
|
||||
|
||||
private:
|
||||
[[nodiscard]] QRect validateShadow(
|
||||
int frameIndex,
|
||||
float64 scale,
|
||||
float64 radius);
|
||||
|
||||
QRect _inner;
|
||||
QSize _outer;
|
||||
QSize _overlay;
|
||||
|
||||
std::array<bool, kFramesCount> _validBg = { { false } };
|
||||
std::array<bool, kFramesCount> _validShadow = { { false } };
|
||||
std::array<bool, kFramesCount> _validOverlayMask = { { false } };
|
||||
std::array<bool, kFramesCount> _validOverlayShadow = { { false } };
|
||||
QColor _background;
|
||||
QColor _gradient;
|
||||
QColor _shadow;
|
||||
QImage _cacheBg;
|
||||
QImage _shadowParts;
|
||||
QImage _overlayCacheParts;
|
||||
QImage _overlayMaskScaled;
|
||||
QImage _overlayShadowScaled;
|
||||
QImage _shadowBuffer;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
113
Telegram/lib_ui/ui/effects/show_animation.cpp
Normal file
113
Telegram/lib_ui/ui/effects/show_animation.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/show_animation.h"
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui::Animations {
|
||||
namespace {
|
||||
|
||||
void AnimateWidgets(const Widgets &targets, bool show) {
|
||||
enum class Finish {
|
||||
Bad,
|
||||
Good,
|
||||
};
|
||||
struct Object {
|
||||
base::unique_qptr<Ui::RpWidget> container;
|
||||
base::weak_qptr<Ui::RpWidget> weakTarget;
|
||||
};
|
||||
struct State {
|
||||
rpl::event_stream<Finish> destroy;
|
||||
Ui::Animations::Simple animation;
|
||||
std::vector<Object> objects;
|
||||
};
|
||||
auto lifetime = std::make_shared<rpl::lifetime>();
|
||||
const auto state = lifetime->make_state<State>();
|
||||
|
||||
const auto from = show ? 0. : 1.;
|
||||
const auto to = show ? 1. : 0.;
|
||||
|
||||
for (const auto &target : targets) {
|
||||
state->objects.push_back({
|
||||
base::make_unique_q<Ui::RpWidget>(target->parentWidget()),
|
||||
base::make_weak(target),
|
||||
});
|
||||
|
||||
const auto pixmap = Ui::GrabWidget(target);
|
||||
const auto raw = state->objects.back().container.get();
|
||||
|
||||
raw->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
QPainter p(raw);
|
||||
|
||||
p.setOpacity(state->animation.value(to));
|
||||
p.drawPixmap(QPoint(), pixmap);
|
||||
}, raw->lifetime());
|
||||
|
||||
target->geometryValue(
|
||||
) | rpl::on_next([=](const QRect &r) {
|
||||
raw->setGeometry(r);
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->show();
|
||||
|
||||
if (!show) {
|
||||
target->hide();
|
||||
}
|
||||
}
|
||||
|
||||
state->destroy.events(
|
||||
) | rpl::take(
|
||||
1
|
||||
) | rpl::on_next([=](Finish type) mutable {
|
||||
if (type == Finish::Good && show) {
|
||||
for (const auto &object : state->objects) {
|
||||
if (object.weakTarget) {
|
||||
object.weakTarget->show();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lifetime) {
|
||||
base::take(lifetime)->destroy();
|
||||
}
|
||||
}, *lifetime);
|
||||
|
||||
state->animation.start(
|
||||
[=](auto value) {
|
||||
for (const auto &object : state->objects) {
|
||||
if (object.container) {
|
||||
object.container->update();
|
||||
}
|
||||
|
||||
if (!object.weakTarget && show) {
|
||||
state->destroy.fire(Finish::Bad);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (value == to) {
|
||||
state->destroy.fire(Finish::Good);
|
||||
}
|
||||
},
|
||||
from,
|
||||
to,
|
||||
st::defaultToggle.duration);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ShowWidgets(const Widgets &targets) {
|
||||
AnimateWidgets(targets, true);
|
||||
}
|
||||
|
||||
void HideWidgets(const Widgets &targets) {
|
||||
AnimateWidgets(targets, false);
|
||||
}
|
||||
|
||||
} // namespace Ui::Animations
|
||||
20
Telegram/lib_ui/ui/effects/show_animation.h
Normal file
20
Telegram/lib_ui/ui/effects/show_animation.h
Normal file
@@ -0,0 +1,20 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Animations {
|
||||
|
||||
using Widgets = std::vector<not_null<Ui::RpWidget*>>;
|
||||
|
||||
void ShowWidgets(const Widgets &targets);
|
||||
void HideWidgets(const Widgets &targets);
|
||||
|
||||
} // namespace Ui::Animations
|
||||
89
Telegram/lib_ui/ui/effects/slide_animation.cpp
Normal file
89
Telegram/lib_ui/ui/effects/slide_animation.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/slide_animation.h"
|
||||
|
||||
#include <QtGui/QPainter>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
void SlideAnimation::setSnapshots(
|
||||
QPixmap leftSnapshot,
|
||||
QPixmap rightSnapshot) {
|
||||
Expects(!leftSnapshot.isNull());
|
||||
Expects(!rightSnapshot.isNull());
|
||||
|
||||
_leftSnapshot = std::move(leftSnapshot);
|
||||
_rightSnapshot = std::move(rightSnapshot);
|
||||
_leftSnapshot.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
_rightSnapshot.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
}
|
||||
|
||||
void SlideAnimation::paintFrame(QPainter &p, int x, int y, int outerWidth) {
|
||||
if (!animating()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto pixelRatio = style::DevicePixelRatio();
|
||||
const auto state = this->state();
|
||||
const auto leftCoord = _slideLeft
|
||||
? anim::interpolate(-_leftSnapshotWidth, 0, state.leftProgress)
|
||||
: anim::interpolate(0, -_leftSnapshotWidth, state.leftProgress);
|
||||
const auto rightCoord = _slideLeft
|
||||
? anim::interpolate(0, _rightSnapshotWidth, state.rightProgress)
|
||||
: anim::interpolate(_rightSnapshotWidth, 0, state.rightProgress);
|
||||
|
||||
if (_overflowHidden) {
|
||||
const auto leftWidth = (_leftSnapshotWidth + leftCoord);
|
||||
if (leftWidth > 0) {
|
||||
p.setOpacity(state.leftAlpha);
|
||||
p.drawPixmap(
|
||||
x,
|
||||
y,
|
||||
leftWidth,
|
||||
_leftSnapshotHeight,
|
||||
_leftSnapshot,
|
||||
(_leftSnapshot.width() - leftWidth * pixelRatio),
|
||||
0,
|
||||
leftWidth * pixelRatio,
|
||||
_leftSnapshot.height());
|
||||
}
|
||||
const auto rightWidth = _rightSnapshotWidth - rightCoord;
|
||||
if (rightWidth > 0) {
|
||||
p.setOpacity(state.rightAlpha);
|
||||
p.drawPixmap(
|
||||
x + rightCoord,
|
||||
y,
|
||||
_rightSnapshot,
|
||||
0,
|
||||
0,
|
||||
rightWidth * pixelRatio,
|
||||
_rightSnapshot.height());
|
||||
}
|
||||
} else {
|
||||
p.setOpacity(state.leftAlpha);
|
||||
p.drawPixmap(x + leftCoord, y, _leftSnapshot);
|
||||
p.setOpacity(state.rightAlpha);
|
||||
p.drawPixmap(x + rightCoord, y, _rightSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
SlideAnimation::State SlideAnimation::state() const {
|
||||
const auto dt = _animation.value(1.);
|
||||
const auto easeOut = anim::easeOutCirc(1., dt);
|
||||
const auto easeIn = anim::easeInCirc(1., dt);
|
||||
const auto arrivingAlpha = easeIn;
|
||||
const auto departingAlpha = 1. - easeOut;
|
||||
|
||||
auto result = State();
|
||||
result.leftProgress = _slideLeft ? easeOut : easeIn;
|
||||
result.leftAlpha = _slideLeft ? arrivingAlpha : departingAlpha;
|
||||
result.rightProgress = _slideLeft ? easeIn : easeOut;
|
||||
result.rightAlpha = _slideLeft ? departingAlpha : arrivingAlpha;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
66
Telegram/lib_ui/ui/effects/slide_animation.h
Normal file
66
Telegram/lib_ui/ui/effects/slide_animation.h
Normal file
@@ -0,0 +1,66 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class SlideAnimation {
|
||||
public:
|
||||
struct State {
|
||||
float64 leftProgress = 0.;
|
||||
float64 leftAlpha = 0.;
|
||||
float64 rightProgress = 0.;
|
||||
float64 rightAlpha = 0.;
|
||||
};
|
||||
|
||||
void setSnapshots(QPixmap leftSnapshot, QPixmap rightSnapshot);
|
||||
|
||||
void setOverflowHidden(bool hidden) {
|
||||
_overflowHidden = hidden;
|
||||
}
|
||||
|
||||
template <typename Lambda>
|
||||
void start(bool slideLeft, Lambda &&updateCallback, float64 duration);
|
||||
|
||||
void paintFrame(QPainter &p, int x, int y, int outerWidth);
|
||||
|
||||
[[nodiscard]] State state() const;
|
||||
[[nodiscard]] bool animating() const {
|
||||
return _animation.animating();
|
||||
}
|
||||
|
||||
private:
|
||||
Ui::Animations::Simple _animation;
|
||||
QPixmap _leftSnapshot;
|
||||
QPixmap _rightSnapshot;
|
||||
bool _slideLeft = false;
|
||||
bool _overflowHidden = true;
|
||||
int _leftSnapshotWidth = 0;
|
||||
int _leftSnapshotHeight = 0;
|
||||
int _rightSnapshotWidth = 0;
|
||||
|
||||
};
|
||||
|
||||
template <typename Lambda>
|
||||
void SlideAnimation::start(
|
||||
bool slideLeft,
|
||||
Lambda &&updateCallback,
|
||||
float64 duration) {
|
||||
_slideLeft = slideLeft;
|
||||
if (_slideLeft) {
|
||||
std::swap(_leftSnapshot, _rightSnapshot);
|
||||
}
|
||||
const auto pixelRatio = style::DevicePixelRatio();
|
||||
_leftSnapshotWidth = _leftSnapshot.width() / pixelRatio;
|
||||
_leftSnapshotHeight = _leftSnapshot.height() / pixelRatio;
|
||||
_rightSnapshotWidth = _rightSnapshot.width() / pixelRatio;
|
||||
_animation.start(std::forward<Lambda>(updateCallback), 0., 1., duration);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
878
Telegram/lib_ui/ui/effects/spoiler_mess.cpp
Normal file
878
Telegram/lib_ui/ui/effects/spoiler_mess.cpp
Normal file
@@ -0,0 +1,878 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/integration.h"
|
||||
#include "base/random.h"
|
||||
#include "base/flags.h"
|
||||
|
||||
#include <QtCore/QBuffer>
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QDir>
|
||||
|
||||
#include <crl/crl_async.h>
|
||||
#include <xxhash.h>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kVersion = 2;
|
||||
constexpr auto kFramesPerRow = 10;
|
||||
constexpr auto kImageSpoilerDarkenAlpha = 32;
|
||||
constexpr auto kMaxCacheSize = 5 * 1024 * 1024;
|
||||
constexpr auto kDefaultFrameDuration = crl::time(33);
|
||||
constexpr auto kDefaultFramesCount = 60;
|
||||
constexpr auto kAutoPauseTimeout = crl::time(1000);
|
||||
|
||||
[[nodiscard]] SpoilerMessDescriptor DefaultDescriptorText() {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto size = style::ConvertScale(128) * ratio;
|
||||
return {
|
||||
.particleFadeInDuration = crl::time(200),
|
||||
.particleShownDuration = crl::time(200),
|
||||
.particleFadeOutDuration = crl::time(200),
|
||||
.particleSizeMin = style::ConvertScaleExact(1.5) * ratio,
|
||||
.particleSizeMax = style::ConvertScaleExact(2.) * ratio,
|
||||
.particleSpeedMin = style::ConvertScaleExact(4.),
|
||||
.particleSpeedMax = style::ConvertScaleExact(8.),
|
||||
.particleSpritesCount = 5,
|
||||
.particlesCount = 9000,
|
||||
.canvasSize = size,
|
||||
.framesCount = kDefaultFramesCount,
|
||||
.frameDuration = kDefaultFrameDuration,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] SpoilerMessDescriptor DefaultDescriptorImage() {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto size = style::ConvertScale(128) * ratio;
|
||||
return {
|
||||
.particleFadeInDuration = crl::time(300),
|
||||
.particleShownDuration = crl::time(0),
|
||||
.particleFadeOutDuration = crl::time(300),
|
||||
.particleSizeMin = style::ConvertScaleExact(1.5) * ratio,
|
||||
.particleSizeMax = style::ConvertScaleExact(2.) * ratio,
|
||||
.particleSpeedMin = style::ConvertScaleExact(10.),
|
||||
.particleSpeedMax = style::ConvertScaleExact(20.),
|
||||
.particleSpritesCount = 5,
|
||||
.particlesCount = 3000,
|
||||
.canvasSize = size,
|
||||
.framesCount = kDefaultFramesCount,
|
||||
.frameDuration = kDefaultFrameDuration,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class SpoilerAnimationManager final {
|
||||
public:
|
||||
explicit SpoilerAnimationManager(not_null<SpoilerAnimation*> animation);
|
||||
|
||||
void add(not_null<SpoilerAnimation*> animation);
|
||||
void remove(not_null<SpoilerAnimation*> animation);
|
||||
|
||||
private:
|
||||
void destroyIfEmpty();
|
||||
|
||||
Ui::Animations::Basic _animation;
|
||||
base::flat_set<not_null<SpoilerAnimation*>> _list;
|
||||
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
struct DefaultSpoilerWaiter {
|
||||
std::condition_variable variable;
|
||||
std::mutex mutex;
|
||||
};
|
||||
struct DefaultSpoiler {
|
||||
std::atomic<const SpoilerMessCached*> cached/* = nullptr*/;
|
||||
std::atomic<DefaultSpoilerWaiter*> waiter/* = nullptr*/;
|
||||
};
|
||||
DefaultSpoiler DefaultTextMask;
|
||||
DefaultSpoiler DefaultImageCached;
|
||||
|
||||
SpoilerAnimationManager *DefaultAnimationManager/* = nullptr*/;
|
||||
|
||||
struct Header {
|
||||
uint32 version = 0;
|
||||
uint32 dataLength = 0;
|
||||
uint32 dataHash = 0;
|
||||
int32 framesCount = 0;
|
||||
int32 canvasSize = 0;
|
||||
int32 frameDuration = 0;
|
||||
};
|
||||
|
||||
struct Particle {
|
||||
crl::time start = 0;
|
||||
int spriteIndex = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
float64 dx = 0.;
|
||||
float64 dy = 0.;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::pair<float64, float64> RandomSpeed(
|
||||
const SpoilerMessDescriptor &descriptor,
|
||||
base::BufferedRandom<uint32> &random) {
|
||||
const auto count = descriptor.particlesCount;
|
||||
const auto speedMax = descriptor.particleSpeedMax;
|
||||
const auto speedMin = descriptor.particleSpeedMin;
|
||||
const auto value = RandomIndex(2 * count + 2, random);
|
||||
const auto negative = (value < count + 1);
|
||||
const auto module = (negative ? value : (value - count - 1));
|
||||
const auto speed = speedMin + (((speedMax - speedMin) * module) / count);
|
||||
const auto lifetime = descriptor.particleFadeInDuration
|
||||
+ descriptor.particleShownDuration
|
||||
+ descriptor.particleFadeOutDuration;
|
||||
const auto max = int(std::ceil(speedMax * lifetime));
|
||||
const auto k = speed / lifetime;
|
||||
const auto x = (speedMax > 0)
|
||||
? ((RandomIndex(2 * max + 1, random) - max) / float64(max))
|
||||
: 0.;
|
||||
const auto y = (speedMax > 0)
|
||||
? (sqrt(1 - x * x) * (negative ? -1 : 1))
|
||||
: 0.;
|
||||
return { k * x, k * y };
|
||||
}
|
||||
|
||||
[[nodiscard]] Particle GenerateParticle(
|
||||
const SpoilerMessDescriptor &descriptor,
|
||||
int index,
|
||||
base::BufferedRandom<uint32> &random) {
|
||||
const auto speed = RandomSpeed(descriptor, random);
|
||||
return {
|
||||
.start = (index * descriptor.framesCount * descriptor.frameDuration
|
||||
/ descriptor.particlesCount),
|
||||
.spriteIndex = RandomIndex(descriptor.particleSpritesCount, random),
|
||||
.x = RandomIndex(descriptor.canvasSize, random),
|
||||
.y = RandomIndex(descriptor.canvasSize, random),
|
||||
.dx = speed.first,
|
||||
.dy = speed.second,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage GenerateSprite(
|
||||
const SpoilerMessDescriptor &descriptor,
|
||||
int index,
|
||||
int size,
|
||||
base::BufferedRandom<uint32> &random) {
|
||||
Expects(index >= 0 && index < descriptor.particleSpritesCount);
|
||||
|
||||
const auto count = descriptor.particleSpritesCount;
|
||||
const auto middle = count / 2;
|
||||
const auto min = descriptor.particleSizeMin;
|
||||
const auto delta = descriptor.particleSizeMax - min;
|
||||
const auto width = (index < middle)
|
||||
? (min + delta * (middle - index) / float64(middle))
|
||||
: min;
|
||||
const auto height = (index > middle)
|
||||
? (min + delta * (index - middle) / float64(count - 1 - middle))
|
||||
: min;
|
||||
const auto radius = min / 2.;
|
||||
|
||||
auto result = QImage(size, size, QImage::Format_ARGB32_Premultiplied);
|
||||
result.fill(Qt::transparent);
|
||||
auto p = QPainter(&result);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(Qt::white);
|
||||
QPainterPath path;
|
||||
path.addRoundedRect(1., 1., width, height, radius, radius);
|
||||
p.drawPath(path);
|
||||
p.end();
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString DefaultMaskCacheFolder() {
|
||||
const auto base = Integration::Instance().emojiCacheFolder();
|
||||
return base.isEmpty() ? QString() : (base + "/spoiler");
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<SpoilerMessCached> ReadDefaultMask(
|
||||
const QString &name,
|
||||
std::optional<SpoilerMessCached::Validator> validator) {
|
||||
const auto folder = DefaultMaskCacheFolder();
|
||||
if (folder.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
auto file = QFile(folder + '/' + name);
|
||||
return (file.open(QIODevice::ReadOnly) && file.size() <= kMaxCacheSize)
|
||||
? SpoilerMessCached::FromSerialized(file.readAll(), validator)
|
||||
: std::nullopt;
|
||||
}
|
||||
|
||||
void WriteDefaultMask(
|
||||
const QString &name,
|
||||
const SpoilerMessCached &mask) {
|
||||
const auto folder = DefaultMaskCacheFolder();
|
||||
if (!QDir().mkpath(folder)) {
|
||||
return;
|
||||
}
|
||||
const auto bytes = mask.serialize();
|
||||
auto file = QFile(folder + '/' + name);
|
||||
if (file.open(QIODevice::WriteOnly) && bytes.size() <= kMaxCacheSize) {
|
||||
file.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
void Register(not_null<SpoilerAnimation*> animation) {
|
||||
if (DefaultAnimationManager) {
|
||||
DefaultAnimationManager->add(animation);
|
||||
} else {
|
||||
new SpoilerAnimationManager(animation);
|
||||
}
|
||||
}
|
||||
|
||||
void Unregister(not_null<SpoilerAnimation*> animation) {
|
||||
Expects(DefaultAnimationManager != nullptr);
|
||||
|
||||
DefaultAnimationManager->remove(animation);
|
||||
}
|
||||
|
||||
// DescriptorFactory: (void) -> SpoilerMessDescriptor.
|
||||
// Postprocess: (unique_ptr<MessCached>) -> unique_ptr<MessCached>.
|
||||
template <typename DescriptorFactory, typename Postprocess>
|
||||
void PrepareDefaultSpoiler(
|
||||
DefaultSpoiler &spoiler,
|
||||
const char *nameFactory,
|
||||
DescriptorFactory descriptorFactory,
|
||||
Postprocess postprocess) {
|
||||
if (spoiler.waiter.load()) {
|
||||
return;
|
||||
}
|
||||
const auto waiter = new DefaultSpoilerWaiter();
|
||||
auto expected = (DefaultSpoilerWaiter*)nullptr;
|
||||
if (!spoiler.waiter.compare_exchange_strong(expected, waiter)) {
|
||||
delete waiter;
|
||||
return;
|
||||
}
|
||||
const auto name = QString::fromUtf8(nameFactory);
|
||||
crl::async([=, &spoiler] {
|
||||
const auto descriptor = descriptorFactory();
|
||||
auto cached = ReadDefaultMask(name, SpoilerMessCached::Validator{
|
||||
.frameDuration = descriptor.frameDuration,
|
||||
.framesCount = descriptor.framesCount,
|
||||
.canvasSize = descriptor.canvasSize,
|
||||
});
|
||||
spoiler.cached = postprocess(cached
|
||||
? std::make_unique<SpoilerMessCached>(std::move(*cached))
|
||||
: std::make_unique<SpoilerMessCached>(
|
||||
GenerateSpoilerMess(descriptor))
|
||||
).release();
|
||||
auto lock = std::unique_lock(waiter->mutex);
|
||||
waiter->variable.notify_all();
|
||||
if (!cached) {
|
||||
WriteDefaultMask(name, *spoiler.cached);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] const SpoilerMessCached &WaitDefaultSpoiler(
|
||||
DefaultSpoiler &spoiler) {
|
||||
const auto &cached = spoiler.cached;
|
||||
if (const auto result = cached.load()) {
|
||||
return *result;
|
||||
}
|
||||
const auto waiter = spoiler.waiter.load();
|
||||
Assert(waiter != nullptr);
|
||||
while (true) {
|
||||
auto lock = std::unique_lock(waiter->mutex);
|
||||
if (const auto result = cached.load()) {
|
||||
return *result;
|
||||
}
|
||||
waiter->variable.wait(lock);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SpoilerAnimationManager::SpoilerAnimationManager(
|
||||
not_null<SpoilerAnimation*> animation)
|
||||
: _animation([=](crl::time now) {
|
||||
for (auto i = begin(_list); i != end(_list);) {
|
||||
if ((*i)->repaint(now)) {
|
||||
++i;
|
||||
} else {
|
||||
i = _list.erase(i);
|
||||
}
|
||||
}
|
||||
destroyIfEmpty();
|
||||
})
|
||||
, _list{ { animation } } {
|
||||
Expects(!DefaultAnimationManager);
|
||||
|
||||
DefaultAnimationManager = this;
|
||||
_animation.start();
|
||||
}
|
||||
|
||||
void SpoilerAnimationManager::add(not_null<SpoilerAnimation*> animation) {
|
||||
_list.emplace(animation);
|
||||
}
|
||||
|
||||
void SpoilerAnimationManager::remove(not_null<SpoilerAnimation*> animation) {
|
||||
_list.remove(animation);
|
||||
destroyIfEmpty();
|
||||
}
|
||||
|
||||
void SpoilerAnimationManager::destroyIfEmpty() {
|
||||
if (_list.empty()) {
|
||||
Assert(DefaultAnimationManager == this);
|
||||
delete base::take(DefaultAnimationManager);
|
||||
}
|
||||
}
|
||||
|
||||
SpoilerMessCached GenerateSpoilerMess(
|
||||
const SpoilerMessDescriptor &descriptor) {
|
||||
Expects(descriptor.framesCount > 0);
|
||||
Expects(descriptor.frameDuration > 0);
|
||||
Expects(descriptor.particlesCount > 0);
|
||||
Expects(descriptor.canvasSize > 0);
|
||||
Expects(descriptor.particleSizeMax >= descriptor.particleSizeMin);
|
||||
Expects(descriptor.particleSizeMin > 0.);
|
||||
|
||||
const auto frames = descriptor.framesCount;
|
||||
const auto rows = (frames + kFramesPerRow - 1) / kFramesPerRow;
|
||||
const auto columns = std::min(frames, kFramesPerRow);
|
||||
const auto size = descriptor.canvasSize;
|
||||
const auto count = descriptor.particlesCount;
|
||||
const auto width = size * columns;
|
||||
const auto height = size * rows;
|
||||
const auto spriteSize = 2 + int(std::ceil(descriptor.particleSizeMax));
|
||||
const auto singleDuration = descriptor.particleFadeInDuration
|
||||
+ descriptor.particleShownDuration
|
||||
+ descriptor.particleFadeOutDuration;
|
||||
const auto fullDuration = frames * descriptor.frameDuration;
|
||||
Assert(fullDuration > singleDuration);
|
||||
|
||||
auto random = base::BufferedRandom<uint32>(count * 5);
|
||||
|
||||
auto particles = std::vector<Particle>();
|
||||
particles.reserve(descriptor.particlesCount);
|
||||
for (auto i = 0; i != descriptor.particlesCount; ++i) {
|
||||
particles.push_back(GenerateParticle(descriptor, i, random));
|
||||
}
|
||||
|
||||
auto sprites = std::vector<QImage>();
|
||||
sprites.reserve(descriptor.particleSpritesCount);
|
||||
for (auto i = 0; i != descriptor.particleSpritesCount; ++i) {
|
||||
sprites.push_back(GenerateSprite(descriptor, i, spriteSize, random));
|
||||
}
|
||||
|
||||
auto frame = 0;
|
||||
auto image = QImage(width, height, QImage::Format_ARGB32_Premultiplied);
|
||||
image.fill(Qt::transparent);
|
||||
auto p = QPainter(&image);
|
||||
const auto paintOneAt = [&](const Particle &particle, crl::time now) {
|
||||
if (now <= 0 || now >= singleDuration) {
|
||||
return;
|
||||
}
|
||||
const auto clamp = [&](int value) {
|
||||
return ((value % size) + size) % size;
|
||||
};
|
||||
const auto x = clamp(
|
||||
particle.x + int(base::SafeRound(now * particle.dx)));
|
||||
const auto y = clamp(
|
||||
particle.y + int(base::SafeRound(now * particle.dy)));
|
||||
const auto opacity = (now < descriptor.particleFadeInDuration)
|
||||
? (now / float64(descriptor.particleFadeInDuration))
|
||||
: (now > singleDuration - descriptor.particleFadeOutDuration)
|
||||
? ((singleDuration - now)
|
||||
/ float64(descriptor.particleFadeOutDuration))
|
||||
: 1.;
|
||||
p.setOpacity(opacity);
|
||||
const auto &sprite = sprites[particle.spriteIndex];
|
||||
p.drawImage(x, y, sprite);
|
||||
if (x + spriteSize > size) {
|
||||
p.drawImage(x - size, y, sprite);
|
||||
if (y + spriteSize > size) {
|
||||
p.drawImage(x, y - size, sprite);
|
||||
p.drawImage(x - size, y - size, sprite);
|
||||
}
|
||||
} else if (y + spriteSize > size) {
|
||||
p.drawImage(x, y - size, sprite);
|
||||
}
|
||||
};
|
||||
const auto paintOne = [&](const Particle &particle, crl::time now) {
|
||||
paintOneAt(particle, now - particle.start);
|
||||
paintOneAt(particle, now + fullDuration - particle.start);
|
||||
};
|
||||
for (auto y = 0; y != rows; ++y) {
|
||||
for (auto x = 0; x != columns; ++x) {
|
||||
const auto rect = QRect(x * size, y * size, size, size);
|
||||
p.setClipRect(rect);
|
||||
p.translate(rect.topLeft());
|
||||
const auto time = frame * descriptor.frameDuration;
|
||||
for (auto index = 0; index != count; ++index) {
|
||||
paintOne(particles[index], time);
|
||||
}
|
||||
p.translate(-rect.topLeft());
|
||||
if (++frame >= frames) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return SpoilerMessCached(
|
||||
std::move(image),
|
||||
frames,
|
||||
descriptor.frameDuration,
|
||||
size);
|
||||
}
|
||||
|
||||
void FillSpoilerRect(
|
||||
QPainter &p,
|
||||
QRect rect,
|
||||
const SpoilerMessFrame &frame,
|
||||
QPoint originShift) {
|
||||
if (rect.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto &image = *frame.image;
|
||||
const auto source = frame.source;
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto origin = rect.topLeft() + originShift;
|
||||
const auto size = source.width() / ratio;
|
||||
const auto xSkipFrames = (origin.x() <= rect.x())
|
||||
? ((rect.x() - origin.x()) / size)
|
||||
: -((origin.x() - rect.x() + size - 1) / size);
|
||||
const auto ySkipFrames = (origin.y() <= rect.y())
|
||||
? ((rect.y() - origin.y()) / size)
|
||||
: -((origin.y() - rect.y() + size - 1) / size);
|
||||
const auto xFrom = origin.x() + size * xSkipFrames;
|
||||
const auto yFrom = origin.y() + size * ySkipFrames;
|
||||
Assert((xFrom <= rect.x())
|
||||
&& (yFrom <= rect.y())
|
||||
&& (xFrom + size > rect.x())
|
||||
&& (yFrom + size > rect.y()));
|
||||
const auto xTill = rect.x() + rect.width();
|
||||
const auto yTill = rect.y() + rect.height();
|
||||
const auto xCount = (xTill - xFrom + size - 1) / size;
|
||||
const auto yCount = (yTill - yFrom + size - 1) / size;
|
||||
Assert(xCount > 0 && yCount > 0);
|
||||
const auto xFullFrom = (xFrom < rect.x()) ? 1 : 0;
|
||||
const auto yFullFrom = (yFrom < rect.y()) ? 1 : 0;
|
||||
const auto xFullTill = xCount - (xFrom + xCount * size > xTill ? 1 : 0);
|
||||
const auto yFullTill = yCount - (yFrom + yCount * size > yTill ? 1 : 0);
|
||||
const auto targetRect = [&](int x, int y) {
|
||||
return QRect(xFrom + x * size, yFrom + y * size, size, size);
|
||||
};
|
||||
const auto drawFull = [&](int x, int y) {
|
||||
p.drawImage(targetRect(x, y), image, source);
|
||||
};
|
||||
const auto drawPart = [&](int x, int y) {
|
||||
const auto target = targetRect(x, y);
|
||||
const auto fill = target.intersected(rect);
|
||||
Assert(!fill.isEmpty());
|
||||
p.drawImage(fill, image, QRect(
|
||||
source.topLeft() + ((fill.topLeft() - target.topLeft()) * ratio),
|
||||
fill.size() * ratio));
|
||||
};
|
||||
if (yFullFrom) {
|
||||
for (auto x = 0; x != xCount; ++x) {
|
||||
drawPart(x, 0);
|
||||
}
|
||||
}
|
||||
if (yFullFrom < yFullTill) {
|
||||
if (xFullFrom) {
|
||||
for (auto y = yFullFrom; y != yFullTill; ++y) {
|
||||
drawPart(0, y);
|
||||
}
|
||||
}
|
||||
if (xFullFrom < xFullTill) {
|
||||
for (auto y = yFullFrom; y != yFullTill; ++y) {
|
||||
for (auto x = xFullFrom; x != xFullTill; ++x) {
|
||||
drawFull(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (xFullFrom <= xFullTill && xFullTill < xCount) {
|
||||
for (auto y = yFullFrom; y != yFullTill; ++y) {
|
||||
drawPart(xFullTill, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (yFullFrom <= yFullTill && yFullTill < yCount) {
|
||||
for (auto x = 0; x != xCount; ++x) {
|
||||
drawPart(x, yFullTill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FillSpoilerRect(
|
||||
QPainter &p,
|
||||
QRect rect,
|
||||
Images::CornersMaskRef mask,
|
||||
const SpoilerMessFrame &frame,
|
||||
QImage &cornerCache,
|
||||
QPoint originShift) {
|
||||
using namespace Images;
|
||||
|
||||
if ((!mask.p[kTopLeft] || mask.p[kTopLeft]->isNull())
|
||||
&& (!mask.p[kTopRight] || mask.p[kTopRight]->isNull())
|
||||
&& (!mask.p[kBottomLeft] || mask.p[kBottomLeft]->isNull())
|
||||
&& (!mask.p[kBottomRight] || mask.p[kBottomRight]->isNull())) {
|
||||
FillSpoilerRect(p, rect, frame, originShift);
|
||||
return;
|
||||
}
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto cornerSize = [&](int index) {
|
||||
const auto corner = mask.p[index];
|
||||
return (!corner || corner->isNull()) ? 0 : (corner->width() / ratio);
|
||||
};
|
||||
const auto verticalSkip = [&](int left, int right) {
|
||||
return std::max(cornerSize(left), cornerSize(right));
|
||||
};
|
||||
const auto fillBg = [&](QRect part) {
|
||||
FillSpoilerRect(
|
||||
p,
|
||||
part.translated(rect.topLeft()),
|
||||
frame,
|
||||
originShift - rect.topLeft() - part.topLeft());
|
||||
};
|
||||
const auto fillCorner = [&](int x, int y, int index) {
|
||||
const auto position = QPoint(x, y);
|
||||
const auto corner = mask.p[index];
|
||||
if (!corner || corner->isNull()) {
|
||||
return;
|
||||
}
|
||||
if (cornerCache.width() < corner->width()
|
||||
|| cornerCache.height() < corner->height()) {
|
||||
cornerCache = QImage(
|
||||
std::max(cornerCache.width(), corner->width()),
|
||||
std::max(cornerCache.height(), corner->height()),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
cornerCache.setDevicePixelRatio(ratio);
|
||||
}
|
||||
const auto size = corner->size() / ratio;
|
||||
const auto target = QRect(QPoint(), size);
|
||||
auto q = QPainter(&cornerCache);
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
FillSpoilerRect(
|
||||
q,
|
||||
target,
|
||||
frame,
|
||||
originShift - rect.topLeft() - position);
|
||||
q.setCompositionMode(QPainter::CompositionMode_DestinationIn);
|
||||
q.drawImage(target, *corner);
|
||||
q.end();
|
||||
p.drawImage(
|
||||
QRect(rect.topLeft() + position, size),
|
||||
cornerCache,
|
||||
QRect(QPoint(), corner->size()));
|
||||
};
|
||||
const auto top = verticalSkip(kTopLeft, kTopRight);
|
||||
const auto bottom = verticalSkip(kBottomLeft, kBottomRight);
|
||||
if (top) {
|
||||
const auto left = cornerSize(kTopLeft);
|
||||
const auto right = cornerSize(kTopRight);
|
||||
if (left) {
|
||||
fillCorner(0, 0, kTopLeft);
|
||||
if (const auto add = top - left) {
|
||||
fillBg({ 0, left, left, add });
|
||||
}
|
||||
}
|
||||
if (const auto fill = rect.width() - left - right; fill > 0) {
|
||||
fillBg({ left, 0, fill, top });
|
||||
}
|
||||
if (right) {
|
||||
fillCorner(rect.width() - right, 0, kTopRight);
|
||||
if (const auto add = top - right) {
|
||||
fillBg({ rect.width() - right, right, right, add });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (const auto h = rect.height() - top - bottom; h > 0) {
|
||||
fillBg({ 0, top, rect.width(), h });
|
||||
}
|
||||
if (bottom) {
|
||||
const auto left = cornerSize(kBottomLeft);
|
||||
const auto right = cornerSize(kBottomRight);
|
||||
if (left) {
|
||||
fillCorner(0, rect.height() - left, kBottomLeft);
|
||||
if (const auto add = bottom - left) {
|
||||
fillBg({ 0, rect.height() - bottom, left, add });
|
||||
}
|
||||
}
|
||||
if (const auto fill = rect.width() - left - right; fill > 0) {
|
||||
fillBg({ left, rect.height() - bottom, fill, bottom });
|
||||
}
|
||||
if (right) {
|
||||
fillCorner(
|
||||
rect.width() - right,
|
||||
rect.height() - right,
|
||||
kBottomRight);
|
||||
if (const auto add = bottom - right) {
|
||||
fillBg({
|
||||
rect.width() - right,
|
||||
rect.height() - bottom,
|
||||
right,
|
||||
add,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpoilerMessCached::SpoilerMessCached(
|
||||
QImage image,
|
||||
int framesCount,
|
||||
crl::time frameDuration,
|
||||
int canvasSize)
|
||||
: _image(std::move(image))
|
||||
, _frameDuration(frameDuration)
|
||||
, _framesCount(framesCount)
|
||||
, _canvasSize(canvasSize) {
|
||||
Expects(_frameDuration > 0);
|
||||
Expects(_framesCount > 0);
|
||||
Expects(_canvasSize > 0);
|
||||
Expects(_image.size() == QSize(
|
||||
std::min(_framesCount, kFramesPerRow) * _canvasSize,
|
||||
((_framesCount + kFramesPerRow - 1) / kFramesPerRow) * _canvasSize));
|
||||
}
|
||||
|
||||
SpoilerMessCached::SpoilerMessCached(
|
||||
const SpoilerMessCached &mask,
|
||||
const QColor &color)
|
||||
: SpoilerMessCached(
|
||||
style::colorizeImage(*mask.frame(0).image, color),
|
||||
mask.framesCount(),
|
||||
mask.frameDuration(),
|
||||
mask.canvasSize()) {
|
||||
}
|
||||
|
||||
SpoilerMessFrame SpoilerMessCached::frame(int index) const {
|
||||
const auto row = index / kFramesPerRow;
|
||||
const auto column = index - row * kFramesPerRow;
|
||||
return {
|
||||
.image = &_image,
|
||||
.source = QRect(
|
||||
column * _canvasSize,
|
||||
row * _canvasSize,
|
||||
_canvasSize,
|
||||
_canvasSize),
|
||||
};
|
||||
}
|
||||
|
||||
SpoilerMessFrame SpoilerMessCached::frame() const {
|
||||
return frame((crl::now() / _frameDuration) % _framesCount);
|
||||
}
|
||||
|
||||
crl::time SpoilerMessCached::frameDuration() const {
|
||||
return _frameDuration;
|
||||
}
|
||||
|
||||
int SpoilerMessCached::framesCount() const {
|
||||
return _framesCount;
|
||||
}
|
||||
|
||||
int SpoilerMessCached::canvasSize() const {
|
||||
return _canvasSize;
|
||||
}
|
||||
|
||||
QByteArray SpoilerMessCached::serialize() const {
|
||||
Expects(_frameDuration < std::numeric_limits<int32>::max());
|
||||
|
||||
const auto skip = sizeof(Header);
|
||||
auto result = QByteArray(skip, Qt::Uninitialized);
|
||||
auto header = Header{
|
||||
.version = kVersion,
|
||||
.framesCount = _framesCount,
|
||||
.canvasSize = _canvasSize,
|
||||
.frameDuration = int32(_frameDuration),
|
||||
};
|
||||
const auto width = int(_image.width());
|
||||
const auto height = int(_image.height());
|
||||
auto grayscale = QImage(width, height, QImage::Format_Grayscale8);
|
||||
{
|
||||
auto tobytes = grayscale.bits();
|
||||
auto frombytes = _image.constBits();
|
||||
const auto toadd = grayscale.bytesPerLine() - width;
|
||||
const auto fromadd = _image.bytesPerLine() - (width * 4);
|
||||
for (auto y = 0; y != height; ++y) {
|
||||
for (auto x = 0; x != width; ++x) {
|
||||
*tobytes++ = *frombytes;
|
||||
frombytes += 4;
|
||||
}
|
||||
tobytes += toadd;
|
||||
frombytes += fromadd;
|
||||
}
|
||||
}
|
||||
|
||||
auto device = QBuffer(&result);
|
||||
device.open(QIODevice::WriteOnly);
|
||||
device.seek(skip);
|
||||
grayscale.save(&device, "PNG");
|
||||
device.close();
|
||||
header.dataLength = result.size() - skip;
|
||||
header.dataHash = XXH32(result.data() + skip, header.dataLength, 0);
|
||||
memcpy(result.data(), &header, skip);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<SpoilerMessCached> SpoilerMessCached::FromSerialized(
|
||||
QByteArray data,
|
||||
std::optional<Validator> validator) {
|
||||
const auto skip = sizeof(Header);
|
||||
const auto length = data.size();
|
||||
const auto bytes = reinterpret_cast<const uchar*>(data.constData());
|
||||
if (length <= skip) {
|
||||
return {};
|
||||
}
|
||||
auto header = Header();
|
||||
memcpy(&header, bytes, skip);
|
||||
if (header.version != kVersion
|
||||
|| header.canvasSize <= 0
|
||||
|| header.framesCount <= 0
|
||||
|| header.frameDuration <= 0
|
||||
|| (validator
|
||||
&& (validator->frameDuration != header.frameDuration
|
||||
|| validator->framesCount != header.framesCount
|
||||
|| validator->canvasSize != header.canvasSize))
|
||||
|| (skip + header.dataLength != length)
|
||||
|| (XXH32(bytes + skip, header.dataLength, 0) != header.dataHash)) {
|
||||
return {};
|
||||
}
|
||||
auto grayscale = QImage();
|
||||
if (!grayscale.loadFromData(bytes + skip, header.dataLength, "PNG")
|
||||
|| (grayscale.format() != QImage::Format_Grayscale8)) {
|
||||
return {};
|
||||
}
|
||||
const auto count = header.framesCount;
|
||||
const auto rows = (count + kFramesPerRow - 1) / kFramesPerRow;
|
||||
const auto columns = std::min(count, kFramesPerRow);
|
||||
const auto width = grayscale.width();
|
||||
const auto height = grayscale.height();
|
||||
if (QSize(width, height) != QSize(columns, rows) * header.canvasSize) {
|
||||
return {};
|
||||
}
|
||||
auto image = QImage(width, height, QImage::Format_ARGB32_Premultiplied);
|
||||
{
|
||||
Assert(image.bytesPerLine() % 4 == 0);
|
||||
auto toints = reinterpret_cast<uint32*>(image.bits());
|
||||
auto frombytes = grayscale.constBits();
|
||||
const auto toadd = (image.bytesPerLine() / 4) - width;
|
||||
const auto fromadd = grayscale.bytesPerLine() - width;
|
||||
for (auto y = 0; y != height; ++y) {
|
||||
for (auto x = 0; x != width; ++x) {
|
||||
const auto byte = uint32(*frombytes++);
|
||||
*toints++ = (byte << 24) | (byte << 16) | (byte << 8) | byte;
|
||||
}
|
||||
toints += toadd;
|
||||
frombytes += fromadd;
|
||||
}
|
||||
|
||||
}
|
||||
return SpoilerMessCached(
|
||||
std::move(image),
|
||||
count,
|
||||
header.frameDuration,
|
||||
header.canvasSize);
|
||||
}
|
||||
|
||||
SpoilerAnimation::SpoilerAnimation(Fn<void()> repaint)
|
||||
: _repaint(std::move(repaint)) {
|
||||
Expects(_repaint != nullptr);
|
||||
}
|
||||
|
||||
SpoilerAnimation::~SpoilerAnimation() {
|
||||
if (_animating) {
|
||||
_animating = false;
|
||||
Unregister(this);
|
||||
}
|
||||
}
|
||||
|
||||
int SpoilerAnimation::index(crl::time now, bool paused) {
|
||||
_scheduled = false;
|
||||
const auto add = std::min(now - _last, kDefaultFrameDuration);
|
||||
if (anim::Disabled()) {
|
||||
paused = true;
|
||||
}
|
||||
if (!paused || _last) {
|
||||
_accumulated += add;
|
||||
_last = paused ? 0 : now;
|
||||
}
|
||||
const auto absolute = (_accumulated / kDefaultFrameDuration);
|
||||
if (!paused && !_animating) {
|
||||
_animating = true;
|
||||
Register(this);
|
||||
} else if (paused && _animating) {
|
||||
_animating = false;
|
||||
Unregister(this);
|
||||
}
|
||||
return absolute % kDefaultFramesCount;
|
||||
}
|
||||
|
||||
Fn<void()> SpoilerAnimation::repaintCallback() const {
|
||||
return _repaint;
|
||||
}
|
||||
|
||||
bool SpoilerAnimation::repaint(crl::time now) {
|
||||
if (!_scheduled) {
|
||||
_scheduled = true;
|
||||
_repaint();
|
||||
} else if (_animating && _last && _last + kAutoPauseTimeout <= now) {
|
||||
_animating = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void PreloadTextSpoilerMask() {
|
||||
PrepareDefaultSpoiler(
|
||||
DefaultTextMask,
|
||||
"text",
|
||||
DefaultDescriptorText,
|
||||
[](std::unique_ptr<SpoilerMessCached> cached) { return cached; });
|
||||
}
|
||||
|
||||
const SpoilerMessCached &DefaultTextSpoilerMask() {
|
||||
[[maybe_unused]] static const auto once = [&] {
|
||||
PreloadTextSpoilerMask();
|
||||
return 0;
|
||||
}();
|
||||
return WaitDefaultSpoiler(DefaultTextMask);
|
||||
}
|
||||
|
||||
void PreloadImageSpoiler() {
|
||||
const auto postprocess = [](std::unique_ptr<SpoilerMessCached> cached) {
|
||||
Expects(cached != nullptr);
|
||||
|
||||
const auto frame = cached->frame(0);
|
||||
auto image = QImage(
|
||||
frame.image->size(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.fill(QColor(0, 0, 0, kImageSpoilerDarkenAlpha));
|
||||
auto p = QPainter(&image);
|
||||
p.drawImage(0, 0, *frame.image);
|
||||
p.end();
|
||||
return std::make_unique<SpoilerMessCached>(
|
||||
std::move(image),
|
||||
cached->framesCount(),
|
||||
cached->frameDuration(),
|
||||
cached->canvasSize());
|
||||
};
|
||||
PrepareDefaultSpoiler(
|
||||
DefaultImageCached,
|
||||
"image",
|
||||
DefaultDescriptorImage,
|
||||
postprocess);
|
||||
}
|
||||
|
||||
const SpoilerMessCached &DefaultImageSpoiler() {
|
||||
[[maybe_unused]] static const auto once = [&] {
|
||||
PreloadImageSpoiler();
|
||||
return 0;
|
||||
}();
|
||||
return WaitDefaultSpoiler(DefaultImageCached);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
117
Telegram/lib_ui/ui/effects/spoiler_mess.h
Normal file
117
Telegram/lib_ui/ui/effects/spoiler_mess.h
Normal file
@@ -0,0 +1,117 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include <crl/crl_time.h>
|
||||
|
||||
namespace Images {
|
||||
struct CornersMaskRef;
|
||||
} // namespace Images
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct SpoilerMessDescriptor {
|
||||
crl::time particleFadeInDuration = 0;
|
||||
crl::time particleShownDuration = 0;
|
||||
crl::time particleFadeOutDuration = 0;
|
||||
float64 particleSizeMin = 0.;
|
||||
float64 particleSizeMax = 0.;
|
||||
float64 particleSpeedMin = 0.;
|
||||
float64 particleSpeedMax = 0.;
|
||||
int particleSpritesCount = 0;
|
||||
int particlesCount = 0;
|
||||
int canvasSize = 0;
|
||||
int framesCount = 0;
|
||||
crl::time frameDuration = 0;
|
||||
};
|
||||
|
||||
struct SpoilerMessFrame {
|
||||
not_null<const QImage*> image;
|
||||
QRect source;
|
||||
};
|
||||
|
||||
void FillSpoilerRect(
|
||||
QPainter &p,
|
||||
QRect rect,
|
||||
const SpoilerMessFrame &frame,
|
||||
QPoint originShift = {});
|
||||
|
||||
void FillSpoilerRect(
|
||||
QPainter &p,
|
||||
QRect rect,
|
||||
Images::CornersMaskRef mask,
|
||||
const SpoilerMessFrame &frame,
|
||||
QImage &cornerCache,
|
||||
QPoint originShift = {});
|
||||
|
||||
class SpoilerMessCached final {
|
||||
public:
|
||||
SpoilerMessCached(
|
||||
QImage image,
|
||||
int framesCount,
|
||||
crl::time frameDuration,
|
||||
int canvasSize);
|
||||
SpoilerMessCached(const SpoilerMessCached &mask, const QColor &color);
|
||||
|
||||
[[nodiscard]] SpoilerMessFrame frame(int index) const;
|
||||
[[nodiscard]] SpoilerMessFrame frame() const; // Current by time.
|
||||
|
||||
[[nodiscard]] crl::time frameDuration() const;
|
||||
[[nodiscard]] int framesCount() const;
|
||||
[[nodiscard]] int canvasSize() const;
|
||||
|
||||
struct Validator {
|
||||
crl::time frameDuration = 0;
|
||||
int framesCount = 0;
|
||||
int canvasSize = 0;
|
||||
};
|
||||
[[nodiscard]] QByteArray serialize() const;
|
||||
[[nodiscard]] static std::optional<SpoilerMessCached> FromSerialized(
|
||||
QByteArray data,
|
||||
std::optional<Validator> validator = {});
|
||||
|
||||
private:
|
||||
QImage _image;
|
||||
crl::time _frameDuration = 0;
|
||||
int _framesCount = 0;
|
||||
int _canvasSize = 0;
|
||||
|
||||
};
|
||||
|
||||
// Works with default frame duration and default frame count.
|
||||
class SpoilerAnimationManager;
|
||||
class SpoilerAnimation final {
|
||||
public:
|
||||
explicit SpoilerAnimation(Fn<void()> repaint);
|
||||
~SpoilerAnimation();
|
||||
|
||||
[[nodiscard]] int index(crl::time now, bool paused);
|
||||
|
||||
[[nodiscard]] Fn<void()> repaintCallback() const;
|
||||
|
||||
private:
|
||||
friend class SpoilerAnimationManager;
|
||||
|
||||
[[nodiscard]] bool repaint(crl::time now);
|
||||
|
||||
const Fn<void()> _repaint;
|
||||
crl::time _accumulated = 0;
|
||||
crl::time _last = 0;
|
||||
bool _animating : 1 = false;
|
||||
bool _scheduled : 1 = false;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] SpoilerMessCached GenerateSpoilerMess(
|
||||
const SpoilerMessDescriptor &descriptor);
|
||||
|
||||
void PreloadTextSpoilerMask();
|
||||
[[nodiscard]] const SpoilerMessCached &DefaultTextSpoilerMask();
|
||||
void PreloadImageSpoiler();
|
||||
[[nodiscard]] const SpoilerMessCached &DefaultImageSpoiler();
|
||||
|
||||
} // namespace Ui
|
||||
917
Telegram/lib_ui/ui/emoji_config.cpp
Normal file
917
Telegram/lib_ui/ui/emoji_config.cpp
Normal 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
|
||||
184
Telegram/lib_ui/ui/emoji_config.h
Normal file
184
Telegram/lib_ui/ui/emoji_config.h
Normal 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
|
||||
41
Telegram/lib_ui/ui/focus_persister.h
Normal file
41
Telegram/lib_ui/ui/focus_persister.h
Normal 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
|
||||
290
Telegram/lib_ui/ui/gl/gl_detection.cpp
Normal file
290
Telegram/lib_ui/ui/gl/gl_detection.cpp
Normal 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
|
||||
53
Telegram/lib_ui/ui/gl/gl_detection.h
Normal file
53
Telegram/lib_ui/ui/gl/gl_detection.h
Normal 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
|
||||
165
Telegram/lib_ui/ui/gl/gl_image.cpp
Normal file
165
Telegram/lib_ui/ui/gl/gl_image.cpp
Normal 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
|
||||
145
Telegram/lib_ui/ui/gl/gl_image.h
Normal file
145
Telegram/lib_ui/ui/gl/gl_image.h
Normal 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
|
||||
36
Telegram/lib_ui/ui/gl/gl_math.cpp
Normal file
36
Telegram/lib_ui/ui/gl/gl_math.cpp
Normal 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
|
||||
83
Telegram/lib_ui/ui/gl/gl_math.h
Normal file
83
Telegram/lib_ui/ui/gl/gl_math.h
Normal 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
|
||||
125
Telegram/lib_ui/ui/gl/gl_primitives.cpp
Normal file
125
Telegram/lib_ui/ui/gl/gl_primitives.cpp
Normal 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
|
||||
39
Telegram/lib_ui/ui/gl/gl_primitives.h
Normal file
39
Telegram/lib_ui/ui/gl/gl_primitives.h
Normal 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
|
||||
240
Telegram/lib_ui/ui/gl/gl_shader.cpp
Normal file
240
Telegram/lib_ui/ui/gl/gl_shader.cpp
Normal 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
|
||||
48
Telegram/lib_ui/ui/gl/gl_shader.h
Normal file
48
Telegram/lib_ui/ui/gl/gl_shader.h
Normal file
@@ -0,0 +1,48 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include <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
|
||||
187
Telegram/lib_ui/ui/gl/gl_surface.cpp
Normal file
187
Telegram/lib_ui/ui/gl/gl_surface.cpp
Normal 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
|
||||
66
Telegram/lib_ui/ui/gl/gl_surface.h
Normal file
66
Telegram/lib_ui/ui/gl/gl_surface.h
Normal file
@@ -0,0 +1,66 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/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
|
||||
55
Telegram/lib_ui/ui/gl/gl_window.cpp
Normal file
55
Telegram/lib_ui/ui/gl/gl_window.cpp
Normal 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
|
||||
38
Telegram/lib_ui/ui/gl/gl_window.h
Normal file
38
Telegram/lib_ui/ui/gl/gl_window.h
Normal 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
|
||||
1647
Telegram/lib_ui/ui/image/image_prepare.cpp
Normal file
1647
Telegram/lib_ui/ui/image/image_prepare.cpp
Normal file
File diff suppressed because it is too large
Load Diff
216
Telegram/lib_ui/ui/image/image_prepare.h
Normal file
216
Telegram/lib_ui/ui/image/image_prepare.h
Normal 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
|
||||
53
Telegram/lib_ui/ui/inactive_press.cpp
Normal file
53
Telegram/lib_ui/ui/inactive_press.cpp
Normal 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
|
||||
14
Telegram/lib_ui/ui/inactive_press.h
Normal file
14
Telegram/lib_ui/ui/inactive_press.h
Normal 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
|
||||
223
Telegram/lib_ui/ui/integration.cpp
Normal file
223
Telegram/lib_ui/ui/integration.cpp
Normal 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
|
||||
105
Telegram/lib_ui/ui/integration.h
Normal file
105
Telegram/lib_ui/ui/integration.h
Normal 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
|
||||
455
Telegram/lib_ui/ui/layers/box_content.cpp
Normal file
455
Telegram/lib_ui/ui/layers/box_content.cpp
Normal 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
|
||||
396
Telegram/lib_ui/ui/layers/box_content.h
Normal file
396
Telegram/lib_ui/ui/layers/box_content.h
Normal 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
|
||||
427
Telegram/lib_ui/ui/layers/box_layer_widget.cpp
Normal file
427
Telegram/lib_ui/ui/layers/box_layer_widget.cpp
Normal 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
|
||||
143
Telegram/lib_ui/ui/layers/box_layer_widget.h
Normal file
143
Telegram/lib_ui/ui/layers/box_layer_widget.h
Normal file
@@ -0,0 +1,143 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "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
|
||||
131
Telegram/lib_ui/ui/layers/generic_box.cpp
Normal file
131
Telegram/lib_ui/ui/layers/generic_box.cpp
Normal 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
|
||||
230
Telegram/lib_ui/ui/layers/generic_box.h
Normal file
230
Telegram/lib_ui/ui/layers/generic_box.h
Normal 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
|
||||
191
Telegram/lib_ui/ui/layers/layer_manager.cpp
Normal file
191
Telegram/lib_ui/ui/layers/layer_manager.cpp
Normal 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
|
||||
73
Telegram/lib_ui/ui/layers/layer_manager.h
Normal file
73
Telegram/lib_ui/ui/layers/layer_manager.h
Normal 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
|
||||
955
Telegram/lib_ui/ui/layers/layer_widget.cpp
Normal file
955
Telegram/lib_ui/ui/layers/layer_widget.cpp
Normal 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 ¶ms) {
|
||||
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
|
||||
244
Telegram/lib_ui/ui/layers/layer_widget.h
Normal file
244
Telegram/lib_ui/ui/layers/layer_widget.h
Normal 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 ¶ms) {
|
||||
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 ¶ms);
|
||||
|
||||
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
|
||||
235
Telegram/lib_ui/ui/layers/layers.style
Normal file
235
Telegram/lib_ui/ui/layers/layers.style
Normal 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);
|
||||
61
Telegram/lib_ui/ui/layers/show.cpp
Normal file
61
Telegram/lib_ui/ui/layers/show.cpp
Normal 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
|
||||
81
Telegram/lib_ui/ui/layers/show.h
Normal file
81
Telegram/lib_ui/ui/layers/show.h
Normal 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
|
||||
129
Telegram/lib_ui/ui/main_queue_processor.cpp
Normal file
129
Telegram/lib_ui/ui/main_queue_processor.cpp
Normal 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
|
||||
27
Telegram/lib_ui/ui/main_queue_processor.h
Normal file
27
Telegram/lib_ui/ui/main_queue_processor.h
Normal 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
|
||||
220
Telegram/lib_ui/ui/paint/arcs.cpp
Normal file
220
Telegram/lib_ui/ui/paint/arcs.cpp
Normal 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
|
||||
85
Telegram/lib_ui/ui/paint/arcs.h
Normal file
85
Telegram/lib_ui/ui/paint/arcs.h
Normal 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
|
||||
244
Telegram/lib_ui/ui/paint/blob.cpp
Normal file
244
Telegram/lib_ui/ui/paint/blob.cpp
Normal 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
|
||||
113
Telegram/lib_ui/ui/paint/blob.h
Normal file
113
Telegram/lib_ui/ui/paint/blob.h
Normal file
@@ -0,0 +1,113 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#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
Reference in New Issue
Block a user