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.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_utilities.cpp
|
||||
ui/text/text_utilities.h
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ void transformLoadingCross(float64 loading, std::array<QPointF, kPointCount> &po
|
|||
} // namespace
|
||||
|
||||
void CrossAnimation::paintStaticLoading(
|
||||
Painter &p,
|
||||
QPainter &p,
|
||||
const style::CrossAnimation &st,
|
||||
style::color color,
|
||||
int x,
|
||||
|
|
@ -110,7 +110,7 @@ void CrossAnimation::paintStaticLoading(
|
|||
}
|
||||
|
||||
void CrossAnimation::paint(
|
||||
Painter &p,
|
||||
QPainter &p,
|
||||
const style::CrossAnimation &st,
|
||||
style::color color,
|
||||
int x,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Ui {
|
|||
class CrossAnimation {
|
||||
public:
|
||||
static void paint(
|
||||
Painter &p,
|
||||
QPainter &p,
|
||||
const style::CrossAnimation &st,
|
||||
style::color color,
|
||||
int x,
|
||||
|
|
@ -24,7 +24,7 @@ public:
|
|||
float64 shown,
|
||||
float64 loading = 0.);
|
||||
static void paintStaticLoading(
|
||||
Painter &p,
|
||||
QPainter &p,
|
||||
const style::CrossAnimation &st,
|
||||
style::color color,
|
||||
int x,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ CrossLineAnimation::CrossLineAnimation(
|
|||
}
|
||||
|
||||
void CrossLineAnimation::paint(
|
||||
Painter &p,
|
||||
QPainter &p,
|
||||
QPoint position,
|
||||
float64 progress,
|
||||
std::optional<QColor> colorOverride) {
|
||||
|
|
@ -44,7 +44,7 @@ void CrossLineAnimation::paint(
|
|||
}
|
||||
|
||||
void CrossLineAnimation::paint(
|
||||
Painter &p,
|
||||
QPainter &p,
|
||||
int left,
|
||||
int top,
|
||||
float64 progress,
|
||||
|
|
@ -86,7 +86,7 @@ void CrossLineAnimation::fillFrame(
|
|||
topLine.setLength(topLine.length() * progress);
|
||||
auto bottomLine = topLine.translated(0, _strokePen.widthF() + 1);
|
||||
|
||||
Painter q(&_frame);
|
||||
auto q = QPainter(&_frame);
|
||||
PainterHighQualityEnabler hq(q);
|
||||
const auto colorize = ((colorOverride && colorOverride->alpha() != 255)
|
||||
|| (!colorOverride && _st.fg->c.alpha() != 255));
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@
|
|||
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class CrossLineAnimation {
|
||||
|
|
@ -20,12 +18,12 @@ public:
|
|||
float angle = 315);
|
||||
|
||||
void paint(
|
||||
Painter &p,
|
||||
QPainter &p,
|
||||
QPoint position,
|
||||
float64 progress,
|
||||
std::optional<QColor> colorOverride = std::nullopt);
|
||||
void paint(
|
||||
Painter &p,
|
||||
QPainter &p,
|
||||
int left,
|
||||
int top,
|
||||
float64 progress,
|
||||
|
|
|
|||
|
|
@ -31,22 +31,77 @@ constexpr auto kImageSpoilerDarkenAlpha = 32;
|
|||
constexpr auto kMaxCacheSize = 5 * 1024 * 1024;
|
||||
constexpr auto kDefaultFrameDuration = crl::time(33);
|
||||
constexpr auto kDefaultFramesCount = 60;
|
||||
constexpr auto kDefaultCanvasSize = 128;
|
||||
constexpr auto kDefaultParticlesCount = 3000;
|
||||
constexpr auto kDefaultFadeInDuration = crl::time(300);
|
||||
constexpr auto kDefaultParticleShownDuration = crl::time(0);
|
||||
constexpr auto kDefaultFadeOutDuration = crl::time(300);
|
||||
constexpr auto kAutoPauseTimeout = crl::time(1000);
|
||||
|
||||
std::atomic<const SpoilerMessCached*> DefaultMask/* = nullptr*/;
|
||||
std::condition_variable *DefaultMaskSignal/* = nullptr*/;
|
||||
std::mutex *DefaultMaskMutex/* = nullptr*/;
|
||||
[[nodiscard]] SpoilerMessDescriptor DefaultDescriptorText() {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto size = style::ConvertScale(128) * ratio;
|
||||
return {
|
||||
.particleFadeInDuration = crl::time(200),
|
||||
.particleShownDuration = crl::time(200),
|
||||
.particleFadeOutDuration = crl::time(200),
|
||||
.particleSizeMin = style::ConvertScaleExact(1.5) * ratio,
|
||||
.particleSizeMax = style::ConvertScaleExact(2.) * ratio,
|
||||
.particleSpeedMin = style::ConvertScaleExact(4.),
|
||||
.particleSpeedMax = style::ConvertScaleExact(8.),
|
||||
.particleSpritesCount = 5,
|
||||
.particlesCount = 9000,
|
||||
.canvasSize = size,
|
||||
.framesCount = kDefaultFramesCount,
|
||||
.frameDuration = kDefaultFrameDuration,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] SpoilerMessDescriptor DefaultDescriptorImage() {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto size = style::ConvertScale(128) * ratio;
|
||||
return {
|
||||
.particleFadeInDuration = crl::time(300),
|
||||
.particleShownDuration = crl::time(0),
|
||||
.particleFadeOutDuration = crl::time(300),
|
||||
.particleSizeMin = style::ConvertScaleExact(1.5) * ratio,
|
||||
.particleSizeMax = style::ConvertScaleExact(2.) * ratio,
|
||||
.particleSpeedMin = style::ConvertScaleExact(10.),
|
||||
.particleSpeedMax = style::ConvertScaleExact(20.),
|
||||
.particleSpritesCount = 5,
|
||||
.particlesCount = 3000,
|
||||
.canvasSize = size,
|
||||
.framesCount = kDefaultFramesCount,
|
||||
.frameDuration = kDefaultFrameDuration,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class SpoilerAnimationManager final {
|
||||
public:
|
||||
explicit SpoilerAnimationManager(not_null<SpoilerAnimation*> animation);
|
||||
|
||||
void add(not_null<SpoilerAnimation*> animation);
|
||||
void remove(not_null<SpoilerAnimation*> animation);
|
||||
|
||||
private:
|
||||
void destroyIfEmpty();
|
||||
|
||||
Ui::Animations::Basic _animation;
|
||||
base::flat_set<not_null<SpoilerAnimation*>> _list;
|
||||
|
||||
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 {
|
||||
uint32 version = 0;
|
||||
|
|
@ -137,58 +192,139 @@ struct Particle {
|
|||
return base.isEmpty() ? QString() : (base + "/spoiler");
|
||||
}
|
||||
|
||||
[[nodiscard]] QString DefaultMaskCachePath(const QString &folder) {
|
||||
return folder + "/mask";
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<SpoilerMessCached> ReadDefaultMask(
|
||||
const QString &name,
|
||||
std::optional<SpoilerMessCached::Validator> validator) {
|
||||
const auto folder = DefaultMaskCacheFolder();
|
||||
if (folder.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
auto file = QFile(DefaultMaskCachePath(folder));
|
||||
auto file = QFile(folder + '/' + name);
|
||||
return (file.open(QIODevice::ReadOnly) && file.size() <= kMaxCacheSize)
|
||||
? SpoilerMessCached::FromSerialized(file.readAll(), validator)
|
||||
: std::nullopt;
|
||||
}
|
||||
|
||||
void WriteDefaultMask(const SpoilerMessCached &mask) {
|
||||
void WriteDefaultMask(
|
||||
const QString &name,
|
||||
const SpoilerMessCached &mask) {
|
||||
const auto folder = DefaultMaskCacheFolder();
|
||||
if (!QDir().mkpath(folder)) {
|
||||
return;
|
||||
}
|
||||
const auto bytes = mask.serialize();
|
||||
auto file = QFile(DefaultMaskCachePath(folder));
|
||||
auto file = QFile(folder + '/' + name);
|
||||
if (file.open(QIODevice::WriteOnly) && bytes.size() <= kMaxCacheSize) {
|
||||
file.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
void Register(not_null<SpoilerAnimation*> animation) {
|
||||
if (!DefaultAnimationManager) {
|
||||
DefaultAnimationManager = new AnimationManager();
|
||||
DefaultAnimationManager->animation.init([] {
|
||||
for (const auto &animation : DefaultAnimationManager->list) {
|
||||
animation->repaint();
|
||||
}
|
||||
});
|
||||
DefaultAnimationManager->animation.start();
|
||||
if (DefaultAnimationManager) {
|
||||
DefaultAnimationManager->add(animation);
|
||||
} else {
|
||||
new SpoilerAnimationManager(animation);
|
||||
}
|
||||
DefaultAnimationManager->list.emplace(animation);
|
||||
}
|
||||
|
||||
void Unregister(not_null<SpoilerAnimation*> animation) {
|
||||
Expects(DefaultAnimationManager != nullptr);
|
||||
|
||||
DefaultAnimationManager->list.remove(animation);
|
||||
if (DefaultAnimationManager->list.empty()) {
|
||||
delete base::take(DefaultAnimationManager);
|
||||
DefaultAnimationManager->remove(animation);
|
||||
}
|
||||
|
||||
// DescriptorFactory: (void) -> SpoilerMessDescriptor.
|
||||
// Postprocess: (unique_ptr<MessCached>) -> unique_ptr<MessCached>.
|
||||
template <typename DescriptorFactory, typename Postprocess>
|
||||
void PrepareDefaultSpoiler(
|
||||
DefaultSpoiler &spoiler,
|
||||
const char *nameFactory,
|
||||
DescriptorFactory descriptorFactory,
|
||||
Postprocess postprocess) {
|
||||
if (spoiler.waiter.load()) {
|
||||
return;
|
||||
}
|
||||
const auto waiter = new DefaultSpoilerWaiter();
|
||||
auto expected = (DefaultSpoilerWaiter*)nullptr;
|
||||
if (!spoiler.waiter.compare_exchange_strong(expected, waiter)) {
|
||||
delete waiter;
|
||||
return;
|
||||
}
|
||||
const auto name = QString::fromUtf8(nameFactory);
|
||||
crl::async([=, &spoiler] {
|
||||
const auto descriptor = descriptorFactory();
|
||||
auto cached = ReadDefaultMask(name, SpoilerMessCached::Validator{
|
||||
.frameDuration = descriptor.frameDuration,
|
||||
.framesCount = descriptor.framesCount,
|
||||
.canvasSize = descriptor.canvasSize,
|
||||
});
|
||||
spoiler.cached = postprocess(cached
|
||||
? std::make_unique<SpoilerMessCached>(std::move(*cached))
|
||||
: std::make_unique<SpoilerMessCached>(
|
||||
GenerateSpoilerMess(descriptor))
|
||||
).release();
|
||||
auto lock = std::unique_lock(waiter->mutex);
|
||||
waiter->variable.notify_all();
|
||||
if (!cached) {
|
||||
WriteDefaultMask(name, *spoiler.cached);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] const SpoilerMessCached &WaitDefaultSpoiler(
|
||||
DefaultSpoiler &spoiler) {
|
||||
const auto &cached = spoiler.cached;
|
||||
if (const auto result = cached.load()) {
|
||||
return *result;
|
||||
}
|
||||
const auto waiter = spoiler.waiter.load();
|
||||
Assert(waiter != nullptr);
|
||||
while (true) {
|
||||
auto lock = std::unique_lock(waiter->mutex);
|
||||
if (const auto result = cached.load()) {
|
||||
return *result;
|
||||
}
|
||||
waiter->variable.wait(lock);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SpoilerAnimationManager::SpoilerAnimationManager(
|
||||
not_null<SpoilerAnimation*> animation)
|
||||
: _animation([=](crl::time now) {
|
||||
for (auto i = begin(_list); i != end(_list);) {
|
||||
if ((*i)->repaint(now)) {
|
||||
++i;
|
||||
} else {
|
||||
i = _list.erase(i);
|
||||
}
|
||||
}
|
||||
destroyIfEmpty();
|
||||
})
|
||||
, _list{ { animation } } {
|
||||
Expects(!DefaultAnimationManager);
|
||||
|
||||
DefaultAnimationManager = this;
|
||||
_animation.start();
|
||||
}
|
||||
|
||||
void SpoilerAnimationManager::add(not_null<SpoilerAnimation*> animation) {
|
||||
_list.emplace(animation);
|
||||
}
|
||||
|
||||
void SpoilerAnimationManager::remove(not_null<SpoilerAnimation*> animation) {
|
||||
_list.remove(animation);
|
||||
destroyIfEmpty();
|
||||
}
|
||||
|
||||
void SpoilerAnimationManager::destroyIfEmpty() {
|
||||
if (_list.empty()) {
|
||||
Assert(DefaultAnimationManager == this);
|
||||
delete base::take(DefaultAnimationManager);
|
||||
}
|
||||
}
|
||||
|
||||
SpoilerMessCached GenerateSpoilerMess(
|
||||
const SpoilerMessDescriptor &descriptor) {
|
||||
Expects(descriptor.framesCount > 0);
|
||||
|
|
@ -619,6 +755,7 @@ SpoilerAnimation::~SpoilerAnimation() {
|
|||
}
|
||||
|
||||
int SpoilerAnimation::index(crl::time now, bool paused) {
|
||||
_scheduled = false;
|
||||
const auto add = std::min(now - _last, kDefaultFrameDuration);
|
||||
if (anim::Disabled()) {
|
||||
paused = true;
|
||||
|
|
@ -638,66 +775,34 @@ int SpoilerAnimation::index(crl::time now, bool paused) {
|
|||
return absolute % kDefaultFramesCount;
|
||||
}
|
||||
|
||||
void SpoilerAnimation::repaint() {
|
||||
_repaint();
|
||||
}
|
||||
|
||||
void PrepareDefaultSpoilerMess() {
|
||||
DefaultMaskSignal = new std::condition_variable();
|
||||
DefaultMaskMutex = new std::mutex();
|
||||
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);
|
||||
bool SpoilerAnimation::repaint(crl::time now) {
|
||||
if (!_scheduled) {
|
||||
_scheduled = true;
|
||||
_repaint();
|
||||
} else if (_animating && _last && _last + kAutoPauseTimeout <= now) {
|
||||
_animating = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const SpoilerMessCached &DefaultImageSpoiler() {
|
||||
static const auto result = [&] {
|
||||
const auto mask = Ui::DefaultSpoilerMask();
|
||||
const auto frame = mask.frame(0);
|
||||
void PrepareTextSpoilerMask() {
|
||||
PrepareDefaultSpoiler(
|
||||
DefaultTextMask,
|
||||
"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(
|
||||
frame.image->size(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
|
|
@ -705,13 +810,21 @@ const SpoilerMessCached &DefaultImageSpoiler() {
|
|||
auto p = QPainter(&image);
|
||||
p.drawImage(0, 0, *frame.image);
|
||||
p.end();
|
||||
return Ui::SpoilerMessCached(
|
||||
return std::make_unique<SpoilerMessCached>(
|
||||
std::move(image),
|
||||
mask.framesCount(),
|
||||
mask.frameDuration(),
|
||||
mask.canvasSize());
|
||||
}();
|
||||
return result;
|
||||
cached->framesCount(),
|
||||
cached->frameDuration(),
|
||||
cached->canvasSize());
|
||||
};
|
||||
PrepareDefaultSpoiler(
|
||||
DefaultImageCached,
|
||||
"image",
|
||||
DefaultDescriptorImage,
|
||||
postprocess);
|
||||
}
|
||||
|
||||
const SpoilerMessCached &DefaultImageSpoiler() {
|
||||
return WaitDefaultSpoiler(DefaultImageCached);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ private:
|
|||
};
|
||||
|
||||
// Works with default frame duration and default frame count.
|
||||
class SpoilerAnimationManager;
|
||||
class SpoilerAnimation final {
|
||||
public:
|
||||
explicit SpoilerAnimation(Fn<void()> repaint);
|
||||
|
|
@ -97,21 +98,25 @@ public:
|
|||
|
||||
int index(crl::time now, bool paused);
|
||||
|
||||
void repaint();
|
||||
|
||||
private:
|
||||
friend class SpoilerAnimationManager;
|
||||
|
||||
[[nodiscard]] bool repaint(crl::time now);
|
||||
|
||||
const Fn<void()> _repaint;
|
||||
crl::time _accumulated = 0;
|
||||
crl::time _last = 0;
|
||||
bool _animating = false;
|
||||
bool _animating : 1 = false;
|
||||
bool _scheduled : 1 = false;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] SpoilerMessCached GenerateSpoilerMess(
|
||||
const SpoilerMessDescriptor &descriptor);
|
||||
|
||||
void PrepareDefaultSpoilerMess();
|
||||
[[nodiscard]] const SpoilerMessCached &DefaultSpoilerMask();
|
||||
void PrepareTextSpoilerMask();
|
||||
[[nodiscard]] const SpoilerMessCached &DefaultTextSpoilerMask();
|
||||
void PrepareImageSpoiler();
|
||||
[[nodiscard]] const SpoilerMessCached &DefaultImageSpoiler();
|
||||
|
||||
} // namespace Ui
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ std::unique_ptr<Text::CustomEmoji> Integration::createCustomEmoji(
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
Fn<void()> Integration::createSpoilerRepaint(const std::any &context) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Integration::handleUrlClick(
|
||||
const QString &url,
|
||||
const QVariant &context) {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ public:
|
|||
[[nodiscard]] virtual auto createCustomEmoji(
|
||||
const QString &data,
|
||||
const std::any &context) -> std::unique_ptr<Text::CustomEmoji>;
|
||||
[[nodiscard]] virtual Fn<void()> createSpoilerRepaint(
|
||||
const std::any &context);
|
||||
|
||||
[[nodiscard]] virtual rpl::producer<> forcePopupMenuHideRequests();
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "ui/layers/box_layer_widget.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ bool ArcsAnimation::isArcFinished(const Arc &arc) const {
|
|||
|| ((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);
|
||||
QPen pen;
|
||||
if (_strokeRatio) {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public:
|
|||
Direction direction);
|
||||
|
||||
void paint(
|
||||
Painter &p,
|
||||
QPainter &p,
|
||||
std::optional<QColor> colorOverride = std::nullopt);
|
||||
|
||||
void setValue(float64 value);
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ RadialBlob::RadialBlob(int n, float minScale, float minSpeed, float maxSpeed)
|
|||
, _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 m = QTransform();
|
||||
|
||||
|
|
@ -172,7 +172,7 @@ LinearBlob::LinearBlob(
|
|||
, _segments(_segmentsCount) {
|
||||
}
|
||||
|
||||
void LinearBlob::paint(Painter &p, const QBrush &brush, int width) {
|
||||
void LinearBlob::paint(QPainter &p, const QBrush &brush, int width) {
|
||||
if (!width) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class RadialBlob final : public Blob {
|
|||
public:
|
||||
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);
|
||||
|
||||
private:
|
||||
|
|
@ -94,7 +94,7 @@ public:
|
|||
float minSpeed = 0,
|
||||
float maxSpeed = 0);
|
||||
|
||||
void paint(Painter &p, const QBrush &brush, int width);
|
||||
void paint(QPainter &p, const QBrush &brush, int width);
|
||||
|
||||
private:
|
||||
struct Segment : Blob::Segment {
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ void Blobs::resetLevel() {
|
|||
_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();
|
||||
for (auto i = 0; i < _blobs.size(); i++) {
|
||||
const auto alpha = _blobDatas[i].alpha;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ public:
|
|||
|
||||
void setLevel(float value);
|
||||
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);
|
||||
|
||||
[[nodiscard]] float maxRadius() const;
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ void LinearBlobs::setLevel(float value) {
|
|||
_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);
|
||||
const auto opacity = p.opacity();
|
||||
for (auto i = 0; i < _blobs.size(); i++) {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@
|
|||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/paint/blob.h"
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace Ui::Paint {
|
||||
|
||||
class LinearBlobs final {
|
||||
|
|
@ -38,7 +36,7 @@ public:
|
|||
Blob::Radiuses radiusesAt(int index);
|
||||
|
||||
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);
|
||||
|
||||
[[nodiscard]] float maxRadius() const;
|
||||
|
|
|
|||
14
ui/painter.h
14
ui/painter.h
|
|
@ -11,6 +11,10 @@
|
|||
#include <QtCore/QPoint>
|
||||
#include <QtGui/QPainter>
|
||||
|
||||
namespace Ui::Text {
|
||||
struct SpoilerMess;
|
||||
} // namespace Ui::Text
|
||||
|
||||
class Painter : public QPainter {
|
||||
public:
|
||||
explicit Painter(QPaintDevice *device) : QPainter(device) {
|
||||
|
|
@ -78,9 +82,19 @@ public:
|
|||
[[nodiscard]] bool inactive() const {
|
||||
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:
|
||||
const style::TextPalette *_textPalette = nullptr;
|
||||
Ui::Text::SpoilerMess *_spoilerMess = nullptr;
|
||||
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_block.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "ui/click_handler.h"
|
||||
#include "base/flags.h"
|
||||
|
||||
#include <private/qfixed_p.h>
|
||||
#include <any>
|
||||
|
||||
class Painter;
|
||||
class SpoilerClickHandler;
|
||||
|
||||
namespace style {
|
||||
struct TextPalette;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
static const auto kQEllipsis = QStringLiteral("...");
|
||||
} // namespace Ui
|
||||
|
|
@ -90,7 +95,7 @@ struct StateResult {
|
|||
uint16 symbol = 0;
|
||||
};
|
||||
|
||||
struct StateRequestElided : public StateRequest {
|
||||
struct StateRequestElided : StateRequest {
|
||||
StateRequestElided() {
|
||||
}
|
||||
StateRequestElided(const StateRequest &other) : StateRequest(other) {
|
||||
|
|
@ -99,6 +104,48 @@ struct StateRequestElided : public StateRequest {
|
|||
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 {
|
||||
public:
|
||||
String(int32 minResizeWidth = QFIXED_MAX);
|
||||
|
|
@ -107,9 +154,9 @@ public:
|
|||
const QString &text,
|
||||
const TextParseOptions &options = kDefaultTextOptions,
|
||||
int32 minResizeWidth = QFIXED_MAX);
|
||||
String(String &&other) = default;
|
||||
String &operator=(String &&other) = default;
|
||||
~String() = default;
|
||||
String(String &&other);
|
||||
String &operator=(String &&other);
|
||||
~String();
|
||||
|
||||
[[nodiscard]] int countWidth(int width, bool breakEverywhere = false) const;
|
||||
[[nodiscard]] int countHeight(int width, bool breakEverywhere = false) const;
|
||||
|
|
@ -137,6 +184,8 @@ public:
|
|||
}
|
||||
[[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 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;
|
||||
|
|
@ -169,8 +218,8 @@ public:
|
|||
[[nodiscard]] TextForMimeData toTextForMimeData(
|
||||
TextSelection selection = AllTextSelection) const;
|
||||
|
||||
[[nodiscard]] bool hasCustomEmoji() const;
|
||||
void unloadCustomEmoji();
|
||||
[[nodiscard]] bool hasPersistentAnimation() const;
|
||||
void unloadPersistentAnimation();
|
||||
|
||||
[[nodiscard]] bool isIsolatedEmoji() const;
|
||||
[[nodiscard]] IsolatedEmoji toIsolatedEmoji() const;
|
||||
|
|
@ -217,9 +266,9 @@ private:
|
|||
QFixed _minResizeWidth;
|
||||
QFixed _maxWidth = 0;
|
||||
int32 _minHeight = 0;
|
||||
bool _hasCustomEmoji : 1;
|
||||
bool _isIsolatedEmoji : 1;
|
||||
bool _isOnlyCustomEmoji : 1;
|
||||
bool _hasCustomEmoji : 1 = false;
|
||||
bool _isIsolatedEmoji : 1 = false;
|
||||
bool _isOnlyCustomEmoji : 1 = false;
|
||||
|
||||
QString _text;
|
||||
const style::TextStyle *_st = nullptr;
|
||||
|
|
@ -229,13 +278,14 @@ private:
|
|||
|
||||
Qt::LayoutDirection _startDir = Qt::LayoutDirectionAuto;
|
||||
|
||||
std::shared_ptr<SpoilerData> _spoiler;
|
||||
std::unique_ptr<SpoilerData> _spoiler;
|
||||
|
||||
friend class Parser;
|
||||
friend class Renderer;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] bool IsBad(QChar ch);
|
||||
[[nodiscard]] bool IsWordSeparator(QChar ch);
|
||||
[[nodiscard]] bool IsAlmostLinkEnd(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 Ui
|
||||
|
|
|
|||
|
|
@ -14,8 +14,11 @@
|
|||
|
||||
#include <crl/crl_time.h>
|
||||
|
||||
namespace Ui {
|
||||
namespace Text {
|
||||
namespace style {
|
||||
struct TextStyle;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
enum TextBlockType {
|
||||
TextBlockTNewline = 0x01,
|
||||
|
|
@ -318,5 +321,12 @@ private:
|
|||
|
||||
};
|
||||
|
||||
} // namespace Text
|
||||
} // namespace Ui
|
||||
[[nodiscard]] int CountBlockHeight(
|
||||
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
|
||||
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
|
||||
class SpoilerClickHandler;
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
struct SpoilerData {
|
||||
struct {
|
||||
std::array<QImage, 4> corners;
|
||||
QColor color;
|
||||
} spoilerCache, spoilerShownCache;
|
||||
explicit SpoilerData(Fn<void()> repaint)
|
||||
: animation(std::move(repaint)) {
|
||||
}
|
||||
|
||||
SpoilerAnimation animation;
|
||||
QVector<std::shared_ptr<SpoilerClickHandler>> links;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "ui/toast/toast_widget.h"
|
||||
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/palette.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
|
|
|
|||
|
|
@ -597,7 +597,7 @@ void CrossButton::animationCallback() {
|
|||
}
|
||||
|
||||
void CrossButton::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
auto p = QPainter(this);
|
||||
|
||||
auto over = isOver();
|
||||
auto shown = _showAnimation.value(_shown ? 1. : 0.);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/basic_click_handlers.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/painter.h"
|
||||
|
||||
#include <QtGui/QtEvents>
|
||||
|
||||
|
|
@ -96,7 +97,7 @@ void ToggleView::setStyle(const style::Toggle &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;
|
||||
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->stroke > 0);
|
||||
|
||||
|
|
@ -247,7 +248,7 @@ void CheckView::setStyle(const style::Check &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 pen = _untoggledOverride
|
||||
? anim::pen(*_untoggledOverride, _st->toggledFg, toggled)
|
||||
|
|
@ -305,7 +306,7 @@ void RadioView::setStyle(const style::Radio &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);
|
||||
|
||||
auto toggled = currentAnimationValue();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "ui/text/text.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
class QPainter;
|
||||
class Painter;
|
||||
|
||||
namespace Ui {
|
||||
|
|
@ -38,7 +39,7 @@ public:
|
|||
|
||||
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 bool checkRippleStartPosition(QPoint position) const = 0;
|
||||
|
||||
|
|
@ -67,7 +68,7 @@ public:
|
|||
void setStyle(const style::Check &st);
|
||||
|
||||
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;
|
||||
bool checkRippleStartPosition(QPoint position) const override;
|
||||
|
||||
|
|
@ -95,7 +96,7 @@ public:
|
|||
void setUntoggledOverride(std::optional<QColor> untoggledOverride);
|
||||
|
||||
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;
|
||||
bool checkRippleStartPosition(QPoint position) const override;
|
||||
|
||||
|
|
@ -118,13 +119,13 @@ public:
|
|||
void setStyle(const style::Toggle &st);
|
||||
|
||||
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;
|
||||
bool checkRippleStartPosition(QPoint position) const override;
|
||||
void setLocked(bool locked);
|
||||
|
||||
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;
|
||||
|
||||
not_null<const style::Toggle*> _st;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ void IconButtonWithText::paintEvent(QPaintEvent *e) {
|
|||
|
||||
const auto r = rect() - _st.textPadding;
|
||||
|
||||
Painter p(this);
|
||||
auto p = QPainter(this);
|
||||
p.setFont(_st.font);
|
||||
p.setPen(_st.textFg);
|
||||
p.drawText(r, _text, _st.textAlign);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "ui/text/text.h"
|
||||
#include "ui/emoji_config.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/painter.h"
|
||||
#include "base/invoke_queued.h"
|
||||
#include "base/random.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
|
|
@ -1199,7 +1200,7 @@ void FlatInput::finishAnimations() {
|
|||
}
|
||||
|
||||
void FlatInput::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
auto p = QPainter(this);
|
||||
|
||||
auto placeholderFocused = _placeholderFocusedAnimation.value(_focused ? 1. : 0.);
|
||||
auto pen = anim::pen(_st.borderColor, _st.borderActive, placeholderFocused);
|
||||
|
|
@ -1886,7 +1887,7 @@ void InputField::handleTouchEvent(QTouchEvent *e) {
|
|||
}
|
||||
|
||||
void InputField::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
auto p = QPainter(this);
|
||||
|
||||
auto r = rect().intersected(e->rect());
|
||||
if (_st.textBg->c.alphaF() > 0.) {
|
||||
|
|
@ -4205,7 +4206,7 @@ void MaskedInputField::touchEvent(QTouchEvent *e) {
|
|||
}
|
||||
|
||||
void MaskedInputField::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
auto p = QPainter(this);
|
||||
|
||||
auto r = rect().intersected(e->rect());
|
||||
p.fillRect(r, _st.textBg);
|
||||
|
|
@ -4421,7 +4422,7 @@ QRect MaskedInputField::placeholderRect() const {
|
|||
return rect().marginsRemoved(_textMargins + _st.placeholderMargins);
|
||||
}
|
||||
|
||||
void MaskedInputField::placeholderAdditionalPrepare(Painter &p) {
|
||||
void MaskedInputField::placeholderAdditionalPrepare(QPainter &p) {
|
||||
p.setFont(_st.font);
|
||||
p.setPen(_st.placeholderFg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -698,14 +698,14 @@ protected:
|
|||
}
|
||||
void setCorrectedText(QString &now, int &nowCursor, const QString &newText, int newPos);
|
||||
|
||||
virtual void paintAdditionalPlaceholder(Painter &p) {
|
||||
virtual void paintAdditionalPlaceholder(QPainter &p) {
|
||||
}
|
||||
|
||||
style::font phFont() {
|
||||
return _st.font;
|
||||
}
|
||||
|
||||
void placeholderAdditionalPrepare(Painter &p);
|
||||
void placeholderAdditionalPrepare(QPainter &p);
|
||||
QRect placeholderRect() const;
|
||||
|
||||
void setTextMargins(const QMargins &mrg);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include "ui/widgets/box_content_divider.h"
|
||||
#include "ui/basic_click_handlers.h" // UrlClickHandler
|
||||
#include "ui/inactive_press.h"
|
||||
#include "ui/painter.h"
|
||||
#include "base/qt/qt_common_adapters.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/palette.h"
|
||||
|
|
|
|||
|
|
@ -48,8 +48,7 @@ void Menu::init() {
|
|||
|
||||
paintRequest(
|
||||
) | rpl::start_with_next([=](const QRect &clip) {
|
||||
Painter p(this);
|
||||
p.fillRect(clip, _st.itemBg);
|
||||
QPainter(this).fillRect(clip, _st.itemBg);
|
||||
}, lifetime());
|
||||
|
||||
positionValue(
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ void Action::paintEvent(QPaintEvent *e) {
|
|||
paint(p);
|
||||
}
|
||||
|
||||
void Action::paintBackground(Painter &p, bool selected) {
|
||||
void Action::paintBackground(QPainter &p, bool selected) {
|
||||
if (selected && _st.itemBgOver->c.alpha() < 255) {
|
||||
p.fillRect(0, 0, width(), _height, _st.itemBg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ protected:
|
|||
|
||||
int contentHeight() const override;
|
||||
|
||||
void paintBackground(Painter &p, bool selected);
|
||||
void paintBackground(QPainter &p, bool selected);
|
||||
void paintText(Painter &p);
|
||||
|
||||
private:
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ Toggle::~Toggle() = default;
|
|||
void Toggle::paintEvent(QPaintEvent *e) {
|
||||
Action::paintEvent(e);
|
||||
if (_toggle) {
|
||||
Painter p(this);
|
||||
auto p = QPainter(this);
|
||||
const auto toggleSize = _toggle->getSize();
|
||||
_toggle->paint(
|
||||
p,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "ui/platform/ui_platform_utility.h"
|
||||
#include "ui/layers/layer_widget.h"
|
||||
#include "ui/layers/show.h"
|
||||
#include "ui/painter.h"
|
||||
#include "base/debug_log.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
|
@ -311,7 +312,7 @@ void SeparatePanel::createBorderImage() {
|
|||
cache.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
cache.fill(Qt::transparent);
|
||||
{
|
||||
Painter p(&cache);
|
||||
auto p = QPainter(&cache);
|
||||
auto inner = QRect(0, 0, cacheSize, cacheSize).marginsRemoved(
|
||||
shadowPadding);
|
||||
Ui::Shadow::paint(p, inner, cacheSize, st::callShadow);
|
||||
|
|
@ -572,7 +573,7 @@ void SeparatePanel::updateControlsGeometry() {
|
|||
}
|
||||
|
||||
void SeparatePanel::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
auto p = QPainter(this);
|
||||
if (!_animationCache.isNull()) {
|
||||
auto opacity = _opacityAnimation.value(_visible ? 1. : 0.);
|
||||
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 size = st::separatePanelBorderCacheSize;
|
||||
const auto part1 = size / 3;
|
||||
|
|
@ -685,7 +686,7 @@ void SeparatePanel::paintShadowBorder(Painter &p) const {
|
|||
st::windowBg);
|
||||
}
|
||||
|
||||
void SeparatePanel::paintOpaqueBorder(Painter &p) const {
|
||||
void SeparatePanel::paintOpaqueBorder(QPainter &p) const {
|
||||
const auto border = st::windowShadowFgFallback;
|
||||
p.fillRect(0, 0, width(), _padding.top(), border);
|
||||
p.fillRect(
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ private:
|
|||
|
||||
void updateTitleGeometry(int newWidth);
|
||||
void updateTitlePosition();
|
||||
void paintShadowBorder(Painter &p) const;
|
||||
void paintOpaqueBorder(Painter &p) const;
|
||||
void paintShadowBorder(QPainter &p) const;
|
||||
void paintOpaqueBorder(QPainter &p) const;
|
||||
|
||||
void toggleOpacityAnimation(bool visible);
|
||||
void finishAnimating();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "ui/widgets/side_bar_button.h"
|
||||
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <QtGui/QtEvents>
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ rpl::producer<> TimeInput::focuses() const {
|
|||
}
|
||||
|
||||
void TimeInput::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
auto p = QPainter(this);
|
||||
|
||||
const auto &_st = _stDateField;
|
||||
const auto height = _st.heightMin;
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@
|
|||
#include "ui/widgets/tooltip.h"
|
||||
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/platform/ui_platform_utility.h"
|
||||
#include "base/invoke_queued.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <QtGui/QScreen>
|
||||
#include <QtGui/QWindow>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue