Implement animated spoilers.
This commit is contained in:
parent
f82162f05a
commit
73b6bc5e13
45 changed files with 3469 additions and 3074 deletions
|
|
@ -162,6 +162,10 @@ PRIVATE
|
||||||
ui/text/text_entity.cpp
|
ui/text/text_entity.cpp
|
||||||
ui/text/text_entity.h
|
ui/text/text_entity.h
|
||||||
ui/text/text_isolated_emoji.h
|
ui/text/text_isolated_emoji.h
|
||||||
|
ui/text/text_parser.cpp
|
||||||
|
ui/text/text_parser.h
|
||||||
|
ui/text/text_renderer.cpp
|
||||||
|
ui/text/text_renderer.h
|
||||||
ui/text/text_spoiler_data.h
|
ui/text/text_spoiler_data.h
|
||||||
ui/text/text_utilities.cpp
|
ui/text/text_utilities.cpp
|
||||||
ui/text/text_utilities.h
|
ui/text/text_utilities.h
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ void transformLoadingCross(float64 loading, std::array<QPointF, kPointCount> &po
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void CrossAnimation::paintStaticLoading(
|
void CrossAnimation::paintStaticLoading(
|
||||||
Painter &p,
|
QPainter &p,
|
||||||
const style::CrossAnimation &st,
|
const style::CrossAnimation &st,
|
||||||
style::color color,
|
style::color color,
|
||||||
int x,
|
int x,
|
||||||
|
|
@ -110,7 +110,7 @@ void CrossAnimation::paintStaticLoading(
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossAnimation::paint(
|
void CrossAnimation::paint(
|
||||||
Painter &p,
|
QPainter &p,
|
||||||
const style::CrossAnimation &st,
|
const style::CrossAnimation &st,
|
||||||
style::color color,
|
style::color color,
|
||||||
int x,
|
int x,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ namespace Ui {
|
||||||
class CrossAnimation {
|
class CrossAnimation {
|
||||||
public:
|
public:
|
||||||
static void paint(
|
static void paint(
|
||||||
Painter &p,
|
QPainter &p,
|
||||||
const style::CrossAnimation &st,
|
const style::CrossAnimation &st,
|
||||||
style::color color,
|
style::color color,
|
||||||
int x,
|
int x,
|
||||||
|
|
@ -24,7 +24,7 @@ public:
|
||||||
float64 shown,
|
float64 shown,
|
||||||
float64 loading = 0.);
|
float64 loading = 0.);
|
||||||
static void paintStaticLoading(
|
static void paintStaticLoading(
|
||||||
Painter &p,
|
QPainter &p,
|
||||||
const style::CrossAnimation &st,
|
const style::CrossAnimation &st,
|
||||||
style::color color,
|
style::color color,
|
||||||
int x,
|
int x,
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ CrossLineAnimation::CrossLineAnimation(
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossLineAnimation::paint(
|
void CrossLineAnimation::paint(
|
||||||
Painter &p,
|
QPainter &p,
|
||||||
QPoint position,
|
QPoint position,
|
||||||
float64 progress,
|
float64 progress,
|
||||||
std::optional<QColor> colorOverride) {
|
std::optional<QColor> colorOverride) {
|
||||||
|
|
@ -44,7 +44,7 @@ void CrossLineAnimation::paint(
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossLineAnimation::paint(
|
void CrossLineAnimation::paint(
|
||||||
Painter &p,
|
QPainter &p,
|
||||||
int left,
|
int left,
|
||||||
int top,
|
int top,
|
||||||
float64 progress,
|
float64 progress,
|
||||||
|
|
@ -86,7 +86,7 @@ void CrossLineAnimation::fillFrame(
|
||||||
topLine.setLength(topLine.length() * progress);
|
topLine.setLength(topLine.length() * progress);
|
||||||
auto bottomLine = topLine.translated(0, _strokePen.widthF() + 1);
|
auto bottomLine = topLine.translated(0, _strokePen.widthF() + 1);
|
||||||
|
|
||||||
Painter q(&_frame);
|
auto q = QPainter(&_frame);
|
||||||
PainterHighQualityEnabler hq(q);
|
PainterHighQualityEnabler hq(q);
|
||||||
const auto colorize = ((colorOverride && colorOverride->alpha() != 255)
|
const auto colorize = ((colorOverride && colorOverride->alpha() != 255)
|
||||||
|| (!colorOverride && _st.fg->c.alpha() != 255));
|
|| (!colorOverride && _st.fg->c.alpha() != 255));
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@
|
||||||
|
|
||||||
#include "styles/style_widgets.h"
|
#include "styles/style_widgets.h"
|
||||||
|
|
||||||
class Painter;
|
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
|
|
||||||
class CrossLineAnimation {
|
class CrossLineAnimation {
|
||||||
|
|
@ -20,12 +18,12 @@ public:
|
||||||
float angle = 315);
|
float angle = 315);
|
||||||
|
|
||||||
void paint(
|
void paint(
|
||||||
Painter &p,
|
QPainter &p,
|
||||||
QPoint position,
|
QPoint position,
|
||||||
float64 progress,
|
float64 progress,
|
||||||
std::optional<QColor> colorOverride = std::nullopt);
|
std::optional<QColor> colorOverride = std::nullopt);
|
||||||
void paint(
|
void paint(
|
||||||
Painter &p,
|
QPainter &p,
|
||||||
int left,
|
int left,
|
||||||
int top,
|
int top,
|
||||||
float64 progress,
|
float64 progress,
|
||||||
|
|
|
||||||
|
|
@ -31,22 +31,77 @@ constexpr auto kImageSpoilerDarkenAlpha = 32;
|
||||||
constexpr auto kMaxCacheSize = 5 * 1024 * 1024;
|
constexpr auto kMaxCacheSize = 5 * 1024 * 1024;
|
||||||
constexpr auto kDefaultFrameDuration = crl::time(33);
|
constexpr auto kDefaultFrameDuration = crl::time(33);
|
||||||
constexpr auto kDefaultFramesCount = 60;
|
constexpr auto kDefaultFramesCount = 60;
|
||||||
constexpr auto kDefaultCanvasSize = 128;
|
constexpr auto kAutoPauseTimeout = crl::time(1000);
|
||||||
constexpr auto kDefaultParticlesCount = 3000;
|
|
||||||
constexpr auto kDefaultFadeInDuration = crl::time(300);
|
|
||||||
constexpr auto kDefaultParticleShownDuration = crl::time(0);
|
|
||||||
constexpr auto kDefaultFadeOutDuration = crl::time(300);
|
|
||||||
|
|
||||||
std::atomic<const SpoilerMessCached*> DefaultMask/* = nullptr*/;
|
[[nodiscard]] SpoilerMessDescriptor DefaultDescriptorText() {
|
||||||
std::condition_variable *DefaultMaskSignal/* = nullptr*/;
|
const auto ratio = style::DevicePixelRatio();
|
||||||
std::mutex *DefaultMaskMutex/* = nullptr*/;
|
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;
|
||||||
|
|
||||||
struct AnimationManager {
|
|
||||||
Ui::Animations::Basic animation;
|
|
||||||
base::flat_set<not_null<SpoilerAnimation*>> list;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AnimationManager *DefaultAnimationManager/* = nullptr*/;
|
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 {
|
struct Header {
|
||||||
uint32 version = 0;
|
uint32 version = 0;
|
||||||
|
|
@ -137,58 +192,139 @@ struct Particle {
|
||||||
return base.isEmpty() ? QString() : (base + "/spoiler");
|
return base.isEmpty() ? QString() : (base + "/spoiler");
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] QString DefaultMaskCachePath(const QString &folder) {
|
|
||||||
return folder + "/mask";
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] std::optional<SpoilerMessCached> ReadDefaultMask(
|
[[nodiscard]] std::optional<SpoilerMessCached> ReadDefaultMask(
|
||||||
|
const QString &name,
|
||||||
std::optional<SpoilerMessCached::Validator> validator) {
|
std::optional<SpoilerMessCached::Validator> validator) {
|
||||||
const auto folder = DefaultMaskCacheFolder();
|
const auto folder = DefaultMaskCacheFolder();
|
||||||
if (folder.isEmpty()) {
|
if (folder.isEmpty()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
auto file = QFile(DefaultMaskCachePath(folder));
|
auto file = QFile(folder + '/' + name);
|
||||||
return (file.open(QIODevice::ReadOnly) && file.size() <= kMaxCacheSize)
|
return (file.open(QIODevice::ReadOnly) && file.size() <= kMaxCacheSize)
|
||||||
? SpoilerMessCached::FromSerialized(file.readAll(), validator)
|
? SpoilerMessCached::FromSerialized(file.readAll(), validator)
|
||||||
: std::nullopt;
|
: std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WriteDefaultMask(const SpoilerMessCached &mask) {
|
void WriteDefaultMask(
|
||||||
|
const QString &name,
|
||||||
|
const SpoilerMessCached &mask) {
|
||||||
const auto folder = DefaultMaskCacheFolder();
|
const auto folder = DefaultMaskCacheFolder();
|
||||||
if (!QDir().mkpath(folder)) {
|
if (!QDir().mkpath(folder)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto bytes = mask.serialize();
|
const auto bytes = mask.serialize();
|
||||||
auto file = QFile(DefaultMaskCachePath(folder));
|
auto file = QFile(folder + '/' + name);
|
||||||
if (file.open(QIODevice::WriteOnly) && bytes.size() <= kMaxCacheSize) {
|
if (file.open(QIODevice::WriteOnly) && bytes.size() <= kMaxCacheSize) {
|
||||||
file.write(bytes);
|
file.write(bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Register(not_null<SpoilerAnimation*> animation) {
|
void Register(not_null<SpoilerAnimation*> animation) {
|
||||||
if (!DefaultAnimationManager) {
|
if (DefaultAnimationManager) {
|
||||||
DefaultAnimationManager = new AnimationManager();
|
DefaultAnimationManager->add(animation);
|
||||||
DefaultAnimationManager->animation.init([] {
|
} else {
|
||||||
for (const auto &animation : DefaultAnimationManager->list) {
|
new SpoilerAnimationManager(animation);
|
||||||
animation->repaint();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
DefaultAnimationManager->animation.start();
|
|
||||||
}
|
}
|
||||||
DefaultAnimationManager->list.emplace(animation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Unregister(not_null<SpoilerAnimation*> animation) {
|
void Unregister(not_null<SpoilerAnimation*> animation) {
|
||||||
Expects(DefaultAnimationManager != nullptr);
|
Expects(DefaultAnimationManager != nullptr);
|
||||||
|
|
||||||
DefaultAnimationManager->list.remove(animation);
|
DefaultAnimationManager->remove(animation);
|
||||||
if (DefaultAnimationManager->list.empty()) {
|
}
|
||||||
delete base::take(DefaultAnimationManager);
|
|
||||||
|
// 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
|
} // 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(
|
SpoilerMessCached GenerateSpoilerMess(
|
||||||
const SpoilerMessDescriptor &descriptor) {
|
const SpoilerMessDescriptor &descriptor) {
|
||||||
Expects(descriptor.framesCount > 0);
|
Expects(descriptor.framesCount > 0);
|
||||||
|
|
@ -619,6 +755,7 @@ SpoilerAnimation::~SpoilerAnimation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
int SpoilerAnimation::index(crl::time now, bool paused) {
|
int SpoilerAnimation::index(crl::time now, bool paused) {
|
||||||
|
_scheduled = false;
|
||||||
const auto add = std::min(now - _last, kDefaultFrameDuration);
|
const auto add = std::min(now - _last, kDefaultFrameDuration);
|
||||||
if (anim::Disabled()) {
|
if (anim::Disabled()) {
|
||||||
paused = true;
|
paused = true;
|
||||||
|
|
@ -638,66 +775,34 @@ int SpoilerAnimation::index(crl::time now, bool paused) {
|
||||||
return absolute % kDefaultFramesCount;
|
return absolute % kDefaultFramesCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpoilerAnimation::repaint() {
|
bool SpoilerAnimation::repaint(crl::time now) {
|
||||||
_repaint();
|
if (!_scheduled) {
|
||||||
}
|
_scheduled = true;
|
||||||
|
_repaint();
|
||||||
void PrepareDefaultSpoilerMess() {
|
} else if (_animating && _last && _last + kAutoPauseTimeout <= now) {
|
||||||
DefaultMaskSignal = new std::condition_variable();
|
_animating = false;
|
||||||
DefaultMaskMutex = new std::mutex();
|
return false;
|
||||||
crl::async([] {
|
|
||||||
const auto ratio = style::DevicePixelRatio();
|
|
||||||
const auto size = style::ConvertScale(kDefaultCanvasSize) * ratio;
|
|
||||||
auto cached = ReadDefaultMask(SpoilerMessCached::Validator{
|
|
||||||
.frameDuration = kDefaultFrameDuration,
|
|
||||||
.framesCount = kDefaultFramesCount,
|
|
||||||
.canvasSize = size,
|
|
||||||
});
|
|
||||||
if (cached) {
|
|
||||||
DefaultMask = new SpoilerMessCached(std::move(*cached));
|
|
||||||
} else {
|
|
||||||
DefaultMask = new SpoilerMessCached(GenerateSpoilerMess({
|
|
||||||
.particleFadeInDuration = kDefaultFadeInDuration,
|
|
||||||
.particleShownDuration = kDefaultParticleShownDuration,
|
|
||||||
.particleFadeOutDuration = kDefaultFadeOutDuration,
|
|
||||||
.particleSizeMin = style::ConvertScaleExact(1.5) * ratio,
|
|
||||||
.particleSizeMax = style::ConvertScaleExact(2.) * ratio,
|
|
||||||
.particleSpeedMin = style::ConvertScaleExact(10.),
|
|
||||||
.particleSpeedMax = style::ConvertScaleExact(20.),
|
|
||||||
.particleSpritesCount = 5,
|
|
||||||
.particlesCount = kDefaultParticlesCount,
|
|
||||||
.canvasSize = size,
|
|
||||||
.framesCount = kDefaultFramesCount,
|
|
||||||
.frameDuration = kDefaultFrameDuration,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
auto lock = std::unique_lock(*DefaultMaskMutex);
|
|
||||||
DefaultMaskSignal->notify_all();
|
|
||||||
if (!cached) {
|
|
||||||
WriteDefaultMask(*DefaultMask);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const SpoilerMessCached &DefaultSpoilerMask() {
|
|
||||||
if (const auto result = DefaultMask.load()) {
|
|
||||||
return *result;
|
|
||||||
}
|
|
||||||
Assert(DefaultMaskSignal != nullptr);
|
|
||||||
Assert(DefaultMaskMutex != nullptr);
|
|
||||||
while (true) {
|
|
||||||
auto lock = std::unique_lock(*DefaultMaskMutex);
|
|
||||||
if (const auto result = DefaultMask.load()) {
|
|
||||||
return *result;
|
|
||||||
}
|
|
||||||
DefaultMaskSignal->wait(lock);
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SpoilerMessCached &DefaultImageSpoiler() {
|
void PrepareTextSpoilerMask() {
|
||||||
static const auto result = [&] {
|
PrepareDefaultSpoiler(
|
||||||
const auto mask = Ui::DefaultSpoilerMask();
|
DefaultTextMask,
|
||||||
const auto frame = mask.frame(0);
|
"text",
|
||||||
|
DefaultDescriptorText,
|
||||||
|
[](std::unique_ptr<SpoilerMessCached> cached) { return cached; });
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpoilerMessCached &DefaultTextSpoilerMask() {
|
||||||
|
return WaitDefaultSpoiler(DefaultTextMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrepareImageSpoiler() {
|
||||||
|
const auto postprocess = [](std::unique_ptr<SpoilerMessCached> cached) {
|
||||||
|
Expects(cached != nullptr);
|
||||||
|
|
||||||
|
const auto frame = cached->frame(0);
|
||||||
auto image = QImage(
|
auto image = QImage(
|
||||||
frame.image->size(),
|
frame.image->size(),
|
||||||
QImage::Format_ARGB32_Premultiplied);
|
QImage::Format_ARGB32_Premultiplied);
|
||||||
|
|
@ -705,13 +810,21 @@ const SpoilerMessCached &DefaultImageSpoiler() {
|
||||||
auto p = QPainter(&image);
|
auto p = QPainter(&image);
|
||||||
p.drawImage(0, 0, *frame.image);
|
p.drawImage(0, 0, *frame.image);
|
||||||
p.end();
|
p.end();
|
||||||
return Ui::SpoilerMessCached(
|
return std::make_unique<SpoilerMessCached>(
|
||||||
std::move(image),
|
std::move(image),
|
||||||
mask.framesCount(),
|
cached->framesCount(),
|
||||||
mask.frameDuration(),
|
cached->frameDuration(),
|
||||||
mask.canvasSize());
|
cached->canvasSize());
|
||||||
}();
|
};
|
||||||
return result;
|
PrepareDefaultSpoiler(
|
||||||
|
DefaultImageCached,
|
||||||
|
"image",
|
||||||
|
DefaultDescriptorImage,
|
||||||
|
postprocess);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpoilerMessCached &DefaultImageSpoiler() {
|
||||||
|
return WaitDefaultSpoiler(DefaultImageCached);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ private:
|
||||||
};
|
};
|
||||||
|
|
||||||
// Works with default frame duration and default frame count.
|
// Works with default frame duration and default frame count.
|
||||||
|
class SpoilerAnimationManager;
|
||||||
class SpoilerAnimation final {
|
class SpoilerAnimation final {
|
||||||
public:
|
public:
|
||||||
explicit SpoilerAnimation(Fn<void()> repaint);
|
explicit SpoilerAnimation(Fn<void()> repaint);
|
||||||
|
|
@ -97,21 +98,25 @@ public:
|
||||||
|
|
||||||
int index(crl::time now, bool paused);
|
int index(crl::time now, bool paused);
|
||||||
|
|
||||||
void repaint();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
friend class SpoilerAnimationManager;
|
||||||
|
|
||||||
|
[[nodiscard]] bool repaint(crl::time now);
|
||||||
|
|
||||||
const Fn<void()> _repaint;
|
const Fn<void()> _repaint;
|
||||||
crl::time _accumulated = 0;
|
crl::time _accumulated = 0;
|
||||||
crl::time _last = 0;
|
crl::time _last = 0;
|
||||||
bool _animating = false;
|
bool _animating : 1 = false;
|
||||||
|
bool _scheduled : 1 = false;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
[[nodiscard]] SpoilerMessCached GenerateSpoilerMess(
|
[[nodiscard]] SpoilerMessCached GenerateSpoilerMess(
|
||||||
const SpoilerMessDescriptor &descriptor);
|
const SpoilerMessDescriptor &descriptor);
|
||||||
|
|
||||||
void PrepareDefaultSpoilerMess();
|
void PrepareTextSpoilerMask();
|
||||||
[[nodiscard]] const SpoilerMessCached &DefaultSpoilerMask();
|
[[nodiscard]] const SpoilerMessCached &DefaultTextSpoilerMask();
|
||||||
|
void PrepareImageSpoiler();
|
||||||
[[nodiscard]] const SpoilerMessCached &DefaultImageSpoiler();
|
[[nodiscard]] const SpoilerMessCached &DefaultImageSpoiler();
|
||||||
|
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,10 @@ std::unique_ptr<Text::CustomEmoji> Integration::createCustomEmoji(
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Fn<void()> Integration::createSpoilerRepaint(const std::any &context) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
bool Integration::handleUrlClick(
|
bool Integration::handleUrlClick(
|
||||||
const QString &url,
|
const QString &url,
|
||||||
const QVariant &context) {
|
const QVariant &context) {
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,8 @@ public:
|
||||||
[[nodiscard]] virtual auto createCustomEmoji(
|
[[nodiscard]] virtual auto createCustomEmoji(
|
||||||
const QString &data,
|
const QString &data,
|
||||||
const std::any &context) -> std::unique_ptr<Text::CustomEmoji>;
|
const std::any &context) -> std::unique_ptr<Text::CustomEmoji>;
|
||||||
|
[[nodiscard]] virtual Fn<void()> createSpoilerRepaint(
|
||||||
|
const std::any &context);
|
||||||
|
|
||||||
[[nodiscard]] virtual rpl::producer<> forcePopupMenuHideRequests();
|
[[nodiscard]] virtual rpl::producer<> forcePopupMenuHideRequests();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include "ui/layers/box_layer_widget.h"
|
#include "ui/layers/box_layer_widget.h"
|
||||||
#include "ui/widgets/shadow.h"
|
#include "ui/widgets/shadow.h"
|
||||||
#include "ui/image/image_prepare.h"
|
#include "ui/image/image_prepare.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
#include "ui/ui_utility.h"
|
#include "ui/ui_utility.h"
|
||||||
#include "ui/round_rect.h"
|
#include "ui/round_rect.h"
|
||||||
#include "styles/style_layers.h"
|
#include "styles/style_layers.h"
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ bool ArcsAnimation::isArcFinished(const Arc &arc) const {
|
||||||
|| ((arc.threshold <= _currentValue) && (arc.progress == 0.));
|
|| ((arc.threshold <= _currentValue) && (arc.progress == 0.));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ArcsAnimation::paint(Painter &p, std::optional<QColor> colorOverride) {
|
void ArcsAnimation::paint(QPainter &p, std::optional<QColor> colorOverride) {
|
||||||
PainterHighQualityEnabler hq(p);
|
PainterHighQualityEnabler hq(p);
|
||||||
QPen pen;
|
QPen pen;
|
||||||
if (_strokeRatio) {
|
if (_strokeRatio) {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ public:
|
||||||
Direction direction);
|
Direction direction);
|
||||||
|
|
||||||
void paint(
|
void paint(
|
||||||
Painter &p,
|
QPainter &p,
|
||||||
std::optional<QColor> colorOverride = std::nullopt);
|
std::optional<QColor> colorOverride = std::nullopt);
|
||||||
|
|
||||||
void setValue(float64 value);
|
void setValue(float64 value);
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ RadialBlob::RadialBlob(int n, float minScale, float minSpeed, float maxSpeed)
|
||||||
, _segments(n) {
|
, _segments(n) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void RadialBlob::paint(Painter &p, const QBrush &brush, float outerScale) {
|
void RadialBlob::paint(QPainter &p, const QBrush &brush, float outerScale) {
|
||||||
auto path = QPainterPath();
|
auto path = QPainterPath();
|
||||||
auto m = QTransform();
|
auto m = QTransform();
|
||||||
|
|
||||||
|
|
@ -172,7 +172,7 @@ LinearBlob::LinearBlob(
|
||||||
, _segments(_segmentsCount) {
|
, _segments(_segmentsCount) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void LinearBlob::paint(Painter &p, const QBrush &brush, int width) {
|
void LinearBlob::paint(QPainter &p, const QBrush &brush, int width) {
|
||||||
if (!width) {
|
if (!width) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ class RadialBlob final : public Blob {
|
||||||
public:
|
public:
|
||||||
RadialBlob(int n, float minScale, float minSpeed = 0, float maxSpeed = 0);
|
RadialBlob(int n, float minScale, float minSpeed = 0, float maxSpeed = 0);
|
||||||
|
|
||||||
void paint(Painter &p, const QBrush &brush, float outerScale = 1.);
|
void paint(QPainter &p, const QBrush &brush, float outerScale = 1.);
|
||||||
void update(float level, float speedScale, float64 rate);
|
void update(float level, float speedScale, float64 rate);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
@ -94,7 +94,7 @@ public:
|
||||||
float minSpeed = 0,
|
float minSpeed = 0,
|
||||||
float maxSpeed = 0);
|
float maxSpeed = 0);
|
||||||
|
|
||||||
void paint(Painter &p, const QBrush &brush, int width);
|
void paint(QPainter &p, const QBrush &brush, int width);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Segment : Blob::Segment {
|
struct Segment : Blob::Segment {
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ void Blobs::resetLevel() {
|
||||||
_levelValue.reset();
|
_levelValue.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Blobs::paint(Painter &p, const QBrush &brush, float outerScale) {
|
void Blobs::paint(QPainter &p, const QBrush &brush, float outerScale) {
|
||||||
const auto opacity = p.opacity();
|
const auto opacity = p.opacity();
|
||||||
for (auto i = 0; i < _blobs.size(); i++) {
|
for (auto i = 0; i < _blobs.size(); i++) {
|
||||||
const auto alpha = _blobDatas[i].alpha;
|
const auto alpha = _blobDatas[i].alpha;
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ public:
|
||||||
|
|
||||||
void setLevel(float value);
|
void setLevel(float value);
|
||||||
void resetLevel();
|
void resetLevel();
|
||||||
void paint(Painter &p, const QBrush &brush, float outerScale = 1.);
|
void paint(QPainter &p, const QBrush &brush, float outerScale = 1.);
|
||||||
void updateLevel(crl::time dt);
|
void updateLevel(crl::time dt);
|
||||||
|
|
||||||
[[nodiscard]] float maxRadius() const;
|
[[nodiscard]] float maxRadius() const;
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ void LinearBlobs::setLevel(float value) {
|
||||||
_levelValue.start(to);
|
_levelValue.start(to);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LinearBlobs::paint(Painter &p, const QBrush &brush, int width) {
|
void LinearBlobs::paint(QPainter &p, const QBrush &brush, int width) {
|
||||||
PainterHighQualityEnabler hq(p);
|
PainterHighQualityEnabler hq(p);
|
||||||
const auto opacity = p.opacity();
|
const auto opacity = p.opacity();
|
||||||
for (auto i = 0; i < _blobs.size(); i++) {
|
for (auto i = 0; i < _blobs.size(); i++) {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@
|
||||||
#include "ui/effects/animation_value.h"
|
#include "ui/effects/animation_value.h"
|
||||||
#include "ui/paint/blob.h"
|
#include "ui/paint/blob.h"
|
||||||
|
|
||||||
class Painter;
|
|
||||||
|
|
||||||
namespace Ui::Paint {
|
namespace Ui::Paint {
|
||||||
|
|
||||||
class LinearBlobs final {
|
class LinearBlobs final {
|
||||||
|
|
@ -38,7 +36,7 @@ public:
|
||||||
Blob::Radiuses radiusesAt(int index);
|
Blob::Radiuses radiusesAt(int index);
|
||||||
|
|
||||||
void setLevel(float value);
|
void setLevel(float value);
|
||||||
void paint(Painter &p, const QBrush &brush, int width);
|
void paint(QPainter &p, const QBrush &brush, int width);
|
||||||
void updateLevel(crl::time dt);
|
void updateLevel(crl::time dt);
|
||||||
|
|
||||||
[[nodiscard]] float maxRadius() const;
|
[[nodiscard]] float maxRadius() const;
|
||||||
|
|
|
||||||
14
ui/painter.h
14
ui/painter.h
|
|
@ -11,6 +11,10 @@
|
||||||
#include <QtCore/QPoint>
|
#include <QtCore/QPoint>
|
||||||
#include <QtGui/QPainter>
|
#include <QtGui/QPainter>
|
||||||
|
|
||||||
|
namespace Ui::Text {
|
||||||
|
struct SpoilerMess;
|
||||||
|
} // namespace Ui::Text
|
||||||
|
|
||||||
class Painter : public QPainter {
|
class Painter : public QPainter {
|
||||||
public:
|
public:
|
||||||
explicit Painter(QPaintDevice *device) : QPainter(device) {
|
explicit Painter(QPaintDevice *device) : QPainter(device) {
|
||||||
|
|
@ -78,9 +82,19 @@ public:
|
||||||
[[nodiscard]] bool inactive() const {
|
[[nodiscard]] bool inactive() const {
|
||||||
return _inactive;
|
return _inactive;
|
||||||
}
|
}
|
||||||
|
void setTextSpoilerMess(not_null<Ui::Text::SpoilerMess*> mess) {
|
||||||
|
_spoilerMess = mess;
|
||||||
|
}
|
||||||
|
void restoreTextSpoilerMess() {
|
||||||
|
_spoilerMess = nullptr;
|
||||||
|
}
|
||||||
|
[[nodiscard]] Ui::Text::SpoilerMess *textSpoilerMess() const {
|
||||||
|
return _spoilerMess;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const style::TextPalette *_textPalette = nullptr;
|
const style::TextPalette *_textPalette = nullptr;
|
||||||
|
Ui::Text::SpoilerMess *_spoilerMess = nullptr;
|
||||||
bool _inactive = false;
|
bool _inactive = false;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
3036
ui/text/text.cpp
3036
ui/text/text.cpp
File diff suppressed because it is too large
Load diff
|
|
@ -8,15 +8,20 @@
|
||||||
|
|
||||||
#include "ui/text/text_entity.h"
|
#include "ui/text/text_entity.h"
|
||||||
#include "ui/text/text_block.h"
|
#include "ui/text/text_block.h"
|
||||||
#include "ui/painter.h"
|
#include "ui/effects/spoiler_mess.h"
|
||||||
#include "ui/click_handler.h"
|
#include "ui/click_handler.h"
|
||||||
#include "base/flags.h"
|
#include "base/flags.h"
|
||||||
|
|
||||||
#include <private/qfixed_p.h>
|
#include <private/qfixed_p.h>
|
||||||
#include <any>
|
#include <any>
|
||||||
|
|
||||||
|
class Painter;
|
||||||
class SpoilerClickHandler;
|
class SpoilerClickHandler;
|
||||||
|
|
||||||
|
namespace style {
|
||||||
|
struct TextPalette;
|
||||||
|
} // namespace style
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
static const auto kQEllipsis = QStringLiteral("...");
|
static const auto kQEllipsis = QStringLiteral("...");
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
|
@ -90,7 +95,7 @@ struct StateResult {
|
||||||
uint16 symbol = 0;
|
uint16 symbol = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct StateRequestElided : public StateRequest {
|
struct StateRequestElided : StateRequest {
|
||||||
StateRequestElided() {
|
StateRequestElided() {
|
||||||
}
|
}
|
||||||
StateRequestElided(const StateRequest &other) : StateRequest(other) {
|
StateRequestElided(const StateRequest &other) : StateRequest(other) {
|
||||||
|
|
@ -99,6 +104,48 @@ struct StateRequestElided : public StateRequest {
|
||||||
int removeFromEnd = 0;
|
int removeFromEnd = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class SpoilerMessCache {
|
||||||
|
public:
|
||||||
|
explicit SpoilerMessCache(int capacity);
|
||||||
|
|
||||||
|
[[nodiscard]] not_null<SpoilerMessCached*> lookup(QColor color);
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Entry {
|
||||||
|
SpoilerMessCached mess;
|
||||||
|
QColor color;
|
||||||
|
int generation = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Entry> _cache;
|
||||||
|
const int _capacity = 0;
|
||||||
|
int _generation = 0;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] not_null<SpoilerMessCache*> DefaultSpoilerCache();
|
||||||
|
|
||||||
|
struct PaintContext {
|
||||||
|
QPoint position;
|
||||||
|
int outerWidth = 0; // For automatic RTL Ui inversion.
|
||||||
|
int availableWidth = 0;
|
||||||
|
style::align align = style::al_left;
|
||||||
|
QRect clip;
|
||||||
|
|
||||||
|
const style::TextPalette *palette = nullptr;
|
||||||
|
SpoilerMessCache *spoiler = nullptr;
|
||||||
|
crl::time now = 0;
|
||||||
|
bool paused = false;
|
||||||
|
|
||||||
|
TextSelection selection;
|
||||||
|
bool fullWidthSelection = true;
|
||||||
|
|
||||||
|
int elisionLines = 0;
|
||||||
|
int elisionRemoveFromEnd = 0;
|
||||||
|
bool elisionBreakEverywhere = false;
|
||||||
|
};
|
||||||
|
|
||||||
class String {
|
class String {
|
||||||
public:
|
public:
|
||||||
String(int32 minResizeWidth = QFIXED_MAX);
|
String(int32 minResizeWidth = QFIXED_MAX);
|
||||||
|
|
@ -107,9 +154,9 @@ public:
|
||||||
const QString &text,
|
const QString &text,
|
||||||
const TextParseOptions &options = kDefaultTextOptions,
|
const TextParseOptions &options = kDefaultTextOptions,
|
||||||
int32 minResizeWidth = QFIXED_MAX);
|
int32 minResizeWidth = QFIXED_MAX);
|
||||||
String(String &&other) = default;
|
String(String &&other);
|
||||||
String &operator=(String &&other) = default;
|
String &operator=(String &&other);
|
||||||
~String() = default;
|
~String();
|
||||||
|
|
||||||
[[nodiscard]] int countWidth(int width, bool breakEverywhere = false) const;
|
[[nodiscard]] int countWidth(int width, bool breakEverywhere = false) const;
|
||||||
[[nodiscard]] int countHeight(int width, bool breakEverywhere = false) const;
|
[[nodiscard]] int countHeight(int width, bool breakEverywhere = false) const;
|
||||||
|
|
@ -137,6 +184,8 @@ public:
|
||||||
}
|
}
|
||||||
[[nodiscard]] int countMaxMonospaceWidth() const;
|
[[nodiscard]] int countMaxMonospaceWidth() const;
|
||||||
|
|
||||||
|
void draw(QPainter &p, const PaintContext &context) const;
|
||||||
|
|
||||||
void draw(Painter &p, int32 left, int32 top, int32 width, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, TextSelection selection = { 0, 0 }, bool fullWidthSelection = true) const;
|
void draw(Painter &p, int32 left, int32 top, int32 width, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, TextSelection selection = { 0, 0 }, bool fullWidthSelection = true) const;
|
||||||
void drawElided(Painter &p, int32 left, int32 top, int32 width, int32 lines = 1, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, int32 removeFromEnd = 0, bool breakEverywhere = false, TextSelection selection = { 0, 0 }) const;
|
void drawElided(Painter &p, int32 left, int32 top, int32 width, int32 lines = 1, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, int32 removeFromEnd = 0, bool breakEverywhere = false, TextSelection selection = { 0, 0 }) const;
|
||||||
void drawLeft(Painter &p, int32 left, int32 top, int32 width, int32 outerw, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, TextSelection selection = { 0, 0 }) const;
|
void drawLeft(Painter &p, int32 left, int32 top, int32 width, int32 outerw, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, TextSelection selection = { 0, 0 }) const;
|
||||||
|
|
@ -169,8 +218,8 @@ public:
|
||||||
[[nodiscard]] TextForMimeData toTextForMimeData(
|
[[nodiscard]] TextForMimeData toTextForMimeData(
|
||||||
TextSelection selection = AllTextSelection) const;
|
TextSelection selection = AllTextSelection) const;
|
||||||
|
|
||||||
[[nodiscard]] bool hasCustomEmoji() const;
|
[[nodiscard]] bool hasPersistentAnimation() const;
|
||||||
void unloadCustomEmoji();
|
void unloadPersistentAnimation();
|
||||||
|
|
||||||
[[nodiscard]] bool isIsolatedEmoji() const;
|
[[nodiscard]] bool isIsolatedEmoji() const;
|
||||||
[[nodiscard]] IsolatedEmoji toIsolatedEmoji() const;
|
[[nodiscard]] IsolatedEmoji toIsolatedEmoji() const;
|
||||||
|
|
@ -217,9 +266,9 @@ private:
|
||||||
QFixed _minResizeWidth;
|
QFixed _minResizeWidth;
|
||||||
QFixed _maxWidth = 0;
|
QFixed _maxWidth = 0;
|
||||||
int32 _minHeight = 0;
|
int32 _minHeight = 0;
|
||||||
bool _hasCustomEmoji : 1;
|
bool _hasCustomEmoji : 1 = false;
|
||||||
bool _isIsolatedEmoji : 1;
|
bool _isIsolatedEmoji : 1 = false;
|
||||||
bool _isOnlyCustomEmoji : 1;
|
bool _isOnlyCustomEmoji : 1 = false;
|
||||||
|
|
||||||
QString _text;
|
QString _text;
|
||||||
const style::TextStyle *_st = nullptr;
|
const style::TextStyle *_st = nullptr;
|
||||||
|
|
@ -229,13 +278,14 @@ private:
|
||||||
|
|
||||||
Qt::LayoutDirection _startDir = Qt::LayoutDirectionAuto;
|
Qt::LayoutDirection _startDir = Qt::LayoutDirectionAuto;
|
||||||
|
|
||||||
std::shared_ptr<SpoilerData> _spoiler;
|
std::unique_ptr<SpoilerData> _spoiler;
|
||||||
|
|
||||||
friend class Parser;
|
friend class Parser;
|
||||||
friend class Renderer;
|
friend class Renderer;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsBad(QChar ch);
|
||||||
[[nodiscard]] bool IsWordSeparator(QChar ch);
|
[[nodiscard]] bool IsWordSeparator(QChar ch);
|
||||||
[[nodiscard]] bool IsAlmostLinkEnd(QChar ch);
|
[[nodiscard]] bool IsAlmostLinkEnd(QChar ch);
|
||||||
[[nodiscard]] bool IsLinkEnd(QChar ch);
|
[[nodiscard]] bool IsLinkEnd(QChar ch);
|
||||||
|
|
|
||||||
|
|
@ -758,5 +758,15 @@ void Block::destroy() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int CountBlockHeight(
|
||||||
|
const AbstractBlock *block,
|
||||||
|
const style::TextStyle *st) {
|
||||||
|
return (block->type() == TextBlockTSkip)
|
||||||
|
? static_cast<const SkipBlock*>(block)->height()
|
||||||
|
: (st->lineHeight > st->font->height)
|
||||||
|
? st->lineHeight
|
||||||
|
: st->font->height;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Text
|
} // namespace Text
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,11 @@
|
||||||
|
|
||||||
#include <crl/crl_time.h>
|
#include <crl/crl_time.h>
|
||||||
|
|
||||||
namespace Ui {
|
namespace style {
|
||||||
namespace Text {
|
struct TextStyle;
|
||||||
|
} // namespace style
|
||||||
|
|
||||||
|
namespace Ui::Text {
|
||||||
|
|
||||||
enum TextBlockType {
|
enum TextBlockType {
|
||||||
TextBlockTNewline = 0x01,
|
TextBlockTNewline = 0x01,
|
||||||
|
|
@ -318,5 +321,12 @@ private:
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Text
|
[[nodiscard]] int CountBlockHeight(
|
||||||
} // namespace Ui
|
const AbstractBlock *block,
|
||||||
|
const style::TextStyle *st);
|
||||||
|
|
||||||
|
[[nodiscard]] inline bool IsMono(int32 flags) {
|
||||||
|
return (flags & TextBlockFPre) || (flags & TextBlockFCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Ui::Text
|
||||||
|
|
|
||||||
739
ui/text/text_parser.cpp
Normal file
739
ui/text/text_parser.cpp
Normal file
|
|
@ -0,0 +1,739 @@
|
||||||
|
// This file is part of Desktop App Toolkit,
|
||||||
|
// a set of 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/text/text_parser.h"
|
||||||
|
|
||||||
|
#include "base/platform/base_platform_info.h"
|
||||||
|
#include "ui/integration.h"
|
||||||
|
#include "ui/text/text_isolated_emoji.h"
|
||||||
|
#include "ui/text/text_spoiler_data.h"
|
||||||
|
#include "ui/spoiler_click_handler.h"
|
||||||
|
#include "styles/style_basic.h"
|
||||||
|
|
||||||
|
#include <QtCore/QUrl>
|
||||||
|
|
||||||
|
namespace Ui::Text {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kStringLinkIndexShift = uint16(0x8000);
|
||||||
|
constexpr auto kMaxDiacAfterSymbol = 2;
|
||||||
|
|
||||||
|
[[nodiscard]] TextWithEntities PrepareRichFromRich(
|
||||||
|
const TextWithEntities &text,
|
||||||
|
const TextParseOptions &options) {
|
||||||
|
auto result = text;
|
||||||
|
const auto &preparsed = text.entities;
|
||||||
|
const bool parseLinks = (options.flags & TextParseLinks);
|
||||||
|
const bool parsePlainLinks = (options.flags & TextParsePlainLinks);
|
||||||
|
if (!preparsed.isEmpty() && (parseLinks || parsePlainLinks)) {
|
||||||
|
bool parseMentions = (options.flags & TextParseMentions);
|
||||||
|
bool parseHashtags = (options.flags & TextParseHashtags);
|
||||||
|
bool parseBotCommands = (options.flags & TextParseBotCommands);
|
||||||
|
bool parseMarkdown = (options.flags & TextParseMarkdown);
|
||||||
|
if (!parseMentions || !parseHashtags || !parseBotCommands || !parseMarkdown) {
|
||||||
|
int32 i = 0, l = preparsed.size();
|
||||||
|
result.entities.clear();
|
||||||
|
result.entities.reserve(l);
|
||||||
|
for (; i < l; ++i) {
|
||||||
|
auto type = preparsed.at(i).type();
|
||||||
|
if (((type == EntityType::Mention || type == EntityType::MentionName) && !parseMentions) ||
|
||||||
|
(type == EntityType::Hashtag && !parseHashtags) ||
|
||||||
|
(type == EntityType::Cashtag && !parseHashtags) ||
|
||||||
|
(type == EntityType::PlainLink
|
||||||
|
&& !parsePlainLinks
|
||||||
|
&& !parseMarkdown) ||
|
||||||
|
(!parseLinks
|
||||||
|
&& (type == EntityType::Url
|
||||||
|
|| type == EntityType::CustomUrl)) ||
|
||||||
|
(type == EntityType::BotCommand && !parseBotCommands) || // #TODO entities
|
||||||
|
(!parseMarkdown && (type == EntityType::Bold
|
||||||
|
|| type == EntityType::Semibold
|
||||||
|
|| type == EntityType::Italic
|
||||||
|
|| type == EntityType::Underline
|
||||||
|
|| type == EntityType::StrikeOut
|
||||||
|
|| type == EntityType::Code
|
||||||
|
|| type == EntityType::Pre))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.entities.push_back(preparsed.at(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] QFixed ComputeStopAfter(
|
||||||
|
const TextParseOptions &options,
|
||||||
|
const style::TextStyle &st) {
|
||||||
|
return (options.maxw > 0 && options.maxh > 0)
|
||||||
|
? ((options.maxh / st.font->height) + 1) * options.maxw
|
||||||
|
: QFIXED_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open Sans tilde fix.
|
||||||
|
[[nodiscard]] bool ComputeCheckTilde(const style::TextStyle &st) {
|
||||||
|
const auto &font = st.font;
|
||||||
|
return (font->size() * style::DevicePixelRatio() == 13)
|
||||||
|
&& (font->flags() == 0)
|
||||||
|
&& (font->f.family() == qstr("DAOpenSansRegular"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Parser::StartedEntity::StartedEntity(TextBlockFlags flags)
|
||||||
|
: _value(flags)
|
||||||
|
, _type(Type::Flags) {
|
||||||
|
Expects(_value >= 0 && _value < int(kStringLinkIndexShift));
|
||||||
|
}
|
||||||
|
|
||||||
|
Parser::StartedEntity::StartedEntity(uint16 index, Type type)
|
||||||
|
: _value(index)
|
||||||
|
, _type(type) {
|
||||||
|
Expects((_type == Type::Link)
|
||||||
|
? (_value >= kStringLinkIndexShift)
|
||||||
|
: (_value < kStringLinkIndexShift));
|
||||||
|
}
|
||||||
|
|
||||||
|
Parser::StartedEntity::Type Parser::StartedEntity::type() const {
|
||||||
|
return _type;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<TextBlockFlags> Parser::StartedEntity::flags() const {
|
||||||
|
if (_value < int(kStringLinkIndexShift) && (_type == Type::Flags)) {
|
||||||
|
return TextBlockFlags(_value);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<uint16> Parser::StartedEntity::lnkIndex() const {
|
||||||
|
if ((_value < int(kStringLinkIndexShift) && (_type == Type::IndexedLink))
|
||||||
|
|| (_value >= int(kStringLinkIndexShift) && (_type == Type::Link))) {
|
||||||
|
return uint16(_value);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<uint16> Parser::StartedEntity::spoilerIndex() const {
|
||||||
|
if (_value < int(kStringLinkIndexShift) && (_type == Type::Spoiler)) {
|
||||||
|
return uint16(_value);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
Parser::Parser(
|
||||||
|
not_null<String*> string,
|
||||||
|
const TextWithEntities &textWithEntities,
|
||||||
|
const TextParseOptions &options,
|
||||||
|
const std::any &context)
|
||||||
|
: Parser(
|
||||||
|
string,
|
||||||
|
PrepareRichFromRich(textWithEntities, options),
|
||||||
|
options,
|
||||||
|
context,
|
||||||
|
ReadyToken()) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Parser::Parser(
|
||||||
|
not_null<String*> string,
|
||||||
|
TextWithEntities &&source,
|
||||||
|
const TextParseOptions &options,
|
||||||
|
const std::any &context,
|
||||||
|
ReadyToken)
|
||||||
|
: _t(string)
|
||||||
|
, _source(std::move(source))
|
||||||
|
, _context(context)
|
||||||
|
, _start(_source.text.constData())
|
||||||
|
, _end(_start + _source.text.size())
|
||||||
|
, _ptr(_start)
|
||||||
|
, _entitiesEnd(_source.entities.end())
|
||||||
|
, _waitingEntity(_source.entities.begin())
|
||||||
|
, _multiline(options.flags & TextParseMultiline)
|
||||||
|
, _stopAfterWidth(ComputeStopAfter(options, *_t->_st))
|
||||||
|
, _checkTilde(ComputeCheckTilde(*_t->_st)) {
|
||||||
|
parse(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parser::blockCreated() {
|
||||||
|
_sumWidth += _t->_blocks.back()->f_width();
|
||||||
|
if (_sumWidth.floor().toInt() > _stopAfterWidth) {
|
||||||
|
_sumFinished = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parser::createBlock(int32 skipBack) {
|
||||||
|
if (_lnkIndex < kStringLinkIndexShift && _lnkIndex > _maxLnkIndex) {
|
||||||
|
_maxLnkIndex = _lnkIndex;
|
||||||
|
}
|
||||||
|
if (_lnkIndex > kStringLinkIndexShift) {
|
||||||
|
_maxShiftedLnkIndex = std::max(
|
||||||
|
uint16(_lnkIndex - kStringLinkIndexShift),
|
||||||
|
_maxShiftedLnkIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 len = int32(_t->_text.size()) + skipBack - _blockStart;
|
||||||
|
if (len > 0) {
|
||||||
|
bool newline = !_emoji && (len == 1 && _t->_text.at(_blockStart) == QChar::LineFeed);
|
||||||
|
if (_newlineAwaited) {
|
||||||
|
_newlineAwaited = false;
|
||||||
|
if (!newline) {
|
||||||
|
_t->_text.insert(_blockStart, QChar::LineFeed);
|
||||||
|
createBlock(skipBack - len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_lastSkipped = false;
|
||||||
|
const auto lnkIndex = _monoIndex ? _monoIndex : _lnkIndex;
|
||||||
|
auto custom = _customEmojiData.isEmpty()
|
||||||
|
? nullptr
|
||||||
|
: Integration::Instance().createCustomEmoji(
|
||||||
|
_customEmojiData,
|
||||||
|
_context);
|
||||||
|
if (custom) {
|
||||||
|
_t->_blocks.push_back(Block::CustomEmoji(_t->_st->font, _t->_text, _blockStart, len, _flags, lnkIndex, _spoilerIndex, std::move(custom)));
|
||||||
|
_lastSkipped = true;
|
||||||
|
} else if (_emoji) {
|
||||||
|
_t->_blocks.push_back(Block::Emoji(_t->_st->font, _t->_text, _blockStart, len, _flags, lnkIndex, _spoilerIndex, _emoji));
|
||||||
|
_lastSkipped = true;
|
||||||
|
} else if (newline) {
|
||||||
|
_t->_blocks.push_back(Block::Newline(_t->_st->font, _t->_text, _blockStart, len, _flags, lnkIndex, _spoilerIndex));
|
||||||
|
} else {
|
||||||
|
_t->_blocks.push_back(Block::Text(_t->_st->font, _t->_text, _t->_minResizeWidth, _blockStart, len, _flags, lnkIndex, _spoilerIndex));
|
||||||
|
}
|
||||||
|
_blockStart += len;
|
||||||
|
_customEmojiData = QByteArray();
|
||||||
|
_emoji = nullptr;
|
||||||
|
blockCreated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unused.
|
||||||
|
// void Parser::createSkipBlock(int32 w, int32 h) {
|
||||||
|
// createBlock();
|
||||||
|
// _t->_text.push_back('_');
|
||||||
|
// _t->_blocks.push_back(Block::Skip(_t->_st->font, _t->_text, _blockStart++, w, h, _monoIndex ? _monoIndex : _lnkIndex, _spoilerIndex));
|
||||||
|
// blockCreated();
|
||||||
|
// }
|
||||||
|
|
||||||
|
void Parser::createNewlineBlock() {
|
||||||
|
createBlock();
|
||||||
|
_t->_text.push_back(QChar::LineFeed);
|
||||||
|
createBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parser::finishEntities() {
|
||||||
|
while (!_startedEntities.empty()
|
||||||
|
&& (_ptr >= _startedEntities.begin()->first || _ptr >= _end)) {
|
||||||
|
auto list = std::move(_startedEntities.begin()->second);
|
||||||
|
_startedEntities.erase(_startedEntities.begin());
|
||||||
|
|
||||||
|
while (!list.empty()) {
|
||||||
|
if (list.back().type() == StartedEntity::Type::CustomEmoji) {
|
||||||
|
createBlock();
|
||||||
|
} else if (const auto flags = list.back().flags()) {
|
||||||
|
if (_flags & (*flags)) {
|
||||||
|
createBlock();
|
||||||
|
_flags &= ~(*flags);
|
||||||
|
if (((*flags) & TextBlockFPre)
|
||||||
|
&& !_t->_blocks.empty()
|
||||||
|
&& _t->_blocks.back()->type() != TextBlockTNewline) {
|
||||||
|
_newlineAwaited = true;
|
||||||
|
}
|
||||||
|
if (IsMono(*flags)) {
|
||||||
|
_monoIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (const auto lnkIndex = list.back().lnkIndex()) {
|
||||||
|
if (_lnkIndex == *lnkIndex) {
|
||||||
|
createBlock();
|
||||||
|
_lnkIndex = 0;
|
||||||
|
}
|
||||||
|
} else if (const auto spoilerIndex = list.back().spoilerIndex()) {
|
||||||
|
if (_spoilerIndex == *spoilerIndex && (_spoilerIndex != 0)) {
|
||||||
|
createBlock();
|
||||||
|
_spoilerIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if at least one entity was parsed in the current position.
|
||||||
|
bool Parser::checkEntities() {
|
||||||
|
finishEntities();
|
||||||
|
skipPassedEntities();
|
||||||
|
if (_waitingEntity == _entitiesEnd
|
||||||
|
|| _ptr < _start + _waitingEntity->offset()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto flags = TextBlockFlags();
|
||||||
|
auto link = EntityLinkData();
|
||||||
|
auto monoIndex = 0;
|
||||||
|
const auto entityType = _waitingEntity->type();
|
||||||
|
const auto entityLength = _waitingEntity->length();
|
||||||
|
const auto entityBegin = _start + _waitingEntity->offset();
|
||||||
|
const auto entityEnd = entityBegin + entityLength;
|
||||||
|
const auto pushSimpleUrl = [&](EntityType type) {
|
||||||
|
link.type = type;
|
||||||
|
link.data = QString(entityBegin, entityLength);
|
||||||
|
if (type == EntityType::Url) {
|
||||||
|
computeLinkText(link.data, &link.text, &link.shown);
|
||||||
|
} else {
|
||||||
|
link.text = link.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const auto pushComplexUrl = [&] {
|
||||||
|
link.type = entityType;
|
||||||
|
link.data = _waitingEntity->data();
|
||||||
|
link.text = QString(entityBegin, entityLength);
|
||||||
|
};
|
||||||
|
|
||||||
|
using Type = StartedEntity::Type;
|
||||||
|
|
||||||
|
if (entityType == EntityType::CustomEmoji) {
|
||||||
|
createBlock();
|
||||||
|
_customEmojiData = _waitingEntity->data();
|
||||||
|
_startedEntities[entityEnd].emplace_back(0, Type::CustomEmoji);
|
||||||
|
} else if (entityType == EntityType::Bold) {
|
||||||
|
flags = TextBlockFBold;
|
||||||
|
} else if (entityType == EntityType::Semibold) {
|
||||||
|
flags = TextBlockFSemibold;
|
||||||
|
} else if (entityType == EntityType::Italic) {
|
||||||
|
flags = TextBlockFItalic;
|
||||||
|
} else if (entityType == EntityType::Underline) {
|
||||||
|
flags = TextBlockFUnderline;
|
||||||
|
} else if (entityType == EntityType::PlainLink) {
|
||||||
|
flags = TextBlockFPlainLink;
|
||||||
|
} else if (entityType == EntityType::StrikeOut) {
|
||||||
|
flags = TextBlockFStrikeOut;
|
||||||
|
} else if ((entityType == EntityType::Code) // #TODO entities
|
||||||
|
|| (entityType == EntityType::Pre)) {
|
||||||
|
if (entityType == EntityType::Code) {
|
||||||
|
flags = TextBlockFCode;
|
||||||
|
} else {
|
||||||
|
flags = TextBlockFPre;
|
||||||
|
createBlock();
|
||||||
|
if (!_t->_blocks.empty()
|
||||||
|
&& _t->_blocks.back()->type() != TextBlockTNewline
|
||||||
|
&& _customEmojiData.isEmpty()) {
|
||||||
|
createNewlineBlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto text = QString(entityBegin, entityLength);
|
||||||
|
|
||||||
|
// It is better to trim the text to identify "Sample\n" as inline.
|
||||||
|
const auto trimmed = text.trimmed();
|
||||||
|
const auto isSingleLine = !trimmed.isEmpty()
|
||||||
|
&& ranges::none_of(trimmed, IsNewline);
|
||||||
|
|
||||||
|
// TODO: remove trimming.
|
||||||
|
if (isSingleLine && (entityType == EntityType::Code)) {
|
||||||
|
_monos.push_back({ .text = text, .type = entityType });
|
||||||
|
monoIndex = _monos.size();
|
||||||
|
}
|
||||||
|
} else if (entityType == EntityType::Url
|
||||||
|
|| entityType == EntityType::Email
|
||||||
|
|| entityType == EntityType::Mention
|
||||||
|
|| entityType == EntityType::Hashtag
|
||||||
|
|| entityType == EntityType::Cashtag
|
||||||
|
|| entityType == EntityType::BotCommand) {
|
||||||
|
pushSimpleUrl(entityType);
|
||||||
|
} else if (entityType == EntityType::CustomUrl) {
|
||||||
|
const auto url = _waitingEntity->data();
|
||||||
|
const auto text = QString(entityBegin, entityLength);
|
||||||
|
if (url == text) {
|
||||||
|
pushSimpleUrl(EntityType::Url);
|
||||||
|
} else {
|
||||||
|
pushComplexUrl();
|
||||||
|
}
|
||||||
|
} else if (entityType == EntityType::MentionName) {
|
||||||
|
pushComplexUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (link.type != EntityType::Invalid) {
|
||||||
|
createBlock();
|
||||||
|
|
||||||
|
_links.push_back(link);
|
||||||
|
const auto tempIndex = _links.size();
|
||||||
|
const auto useCustom = processCustomIndex(tempIndex);
|
||||||
|
_lnkIndex = tempIndex + (useCustom ? 0 : kStringLinkIndexShift);
|
||||||
|
_startedEntities[entityEnd].emplace_back(
|
||||||
|
_lnkIndex,
|
||||||
|
useCustom ? Type::IndexedLink : Type::Link);
|
||||||
|
} else if (flags) {
|
||||||
|
if (!(_flags & flags)) {
|
||||||
|
createBlock();
|
||||||
|
_flags |= flags;
|
||||||
|
_startedEntities[entityEnd].emplace_back(flags);
|
||||||
|
_monoIndex = monoIndex;
|
||||||
|
}
|
||||||
|
} else if (entityType == EntityType::Spoiler) {
|
||||||
|
createBlock();
|
||||||
|
|
||||||
|
_spoilers.push_back(EntityLinkData{
|
||||||
|
.data = QString::number(_spoilers.size() + 1),
|
||||||
|
.type = entityType,
|
||||||
|
});
|
||||||
|
_spoilerIndex = _spoilers.size();
|
||||||
|
|
||||||
|
_startedEntities[entityEnd].emplace_back(
|
||||||
|
_spoilerIndex,
|
||||||
|
Type::Spoiler);
|
||||||
|
}
|
||||||
|
|
||||||
|
++_waitingEntity;
|
||||||
|
skipBadEntities();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Parser::processCustomIndex(uint16 index) {
|
||||||
|
auto &url = _links[index - 1].data;
|
||||||
|
if (url.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (url.startsWith("internal:index")) {
|
||||||
|
const auto customIndex = uint16(url.back().unicode());
|
||||||
|
// if (customIndex != index) {
|
||||||
|
url = QString();
|
||||||
|
_linksIndexes.push_back(customIndex);
|
||||||
|
return true;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parser::skipPassedEntities() {
|
||||||
|
while (_waitingEntity != _entitiesEnd
|
||||||
|
&& _start + _waitingEntity->offset() + _waitingEntity->length() <= _ptr) {
|
||||||
|
++_waitingEntity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parser::skipBadEntities() {
|
||||||
|
if (_links.size() >= 0x7FFF) {
|
||||||
|
while (_waitingEntity != _entitiesEnd
|
||||||
|
&& (isLinkEntity(*_waitingEntity)
|
||||||
|
|| isInvalidEntity(*_waitingEntity))) {
|
||||||
|
++_waitingEntity;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (_waitingEntity != _entitiesEnd && isInvalidEntity(*_waitingEntity)) {
|
||||||
|
++_waitingEntity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parser::parseCurrentChar() {
|
||||||
|
_ch = ((_ptr < _end) ? *_ptr : 0);
|
||||||
|
_emojiLookback = 0;
|
||||||
|
const auto inCustomEmoji = !_customEmojiData.isEmpty();
|
||||||
|
const auto isNewLine = !inCustomEmoji && _multiline && IsNewline(_ch);
|
||||||
|
const auto isSpace = IsSpace(_ch);
|
||||||
|
const auto isDiac = IsDiac(_ch);
|
||||||
|
const auto isTilde = !inCustomEmoji && _checkTilde && (_ch == '~');
|
||||||
|
const auto skip = [&] {
|
||||||
|
if (IsBad(_ch) || _ch.isLowSurrogate()) {
|
||||||
|
return true;
|
||||||
|
} else if (_ch == 0xFE0F && Platform::IsMac()) {
|
||||||
|
// Some sequences like 0x0E53 0xFE0F crash OS X harfbuzz text processing :(
|
||||||
|
return true;
|
||||||
|
} else if (isDiac) {
|
||||||
|
if (_lastSkipped || _emoji || ++_diacs > kMaxDiacAfterSymbol) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (_ch.isHighSurrogate()) {
|
||||||
|
if (_ptr + 1 >= _end || !(_ptr + 1)->isLowSurrogate()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const auto ucs4 = QChar::surrogateToUcs4(_ch, *(_ptr + 1));
|
||||||
|
if (ucs4 >= 0xE0000) {
|
||||||
|
// Unicode tags are skipped.
|
||||||
|
// Only place they work is in some flag emoji,
|
||||||
|
// but in that case they were already parsed as emoji before.
|
||||||
|
//
|
||||||
|
// For unknown reason in some unknown cases strings with such
|
||||||
|
// symbols lead to crashes on some Linux distributions, see
|
||||||
|
// https://github.com/telegramdesktop/tdesktop/issues/7005
|
||||||
|
//
|
||||||
|
// At least one crashing text was starting that way:
|
||||||
|
//
|
||||||
|
// 0xd83d 0xdcda 0xdb40 0xdc69 0xdb40 0xdc64 0xdb40 0xdc6a
|
||||||
|
// 0xdb40 0xdc77 0xdb40 0xdc7f 0x32 ... simple text here ...
|
||||||
|
//
|
||||||
|
// or in codepoints:
|
||||||
|
//
|
||||||
|
// 0x1f4da 0xe0069 0xe0064 0xe006a 0xe0077 0xe007f 0x32 ...
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (_ch.isHighSurrogate() && !skip) {
|
||||||
|
_t->_text.push_back(_ch);
|
||||||
|
++_ptr;
|
||||||
|
_ch = *_ptr;
|
||||||
|
_emojiLookback = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastSkipped = skip;
|
||||||
|
if (skip) {
|
||||||
|
_ch = 0;
|
||||||
|
} else {
|
||||||
|
if (isTilde) { // tilde fix in OpenSans
|
||||||
|
if (!(_flags & TextBlockFTilde)) {
|
||||||
|
createBlock(-_emojiLookback);
|
||||||
|
_flags |= TextBlockFTilde;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_flags & TextBlockFTilde) {
|
||||||
|
createBlock(-_emojiLookback);
|
||||||
|
_flags &= ~TextBlockFTilde;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isNewLine) {
|
||||||
|
createNewlineBlock();
|
||||||
|
} else if (isSpace) {
|
||||||
|
_t->_text.push_back(QChar::Space);
|
||||||
|
} else {
|
||||||
|
if (_emoji) {
|
||||||
|
createBlock(-_emojiLookback);
|
||||||
|
}
|
||||||
|
_t->_text.push_back(_ch);
|
||||||
|
}
|
||||||
|
if (!isDiac) _diacs = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parser::parseEmojiFromCurrent() {
|
||||||
|
if (!_customEmojiData.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int len = 0;
|
||||||
|
auto e = Emoji::Find(_ptr - _emojiLookback, _end, &len);
|
||||||
|
if (!e) return;
|
||||||
|
|
||||||
|
for (int l = len - _emojiLookback - 1; l > 0; --l) {
|
||||||
|
_t->_text.push_back(*++_ptr);
|
||||||
|
}
|
||||||
|
if (e->hasPostfix()) {
|
||||||
|
Assert(!_t->_text.isEmpty());
|
||||||
|
const auto last = _t->_text[_t->_text.size() - 1];
|
||||||
|
if (last.unicode() != Emoji::kPostfix) {
|
||||||
|
_t->_text.push_back(QChar(Emoji::kPostfix));
|
||||||
|
++len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createBlock(-len);
|
||||||
|
_emoji = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Parser::isInvalidEntity(const EntityInText &entity) const {
|
||||||
|
const auto length = entity.length();
|
||||||
|
return (_start + entity.offset() + length > _end) || (length <= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Parser::isLinkEntity(const EntityInText &entity) const {
|
||||||
|
const auto type = entity.type();
|
||||||
|
const auto urls = {
|
||||||
|
EntityType::Url,
|
||||||
|
EntityType::CustomUrl,
|
||||||
|
EntityType::Email,
|
||||||
|
EntityType::Hashtag,
|
||||||
|
EntityType::Cashtag,
|
||||||
|
EntityType::Mention,
|
||||||
|
EntityType::MentionName,
|
||||||
|
EntityType::BotCommand
|
||||||
|
};
|
||||||
|
return ranges::find(urls, type) != std::end(urls);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parser::parse(const TextParseOptions &options) {
|
||||||
|
skipBadEntities();
|
||||||
|
trimSourceRange();
|
||||||
|
|
||||||
|
_t->_text.resize(0);
|
||||||
|
_t->_text.reserve(_end - _ptr);
|
||||||
|
|
||||||
|
for (; _ptr <= _end; ++_ptr) {
|
||||||
|
while (checkEntities()) {
|
||||||
|
}
|
||||||
|
parseCurrentChar();
|
||||||
|
parseEmojiFromCurrent();
|
||||||
|
|
||||||
|
if (_sumFinished || _t->_text.size() >= 0x8000) {
|
||||||
|
break; // 32k max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createBlock();
|
||||||
|
finalize(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parser::trimSourceRange() {
|
||||||
|
const auto firstMonospaceOffset = EntityInText::FirstMonospaceOffset(
|
||||||
|
_source.entities,
|
||||||
|
_end - _start);
|
||||||
|
|
||||||
|
while (_ptr != _end && IsTrimmed(*_ptr) && _ptr != _start + firstMonospaceOffset) {
|
||||||
|
++_ptr;
|
||||||
|
}
|
||||||
|
while (_ptr != _end && IsTrimmed(*(_end - 1))) {
|
||||||
|
--_end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// void Parser::checkForElidedSkipBlock() {
|
||||||
|
// if (!_sumFinished || !_rich) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// // We could've skipped the final skip block command.
|
||||||
|
// for (; _ptr < _end; ++_ptr) {
|
||||||
|
// if (*_ptr == TextCommand && readSkipBlockCommand()) {
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
void Parser::finalize(const TextParseOptions &options) {
|
||||||
|
_t->_links.resize(_maxLnkIndex + _maxShiftedLnkIndex);
|
||||||
|
auto counterCustomIndex = uint16(0);
|
||||||
|
auto currentIndex = uint16(0); // Current the latest index of _t->_links.
|
||||||
|
struct {
|
||||||
|
uint16 mono = 0;
|
||||||
|
uint16 lnk = 0;
|
||||||
|
} lastHandlerIndex;
|
||||||
|
const auto avoidIntersectionsWithCustom = [&] {
|
||||||
|
while (ranges::contains(_linksIndexes, currentIndex)) {
|
||||||
|
currentIndex++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
auto isolatedEmojiCount = 0;
|
||||||
|
_t->_hasCustomEmoji = false;
|
||||||
|
_t->_isIsolatedEmoji = true;
|
||||||
|
_t->_isOnlyCustomEmoji = true;
|
||||||
|
for (auto &block : _t->_blocks) {
|
||||||
|
if (block->type() == TextBlockTCustomEmoji) {
|
||||||
|
_t->_hasCustomEmoji = true;
|
||||||
|
} else if (block->type() != TextBlockTNewline
|
||||||
|
&& block->type() != TextBlockTSkip) {
|
||||||
|
_t->_isOnlyCustomEmoji = false;
|
||||||
|
} else if (block->lnkIndex()) {
|
||||||
|
_t->_isOnlyCustomEmoji = _t->_isIsolatedEmoji = false;
|
||||||
|
}
|
||||||
|
if (_t->_isIsolatedEmoji) {
|
||||||
|
if (block->type() == TextBlockTCustomEmoji
|
||||||
|
|| block->type() == TextBlockTEmoji) {
|
||||||
|
if (++isolatedEmojiCount > kIsolatedEmojiLimit) {
|
||||||
|
_t->_isIsolatedEmoji = false;
|
||||||
|
}
|
||||||
|
} else if (block->type() != TextBlockTSkip) {
|
||||||
|
_t->_isIsolatedEmoji = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto spoilerIndex = block->spoilerIndex();
|
||||||
|
if (spoilerIndex) {
|
||||||
|
if (!_t->_spoiler) {
|
||||||
|
_t->_spoiler = std::make_unique<SpoilerData>(
|
||||||
|
Integration::Instance().createSpoilerRepaint(_context));
|
||||||
|
}
|
||||||
|
if (_t->_spoiler->links.size() < spoilerIndex) {
|
||||||
|
_t->_spoiler->links.resize(spoilerIndex);
|
||||||
|
const auto handler = (options.flags & TextParseLinks)
|
||||||
|
? std::make_shared<SpoilerClickHandler>()
|
||||||
|
: nullptr;
|
||||||
|
_t->setSpoiler(spoilerIndex, std::move(handler));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto shiftedIndex = block->lnkIndex();
|
||||||
|
auto useCustomIndex = false;
|
||||||
|
if (shiftedIndex <= kStringLinkIndexShift) {
|
||||||
|
if (IsMono(block->flags()) && shiftedIndex) {
|
||||||
|
const auto monoIndex = shiftedIndex;
|
||||||
|
|
||||||
|
if (lastHandlerIndex.mono == monoIndex) {
|
||||||
|
block->setLnkIndex(currentIndex);
|
||||||
|
continue; // Optimization.
|
||||||
|
} else {
|
||||||
|
currentIndex++;
|
||||||
|
}
|
||||||
|
avoidIntersectionsWithCustom();
|
||||||
|
block->setLnkIndex(currentIndex);
|
||||||
|
const auto handler = Integration::Instance().createLinkHandler(
|
||||||
|
_monos[monoIndex - 1],
|
||||||
|
_context);
|
||||||
|
_t->_links.resize(currentIndex);
|
||||||
|
if (handler) {
|
||||||
|
_t->setLink(currentIndex, handler);
|
||||||
|
}
|
||||||
|
lastHandlerIndex.mono = monoIndex;
|
||||||
|
continue;
|
||||||
|
} else if (shiftedIndex) {
|
||||||
|
useCustomIndex = true;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto usedIndex = [&] {
|
||||||
|
return useCustomIndex
|
||||||
|
? _linksIndexes[counterCustomIndex - 1]
|
||||||
|
: currentIndex;
|
||||||
|
};
|
||||||
|
const auto realIndex = useCustomIndex
|
||||||
|
? shiftedIndex
|
||||||
|
: (shiftedIndex - kStringLinkIndexShift);
|
||||||
|
if (lastHandlerIndex.lnk == realIndex) {
|
||||||
|
block->setLnkIndex(usedIndex());
|
||||||
|
continue; // Optimization.
|
||||||
|
} else {
|
||||||
|
(useCustomIndex ? counterCustomIndex : currentIndex)++;
|
||||||
|
}
|
||||||
|
if (!useCustomIndex) {
|
||||||
|
avoidIntersectionsWithCustom();
|
||||||
|
}
|
||||||
|
block->setLnkIndex(usedIndex());
|
||||||
|
|
||||||
|
_t->_links.resize(std::max(usedIndex(), uint16(_t->_links.size())));
|
||||||
|
const auto handler = Integration::Instance().createLinkHandler(
|
||||||
|
_links[realIndex - 1],
|
||||||
|
_context);
|
||||||
|
if (handler) {
|
||||||
|
_t->setLink(usedIndex(), handler);
|
||||||
|
}
|
||||||
|
lastHandlerIndex.lnk = realIndex;
|
||||||
|
}
|
||||||
|
if (!_t->_hasCustomEmoji || _t->_spoiler) {
|
||||||
|
_t->_isOnlyCustomEmoji = false;
|
||||||
|
}
|
||||||
|
if (_t->_blocks.empty() || _t->_spoiler) {
|
||||||
|
_t->_isIsolatedEmoji = false;
|
||||||
|
}
|
||||||
|
_t->_links.squeeze();
|
||||||
|
if (_t->_spoiler) {
|
||||||
|
_t->_spoiler->links.squeeze();
|
||||||
|
}
|
||||||
|
_t->_blocks.shrink_to_fit();
|
||||||
|
_t->_text.squeeze();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parser::computeLinkText(
|
||||||
|
const QString &linkData,
|
||||||
|
QString *outLinkText,
|
||||||
|
EntityLinkShown *outShown) {
|
||||||
|
auto url = QUrl(linkData);
|
||||||
|
auto good = QUrl(url.isValid()
|
||||||
|
? url.toEncoded()
|
||||||
|
: QByteArray());
|
||||||
|
auto readable = good.isValid()
|
||||||
|
? good.toDisplayString()
|
||||||
|
: linkData;
|
||||||
|
*outLinkText = _t->_st->font->elided(readable, st::linkCropLimit);
|
||||||
|
*outShown = (*outLinkText == readable)
|
||||||
|
? EntityLinkShown::Full
|
||||||
|
: EntityLinkShown::Partial;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Ui::Text
|
||||||
128
ui/text/text_parser.h
Normal file
128
ui/text/text_parser.h
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
// This file is part of Desktop App Toolkit,
|
||||||
|
// a set of 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/text/text.h"
|
||||||
|
|
||||||
|
namespace Ui::Text {
|
||||||
|
|
||||||
|
class Parser {
|
||||||
|
public:
|
||||||
|
Parser(
|
||||||
|
not_null<String*> string,
|
||||||
|
const TextWithEntities &textWithEntities,
|
||||||
|
const TextParseOptions &options,
|
||||||
|
const std::any &context);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct ReadyToken {
|
||||||
|
};
|
||||||
|
|
||||||
|
class StartedEntity {
|
||||||
|
public:
|
||||||
|
enum class Type {
|
||||||
|
Flags,
|
||||||
|
Link,
|
||||||
|
IndexedLink,
|
||||||
|
Spoiler,
|
||||||
|
CustomEmoji,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit StartedEntity(TextBlockFlags flags);
|
||||||
|
explicit StartedEntity(uint16 index, Type type);
|
||||||
|
|
||||||
|
[[nodiscard]] Type type() const;
|
||||||
|
[[nodiscard]] std::optional<TextBlockFlags> flags() const;
|
||||||
|
[[nodiscard]] std::optional<uint16> lnkIndex() const;
|
||||||
|
[[nodiscard]] std::optional<uint16> spoilerIndex() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const int _value = 0;
|
||||||
|
const Type _type;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
Parser(
|
||||||
|
not_null<String*> string,
|
||||||
|
TextWithEntities &&source,
|
||||||
|
const TextParseOptions &options,
|
||||||
|
const std::any &context,
|
||||||
|
ReadyToken);
|
||||||
|
|
||||||
|
void trimSourceRange();
|
||||||
|
void blockCreated();
|
||||||
|
void createBlock(int32 skipBack = 0);
|
||||||
|
// void createSkipBlock(int32 w, int32 h);
|
||||||
|
void createNewlineBlock();
|
||||||
|
|
||||||
|
// Returns true if at least one entity was parsed in the current position.
|
||||||
|
bool checkEntities();
|
||||||
|
void parseCurrentChar();
|
||||||
|
void parseEmojiFromCurrent();
|
||||||
|
void finalize(const TextParseOptions &options);
|
||||||
|
|
||||||
|
void finishEntities();
|
||||||
|
void skipPassedEntities();
|
||||||
|
void skipBadEntities();
|
||||||
|
|
||||||
|
bool isInvalidEntity(const EntityInText &entity) const;
|
||||||
|
bool isLinkEntity(const EntityInText &entity) const;
|
||||||
|
|
||||||
|
bool processCustomIndex(uint16 index);
|
||||||
|
|
||||||
|
void parse(const TextParseOptions &options);
|
||||||
|
void computeLinkText(
|
||||||
|
const QString &linkData,
|
||||||
|
QString *outLinkText,
|
||||||
|
EntityLinkShown *outShown);
|
||||||
|
|
||||||
|
const not_null<String*> _t;
|
||||||
|
const TextWithEntities _source;
|
||||||
|
const std::any &_context;
|
||||||
|
const QChar * const _start = nullptr;
|
||||||
|
const QChar *_end = nullptr; // mutable, because we trim by decrementing.
|
||||||
|
const QChar *_ptr = nullptr;
|
||||||
|
const EntitiesInText::const_iterator _entitiesEnd;
|
||||||
|
EntitiesInText::const_iterator _waitingEntity;
|
||||||
|
QString _customEmojiData;
|
||||||
|
const bool _multiline = false;
|
||||||
|
|
||||||
|
const QFixed _stopAfterWidth; // summary width of all added words
|
||||||
|
const bool _checkTilde = false; // do we need a special text block for tilde symbol
|
||||||
|
|
||||||
|
std::vector<uint16> _linksIndexes;
|
||||||
|
|
||||||
|
std::vector<EntityLinkData> _links;
|
||||||
|
std::vector<EntityLinkData> _spoilers;
|
||||||
|
std::vector<EntityLinkData> _monos;
|
||||||
|
base::flat_map<
|
||||||
|
const QChar*,
|
||||||
|
std::vector<StartedEntity>> _startedEntities;
|
||||||
|
|
||||||
|
uint16 _maxLnkIndex = 0;
|
||||||
|
uint16 _maxShiftedLnkIndex = 0;
|
||||||
|
|
||||||
|
// current state
|
||||||
|
int32 _flags = 0;
|
||||||
|
uint16 _lnkIndex = 0;
|
||||||
|
uint16 _spoilerIndex = 0;
|
||||||
|
uint16 _monoIndex = 0;
|
||||||
|
EmojiPtr _emoji = nullptr; // current emoji, if current word is an emoji, or zero
|
||||||
|
int32 _blockStart = 0; // offset in result, from which current parsed block is started
|
||||||
|
int32 _diacs = 0; // diac chars skipped without good char
|
||||||
|
QFixed _sumWidth;
|
||||||
|
bool _sumFinished = false;
|
||||||
|
bool _newlineAwaited = false;
|
||||||
|
|
||||||
|
// current char data
|
||||||
|
QChar _ch; // current char (low surrogate, if current char is surrogate pair)
|
||||||
|
int32 _emojiLookback = 0; // how far behind the current ptr to look for current emoji
|
||||||
|
bool _lastSkipped = false; // did we skip current char
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Ui::Text
|
||||||
1922
ui/text/text_renderer.cpp
Normal file
1922
ui/text/text_renderer.cpp
Normal file
File diff suppressed because it is too large
Load diff
158
ui/text/text_renderer.h
Normal file
158
ui/text/text_renderer.h
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
// This file is part of Desktop App Toolkit,
|
||||||
|
// a set of 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/text/text.h"
|
||||||
|
|
||||||
|
#include <private/qtextengine_p.h>
|
||||||
|
|
||||||
|
struct QScriptAnalysis;
|
||||||
|
struct QScriptLine;
|
||||||
|
|
||||||
|
namespace Ui::Text {
|
||||||
|
|
||||||
|
class Renderer final {
|
||||||
|
public:
|
||||||
|
explicit Renderer(const Ui::Text::String &t);
|
||||||
|
~Renderer();
|
||||||
|
|
||||||
|
void draw(QPainter &p, const PaintContext &context);
|
||||||
|
[[nodiscard]] StateResult getState(
|
||||||
|
QPoint point,
|
||||||
|
int w,
|
||||||
|
StateRequest request);
|
||||||
|
[[nodiscard]] StateResult getStateElided(
|
||||||
|
QPoint point,
|
||||||
|
int w,
|
||||||
|
StateRequestElided request);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct BidiControl;
|
||||||
|
|
||||||
|
void enumerate();
|
||||||
|
|
||||||
|
[[nodiscard]] crl::time now() const;
|
||||||
|
void initNextParagraph(String::TextBlocks::const_iterator i);
|
||||||
|
void initParagraphBidi();
|
||||||
|
bool drawLine(
|
||||||
|
uint16 _lineEnd,
|
||||||
|
const String::TextBlocks::const_iterator &_endBlockIter,
|
||||||
|
const String::TextBlocks::const_iterator &_end);
|
||||||
|
void fillSelectRange(QFixed from, QFixed to);
|
||||||
|
[[nodiscard]] float64 fillSpoilerOpacity();
|
||||||
|
void fillSpoilerRange(
|
||||||
|
QFixed x,
|
||||||
|
QFixed width,
|
||||||
|
int currentBlockIndex,
|
||||||
|
int positionFrom,
|
||||||
|
int positionTill);
|
||||||
|
void elideSaveBlock(
|
||||||
|
int32 blockIndex,
|
||||||
|
const AbstractBlock *&_endBlock,
|
||||||
|
int32 elideStart,
|
||||||
|
int32 elideWidth);
|
||||||
|
void setElideBidi(int32 elideStart, int32 elideLen);
|
||||||
|
void prepareElidedLine(
|
||||||
|
QString &lineText,
|
||||||
|
int32 lineStart,
|
||||||
|
int32 &lineLength,
|
||||||
|
const AbstractBlock *&_endBlock,
|
||||||
|
int repeat = 0);
|
||||||
|
void restoreAfterElided();
|
||||||
|
|
||||||
|
// COPIED FROM qtextengine.cpp AND MODIFIED
|
||||||
|
static void eAppendItems(
|
||||||
|
QScriptAnalysis *analysis,
|
||||||
|
int &start,
|
||||||
|
int &stop,
|
||||||
|
const BidiControl &control,
|
||||||
|
QChar::Direction dir);
|
||||||
|
void eShapeLine(const QScriptLine &line);
|
||||||
|
[[nodiscard]] style::font applyFlags(int32 flags, const style::font &f);
|
||||||
|
void eSetFont(const AbstractBlock *block);
|
||||||
|
void eItemize();
|
||||||
|
QChar::Direction eSkipBoundryNeutrals(
|
||||||
|
QScriptAnalysis *analysis,
|
||||||
|
const ushort *unicode,
|
||||||
|
int &sor, int &eor, BidiControl &control,
|
||||||
|
String::TextBlocks::const_iterator i);
|
||||||
|
|
||||||
|
// creates the next QScript items.
|
||||||
|
bool eBidiItemize(QScriptAnalysis *analysis, BidiControl &control);
|
||||||
|
|
||||||
|
void applyBlockProperties(const AbstractBlock *block);
|
||||||
|
|
||||||
|
const String *_t = nullptr;
|
||||||
|
SpoilerMessCache *_spoilerCache = nullptr;
|
||||||
|
QPainter *_p = nullptr;
|
||||||
|
const style::TextPalette *_palette = nullptr;
|
||||||
|
bool _elideLast = false;
|
||||||
|
bool _breakEverywhere = false;
|
||||||
|
int _elideRemoveFromEnd = 0;
|
||||||
|
bool _paused = false;
|
||||||
|
style::align _align = style::al_topleft;
|
||||||
|
QPen _originalPen;
|
||||||
|
QPen _originalPenSelected;
|
||||||
|
const QPen *_currentPen = nullptr;
|
||||||
|
const QPen *_currentPenSelected = nullptr;
|
||||||
|
struct {
|
||||||
|
const style::color *color = nullptr;
|
||||||
|
bool inFront = false;
|
||||||
|
crl::time startMs = 0;
|
||||||
|
uint16 spoilerIndex = 0;
|
||||||
|
|
||||||
|
bool selectActiveBlock = false; // For monospace.
|
||||||
|
} _background;
|
||||||
|
int _yFrom = 0;
|
||||||
|
int _yTo = 0;
|
||||||
|
int _yToElide = 0;
|
||||||
|
TextSelection _selection = { 0, 0 };
|
||||||
|
bool _fullWidthSelection = true;
|
||||||
|
const QChar *_str = nullptr;
|
||||||
|
mutable crl::time _cachedNow = 0;
|
||||||
|
|
||||||
|
int _customEmojiSize = 0;
|
||||||
|
int _customEmojiSkip = 0;
|
||||||
|
int _indexOfElidedBlock = -1; // For spoilers.
|
||||||
|
|
||||||
|
// current paragraph data
|
||||||
|
String::TextBlocks::const_iterator _parStartBlock;
|
||||||
|
Qt::LayoutDirection _parDirection;
|
||||||
|
int _parStart = 0;
|
||||||
|
int _parLength = 0;
|
||||||
|
bool _parHasBidi = false;
|
||||||
|
QVarLengthArray<QScriptAnalysis, 4096> _parAnalysis;
|
||||||
|
|
||||||
|
// current line data
|
||||||
|
QTextEngine *_e = nullptr;
|
||||||
|
style::font _f;
|
||||||
|
QFixed _x, _w, _wLeft, _last_rPadding;
|
||||||
|
int _y = 0;
|
||||||
|
int _yDelta = 0;
|
||||||
|
int _lineHeight = 0;
|
||||||
|
int _fontHeight = 0;
|
||||||
|
|
||||||
|
// elided hack support
|
||||||
|
int _blocksSize = 0;
|
||||||
|
int _elideSavedIndex = 0;
|
||||||
|
std::optional<Block> _elideSavedBlock;
|
||||||
|
|
||||||
|
int _lineStart = 0;
|
||||||
|
int _localFrom = 0;
|
||||||
|
int _lineStartBlock = 0;
|
||||||
|
|
||||||
|
// link and symbol resolve
|
||||||
|
QFixed _lookupX = 0;
|
||||||
|
int _lookupY = 0;
|
||||||
|
bool _lookupSymbol = false;
|
||||||
|
bool _lookupLink = false;
|
||||||
|
StateRequest _lookupRequest;
|
||||||
|
StateResult _lookupResult;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Ui::Text
|
||||||
|
|
@ -6,16 +6,18 @@
|
||||||
//
|
//
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "ui/effects/spoiler_mess.h"
|
||||||
|
|
||||||
class SpoilerClickHandler;
|
class SpoilerClickHandler;
|
||||||
|
|
||||||
namespace Ui::Text {
|
namespace Ui::Text {
|
||||||
|
|
||||||
struct SpoilerData {
|
struct SpoilerData {
|
||||||
struct {
|
explicit SpoilerData(Fn<void()> repaint)
|
||||||
std::array<QImage, 4> corners;
|
: animation(std::move(repaint)) {
|
||||||
QColor color;
|
}
|
||||||
} spoilerCache, spoilerShownCache;
|
|
||||||
|
|
||||||
|
SpoilerAnimation animation;
|
||||||
QVector<std::shared_ptr<SpoilerClickHandler>> links;
|
QVector<std::shared_ptr<SpoilerClickHandler>> links;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include "ui/toast/toast_widget.h"
|
#include "ui/toast/toast_widget.h"
|
||||||
|
|
||||||
#include "ui/image/image_prepare.h"
|
#include "ui/image/image_prepare.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
#include "styles/palette.h"
|
#include "styles/palette.h"
|
||||||
#include "styles/style_widgets.h"
|
#include "styles/style_widgets.h"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -597,7 +597,7 @@ void CrossButton::animationCallback() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossButton::paintEvent(QPaintEvent *e) {
|
void CrossButton::paintEvent(QPaintEvent *e) {
|
||||||
Painter p(this);
|
auto p = QPainter(this);
|
||||||
|
|
||||||
auto over = isOver();
|
auto over = isOver();
|
||||||
auto shown = _showAnimation.value(_shown ? 1. : 0.);
|
auto shown = _showAnimation.value(_shown ? 1. : 0.);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#include "ui/effects/ripple_animation.h"
|
#include "ui/effects/ripple_animation.h"
|
||||||
#include "ui/basic_click_handlers.h"
|
#include "ui/basic_click_handlers.h"
|
||||||
#include "ui/ui_utility.h"
|
#include "ui/ui_utility.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
|
|
||||||
#include <QtGui/QtEvents>
|
#include <QtGui/QtEvents>
|
||||||
|
|
||||||
|
|
@ -96,7 +97,7 @@ void ToggleView::setStyle(const style::Toggle &st) {
|
||||||
_st = &st;
|
_st = &st;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ToggleView::paint(Painter &p, int left, int top, int outerWidth) {
|
void ToggleView::paint(QPainter &p, int left, int top, int outerWidth) {
|
||||||
left += _st->border;
|
left += _st->border;
|
||||||
top += _st->border;
|
top += _st->border;
|
||||||
|
|
||||||
|
|
@ -132,7 +133,7 @@ void ToggleView::paint(Painter &p, int left, int top, int outerWidth) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ToggleView::paintXV(Painter &p, int left, int top, int outerWidth, float64 toggled, const QBrush &brush) {
|
void ToggleView::paintXV(QPainter &p, int left, int top, int outerWidth, float64 toggled, const QBrush &brush) {
|
||||||
Expects(_st->vsize > 0);
|
Expects(_st->vsize > 0);
|
||||||
Expects(_st->stroke > 0);
|
Expects(_st->stroke > 0);
|
||||||
|
|
||||||
|
|
@ -247,7 +248,7 @@ void CheckView::setStyle(const style::Check &st) {
|
||||||
_st = &st;
|
_st = &st;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CheckView::paint(Painter &p, int left, int top, int outerWidth) {
|
void CheckView::paint(QPainter &p, int left, int top, int outerWidth) {
|
||||||
auto toggled = currentAnimationValue();
|
auto toggled = currentAnimationValue();
|
||||||
auto pen = _untoggledOverride
|
auto pen = _untoggledOverride
|
||||||
? anim::pen(*_untoggledOverride, _st->toggledFg, toggled)
|
? anim::pen(*_untoggledOverride, _st->toggledFg, toggled)
|
||||||
|
|
@ -305,7 +306,7 @@ void RadioView::setStyle(const style::Radio &st) {
|
||||||
_st = &st;
|
_st = &st;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RadioView::paint(Painter &p, int left, int top, int outerWidth) {
|
void RadioView::paint(QPainter &p, int left, int top, int outerWidth) {
|
||||||
PainterHighQualityEnabler hq(p);
|
PainterHighQualityEnabler hq(p);
|
||||||
|
|
||||||
auto toggled = currentAnimationValue();
|
auto toggled = currentAnimationValue();
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include "ui/text/text.h"
|
#include "ui/text/text.h"
|
||||||
#include "styles/style_widgets.h"
|
#include "styles/style_widgets.h"
|
||||||
|
|
||||||
|
class QPainter;
|
||||||
class Painter;
|
class Painter;
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
|
|
@ -38,7 +39,7 @@ public:
|
||||||
|
|
||||||
virtual QSize getSize() const = 0;
|
virtual QSize getSize() const = 0;
|
||||||
|
|
||||||
virtual void paint(Painter &p, int left, int top, int outerWidth) = 0;
|
virtual void paint(QPainter &p, int left, int top, int outerWidth) = 0;
|
||||||
virtual QImage prepareRippleMask() const = 0;
|
virtual QImage prepareRippleMask() const = 0;
|
||||||
virtual bool checkRippleStartPosition(QPoint position) const = 0;
|
virtual bool checkRippleStartPosition(QPoint position) const = 0;
|
||||||
|
|
||||||
|
|
@ -67,7 +68,7 @@ public:
|
||||||
void setStyle(const style::Check &st);
|
void setStyle(const style::Check &st);
|
||||||
|
|
||||||
QSize getSize() const override;
|
QSize getSize() const override;
|
||||||
void paint(Painter &p, int left, int top, int outerWidth) override;
|
void paint(QPainter &p, int left, int top, int outerWidth) override;
|
||||||
QImage prepareRippleMask() const override;
|
QImage prepareRippleMask() const override;
|
||||||
bool checkRippleStartPosition(QPoint position) const override;
|
bool checkRippleStartPosition(QPoint position) const override;
|
||||||
|
|
||||||
|
|
@ -95,7 +96,7 @@ public:
|
||||||
void setUntoggledOverride(std::optional<QColor> untoggledOverride);
|
void setUntoggledOverride(std::optional<QColor> untoggledOverride);
|
||||||
|
|
||||||
QSize getSize() const override;
|
QSize getSize() const override;
|
||||||
void paint(Painter &p, int left, int top, int outerWidth) override;
|
void paint(QPainter &p, int left, int top, int outerWidth) override;
|
||||||
QImage prepareRippleMask() const override;
|
QImage prepareRippleMask() const override;
|
||||||
bool checkRippleStartPosition(QPoint position) const override;
|
bool checkRippleStartPosition(QPoint position) const override;
|
||||||
|
|
||||||
|
|
@ -118,13 +119,13 @@ public:
|
||||||
void setStyle(const style::Toggle &st);
|
void setStyle(const style::Toggle &st);
|
||||||
|
|
||||||
QSize getSize() const override;
|
QSize getSize() const override;
|
||||||
void paint(Painter &p, int left, int top, int outerWidth) override;
|
void paint(QPainter &p, int left, int top, int outerWidth) override;
|
||||||
QImage prepareRippleMask() const override;
|
QImage prepareRippleMask() const override;
|
||||||
bool checkRippleStartPosition(QPoint position) const override;
|
bool checkRippleStartPosition(QPoint position) const override;
|
||||||
void setLocked(bool locked);
|
void setLocked(bool locked);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void paintXV(Painter &p, int left, int top, int outerWidth, float64 toggled, const QBrush &brush);
|
void paintXV(QPainter &p, int left, int top, int outerWidth, float64 toggled, const QBrush &brush);
|
||||||
QSize rippleSize() const;
|
QSize rippleSize() const;
|
||||||
|
|
||||||
not_null<const style::Toggle*> _st;
|
not_null<const style::Toggle*> _st;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ void IconButtonWithText::paintEvent(QPaintEvent *e) {
|
||||||
|
|
||||||
const auto r = rect() - _st.textPadding;
|
const auto r = rect() - _st.textPadding;
|
||||||
|
|
||||||
Painter p(this);
|
auto p = QPainter(this);
|
||||||
p.setFont(_st.font);
|
p.setFont(_st.font);
|
||||||
p.setPen(_st.textFg);
|
p.setPen(_st.textFg);
|
||||||
p.drawText(r, _text, _st.textAlign);
|
p.drawText(r, _text, _st.textAlign);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include "ui/text/text.h"
|
#include "ui/text/text.h"
|
||||||
#include "ui/emoji_config.h"
|
#include "ui/emoji_config.h"
|
||||||
#include "ui/ui_utility.h"
|
#include "ui/ui_utility.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
#include "base/invoke_queued.h"
|
#include "base/invoke_queued.h"
|
||||||
#include "base/random.h"
|
#include "base/random.h"
|
||||||
#include "base/platform/base_platform_info.h"
|
#include "base/platform/base_platform_info.h"
|
||||||
|
|
@ -1199,7 +1200,7 @@ void FlatInput::finishAnimations() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void FlatInput::paintEvent(QPaintEvent *e) {
|
void FlatInput::paintEvent(QPaintEvent *e) {
|
||||||
Painter p(this);
|
auto p = QPainter(this);
|
||||||
|
|
||||||
auto placeholderFocused = _placeholderFocusedAnimation.value(_focused ? 1. : 0.);
|
auto placeholderFocused = _placeholderFocusedAnimation.value(_focused ? 1. : 0.);
|
||||||
auto pen = anim::pen(_st.borderColor, _st.borderActive, placeholderFocused);
|
auto pen = anim::pen(_st.borderColor, _st.borderActive, placeholderFocused);
|
||||||
|
|
@ -1886,7 +1887,7 @@ void InputField::handleTouchEvent(QTouchEvent *e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void InputField::paintEvent(QPaintEvent *e) {
|
void InputField::paintEvent(QPaintEvent *e) {
|
||||||
Painter p(this);
|
auto p = QPainter(this);
|
||||||
|
|
||||||
auto r = rect().intersected(e->rect());
|
auto r = rect().intersected(e->rect());
|
||||||
if (_st.textBg->c.alphaF() > 0.) {
|
if (_st.textBg->c.alphaF() > 0.) {
|
||||||
|
|
@ -4205,7 +4206,7 @@ void MaskedInputField::touchEvent(QTouchEvent *e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void MaskedInputField::paintEvent(QPaintEvent *e) {
|
void MaskedInputField::paintEvent(QPaintEvent *e) {
|
||||||
Painter p(this);
|
auto p = QPainter(this);
|
||||||
|
|
||||||
auto r = rect().intersected(e->rect());
|
auto r = rect().intersected(e->rect());
|
||||||
p.fillRect(r, _st.textBg);
|
p.fillRect(r, _st.textBg);
|
||||||
|
|
@ -4421,7 +4422,7 @@ QRect MaskedInputField::placeholderRect() const {
|
||||||
return rect().marginsRemoved(_textMargins + _st.placeholderMargins);
|
return rect().marginsRemoved(_textMargins + _st.placeholderMargins);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MaskedInputField::placeholderAdditionalPrepare(Painter &p) {
|
void MaskedInputField::placeholderAdditionalPrepare(QPainter &p) {
|
||||||
p.setFont(_st.font);
|
p.setFont(_st.font);
|
||||||
p.setPen(_st.placeholderFg);
|
p.setPen(_st.placeholderFg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -698,14 +698,14 @@ protected:
|
||||||
}
|
}
|
||||||
void setCorrectedText(QString &now, int &nowCursor, const QString &newText, int newPos);
|
void setCorrectedText(QString &now, int &nowCursor, const QString &newText, int newPos);
|
||||||
|
|
||||||
virtual void paintAdditionalPlaceholder(Painter &p) {
|
virtual void paintAdditionalPlaceholder(QPainter &p) {
|
||||||
}
|
}
|
||||||
|
|
||||||
style::font phFont() {
|
style::font phFont() {
|
||||||
return _st.font;
|
return _st.font;
|
||||||
}
|
}
|
||||||
|
|
||||||
void placeholderAdditionalPrepare(Painter &p);
|
void placeholderAdditionalPrepare(QPainter &p);
|
||||||
QRect placeholderRect() const;
|
QRect placeholderRect() const;
|
||||||
|
|
||||||
void setTextMargins(const QMargins &mrg);
|
void setTextMargins(const QMargins &mrg);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include "ui/widgets/box_content_divider.h"
|
#include "ui/widgets/box_content_divider.h"
|
||||||
#include "ui/basic_click_handlers.h" // UrlClickHandler
|
#include "ui/basic_click_handlers.h" // UrlClickHandler
|
||||||
#include "ui/inactive_press.h"
|
#include "ui/inactive_press.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
#include "base/qt/qt_common_adapters.h"
|
#include "base/qt/qt_common_adapters.h"
|
||||||
#include "styles/style_layers.h"
|
#include "styles/style_layers.h"
|
||||||
#include "styles/palette.h"
|
#include "styles/palette.h"
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,7 @@ void Menu::init() {
|
||||||
|
|
||||||
paintRequest(
|
paintRequest(
|
||||||
) | rpl::start_with_next([=](const QRect &clip) {
|
) | rpl::start_with_next([=](const QRect &clip) {
|
||||||
Painter p(this);
|
QPainter(this).fillRect(clip, _st.itemBg);
|
||||||
p.fillRect(clip, _st.itemBg);
|
|
||||||
}, lifetime());
|
}, lifetime());
|
||||||
|
|
||||||
positionValue(
|
positionValue(
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ void Action::paintEvent(QPaintEvent *e) {
|
||||||
paint(p);
|
paint(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Action::paintBackground(Painter &p, bool selected) {
|
void Action::paintBackground(QPainter &p, bool selected) {
|
||||||
if (selected && _st.itemBgOver->c.alpha() < 255) {
|
if (selected && _st.itemBgOver->c.alpha() < 255) {
|
||||||
p.fillRect(0, 0, width(), _height, _st.itemBg);
|
p.fillRect(0, 0, width(), _height, _st.itemBg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ protected:
|
||||||
|
|
||||||
int contentHeight() const override;
|
int contentHeight() const override;
|
||||||
|
|
||||||
void paintBackground(Painter &p, bool selected);
|
void paintBackground(QPainter &p, bool selected);
|
||||||
void paintText(Painter &p);
|
void paintText(Painter &p);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ Toggle::~Toggle() = default;
|
||||||
void Toggle::paintEvent(QPaintEvent *e) {
|
void Toggle::paintEvent(QPaintEvent *e) {
|
||||||
Action::paintEvent(e);
|
Action::paintEvent(e);
|
||||||
if (_toggle) {
|
if (_toggle) {
|
||||||
Painter p(this);
|
auto p = QPainter(this);
|
||||||
const auto toggleSize = _toggle->getSize();
|
const auto toggleSize = _toggle->getSize();
|
||||||
_toggle->paint(
|
_toggle->paint(
|
||||||
p,
|
p,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "ui/platform/ui_platform_utility.h"
|
#include "ui/platform/ui_platform_utility.h"
|
||||||
#include "ui/layers/layer_widget.h"
|
#include "ui/layers/layer_widget.h"
|
||||||
#include "ui/layers/show.h"
|
#include "ui/layers/show.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
#include "base/debug_log.h"
|
#include "base/debug_log.h"
|
||||||
#include "styles/style_widgets.h"
|
#include "styles/style_widgets.h"
|
||||||
#include "styles/style_layers.h"
|
#include "styles/style_layers.h"
|
||||||
|
|
@ -311,7 +312,7 @@ void SeparatePanel::createBorderImage() {
|
||||||
cache.setDevicePixelRatio(style::DevicePixelRatio());
|
cache.setDevicePixelRatio(style::DevicePixelRatio());
|
||||||
cache.fill(Qt::transparent);
|
cache.fill(Qt::transparent);
|
||||||
{
|
{
|
||||||
Painter p(&cache);
|
auto p = QPainter(&cache);
|
||||||
auto inner = QRect(0, 0, cacheSize, cacheSize).marginsRemoved(
|
auto inner = QRect(0, 0, cacheSize, cacheSize).marginsRemoved(
|
||||||
shadowPadding);
|
shadowPadding);
|
||||||
Ui::Shadow::paint(p, inner, cacheSize, st::callShadow);
|
Ui::Shadow::paint(p, inner, cacheSize, st::callShadow);
|
||||||
|
|
@ -572,7 +573,7 @@ void SeparatePanel::updateControlsGeometry() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void SeparatePanel::paintEvent(QPaintEvent *e) {
|
void SeparatePanel::paintEvent(QPaintEvent *e) {
|
||||||
Painter p(this);
|
auto p = QPainter(this);
|
||||||
if (!_animationCache.isNull()) {
|
if (!_animationCache.isNull()) {
|
||||||
auto opacity = _opacityAnimation.value(_visible ? 1. : 0.);
|
auto opacity = _opacityAnimation.value(_visible ? 1. : 0.);
|
||||||
if (!_opacityAnimation.animating()) {
|
if (!_opacityAnimation.animating()) {
|
||||||
|
|
@ -605,7 +606,7 @@ void SeparatePanel::paintEvent(QPaintEvent *e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SeparatePanel::paintShadowBorder(Painter &p) const {
|
void SeparatePanel::paintShadowBorder(QPainter &p) const {
|
||||||
const auto factor = style::DevicePixelRatio();
|
const auto factor = style::DevicePixelRatio();
|
||||||
const auto size = st::separatePanelBorderCacheSize;
|
const auto size = st::separatePanelBorderCacheSize;
|
||||||
const auto part1 = size / 3;
|
const auto part1 = size / 3;
|
||||||
|
|
@ -685,7 +686,7 @@ void SeparatePanel::paintShadowBorder(Painter &p) const {
|
||||||
st::windowBg);
|
st::windowBg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SeparatePanel::paintOpaqueBorder(Painter &p) const {
|
void SeparatePanel::paintOpaqueBorder(QPainter &p) const {
|
||||||
const auto border = st::windowShadowFgFallback;
|
const auto border = st::windowShadowFgFallback;
|
||||||
p.fillRect(0, 0, width(), _padding.top(), border);
|
p.fillRect(0, 0, width(), _padding.top(), border);
|
||||||
p.fillRect(
|
p.fillRect(
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,8 @@ private:
|
||||||
|
|
||||||
void updateTitleGeometry(int newWidth);
|
void updateTitleGeometry(int newWidth);
|
||||||
void updateTitlePosition();
|
void updateTitlePosition();
|
||||||
void paintShadowBorder(Painter &p) const;
|
void paintShadowBorder(QPainter &p) const;
|
||||||
void paintOpaqueBorder(Painter &p) const;
|
void paintOpaqueBorder(QPainter &p) const;
|
||||||
|
|
||||||
void toggleOpacityAnimation(bool visible);
|
void toggleOpacityAnimation(bool visible);
|
||||||
void finishAnimating();
|
void finishAnimating();
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include "ui/widgets/side_bar_button.h"
|
#include "ui/widgets/side_bar_button.h"
|
||||||
|
|
||||||
#include "ui/effects/ripple_animation.h"
|
#include "ui/effects/ripple_animation.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
#include "styles/style_widgets.h"
|
#include "styles/style_widgets.h"
|
||||||
|
|
||||||
#include <QtGui/QtEvents>
|
#include <QtGui/QtEvents>
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ rpl::producer<> TimeInput::focuses() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
void TimeInput::paintEvent(QPaintEvent *e) {
|
void TimeInput::paintEvent(QPaintEvent *e) {
|
||||||
Painter p(this);
|
auto p = QPainter(this);
|
||||||
|
|
||||||
const auto &_st = _stDateField;
|
const auto &_st = _stDateField;
|
||||||
const auto height = _st.heightMin;
|
const auto height = _st.heightMin;
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,11 @@
|
||||||
#include "ui/widgets/tooltip.h"
|
#include "ui/widgets/tooltip.h"
|
||||||
|
|
||||||
#include "ui/ui_utility.h"
|
#include "ui/ui_utility.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
#include "ui/platform/ui_platform_utility.h"
|
#include "ui/platform/ui_platform_utility.h"
|
||||||
#include "base/invoke_queued.h"
|
#include "base/invoke_queued.h"
|
||||||
#include "styles/style_widgets.h"
|
|
||||||
#include "base/platform/base_platform_info.h"
|
#include "base/platform/base_platform_info.h"
|
||||||
|
#include "styles/style_widgets.h"
|
||||||
|
|
||||||
#include <QtGui/QScreen>
|
#include <QtGui/QScreen>
|
||||||
#include <QtGui/QWindow>
|
#include <QtGui/QWindow>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue