Implement animated spoilers.

This commit is contained in:
John Preston 2022-09-17 00:22:08 +04:00
parent f82162f05a
commit 73b6bc5e13
45 changed files with 3469 additions and 3074 deletions

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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));

View file

@ -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,

View file

@ -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();
if (DefaultAnimationManager) {
DefaultAnimationManager->add(animation);
} else {
new SpoilerAnimationManager(animation);
}
});
DefaultAnimationManager->animation.start();
}
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() {
bool SpoilerAnimation::repaint(crl::time now) {
if (!_scheduled) {
_scheduled = true;
_repaint();
} else if (_animating && _last && _last + kAutoPauseTimeout <= now) {
_animating = false;
return false;
}
return true;
}
void 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);
}
});
void PrepareTextSpoilerMask() {
PrepareDefaultSpoiler(
DefaultTextMask,
"text",
DefaultDescriptorText,
[](std::unique_ptr<SpoilerMessCached> cached) { return cached; });
}
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);
}
const SpoilerMessCached &DefaultTextSpoilerMask() {
return WaitDefaultSpoiler(DefaultTextMask);
}
const SpoilerMessCached &DefaultImageSpoiler() {
static const auto result = [&] {
const auto mask = Ui::DefaultSpoilerMask();
const auto frame = mask.frame(0);
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

View file

@ -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

View file

@ -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) {

View file

@ -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();

View file

@ -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"

View file

@ -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) {

View file

@ -29,7 +29,7 @@ public:
Direction direction);
void paint(
Painter &p,
QPainter &p,
std::optional<QColor> colorOverride = std::nullopt);
void setValue(float64 value);

View file

@ -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;
}

View file

@ -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 {

View file

@ -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;

View file

@ -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;

View file

@ -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++) {

View file

@ -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;

View file

@ -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;
};

File diff suppressed because it is too large Load diff

View file

@ -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);

View file

@ -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

View file

@ -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
View 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
View 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

File diff suppressed because it is too large Load diff

158
ui/text/text_renderer.h Normal file
View 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

View file

@ -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;
};

View file

@ -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"

View file

@ -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.);

View file

@ -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();

View file

@ -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;

View file

@ -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);

View file

@ -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);
}

View file

@ -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);

View file

@ -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"

View file

@ -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(

View file

@ -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);
}

View file

@ -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:

View file

@ -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,

View file

@ -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(

View file

@ -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();

View file

@ -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>

View file

@ -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;

View file

@ -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>