From 73b6bc5e136df9aa13bad8841d9a95c4f357a4de Mon Sep 17 00:00:00 2001 From: John Preston Date: Sat, 17 Sep 2022 00:22:08 +0400 Subject: [PATCH] Implement animated spoilers. --- CMakeLists.txt | 4 + ui/effects/cross_animation.cpp | 4 +- ui/effects/cross_animation.h | 4 +- ui/effects/cross_line.cpp | 6 +- ui/effects/cross_line.h | 6 +- ui/effects/spoiler_mess.cpp | 301 ++- ui/effects/spoiler_mess.h | 15 +- ui/integration.cpp | 4 + ui/integration.h | 2 + ui/layers/layer_widget.cpp | 1 + ui/paint/arcs.cpp | 2 +- ui/paint/arcs.h | 2 +- ui/paint/blob.cpp | 4 +- ui/paint/blob.h | 4 +- ui/paint/blobs.cpp | 2 +- ui/paint/blobs.h | 2 +- ui/paint/blobs_linear.cpp | 2 +- ui/paint/blobs_linear.h | 4 +- ui/painter.h | 14 + ui/text/text.cpp | 3036 ++------------------------ ui/text/text.h | 72 +- ui/text/text_block.cpp | 10 + ui/text/text_block.h | 18 +- ui/text/text_parser.cpp | 739 +++++++ ui/text/text_parser.h | 128 ++ ui/text/text_renderer.cpp | 1922 ++++++++++++++++ ui/text/text_renderer.h | 158 ++ ui/text/text_spoiler_data.h | 10 +- ui/toast/toast_widget.cpp | 1 + ui/widgets/buttons.cpp | 2 +- ui/widgets/checkbox.cpp | 9 +- ui/widgets/checkbox.h | 11 +- ui/widgets/icon_button_with_text.cpp | 2 +- ui/widgets/input_fields.cpp | 9 +- ui/widgets/input_fields.h | 4 +- ui/widgets/labels.cpp | 1 + ui/widgets/menu/menu.cpp | 3 +- ui/widgets/menu/menu_action.cpp | 2 +- ui/widgets/menu/menu_action.h | 2 +- ui/widgets/menu/menu_toggle.cpp | 2 +- ui/widgets/separate_panel.cpp | 9 +- ui/widgets/separate_panel.h | 4 +- ui/widgets/side_bar_button.cpp | 1 + ui/widgets/time_input.cpp | 2 +- ui/widgets/tooltip.cpp | 3 +- 45 files changed, 3469 insertions(+), 3074 deletions(-) create mode 100644 ui/text/text_parser.cpp create mode 100644 ui/text/text_parser.h create mode 100644 ui/text/text_renderer.cpp create mode 100644 ui/text/text_renderer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f565386..efd551e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/ui/effects/cross_animation.cpp b/ui/effects/cross_animation.cpp index 4b6f7f9..865bc8a 100644 --- a/ui/effects/cross_animation.cpp +++ b/ui/effects/cross_animation.cpp @@ -99,7 +99,7 @@ void transformLoadingCross(float64 loading, std::array &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, diff --git a/ui/effects/cross_animation.h b/ui/effects/cross_animation.h index ffa17bd..e0ca4c4 100644 --- a/ui/effects/cross_animation.h +++ b/ui/effects/cross_animation.h @@ -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, diff --git a/ui/effects/cross_line.cpp b/ui/effects/cross_line.cpp index c1e0f96..aeffdef 100644 --- a/ui/effects/cross_line.cpp +++ b/ui/effects/cross_line.cpp @@ -36,7 +36,7 @@ CrossLineAnimation::CrossLineAnimation( } void CrossLineAnimation::paint( - Painter &p, + QPainter &p, QPoint position, float64 progress, std::optional 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)); diff --git a/ui/effects/cross_line.h b/ui/effects/cross_line.h index f8a27ea..99fc506 100644 --- a/ui/effects/cross_line.h +++ b/ui/effects/cross_line.h @@ -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 colorOverride = std::nullopt); void paint( - Painter &p, + QPainter &p, int left, int top, float64 progress, diff --git a/ui/effects/spoiler_mess.cpp b/ui/effects/spoiler_mess.cpp index 997a5fc..a125bd5 100644 --- a/ui/effects/spoiler_mess.cpp +++ b/ui/effects/spoiler_mess.cpp @@ -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 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 animation); + + void add(not_null animation); + void remove(not_null animation); + +private: + void destroyIfEmpty(); + + Ui::Animations::Basic _animation; + base::flat_set> _list; -struct AnimationManager { - Ui::Animations::Basic animation; - base::flat_set> list; }; -AnimationManager *DefaultAnimationManager/* = nullptr*/; +namespace { + +struct DefaultSpoilerWaiter { + std::condition_variable variable; + std::mutex mutex; +}; +struct DefaultSpoiler { + std::atomic cached/* = nullptr*/; + std::atomic 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 ReadDefaultMask( + const QString &name, std::optional 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 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 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) -> unique_ptr. +template +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(std::move(*cached)) + : std::make_unique( + 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 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 animation) { + _list.emplace(animation); +} + +void SpoilerAnimationManager::remove(not_null 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 cached) { return cached; }); +} + +const SpoilerMessCached &DefaultTextSpoilerMask() { + return WaitDefaultSpoiler(DefaultTextMask); +} + +void PrepareImageSpoiler() { + const auto postprocess = [](std::unique_ptr 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( 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 diff --git a/ui/effects/spoiler_mess.h b/ui/effects/spoiler_mess.h index b6510b0..0ca429f 100644 --- a/ui/effects/spoiler_mess.h +++ b/ui/effects/spoiler_mess.h @@ -90,6 +90,7 @@ private: }; // Works with default frame duration and default frame count. +class SpoilerAnimationManager; class SpoilerAnimation final { public: explicit SpoilerAnimation(Fn 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 _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 diff --git a/ui/integration.cpp b/ui/integration.cpp index ee44160..99ab7c4 100644 --- a/ui/integration.cpp +++ b/ui/integration.cpp @@ -76,6 +76,10 @@ std::unique_ptr Integration::createCustomEmoji( return nullptr; } +Fn Integration::createSpoilerRepaint(const std::any &context) { + return nullptr; +} + bool Integration::handleUrlClick( const QString &url, const QVariant &context) { diff --git a/ui/integration.h b/ui/integration.h index 6eb7d06..db065d5 100644 --- a/ui/integration.h +++ b/ui/integration.h @@ -63,6 +63,8 @@ public: [[nodiscard]] virtual auto createCustomEmoji( const QString &data, const std::any &context) -> std::unique_ptr; + [[nodiscard]] virtual Fn createSpoilerRepaint( + const std::any &context); [[nodiscard]] virtual rpl::producer<> forcePopupMenuHideRequests(); diff --git a/ui/layers/layer_widget.cpp b/ui/layers/layer_widget.cpp index 5f07694..4a67d03 100644 --- a/ui/layers/layer_widget.cpp +++ b/ui/layers/layer_widget.cpp @@ -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" diff --git a/ui/paint/arcs.cpp b/ui/paint/arcs.cpp index 0204e06..2f346e4 100644 --- a/ui/paint/arcs.cpp +++ b/ui/paint/arcs.cpp @@ -186,7 +186,7 @@ bool ArcsAnimation::isArcFinished(const Arc &arc) const { || ((arc.threshold <= _currentValue) && (arc.progress == 0.)); } -void ArcsAnimation::paint(Painter &p, std::optional colorOverride) { +void ArcsAnimation::paint(QPainter &p, std::optional colorOverride) { PainterHighQualityEnabler hq(p); QPen pen; if (_strokeRatio) { diff --git a/ui/paint/arcs.h b/ui/paint/arcs.h index 022b6d6..27d98bd 100644 --- a/ui/paint/arcs.h +++ b/ui/paint/arcs.h @@ -29,7 +29,7 @@ public: Direction direction); void paint( - Painter &p, + QPainter &p, std::optional colorOverride = std::nullopt); void setValue(float64 value); diff --git a/ui/paint/blob.cpp b/ui/paint/blob.cpp index c694ebc..5722659 100644 --- a/ui/paint/blob.cpp +++ b/ui/paint/blob.cpp @@ -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; } diff --git a/ui/paint/blob.h b/ui/paint/blob.h index 805914b..4006d25 100644 --- a/ui/paint/blob.h +++ b/ui/paint/blob.h @@ -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 { diff --git a/ui/paint/blobs.cpp b/ui/paint/blobs.cpp index 903f5d9..00e2cb8 100644 --- a/ui/paint/blobs.cpp +++ b/ui/paint/blobs.cpp @@ -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; diff --git a/ui/paint/blobs.h b/ui/paint/blobs.h index c93f419..2dc446c 100644 --- a/ui/paint/blobs.h +++ b/ui/paint/blobs.h @@ -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; diff --git a/ui/paint/blobs_linear.cpp b/ui/paint/blobs_linear.cpp index 1f46e1f..4d274f2 100644 --- a/ui/paint/blobs_linear.cpp +++ b/ui/paint/blobs_linear.cpp @@ -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++) { diff --git a/ui/paint/blobs_linear.h b/ui/paint/blobs_linear.h index e175870..ea85f0e 100644 --- a/ui/paint/blobs_linear.h +++ b/ui/paint/blobs_linear.h @@ -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; diff --git a/ui/painter.h b/ui/painter.h index d2b73d8..4d15b82 100644 --- a/ui/painter.h +++ b/ui/painter.h @@ -11,6 +11,10 @@ #include #include +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 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; }; diff --git a/ui/text/text.cpp b/ui/text/text.cpp index 7a2af6c..764d01b 100644 --- a/ui/text/text.cpp +++ b/ui/text/text.cpp @@ -6,34 +6,22 @@ // #include "ui/text/text.h" -#include "ui/basic_click_handlers.h" -#include "ui/text/text_block.h" #include "ui/text/text_isolated_emoji.h" +#include "ui/text/text_parser.h" +#include "ui/text/text_renderer.h" #include "ui/text/text_spoiler_data.h" -#include "ui/emoji_config.h" -#include "ui/integration.h" -#include "ui/round_rect.h" -#include "ui/image/image_prepare.h" +#include "ui/basic_click_handlers.h" +#include "ui/painter.h" #include "ui/spoiler_click_handler.h" #include "base/platform/base_platform_info.h" -#include "base/qt/qt_common_adapters.h" - -#include -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) -#include -#endif // Qt < 6.0.0 +#include "styles/style_basic.h" namespace Ui::Text { namespace { -constexpr auto kStringLinkIndexShift = uint16(0x8000); -constexpr auto kMaxDiacAfterSymbol = 2; +constexpr auto kDefaultSpoilerCacheCapacity = 24; -inline bool IsMono(int32 flags) { - return (flags & TextBlockFPre) || (flags & TextBlockFCode); -} - -Qt::LayoutDirection StringDirection( +[[nodiscard]] Qt::LayoutDirection StringDirection( const QString &str, int from, int to) { @@ -62,66 +50,6 @@ Qt::LayoutDirection StringDirection( return Qt::LayoutDirectionAuto; } -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; -} - -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. -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")); -} - bool IsParagraphSeparator(QChar ch) { switch (ch.unicode()) { case QChar::LineFeed: @@ -132,42 +60,6 @@ bool IsParagraphSeparator(QChar ch) { return false; } -bool IsBad(QChar ch) { - return (ch == 0) - || (ch >= 8232 && ch < 8237) - || (ch >= 65024 && ch < 65040 && ch != 65039) - || (ch >= 127 && ch < 160 && ch != 156) - - // qt harfbuzz crash see https://github.com/telegramdesktop/tdesktop/issues/4551 - || (Platform::IsMac() && ch == 6158); -} - -void InitTextItemWithScriptItem(QTextItemInt &ti, const QScriptItem &si) { - // explicitly initialize flags so that initFontAttributes can be called - // multiple times on the same TextItem - ti.flags = { }; - if (si.analysis.bidiLevel %2) - ti.flags |= QTextItem::RightToLeft; - ti.ascent = si.ascent; - ti.descent = si.descent; - - if (ti.charFormat.hasProperty(QTextFormat::TextUnderlineStyle)) { - ti.underlineStyle = ti.charFormat.underlineStyle(); - } else if (ti.charFormat.boolProperty(QTextFormat::FontUnderline) - || ti.f->underline()) { - ti.underlineStyle = QTextCharFormat::SingleUnderline; - } - - // compat - if (ti.underlineStyle == QTextCharFormat::SingleUnderline) - ti.flags |= QTextItem::Underline; - - if (ti.f->overline() || ti.charFormat.fontOverline()) - ti.flags |= QTextItem::Overline; - if (ti.f->strikeOut() || ti.charFormat.fontStrikeOut()) - ti.flags |= QTextItem::StrikeOut; -} - } // namespace } // namespace Ui::Text @@ -192,2771 +84,61 @@ const TextParseOptions kPlainTextOptions = { Qt::LayoutDirectionAuto, // dir }; -namespace Ui { -namespace Text { +namespace Ui::Text { -class Parser { -public: - Parser( - not_null string, - const TextWithEntities &textWithEntities, - const TextParseOptions &options, - const std::any &context); +SpoilerMessCache::SpoilerMessCache(int capacity) : _capacity(capacity) { + Expects(capacity > 0); -private: - struct ReadyToken { + _cache.reserve(capacity); +} + +not_null SpoilerMessCache::lookup(QColor color) { + for (auto &entry : _cache) { + if (entry.color == color) { + entry.generation = ++_generation; + return &entry.mess; + } + } + if (_cache.size() == _capacity) { + const auto i = ranges::min_element( + _cache, + ranges::less(), + &Entry::generation); + i->generation = ++_generation; + i->color = color; + i->mess = Ui::SpoilerMessCached(DefaultTextSpoilerMask(), color); + return &i->mess; + } + _cache.push_back({ + .mess = Ui::SpoilerMessCached(DefaultTextSpoilerMask(), color), + .color = color, + .generation = ++_generation, + }); + return &_cache.back().mess; +} + +void SpoilerMessCache::reset() { + _cache.clear(); +} + +not_null DefaultSpoilerCache() { + struct Data { + Data() : cache(kDefaultSpoilerCacheCapacity) { + style::PaletteChanged() | rpl::start_with_next([=] { + cache.reset(); + }, lifetime); + } + + SpoilerMessCache cache; + rpl::lifetime lifetime; }; - 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 flags() const; - [[nodiscard]] std::optional lnkIndex() const; - [[nodiscard]] std::optional spoilerIndex() const; - - private: - const int _value = 0; - const Type _type; - - }; - - Parser( - not_null 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 _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 _linksIndexes; - - std::vector _links; - std::vector _spoilers; - std::vector _monos; - base::flat_map< - const QChar*, - std::vector> _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 - -}; - -Parser::StartedEntity::StartedEntity(TextBlockFlags flags) -: _value(flags) -, _type(Type::Flags) { - Expects(_value >= 0 && _value < int(kStringLinkIndexShift)); + static auto data = Data(); + return &data.cache; } -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 Parser::StartedEntity::flags() const { - if (_value < int(kStringLinkIndexShift) && (_type == Type::Flags)) { - return TextBlockFlags(_value); - } - return std::nullopt; -} - -std::optional 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 Parser::StartedEntity::spoilerIndex() const { - if (_value < int(kStringLinkIndexShift) && (_type == Type::Spoiler)) { - return uint16(_value); - } - return std::nullopt; -} - -Parser::Parser( - not_null string, - const TextWithEntities &textWithEntities, - const TextParseOptions &options, - const std::any &context) -: Parser( - string, - PrepareRichFromRich(textWithEntities, options), - options, - context, - ReadyToken()) { -} - -Parser::Parser( - not_null 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_shared(); - } - if (_t->_spoiler->links.size() < spoilerIndex) { - _t->_spoiler->links.resize(spoilerIndex); - const auto handler = (options.flags & TextParseLinks) - ? std::make_shared() - : 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 { - -// COPIED FROM qtextengine.cpp AND MODIFIED - -struct BidiStatus { - BidiStatus() { - eor = QChar::DirON; - lastStrong = QChar::DirON; - last = QChar:: DirON; - dir = QChar::DirON; - } - QChar::Direction eor; - QChar::Direction lastStrong; - QChar::Direction last; - QChar::Direction dir; -}; - -enum { _MaxBidiLevel = 61 }; -enum { _MaxItemLength = 4096 }; - -struct BidiControl { - inline BidiControl(bool rtl) - : base(rtl ? 1 : 0), level(rtl ? 1 : 0) {} - - inline void embed(bool rtl, bool o = false) { - unsigned int toAdd = 1; - if((level%2 != 0) == rtl ) { - ++toAdd; - } - if (level + toAdd <= _MaxBidiLevel) { - ctx[cCtx].level = level; - ctx[cCtx].override = override; - cCtx++; - override = o; - level += toAdd; - } - } - inline bool canPop() const { return cCtx != 0; } - inline void pdf() { - Q_ASSERT(cCtx); - --cCtx; - level = ctx[cCtx].level; - override = ctx[cCtx].override; - } - - inline QChar::Direction basicDirection() const { - return (base ? QChar::DirR : QChar:: DirL); - } - inline unsigned int baseLevel() const { - return base; - } - inline QChar::Direction direction() const { - return ((level%2) ? QChar::DirR : QChar:: DirL); - } - - struct { - unsigned int level = 0; - bool override = false; - } ctx[_MaxBidiLevel]; - unsigned int cCtx = 0; - const unsigned int base; - unsigned int level; - bool override = false; -}; - -static void eAppendItems(QScriptAnalysis *analysis, int &start, int &stop, const BidiControl &control, QChar::Direction dir) { - if (start > stop) - return; - - int level = control.level; - - if(dir != QChar::DirON && !control.override) { - // add level of run (cases I1 & I2) - if(level % 2) { - if(dir == QChar::DirL || dir == QChar::DirAN || dir == QChar::DirEN) - level++; - } else { - if(dir == QChar::DirR) - level++; - else if(dir == QChar::DirAN || dir == QChar::DirEN) - level += 2; - } - } - - QScriptAnalysis *s = analysis + start; - const QScriptAnalysis *e = analysis + stop; - while (s <= e) { - s->bidiLevel = level; - ++s; - } - ++stop; - start = stop; -} - -inline int32 countBlockHeight(const AbstractBlock *b, const style::TextStyle *st) { - return (b->type() == TextBlockTSkip) ? static_cast(b)->height() : (st->lineHeight > st->font->height) ? st->lineHeight : st->font->height; -} - -} // namespace - -class Renderer { -public: - Renderer(Painter *p, const String *t) - : _p(p) - , _t(t) - , _originalPen(p ? p->pen() : QPen()) { - } - - ~Renderer() { - restoreAfterElided(); - if (_p) { - _p->setPen(_originalPen); - } - } - - void draw(int32 left, int32 top, int32 w, style::align align, int32 yFrom, int32 yTo, TextSelection selection = { 0, 0 }, bool fullWidthSelection = true) { - if (_t->isEmpty()) return; - - _blocksSize = _t->_blocks.size(); - if (_p) { - _p->setFont(_t->_st->font); - _textPalette = &_p->textPalette(); - _originalPenSelected = (_textPalette->selectFg->c.alphaF() == 0) ? _originalPen : _textPalette->selectFg->p; - } - - _x = left; - _y = top; - _yFrom = yFrom + top; - _yTo = (yTo < 0) ? -1 : (yTo + top); - _selection = selection; - _fullWidthSelection = fullWidthSelection; - _wLeft = _w = w; - if (_elideLast) { - _yToElide = _yTo; - if (_elideRemoveFromEnd > 0 && !_t->_blocks.empty()) { - int firstBlockHeight = countBlockHeight(_t->_blocks.front().get(), _t->_st); - if (_y + firstBlockHeight >= _yToElide) { - _wLeft -= _elideRemoveFromEnd; - } - } - } - _str = _t->_text.unicode(); - - if (_p) { - auto clip = _p->hasClipping() ? _p->clipBoundingRect() : QRect(); - if (clip.width() > 0 || clip.height() > 0) { - if (_yFrom < clip.y()) _yFrom = clip.y(); - if (_yTo < 0 || _yTo > clip.y() + clip.height()) _yTo = clip.y() + clip.height(); - } - } - - _align = align; - - _parDirection = _t->_startDir; - if (_parDirection == Qt::LayoutDirectionAuto) _parDirection = style::LayoutDirection(); - if ((*_t->_blocks.cbegin())->type() != TextBlockTNewline) { - initNextParagraph(_t->_blocks.cbegin()); - } - - _lineStart = 0; - _lineStartBlock = 0; - - _lineHeight = 0; - _fontHeight = _t->_st->font->height; - auto last_rBearing = QFixed(0); - _last_rPadding = QFixed(0); - - auto blockIndex = 0; - bool longWordLine = true; - auto e = _t->_blocks.cend(); - for (auto i = _t->_blocks.cbegin(); i != e; ++i, ++blockIndex) { - auto b = i->get(); - auto _btype = b->type(); - auto blockHeight = countBlockHeight(b, _t->_st); - - if (_btype == TextBlockTNewline) { - if (!_lineHeight) _lineHeight = blockHeight; - if (!drawLine((*i)->from(), i, e)) { - return; - } - - _y += _lineHeight; - _lineHeight = 0; - _lineStart = _t->countBlockEnd(i, e); - _lineStartBlock = blockIndex + 1; - - last_rBearing = b->f_rbearing(); - _last_rPadding = b->f_rpadding(); - _wLeft = _w - (b->f_width() - last_rBearing); - if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) { - _wLeft -= _elideRemoveFromEnd; - } - - _parDirection = static_cast(b)->nextDirection(); - if (_parDirection == Qt::LayoutDirectionAuto) _parDirection = style::LayoutDirection(); - initNextParagraph(i + 1); - - longWordLine = true; - continue; - } - - auto b__f_rbearing = b->f_rbearing(); - auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + b->f_width() - b__f_rbearing); - if (newWidthLeft >= 0) { - last_rBearing = b__f_rbearing; - _last_rPadding = b->f_rpadding(); - _wLeft = newWidthLeft; - - _lineHeight = qMax(_lineHeight, blockHeight); - - longWordLine = false; - continue; - } - - if (_btype == TextBlockTText) { - auto t = static_cast(b); - if (t->_words.isEmpty()) { // no words in this block, spaces only => layout this block in the same line - _last_rPadding += b->f_rpadding(); - - _lineHeight = qMax(_lineHeight, blockHeight); - - longWordLine = false; - continue; - } - - auto f_wLeft = _wLeft; // vars for saving state of the last word start - auto f_lineHeight = _lineHeight; // f points to the last word-start element of t->_words - for (auto j = t->_words.cbegin(), en = t->_words.cend(), f = j; j != en; ++j) { - auto wordEndsHere = (j->f_width() >= 0); - auto j_width = wordEndsHere ? j->f_width() : -j->f_width(); - - auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + j_width - j->f_rbearing()); - if (newWidthLeft >= 0) { - last_rBearing = j->f_rbearing(); - _last_rPadding = j->f_rpadding(); - _wLeft = newWidthLeft; - - _lineHeight = qMax(_lineHeight, blockHeight); - - if (wordEndsHere) { - longWordLine = false; - } - if (wordEndsHere || longWordLine) { - f = j + 1; - f_wLeft = _wLeft; - f_lineHeight = _lineHeight; - } - continue; - } - - auto elidedLineHeight = qMax(_lineHeight, blockHeight); - auto elidedLine = _elideLast && (_y + elidedLineHeight >= _yToElide); - if (elidedLine) { - _lineHeight = elidedLineHeight; - } else if (f != j && !_breakEverywhere) { - // word did not fit completely, so we roll back the state to the beginning of this long word - j = f; - _wLeft = f_wLeft; - _lineHeight = f_lineHeight; - j_width = (j->f_width() >= 0) ? j->f_width() : -j->f_width(); - } - if (!drawLine(elidedLine ? ((j + 1 == en) ? _t->countBlockEnd(i, e) : (j + 1)->from()) : j->from(), i, e)) { - return; - } - _y += _lineHeight; - _lineHeight = qMax(0, blockHeight); - _lineStart = j->from(); - _lineStartBlock = blockIndex; - - last_rBearing = j->f_rbearing(); - _last_rPadding = j->f_rpadding(); - _wLeft = _w - (j_width - last_rBearing); - if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) { - _wLeft -= _elideRemoveFromEnd; - } - - longWordLine = !wordEndsHere; - f = j + 1; - f_wLeft = _wLeft; - f_lineHeight = _lineHeight; - } - continue; - } - - auto elidedLineHeight = qMax(_lineHeight, blockHeight); - auto elidedLine = _elideLast && (_y + elidedLineHeight >= _yToElide); - if (elidedLine) { - _lineHeight = elidedLineHeight; - } - if (!drawLine(elidedLine ? _t->countBlockEnd(i, e) : b->from(), i, e)) { - return; - } - _y += _lineHeight; - _lineHeight = qMax(0, blockHeight); - _lineStart = b->from(); - _lineStartBlock = blockIndex; - - last_rBearing = b__f_rbearing; - _last_rPadding = b->f_rpadding(); - _wLeft = _w - (b->f_width() - last_rBearing); - if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) { - _wLeft -= _elideRemoveFromEnd; - } - - longWordLine = true; - continue; - } - if (_lineStart < _t->_text.size()) { - if (!drawLine(_t->_text.size(), e, e)) return; - } - if (!_p && _lookupSymbol) { - _lookupResult.symbol = _t->_text.size(); - _lookupResult.afterSymbol = false; - } - } - - void drawElided(int32 left, int32 top, int32 w, style::align align, int32 lines, int32 yFrom, int32 yTo, int32 removeFromEnd, bool breakEverywhere, TextSelection selection) { - if (lines <= 0 || _t->isNull()) return; - - if (yTo < 0 || (lines - 1) * _t->_st->font->height < yTo) { - yTo = lines * _t->_st->font->height; - _elideLast = true; - _elideRemoveFromEnd = removeFromEnd; - } - _breakEverywhere = breakEverywhere; - draw(left, top, w, align, yFrom, yTo, selection); - } - - StateResult getState(QPoint point, int w, StateRequest request) { - if (!_t->isNull() && point.y() >= 0) { - _lookupRequest = request; - _lookupX = point.x(); - _lookupY = point.y(); - - _breakEverywhere = (_lookupRequest.flags & StateRequest::Flag::BreakEverywhere); - _lookupSymbol = (_lookupRequest.flags & StateRequest::Flag::LookupSymbol); - _lookupLink = (_lookupRequest.flags & StateRequest::Flag::LookupLink); - if (_lookupSymbol || (_lookupX >= 0 && _lookupX < w)) { - draw(0, 0, w, _lookupRequest.align, _lookupY, _lookupY + 1); - } - } - return _lookupResult; - } - - StateResult getStateElided(QPoint point, int w, StateRequestElided request) { - if (!_t->isNull() && point.y() >= 0 && request.lines > 0) { - _lookupRequest = request; - _lookupX = point.x(); - _lookupY = point.y(); - - _breakEverywhere = (_lookupRequest.flags & StateRequest::Flag::BreakEverywhere); - _lookupSymbol = (_lookupRequest.flags & StateRequest::Flag::LookupSymbol); - _lookupLink = (_lookupRequest.flags & StateRequest::Flag::LookupLink); - if (_lookupSymbol || (_lookupX >= 0 && _lookupX < w)) { - int yTo = _lookupY + 1; - if (yTo < 0 || (request.lines - 1) * _t->_st->font->height < yTo) { - yTo = request.lines * _t->_st->font->height; - _elideLast = true; - _elideRemoveFromEnd = request.removeFromEnd; - } - draw(0, 0, w, _lookupRequest.align, _lookupY, _lookupY + 1); - } - } - return _lookupResult; - } - -private: - void initNextParagraph(String::TextBlocks::const_iterator i) { - _parStartBlock = i; - const auto e = _t->_blocks.cend(); - if (i == e) { - _parStart = _t->_text.size(); - _parLength = 0; - } else { - _parStart = (*i)->from(); - for (; i != e; ++i) { - if ((*i)->type() == TextBlockTNewline) { - break; - } - } - _parLength = ((i == e) ? _t->_text.size() : (*i)->from()) - _parStart; - } - _parAnalysis.resize(0); - } - - void initParagraphBidi() { - if (!_parLength || !_parAnalysis.isEmpty()) return; - - String::TextBlocks::const_iterator i = _parStartBlock, e = _t->_blocks.cend(), n = i + 1; - - bool ignore = false; - bool rtl = (_parDirection == Qt::RightToLeft); - if (!ignore && !rtl) { - ignore = true; - const ushort *start = reinterpret_cast(_str) + _parStart; - const ushort *curr = start; - const ushort *end = start + _parLength; - while (curr < end) { - while (n != e && (*n)->from() <= _parStart + (curr - start)) { - i = n; - ++n; - } - const auto type = (*i)->type(); - if (type != TextBlockTEmoji - && type != TextBlockTCustomEmoji - && *curr >= 0x590) { - ignore = false; - break; - } - ++curr; - } - } - - _parAnalysis.resize(_parLength); - QScriptAnalysis *analysis = _parAnalysis.data(); - - BidiControl control(rtl); - - _parHasBidi = false; - if (ignore) { - memset(analysis, 0, _parLength * sizeof(QScriptAnalysis)); - if (rtl) { - for (int i = 0; i < _parLength; ++i) - analysis[i].bidiLevel = 1; - _parHasBidi = true; - } - } else { - _parHasBidi = eBidiItemize(analysis, control); - } - } - - bool drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterator &_endBlockIter, const String::TextBlocks::const_iterator &_end) { - _yDelta = (_lineHeight - _fontHeight) / 2; - if (_yTo >= 0 && (_y + _yDelta >= _yTo || _y >= _yTo)) return false; - if (_y + _yDelta + _fontHeight <= _yFrom) { - if (_lookupSymbol) { - _lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart; - _lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false; - } - return true; - } - - // Trimming pending spaces, because they sometimes don't fit on the line. - // They also are not counted in the line width, they're in the right padding. - // Line width is a sum of block / word widths and paddings between them, without trailing one. - auto trimmedLineEnd = _lineEnd; - for (; trimmedLineEnd > _lineStart; --trimmedLineEnd) { - auto ch = _t->_text[trimmedLineEnd - 1]; - if (ch != QChar::Space && ch != QChar::LineFeed) { - break; - } - } - - auto _endBlock = (_endBlockIter == _end) ? nullptr : _endBlockIter->get(); - auto elidedLine = _elideLast && (_y + _lineHeight >= _yToElide); - if (elidedLine) { - // If we decided to draw the last line elided only because of the skip block - // that did not fit on this line, we just draw the line till the very end. - // Skip block is ignored in the elided lines, instead "removeFromEnd" is used. - if (_endBlock && _endBlock->type() == TextBlockTSkip) { - _endBlock = nullptr; - } - if (!_endBlock) { - elidedLine = false; - } - } - - auto blockIndex = _lineStartBlock; - auto currentBlock = _t->_blocks[blockIndex].get(); - auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; - - const auto extendLeft = (currentBlock->from() < _lineStart) - ? qMin(_lineStart - currentBlock->from(), 2) - : 0; - _localFrom = _lineStart - extendLeft; - const auto extendedLineEnd = (_endBlock && _endBlock->from() < trimmedLineEnd && !elidedLine) - ? qMin(uint16(trimmedLineEnd + 2), _t->countBlockEnd(_endBlockIter, _end)) - : trimmedLineEnd; - - auto lineText = _t->_text.mid(_localFrom, extendedLineEnd - _localFrom); - auto lineStart = extendLeft; - auto lineLength = trimmedLineEnd - _lineStart; - - if (elidedLine) { - initParagraphBidi(); - prepareElidedLine(lineText, lineStart, lineLength, _endBlock); - } - - auto x = _x; - if (_align & Qt::AlignHCenter) { - x += (_wLeft / 2).toInt(); - } else if (((_align & Qt::AlignLeft) && _parDirection == Qt::RightToLeft) || ((_align & Qt::AlignRight) && _parDirection == Qt::LeftToRight)) { - x += _wLeft; - } - - if (!_p) { - if (_lookupX < x) { - if (_lookupSymbol) { - if (_parDirection == Qt::RightToLeft) { - _lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart; - _lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false; -// _lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockTSkip)) ? true : false; - } else { - _lookupResult.symbol = _lineStart; - _lookupResult.afterSymbol = false; -// _lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineStart > 0)) ? true : false; - } - } - if (_lookupLink) { - _lookupResult.link = nullptr; - } - _lookupResult.uponSymbol = false; - return false; - } else if (_lookupX >= x + (_w - _wLeft)) { - if (_parDirection == Qt::RightToLeft) { - _lookupResult.symbol = _lineStart; - _lookupResult.afterSymbol = false; -// _lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineStart > 0)) ? true : false; - } else { - _lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart; - _lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false; -// _lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockTSkip)) ? true : false; - } - if (_lookupLink) { - _lookupResult.link = nullptr; - } - _lookupResult.uponSymbol = false; - return false; - } - } - - if (_fullWidthSelection) { - const auto selectFromStart = (_selection.to > _lineStart) - && (_lineStart > 0) - && (_selection.from <= _lineStart); - const auto selectTillEnd = (_selection.to > trimmedLineEnd) - && (trimmedLineEnd < _t->_text.size()) - && (_selection.from <= trimmedLineEnd) - && (!_endBlock || _endBlock->type() != TextBlockTSkip); - - if ((selectFromStart && _parDirection == Qt::LeftToRight) - || (selectTillEnd && _parDirection == Qt::RightToLeft)) { - if (x > _x) { - fillSelectRange(_x, x); - } - } - if ((selectTillEnd && _parDirection == Qt::LeftToRight) - || (selectFromStart && _parDirection == Qt::RightToLeft)) { - if (x < _x + _wLeft) { - fillSelectRange(x + _w - _wLeft, _x + _w); - } - } - } - if (trimmedLineEnd == _lineStart && !elidedLine) { - return true; - } - - if (!elidedLine) initParagraphBidi(); // if was not inited - - _f = _t->_st->font; - QStackTextEngine engine(lineText, _f->f); - engine.option.setTextDirection(_parDirection); - _e = &engine; - - eItemize(); - - QScriptLine line; - line.from = lineStart; - line.length = lineLength; - eShapeLine(line); - - int firstItem = engine.findItem(line.from), lastItem = engine.findItem(line.from + line.length - 1); - int nItems = (firstItem >= 0 && lastItem >= firstItem) ? (lastItem - firstItem + 1) : 0; - if (!nItems) { - return true; - } - - int skipIndex = -1; - QVarLengthArray visualOrder(nItems); - QVarLengthArray levels(nItems); - for (int i = 0; i < nItems; ++i) { - auto &si = engine.layoutData->items[firstItem + i]; - while (nextBlock && nextBlock->from() <= _localFrom + si.position) { - currentBlock = nextBlock; - nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; - } - auto _type = currentBlock->type(); - if (_type == TextBlockTSkip) { - levels[i] = si.analysis.bidiLevel = 0; - skipIndex = i; - } else { - levels[i] = si.analysis.bidiLevel; - } - if (si.analysis.flags == QScriptAnalysis::Object) { - if (_type == TextBlockTEmoji - || _type == TextBlockTCustomEmoji - || _type == TextBlockTSkip) { - si.width = currentBlock->f_width() - + (nextBlock == _endBlock && (!nextBlock || nextBlock->from() >= trimmedLineEnd) - ? 0 - : currentBlock->f_rpadding()); - } - } - } - QTextEngine::bidiReorder(nItems, levels.data(), visualOrder.data()); - if (style::RightToLeft() && skipIndex == nItems - 1) { - for (int32 i = nItems; i > 1;) { - --i; - visualOrder[i] = visualOrder[i - 1]; - } - visualOrder[0] = skipIndex; - } - - blockIndex = _lineStartBlock; - currentBlock = _t->_blocks[blockIndex].get(); - nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; - - int32 textY = _y + _yDelta + _t->_st->font->ascent, emojiY = (_t->_st->font->height - st::emojiSize) / 2; - - applyBlockProperties(currentBlock); - for (int i = 0; i < nItems; ++i) { - int item = firstItem + visualOrder[i]; - const QScriptItem &si = engine.layoutData->items.at(item); - bool rtl = (si.analysis.bidiLevel % 2); - - while (blockIndex > _lineStartBlock + 1 && _t->_blocks[blockIndex - 1]->from() > _localFrom + si.position) { - nextBlock = currentBlock; - currentBlock = _t->_blocks[--blockIndex - 1].get(); - applyBlockProperties(currentBlock); - } - while (nextBlock && nextBlock->from() <= _localFrom + si.position) { - currentBlock = nextBlock; - nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; - applyBlockProperties(currentBlock); - } - if (si.analysis.flags >= QScriptAnalysis::TabOrObject) { - TextBlockType _type = currentBlock->type(); - if (!_p && _lookupX >= x && _lookupX < x + si.width) { // _lookupRequest - if (_lookupLink) { - if (_lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) { - const auto spoilerLink = _t->spoilerLink(currentBlock->spoilerIndex()); - const auto resultLink = (spoilerLink || !currentBlock->lnkIndex()) - ? spoilerLink - : _t->_links.at(currentBlock->lnkIndex() - 1); - if (resultLink) { - _lookupResult.link = resultLink; - } - } - } - if (_type != TextBlockTSkip) { - _lookupResult.uponSymbol = true; - } - if (_lookupSymbol) { - if (_type == TextBlockTSkip) { - if (_parDirection == Qt::RightToLeft) { - _lookupResult.symbol = _lineStart; - _lookupResult.afterSymbol = false; - } else { - _lookupResult.symbol = (trimmedLineEnd > _lineStart) ? (trimmedLineEnd - 1) : _lineStart; - _lookupResult.afterSymbol = (trimmedLineEnd > _lineStart) ? true : false; - } - return false; - } - - // Emoji with spaces after symbol lookup - auto chFrom = _str + currentBlock->from(); - auto chTo = chFrom + ((nextBlock ? nextBlock->from() : _t->_text.size()) - currentBlock->from()); - auto spacesWidth = (si.width - currentBlock->f_width()); - auto spacesCount = 0; - while (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space) { - ++spacesCount; - --chTo; - } - if (spacesCount > 0) { // Check if we're over a space. - if (rtl) { - if (_lookupX < x + spacesWidth) { - _lookupResult.symbol = (chTo - _str); // up to a space, included, rtl - _lookupResult.afterSymbol = (_lookupX < x + (spacesWidth / 2)) ? true : false; - return false; - } - } else if (_lookupX >= x + si.width - spacesWidth) { - _lookupResult.symbol = (chTo - _str); // up to a space, inclided, ltr - _lookupResult.afterSymbol = (_lookupX >= x + si.width - spacesWidth + (spacesWidth / 2)) ? true : false; - return false; - } - } - if (_lookupX < x + (rtl ? (si.width - currentBlock->f_width()) : 0) + (currentBlock->f_width() / 2)) { - _lookupResult.symbol = ((rtl && chTo > chFrom) ? (chTo - 1) : chFrom) - _str; - _lookupResult.afterSymbol = (rtl && chTo > chFrom) ? true : false; - } else { - _lookupResult.symbol = ((rtl || chTo <= chFrom) ? chFrom : (chTo - 1)) - _str; - _lookupResult.afterSymbol = (rtl || chTo <= chFrom) ? false : true; - } - } - return false; - } else if (_p && (_type == TextBlockTEmoji || _type == TextBlockTCustomEmoji)) { - auto glyphX = x; - auto spacesWidth = (si.width - currentBlock->f_width()); - if (rtl) { - glyphX += spacesWidth; - } - struct { - QFixed from; - QFixed to; - } fillSelect; - struct { - QFixed from; - QFixed width; - } fillSpoiler; - if (_background.selectActiveBlock) { - fillSelect = { x, x + si.width }; - } else if (_localFrom + si.position < _selection.to) { - auto chFrom = _str + currentBlock->from(); - auto chTo = chFrom + ((nextBlock ? nextBlock->from() : _t->_text.size()) - currentBlock->from()); - if (_localFrom + si.position >= _selection.from) { // could be without space - if (chTo == chFrom || (chTo - 1)->unicode() != QChar::Space || _selection.to >= (chTo - _str)) { - fillSelect = { x, x + si.width }; - } else { // or with space - fillSelect = { glyphX, glyphX + currentBlock->f_width() }; - } - } else if (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space && (chTo - 1 - _str) >= _selection.from) { - if (rtl) { // rtl space only - fillSelect = { x, glyphX }; - } else { // ltr space only - fillSelect = { x + currentBlock->f_width(), x + si.width }; - } - } - } - const auto hasSpoiler = _background.color && - (_background.inFront || _background.startMs); - if (hasSpoiler) { - fillSpoiler = { x, si.width }; - } - const auto spoilerOpacity = hasSpoiler - ? fillSpoilerOpacity() - : 0.; - const auto hasSelect = fillSelect.to != QFixed(); - if (hasSelect) { - fillSelectRange(fillSelect.from, fillSelect.to); - } - const auto opacity = _p->opacity(); - if (spoilerOpacity < 1.) { - if (hasSpoiler) { - _p->setOpacity(opacity * (1. - spoilerOpacity)); - } - const auto x = (glyphX + st::emojiPadding).toInt(); - const auto y = _y + _yDelta + emojiY; - if (_type == TextBlockTEmoji) { - Emoji::Draw( - *_p, - static_cast(currentBlock)->_emoji, - Emoji::GetSizeNormal(), - x, - y); - } else if (const auto custom = static_cast(currentBlock)->_custom.get()) { - if (!_now) { - _now = crl::now(); - } - if (!_customEmojiSize) { - _customEmojiSize = AdjustCustomEmojiSize(st::emojiSize); - _customEmojiSkip = (st::emojiSize - _customEmojiSize) / 2; - } - custom->paint(*_p, { - .preview = _textPalette->spoilerActiveBg->c, - .now = _now, - .position = { x + _customEmojiSkip, y + _customEmojiSkip }, - .paused = _p->inactive(), - }); - } - } - if (hasSpoiler) { - _p->setOpacity(opacity * spoilerOpacity); - fillSpoilerRange( - fillSpoiler.from, - fillSpoiler.width, - blockIndex, - currentBlock->from(), - (nextBlock ? nextBlock->from() : _t->_text.size())); - _p->setOpacity(opacity); - } -// } else if (_p && currentBlock->type() == TextBlockSkip) { // debug -// _p->fillRect(QRect(x.toInt(), _y, currentBlock->width(), static_cast(currentBlock)->height()), QColor(0, 0, 0, 32)); - } - x += si.width; - continue; - } - - unsigned short *logClusters = engine.logClusters(&si); - QGlyphLayout glyphs = engine.shapedGlyphs(&si); - - int itemStart = qMax(line.from, si.position), itemEnd; - int itemLength = engine.length(item); - int glyphsStart = logClusters[itemStart - si.position], glyphsEnd; - if (line.from + line.length < si.position + itemLength) { - itemEnd = line.from + line.length; - glyphsEnd = logClusters[itemEnd - si.position]; - } else { - itemEnd = si.position + itemLength; - glyphsEnd = si.num_glyphs; - } - - QFixed itemWidth = 0; - for (int g = glyphsStart; g < glyphsEnd; ++g) - itemWidth += glyphs.effectiveAdvance(g); - - if (!_p && _lookupX >= x && _lookupX < x + itemWidth) { // _lookupRequest - if (_lookupLink) { - if (_lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) { - const auto spoilerLink = _t->spoilerLink(currentBlock->spoilerIndex()); - const auto resultLink = (spoilerLink || !currentBlock->lnkIndex()) - ? spoilerLink - : _t->_links.at(currentBlock->lnkIndex() - 1); - if (resultLink) { - _lookupResult.link = resultLink; - } - } - } - _lookupResult.uponSymbol = true; - if (_lookupSymbol) { - QFixed tmpx = rtl ? (x + itemWidth) : x; - for (int ch = 0, g, itemL = itemEnd - itemStart; ch < itemL;) { - g = logClusters[itemStart - si.position + ch]; - QFixed gwidth = glyphs.effectiveAdvance(g); - // ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes - int ch2 = ch + 1; - while ((ch2 < itemL) && (g == logClusters[itemStart - si.position + ch2])) { - ++ch2; - } - for (int charsCount = (ch2 - ch); ch < ch2; ++ch) { - QFixed shift1 = QFixed(2 * (charsCount - (ch2 - ch)) + 2) * gwidth / QFixed(2 * charsCount), - shift2 = QFixed(2 * (charsCount - (ch2 - ch)) + 1) * gwidth / QFixed(2 * charsCount); - if ((rtl && _lookupX >= tmpx - shift1) || - (!rtl && _lookupX < tmpx + shift1)) { - _lookupResult.symbol = _localFrom + itemStart + ch; - if ((rtl && _lookupX >= tmpx - shift2) || - (!rtl && _lookupX < tmpx + shift2)) { - _lookupResult.afterSymbol = false; - } else { - _lookupResult.afterSymbol = true; - } - return false; - } - } - if (rtl) { - tmpx -= gwidth; - } else { - tmpx += gwidth; - } - } - if (itemEnd > itemStart) { - _lookupResult.symbol = _localFrom + itemEnd - 1; - _lookupResult.afterSymbol = true; - } else { - _lookupResult.symbol = _localFrom + itemStart; - _lookupResult.afterSymbol = false; - } - } - return false; - } else if (_p) { - QTextItemInt gf; - gf.glyphs = glyphs.mid(glyphsStart, glyphsEnd - glyphsStart); - gf.f = &_e->fnt; - gf.chars = engine.layoutData->string.unicode() + itemStart; - gf.num_chars = itemEnd - itemStart; - gf.fontEngine = engine.fontEngine(si); - gf.logClusters = logClusters + itemStart - si.position; - gf.width = itemWidth; - gf.justified = false; - InitTextItemWithScriptItem(gf, si); - - auto hasSelected = false; - auto hasNotSelected = true; - auto selectedRect = QRect(); - if (_background.selectActiveBlock) { - fillSelectRange(x, x + itemWidth); - } else if (_localFrom + itemStart < _selection.to && _localFrom + itemEnd > _selection.from) { - hasSelected = true; - auto selX = x; - auto selWidth = itemWidth; - if (_localFrom + itemStart >= _selection.from && _localFrom + itemEnd <= _selection.to) { - hasNotSelected = false; - } else { - selWidth = 0; - int itemL = itemEnd - itemStart; - int selStart = _selection.from - (_localFrom + itemStart), selEnd = _selection.to - (_localFrom + itemStart); - if (selStart < 0) selStart = 0; - if (selEnd > itemL) selEnd = itemL; - for (int ch = 0, g; ch < selEnd;) { - g = logClusters[itemStart - si.position + ch]; - QFixed gwidth = glyphs.effectiveAdvance(g); - // ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes - int ch2 = ch + 1; - while ((ch2 < itemL) && (g == logClusters[itemStart - si.position + ch2])) { - ++ch2; - } - if (ch2 <= selStart) { - selX += gwidth; - } else if (ch >= selStart && ch2 <= selEnd) { - selWidth += gwidth; - } else { - int sStart = ch, sEnd = ch2; - if (ch < selStart) { - sStart = selStart; - selX += QFixed(sStart - ch) * gwidth / QFixed(ch2 - ch); - } - if (ch2 >= selEnd) { - sEnd = selEnd; - selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch); - break; - } - selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch); - } - ch = ch2; - } - } - if (rtl) selX = x + itemWidth - (selX - x) - selWidth; - selectedRect = QRect(selX.toInt(), _y + _yDelta, (selX + selWidth).toInt() - selX.toInt(), _fontHeight); - fillSelectRange(selX, selX + selWidth); - } - const auto hasSpoiler = (_background.inFront || _background.startMs); - const auto spoilerOpacity = hasSpoiler - ? fillSpoilerOpacity() - : 0.; - const auto opacity = _p->opacity(); - const auto isElidedBlock = (!rtl) - && (_indexOfElidedBlock == blockIndex); - if ((spoilerOpacity < 1.) || isElidedBlock) { - if (hasSpoiler && !isElidedBlock) { - _p->setOpacity(opacity * (1. - spoilerOpacity)); - } - if (Q_UNLIKELY(hasSelected)) { - if (Q_UNLIKELY(hasNotSelected)) { - // There is a bug in retina QPainter clipping stack. - // You can see glitches in rendering in such text: - // aA - // Aa - // Where selection is both 'A'-s. - // I can't debug it right now, this is a workaround. -#ifdef Q_OS_MAC - _p->save(); -#endif // Q_OS_MAC - const auto clippingEnabled = _p->hasClipping(); - const auto clippingRegion = _p->clipRegion(); - _p->setClipRect(selectedRect, Qt::IntersectClip); - _p->setPen(*_currentPenSelected); - _p->drawTextItem(QPointF(x.toReal(), textY), gf); - const auto externalClipping = clippingEnabled - ? clippingRegion - : QRegion(QRect( - (_x - _w).toInt(), - _y - _lineHeight, - (_x + 2 * _w).toInt(), - _y + 2 * _lineHeight)); - _p->setClipRegion(externalClipping - selectedRect); - _p->setPen(*_currentPen); - _p->drawTextItem(QPointF(x.toReal(), textY), gf); -#ifdef Q_OS_MAC - _p->restore(); -#else // Q_OS_MAC - if (clippingEnabled) { - _p->setClipRegion(clippingRegion); - } else { - _p->setClipping(false); - } -#endif // Q_OS_MAC - } else { - _p->setPen(*_currentPenSelected); - _p->drawTextItem(QPointF(x.toReal(), textY), gf); - } - } else { - _p->setPen(*_currentPen); - _p->drawTextItem(QPointF(x.toReal(), textY), gf); - } - } - - if (hasSpoiler) { - _p->setOpacity(opacity * spoilerOpacity); - fillSpoilerRange( - x, - itemWidth, - blockIndex, - _localFrom + itemStart, - _localFrom + itemEnd); - _p->setOpacity(opacity); - } - } - - x += itemWidth; - } - return true; - } - void fillSelectRange(QFixed from, QFixed to) { - auto left = from.toInt(); - auto width = to.toInt() - left; - _p->fillRect(left, _y + _yDelta, width, _fontHeight, _textPalette->selectBg); - } - - float fillSpoilerOpacity() { - if (!_background.startMs) { - return 1.; - } - if (!_now) { - _now = crl::now(); - } - const auto progress = float64(_now - _background.startMs) - / st::fadeWrapDuration; - if ((progress > 1.) && _background.spoilerIndex && _t->_spoiler) { - const auto link = _t->_spoiler->links.at( - _background.spoilerIndex - 1); - if (link) { - link->setStartMs(0); - } - } - return (1. - std::min(progress, 1.)); - } - - void fillSpoilerRange( - QFixed x, - QFixed width, - int currentBlockIndex, - int positionFrom, - int positionTill) { - if (!_background.color) { - return; - } - if (!_t->_spoiler) { - return; - } - const auto elideOffset = (_indexOfElidedBlock == currentBlockIndex) - ? (_elideRemoveFromEnd + _f->elidew) - : 0; - - const auto parts = [&] { - const auto blockIndex = currentBlockIndex - 1; - const auto block = _t->_blocks[blockIndex].get(); - const auto nextBlock = (blockIndex + 1 < _t->_blocks.size()) - ? _t->_blocks[blockIndex + 1].get() - : nullptr; - const auto blockEnd = nextBlock ? nextBlock->from() : _t->_text.size(); - const auto now = block->spoilerIndex(); - const auto was = (positionFrom > block->from()) - ? now - : (blockIndex > 0) - ? _t->_blocks[blockIndex - 1]->spoilerIndex() - : 0; - const auto will = elideOffset - ? 0 - : (positionTill < blockEnd) - ? now - : nextBlock - ? nextBlock->spoilerIndex() - : 0; - return RectPart::None - | ((now != was) ? (RectPart::FullLeft) : RectPart::None) - | ((now != will) ? (RectPart::FullRight) : RectPart::None); - }(); - const auto hasLeft = (parts & RectPart::Left) != 0; - const auto hasRight = (parts & RectPart::Right) != 0; - - const auto &cache = _background.inFront - ? _t->_spoiler->spoilerCache - : _t->_spoiler->spoilerShownCache; - const auto cornerWidth = cache.corners[0].width() - / style::DevicePixelRatio(); - const auto useWidth = ((x + width).toInt() - x.toInt()) - elideOffset; - const auto rect = QRect( - x.toInt(), - _y + _yDelta, - std::max( - useWidth, - (hasRight ? cornerWidth : 0) + (hasLeft ? cornerWidth : 0)), - _fontHeight); - if (!rect.isValid()) { - return; - } - - if (parts != RectPart::None) { - DrawRoundedRect( - *_p, - rect, - *_background.color, - cache.corners, - parts); - } - _p->fillRect( - rect.left() + (hasLeft ? cornerWidth : 0), - rect.top(), - rect.width() - - (hasRight ? cornerWidth : 0) - - (hasLeft ? cornerWidth : 0), - rect.height(), - *_background.color); - } - - void elideSaveBlock(int32 blockIndex, const AbstractBlock *&_endBlock, int32 elideStart, int32 elideWidth) { - if (_elideSavedBlock) { - restoreAfterElided(); - } - - _elideSavedIndex = blockIndex; - auto mutableText = const_cast(_t); - _elideSavedBlock = std::move(mutableText->_blocks[blockIndex]); - mutableText->_blocks[blockIndex] = Block::Text(_t->_st->font, _t->_text, QFIXED_MAX, elideStart, 0, (*_elideSavedBlock)->flags(), (*_elideSavedBlock)->lnkIndex(), (*_elideSavedBlock)->spoilerIndex()); - _blocksSize = blockIndex + 1; - _endBlock = (blockIndex + 1 < _t->_blocks.size() ? _t->_blocks[blockIndex + 1].get() : nullptr); - } - - void setElideBidi(int32 elideStart, int32 elideLen) { - int32 newParLength = elideStart + elideLen - _parStart; - if (newParLength > _parAnalysis.size()) { - _parAnalysis.resize(newParLength); - } - for (int32 i = elideLen; i > 0; --i) { - _parAnalysis[newParLength - i].bidiLevel = (_parDirection == Qt::RightToLeft) ? 1 : 0; - } - } - - void prepareElidedLine(QString &lineText, int32 lineStart, int32 &lineLength, const AbstractBlock *&_endBlock, int repeat = 0) { - _f = _t->_st->font; - QStackTextEngine engine(lineText, _f->f); - engine.option.setTextDirection(_parDirection); - _e = &engine; - - eItemize(); - - auto blockIndex = _lineStartBlock; - auto currentBlock = _t->_blocks[blockIndex].get(); - auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; - - QScriptLine line; - line.from = lineStart; - line.length = lineLength; - eShapeLine(line); - - auto elideWidth = _f->elidew; - _wLeft = _w - elideWidth - _elideRemoveFromEnd; - - int firstItem = engine.findItem(line.from), lastItem = engine.findItem(line.from + line.length - 1); - int nItems = (firstItem >= 0 && lastItem >= firstItem) ? (lastItem - firstItem + 1) : 0, i; - - for (i = 0; i < nItems; ++i) { - QScriptItem &si(engine.layoutData->items[firstItem + i]); - while (nextBlock && nextBlock->from() <= _localFrom + si.position) { - currentBlock = nextBlock; - nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; - } - TextBlockType _type = currentBlock->type(); - if (si.analysis.flags == QScriptAnalysis::Object) { - if (_type == TextBlockTEmoji - || _type == TextBlockTCustomEmoji - || _type == TextBlockTSkip) { - si.width = currentBlock->f_width() + currentBlock->f_rpadding(); - } - } - if (_type == TextBlockTEmoji - || _type == TextBlockTCustomEmoji - || _type == TextBlockTSkip - || _type == TextBlockTNewline) { - if (_wLeft < si.width) { - lineText = lineText.mid(0, currentBlock->from() - _localFrom) + kQEllipsis; - lineLength = currentBlock->from() + kQEllipsis.size() - _lineStart; - _selection.to = qMin(_selection.to, currentBlock->from()); - _indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0); - setElideBidi(currentBlock->from(), kQEllipsis.size()); - elideSaveBlock(blockIndex - 1, _endBlock, currentBlock->from(), elideWidth); - return; - } - _wLeft -= si.width; - } else if (_type == TextBlockTText) { - unsigned short *logClusters = engine.logClusters(&si); - QGlyphLayout glyphs = engine.shapedGlyphs(&si); - - int itemStart = qMax(line.from, si.position), itemEnd; - int itemLength = engine.length(firstItem + i); - int glyphsStart = logClusters[itemStart - si.position], glyphsEnd; - if (line.from + line.length < si.position + itemLength) { - itemEnd = line.from + line.length; - glyphsEnd = logClusters[itemEnd - si.position]; - } else { - itemEnd = si.position + itemLength; - glyphsEnd = si.num_glyphs; - } - - for (auto g = glyphsStart; g < glyphsEnd; ++g) { - auto adv = glyphs.effectiveAdvance(g); - if (_wLeft < adv) { - auto pos = itemStart; - while (pos < itemEnd && logClusters[pos - si.position] < g) { - ++pos; - } - - if (lineText.size() <= pos || repeat > 3) { - lineText += kQEllipsis; - lineLength = _localFrom + pos + kQEllipsis.size() - _lineStart; - _selection.to = qMin(_selection.to, uint16(_localFrom + pos)); - _indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0); - setElideBidi(_localFrom + pos, kQEllipsis.size()); - _blocksSize = blockIndex; - _endBlock = nextBlock; - } else { - lineText = lineText.mid(0, pos); - lineLength = _localFrom + pos - _lineStart; - _blocksSize = blockIndex; - _endBlock = nextBlock; - prepareElidedLine(lineText, lineStart, lineLength, _endBlock, repeat + 1); - } - return; - } else { - _wLeft -= adv; - } - } - } - } - - int32 elideStart = _localFrom + lineText.size(); - _selection.to = qMin(_selection.to, uint16(elideStart)); - _indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0); - setElideBidi(elideStart, kQEllipsis.size()); - - lineText += kQEllipsis; - lineLength += kQEllipsis.size(); - - if (!repeat) { - for (; blockIndex < _blocksSize && _t->_blocks[blockIndex].get() != _endBlock && _t->_blocks[blockIndex]->from() < elideStart; ++blockIndex) { - } - if (blockIndex < _blocksSize) { - elideSaveBlock(blockIndex, _endBlock, elideStart, elideWidth); - } - } - } - - void restoreAfterElided() { - if (_elideSavedBlock) { - const_cast(_t)->_blocks[_elideSavedIndex] = std::move(*_elideSavedBlock); - } - } - - // COPIED FROM qtextengine.cpp AND MODIFIED - void eShapeLine(const QScriptLine &line) { - int item = _e->findItem(line.from); - if (item == -1) - return; - - auto end = _e->findItem(line.from + line.length - 1, item); - auto blockIndex = _lineStartBlock; - auto currentBlock = _t->_blocks[blockIndex].get(); - auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; - eSetFont(currentBlock); - for (; item <= end; ++item) { - QScriptItem &si = _e->layoutData->items[item]; - while (nextBlock && nextBlock->from() <= _localFrom + si.position) { - currentBlock = nextBlock; - nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; - eSetFont(currentBlock); - } - _e->shape(item); - } - } - - style::font applyFlags(int32 flags, const style::font &f) { - if (!flags) { - return f; - } - auto result = f; - if (IsMono(flags)) { - result = result->monospace(); - } else { - if (flags & TextBlockFBold) { - result = result->bold(); - } else if (flags & TextBlockFSemibold) { - result = result->semibold(); - } - if (flags & TextBlockFItalic) result = result->italic(); - if (flags & TextBlockFUnderline) result = result->underline(); - if (flags & TextBlockFStrikeOut) result = result->strikeout(); - if (flags & TextBlockFTilde) { // tilde fix in OpenSans - result = result->semibold(); - } - } - return result; - } - - void eSetFont(const AbstractBlock *block) { - const auto flags = block->flags(); - const auto usedFont = [&] { - if (const auto index = block->lnkIndex()) { - const auto active = ClickHandler::showAsActive( - _t->_links.at(index - 1) - ) || (_textPalette && _textPalette->linkAlwaysActive > 0); - return active - ? _t->_st->linkFontOver - : _t->_st->linkFont; - } - return _t->_st->font; - }(); - const auto newFont = applyFlags(flags, usedFont); - if (newFont != _f) { - _f = (newFont->family() == _t->_st->font->family()) - ? applyFlags(flags | newFont->flags(), _t->_st->font) - : newFont; - _e->fnt = _f->f; - _e->resetFontEngineCache(); - } - } - - void eItemize() { - _e->validate(); - if (_e->layoutData->items.size()) - return; - - int length = _e->layoutData->string.length(); - if (!length) - return; - - const ushort *string = reinterpret_cast(_e->layoutData->string.unicode()); - - auto blockIndex = _lineStartBlock; - auto currentBlock = _t->_blocks[blockIndex].get(); - auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; - - _e->layoutData->hasBidi = _parHasBidi; - auto analysis = _parAnalysis.data() + (_localFrom - _parStart); - - { -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - QUnicodeTools::ScriptItemArray scriptItems; - QUnicodeTools::initScripts(_e->layoutData->string, &scriptItems); - for (int i = 0; i < scriptItems.length(); ++i) { - const auto &item = scriptItems.at(i); - int end = i < scriptItems.length() - 1 ? scriptItems.at(i + 1).position : length; - for (int j = item.position; j < end; ++j) - analysis[j].script = item.script; - } -#else // Qt >= 6.0.0 - QVarLengthArray scripts(length); - QUnicodeTools::initScripts(string, length, scripts.data()); - for (int i = 0; i < length; ++i) - analysis[i].script = scripts.at(i); -#endif // Qt < 6.0.0 - } - - blockIndex = _lineStartBlock; - currentBlock = _t->_blocks[blockIndex].get(); - nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; - - auto start = string; - auto end = start + length; - while (start < end) { - while (nextBlock && nextBlock->from() <= _localFrom + (start - string)) { - currentBlock = nextBlock; - nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; - } - auto _type = currentBlock->type(); - if (_type == TextBlockTEmoji - || _type == TextBlockTCustomEmoji - || _type == TextBlockTSkip) { - analysis->script = QChar::Script_Common; - analysis->flags = QScriptAnalysis::Object; - } else { - analysis->flags = QScriptAnalysis::None; - } -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - analysis->script = hbscript_to_script(script_to_hbscript(analysis->script)); // retain the old behavior -#endif // Qt < 6.0.0 - ++start; - ++analysis; - } - - { - auto i_string = &_e->layoutData->string; - auto i_analysis = _parAnalysis.data() + (_localFrom - _parStart); - auto i_items = &_e->layoutData->items; - - blockIndex = _lineStartBlock; - currentBlock = _t->_blocks[blockIndex].get(); - nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; - auto startBlock = currentBlock; - - if (!length) { - return; - } - auto start = 0; - auto end = start + length; - for (int i = start + 1; i < end; ++i) { - while (nextBlock && nextBlock->from() <= _localFrom + i) { - currentBlock = nextBlock; - nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; - } - // According to the unicode spec we should be treating characters in the Common script - // (punctuation, spaces, etc) as being the same script as the surrounding text for the - // purpose of splitting up text. This is important because, for example, a fullstop - // (0x2E) can be used to indicate an abbreviation and so must be treated as part of a - // word. Thus it must be passed along with the word in languages that have to calculate - // word breaks. For example the thai word "ครม." has no word breaks but the word "ครม" - // does. - // Unfortuntely because we split up the strings for both wordwrapping and for setting - // the font and because Japanese and Chinese are also aliases of the script "Common", - // doing this would break too many things. So instead we only pass the full stop - // along, and nothing else. - if (currentBlock == startBlock - && i_analysis[i].bidiLevel == i_analysis[start].bidiLevel - && i_analysis[i].flags == i_analysis[start].flags - && (i_analysis[i].script == i_analysis[start].script || i_string->at(i) == QLatin1Char('.')) -// && i_analysis[i].flags < QScriptAnalysis::SpaceTabOrObject // only emojis are objects here, no tabs - && i - start < _MaxItemLength) - continue; - i_items->append(QScriptItem(start, i_analysis[start])); - start = i; - startBlock = currentBlock; - } - i_items->append(QScriptItem(start, i_analysis[start])); - } - } - - QChar::Direction eSkipBoundryNeutrals(QScriptAnalysis *analysis, - const ushort *unicode, - int &sor, int &eor, BidiControl &control, - String::TextBlocks::const_iterator i) { - String::TextBlocks::const_iterator e = _t->_blocks.cend(), n = i + 1; - - QChar::Direction dir = control.basicDirection(); - int level = sor > 0 ? analysis[sor - 1].bidiLevel : control.level; - while (sor <= _parLength) { - while (i != _parStartBlock && (*i)->from() > _parStart + sor) { - n = i; - --i; - } - while (n != e && (*n)->from() <= _parStart + sor) { - i = n; - ++n; - } - - TextBlockType _itype = (*i)->type(); - if (eor == _parLength) - dir = control.basicDirection(); - else if (_itype == TextBlockTEmoji - || _itype == TextBlockTCustomEmoji) - dir = QChar::DirCS; - else if (_itype == TextBlockTSkip) - dir = QChar::DirCS; - else - dir = QChar::direction(unicode[sor]); - // Keep skipping DirBN as if it doesn't exist - if (dir != QChar::DirBN) - break; - analysis[sor++].bidiLevel = level; - } - - eor = sor; - - return dir; - } - - // creates the next QScript items. - bool eBidiItemize(QScriptAnalysis *analysis, BidiControl &control) { - bool rightToLeft = (control.basicDirection() == 1); - bool hasBidi = rightToLeft; - - int sor = 0; - int eor = -1; - - const ushort *unicode = reinterpret_cast(_t->_text.unicode()) + _parStart; - int current = 0; - - QChar::Direction dir = rightToLeft ? QChar::DirR : QChar::DirL; - BidiStatus status; - - String::TextBlocks::const_iterator i = _parStartBlock, e = _t->_blocks.cend(), n = i + 1; - - QChar::Direction sdir; - TextBlockType _stype = (*_parStartBlock)->type(); - if (_stype == TextBlockTEmoji || _stype == TextBlockTCustomEmoji) - sdir = QChar::DirCS; - else if (_stype == TextBlockTSkip) - sdir = QChar::DirCS; - else - sdir = QChar::direction(*unicode); - if (sdir != QChar::DirL && sdir != QChar::DirR && sdir != QChar::DirEN && sdir != QChar::DirAN) - sdir = QChar::DirON; - else - dir = QChar::DirON; - - status.eor = sdir; - status.lastStrong = rightToLeft ? QChar::DirR : QChar::DirL; - status.last = status.lastStrong; - status.dir = sdir; - - while (current <= _parLength) { - while (n != e && (*n)->from() <= _parStart + current) { - i = n; - ++n; - } - - QChar::Direction dirCurrent; - TextBlockType _itype = (*i)->type(); - if (current == (int)_parLength) - dirCurrent = control.basicDirection(); - else if (_itype == TextBlockTEmoji - || _itype == TextBlockTCustomEmoji) - dirCurrent = QChar::DirCS; - else if (_itype == TextBlockTSkip) - dirCurrent = QChar::DirCS; - else - dirCurrent = QChar::direction(unicode[current]); - - switch (dirCurrent) { - - // embedding and overrides (X1-X9 in the BiDi specs) - case QChar::DirRLE: - case QChar::DirRLO: - case QChar::DirLRE: - case QChar::DirLRO: - { - bool rtl = (dirCurrent == QChar::DirRLE || dirCurrent == QChar::DirRLO); - hasBidi |= rtl; - bool override = (dirCurrent == QChar::DirLRO || dirCurrent == QChar::DirRLO); - - unsigned int level = control.level+1; - if ((level%2 != 0) == rtl) ++level; - if (level < _MaxBidiLevel) { - eor = current-1; - eAppendItems(analysis, sor, eor, control, dir); - eor = current; - control.embed(rtl, override); - QChar::Direction edir = (rtl ? QChar::DirR : QChar::DirL); - dir = status.eor = edir; - status.lastStrong = edir; - } - break; - } - case QChar::DirPDF: - { - if (control.canPop()) { - if (dir != control.direction()) { - eor = current-1; - eAppendItems(analysis, sor, eor, control, dir); - dir = control.direction(); - } - eor = current; - eAppendItems(analysis, sor, eor, control, dir); - control.pdf(); - dir = QChar::DirON; status.eor = QChar::DirON; - status.last = control.direction(); - if (control.override) - dir = control.direction(); - else - dir = QChar::DirON; - status.lastStrong = control.direction(); - } - break; - } - - // strong types - case QChar::DirL: - if(dir == QChar::DirON) - dir = QChar::DirL; - switch(status.last) - { - case QChar::DirL: - eor = current; status.eor = QChar::DirL; break; - case QChar::DirR: - case QChar::DirAL: - case QChar::DirEN: - case QChar::DirAN: - if (eor >= 0) { - eAppendItems(analysis, sor, eor, control, dir); - status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i); - } else { - eor = current; status.eor = dir; - } - break; - case QChar::DirES: - case QChar::DirET: - case QChar::DirCS: - case QChar::DirBN: - case QChar::DirB: - case QChar::DirS: - case QChar::DirWS: - case QChar::DirON: - if(dir != QChar::DirL) { - //last stuff takes embedding dir - if(control.direction() == QChar::DirR) { - if(status.eor != QChar::DirR) { - // AN or EN - eAppendItems(analysis, sor, eor, control, dir); - status.eor = QChar::DirON; - dir = QChar::DirR; - } - eor = current - 1; - eAppendItems(analysis, sor, eor, control, dir); - status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i); - } else { - if(status.eor != QChar::DirL) { - eAppendItems(analysis, sor, eor, control, dir); - status.eor = QChar::DirON; - dir = QChar::DirL; - } else { - eor = current; status.eor = QChar::DirL; break; - } - } - } else { - eor = current; status.eor = QChar::DirL; - } - default: - break; - } - status.lastStrong = QChar::DirL; - break; - case QChar::DirAL: - case QChar::DirR: - hasBidi = true; - if(dir == QChar::DirON) dir = QChar::DirR; - switch(status.last) - { - case QChar::DirL: - case QChar::DirEN: - case QChar::DirAN: - if (eor >= 0) - eAppendItems(analysis, sor, eor, control, dir); - // fall through - case QChar::DirR: - case QChar::DirAL: - dir = QChar::DirR; eor = current; status.eor = QChar::DirR; break; - case QChar::DirES: - case QChar::DirET: - case QChar::DirCS: - case QChar::DirBN: - case QChar::DirB: - case QChar::DirS: - case QChar::DirWS: - case QChar::DirON: - if(status.eor != QChar::DirR && status.eor != QChar::DirAL) { - //last stuff takes embedding dir - if(control.direction() == QChar::DirR - || status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) { - eAppendItems(analysis, sor, eor, control, dir); - dir = QChar::DirR; status.eor = QChar::DirON; - eor = current; - } else { - eor = current - 1; - eAppendItems(analysis, sor, eor, control, dir); - dir = QChar::DirR; status.eor = QChar::DirON; - } - } else { - eor = current; status.eor = QChar::DirR; - } - default: - break; - } - status.lastStrong = dirCurrent; - break; - - // weak types: - - case QChar::DirNSM: - if (eor == current-1) - eor = current; - break; - case QChar::DirEN: - // if last strong was AL change EN to AN - if(status.lastStrong != QChar::DirAL) { - if(dir == QChar::DirON) { - if(status.lastStrong == QChar::DirL) - dir = QChar::DirL; - else - dir = QChar::DirEN; - } - switch(status.last) - { - case QChar::DirET: - if (status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) { - eAppendItems(analysis, sor, eor, control, dir); - status.eor = QChar::DirON; - dir = QChar::DirAN; - } - [[fallthrough]]; - case QChar::DirEN: - case QChar::DirL: - eor = current; - status.eor = dirCurrent; - break; - case QChar::DirR: - case QChar::DirAL: - case QChar::DirAN: - if (eor >= 0) - eAppendItems(analysis, sor, eor, control, dir); - else - eor = current; - status.eor = QChar::DirEN; - dir = QChar::DirAN; - break; - case QChar::DirES: - case QChar::DirCS: - if(status.eor == QChar::DirEN || dir == QChar::DirAN) { - eor = current; break; - } - [[fallthrough]]; - case QChar::DirBN: - case QChar::DirB: - case QChar::DirS: - case QChar::DirWS: - case QChar::DirON: - if(status.eor == QChar::DirR) { - // neutrals go to R - eor = current - 1; - eAppendItems(analysis, sor, eor, control, dir); - status.eor = QChar::DirEN; - dir = QChar::DirAN; - } - else if(status.eor == QChar::DirL || - (status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) { - eor = current; status.eor = dirCurrent; - } else { - // numbers on both sides, neutrals get right to left direction - if(dir != QChar::DirL) { - eAppendItems(analysis, sor, eor, control, dir); - status.eor = QChar::DirON; - eor = current - 1; - dir = QChar::DirR; - eAppendItems(analysis, sor, eor, control, dir); - status.eor = QChar::DirON; - dir = QChar::DirAN; - } else { - eor = current; status.eor = dirCurrent; - } - } - [[fallthrough]]; - default: - break; - } - break; - } - [[fallthrough]]; - case QChar::DirAN: - hasBidi = true; - dirCurrent = QChar::DirAN; - if(dir == QChar::DirON) dir = QChar::DirAN; - switch(status.last) - { - case QChar::DirL: - case QChar::DirAN: - eor = current; status.eor = QChar::DirAN; - break; - case QChar::DirR: - case QChar::DirAL: - case QChar::DirEN: - if (eor >= 0){ - eAppendItems(analysis, sor, eor, control, dir); - } else { - eor = current; - } - dir = QChar::DirAN; status.eor = QChar::DirAN; - break; - case QChar::DirCS: - if(status.eor == QChar::DirAN) { - eor = current; break; - } - [[fallthrough]]; - case QChar::DirES: - case QChar::DirET: - case QChar::DirBN: - case QChar::DirB: - case QChar::DirS: - case QChar::DirWS: - case QChar::DirON: - if(status.eor == QChar::DirR) { - // neutrals go to R - eor = current - 1; - eAppendItems(analysis, sor, eor, control, dir); - status.eor = QChar::DirAN; - dir = QChar::DirAN; - } else if(status.eor == QChar::DirL || - (status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) { - eor = current; status.eor = dirCurrent; - } else { - // numbers on both sides, neutrals get right to left direction - if(dir != QChar::DirL) { - eAppendItems(analysis, sor, eor, control, dir); - status.eor = QChar::DirON; - eor = current - 1; - dir = QChar::DirR; - eAppendItems(analysis, sor, eor, control, dir); - status.eor = QChar::DirAN; - dir = QChar::DirAN; - } else { - eor = current; status.eor = dirCurrent; - } - } - [[fallthrough]]; - default: - break; - } - break; - case QChar::DirES: - case QChar::DirCS: - break; - case QChar::DirET: - if(status.last == QChar::DirEN) { - dirCurrent = QChar::DirEN; - eor = current; status.eor = dirCurrent; - } - break; - - // boundary neutrals should be ignored - case QChar::DirBN: - break; - // neutrals - case QChar::DirB: - // ### what do we do with newline and paragraph separators that come to here? - break; - case QChar::DirS: - // ### implement rule L1 - break; - case QChar::DirWS: - case QChar::DirON: - break; - default: - break; - } - - if(current >= (int)_parLength) break; - - // set status.last as needed. - switch(dirCurrent) { - case QChar::DirET: - case QChar::DirES: - case QChar::DirCS: - case QChar::DirS: - case QChar::DirWS: - case QChar::DirON: - switch(status.last) - { - case QChar::DirL: - case QChar::DirR: - case QChar::DirAL: - case QChar::DirEN: - case QChar::DirAN: - status.last = dirCurrent; - break; - default: - status.last = QChar::DirON; - } - break; - case QChar::DirNSM: - case QChar::DirBN: - // ignore these - break; - case QChar::DirLRO: - case QChar::DirLRE: - status.last = QChar::DirL; - break; - case QChar::DirRLO: - case QChar::DirRLE: - status.last = QChar::DirR; - break; - case QChar::DirEN: - if (status.last == QChar::DirL) { - status.last = QChar::DirL; - break; - } - [[fallthrough]]; - default: - status.last = dirCurrent; - } - - ++current; - } - - eor = current - 1; // remove dummy char - - if (sor <= eor) - eAppendItems(analysis, sor, eor, control, dir); - - return hasBidi; - } - -private: - void applyBlockProperties(const AbstractBlock *block) { - eSetFont(block); - if (_p) { - const auto isMono = IsMono(block->flags()); - _background = {}; - if (block->spoilerIndex() && _t->_spoiler) { - const auto handler - = _t->_spoiler->links.at(block->spoilerIndex() - 1); - const auto inBack = (handler && handler->shown()); - _background.inFront = !inBack; - _background.color = inBack - ? &_textPalette->spoilerActiveBg - : &_textPalette->spoilerBg; - _background.startMs = handler ? handler->startMs() : 0; - _background.spoilerIndex = block->spoilerIndex(); - - Assert(_t->_spoiler != nullptr); - const auto &cache = _background.inFront - ? _t->_spoiler->spoilerCache - : _t->_spoiler->spoilerShownCache; - if (cache.color != (*_background.color)->c) { - auto mutableText = const_cast(_t); - auto &mutableCache = _background.inFront - ? mutableText->_spoiler->spoilerCache - : mutableText->_spoiler->spoilerShownCache; - mutableCache.corners = Images::PrepareCorners( - ImageRoundRadius::Small, - *_background.color); - mutableCache.color = (*_background.color)->c; - } - } - if (isMono && block->lnkIndex() && !_background.inFront) { - _background.selectActiveBlock = ClickHandler::showAsPressed( - _t->_links.at(block->lnkIndex() - 1)); - } - - if (isMono) { - _currentPen = &_textPalette->monoFg->p; - _currentPenSelected = &_textPalette->selectMonoFg->p; - } else if (block->lnkIndex() - || (block->flags() & TextBlockFPlainLink)) { - _currentPen = &_textPalette->linkFg->p; - _currentPenSelected = &_textPalette->selectLinkFg->p; - } else { - _currentPen = &_originalPen; - _currentPenSelected = &_originalPenSelected; - } - } - } - - Painter *_p = nullptr; - const style::TextPalette *_textPalette = nullptr; - const String *_t = nullptr; - bool _elideLast = false; - bool _breakEverywhere = false; - int _elideRemoveFromEnd = 0; - style::align _align = style::al_topleft; - const 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; - crl::time _now = 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 _parAnalysis; - - // current line data - QTextEngine *_e = nullptr; - style::font _f; - QFixed _x, _w, _wLeft, _last_rPadding; - int32 _y, _yDelta, _lineHeight, _fontHeight; - - // elided hack support - int _blocksSize = 0; - int _elideSavedIndex = 0; - std::optional _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; - -}; - String::String(int32 minResizeWidth) -: _minResizeWidth(minResizeWidth) -, _hasCustomEmoji(false) -, _isOnlyCustomEmoji(false) { +: _minResizeWidth(minResizeWidth) { } String::String( @@ -2964,12 +146,16 @@ String::String( const QString &text, const TextParseOptions &options, int32 minResizeWidth) -: _minResizeWidth(minResizeWidth) -, _hasCustomEmoji(false) -, _isOnlyCustomEmoji(false) { +: _minResizeWidth(minResizeWidth) { setText(st, text, options); } +String::String(String &&other) = default; + +String &String::operator=(String &&other) = default; + +String::~String() = default; + void String::setText(const style::TextStyle &st, const QString &text, const TextParseOptions &options) { _st = &st; clear(); @@ -2989,7 +175,7 @@ void String::recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir) { for (auto &block : _blocks) { auto b = block.get(); auto _btype = b->type(); - auto blockHeight = countBlockHeight(b, _st); + auto blockHeight = CountBlockHeight(b, _st); if (_btype == TextBlockTNewline) { if (!lineHeight) lineHeight = blockHeight; if (initial) { @@ -3046,7 +232,7 @@ void String::recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir) { } } if (_width > 0) { - if (!lineHeight) lineHeight = countBlockHeight(_blocks.back().get(), _st); + if (!lineHeight) lineHeight = CountBlockHeight(_blocks.back().get(), _st); _minHeight += lineHeight; accumulate_max(_maxWidth, _width); } @@ -3250,7 +436,7 @@ void String::enumerateLines( bool longWordLine = true; for (auto &b : _blocks) { auto _btype = b->type(); - int blockHeight = countBlockHeight(b.get(), _st); + int blockHeight = CountBlockHeight(b.get(), _st); if (_btype == TextBlockTNewline) { if (!lineHeight) lineHeight = blockHeight; @@ -3350,19 +536,55 @@ void String::enumerateLines( } } -void String::draw(Painter &painter, int32 left, int32 top, int32 w, style::align align, int32 yFrom, int32 yTo, TextSelection selection, bool fullWidthSelection) const { -// painter.fillRect(QRect(left, top, w, countHeight(w)), QColor(0, 0, 0, 32)); // debug - Renderer p(&painter, this); - p.draw(left, top, w, align, yFrom, yTo, selection, fullWidthSelection); +void String::draw(QPainter &p, const PaintContext &context) const { + Renderer(*this).draw(p, context); } -void String::drawElided(Painter &painter, int32 left, int32 top, int32 w, int32 lines, style::align align, int32 yFrom, int32 yTo, int32 removeFromEnd, bool breakEverywhere, TextSelection selection) const { -// painter.fillRect(QRect(left, top, w, countHeight(w)), QColor(0, 0, 0, 32)); // debug - Renderer p(&painter, this); - p.drawElided(left, top, w, align, lines, yFrom, yTo, removeFromEnd, breakEverywhere, selection); +void String::draw(Painter &p, int32 left, int32 top, int32 w, style::align align, int32 yFrom, int32 yTo, TextSelection selection, bool fullWidthSelection) const { +// p.fillRect(QRect(left, top, w, countHeight(w)), QColor(0, 0, 0, 32)); // debug + Renderer(*this).draw(p, { + .position = { left, top }, + .availableWidth = w, + .align = align, + .clip = (yTo >= 0 + ? QRect(left, top + yFrom, w, yTo - yFrom) + : QRect()), + .palette = &p.textPalette(), + .paused = p.inactive(), + .selection = selection, + .fullWidthSelection = fullWidthSelection, + }); +} + +void String::drawElided(Painter &p, int32 left, int32 top, int32 w, int32 lines, style::align align, int32 yFrom, int32 yTo, int32 removeFromEnd, bool breakEverywhere, TextSelection selection) const { +// p.fillRect(QRect(left, top, w, countHeight(w)), QColor(0, 0, 0, 32)); // debug + Renderer(*this).draw(p, { + .position = { left, top }, + .availableWidth = w, + .align = align, + .clip = (yTo >= 0 + ? QRect(left, top + yFrom, w, yTo - yFrom) + : QRect()), + .palette = &p.textPalette(), + .paused = p.inactive(), + .selection = selection, + .elisionLines = lines, + .elisionRemoveFromEnd = removeFromEnd, + }); } void String::drawLeft(Painter &p, int32 left, int32 top, int32 width, int32 outerw, style::align align, int32 yFrom, int32 yTo, TextSelection selection) const { + Renderer(*this).draw(p, { + .position = { left, top }, + .availableWidth = width, + .align = align, + .clip = (yTo >= 0 + ? QRect(left, top + yFrom, width, yTo - yFrom) + : QRect()), + .palette = &p.textPalette(), + .paused = p.inactive(), + .selection = selection, + }); draw(p, style::RightToLeft() ? (outerw - left - width) : left, top, width, align, yFrom, yTo, selection); } @@ -3371,15 +593,15 @@ void String::drawLeftElided(Painter &p, int32 left, int32 top, int32 width, int3 } void String::drawRight(Painter &p, int32 right, int32 top, int32 width, int32 outerw, style::align align, int32 yFrom, int32 yTo, TextSelection selection) const { - draw(p, style::RightToLeft() ? right : (outerw - right - width), top, width, align, yFrom, yTo, selection); + drawLeft(p, (outerw - right - width), top, width, outerw, align, yFrom, yTo, selection); } void String::drawRightElided(Painter &p, int32 right, int32 top, int32 width, int32 outerw, int32 lines, style::align align, int32 yFrom, int32 yTo, int32 removeFromEnd, bool breakEverywhere, TextSelection selection) const { - drawElided(p, style::RightToLeft() ? right : (outerw - right - width), top, width, lines, align, yFrom, yTo, removeFromEnd, breakEverywhere, selection); + drawLeftElided(p, (outerw - right - width), top, width, outerw, lines, align, yFrom, yTo, removeFromEnd, breakEverywhere, selection); } StateResult String::getState(QPoint point, int width, StateRequest request) const { - return Renderer(nullptr, this).getState(point, width, request); + return Renderer(*this).getState(point, width, request); } StateResult String::getStateLeft(QPoint point, int width, int outerw, StateRequest request) const { @@ -3387,7 +609,7 @@ StateResult String::getStateLeft(QPoint point, int width, int outerw, StateReque } StateResult String::getStateElided(QPoint point, int width, StateRequestElided request) const { - return Renderer(nullptr, this).getStateElided(point, width, request); + return Renderer(*this).getStateElided(point, width, request); } StateResult String::getStateElidedLeft(QPoint point, int width, int outerw, StateRequestElided request) const { @@ -3583,18 +805,17 @@ void String::enumerateText( } } -bool String::hasCustomEmoji() const { - return _hasCustomEmoji; +bool String::hasPersistentAnimation() const { + return _hasCustomEmoji || _spoiler; } -void String::unloadCustomEmoji() { - if (!_hasCustomEmoji) { - return; - } - for (const auto &block : _blocks) { - const auto raw = block.get(); - if (raw->type() == TextBlockTCustomEmoji) { - static_cast(raw)->_custom->unload(); +void String::unloadPersistentAnimation() { + if (_hasCustomEmoji) { + for (const auto &block : _blocks) { + const auto raw = block.get(); + if (raw->type() == TextBlockTCustomEmoji) { + static_cast(raw)->_custom->unload(); + } } } } @@ -3838,6 +1059,16 @@ ClickHandlerPtr String::spoilerLink(uint16 spoilerIndex) const { return nullptr; } +bool IsBad(QChar ch) { + return (ch == 0) + || (ch >= 8232 && ch < 8237) + || (ch >= 65024 && ch < 65040 && ch != 65039) + || (ch >= 127 && ch < 160 && ch != 156) + + // qt harfbuzz crash see https://github.com/telegramdesktop/tdesktop/issues/4551 + || (Platform::IsMac() && ch == 6158); +} + bool IsWordSeparator(QChar ch) { switch (ch.unicode()) { case QChar::Space: @@ -3944,5 +1175,4 @@ bool IsTrimmed(QChar ch) { return (IsSpace(ch) || IsBad(ch)); } -} // namespace Text -} // namespace Ui +} // namespace Ui::Text diff --git a/ui/text/text.h b/ui/text/text.h index 2b3e86d..47de231 100644 --- a/ui/text/text.h +++ b/ui/text/text.h @@ -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 #include +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 lookup(QColor color); + void reset(); + +private: + struct Entry { + SpoilerMessCached mess; + QColor color; + int generation = 0; + }; + + std::vector _cache; + const int _capacity = 0; + int _generation = 0; + +}; + +[[nodiscard]] not_null 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 _spoiler; + std::unique_ptr _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); diff --git a/ui/text/text_block.cpp b/ui/text/text_block.cpp index f3d2275..5cd9f92 100644 --- a/ui/text/text_block.cpp +++ b/ui/text/text_block.cpp @@ -758,5 +758,15 @@ void Block::destroy() { } } +int CountBlockHeight( + const AbstractBlock *block, + const style::TextStyle *st) { + return (block->type() == TextBlockTSkip) + ? static_cast(block)->height() + : (st->lineHeight > st->font->height) + ? st->lineHeight + : st->font->height; +} + } // namespace Text } // namespace Ui diff --git a/ui/text/text_block.h b/ui/text/text_block.h index 0580146..b5a140a 100644 --- a/ui/text/text_block.h +++ b/ui/text/text_block.h @@ -14,8 +14,11 @@ #include -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 diff --git a/ui/text/text_parser.cpp b/ui/text/text_parser.cpp new file mode 100644 index 0000000..0db97da --- /dev/null +++ b/ui/text/text_parser.cpp @@ -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 + +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 Parser::StartedEntity::flags() const { + if (_value < int(kStringLinkIndexShift) && (_type == Type::Flags)) { + return TextBlockFlags(_value); + } + return std::nullopt; +} + +std::optional 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 Parser::StartedEntity::spoilerIndex() const { + if (_value < int(kStringLinkIndexShift) && (_type == Type::Spoiler)) { + return uint16(_value); + } + return std::nullopt; +} + +Parser::Parser( + not_null string, + const TextWithEntities &textWithEntities, + const TextParseOptions &options, + const std::any &context) +: Parser( + string, + PrepareRichFromRich(textWithEntities, options), + options, + context, + ReadyToken()) { +} + +Parser::Parser( + not_null 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( + Integration::Instance().createSpoilerRepaint(_context)); + } + if (_t->_spoiler->links.size() < spoilerIndex) { + _t->_spoiler->links.resize(spoilerIndex); + const auto handler = (options.flags & TextParseLinks) + ? std::make_shared() + : 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 diff --git a/ui/text/text_parser.h b/ui/text/text_parser.h new file mode 100644 index 0000000..f105a1c --- /dev/null +++ b/ui/text/text_parser.h @@ -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, + 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 flags() const; + [[nodiscard]] std::optional lnkIndex() const; + [[nodiscard]] std::optional spoilerIndex() const; + + private: + const int _value = 0; + const Type _type; + + }; + + Parser( + not_null 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 _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 _linksIndexes; + + std::vector _links; + std::vector _spoilers; + std::vector _monos; + base::flat_map< + const QChar*, + std::vector> _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 diff --git a/ui/text/text_renderer.cpp b/ui/text/text_renderer.cpp new file mode 100644 index 0000000..9735de6 --- /dev/null +++ b/ui/text/text_renderer.cpp @@ -0,0 +1,1922 @@ +// 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_renderer.h" + +#include "ui/text/text_spoiler_data.h" +#include "ui/spoiler_click_handler.h" +#include "styles/style_basic.h" + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#include +#endif // Qt < 6.0.0 + +namespace Ui::Text { +namespace { + +// COPIED FROM qtextengine.cpp AND MODIFIED + +struct BidiStatus { + BidiStatus() { + eor = QChar::DirON; + lastStrong = QChar::DirON; + last = QChar::DirON; + dir = QChar::DirON; + } + QChar::Direction eor; + QChar::Direction lastStrong; + QChar::Direction last; + QChar::Direction dir; +}; + +enum { _MaxBidiLevel = 61 }; +enum { _MaxItemLength = 4096 }; + +void InitTextItemWithScriptItem(QTextItemInt &ti, const QScriptItem &si) { + // explicitly initialize flags so that initFontAttributes can be called + // multiple times on the same TextItem + ti.flags = { }; + if (si.analysis.bidiLevel % 2) + ti.flags |= QTextItem::RightToLeft; + ti.ascent = si.ascent; + ti.descent = si.descent; + + if (ti.charFormat.hasProperty(QTextFormat::TextUnderlineStyle)) { + ti.underlineStyle = ti.charFormat.underlineStyle(); + } else if (ti.charFormat.boolProperty(QTextFormat::FontUnderline) + || ti.f->underline()) { + ti.underlineStyle = QTextCharFormat::SingleUnderline; + } + + // compat + if (ti.underlineStyle == QTextCharFormat::SingleUnderline) + ti.flags |= QTextItem::Underline; + + if (ti.f->overline() || ti.charFormat.fontOverline()) + ti.flags |= QTextItem::Overline; + if (ti.f->strikeOut() || ti.charFormat.fontStrikeOut()) + ti.flags |= QTextItem::StrikeOut; +} + +} // namespace + + +struct Renderer::BidiControl { + inline BidiControl(bool rtl) + : base(rtl ? 1 : 0), level(rtl ? 1 : 0) {} + + inline void embed(bool rtl, bool o = false) { + unsigned int toAdd = 1; + if ((level % 2 != 0) == rtl) { + ++toAdd; + } + if (level + toAdd <= _MaxBidiLevel) { + ctx[cCtx].level = level; + ctx[cCtx].override = override; + cCtx++; + override = o; + level += toAdd; + } + } + inline bool canPop() const { return cCtx != 0; } + inline void pdf() { + Q_ASSERT(cCtx); + --cCtx; + level = ctx[cCtx].level; + override = ctx[cCtx].override; + } + + inline QChar::Direction basicDirection() const { + return (base ? QChar::DirR : QChar::DirL); + } + inline unsigned int baseLevel() const { + return base; + } + inline QChar::Direction direction() const { + return ((level % 2) ? QChar::DirR : QChar::DirL); + } + + struct { + unsigned int level = 0; + bool override = false; + } ctx[_MaxBidiLevel]; + unsigned int cCtx = 0; + const unsigned int base; + unsigned int level; + bool override = false; +}; + +Renderer::Renderer(const Ui::Text::String &t) +: _t(&t) { +} + +Renderer::~Renderer() { + restoreAfterElided(); + if (_p) { + _p->setPen(_originalPen); + } +} + +void Renderer::draw(QPainter &p, const PaintContext &context) { + if (_t->isEmpty()) { + return; + } + + _p = &p; + _p->setFont(_t->_st->font); + _palette = context.palette ? context.palette : &st::defaultTextPalette; + _originalPen = _p->pen(); + _originalPenSelected = (_palette->selectFg->c.alphaF() == 0) + ? _originalPen + : _palette->selectFg->p; + + _x = context.position.x(); + _y = context.position.y(); + _yFrom = context.clip.isNull() ? 0 : context.clip.y(); + _yTo = context.clip.isNull() + ? -1 + : (context.clip.y() + context.clip.height()); + if (const auto lines = context.elisionLines) { + if (context.clip.isNull()) + if (_yTo < 0 || (_y + (lines - 1) * _t->_st->font->height) < _yTo) { + _yTo = _y + (lines * _t->_st->font->height); + _elideLast = true; + _elideRemoveFromEnd = context.elisionRemoveFromEnd; + } + _breakEverywhere = context.elisionBreakEverywhere; + } + _spoilerCache = context.spoiler; + _selection = context.selection; + _fullWidthSelection = context.fullWidthSelection; + _w = context.availableWidth; + _align = context.align; + _cachedNow = context.now; + _paused = context.paused; + enumerate(); +} + +void Renderer::enumerate() { + _blocksSize = _t->_blocks.size(); + _wLeft = _w; + if (_elideLast) { + _yToElide = _yTo; + if (_elideRemoveFromEnd > 0 && !_t->_blocks.empty()) { + int firstBlockHeight = CountBlockHeight(_t->_blocks.front().get(), _t->_st); + if (_y + firstBlockHeight >= _yToElide) { + _wLeft -= _elideRemoveFromEnd; + } + } + } + _str = _t->_text.unicode(); + + if (_p) { + auto clip = _p->hasClipping() ? _p->clipBoundingRect() : QRect(); + if (clip.width() > 0 || clip.height() > 0) { + if (_yFrom < clip.y()) _yFrom = clip.y(); + if (_yTo < 0 || _yTo > clip.y() + clip.height()) _yTo = clip.y() + clip.height(); + } + } + + _parDirection = _t->_startDir; + if (_parDirection == Qt::LayoutDirectionAuto) _parDirection = style::LayoutDirection(); + if ((*_t->_blocks.cbegin())->type() != TextBlockTNewline) { + initNextParagraph(_t->_blocks.cbegin()); + } + + _lineStart = 0; + _lineStartBlock = 0; + + _lineHeight = 0; + _fontHeight = _t->_st->font->height; + auto last_rBearing = QFixed(0); + _last_rPadding = QFixed(0); + + auto blockIndex = 0; + bool longWordLine = true; + auto e = _t->_blocks.cend(); + for (auto i = _t->_blocks.cbegin(); i != e; ++i, ++blockIndex) { + auto b = i->get(); + auto _btype = b->type(); + auto blockHeight = CountBlockHeight(b, _t->_st); + + if (_btype == TextBlockTNewline) { + if (!_lineHeight) _lineHeight = blockHeight; + if (!drawLine((*i)->from(), i, e)) { + return; + } + + _y += _lineHeight; + _lineHeight = 0; + _lineStart = _t->countBlockEnd(i, e); + _lineStartBlock = blockIndex + 1; + + last_rBearing = b->f_rbearing(); + _last_rPadding = b->f_rpadding(); + _wLeft = _w - (b->f_width() - last_rBearing); + if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) { + _wLeft -= _elideRemoveFromEnd; + } + + _parDirection = static_cast(b)->nextDirection(); + if (_parDirection == Qt::LayoutDirectionAuto) _parDirection = style::LayoutDirection(); + initNextParagraph(i + 1); + + longWordLine = true; + continue; + } + + auto b__f_rbearing = b->f_rbearing(); + auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + b->f_width() - b__f_rbearing); + if (newWidthLeft >= 0) { + last_rBearing = b__f_rbearing; + _last_rPadding = b->f_rpadding(); + _wLeft = newWidthLeft; + + _lineHeight = qMax(_lineHeight, blockHeight); + + longWordLine = false; + continue; + } + + if (_btype == TextBlockTText) { + auto t = static_cast(b); + if (t->_words.isEmpty()) { // no words in this block, spaces only => layout this block in the same line + _last_rPadding += b->f_rpadding(); + + _lineHeight = qMax(_lineHeight, blockHeight); + + longWordLine = false; + continue; + } + + auto f_wLeft = _wLeft; // vars for saving state of the last word start + auto f_lineHeight = _lineHeight; // f points to the last word-start element of t->_words + for (auto j = t->_words.cbegin(), en = t->_words.cend(), f = j; j != en; ++j) { + auto wordEndsHere = (j->f_width() >= 0); + auto j_width = wordEndsHere ? j->f_width() : -j->f_width(); + + auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + j_width - j->f_rbearing()); + if (newWidthLeft >= 0) { + last_rBearing = j->f_rbearing(); + _last_rPadding = j->f_rpadding(); + _wLeft = newWidthLeft; + + _lineHeight = qMax(_lineHeight, blockHeight); + + if (wordEndsHere) { + longWordLine = false; + } + if (wordEndsHere || longWordLine) { + f = j + 1; + f_wLeft = _wLeft; + f_lineHeight = _lineHeight; + } + continue; + } + + auto elidedLineHeight = qMax(_lineHeight, blockHeight); + auto elidedLine = _elideLast && (_y + elidedLineHeight >= _yToElide); + if (elidedLine) { + _lineHeight = elidedLineHeight; + } else if (f != j && !_breakEverywhere) { + // word did not fit completely, so we roll back the state to the beginning of this long word + j = f; + _wLeft = f_wLeft; + _lineHeight = f_lineHeight; + j_width = (j->f_width() >= 0) ? j->f_width() : -j->f_width(); + } + if (!drawLine(elidedLine ? ((j + 1 == en) ? _t->countBlockEnd(i, e) : (j + 1)->from()) : j->from(), i, e)) { + return; + } + _y += _lineHeight; + _lineHeight = qMax(0, blockHeight); + _lineStart = j->from(); + _lineStartBlock = blockIndex; + + last_rBearing = j->f_rbearing(); + _last_rPadding = j->f_rpadding(); + _wLeft = _w - (j_width - last_rBearing); + if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) { + _wLeft -= _elideRemoveFromEnd; + } + + longWordLine = !wordEndsHere; + f = j + 1; + f_wLeft = _wLeft; + f_lineHeight = _lineHeight; + } + continue; + } + + auto elidedLineHeight = qMax(_lineHeight, blockHeight); + auto elidedLine = _elideLast && (_y + elidedLineHeight >= _yToElide); + if (elidedLine) { + _lineHeight = elidedLineHeight; + } + if (!drawLine(elidedLine ? _t->countBlockEnd(i, e) : b->from(), i, e)) { + return; + } + _y += _lineHeight; + _lineHeight = qMax(0, blockHeight); + _lineStart = b->from(); + _lineStartBlock = blockIndex; + + last_rBearing = b__f_rbearing; + _last_rPadding = b->f_rpadding(); + _wLeft = _w - (b->f_width() - last_rBearing); + if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) { + _wLeft -= _elideRemoveFromEnd; + } + + longWordLine = true; + continue; + } + if (_lineStart < _t->_text.size()) { + if (!drawLine(_t->_text.size(), e, e)) return; + } + if (!_p && _lookupSymbol) { + _lookupResult.symbol = _t->_text.size(); + _lookupResult.afterSymbol = false; + } +} + +StateResult Renderer::getState(QPoint point, int w, StateRequest request) { + if (!_t->isNull() && point.y() >= 0) { + _lookupRequest = request; + _lookupX = point.x(); + _lookupY = point.y(); + + _breakEverywhere = (_lookupRequest.flags & StateRequest::Flag::BreakEverywhere); + _lookupSymbol = (_lookupRequest.flags & StateRequest::Flag::LookupSymbol); + _lookupLink = (_lookupRequest.flags & StateRequest::Flag::LookupLink); + if (_lookupSymbol || (_lookupX >= 0 && _lookupX < w)) { + _w = w; + _yFrom = _lookupY; + _yTo = _lookupY + 1; + _align = _lookupRequest.align; + enumerate(); + } + } + return _lookupResult; +} + +StateResult Renderer::getStateElided(QPoint point, int w, StateRequestElided request) { + if (!_t->isNull() && point.y() >= 0 && request.lines > 0) { + _lookupRequest = request; + _lookupX = point.x(); + _lookupY = point.y(); + + _breakEverywhere = (_lookupRequest.flags & StateRequest::Flag::BreakEverywhere); + _lookupSymbol = (_lookupRequest.flags & StateRequest::Flag::LookupSymbol); + _lookupLink = (_lookupRequest.flags & StateRequest::Flag::LookupLink); + if (_lookupSymbol || (_lookupX >= 0 && _lookupX < w)) { + int yTo = _lookupY + 1; + if (yTo < 0 || (request.lines - 1) * _t->_st->font->height < yTo) { + yTo = request.lines * _t->_st->font->height; + _elideLast = true; + _elideRemoveFromEnd = request.removeFromEnd; + } + _w = w; + _yFrom = _lookupY; + _yTo = _lookupY + 1; + _align = _lookupRequest.align; + enumerate(); + } + } + return _lookupResult; +} + +crl::time Renderer::now() const { + if (!_cachedNow) { + _cachedNow = crl::now(); + } + return _cachedNow; +} + +void Renderer::initNextParagraph(String::TextBlocks::const_iterator i) { + _parStartBlock = i; + const auto e = _t->_blocks.cend(); + if (i == e) { + _parStart = _t->_text.size(); + _parLength = 0; + } else { + _parStart = (*i)->from(); + for (; i != e; ++i) { + if ((*i)->type() == TextBlockTNewline) { + break; + } + } + _parLength = ((i == e) ? _t->_text.size() : (*i)->from()) - _parStart; + } + _parAnalysis.resize(0); +} + +void Renderer::initParagraphBidi() { + if (!_parLength || !_parAnalysis.isEmpty()) return; + + String::TextBlocks::const_iterator i = _parStartBlock, e = _t->_blocks.cend(), n = i + 1; + + bool ignore = false; + bool rtl = (_parDirection == Qt::RightToLeft); + if (!ignore && !rtl) { + ignore = true; + const ushort *start = reinterpret_cast(_str) + _parStart; + const ushort *curr = start; + const ushort *end = start + _parLength; + while (curr < end) { + while (n != e && (*n)->from() <= _parStart + (curr - start)) { + i = n; + ++n; + } + const auto type = (*i)->type(); + if (type != TextBlockTEmoji + && type != TextBlockTCustomEmoji + && *curr >= 0x590) { + ignore = false; + break; + } + ++curr; + } + } + + _parAnalysis.resize(_parLength); + QScriptAnalysis *analysis = _parAnalysis.data(); + + BidiControl control(rtl); + + _parHasBidi = false; + if (ignore) { + memset(analysis, 0, _parLength * sizeof(QScriptAnalysis)); + if (rtl) { + for (int i = 0; i < _parLength; ++i) + analysis[i].bidiLevel = 1; + _parHasBidi = true; + } + } else { + _parHasBidi = eBidiItemize(analysis, control); + } +} + +bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterator &_endBlockIter, const String::TextBlocks::const_iterator &_end) { + _yDelta = (_lineHeight - _fontHeight) / 2; + if (_yTo >= 0 && (_y + _yDelta >= _yTo || _y >= _yTo)) return false; + if (_y + _yDelta + _fontHeight <= _yFrom) { + if (_lookupSymbol) { + _lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart; + _lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false; + } + return true; + } + + // Trimming pending spaces, because they sometimes don't fit on the line. + // They also are not counted in the line width, they're in the right padding. + // Line width is a sum of block / word widths and paddings between them, without trailing one. + auto trimmedLineEnd = _lineEnd; + for (; trimmedLineEnd > _lineStart; --trimmedLineEnd) { + auto ch = _t->_text[trimmedLineEnd - 1]; + if (ch != QChar::Space && ch != QChar::LineFeed) { + break; + } + } + + auto _endBlock = (_endBlockIter == _end) ? nullptr : _endBlockIter->get(); + auto elidedLine = _elideLast && (_y + _lineHeight >= _yToElide); + if (elidedLine) { + // If we decided to draw the last line elided only because of the skip block + // that did not fit on this line, we just draw the line till the very end. + // Skip block is ignored in the elided lines, instead "removeFromEnd" is used. + if (_endBlock && _endBlock->type() == TextBlockTSkip) { + _endBlock = nullptr; + } + if (!_endBlock) { + elidedLine = false; + } + } + + auto blockIndex = _lineStartBlock; + auto currentBlock = _t->_blocks[blockIndex].get(); + auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; + + const auto extendLeft = (currentBlock->from() < _lineStart) + ? qMin(_lineStart - currentBlock->from(), 2) + : 0; + _localFrom = _lineStart - extendLeft; + const auto extendedLineEnd = (_endBlock && _endBlock->from() < trimmedLineEnd && !elidedLine) + ? qMin(uint16(trimmedLineEnd + 2), _t->countBlockEnd(_endBlockIter, _end)) + : trimmedLineEnd; + + auto lineText = _t->_text.mid(_localFrom, extendedLineEnd - _localFrom); + auto lineStart = extendLeft; + auto lineLength = trimmedLineEnd - _lineStart; + + if (elidedLine) { + initParagraphBidi(); + prepareElidedLine(lineText, lineStart, lineLength, _endBlock); + } + + auto x = _x; + if (_align & Qt::AlignHCenter) { + x += (_wLeft / 2).toInt(); + } else if (((_align & Qt::AlignLeft) && _parDirection == Qt::RightToLeft) || ((_align & Qt::AlignRight) && _parDirection == Qt::LeftToRight)) { + x += _wLeft; + } + + if (!_p) { + if (_lookupX < x) { + if (_lookupSymbol) { + if (_parDirection == Qt::RightToLeft) { + _lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart; + _lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false; + // _lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockTSkip)) ? true : false; + } else { + _lookupResult.symbol = _lineStart; + _lookupResult.afterSymbol = false; + // _lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineStart > 0)) ? true : false; + } + } + if (_lookupLink) { + _lookupResult.link = nullptr; + } + _lookupResult.uponSymbol = false; + return false; + } else if (_lookupX >= x + (_w - _wLeft)) { + if (_parDirection == Qt::RightToLeft) { + _lookupResult.symbol = _lineStart; + _lookupResult.afterSymbol = false; + // _lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineStart > 0)) ? true : false; + } else { + _lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart; + _lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false; + // _lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockTSkip)) ? true : false; + } + if (_lookupLink) { + _lookupResult.link = nullptr; + } + _lookupResult.uponSymbol = false; + return false; + } + } + + if (_fullWidthSelection) { + const auto selectFromStart = (_selection.to > _lineStart) + && (_lineStart > 0) + && (_selection.from <= _lineStart); + const auto selectTillEnd = (_selection.to > trimmedLineEnd) + && (trimmedLineEnd < _t->_text.size()) + && (_selection.from <= trimmedLineEnd) + && (!_endBlock || _endBlock->type() != TextBlockTSkip); + + if ((selectFromStart && _parDirection == Qt::LeftToRight) + || (selectTillEnd && _parDirection == Qt::RightToLeft)) { + if (x > _x) { + fillSelectRange(_x, x); + } + } + if ((selectTillEnd && _parDirection == Qt::LeftToRight) + || (selectFromStart && _parDirection == Qt::RightToLeft)) { + if (x < _x + _wLeft) { + fillSelectRange(x + _w - _wLeft, _x + _w); + } + } + } + if (trimmedLineEnd == _lineStart && !elidedLine) { + return true; + } + + if (!elidedLine) initParagraphBidi(); // if was not inited + + _f = _t->_st->font; + QStackTextEngine engine(lineText, _f->f); + engine.option.setTextDirection(_parDirection); + _e = &engine; + + eItemize(); + + QScriptLine line; + line.from = lineStart; + line.length = lineLength; + eShapeLine(line); + + int firstItem = engine.findItem(line.from), lastItem = engine.findItem(line.from + line.length - 1); + int nItems = (firstItem >= 0 && lastItem >= firstItem) ? (lastItem - firstItem + 1) : 0; + if (!nItems) { + return true; + } + + int skipIndex = -1; + QVarLengthArray visualOrder(nItems); + QVarLengthArray levels(nItems); + for (int i = 0; i < nItems; ++i) { + auto &si = engine.layoutData->items[firstItem + i]; + while (nextBlock && nextBlock->from() <= _localFrom + si.position) { + currentBlock = nextBlock; + nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; + } + auto _type = currentBlock->type(); + if (_type == TextBlockTSkip) { + levels[i] = si.analysis.bidiLevel = 0; + skipIndex = i; + } else { + levels[i] = si.analysis.bidiLevel; + } + if (si.analysis.flags == QScriptAnalysis::Object) { + if (_type == TextBlockTEmoji + || _type == TextBlockTCustomEmoji + || _type == TextBlockTSkip) { + si.width = currentBlock->f_width() + + (nextBlock == _endBlock && (!nextBlock || nextBlock->from() >= trimmedLineEnd) + ? 0 + : currentBlock->f_rpadding()); + } + } + } + QTextEngine::bidiReorder(nItems, levels.data(), visualOrder.data()); + if (style::RightToLeft() && skipIndex == nItems - 1) { + for (int32 i = nItems; i > 1;) { + --i; + visualOrder[i] = visualOrder[i - 1]; + } + visualOrder[0] = skipIndex; + } + + blockIndex = _lineStartBlock; + currentBlock = _t->_blocks[blockIndex].get(); + nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; + + int32 textY = _y + _yDelta + _t->_st->font->ascent, emojiY = (_t->_st->font->height - st::emojiSize) / 2; + + applyBlockProperties(currentBlock); + for (int i = 0; i < nItems; ++i) { + int item = firstItem + visualOrder[i]; + const QScriptItem &si = engine.layoutData->items.at(item); + bool rtl = (si.analysis.bidiLevel % 2); + + while (blockIndex > _lineStartBlock + 1 && _t->_blocks[blockIndex - 1]->from() > _localFrom + si.position) { + nextBlock = currentBlock; + currentBlock = _t->_blocks[--blockIndex - 1].get(); + applyBlockProperties(currentBlock); + } + while (nextBlock && nextBlock->from() <= _localFrom + si.position) { + currentBlock = nextBlock; + nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; + applyBlockProperties(currentBlock); + } + if (si.analysis.flags >= QScriptAnalysis::TabOrObject) { + TextBlockType _type = currentBlock->type(); + if (!_p && _lookupX >= x && _lookupX < x + si.width) { // _lookupRequest + if (_lookupLink) { + if (_lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) { + const auto spoilerLink = _t->spoilerLink(currentBlock->spoilerIndex()); + const auto resultLink = (spoilerLink || !currentBlock->lnkIndex()) + ? spoilerLink + : _t->_links.at(currentBlock->lnkIndex() - 1); + if (resultLink) { + _lookupResult.link = resultLink; + } + } + } + if (_type != TextBlockTSkip) { + _lookupResult.uponSymbol = true; + } + if (_lookupSymbol) { + if (_type == TextBlockTSkip) { + if (_parDirection == Qt::RightToLeft) { + _lookupResult.symbol = _lineStart; + _lookupResult.afterSymbol = false; + } else { + _lookupResult.symbol = (trimmedLineEnd > _lineStart) ? (trimmedLineEnd - 1) : _lineStart; + _lookupResult.afterSymbol = (trimmedLineEnd > _lineStart) ? true : false; + } + return false; + } + + // Emoji with spaces after symbol lookup + auto chFrom = _str + currentBlock->from(); + auto chTo = chFrom + ((nextBlock ? nextBlock->from() : _t->_text.size()) - currentBlock->from()); + auto spacesWidth = (si.width - currentBlock->f_width()); + auto spacesCount = 0; + while (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space) { + ++spacesCount; + --chTo; + } + if (spacesCount > 0) { // Check if we're over a space. + if (rtl) { + if (_lookupX < x + spacesWidth) { + _lookupResult.symbol = (chTo - _str); // up to a space, included, rtl + _lookupResult.afterSymbol = (_lookupX < x + (spacesWidth / 2)) ? true : false; + return false; + } + } else if (_lookupX >= x + si.width - spacesWidth) { + _lookupResult.symbol = (chTo - _str); // up to a space, inclided, ltr + _lookupResult.afterSymbol = (_lookupX >= x + si.width - spacesWidth + (spacesWidth / 2)) ? true : false; + return false; + } + } + if (_lookupX < x + (rtl ? (si.width - currentBlock->f_width()) : 0) + (currentBlock->f_width() / 2)) { + _lookupResult.symbol = ((rtl && chTo > chFrom) ? (chTo - 1) : chFrom) - _str; + _lookupResult.afterSymbol = (rtl && chTo > chFrom) ? true : false; + } else { + _lookupResult.symbol = ((rtl || chTo <= chFrom) ? chFrom : (chTo - 1)) - _str; + _lookupResult.afterSymbol = (rtl || chTo <= chFrom) ? false : true; + } + } + return false; + } else if (_p && (_type == TextBlockTEmoji || _type == TextBlockTCustomEmoji)) { + auto glyphX = x; + auto spacesWidth = (si.width - currentBlock->f_width()); + if (rtl) { + glyphX += spacesWidth; + } + struct { + QFixed from; + QFixed to; + } fillSelect; + struct { + QFixed from; + QFixed width; + } fillSpoiler; + if (_background.selectActiveBlock) { + fillSelect = { x, x + si.width }; + } else if (_localFrom + si.position < _selection.to) { + auto chFrom = _str + currentBlock->from(); + auto chTo = chFrom + ((nextBlock ? nextBlock->from() : _t->_text.size()) - currentBlock->from()); + if (_localFrom + si.position >= _selection.from) { // could be without space + if (chTo == chFrom || (chTo - 1)->unicode() != QChar::Space || _selection.to >= (chTo - _str)) { + fillSelect = { x, x + si.width }; + } else { // or with space + fillSelect = { glyphX, glyphX + currentBlock->f_width() }; + } + } else if (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space && (chTo - 1 - _str) >= _selection.from) { + if (rtl) { // rtl space only + fillSelect = { x, glyphX }; + } else { // ltr space only + fillSelect = { x + currentBlock->f_width(), x + si.width }; + } + } + } + const auto hasSpoiler = _background.color && + (_background.inFront || _background.startMs); + if (hasSpoiler) { + fillSpoiler = { x, si.width }; + } + const auto spoilerOpacity = hasSpoiler + ? fillSpoilerOpacity() + : 0.; + const auto hasSelect = fillSelect.to != QFixed(); + if (hasSelect) { + fillSelectRange(fillSelect.from, fillSelect.to); + } + const auto opacity = _p->opacity(); + if (spoilerOpacity < 1.) { + if (hasSpoiler) { + _p->setOpacity(opacity * (1. - spoilerOpacity)); + } + const auto x = (glyphX + st::emojiPadding).toInt(); + const auto y = _y + _yDelta + emojiY; + if (_type == TextBlockTEmoji) { + Emoji::Draw( + *_p, + static_cast(currentBlock)->_emoji, + Emoji::GetSizeNormal(), + x, + y); + } else if (const auto custom = static_cast(currentBlock)->_custom.get()) { + if (!_customEmojiSize) { + _customEmojiSize = AdjustCustomEmojiSize(st::emojiSize); + _customEmojiSkip = (st::emojiSize - _customEmojiSize) / 2; + } + custom->paint(*_p, { + .preview = _palette->spoilerActiveBg->c, + .now = now(), + .position = { x + _customEmojiSkip, y + _customEmojiSkip }, + .paused = _paused, + }); + } + } + if (hasSpoiler) { + _p->setOpacity(opacity * spoilerOpacity); + fillSpoilerRange( + fillSpoiler.from, + fillSpoiler.width, + blockIndex, + currentBlock->from(), + (nextBlock ? nextBlock->from() : _t->_text.size())); + _p->setOpacity(opacity); + } + //} else if (_p && currentBlock->type() == TextBlockSkip) { // debug + // _p->fillRect(QRect(x.toInt(), _y, currentBlock->width(), static_cast(currentBlock)->height()), QColor(0, 0, 0, 32)); + } + x += si.width; + continue; + } + + unsigned short *logClusters = engine.logClusters(&si); + QGlyphLayout glyphs = engine.shapedGlyphs(&si); + + int itemStart = qMax(line.from, si.position), itemEnd; + int itemLength = engine.length(item); + int glyphsStart = logClusters[itemStart - si.position], glyphsEnd; + if (line.from + line.length < si.position + itemLength) { + itemEnd = line.from + line.length; + glyphsEnd = logClusters[itemEnd - si.position]; + } else { + itemEnd = si.position + itemLength; + glyphsEnd = si.num_glyphs; + } + + QFixed itemWidth = 0; + for (int g = glyphsStart; g < glyphsEnd; ++g) + itemWidth += glyphs.effectiveAdvance(g); + + if (!_p && _lookupX >= x && _lookupX < x + itemWidth) { // _lookupRequest + if (_lookupLink) { + if (_lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) { + const auto spoilerLink = _t->spoilerLink(currentBlock->spoilerIndex()); + const auto resultLink = (spoilerLink || !currentBlock->lnkIndex()) + ? spoilerLink + : _t->_links.at(currentBlock->lnkIndex() - 1); + if (resultLink) { + _lookupResult.link = resultLink; + } + } + } + _lookupResult.uponSymbol = true; + if (_lookupSymbol) { + QFixed tmpx = rtl ? (x + itemWidth) : x; + for (int ch = 0, g, itemL = itemEnd - itemStart; ch < itemL;) { + g = logClusters[itemStart - si.position + ch]; + QFixed gwidth = glyphs.effectiveAdvance(g); + // ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes + int ch2 = ch + 1; + while ((ch2 < itemL) && (g == logClusters[itemStart - si.position + ch2])) { + ++ch2; + } + for (int charsCount = (ch2 - ch); ch < ch2; ++ch) { + QFixed shift1 = QFixed(2 * (charsCount - (ch2 - ch)) + 2) * gwidth / QFixed(2 * charsCount), + shift2 = QFixed(2 * (charsCount - (ch2 - ch)) + 1) * gwidth / QFixed(2 * charsCount); + if ((rtl && _lookupX >= tmpx - shift1) || + (!rtl && _lookupX < tmpx + shift1)) { + _lookupResult.symbol = _localFrom + itemStart + ch; + if ((rtl && _lookupX >= tmpx - shift2) || + (!rtl && _lookupX < tmpx + shift2)) { + _lookupResult.afterSymbol = false; + } else { + _lookupResult.afterSymbol = true; + } + return false; + } + } + if (rtl) { + tmpx -= gwidth; + } else { + tmpx += gwidth; + } + } + if (itemEnd > itemStart) { + _lookupResult.symbol = _localFrom + itemEnd - 1; + _lookupResult.afterSymbol = true; + } else { + _lookupResult.symbol = _localFrom + itemStart; + _lookupResult.afterSymbol = false; + } + } + return false; + } else if (_p) { + QTextItemInt gf; + gf.glyphs = glyphs.mid(glyphsStart, glyphsEnd - glyphsStart); + gf.f = &_e->fnt; + gf.chars = engine.layoutData->string.unicode() + itemStart; + gf.num_chars = itemEnd - itemStart; + gf.fontEngine = engine.fontEngine(si); + gf.logClusters = logClusters + itemStart - si.position; + gf.width = itemWidth; + gf.justified = false; + InitTextItemWithScriptItem(gf, si); + + auto hasSelected = false; + auto hasNotSelected = true; + auto selectedRect = QRect(); + if (_background.selectActiveBlock) { + fillSelectRange(x, x + itemWidth); + } else if (_localFrom + itemStart < _selection.to && _localFrom + itemEnd > _selection.from) { + hasSelected = true; + auto selX = x; + auto selWidth = itemWidth; + if (_localFrom + itemStart >= _selection.from && _localFrom + itemEnd <= _selection.to) { + hasNotSelected = false; + } else { + selWidth = 0; + int itemL = itemEnd - itemStart; + int selStart = _selection.from - (_localFrom + itemStart), selEnd = _selection.to - (_localFrom + itemStart); + if (selStart < 0) selStart = 0; + if (selEnd > itemL) selEnd = itemL; + for (int ch = 0, g; ch < selEnd;) { + g = logClusters[itemStart - si.position + ch]; + QFixed gwidth = glyphs.effectiveAdvance(g); + // ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes + int ch2 = ch + 1; + while ((ch2 < itemL) && (g == logClusters[itemStart - si.position + ch2])) { + ++ch2; + } + if (ch2 <= selStart) { + selX += gwidth; + } else if (ch >= selStart && ch2 <= selEnd) { + selWidth += gwidth; + } else { + int sStart = ch, sEnd = ch2; + if (ch < selStart) { + sStart = selStart; + selX += QFixed(sStart - ch) * gwidth / QFixed(ch2 - ch); + } + if (ch2 >= selEnd) { + sEnd = selEnd; + selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch); + break; + } + selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch); + } + ch = ch2; + } + } + if (rtl) selX = x + itemWidth - (selX - x) - selWidth; + selectedRect = QRect(selX.toInt(), _y + _yDelta, (selX + selWidth).toInt() - selX.toInt(), _fontHeight); + fillSelectRange(selX, selX + selWidth); + } + const auto hasSpoiler = (_background.inFront || _background.startMs); + const auto spoilerOpacity = hasSpoiler + ? fillSpoilerOpacity() + : 0.; + const auto opacity = _p->opacity(); + const auto isElidedBlock = (!rtl) + && (_indexOfElidedBlock == blockIndex); + if ((spoilerOpacity < 1.) || isElidedBlock) { + if (hasSpoiler && !isElidedBlock) { + _p->setOpacity(opacity * (1. - spoilerOpacity)); + } + if (Q_UNLIKELY(hasSelected)) { + if (Q_UNLIKELY(hasNotSelected)) { + // There is a bug in retina QPainter clipping stack. + // You can see glitches in rendering in such text: + // aA + // Aa + // Where selection is both 'A'-s. + // I can't debug it right now, this is a workaround. +#ifdef Q_OS_MAC + _p->save(); +#endif // Q_OS_MAC + const auto clippingEnabled = _p->hasClipping(); + const auto clippingRegion = _p->clipRegion(); + _p->setClipRect(selectedRect, Qt::IntersectClip); + _p->setPen(*_currentPenSelected); + _p->drawTextItem(QPointF(x.toReal(), textY), gf); + const auto externalClipping = clippingEnabled + ? clippingRegion + : QRegion(QRect( + (_x - _w).toInt(), + _y - _lineHeight, + (_x + 2 * _w).toInt(), + _y + 2 * _lineHeight)); + _p->setClipRegion(externalClipping - selectedRect); + _p->setPen(*_currentPen); + _p->drawTextItem(QPointF(x.toReal(), textY), gf); +#ifdef Q_OS_MAC + _p->restore(); +#else // Q_OS_MAC + if (clippingEnabled) { + _p->setClipRegion(clippingRegion); + } else { + _p->setClipping(false); + } +#endif // Q_OS_MAC + } else { + _p->setPen(*_currentPenSelected); + _p->drawTextItem(QPointF(x.toReal(), textY), gf); + } + } else { + _p->setPen(*_currentPen); + _p->drawTextItem(QPointF(x.toReal(), textY), gf); + } + } + + if (hasSpoiler) { + _p->setOpacity(opacity * spoilerOpacity); + fillSpoilerRange( + x, + itemWidth, + blockIndex, + _localFrom + itemStart, + _localFrom + itemEnd); + _p->setOpacity(opacity); + } + } + + x += itemWidth; + } + return true; +} + +void Renderer::fillSelectRange(QFixed from, QFixed to) { + auto left = from.toInt(); + auto width = to.toInt() - left; + _p->fillRect(left, _y + _yDelta, width, _fontHeight, _palette->selectBg); +} + +float64 Renderer::fillSpoilerOpacity() { + if (!_background.startMs) { + return 1.; + } + const auto progress = float64(now() - _background.startMs) + / st::fadeWrapDuration; + if ((progress > 1.) && _background.spoilerIndex && _t->_spoiler) { + const auto link = _t->_spoiler->links.at( + _background.spoilerIndex - 1); + if (link) { + link->setStartMs(0); + } + } + return (1. - std::min(progress, 1.)); +} + +void Renderer::fillSpoilerRange( + QFixed x, + QFixed width, + int currentBlockIndex, + int positionFrom, + int positionTill) { + if (!_background.color || !_t->_spoiler) { + return; + } + const auto elided = (_indexOfElidedBlock == currentBlockIndex) + ? (_elideRemoveFromEnd + _f->elidew) + : 0; + const auto left = x.toInt(); + const auto useWidth = ((x + width).toInt() - left) - elided; + if (useWidth <= 0) { + return; + } + const auto rect = QRect( + left, + _y + _yDelta, + useWidth, + _fontHeight); + if (_spoilerCache) { + const auto mess = _spoilerCache->lookup((*_background.color)->c); + const auto spoiler = _t->_spoiler.get(); + const auto frame = mess->frame( + spoiler->animation.index(now(), _paused)); + Ui::FillSpoilerRect(*_p, rect, frame, -rect.topLeft()); + } else { + // Show forgotten spoiler context part. + _p->fillRect(rect, Qt::red); + } +} + +void Renderer::elideSaveBlock(int32 blockIndex, const AbstractBlock *&_endBlock, int32 elideStart, int32 elideWidth) { + if (_elideSavedBlock) { + restoreAfterElided(); + } + + _elideSavedIndex = blockIndex; + auto mutableText = const_cast(_t); + _elideSavedBlock = std::move(mutableText->_blocks[blockIndex]); + mutableText->_blocks[blockIndex] = Block::Text(_t->_st->font, _t->_text, QFIXED_MAX, elideStart, 0, (*_elideSavedBlock)->flags(), (*_elideSavedBlock)->lnkIndex(), (*_elideSavedBlock)->spoilerIndex()); + _blocksSize = blockIndex + 1; + _endBlock = (blockIndex + 1 < _t->_blocks.size() ? _t->_blocks[blockIndex + 1].get() : nullptr); +} + +void Renderer::setElideBidi(int32 elideStart, int32 elideLen) { + int32 newParLength = elideStart + elideLen - _parStart; + if (newParLength > _parAnalysis.size()) { + _parAnalysis.resize(newParLength); + } + for (int32 i = elideLen; i > 0; --i) { + _parAnalysis[newParLength - i].bidiLevel = (_parDirection == Qt::RightToLeft) ? 1 : 0; + } +} + +void Renderer::prepareElidedLine(QString &lineText, int32 lineStart, int32 &lineLength, const AbstractBlock *&_endBlock, int repeat) { + _f = _t->_st->font; + QStackTextEngine engine(lineText, _f->f); + engine.option.setTextDirection(_parDirection); + _e = &engine; + + eItemize(); + + auto blockIndex = _lineStartBlock; + auto currentBlock = _t->_blocks[blockIndex].get(); + auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; + + QScriptLine line; + line.from = lineStart; + line.length = lineLength; + eShapeLine(line); + + auto elideWidth = _f->elidew; + _wLeft = _w - elideWidth - _elideRemoveFromEnd; + + int firstItem = engine.findItem(line.from), lastItem = engine.findItem(line.from + line.length - 1); + int nItems = (firstItem >= 0 && lastItem >= firstItem) ? (lastItem - firstItem + 1) : 0, i; + + for (i = 0; i < nItems; ++i) { + QScriptItem &si(engine.layoutData->items[firstItem + i]); + while (nextBlock && nextBlock->from() <= _localFrom + si.position) { + currentBlock = nextBlock; + nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; + } + TextBlockType _type = currentBlock->type(); + if (si.analysis.flags == QScriptAnalysis::Object) { + if (_type == TextBlockTEmoji + || _type == TextBlockTCustomEmoji + || _type == TextBlockTSkip) { + si.width = currentBlock->f_width() + currentBlock->f_rpadding(); + } + } + if (_type == TextBlockTEmoji + || _type == TextBlockTCustomEmoji + || _type == TextBlockTSkip + || _type == TextBlockTNewline) { + if (_wLeft < si.width) { + lineText = lineText.mid(0, currentBlock->from() - _localFrom) + kQEllipsis; + lineLength = currentBlock->from() + kQEllipsis.size() - _lineStart; + _selection.to = qMin(_selection.to, currentBlock->from()); + _indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0); + setElideBidi(currentBlock->from(), kQEllipsis.size()); + elideSaveBlock(blockIndex - 1, _endBlock, currentBlock->from(), elideWidth); + return; + } + _wLeft -= si.width; + } else if (_type == TextBlockTText) { + unsigned short *logClusters = engine.logClusters(&si); + QGlyphLayout glyphs = engine.shapedGlyphs(&si); + + int itemStart = qMax(line.from, si.position), itemEnd; + int itemLength = engine.length(firstItem + i); + int glyphsStart = logClusters[itemStart - si.position], glyphsEnd; + if (line.from + line.length < si.position + itemLength) { + itemEnd = line.from + line.length; + glyphsEnd = logClusters[itemEnd - si.position]; + } else { + itemEnd = si.position + itemLength; + glyphsEnd = si.num_glyphs; + } + + for (auto g = glyphsStart; g < glyphsEnd; ++g) { + auto adv = glyphs.effectiveAdvance(g); + if (_wLeft < adv) { + auto pos = itemStart; + while (pos < itemEnd && logClusters[pos - si.position] < g) { + ++pos; + } + + if (lineText.size() <= pos || repeat > 3) { + lineText += kQEllipsis; + lineLength = _localFrom + pos + kQEllipsis.size() - _lineStart; + _selection.to = qMin(_selection.to, uint16(_localFrom + pos)); + _indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0); + setElideBidi(_localFrom + pos, kQEllipsis.size()); + _blocksSize = blockIndex; + _endBlock = nextBlock; + } else { + lineText = lineText.mid(0, pos); + lineLength = _localFrom + pos - _lineStart; + _blocksSize = blockIndex; + _endBlock = nextBlock; + prepareElidedLine(lineText, lineStart, lineLength, _endBlock, repeat + 1); + } + return; + } else { + _wLeft -= adv; + } + } + } + } + + int32 elideStart = _localFrom + lineText.size(); + _selection.to = qMin(_selection.to, uint16(elideStart)); + _indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0); + setElideBidi(elideStart, kQEllipsis.size()); + + lineText += kQEllipsis; + lineLength += kQEllipsis.size(); + + if (!repeat) { + for (; blockIndex < _blocksSize && _t->_blocks[blockIndex].get() != _endBlock && _t->_blocks[blockIndex]->from() < elideStart; ++blockIndex) { + } + if (blockIndex < _blocksSize) { + elideSaveBlock(blockIndex, _endBlock, elideStart, elideWidth); + } + } +} + +void Renderer::restoreAfterElided() { + if (_elideSavedBlock) { + const_cast(_t)->_blocks[_elideSavedIndex] = std::move(*_elideSavedBlock); + } +} + +// COPIED FROM qtextengine.cpp AND MODIFIED +void Renderer::eAppendItems(QScriptAnalysis *analysis, int &start, int &stop, const BidiControl &control, QChar::Direction dir) { + if (start > stop) + return; + + int level = control.level; + + if(dir != QChar::DirON && !control.override) { + // add level of run (cases I1 & I2) + if(level % 2) { + if(dir == QChar::DirL || dir == QChar::DirAN || dir == QChar::DirEN) + level++; + } else { + if(dir == QChar::DirR) + level++; + else if(dir == QChar::DirAN || dir == QChar::DirEN) + level += 2; + } + } + + QScriptAnalysis *s = analysis + start; + const QScriptAnalysis *e = analysis + stop; + while (s <= e) { + s->bidiLevel = level; + ++s; + } + ++stop; + start = stop; +} + +void Renderer::eShapeLine(const QScriptLine &line) { + int item = _e->findItem(line.from); + if (item == -1) + return; + + auto end = _e->findItem(line.from + line.length - 1, item); + auto blockIndex = _lineStartBlock; + auto currentBlock = _t->_blocks[blockIndex].get(); + auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; + eSetFont(currentBlock); + for (; item <= end; ++item) { + QScriptItem &si = _e->layoutData->items[item]; + while (nextBlock && nextBlock->from() <= _localFrom + si.position) { + currentBlock = nextBlock; + nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; + eSetFont(currentBlock); + } + _e->shape(item); + } +} + +style::font Renderer::applyFlags(int32 flags, const style::font &f) { + if (!flags) { + return f; + } + auto result = f; + if (IsMono(flags)) { + result = result->monospace(); + } else { + if (flags & TextBlockFBold) { + result = result->bold(); + } else if (flags & TextBlockFSemibold) { + result = result->semibold(); + } + if (flags & TextBlockFItalic) result = result->italic(); + if (flags & TextBlockFUnderline) result = result->underline(); + if (flags & TextBlockFStrikeOut) result = result->strikeout(); + if (flags & TextBlockFTilde) { // tilde fix in OpenSans + result = result->semibold(); + } + } + return result; +} + +void Renderer::eSetFont(const AbstractBlock *block) { + const auto flags = block->flags(); + const auto usedFont = [&] { + if (const auto index = block->lnkIndex()) { + const auto active = ClickHandler::showAsActive( + _t->_links.at(index - 1) + ) || (_palette && _palette->linkAlwaysActive > 0); + return active + ? _t->_st->linkFontOver + : _t->_st->linkFont; + } + return _t->_st->font; + }(); + const auto newFont = applyFlags(flags, usedFont); + if (newFont != _f) { + _f = (newFont->family() == _t->_st->font->family()) + ? applyFlags(flags | newFont->flags(), _t->_st->font) + : newFont; + _e->fnt = _f->f; + _e->resetFontEngineCache(); + } +} + +void Renderer::eItemize() { + _e->validate(); + if (_e->layoutData->items.size()) + return; + + int length = _e->layoutData->string.length(); + if (!length) + return; + + const ushort *string = reinterpret_cast(_e->layoutData->string.unicode()); + + auto blockIndex = _lineStartBlock; + auto currentBlock = _t->_blocks[blockIndex].get(); + auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; + + _e->layoutData->hasBidi = _parHasBidi; + auto analysis = _parAnalysis.data() + (_localFrom - _parStart); + + { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QUnicodeTools::ScriptItemArray scriptItems; + QUnicodeTools::initScripts(_e->layoutData->string, &scriptItems); + for (int i = 0; i < scriptItems.length(); ++i) { + const auto &item = scriptItems.at(i); + int end = i < scriptItems.length() - 1 ? scriptItems.at(i + 1).position : length; + for (int j = item.position; j < end; ++j) + analysis[j].script = item.script; + } +#else // Qt >= 6.0.0 + QVarLengthArray scripts(length); + QUnicodeTools::initScripts(string, length, scripts.data()); + for (int i = 0; i < length; ++i) + analysis[i].script = scripts.at(i); +#endif // Qt < 6.0.0 + } + + blockIndex = _lineStartBlock; + currentBlock = _t->_blocks[blockIndex].get(); + nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; + + auto start = string; + auto end = start + length; + while (start < end) { + while (nextBlock && nextBlock->from() <= _localFrom + (start - string)) { + currentBlock = nextBlock; + nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; + } + auto _type = currentBlock->type(); + if (_type == TextBlockTEmoji + || _type == TextBlockTCustomEmoji + || _type == TextBlockTSkip) { + analysis->script = QChar::Script_Common; + analysis->flags = QScriptAnalysis::Object; + } else { + analysis->flags = QScriptAnalysis::None; + } +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + analysis->script = hbscript_to_script(script_to_hbscript(analysis->script)); // retain the old behavior +#endif // Qt < 6.0.0 + ++start; + ++analysis; + } + + { + auto i_string = &_e->layoutData->string; + auto i_analysis = _parAnalysis.data() + (_localFrom - _parStart); + auto i_items = &_e->layoutData->items; + + blockIndex = _lineStartBlock; + currentBlock = _t->_blocks[blockIndex].get(); + nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; + auto startBlock = currentBlock; + + if (!length) { + return; + } + auto start = 0; + auto end = start + length; + for (int i = start + 1; i < end; ++i) { + while (nextBlock && nextBlock->from() <= _localFrom + i) { + currentBlock = nextBlock; + nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; + } + // According to the unicode spec we should be treating characters in the Common script + // (punctuation, spaces, etc) as being the same script as the surrounding text for the + // purpose of splitting up text. This is important because, for example, a fullstop + // (0x2E) can be used to indicate an abbreviation and so must be treated as part of a + // word. Thus it must be passed along with the word in languages that have to calculate + // word breaks. For example the thai word "[lookup-in-git]." has no word breaks + // but the word "[lookup-too]" does. + // Unfortuntely because we split up the strings for both wordwrapping and for setting + // the font and because Japanese and Chinese are also aliases of the script "Common", + // doing this would break too many things. So instead we only pass the full stop + // along, and nothing else. + if (currentBlock == startBlock + && i_analysis[i].bidiLevel == i_analysis[start].bidiLevel + && i_analysis[i].flags == i_analysis[start].flags + && (i_analysis[i].script == i_analysis[start].script || i_string->at(i) == QLatin1Char('.')) + // && i_analysis[i].flags < QScriptAnalysis::SpaceTabOrObject // only emojis are objects here, no tabs + && i - start < _MaxItemLength) + continue; + i_items->append(QScriptItem(start, i_analysis[start])); + start = i; + startBlock = currentBlock; + } + i_items->append(QScriptItem(start, i_analysis[start])); + } +} + +QChar::Direction Renderer::eSkipBoundryNeutrals(QScriptAnalysis *analysis, + const ushort *unicode, + int &sor, int &eor, BidiControl &control, + String::TextBlocks::const_iterator i) { + String::TextBlocks::const_iterator e = _t->_blocks.cend(), n = i + 1; + + QChar::Direction dir = control.basicDirection(); + int level = sor > 0 ? analysis[sor - 1].bidiLevel : control.level; + while (sor <= _parLength) { + while (i != _parStartBlock && (*i)->from() > _parStart + sor) { + n = i; + --i; + } + while (n != e && (*n)->from() <= _parStart + sor) { + i = n; + ++n; + } + + TextBlockType _itype = (*i)->type(); + if (eor == _parLength) + dir = control.basicDirection(); + else if (_itype == TextBlockTEmoji + || _itype == TextBlockTCustomEmoji) + dir = QChar::DirCS; + else if (_itype == TextBlockTSkip) + dir = QChar::DirCS; + else + dir = QChar::direction(unicode[sor]); + // Keep skipping DirBN as if it doesn't exist + if (dir != QChar::DirBN) + break; + analysis[sor++].bidiLevel = level; + } + + eor = sor; + + return dir; +} + +// creates the next QScript items. +bool Renderer::eBidiItemize(QScriptAnalysis *analysis, BidiControl &control) { + bool rightToLeft = (control.basicDirection() == 1); + bool hasBidi = rightToLeft; + + int sor = 0; + int eor = -1; + + const ushort *unicode = reinterpret_cast(_t->_text.unicode()) + _parStart; + int current = 0; + + QChar::Direction dir = rightToLeft ? QChar::DirR : QChar::DirL; + BidiStatus status; + + String::TextBlocks::const_iterator i = _parStartBlock, e = _t->_blocks.cend(), n = i + 1; + + QChar::Direction sdir; + TextBlockType _stype = (*_parStartBlock)->type(); + if (_stype == TextBlockTEmoji || _stype == TextBlockTCustomEmoji) + sdir = QChar::DirCS; + else if (_stype == TextBlockTSkip) + sdir = QChar::DirCS; + else + sdir = QChar::direction(*unicode); + if (sdir != QChar::DirL && sdir != QChar::DirR && sdir != QChar::DirEN && sdir != QChar::DirAN) + sdir = QChar::DirON; + else + dir = QChar::DirON; + + status.eor = sdir; + status.lastStrong = rightToLeft ? QChar::DirR : QChar::DirL; + status.last = status.lastStrong; + status.dir = sdir; + + while (current <= _parLength) { + while (n != e && (*n)->from() <= _parStart + current) { + i = n; + ++n; + } + + QChar::Direction dirCurrent; + TextBlockType _itype = (*i)->type(); + if (current == (int)_parLength) + dirCurrent = control.basicDirection(); + else if (_itype == TextBlockTEmoji + || _itype == TextBlockTCustomEmoji) + dirCurrent = QChar::DirCS; + else if (_itype == TextBlockTSkip) + dirCurrent = QChar::DirCS; + else + dirCurrent = QChar::direction(unicode[current]); + + switch (dirCurrent) { + + // embedding and overrides (X1-X9 in the BiDi specs) + case QChar::DirRLE: + case QChar::DirRLO: + case QChar::DirLRE: + case QChar::DirLRO: + { + bool rtl = (dirCurrent == QChar::DirRLE || dirCurrent == QChar::DirRLO); + hasBidi |= rtl; + bool override = (dirCurrent == QChar::DirLRO || dirCurrent == QChar::DirRLO); + + unsigned int level = control.level + 1; + if ((level % 2 != 0) == rtl) ++level; + if (level < _MaxBidiLevel) { + eor = current - 1; + eAppendItems(analysis, sor, eor, control, dir); + eor = current; + control.embed(rtl, override); + QChar::Direction edir = (rtl ? QChar::DirR : QChar::DirL); + dir = status.eor = edir; + status.lastStrong = edir; + } + break; + } + case QChar::DirPDF: + { + if (control.canPop()) { + if (dir != control.direction()) { + eor = current - 1; + eAppendItems(analysis, sor, eor, control, dir); + dir = control.direction(); + } + eor = current; + eAppendItems(analysis, sor, eor, control, dir); + control.pdf(); + dir = QChar::DirON; status.eor = QChar::DirON; + status.last = control.direction(); + if (control.override) + dir = control.direction(); + else + dir = QChar::DirON; + status.lastStrong = control.direction(); + } + break; + } + + // strong types + case QChar::DirL: + if (dir == QChar::DirON) + dir = QChar::DirL; + switch (status.last) + { + case QChar::DirL: + eor = current; status.eor = QChar::DirL; break; + case QChar::DirR: + case QChar::DirAL: + case QChar::DirEN: + case QChar::DirAN: + if (eor >= 0) { + eAppendItems(analysis, sor, eor, control, dir); + status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i); + } else { + eor = current; status.eor = dir; + } + break; + case QChar::DirES: + case QChar::DirET: + case QChar::DirCS: + case QChar::DirBN: + case QChar::DirB: + case QChar::DirS: + case QChar::DirWS: + case QChar::DirON: + if (dir != QChar::DirL) { + //last stuff takes embedding dir + if (control.direction() == QChar::DirR) { + if (status.eor != QChar::DirR) { + // AN or EN + eAppendItems(analysis, sor, eor, control, dir); + status.eor = QChar::DirON; + dir = QChar::DirR; + } + eor = current - 1; + eAppendItems(analysis, sor, eor, control, dir); + status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i); + } else { + if (status.eor != QChar::DirL) { + eAppendItems(analysis, sor, eor, control, dir); + status.eor = QChar::DirON; + dir = QChar::DirL; + } else { + eor = current; status.eor = QChar::DirL; break; + } + } + } else { + eor = current; status.eor = QChar::DirL; + } + default: + break; + } + status.lastStrong = QChar::DirL; + break; + case QChar::DirAL: + case QChar::DirR: + hasBidi = true; + if (dir == QChar::DirON) dir = QChar::DirR; + switch (status.last) + { + case QChar::DirL: + case QChar::DirEN: + case QChar::DirAN: + if (eor >= 0) + eAppendItems(analysis, sor, eor, control, dir); + // fall through + case QChar::DirR: + case QChar::DirAL: + dir = QChar::DirR; eor = current; status.eor = QChar::DirR; break; + case QChar::DirES: + case QChar::DirET: + case QChar::DirCS: + case QChar::DirBN: + case QChar::DirB: + case QChar::DirS: + case QChar::DirWS: + case QChar::DirON: + if (status.eor != QChar::DirR && status.eor != QChar::DirAL) { + //last stuff takes embedding dir + if (control.direction() == QChar::DirR + || status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) { + eAppendItems(analysis, sor, eor, control, dir); + dir = QChar::DirR; status.eor = QChar::DirON; + eor = current; + } else { + eor = current - 1; + eAppendItems(analysis, sor, eor, control, dir); + dir = QChar::DirR; status.eor = QChar::DirON; + } + } else { + eor = current; status.eor = QChar::DirR; + } + default: + break; + } + status.lastStrong = dirCurrent; + break; + + // weak types: + + case QChar::DirNSM: + if (eor == current - 1) + eor = current; + break; + case QChar::DirEN: + // if last strong was AL change EN to AN + if (status.lastStrong != QChar::DirAL) { + if (dir == QChar::DirON) { + if (status.lastStrong == QChar::DirL) + dir = QChar::DirL; + else + dir = QChar::DirEN; + } + switch (status.last) + { + case QChar::DirET: + if (status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) { + eAppendItems(analysis, sor, eor, control, dir); + status.eor = QChar::DirON; + dir = QChar::DirAN; + } + [[fallthrough]]; + case QChar::DirEN: + case QChar::DirL: + eor = current; + status.eor = dirCurrent; + break; + case QChar::DirR: + case QChar::DirAL: + case QChar::DirAN: + if (eor >= 0) + eAppendItems(analysis, sor, eor, control, dir); + else + eor = current; + status.eor = QChar::DirEN; + dir = QChar::DirAN; + break; + case QChar::DirES: + case QChar::DirCS: + if (status.eor == QChar::DirEN || dir == QChar::DirAN) { + eor = current; break; + } + [[fallthrough]]; + case QChar::DirBN: + case QChar::DirB: + case QChar::DirS: + case QChar::DirWS: + case QChar::DirON: + if (status.eor == QChar::DirR) { + // neutrals go to R + eor = current - 1; + eAppendItems(analysis, sor, eor, control, dir); + status.eor = QChar::DirEN; + dir = QChar::DirAN; + } else if (status.eor == QChar::DirL || + (status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) { + eor = current; status.eor = dirCurrent; + } else { + // numbers on both sides, neutrals get right to left direction + if (dir != QChar::DirL) { + eAppendItems(analysis, sor, eor, control, dir); + status.eor = QChar::DirON; + eor = current - 1; + dir = QChar::DirR; + eAppendItems(analysis, sor, eor, control, dir); + status.eor = QChar::DirON; + dir = QChar::DirAN; + } else { + eor = current; status.eor = dirCurrent; + } + } + [[fallthrough]]; + default: + break; + } + break; + } + [[fallthrough]]; + case QChar::DirAN: + hasBidi = true; + dirCurrent = QChar::DirAN; + if (dir == QChar::DirON) dir = QChar::DirAN; + switch (status.last) + { + case QChar::DirL: + case QChar::DirAN: + eor = current; status.eor = QChar::DirAN; + break; + case QChar::DirR: + case QChar::DirAL: + case QChar::DirEN: + if (eor >= 0) { + eAppendItems(analysis, sor, eor, control, dir); + } else { + eor = current; + } + dir = QChar::DirAN; status.eor = QChar::DirAN; + break; + case QChar::DirCS: + if (status.eor == QChar::DirAN) { + eor = current; break; + } + [[fallthrough]]; + case QChar::DirES: + case QChar::DirET: + case QChar::DirBN: + case QChar::DirB: + case QChar::DirS: + case QChar::DirWS: + case QChar::DirON: + if (status.eor == QChar::DirR) { + // neutrals go to R + eor = current - 1; + eAppendItems(analysis, sor, eor, control, dir); + status.eor = QChar::DirAN; + dir = QChar::DirAN; + } else if (status.eor == QChar::DirL || + (status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) { + eor = current; status.eor = dirCurrent; + } else { + // numbers on both sides, neutrals get right to left direction + if (dir != QChar::DirL) { + eAppendItems(analysis, sor, eor, control, dir); + status.eor = QChar::DirON; + eor = current - 1; + dir = QChar::DirR; + eAppendItems(analysis, sor, eor, control, dir); + status.eor = QChar::DirAN; + dir = QChar::DirAN; + } else { + eor = current; status.eor = dirCurrent; + } + } + [[fallthrough]]; + default: + break; + } + break; + case QChar::DirES: + case QChar::DirCS: + break; + case QChar::DirET: + if (status.last == QChar::DirEN) { + dirCurrent = QChar::DirEN; + eor = current; status.eor = dirCurrent; + } + break; + + // boundary neutrals should be ignored + case QChar::DirBN: + break; + // neutrals + case QChar::DirB: + // ### what do we do with newline and paragraph separators that come to here? + break; + case QChar::DirS: + // ### implement rule L1 + break; + case QChar::DirWS: + case QChar::DirON: + break; + default: + break; + } + + if (current >= (int)_parLength) break; + + // set status.last as needed. + switch (dirCurrent) { + case QChar::DirET: + case QChar::DirES: + case QChar::DirCS: + case QChar::DirS: + case QChar::DirWS: + case QChar::DirON: + switch (status.last) + { + case QChar::DirL: + case QChar::DirR: + case QChar::DirAL: + case QChar::DirEN: + case QChar::DirAN: + status.last = dirCurrent; + break; + default: + status.last = QChar::DirON; + } + break; + case QChar::DirNSM: + case QChar::DirBN: + // ignore these + break; + case QChar::DirLRO: + case QChar::DirLRE: + status.last = QChar::DirL; + break; + case QChar::DirRLO: + case QChar::DirRLE: + status.last = QChar::DirR; + break; + case QChar::DirEN: + if (status.last == QChar::DirL) { + status.last = QChar::DirL; + break; + } + [[fallthrough]]; + default: + status.last = dirCurrent; + } + + ++current; + } + + eor = current - 1; // remove dummy char + + if (sor <= eor) + eAppendItems(analysis, sor, eor, control, dir); + + return hasBidi; +} + +void Renderer::applyBlockProperties(const AbstractBlock *block) { + eSetFont(block); + if (_p) { + const auto isMono = IsMono(block->flags()); + _background = {}; + if (block->spoilerIndex() && _t->_spoiler) { + const auto handler + = _t->_spoiler->links.at(block->spoilerIndex() - 1); + const auto inBack = (handler && handler->shown()); + _background.inFront = !inBack; + _background.color = inBack + ? &_palette->spoilerActiveBg + : &_palette->spoilerBg; + _background.startMs = handler ? handler->startMs() : 0; + _background.spoilerIndex = block->spoilerIndex(); + } + if (isMono && block->lnkIndex() && !_background.inFront) { + _background.selectActiveBlock = ClickHandler::showAsPressed( + _t->_links.at(block->lnkIndex() - 1)); + } + + if (isMono) { + _currentPen = &_palette->monoFg->p; + _currentPenSelected = &_palette->selectMonoFg->p; + } else if (block->lnkIndex() + || (block->flags() & TextBlockFPlainLink)) { + _currentPen = &_palette->linkFg->p; + _currentPenSelected = &_palette->selectLinkFg->p; + } else { + _currentPen = &_originalPen; + _currentPenSelected = &_originalPenSelected; + } + } +} + +} // namespace Ui::Text diff --git a/ui/text/text_renderer.h b/ui/text/text_renderer.h new file mode 100644 index 0000000..9c4c77e --- /dev/null +++ b/ui/text/text_renderer.h @@ -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 + +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 _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 _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 diff --git a/ui/text/text_spoiler_data.h b/ui/text/text_spoiler_data.h index fd68360..aae50f6 100644 --- a/ui/text/text_spoiler_data.h +++ b/ui/text/text_spoiler_data.h @@ -6,16 +6,18 @@ // #pragma once +#include "ui/effects/spoiler_mess.h" + class SpoilerClickHandler; namespace Ui::Text { struct SpoilerData { - struct { - std::array corners; - QColor color; - } spoilerCache, spoilerShownCache; + explicit SpoilerData(Fn repaint) + : animation(std::move(repaint)) { + } + SpoilerAnimation animation; QVector> links; }; diff --git a/ui/toast/toast_widget.cpp b/ui/toast/toast_widget.cpp index 8978989..495d7f0 100644 --- a/ui/toast/toast_widget.cpp +++ b/ui/toast/toast_widget.cpp @@ -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" diff --git a/ui/widgets/buttons.cpp b/ui/widgets/buttons.cpp index d2f5b4d..adcffdd 100644 --- a/ui/widgets/buttons.cpp +++ b/ui/widgets/buttons.cpp @@ -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.); diff --git a/ui/widgets/checkbox.cpp b/ui/widgets/checkbox.cpp index 7338158..0836cdb 100644 --- a/ui/widgets/checkbox.cpp +++ b/ui/widgets/checkbox.cpp @@ -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 @@ -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(); diff --git a/ui/widgets/checkbox.h b/ui/widgets/checkbox.h index a33f4c5..5ec517f 100644 --- a/ui/widgets/checkbox.h +++ b/ui/widgets/checkbox.h @@ -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 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 _st; diff --git a/ui/widgets/icon_button_with_text.cpp b/ui/widgets/icon_button_with_text.cpp index ec4b85a..608f705 100644 --- a/ui/widgets/icon_button_with_text.cpp +++ b/ui/widgets/icon_button_with_text.cpp @@ -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); diff --git a/ui/widgets/input_fields.cpp b/ui/widgets/input_fields.cpp index 490a025..0f5e2cb 100644 --- a/ui/widgets/input_fields.cpp +++ b/ui/widgets/input_fields.cpp @@ -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); } diff --git a/ui/widgets/input_fields.h b/ui/widgets/input_fields.h index cfd3440..5ab622a 100644 --- a/ui/widgets/input_fields.h +++ b/ui/widgets/input_fields.h @@ -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); diff --git a/ui/widgets/labels.cpp b/ui/widgets/labels.cpp index 034c75e..3225978 100644 --- a/ui/widgets/labels.cpp +++ b/ui/widgets/labels.cpp @@ -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" diff --git a/ui/widgets/menu/menu.cpp b/ui/widgets/menu/menu.cpp index 10197e1..e73d344 100644 --- a/ui/widgets/menu/menu.cpp +++ b/ui/widgets/menu/menu.cpp @@ -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( diff --git a/ui/widgets/menu/menu_action.cpp b/ui/widgets/menu/menu_action.cpp index 8d7d9dd..1963eb5 100644 --- a/ui/widgets/menu/menu_action.cpp +++ b/ui/widgets/menu/menu_action.cpp @@ -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); } diff --git a/ui/widgets/menu/menu_action.h b/ui/widgets/menu/menu_action.h index 482f289..2e92a1a 100644 --- a/ui/widgets/menu/menu_action.h +++ b/ui/widgets/menu/menu_action.h @@ -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: diff --git a/ui/widgets/menu/menu_toggle.cpp b/ui/widgets/menu/menu_toggle.cpp index bc0da8c..93071ea 100644 --- a/ui/widgets/menu/menu_toggle.cpp +++ b/ui/widgets/menu/menu_toggle.cpp @@ -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, diff --git a/ui/widgets/separate_panel.cpp b/ui/widgets/separate_panel.cpp index 443faff..d2acf72 100644 --- a/ui/widgets/separate_panel.cpp +++ b/ui/widgets/separate_panel.cpp @@ -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( diff --git a/ui/widgets/separate_panel.h b/ui/widgets/separate_panel.h index 63cfbdd..f30077c 100644 --- a/ui/widgets/separate_panel.h +++ b/ui/widgets/separate_panel.h @@ -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(); diff --git a/ui/widgets/side_bar_button.cpp b/ui/widgets/side_bar_button.cpp index ba31527..c3d5d81 100644 --- a/ui/widgets/side_bar_button.cpp +++ b/ui/widgets/side_bar_button.cpp @@ -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 diff --git a/ui/widgets/time_input.cpp b/ui/widgets/time_input.cpp index a848849..d51a77b 100644 --- a/ui/widgets/time_input.cpp +++ b/ui/widgets/time_input.cpp @@ -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; diff --git a/ui/widgets/tooltip.cpp b/ui/widgets/tooltip.cpp index 135e30a..895bdde 100644 --- a/ui/widgets/tooltip.cpp +++ b/ui/widgets/tooltip.cpp @@ -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 #include