From e467bc7da974e465c0eb26698139f4e2b588f142 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 15 May 2026 15:08:40 +0400 Subject: [PATCH] Improve style, add zoom controls. --- REVIEW.md | 22 +- Telegram/CMakeLists.txt | 2 + Telegram/SourceFiles/core/core_settings.cpp | 28 +- Telegram/SourceFiles/iv/iv.style | 45 +- Telegram/SourceFiles/iv/iv_cached_media.cpp | 1155 +++++++++++++++++ Telegram/SourceFiles/iv/iv_cached_media.h | 35 + Telegram/SourceFiles/iv/iv_controller.cpp | 194 +-- Telegram/SourceFiles/iv/iv_instance.cpp | 1093 +--------------- Telegram/SourceFiles/iv/iv_zoom_controls.cpp | 225 ++++ Telegram/SourceFiles/iv/iv_zoom_controls.h | 29 + .../iv/markdown/iv_markdown_article.cpp | 140 +- .../iv/markdown/iv_markdown_article.h | 1 + .../iv_markdown_article_layout_blocks.cpp | 27 + .../iv_markdown_article_layout_blocks.h | 2 + .../iv_markdown_article_layout_structure.cpp | 32 +- .../iv/markdown/iv_markdown_article_paint.cpp | 45 +- .../iv_markdown_article_selection.cpp | 3 + .../iv/markdown/iv_markdown_article_text.cpp | 26 +- .../iv/markdown/iv_markdown_common.h | 14 +- .../iv/markdown/iv_markdown_controller.cpp | 4 + .../iv_markdown_history_view_media.cpp | 53 +- .../iv/markdown/iv_markdown_media_block.cpp | 124 +- .../iv/markdown/iv_markdown_prepare.h | 1 + .../markdown/iv_markdown_prepare_blocks.cpp | 1 - .../iv_markdown_prepare_native_blocks.cpp | 10 +- .../iv_markdown_prepare_native_richtext.cpp | 10 +- .../iv_markdown_prepare_native_richtext.h | 3 +- .../iv/markdown/iv_markdown_view.cpp | 6 + Telegram/cmake/td_iv.cmake | 2 + 29 files changed, 1760 insertions(+), 1572 deletions(-) create mode 100644 Telegram/SourceFiles/iv/iv_cached_media.cpp create mode 100644 Telegram/SourceFiles/iv/iv_cached_media.h create mode 100644 Telegram/SourceFiles/iv/iv_zoom_controls.cpp create mode 100644 Telegram/SourceFiles/iv/iv_zoom_controls.h diff --git a/REVIEW.md b/REVIEW.md index 87ebe458e4..dda77c8cfd 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -27,6 +27,10 @@ private: }; ``` +## No consecutive empty lines + +Use at most one empty line between declarations, definitions, include groups, or logical blocks. Two or more empty lines in a row add visual noise without adding structure. + ## Multi-line expressions — operators at the start of continuation lines When splitting an expression across multiple lines, place operators (like `&&`, `||`, `;`, `+`, etc.) at the **beginning** of continuation lines, not at the end of the previous line. This makes it immediately obvious from the left edge whether a line is a continuation or new code. @@ -355,6 +359,22 @@ void MyWidget::paintEvent(QPaintEvent *e) { When there are multiple local classes, put **all class definitions first**, then **all method definitions** after. This keeps the declarations readable as an overview. +## Do not repeat [[nodiscard]] on method definitions + +Put `[[nodiscard]]` on the method declaration inside the class. Do not repeat it on the out-of-class method definition. Free functions may keep `[[nodiscard]]` on their definition when that is the only declaration. + +```cpp +// BAD - duplicated attribute on the definition: +[[nodiscard]] int MyClass::value() const { + return _value; +} + +// GOOD - declaration carries the attribute, definition stays clean: +int MyClass::value() const { + return _value; +} +``` + ## Use RAII for resource cleanup When working with raw resources (Win32 HANDLEs, file descriptors, COM objects), use `gsl::finally` or a dedicated RAII wrapper for cleanup instead of calling release functions manually. Manual cleanup breaks when early returns are added later. @@ -472,7 +492,7 @@ const auto state = lifetime.make_state(); ## Use trailing return type when the return type doesn't fit on one line -When a function's return type is long enough that the declaration would need a line break between the return type and the function name, use trailing return type syntax (`auto ... -> Type`) to keep the function name on the opening line. +When a function's return type is long enough that the declaration or definition would need a line break between the return type and the function name, use trailing return type syntax (`auto ... -> Type`) to keep the function name on the opening line. ```cpp // BAD - return type orphaned on its own line: diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 6d21a6ada9..d1e5e3cd78 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1292,6 +1292,8 @@ PRIVATE intro/intro_step.h intro/intro_widget.cpp intro/intro_widget.h + iv/iv_cached_media.cpp + iv/iv_cached_media.h iv/iv_delegate_impl.cpp iv/iv_delegate_impl.h iv/iv_instance.cpp diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index 3c156814f2..2ab7958485 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -24,6 +24,8 @@ namespace Core { namespace { constexpr auto kInitialVideoQuality = 480; // Start with SD. +constexpr auto kMinIvZoom = 25; +constexpr auto kMaxIvZoom = 400; [[nodiscard]] int DefaultIvZoom() { const auto exact = cScale() * 100 / cScreenScale(); @@ -35,7 +37,10 @@ constexpr auto kInitialVideoQuality = 480; // Start with SD. } [[nodiscard]] int ResolveIvZoom(int value) { - return (value > 0) ? value : DefaultIvZoom(); + return std::clamp( + (value > 0) ? value : DefaultIvZoom(), + kMinIvZoom, + kMaxIvZoom); } [[nodiscard]] WindowPosition Deserialize(const QByteArray &data) { @@ -1798,25 +1803,26 @@ rpl::producer Settings::ivZoomValue() const { } void Settings::setIvZoom(int value) { - if (!value || value == DefaultIvZoom()) { + const auto resolved = ResolveIvZoom(value); + if (!value || resolved == ResolveIvZoom(0)) { _ivZoom = 0; return; } -#ifdef Q_OS_WIN - constexpr auto kMin = 25; - constexpr auto kMax = 500; -#else - constexpr auto kMin = 30; - constexpr auto kMax = 200; -#endif - _ivZoom = std::clamp(value, kMin, kMax); + _ivZoom = resolved; } bool Settings::normalizeIvZoom() { const auto value = _ivZoom.current(); - if (value && value == DefaultIvZoom()) { + if (!value) { + return false; + } + const auto resolved = ResolveIvZoom(value); + if (resolved == ResolveIvZoom(0)) { _ivZoom = 0; return true; + } else if (resolved != value) { + _ivZoom = resolved; + return true; } return false; } diff --git a/Telegram/SourceFiles/iv/iv.style b/Telegram/SourceFiles/iv/iv.style index 0753b60d68..078255edcd 100644 --- a/Telegram/SourceFiles/iv/iv.style +++ b/Telegram/SourceFiles/iv/iv.style @@ -268,6 +268,7 @@ MarkdownEmbedOverlay { Markdown { textPalette: TextPalette; textColor: color; + supplementaryTextColor: color; body: TextStyle; heading1: TextStyle; heading2: TextStyle; @@ -315,22 +316,22 @@ defaultMarkdownTableHeaderStyle: TextStyle(defaultMarkdownBodyStyle) { lineHeight: 24px; } defaultMarkdownBlockSkips: MarkdownBlockSkips { - paragraph: 16px; - heading: 24px; - code: 20px; - rule: 24px; - quote: 18px; - displayMath: 20px; - table: 20px; - photo: 20px; - video: 20px; - audio: 20px; - map: 20px; - channel: 20px; - groupedMedia: 20px; + paragraph: 12px; + heading: 16px; + code: 12px; + rule: 12px; + quote: 12px; + displayMath: 12px; + table: 12px; + photo: 12px; + video: 12px; + audio: 12px; + map: 12px; + channel: 8px; + groupedMedia: 12px; relatedArticle: 0px; - embedPost: 20px; - placeholder: 20px; + embedPost: 12px; + placeholder: 12px; } defaultMarkdownList: MarkdownList { indent: 28px; @@ -452,8 +453,9 @@ defaultMarkdownChannelSubtitleStyle: TextStyle(defaultMarkdownDetailsSummaryStyl } defaultMarkdownChannelButtonStyle: TextStyle(defaultMarkdownChannelTitleStyle) { } +defaultMarkdownSidesMargin: margins(18px, 0px, 18px, 0px); defaultMarkdownChannelButton: MarkdownChannelButton { - padding: margins(44px, 0px, 44px, 0px); + padding: defaultMarkdownSidesMargin; border: 0px; borderFg: windowActiveTextFg; bg: windowBgOver; @@ -462,7 +464,7 @@ defaultMarkdownChannelButton: MarkdownChannelButton { textFg: windowActiveTextFg; } defaultMarkdownChannel: MarkdownChannel { - padding: margins(44px, 8px, 44px, 8px); + padding: margins(18px, 8px, 18px, 8px); border: 0px; borderFg: inputBorderFg; bg: windowBgOver; @@ -486,12 +488,12 @@ defaultMarkdownRelatedArticleSubtitleStyle: TextStyle(defaultMarkdownBodyStyle) defaultMarkdownRelatedArticleFooterStyle: TextStyle(defaultMarkdownDetailsSummaryStyle) { } defaultMarkdownRelatedArticle: MarkdownRelatedArticle { - padding: margins(44px, 16px, 44px, 16px); + padding: margins(18px, 8px, 18px, 8px); border: 0px; borderFg: boxDividerBg; bg: windowBg; radius: 0px; - headerPadding: margins(44px, 14px, 44px, 14px); + headerPadding: margins(18px, 8px, 18px, 8px); headerBg: windowBgOver; separator: 1px; separatorFg: boxDividerBg; @@ -566,6 +568,7 @@ markdownEmbedOverlay: MarkdownEmbedOverlay { defaultMarkdown: Markdown { textPalette: defaultMarkdownTextPalette; textColor: windowFg; + supplementaryTextColor: windowSubTextFg; body: defaultMarkdownBodyStyle; heading1: TextStyle(defaultMarkdownBodyStyle) { font: font(30px semibold); @@ -592,9 +595,9 @@ defaultMarkdown: Markdown { lineHeight: 21px; } code: defaultMarkdownCodeStyle; - pagePadding: margins(0px, 26px, 0px, 32px); + pagePadding: margins(0px, 0px, 0px, 16px); pageMaxWidth: 732px; - textPadding: margins(44px, 0px, 44px, 0px); + textPadding: defaultMarkdownSidesMargin; mediaPadding: margins(0px, 0px, 0px, 0px); blockSkips: defaultMarkdownBlockSkips; list: defaultMarkdownList; diff --git a/Telegram/SourceFiles/iv/iv_cached_media.cpp b/Telegram/SourceFiles/iv/iv_cached_media.cpp new file mode 100644 index 0000000000..fb874eccc0 --- /dev/null +++ b/Telegram/SourceFiles/iv/iv_cached_media.cpp @@ -0,0 +1,1155 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "iv/iv_cached_media.h" + +#include "base/algorithm.h" +#include "base/flat_map.h" +#include "base/weak_ptr.h" +#include "core/application.h" +#include "data/data_channel.h" +#include "data/data_cloud_file.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_file_origin.h" +#include "data/data_location.h" +#include "data/data_media_types.h" +#include "data/data_peer.h" +#include "data/data_photo_media.h" +#include "data/data_session.h" +#include "data/data_web_page.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/view/history_view_element.h" +#include "history/view/media/history_view_location.h" +#include "history/view/media/history_view_photo.h" +#include "info/profile/info_profile_values.h" +#include "iv/markdown/iv_markdown_common.h" +#include "iv/markdown/iv_markdown_history_view_media.h" +#include "iv/markdown/iv_markdown_prepare.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "media/view/media_view_open_common.h" +#include "storage/file_download.h" +#include "ui/dynamic_image.h" +#include "ui/dynamic_thumbnails.h" +#include "ui/image/image.h" +#include "ui/painter.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" + +#include "styles/palette.h" +#include "styles/style_chat.h" + +#include + +#include +#include + +namespace Iv { +namespace { + +constexpr auto kGeoPointScale = 1; +constexpr auto kGeoPointZoomMin = 13; + +enum class CachedPagePhotoImageKind { + Thumbnail, + Full, +}; + +[[nodiscard]] QString SerializeNativeIvChannelContext( + uint64 channelId, + QString username) { + auto result = QString::number(channelId); + if (!username.isEmpty()) { + result += u"\n"_q + username; + } + return result; +} + +[[nodiscard]] Window::SessionController *CurrentSessionController( + not_null session) { + if (const auto window = Core::App().activeWindow()) { + if (const auto current = window->sessionController(); + current && (¤t->session() == session)) { + return current; + } + } + return nullptr; +} + +class CachedPagePhotoDynamicImage final : public Ui::DynamicImage { +public: + CachedPagePhotoDynamicImage( + std::shared_ptr<::Data::PhotoMedia> media, + not_null photo, + ::Data::FileOrigin origin, + CachedPagePhotoImageKind kind); + + [[nodiscard]] std::shared_ptr clone() override; + + [[nodiscard]] QImage image(int size) override; + + void subscribeToUpdates(Fn callback) override; + +private: + void ensureWanted(); + + [[nodiscard]] Image *resolvedImage() const; + + const std::shared_ptr<::Data::PhotoMedia> _media; + const not_null _photo; + const ::Data::FileOrigin _origin; + const CachedPagePhotoImageKind _kind; + rpl::lifetime _subscription; + +}; + +CachedPagePhotoDynamicImage::CachedPagePhotoDynamicImage( + std::shared_ptr<::Data::PhotoMedia> media, + not_null photo, + ::Data::FileOrigin origin, + CachedPagePhotoImageKind kind) +: _media(std::move(media)) +, _photo(photo) +, _origin(std::move(origin)) +, _kind(kind) { +} + +std::shared_ptr CachedPagePhotoDynamicImage::clone() { + return std::make_shared( + _media, + _photo, + _origin, + _kind); +} + +QImage CachedPagePhotoDynamicImage::image(int size) { + ensureWanted(); + if (const auto image = resolvedImage()) { + return image->original(); + } + return QImage(); +} + +void CachedPagePhotoDynamicImage::subscribeToUpdates(Fn callback) { + if (!callback) { + _subscription = {}; + return; + } + _subscription = _photo->owner().photoLoadProgress( + ) | rpl::filter([photo = _photo](not_null updated) { + return (updated == photo); + }) | rpl::on_next([callback = std::move(callback)] { + callback(); + }); +} + +void CachedPagePhotoDynamicImage::ensureWanted() { + switch (_kind) { + case CachedPagePhotoImageKind::Thumbnail: + _media->wanted(::Data::PhotoSize::Small, _origin); + break; + case CachedPagePhotoImageKind::Full: + _media->wanted(::Data::PhotoSize::Large, _origin); + break; + } +} + +Image *CachedPagePhotoDynamicImage::resolvedImage() const { + switch (_kind) { + case CachedPagePhotoImageKind::Full: + if (const auto large = _media->image(::Data::PhotoSize::Large)) { + return large; + } + [[fallthrough]]; + case CachedPagePhotoImageKind::Thumbnail: + if (const auto small = _media->image(::Data::PhotoSize::Small)) { + return small; + } else if (const auto thumbnail = _media->image(::Data::PhotoSize::Thumbnail)) { + return thumbnail; + } + return _media->thumbnailInline(); + } + return nullptr; +} + +class CachedPagePhotoRuntime final : public Markdown::PhotoRuntime { +public: + CachedPagePhotoRuntime( + not_null session, + not_null photo, + ::Data::FileOrigin origin); + + [[nodiscard]] std::shared_ptr thumbnail( + QSize size) const override; + + [[nodiscard]] std::shared_ptr full( + QSize size) const override; + + [[nodiscard]] bool loaded() const override; + + [[nodiscard]] bool loading() const override; + + [[nodiscard]] double progress() const override; + + void open(Qt::MouseButton button) const override; + +private: + const not_null _session; + const not_null _photo; + const ::Data::FileOrigin _origin; + const std::shared_ptr<::Data::PhotoMedia> _media; + +}; + +CachedPagePhotoRuntime::CachedPagePhotoRuntime( + not_null session, + not_null photo, + ::Data::FileOrigin origin) +: _session(session) +, _photo(photo) +, _origin(std::move(origin)) +, _media(photo->createMediaView()) { +} + +std::shared_ptr CachedPagePhotoRuntime::thumbnail( + QSize size) const { + _media->wanted(::Data::PhotoSize::Small, _origin); + return std::make_shared( + _media, + _photo, + _origin, + CachedPagePhotoImageKind::Thumbnail); +} + +std::shared_ptr CachedPagePhotoRuntime::full( + QSize size) const { + _media->wanted(::Data::PhotoSize::Large, _origin); + return std::make_shared( + _media, + _photo, + _origin, + CachedPagePhotoImageKind::Full); +} + +bool CachedPagePhotoRuntime::loaded() const { + _media->wanted(::Data::PhotoSize::Large, _origin); + return _media->loaded(); +} + +bool CachedPagePhotoRuntime::loading() const { + _media->wanted(::Data::PhotoSize::Large, _origin); + return _photo->displayLoading(); +} + +double CachedPagePhotoRuntime::progress() const { + _media->wanted(::Data::PhotoSize::Large, _origin); + return _media->progress(); +} + +void CachedPagePhotoRuntime::open(Qt::MouseButton button) const { + if (button != Qt::LeftButton && button != Qt::MiddleButton) { + return; + } + if (const auto window = Core::App().activeWindow()) { + const auto item = (HistoryItem*)nullptr; + window->openInMediaView({ + CurrentSessionController(_session), + _photo, + item, + MsgId(0), + PeerId(0), + }); + } +} + +[[nodiscard]] ImageWithLocation CachedPageMapImageData( + double latitude, + double longitude, + uint64 accessHash, + QSize size, + int zoom) { + const auto location = GeoPointLocation{ + .lat = latitude, + .lon = longitude, + .access = accessHash, + .width = std::max(size.width(), 1), + .height = std::max(size.height(), 1), + .zoom = std::max(zoom, kGeoPointZoomMin), + .scale = kGeoPointScale, + }; + return { + .location = ImageLocation( + { location }, + location.width, + location.height), + }; +} + +[[nodiscard]] ::Data::LocationPoint CachedPageMapPoint( + double latitude, + double longitude, + uint64 accessHash) { + const auto point = MTP_geoPoint( + MTP_flags(0), + MTP_double(longitude), + MTP_double(latitude), + MTP_long(accessHash), + MTP_int(0)); + return ::Data::LocationPoint(point.c_geoPoint()); +} + +[[nodiscard]] bool CanHostNativeIvVideoDocument( + not_null document) { + return !document->isVideoMessage() + && (document->isVideoFile() || document->isAnimation()); +} + +[[nodiscard]] ::Data::MediaFile::Args CachedPageVideoMediaArgs( + not_null session, + not_null document) { + const auto video = document->video(); + return { + .hasQualitiesList = video && !video->qualities.empty(), + .skipPremiumEffect = !session->premium(), + }; +} + +class CachedPageDocumentRuntime final : public Markdown::DocumentRuntime { +public: + CachedPageDocumentRuntime( + not_null session, + not_null document, + ::Data::FileOrigin origin); + + [[nodiscard]] std::shared_ptr thumbnail( + QSize size) const override; + + [[nodiscard]] std::shared_ptr full( + QSize size) const override; + + [[nodiscard]] bool loaded() const override; + + [[nodiscard]] bool loading() const override; + + [[nodiscard]] double progress() const override; + + void open(Qt::MouseButton button) const override; + +private: + const not_null _session; + const not_null _document; + const ::Data::FileOrigin _origin; + const std::shared_ptr<::Data::DocumentMedia> _media; + +}; + +CachedPageDocumentRuntime::CachedPageDocumentRuntime( + not_null session, + not_null document, + ::Data::FileOrigin origin) +: _session(session) +, _document(document) +, _origin(std::move(origin)) +, _media(document->createMediaView()) { +} + +std::shared_ptr CachedPageDocumentRuntime::thumbnail( + QSize size) const { + return Ui::MakeDocumentThumbnailFit(_document, _origin); +} + +std::shared_ptr CachedPageDocumentRuntime::full( + QSize size) const { + return Ui::MakeDocumentThumbnail(_document, _origin); +} + +bool CachedPageDocumentRuntime::loaded() const { + return _media->loaded(); +} + +bool CachedPageDocumentRuntime::loading() const { + return _document->displayLoading(); +} + +double CachedPageDocumentRuntime::progress() const { + return _document->progress(); +} + +void CachedPageDocumentRuntime::open(Qt::MouseButton button) const { + if (button != Qt::LeftButton && button != Qt::MiddleButton) { + return; + } + if (const auto window = Core::App().activeWindow()) { + const auto item = (HistoryItem*)nullptr; + window->openInMediaView({ + CurrentSessionController(_session), + _document, + item, + MsgId(0), + PeerId(0), + }); + } +} + +class CachedPageInlineDocumentImage final : public Ui::DynamicImage { +public: + CachedPageInlineDocumentImage( + not_null document, + ::Data::FileOrigin origin, + QSize requestedSize); + + [[nodiscard]] std::shared_ptr clone() override; + + [[nodiscard]] QImage image(int size) override; + + void subscribeToUpdates(Fn callback) override; + +private: + void ensureWanted(); + + [[nodiscard]] bool fullImageLoaded() const; + [[nodiscard]] Image *resolvedFullPhotoImage() const; + [[nodiscard]] Image *resolvedPhotoImage() const; + [[nodiscard]] Image *resolvedThumbnailImage() const; + [[nodiscard]] QImage resolvedDocumentImage(); + [[nodiscard]] QImage prepareImage( + QImage image, + int size, + bool full = false) const; + [[nodiscard]] QSize requestedSize(int size) const; + + const not_null _document; + const ::Data::FileOrigin _origin; + const QSize _requestedSize; + const std::shared_ptr<::Data::DocumentMedia> _media; + const std::shared_ptr<::Data::PhotoMedia> _photoMedia; + QImage _documentImage; + mutable QImage _cached; + bool _documentImageRead = false; + mutable bool _cachedFull = false; + rpl::lifetime _subscription; + +}; + +[[nodiscard]] std::shared_ptr<::Data::PhotoMedia> CachedPageInlinePhotoMedia( + not_null document) { + const auto photo = document->goodThumbnailPhoto(); + return photo ? photo->createMediaView() : nullptr; +} + +CachedPageInlineDocumentImage::CachedPageInlineDocumentImage( + not_null document, + ::Data::FileOrigin origin, + QSize requestedSize) +: _document(document) +, _origin(std::move(origin)) +, _requestedSize(requestedSize) +, _media(document->createMediaView()) +, _photoMedia(CachedPageInlinePhotoMedia(document)) { +} + +std::shared_ptr CachedPageInlineDocumentImage::clone() { + return std::make_shared( + _document, + _origin, + _requestedSize); +} + +QImage CachedPageInlineDocumentImage::image(int size) { + ensureWanted(); + if (const auto image = resolvedFullPhotoImage()) { + return prepareImage(image->original(), size, true); + } else if (auto image = resolvedDocumentImage(); !image.isNull()) { + return prepareImage(std::move(image), size, true); + } else if (const auto image = resolvedPhotoImage()) { + return prepareImage(image->original(), size); + } else if (const auto thumbnail = resolvedThumbnailImage()) { + return prepareImage(thumbnail->original(), size); + } + return QImage(); +} + +void CachedPageInlineDocumentImage::subscribeToUpdates(Fn callback) { + _subscription.destroy(); + if (!callback + || (!_photoMedia + && !_document->isImage() + && !_document->hasThumbnail())) { + return; + } + ensureWanted(); + if (fullImageLoaded()) { + return; + } + _document->session().downloaderTaskFinished( + ) | rpl::filter([=] { + return fullImageLoaded() + || (!_photoMedia + && !_document->isImage() + && resolvedThumbnailImage()); + }) | rpl::take(1) | rpl::on_next(std::move(callback), _subscription); +} + +void CachedPageInlineDocumentImage::ensureWanted() { + if (_photoMedia) { + _photoMedia->wanted(::Data::PhotoSize::Large, _origin); + } + if (_document->isImage()) { + _document->forceToCache(true); + _document->save(_origin, QString(), LoadFromCloudOrLocal, true); + } else { + _media->thumbnailWanted(_origin); + } +} + +bool CachedPageInlineDocumentImage::fullImageLoaded() const { + return resolvedFullPhotoImage() + || (_document->isImage() && _media->loaded(true)); +} + +Image *CachedPageInlineDocumentImage::resolvedFullPhotoImage() const { + return _photoMedia + ? _photoMedia->image(::Data::PhotoSize::Large) + : nullptr; +} + +Image *CachedPageInlineDocumentImage::resolvedPhotoImage() const { + if (!_photoMedia) { + return nullptr; + } else if (const auto small = _photoMedia->image(::Data::PhotoSize::Small)) { + return small; + } else if (const auto thumbnail = _photoMedia->image( + ::Data::PhotoSize::Thumbnail)) { + return thumbnail; + } + return _photoMedia->thumbnailInline(); +} + +Image *CachedPageInlineDocumentImage::resolvedThumbnailImage() const { + if (const auto image = _media->thumbnail()) { + return image; + } + return _media->thumbnailInline(); +} + +QImage CachedPageInlineDocumentImage::resolvedDocumentImage() { + if (!_document->isImage() || !_media->loaded(true)) { + return QImage(); + } else if (_documentImageRead) { + return _documentImage; + } + _documentImageRead = true; + _document->saveFromDataSilent(); + auto &location = _document->location(true); + if (location.accessEnable()) { + _documentImage = Images::Read({ + .path = location.name(), + .maxSize = requestedSize(0) * style::DevicePixelRatio(), + }).image; + location.accessDisable(); + } else { + _documentImage = Images::Read({ + .content = _media->bytes(), + .maxSize = requestedSize(0) * style::DevicePixelRatio(), + }).image; + } + return _documentImage; +} + +QImage CachedPageInlineDocumentImage::prepareImage( + QImage image, + int size, + bool full) const { + const auto requested = requestedSize(size); + if (requested.isEmpty() || image.isNull()) { + return image; + } + const auto ratio = style::DevicePixelRatio(); + const auto target = requested * ratio; + if (!_cached.isNull() && (_cachedFull || !full)) { + const auto cachedSize = _cached.size() / ratio; + if (cachedSize.width() == requested.width() + || cachedSize.height() == requested.height()) { + return _cached; + } + } + const auto to = image.size().scaled(requested, Qt::KeepAspectRatio); + _cached = image.scaled( + QSize(std::max(to.width(), 1), std::max(to.height(), 1)) * ratio, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + _cached.setDevicePixelRatio(style::DevicePixelRatio()); + return _cached; +} + +QSize CachedPageInlineDocumentImage::requestedSize(int size) const { + if (!_requestedSize.isEmpty()) { + return _requestedSize; + } + return (size > 0) ? QSize(size, size) : QSize(); +} + +class CachedPageMapDynamicImage final : public Ui::DynamicImage { +public: + CachedPageMapDynamicImage( + not_null<::Data::CloudImage*> data, + not_null session, + ::Data::FileOrigin origin); + + [[nodiscard]] std::shared_ptr clone() override; + + [[nodiscard]] QImage image(int size) override; + + void subscribeToUpdates(Fn callback) override; + +private: + const not_null<::Data::CloudImage*> _data; + const not_null _session; + const ::Data::FileOrigin _origin; + std::shared_ptr _view; + QImage _prepared; + int _paletteVersion = 0; + rpl::lifetime _subscription; + +}; + +CachedPageMapDynamicImage::CachedPageMapDynamicImage( + not_null<::Data::CloudImage*> data, + not_null session, + ::Data::FileOrigin origin) +: _data(data) +, _session(session) +, _origin(std::move(origin)) { +} + +std::shared_ptr CachedPageMapDynamicImage::clone() { + return std::make_shared( + _data, + _session, + _origin); +} + +QImage CachedPageMapDynamicImage::image(int size) { + const auto loaded = _view ? *_view : QImage(); + if (loaded.isNull()) { + return QImage(); + } + const auto paletteVersion = style::PaletteVersion(); + if (_prepared.size() == loaded.size() + && _prepared.devicePixelRatio() == loaded.devicePixelRatio() + && _paletteVersion == paletteVersion) { + return _prepared; + } + _paletteVersion = paletteVersion; + _prepared = loaded.copy(); + _prepared.setDevicePixelRatio(loaded.devicePixelRatio()); + const auto ratio = loaded.devicePixelRatio(); + const auto width = int(loaded.width() / ratio); + const auto height = int(loaded.height() / ratio); + const auto markerSize = std::min(width, height); + auto p = Painter(&_prepared); + auto hq = PainterHighQualityEnabler(p); + const auto pinScale = std::min({ + 1.0, + width / (st::historyMapPoint.height() * 2.5), + height / (st::historyMapPoint.height() * 2.5), + }); + const auto center = QPointF(width / 2.0, height / 2.0); + p.translate(center); + p.scale(pinScale, pinScale); + p.translate(-center); + const auto paintMarker = [&](const style::icon &icon) { + icon.paint( + p, + (width - icon.width()) / 2, + (height / 2) - icon.height(), + markerSize); + }; + paintMarker(st::historyMapPoint); + paintMarker(st::historyMapPointInner); + return _prepared; +} + +void CachedPageMapDynamicImage::subscribeToUpdates(Fn callback) { + _subscription.destroy(); + if (!callback) { + _view = nullptr; + _prepared = QImage(); + return; + } + _view = _data->createView(); + _data->load(_session, _origin); + if (!_view->isNull()) { + return; + } + _subscription = _session->downloaderTaskFinished( + ) | rpl::filter([=] { + return !_view->isNull(); + }) | rpl::take(1) | rpl::on_next([=] { + _prepared = QImage(); + callback(); + }); +} + +class CachedPageMapRuntime final : public Markdown::MapRuntime { +public: + CachedPageMapRuntime( + not_null session, + ::Data::FileOrigin origin, + double latitude, + double longitude, + uint64 accessHash, + QSize size, + int zoom); + + [[nodiscard]] std::shared_ptr thumbnail( + QSize size) const override; + + [[nodiscard]] std::shared_ptr full( + QSize size) const override; + + [[nodiscard]] bool loaded() const override; + + [[nodiscard]] bool loading() const override; + + [[nodiscard]] double progress() const override; + +private: + void ensureLoaded() const; + + const not_null _session; + const ::Data::FileOrigin _origin; + mutable ::Data::CloudImage _image; + +}; + +CachedPageMapRuntime::CachedPageMapRuntime( + not_null session, + ::Data::FileOrigin origin, + double latitude, + double longitude, + uint64 accessHash, + QSize size, + int zoom) +: _session(session) +, _origin(std::move(origin)) +, _image(session, CachedPageMapImageData( + latitude, + longitude, + accessHash, + size, + zoom)) { +} + +std::shared_ptr CachedPageMapRuntime::thumbnail( + QSize size) const { + ensureLoaded(); + return std::make_shared( + &_image, + _session, + _origin); +} + +std::shared_ptr CachedPageMapRuntime::full( + QSize size) const { ensureLoaded(); + return std::make_shared( + &_image, + _session, + _origin); +} + +bool CachedPageMapRuntime::loaded() const { + ensureLoaded(); + return _image.loadedOnce(); +} + +bool CachedPageMapRuntime::loading() const { + ensureLoaded(); + return _image.loading(); +} + +double CachedPageMapRuntime::progress() const { + ensureLoaded(); + return _image.loadedOnce() ? 1. : 0.; +} + +void CachedPageMapRuntime::ensureLoaded() const { + _image.load(_session, _origin); +} + +class CachedPageChannelRuntime final : public Markdown::ChannelRuntime { +public: + CachedPageChannelRuntime( + not_null channel, + QString context, + Fn openChannel, + Fn joinChannel); + + [[nodiscard]] bool joinVisible() const override; + + void open(Qt::MouseButton button) const override; + + void join(Qt::MouseButton button) const override; + +private: + const not_null _channel; + const QString _context; + const Fn _openChannel; + const Fn _joinChannel; + +}; + +CachedPageChannelRuntime::CachedPageChannelRuntime( + not_null channel, + QString context, + Fn openChannel, + Fn joinChannel) +: _channel(channel) +, _context(std::move(context)) +, _openChannel(std::move(openChannel)) +, _joinChannel(std::move(joinChannel)) { +} + +bool CachedPageChannelRuntime::joinVisible() const { + return !_channel->amIn(); +} + +void CachedPageChannelRuntime::open(Qt::MouseButton button) const { + if ((button == Qt::LeftButton || button == Qt::MiddleButton) + && _openChannel) { + _openChannel(_context); + } +} + +void CachedPageChannelRuntime::join(Qt::MouseButton button) const { + if ((button == Qt::LeftButton || button == Qt::MiddleButton) + && _joinChannel) { + _joinChannel(_context); + } +} + +class CachedPageMediaRuntime final : public Markdown::MediaRuntime { +public: + CachedPageMediaRuntime( + not_null session, + not_null page, + Fn openChannel, + Fn joinChannel); + + [[nodiscard]] std::shared_ptr resolveInlineImage( + uint64 documentId, + QSize size) const override; + + [[nodiscard]] std::shared_ptr resolvePhoto( + uint64 photoId) const override; + + [[nodiscard]] std::shared_ptr resolveDocument( + uint64 documentId) const override; + + [[nodiscard]] std::shared_ptr resolveMap( + double latitude, + double longitude, + uint64 accessHash, + QSize size, + int zoom) const override; + + [[nodiscard]] std::shared_ptr resolveChannel( + uint64 channelId, + const QString &username) const override; + + [[nodiscard]] rpl::producer channelJoinedChanges() const override; + + [[nodiscard]] std::shared_ptr + hostedMediaHost( + not_null controller, + not_null history) const; + + [[nodiscard]] std::shared_ptr + hostedMediaBlockFactory() const override; + +private: + void subscribeToChannel( + uint64 channelId, + not_null channel) const; + + [[nodiscard]] ::Data::FileOrigin fileOrigin() const; + + const not_null _session; + const not_null _page; + const Fn _openChannel; + const Fn _joinChannel; + mutable std::shared_ptr _hostedMediaHost; + mutable base::flat_map _channelJoinedSubscriptions; + mutable rpl::event_stream _channelJoinedChanges; + +}; + +CachedPageMediaRuntime::CachedPageMediaRuntime( + not_null session, + not_null page, + Fn openChannel, + Fn joinChannel) +: _session(session) +, _page(page) +, _openChannel(std::move(openChannel)) +, _joinChannel(std::move(joinChannel)) { +} + +std::shared_ptr CachedPageMediaRuntime::resolveInlineImage( + uint64 documentId, + QSize size) const { + const auto document = _session->data().document(DocumentId(documentId)); + if (document->isNull()) { + return nullptr; + } + return std::make_shared( + document, + fileOrigin(), + size); +} + +std::shared_ptr CachedPageMediaRuntime::resolvePhoto( + uint64 photoId) const { + const auto photo = _session->data().photo(PhotoId(photoId)); + if (photo->isNull()) { + return nullptr; + } + return std::make_shared( + _session, + photo, + fileOrigin()); +} + +std::shared_ptr CachedPageMediaRuntime::resolveDocument( + uint64 documentId) const { + const auto document = _session->data().document(DocumentId(documentId)); + if (document->isNull()) { + return nullptr; + } + return std::make_shared( + _session, + document, + fileOrigin()); +} + +std::shared_ptr CachedPageMediaRuntime::resolveMap( + double latitude, + double longitude, + uint64 accessHash, + QSize size, + int zoom) const { + return std::make_shared( + _session, + fileOrigin(), + latitude, + longitude, + accessHash, + size, + zoom); +} + +std::shared_ptr CachedPageMediaRuntime::resolveChannel( + uint64 channelId, + const QString &username) const { + const auto channel = _session->data().channel(ChannelId(channelId)); + subscribeToChannel(channelId, channel); + return std::make_shared( + channel, + SerializeNativeIvChannelContext(channelId, username), + _openChannel, + _joinChannel); +} + +rpl::producer CachedPageMediaRuntime::channelJoinedChanges() const { + return _channelJoinedChanges.events(); +} + +auto CachedPageMediaRuntime::hostedMediaHost( + not_null controller, + not_null history) const +-> std::shared_ptr { + if (!_hostedMediaHost) { + _hostedMediaHost + = std::make_shared( + controller, + history, + _page->url); + } + return _hostedMediaHost; +} + +auto CachedPageMediaRuntime::hostedMediaBlockFactory() const +-> std::shared_ptr { + const auto controller = CurrentSessionController(_session); + if (!controller || !_session->data().peerLoaded( + PeerData::kServiceNotificationsId)) { + return nullptr; + } + const auto history = _session->data().history( + PeerData::kServiceNotificationsId); + if (!history->peer->isUser()) { + return nullptr; + } + const auto host = hostedMediaHost(not_null{ controller }, history); + return std::make_shared( + base::make_weak(controller), + [session = _session, host]( + Window::SessionController *controller, + const Markdown::PreparedPhotoBlockData &prepared) { + if (!controller + || !prepared.viewerOpen + || !prepared.urlOverride.isEmpty()) { + return std::shared_ptr(); + } + const auto photo = session->data().photo(PhotoId(prepared.photoId)); + if (photo->isNull()) { + return std::shared_ptr(); + } + host->registerPhoto(photo); + + auto descriptor = Markdown::IvHistoryViewMediaDescriptor(); + descriptor.stableId = prepared.id.value; + descriptor.kind = Markdown::IvHistoryViewMediaKind::Photo; + descriptor.copyText = u"Photo"_q; + descriptor.layoutHint = QSize(prepared.width, prepared.height); + descriptor.host = host; + descriptor.mediaFactory = [photo]( + not_null view) { + return std::make_unique( + view, + view->data(), + photo, + false); + }; + const auto &pageUrl = host->pageUrl(); + descriptor.photo = std::make_shared( + session, + photo, + ::Data::FileOriginWebPage{ pageUrl }); + return Markdown::CreateIvHistoryViewMediaBlock( + controller, + std::move(descriptor)); + }, + [session = _session, host]( + Window::SessionController *controller, + const Markdown::PreparedVideoBlockData &prepared) { + if (!controller + || prepared.media.kind + != Markdown::PreparedMediaItemKind::Document) { + return std::shared_ptr(); + } + const auto document = session->data().document( + DocumentId(prepared.media.id)); + if (document->isNull() + || !CanHostNativeIvVideoDocument(document)) { + return std::shared_ptr(); + } + host->registerDocument(document); + auto media = std::make_shared<::Data::MediaFile>( + host->item(), + document, + CachedPageVideoMediaArgs(session, document)); + + auto descriptor = Markdown::IvHistoryViewMediaDescriptor(); + descriptor.stableId = prepared.id.value; + descriptor.kind = Markdown::IvHistoryViewMediaKind::Document; + descriptor.copyText = tr::lng_in_dlg_video(tr::now); + descriptor.layoutHint = QSize( + prepared.media.width, + prepared.media.height); + descriptor.host = host; + descriptor.mediaFactory = [media]( + not_null view) { + return media->createView( + view, + view->data()); + }; + descriptor.keepAlive.push_back(base::take(media)); + const auto &pageUrl = host->pageUrl(); + descriptor.document = std::make_shared( + session, + document, + ::Data::FileOriginWebPage{ pageUrl }); + return Markdown::CreateIvHistoryViewMediaBlock( + controller, + std::move(descriptor)); + }, + Markdown::IvHistoryViewMediaBlockFactory::AudioFactory(), + [session = _session, host]( + Window::SessionController *controller, + const Markdown::PreparedMapBlockData &prepared) { + if (!controller) { + return std::shared_ptr(); + } + const auto point = CachedPageMapPoint( + prepared.latitude, + prepared.longitude, + prepared.accessHash); + const auto mapImage = std::make_shared<::Data::CloudImage>( + session, + CachedPageMapImageData( + prepared.latitude, + prepared.longitude, + prepared.accessHash, + QSize(prepared.width, prepared.height), + prepared.zoom)); + const auto mapImagePtr = mapImage.get(); + + auto descriptor = Markdown::IvHistoryViewMediaDescriptor(); + descriptor.stableId = prepared.id.value; + descriptor.kind = Markdown::IvHistoryViewMediaKind::Map; + descriptor.copyText = tr::lng_maps_point(tr::now); + descriptor.layoutHint = QSize(prepared.width, prepared.height); + descriptor.host = host; + descriptor.mediaFactory = [mapImagePtr, point]( + not_null view) { + return std::make_unique( + view, + not_null{ mapImagePtr }, + point); + }; + descriptor.keepAlive.push_back(mapImage); + return Markdown::CreateIvHistoryViewMediaBlock( + controller, + std::move(descriptor)); + }); +} + +void CachedPageMediaRuntime::subscribeToChannel( + uint64 channelId, + not_null channel) const { + if (_channelJoinedSubscriptions.find(channelId) + != end(_channelJoinedSubscriptions)) { + return; + } + Info::Profile::AmInChannelValue(channel) | rpl::on_next([=](bool) { + _channelJoinedChanges.fire_copy(channelId); + }, _channelJoinedSubscriptions[channelId]); +} + +::Data::FileOrigin CachedPageMediaRuntime::fileOrigin() const { + return ::Data::FileOriginWebPage{ _page->url }; +} + +} // namespace + +auto CreateCachedPageMediaRuntime( + not_null session, + not_null page, + Fn openChannel, + Fn joinChannel) +-> std::shared_ptr { + return std::make_shared( + session, + page, + std::move(openChannel), + std::move(joinChannel)); +} + +} // namespace Iv diff --git a/Telegram/SourceFiles/iv/iv_cached_media.h b/Telegram/SourceFiles/iv/iv_cached_media.h new file mode 100644 index 0000000000..fc3f09925f --- /dev/null +++ b/Telegram/SourceFiles/iv/iv_cached_media.h @@ -0,0 +1,35 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/basic_types.h" + +#include + +#include + +struct WebPageData; + +namespace Main { +class Session; +} // namespace Main + +namespace Iv::Markdown { +class MediaRuntime; +} // namespace Iv::Markdown + +namespace Iv { + +[[nodiscard]] auto CreateCachedPageMediaRuntime( + not_null session, + not_null page, + Fn openChannel, + Fn joinChannel) +-> std::shared_ptr; + +} // namespace Iv diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp index d0635cf305..c60245541d 100644 --- a/Telegram/SourceFiles/iv/iv_controller.cpp +++ b/Telegram/SourceFiles/iv/iv_controller.cpp @@ -15,15 +15,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/qthelp_url.h" #include "core/file_utilities.h" #include "iv/iv_data.h" +#include "iv/iv_zoom_controls.h" #include "lang/lang_keys.h" #include "ui/chat/attach/attach_bot_webview.h" #include "ui/platform/ui_platform_window_title.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" -#include "ui/widgets/menu/menu_action.h" #include "ui/widgets/rp_window.h" #include "ui/widgets/popup_menu.h" -#include "ui/widgets/tooltip.h" #include "ui/wrap/fade_wrap.h" #include "ui/basic_click_handlers.h" #include "ui/painter.h" @@ -37,7 +36,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_iv.h" #include "styles/style_menu_icons.h" #include "styles/style_payments.h" // paymentsCriticalError -#include "styles/style_widgets.h" #include "styles/style_window.h" #include @@ -57,195 +55,8 @@ namespace Iv { namespace { constexpr auto kZoomStep = int(10); -constexpr auto kZoomSmallStep = int(5); -constexpr auto kZoomTinyStep = int(1); constexpr auto kDefaultZoom = int(100); -class ItemZoom final - : public Ui::Menu::Action - , public Ui::AbstractTooltipShower { -public: - ItemZoom( - not_null parent, - const not_null delegate, - const style::Menu &st); - - void init(); - - void paintEvent(QPaintEvent *event) override; - - QString tooltipText() const override; - - QPoint tooltipPos() const override; - - bool tooltipWindowActive() const override; - -private: - const not_null _delegate; - const style::Menu &_st; - Ui::Text::String _text; - -}; - -ItemZoom::ItemZoom( - not_null parent, - const not_null delegate, - const style::Menu &st) -: Ui::Menu::Action( - parent->menu(), - st, - Ui::CreateChild(parent), - nullptr, - nullptr) -, _delegate(delegate) -, _st(st) { - init(); -} - - -void ItemZoom::init() { - enableMouseSelecting(); - - AbstractButton::setDisabled(true); - - const auto processTooltip = [=](not_null w) { - w->events() | rpl::on_next([=](not_null e) { - if (e->type() == QEvent::Enter) { - Ui::Tooltip::Show(1000, this); - } else if (e->type() == QEvent::Leave) { - Ui::Tooltip::Hide(); - } - }, w->lifetime()); - }; - - const auto reset = Ui::CreateChild( - this, - rpl::single(QString()), - st::ivResetZoom); - processTooltip(reset); - const auto resetLabel = Ui::CreateChild( - reset, - tr::lng_background_reset_default(), - st::ivResetZoomLabel); - resetLabel->setAttribute(Qt::WA_TransparentForMouseEvents); - reset->setClickedCallback([this] { - _delegate->ivSetZoom(0); - }); - reset->show(); - const auto plus = Ui::CreateSimpleCircleButton( - this, - st::defaultRippleAnimationBgOver); - plus->resize(Size(st::ivZoomButtonsSize)); - plus->paintRequest() | rpl::on_next([=, fg = _st.itemFg] { - auto p = QPainter(plus); - p.setPen(fg); - p.setFont(st::normalFont); - p.drawText(plus->rect(), QChar('+'), style::al_center); - }, plus->lifetime()); - processTooltip(plus); - const auto step = [] { - return base::IsAltPressed() - ? kZoomTinyStep - : base::IsCtrlPressed() - ? kZoomSmallStep - : kZoomStep; - }; - plus->setClickedCallback([this, step] { - _delegate->ivSetZoom(_delegate->ivZoom() + step()); - }); - plus->show(); - const auto minus = Ui::CreateSimpleCircleButton( - this, - st::defaultRippleAnimationBgOver); - minus->resize(Size(st::ivZoomButtonsSize)); - minus->paintRequest() | rpl::on_next([=, fg = _st.itemFg] { - auto p = QPainter(minus); - const auto r = minus->rect(); - p.setPen(fg); - p.setFont(st::normalFont); - p.drawText( - QRectF(r).translated(0, style::ConvertFloatScale(-1)), - QChar(0x2013), - style::al_center); - }, minus->lifetime()); - processTooltip(minus); - minus->setClickedCallback([this, step] { - _delegate->ivSetZoom(_delegate->ivZoom() - step()); - }); - minus->show(); - - { - const auto maxWidthText = u"000%"_q; - _text.setText(_st.itemStyle, maxWidthText); - Ui::Menu::ItemBase::setMinWidth( - _text.maxWidth() - + st::ivResetZoomInnerPadding - + resetLabel->width() - + plus->width() - + minus->width() - + _st.itemPadding.right() * 2); - } - - _delegate->ivZoomValue( - ) | rpl::on_next([this](int value) { - _text.setText(_st.itemStyle, QString::number(value) + '%'); - update(); - }, lifetime()); - - rpl::combine( - sizeValue(), - reset->sizeValue() - ) | rpl::on_next([=](const QSize &size, const QSize &) { - reset->setFullWidth(0 - + resetLabel->width() - + st::ivResetZoomInnerPadding); - resetLabel->moveToLeft( - (reset->width() - resetLabel->width()) / 2, - (reset->height() - resetLabel->height()) / 2); - reset->moveToRight( - _st.itemPadding.right(), - (size.height() - reset->height()) / 2); - plus->moveToRight( - _st.itemPadding.right() + reset->width(), - (size.height() - plus->height()) / 2); - minus->moveToRight( - _st.itemPadding.right() + plus->width() + reset->width(), - (size.height() - minus->height()) / 2); - }, lifetime()); -} - - -void ItemZoom::paintEvent(QPaintEvent *event) { - auto p = QPainter(this); - p.setPen(_st.itemFg); - _text.draw(p, { - .position = QPoint( - _st.itemIconPosition.x(), - (height() - _text.minHeight()) / 2), - .outerWidth = width(), - .availableWidth = width(), - }); -} - - -QString ItemZoom::tooltipText() const { -#ifdef Q_OS_MAC - return tr::lng_iv_zoom_tooltip_cmd(tr::now); -#else - return tr::lng_iv_zoom_tooltip_ctrl(tr::now); -#endif -} - - -QPoint ItemZoom::tooltipPos() const { - return QCursor::pos(); -} - - -bool ItemZoom::tooltipWindowActive() const { - return true; -} - [[nodiscard]] QByteArray ComputeStyles(int zoom) { static const auto map = base::flat_map{ { "shadow-fg", &st::shadowFg }, @@ -1154,8 +965,7 @@ void Controller::showMenu() { }, &st::menuIconShare); _menu->addSeparator(); - _menu->addAction( - base::make_unique_q(_menu, _delegate, _menu->menu()->st())); + _menu->addAction(CreateZoomMenuAction(_menu, _delegate)); _menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); _menu->popup(_window->body()->mapToGlobal( diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp index 1712f61e42..5e628354bb 100644 --- a/Telegram/SourceFiles/iv/iv_instance.cpp +++ b/Telegram/SourceFiles/iv/iv_instance.cpp @@ -32,12 +32,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" #include "history/history_item_helpers.h" -#include "history/view/history_view_element.h" -#include "history/view/media/history_view_location.h" -#include "history/view/media/history_view_photo.h" #include "info/profile/info_profile_values.h" #include "iv/markdown/iv_markdown_controller.h" -#include "iv/markdown/iv_markdown_history_view_media.h" +#include "iv/iv_cached_media.h" #include "iv/iv_controller.h" #include "iv/iv_data.h" #include "iv/iv_prepare.h" @@ -55,19 +52,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/basic_click_handlers.h" -#include "ui/dynamic_image.h" -#include "ui/dynamic_thumbnails.h" -#include "ui/image/image.h" -#include "ui/painter.h" #include "webview/webview_data_stream_memory.h" #include "webview/webview_interface.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "window/window_session_controller_link_info.h" -#include "styles/palette.h" -#include "styles/style_chat.h" - #include #include #include @@ -84,11 +74,6 @@ constexpr auto kMaxLoadParts = 5; constexpr auto kKeepLoadingParts = 8; constexpr auto kAllowPageReloadAfter = 3 * crl::time(1000); -enum class CachedPagePhotoImageKind { - Thumbnail, - Full, -}; - struct NativeIvChannelContext { uint64 channelId = 0; QString username; @@ -105,1085 +90,12 @@ struct NativeIvChannelContext { }; } -[[nodiscard]] QString SerializeNativeIvChannelContext( - uint64 channelId, - QString username) { - auto result = QString::number(channelId); - if (!username.isEmpty()) { - result += u"\n"_q + username; - } - return result; -} - [[nodiscard]] QString ResolveNativeIvChannelUsername( const QString &channelUsername, const QString &contextUsername) { return !channelUsername.isEmpty() ? channelUsername : contextUsername; } -[[nodiscard]] Window::SessionController *CurrentSessionController( - not_null session) { - if (const auto window = Core::App().activeWindow()) { - if (const auto current = window->sessionController(); - current && (¤t->session() == session)) { - return current; - } - } - return nullptr; -} - -class CachedPagePhotoDynamicImage final : public Ui::DynamicImage { -public: - CachedPagePhotoDynamicImage( - std::shared_ptr<::Data::PhotoMedia> media, - not_null photo, - ::Data::FileOrigin origin, - CachedPagePhotoImageKind kind); - - [[nodiscard]] std::shared_ptr clone() override; - - [[nodiscard]] QImage image(int size) override; - - void subscribeToUpdates(Fn callback) override; - -private: - void ensureWanted(); - - [[nodiscard]] Image *resolvedImage() const; - - const std::shared_ptr<::Data::PhotoMedia> _media; - const not_null _photo; - const ::Data::FileOrigin _origin; - const CachedPagePhotoImageKind _kind; - rpl::lifetime _subscription; - -}; - -CachedPagePhotoDynamicImage::CachedPagePhotoDynamicImage( - std::shared_ptr<::Data::PhotoMedia> media, - not_null photo, - ::Data::FileOrigin origin, - CachedPagePhotoImageKind kind) -: _media(std::move(media)) -, _photo(photo) -, _origin(std::move(origin)) -, _kind(kind) { -} - -[[nodiscard]] std::shared_ptr CachedPagePhotoDynamicImage::clone() { - return std::make_shared( - _media, - _photo, - _origin, - _kind); -} - -[[nodiscard]] QImage CachedPagePhotoDynamicImage::image(int size) { - ensureWanted(); - if (const auto image = resolvedImage()) { - return image->original(); - } - return QImage(); -} - -void CachedPagePhotoDynamicImage::subscribeToUpdates(Fn callback) { - if (!callback) { - _subscription = {}; - return; - } - _subscription = _photo->owner().photoLoadProgress( - ) | rpl::filter([photo = _photo](not_null updated) { - return (updated == photo); - }) | rpl::on_next([callback = std::move(callback)] { - callback(); - }); -} - -void CachedPagePhotoDynamicImage::ensureWanted() { - switch (_kind) { - case CachedPagePhotoImageKind::Thumbnail: - _media->wanted(::Data::PhotoSize::Small, _origin); - break; - case CachedPagePhotoImageKind::Full: - _media->wanted(::Data::PhotoSize::Large, _origin); - break; - } -} - -[[nodiscard]] Image *CachedPagePhotoDynamicImage::resolvedImage() const { - switch (_kind) { - case CachedPagePhotoImageKind::Full: - if (const auto large = _media->image(::Data::PhotoSize::Large)) { - return large; - } - [[fallthrough]]; - case CachedPagePhotoImageKind::Thumbnail: - if (const auto small = _media->image(::Data::PhotoSize::Small)) { - return small; - } else if (const auto thumbnail = _media->image(::Data::PhotoSize::Thumbnail)) { - return thumbnail; - } - return _media->thumbnailInline(); - } - return nullptr; -} - -class CachedPagePhotoRuntime final : public Markdown::PhotoRuntime { -public: - CachedPagePhotoRuntime( - not_null session, - not_null photo, - ::Data::FileOrigin origin); - - [[nodiscard]] std::shared_ptr thumbnail( - QSize size) const override; - - [[nodiscard]] std::shared_ptr full( - QSize size) const override; - - [[nodiscard]] bool loaded() const override; - - [[nodiscard]] bool loading() const override; - - [[nodiscard]] double progress() const override; - - void open(Qt::MouseButton button) const override; - -private: - const not_null _session; - const not_null _photo; - const ::Data::FileOrigin _origin; - const std::shared_ptr<::Data::PhotoMedia> _media; - -}; - -CachedPagePhotoRuntime::CachedPagePhotoRuntime( - not_null session, - not_null photo, - ::Data::FileOrigin origin) -: _session(session) -, _photo(photo) -, _origin(std::move(origin)) -, _media(photo->createMediaView()) { -} - -[[nodiscard]] std::shared_ptr CachedPagePhotoRuntime::thumbnail( - QSize size) const { - _media->wanted(::Data::PhotoSize::Small, _origin); - return std::make_shared( - _media, - _photo, - _origin, - CachedPagePhotoImageKind::Thumbnail); -} - -[[nodiscard]] std::shared_ptr CachedPagePhotoRuntime::full( - QSize size) const { - _media->wanted(::Data::PhotoSize::Large, _origin); - return std::make_shared( - _media, - _photo, - _origin, - CachedPagePhotoImageKind::Full); -} - -[[nodiscard]] bool CachedPagePhotoRuntime::loaded() const { - _media->wanted(::Data::PhotoSize::Large, _origin); - return _media->loaded(); -} - -[[nodiscard]] bool CachedPagePhotoRuntime::loading() const { - _media->wanted(::Data::PhotoSize::Large, _origin); - return _photo->displayLoading(); -} - -[[nodiscard]] double CachedPagePhotoRuntime::progress() const { - _media->wanted(::Data::PhotoSize::Large, _origin); - return _media->progress(); -} - -void CachedPagePhotoRuntime::open(Qt::MouseButton button) const { - if (button != Qt::LeftButton && button != Qt::MiddleButton) { - return; - } - if (const auto window = Core::App().activeWindow()) { - const auto item = (HistoryItem*)nullptr; - window->openInMediaView({ - CurrentSessionController(_session), - _photo, - item, - MsgId(0), - PeerId(0), - }); - } -} - -[[nodiscard]] ImageWithLocation CachedPageMapImageData( - double latitude, - double longitude, - uint64 accessHash, - QSize size, - int zoom) { - const auto location = GeoPointLocation{ - .lat = latitude, - .lon = longitude, - .access = accessHash, - .width = std::max(size.width(), 1), - .height = std::max(size.height(), 1), - .zoom = std::max(zoom, kGeoPointZoomMin), - .scale = kGeoPointScale, - }; - return { - .location = ImageLocation( - { location }, - location.width, - location.height), - }; -} - -[[nodiscard]] ::Data::LocationPoint CachedPageMapPoint( - double latitude, - double longitude, - uint64 accessHash) { - const auto point = MTP_geoPoint( - MTP_flags(0), - MTP_double(longitude), - MTP_double(latitude), - MTP_long(accessHash), - MTP_int(0)); - return ::Data::LocationPoint(point.c_geoPoint()); -} - -[[nodiscard]] bool CanHostNativeIvVideoDocument( - not_null document) { - return !document->isVideoMessage() - && (document->isVideoFile() || document->isAnimation()); -} - -[[nodiscard]] ::Data::MediaFile::Args CachedPageVideoMediaArgs( - not_null session, - not_null document) { - const auto video = document->video(); - return { - .hasQualitiesList = video && !video->qualities.empty(), - .skipPremiumEffect = !session->premium(), - }; -} - -class CachedPageDocumentRuntime final : public Markdown::DocumentRuntime { -public: - CachedPageDocumentRuntime( - not_null session, - not_null document, - ::Data::FileOrigin origin); - - [[nodiscard]] std::shared_ptr thumbnail( - QSize size) const override; - - [[nodiscard]] std::shared_ptr full( - QSize size) const override; - - [[nodiscard]] bool loaded() const override; - - [[nodiscard]] bool loading() const override; - - [[nodiscard]] double progress() const override; - - void open(Qt::MouseButton button) const override; - -private: - const not_null _session; - const not_null _document; - const ::Data::FileOrigin _origin; - const std::shared_ptr<::Data::DocumentMedia> _media; - -}; - -CachedPageDocumentRuntime::CachedPageDocumentRuntime( - not_null session, - not_null document, - ::Data::FileOrigin origin) -: _session(session) -, _document(document) -, _origin(std::move(origin)) -, _media(document->createMediaView()) { -} - -[[nodiscard]] std::shared_ptr CachedPageDocumentRuntime::thumbnail( - QSize size) const { - return Ui::MakeDocumentThumbnailFit(_document, _origin); -} - -[[nodiscard]] std::shared_ptr CachedPageDocumentRuntime::full( - QSize size) const { - return Ui::MakeDocumentThumbnail(_document, _origin); -} - -[[nodiscard]] bool CachedPageDocumentRuntime::loaded() const { - return _media->loaded(); -} - -[[nodiscard]] bool CachedPageDocumentRuntime::loading() const { - return _document->displayLoading(); -} - -[[nodiscard]] double CachedPageDocumentRuntime::progress() const { - return _document->progress(); -} - -void CachedPageDocumentRuntime::open(Qt::MouseButton button) const { - if (button != Qt::LeftButton && button != Qt::MiddleButton) { - return; - } - if (const auto window = Core::App().activeWindow()) { - const auto item = (HistoryItem*)nullptr; - window->openInMediaView({ - CurrentSessionController(_session), - _document, - item, - MsgId(0), - PeerId(0), - }); - } -} - -class CachedPageInlineDocumentImage final : public Ui::DynamicImage { -public: - CachedPageInlineDocumentImage( - not_null document, - ::Data::FileOrigin origin, - QSize requestedSize); - - [[nodiscard]] std::shared_ptr clone() override; - - [[nodiscard]] QImage image(int size) override; - - void subscribeToUpdates(Fn callback) override; - -private: - void ensureWanted(); - - [[nodiscard]] bool fullImageLoaded() const; - [[nodiscard]] Image *resolvedFullPhotoImage() const; - [[nodiscard]] Image *resolvedPhotoImage() const; - [[nodiscard]] Image *resolvedThumbnailImage() const; - [[nodiscard]] QImage resolvedDocumentImage(); - [[nodiscard]] QImage prepareImage( - QImage image, - int size, - bool full = false) const; - [[nodiscard]] QSize requestedSize(int size) const; - - const not_null _document; - const ::Data::FileOrigin _origin; - const QSize _requestedSize; - const std::shared_ptr<::Data::DocumentMedia> _media; - const std::shared_ptr<::Data::PhotoMedia> _photoMedia; - QImage _documentImage; - mutable QImage _cached; - bool _documentImageRead = false; - mutable bool _cachedFull = false; - rpl::lifetime _subscription; - -}; - -[[nodiscard]] std::shared_ptr<::Data::PhotoMedia> CachedPageInlinePhotoMedia( - not_null document) { - const auto photo = document->goodThumbnailPhoto(); - return photo ? photo->createMediaView() : nullptr; -} - -CachedPageInlineDocumentImage::CachedPageInlineDocumentImage( - not_null document, - ::Data::FileOrigin origin, - QSize requestedSize) -: _document(document) -, _origin(std::move(origin)) -, _requestedSize(requestedSize) -, _media(document->createMediaView()) -, _photoMedia(CachedPageInlinePhotoMedia(document)) { -} - -std::shared_ptr CachedPageInlineDocumentImage::clone() { - return std::make_shared( - _document, - _origin, - _requestedSize); -} - -QImage CachedPageInlineDocumentImage::image(int size) { - ensureWanted(); - if (const auto image = resolvedFullPhotoImage()) { - return prepareImage(image->original(), size, true); - } else if (auto image = resolvedDocumentImage(); !image.isNull()) { - return prepareImage(std::move(image), size, true); - } else if (const auto image = resolvedPhotoImage()) { - return prepareImage(image->original(), size); - } else if (const auto thumbnail = resolvedThumbnailImage()) { - return prepareImage(thumbnail->original(), size); - } - return QImage(); -} - -void CachedPageInlineDocumentImage::subscribeToUpdates(Fn callback) { - _subscription.destroy(); - if (!callback) { - return; - } - ensureWanted(); - if (fullImageLoaded()) { - return; - } - _document->session().downloaderTaskFinished( - ) | rpl::filter([=] { - return fullImageLoaded() - || (!_photoMedia - && !_document->isImage() - && resolvedThumbnailImage()); - }) | rpl::take(1) | rpl::on_next(std::move(callback), _subscription); -} - -void CachedPageInlineDocumentImage::ensureWanted() { - if (_photoMedia) { - _photoMedia->wanted(::Data::PhotoSize::Large, _origin); - } - if (_document->isImage()) { - _document->forceToCache(true); - _document->save(_origin, QString(), LoadFromCloudOrLocal, true); - } else { - _media->thumbnailWanted(_origin); - } -} - -bool CachedPageInlineDocumentImage::fullImageLoaded() const { - return resolvedFullPhotoImage() - || (_document->isImage() && _media->loaded(true)); -} - -Image *CachedPageInlineDocumentImage::resolvedFullPhotoImage() const { - return _photoMedia - ? _photoMedia->image(::Data::PhotoSize::Large) - : nullptr; -} - -Image *CachedPageInlineDocumentImage::resolvedPhotoImage() const { - if (!_photoMedia) { - return nullptr; - } else if (const auto small = _photoMedia->image(::Data::PhotoSize::Small)) { - return small; - } else if (const auto thumbnail = _photoMedia->image( - ::Data::PhotoSize::Thumbnail)) { - return thumbnail; - } - return _photoMedia->thumbnailInline(); -} - -Image *CachedPageInlineDocumentImage::resolvedThumbnailImage() const { - if (const auto image = _media->thumbnail()) { - return image; - } - return _media->thumbnailInline(); -} - -QImage CachedPageInlineDocumentImage::resolvedDocumentImage() { - if (!_document->isImage() || !_media->loaded(true)) { - return QImage(); - } else if (_documentImageRead) { - return _documentImage; - } - _documentImageRead = true; - _document->saveFromDataSilent(); - auto &location = _document->location(true); - if (location.accessEnable()) { - _documentImage = Images::Read({ - .path = location.name(), - .maxSize = requestedSize(0) * style::DevicePixelRatio(), - }).image; - location.accessDisable(); - } else { - _documentImage = Images::Read({ - .content = _media->bytes(), - .maxSize = requestedSize(0) * style::DevicePixelRatio(), - }).image; - } - return _documentImage; -} - -QImage CachedPageInlineDocumentImage::prepareImage( - QImage image, - int size, - bool full) const { - const auto requested = requestedSize(size); - if (requested.isEmpty() || image.isNull()) { - return image; - } - const auto ratio = style::DevicePixelRatio(); - const auto target = requested * ratio; - if (!_cached.isNull() && (_cachedFull || !full)) { - const auto cachedSize = _cached.size() / ratio; - if (cachedSize.width() == requested.width() - || cachedSize.height() == requested.height()) { - return _cached; - } - } - const auto to = image.size().scaled(requested, Qt::KeepAspectRatio); - _cached = image.scaled( - QSize(std::max(to.width(), 1), std::max(to.height(), 1)) * ratio, - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); - _cached.setDevicePixelRatio(style::DevicePixelRatio()); - return _cached; -} - -QSize CachedPageInlineDocumentImage::requestedSize(int size) const { - if (!_requestedSize.isEmpty()) { - return _requestedSize; - } - return (size > 0) ? QSize(size, size) : QSize(); -} - -class CachedPageMapDynamicImage final : public Ui::DynamicImage { -public: - CachedPageMapDynamicImage( - not_null<::Data::CloudImage*> data, - not_null session, - ::Data::FileOrigin origin); - - [[nodiscard]] std::shared_ptr clone() override; - - [[nodiscard]] QImage image(int size) override; - - void subscribeToUpdates(Fn callback) override; - -private: - const not_null<::Data::CloudImage*> _data; - const not_null _session; - const ::Data::FileOrigin _origin; - std::shared_ptr _view; - QImage _prepared; - int _paletteVersion = 0; - rpl::lifetime _subscription; - -}; - -CachedPageMapDynamicImage::CachedPageMapDynamicImage( - not_null<::Data::CloudImage*> data, - not_null session, - ::Data::FileOrigin origin) -: _data(data) -, _session(session) -, _origin(std::move(origin)) { -} - -std::shared_ptr CachedPageMapDynamicImage::clone() { - return std::make_shared( - _data, - _session, - _origin); -} - -QImage CachedPageMapDynamicImage::image(int size) { - const auto loaded = _view ? *_view : QImage(); - if (loaded.isNull()) { - return QImage(); - } - const auto paletteVersion = style::PaletteVersion(); - if (_prepared.size() == loaded.size() - && _prepared.devicePixelRatio() == loaded.devicePixelRatio() - && _paletteVersion == paletteVersion) { - return _prepared; - } - _paletteVersion = paletteVersion; - _prepared = loaded.copy(); - _prepared.setDevicePixelRatio(loaded.devicePixelRatio()); - const auto ratio = loaded.devicePixelRatio(); - const auto width = int(loaded.width() / ratio); - const auto height = int(loaded.height() / ratio); - const auto markerSize = std::min(width, height); - auto p = Painter(&_prepared); - auto hq = PainterHighQualityEnabler(p); - const auto pinScale = std::min({ - 1.0, - width / (st::historyMapPoint.height() * 2.5), - height / (st::historyMapPoint.height() * 2.5), - }); - const auto center = QPointF(width / 2.0, height / 2.0); - p.translate(center); - p.scale(pinScale, pinScale); - p.translate(-center); - const auto paintMarker = [&](const style::icon &icon) { - icon.paint( - p, - (width - icon.width()) / 2, - (height / 2) - icon.height(), - markerSize); - }; - paintMarker(st::historyMapPoint); - paintMarker(st::historyMapPointInner); - return _prepared; -} - -void CachedPageMapDynamicImage::subscribeToUpdates(Fn callback) { - _subscription.destroy(); - if (!callback) { - _view = nullptr; - _prepared = QImage(); - return; - } - _view = _data->createView(); - _data->load(_session, _origin); - if (!_view->isNull()) { - return; - } - _subscription = _session->downloaderTaskFinished( - ) | rpl::filter([=] { - return !_view->isNull(); - }) | rpl::take(1) | rpl::on_next([=] { - _prepared = QImage(); - callback(); - }); -} - -class CachedPageMapRuntime final : public Markdown::MapRuntime { -public: - CachedPageMapRuntime( - not_null session, - ::Data::FileOrigin origin, - double latitude, - double longitude, - uint64 accessHash, - QSize size, - int zoom); - - [[nodiscard]] std::shared_ptr thumbnail( - QSize size) const override; - - [[nodiscard]] std::shared_ptr full( - QSize size) const override; - - [[nodiscard]] bool loaded() const override; - - [[nodiscard]] bool loading() const override; - - [[nodiscard]] double progress() const override; - -private: - void ensureLoaded() const; - - const not_null _session; - const ::Data::FileOrigin _origin; - mutable ::Data::CloudImage _image; - -}; - -CachedPageMapRuntime::CachedPageMapRuntime( - not_null session, - ::Data::FileOrigin origin, - double latitude, - double longitude, - uint64 accessHash, - QSize size, - int zoom) -: _session(session) -, _origin(std::move(origin)) -, _image(session, CachedPageMapImageData( - latitude, - longitude, - accessHash, - size, - zoom)) { -} - -std::shared_ptr CachedPageMapRuntime::thumbnail( - QSize size) const { - ensureLoaded(); - return std::make_shared( - &_image, - _session, - _origin); -} - -std::shared_ptr CachedPageMapRuntime::full( - QSize size) const { ensureLoaded(); - return std::make_shared( - &_image, - _session, - _origin); -} - -bool CachedPageMapRuntime::loaded() const { - ensureLoaded(); - return _image.loadedOnce(); -} - -bool CachedPageMapRuntime::loading() const { - ensureLoaded(); - return _image.loading(); -} - -double CachedPageMapRuntime::progress() const { - ensureLoaded(); - return _image.loadedOnce() ? 1. : 0.; -} - -void CachedPageMapRuntime::ensureLoaded() const { - _image.load(_session, _origin); -} - -class CachedPageChannelRuntime final : public Markdown::ChannelRuntime { -public: - CachedPageChannelRuntime( - not_null channel, - QString context, - Fn openChannel, - Fn joinChannel); - - [[nodiscard]] bool joinVisible() const override; - - void open(Qt::MouseButton button) const override; - - void join(Qt::MouseButton button) const override; - -private: - const not_null _channel; - const QString _context; - const Fn _openChannel; - const Fn _joinChannel; - -}; - -CachedPageChannelRuntime::CachedPageChannelRuntime( - not_null channel, - QString context, - Fn openChannel, - Fn joinChannel) -: _channel(channel) -, _context(std::move(context)) -, _openChannel(std::move(openChannel)) -, _joinChannel(std::move(joinChannel)) { -} - -bool CachedPageChannelRuntime::joinVisible() const { - return !_channel->amIn(); -} - -void CachedPageChannelRuntime::open(Qt::MouseButton button) const { - if ((button == Qt::LeftButton || button == Qt::MiddleButton) - && _openChannel) { - _openChannel(_context); - } -} - -void CachedPageChannelRuntime::join(Qt::MouseButton button) const { - if ((button == Qt::LeftButton || button == Qt::MiddleButton) - && _joinChannel) { - _joinChannel(_context); - } -} - -class CachedPageMediaRuntime final : public Markdown::MediaRuntime { -public: - CachedPageMediaRuntime( - not_null session, - not_null page, - Fn openChannel, - Fn joinChannel); - - [[nodiscard]] std::shared_ptr resolveInlineImage( - uint64 documentId, - QSize size) const override; - - [[nodiscard]] std::shared_ptr resolvePhoto( - uint64 photoId) const override; - - [[nodiscard]] std::shared_ptr resolveDocument( - uint64 documentId) const override; - - [[nodiscard]] std::shared_ptr resolveMap( - double latitude, - double longitude, - uint64 accessHash, - QSize size, - int zoom) const override; - - [[nodiscard]] std::shared_ptr resolveChannel( - uint64 channelId, - const QString &username) const override; - - [[nodiscard]] rpl::producer channelJoinedChanges() const override; - - [[nodiscard]] std::shared_ptr - hostedMediaHost( - not_null controller, - not_null history) const; - - [[nodiscard]] std::shared_ptr - hostedMediaBlockFactory() const override; - -private: - void subscribeToChannel( - uint64 channelId, - not_null channel) const; - - [[nodiscard]] ::Data::FileOrigin fileOrigin() const; - - const not_null _session; - const not_null _page; - const Fn _openChannel; - const Fn _joinChannel; - mutable std::shared_ptr _hostedMediaHost; - mutable base::flat_map _channelJoinedSubscriptions; - mutable rpl::event_stream _channelJoinedChanges; - -}; - -CachedPageMediaRuntime::CachedPageMediaRuntime( - not_null session, - not_null page, - Fn openChannel, - Fn joinChannel) -: _session(session) -, _page(page) -, _openChannel(std::move(openChannel)) -, _joinChannel(std::move(joinChannel)) { -} - -std::shared_ptr CachedPageMediaRuntime::resolveInlineImage( - uint64 documentId, - QSize size) const { - const auto document = _session->data().document(DocumentId(documentId)); - if (document->isNull()) { - return nullptr; - } - return std::make_shared( - document, - fileOrigin(), - size); -} - -std::shared_ptr CachedPageMediaRuntime::resolvePhoto( - uint64 photoId) const { - const auto photo = _session->data().photo(PhotoId(photoId)); - if (photo->isNull()) { - return nullptr; - } - return std::make_shared( - _session, - photo, - fileOrigin()); -} - -std::shared_ptr CachedPageMediaRuntime::resolveDocument( - uint64 documentId) const { - const auto document = _session->data().document(DocumentId(documentId)); - if (document->isNull()) { - return nullptr; - } - return std::make_shared( - _session, - document, - fileOrigin()); -} - -std::shared_ptr CachedPageMediaRuntime::resolveMap( - double latitude, - double longitude, - uint64 accessHash, - QSize size, - int zoom) const { - return std::make_shared( - _session, - fileOrigin(), - latitude, - longitude, - accessHash, - size, - zoom); -} - -std::shared_ptr CachedPageMediaRuntime::resolveChannel( - uint64 channelId, - const QString &username) const { - const auto channel = _session->data().channel(ChannelId(channelId)); - subscribeToChannel(channelId, channel); - return std::make_shared( - channel, - SerializeNativeIvChannelContext(channelId, username), - _openChannel, - _joinChannel); -} - -rpl::producer CachedPageMediaRuntime::channelJoinedChanges() const { - return _channelJoinedChanges.events(); -} - -std::shared_ptr -CachedPageMediaRuntime::hostedMediaHost( - not_null controller, - not_null history) const { - if (!_hostedMediaHost) { - _hostedMediaHost - = std::make_shared( - controller, - history, - _page->url); - } - return _hostedMediaHost; -} - -std::shared_ptr -CachedPageMediaRuntime::hostedMediaBlockFactory() const { - const auto controller = CurrentSessionController(_session); - if (!controller || !_session->data().peerLoaded( - PeerData::kServiceNotificationsId)) { - return nullptr; - } - const auto history = _session->data().history( - PeerData::kServiceNotificationsId); - if (!history->peer->isUser()) { - return nullptr; - } - const auto host = hostedMediaHost(not_null{ controller }, history); - return std::make_shared( - base::make_weak(controller), - [session = _session, host]( - Window::SessionController *controller, - const Markdown::PreparedPhotoBlockData &prepared) { - if (!controller - || !prepared.viewerOpen - || !prepared.urlOverride.isEmpty()) { - return std::shared_ptr(); - } - const auto photo = session->data().photo(PhotoId(prepared.photoId)); - if (photo->isNull()) { - return std::shared_ptr(); - } - host->registerPhoto(photo); - - auto descriptor = Markdown::IvHistoryViewMediaDescriptor(); - descriptor.stableId = prepared.id.value; - descriptor.kind = Markdown::IvHistoryViewMediaKind::Photo; - descriptor.copyText = u"Photo"_q; - descriptor.layoutHint = QSize(prepared.width, prepared.height); - descriptor.host = host; - descriptor.mediaFactory = [photo]( - not_null view) { - return std::make_unique( - view, - view->data(), - photo, - false); - }; - const auto &pageUrl = host->pageUrl(); - descriptor.photo = std::make_shared( - session, - photo, - ::Data::FileOriginWebPage{ pageUrl }); - return Markdown::CreateIvHistoryViewMediaBlock( - controller, - std::move(descriptor)); - }, - [session = _session, host]( - Window::SessionController *controller, - const Markdown::PreparedVideoBlockData &prepared) { - if (!controller - || prepared.media.kind - != Markdown::PreparedMediaItemKind::Document) { - return std::shared_ptr(); - } - const auto document = session->data().document( - DocumentId(prepared.media.id)); - if (document->isNull() - || !CanHostNativeIvVideoDocument(document)) { - return std::shared_ptr(); - } - host->registerDocument(document); - auto media = std::make_shared<::Data::MediaFile>( - host->item(), - document, - CachedPageVideoMediaArgs(session, document)); - - auto descriptor = Markdown::IvHistoryViewMediaDescriptor(); - descriptor.stableId = prepared.id.value; - descriptor.kind = Markdown::IvHistoryViewMediaKind::Document; - descriptor.copyText = tr::lng_in_dlg_video(tr::now); - descriptor.layoutHint = QSize( - prepared.media.width, - prepared.media.height); - descriptor.host = host; - descriptor.mediaFactory = [media]( - not_null view) { - return media->createView( - view, - view->data()); - }; - descriptor.keepAlive.push_back(base::take(media)); - const auto &pageUrl = host->pageUrl(); - descriptor.document = std::make_shared( - session, - document, - ::Data::FileOriginWebPage{ pageUrl }); - return Markdown::CreateIvHistoryViewMediaBlock( - controller, - std::move(descriptor)); - }, - Markdown::IvHistoryViewMediaBlockFactory::AudioFactory(), - [session = _session, host]( - Window::SessionController *controller, - const Markdown::PreparedMapBlockData &prepared) { - if (!controller) { - return std::shared_ptr(); - } - const auto point = CachedPageMapPoint( - prepared.latitude, - prepared.longitude, - prepared.accessHash); - const auto mapImage = std::make_shared<::Data::CloudImage>( - session, - CachedPageMapImageData( - prepared.latitude, - prepared.longitude, - prepared.accessHash, - QSize(prepared.width, prepared.height), - prepared.zoom)); - const auto mapImagePtr = mapImage.get(); - - auto descriptor = Markdown::IvHistoryViewMediaDescriptor(); - descriptor.stableId = prepared.id.value; - descriptor.kind = Markdown::IvHistoryViewMediaKind::Map; - descriptor.copyText = tr::lng_maps_point(tr::now); - descriptor.layoutHint = QSize(prepared.width, prepared.height); - descriptor.host = host; - descriptor.mediaFactory = [mapImagePtr, point]( - not_null view) { - return std::make_unique( - view, - not_null{ mapImagePtr }, - point); - }; - descriptor.keepAlive.push_back(mapImage); - return Markdown::CreateIvHistoryViewMediaBlock( - controller, - std::move(descriptor)); - }); -} - -void CachedPageMediaRuntime::subscribeToChannel( - uint64 channelId, - not_null channel) const { - if (_channelJoinedSubscriptions.find(channelId) - != end(_channelJoinedSubscriptions)) { - return; - } - Info::Profile::AmInChannelValue(channel) | rpl::on_next([=](bool) { - _channelJoinedChanges.fire_copy(channelId); - }, _channelJoinedSubscriptions[channelId]); -} - -::Data::FileOrigin CachedPageMediaRuntime::fileOrigin() const { - return ::Data::FileOriginWebPage{ _page->url }; -} - struct MarkdownMessageContext { ClickHandlerContext clickHandlerContext; base::weak_ptr sessionWindow; @@ -1729,6 +641,7 @@ Markdown::OpenOptions Shown::markdownOpenOptions( Qt::MouseButton button) { return activateMarkdownMedia(activation, button, *clickHandlerContext); }, + .downloadTaskFinished = page->session().downloaderTaskFinished(), }; if (!page->url.isEmpty()) { options.share = [=, url = page->url](std::shared_ptr show) { @@ -1845,7 +758,7 @@ void Shown::showMarkdownWindowed( std::shared_ptr Shown::createMediaRuntime( not_null page) const { - return std::make_shared( + return CreateCachedPageMediaRuntime( _session, page, _openChannel, diff --git a/Telegram/SourceFiles/iv/iv_zoom_controls.cpp b/Telegram/SourceFiles/iv/iv_zoom_controls.cpp new file mode 100644 index 0000000000..9301882a82 --- /dev/null +++ b/Telegram/SourceFiles/iv/iv_zoom_controls.cpp @@ -0,0 +1,225 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "iv/iv_zoom_controls.h" + +#include "base/qt/qt_key_modifiers.h" +#include "iv/iv_delegate.h" +#include "lang/lang_keys.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/menu/menu_action.h" +#include "ui/widgets/popup_menu.h" +#include "ui/widgets/tooltip.h" +#include "ui/rect.h" +#include "styles/style_iv.h" +#include "styles/style_widgets.h" + +#include +#include +#include +#include + +namespace Iv { +namespace { + +constexpr auto kZoomStep = int(10); +constexpr auto kZoomSmallStep = int(5); +constexpr auto kZoomTinyStep = int(1); + +class ZoomMenuAction final + : public Ui::Menu::Action + , public Ui::AbstractTooltipShower { +public: + ZoomMenuAction( + not_null parent, + not_null delegate, + const style::Menu &st); + + void init(); + + void paintEvent(QPaintEvent *event) override; + + QString tooltipText() const override; + + QPoint tooltipPos() const override; + + bool tooltipWindowActive() const override; + +private: + const not_null _delegate; + const style::Menu &_st; + Ui::Text::String _text; + +}; + +ZoomMenuAction::ZoomMenuAction( + not_null parent, + not_null delegate, + const style::Menu &st) +: Ui::Menu::Action( + parent->menu(), + st, + Ui::CreateChild(parent), + nullptr, + nullptr) +, _delegate(delegate) +, _st(st) { + init(); +} + +void ZoomMenuAction::init() { + enableMouseSelecting(); + + AbstractButton::setDisabled(true); + + const auto processTooltip = [=](not_null w) { + w->events() | rpl::on_next([=](not_null e) { + if (e->type() == QEvent::Enter) { + Ui::Tooltip::Show(1000, this); + } else if (e->type() == QEvent::Leave) { + Ui::Tooltip::Hide(); + } + }, w->lifetime()); + }; + + const auto reset = Ui::CreateChild( + this, + rpl::single(QString()), + st::ivResetZoom); + processTooltip(reset); + const auto resetLabel = Ui::CreateChild( + reset, + tr::lng_background_reset_default(), + st::ivResetZoomLabel); + resetLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + reset->setClickedCallback([this] { + _delegate->ivSetZoom(0); + }); + reset->show(); + const auto plus = Ui::CreateSimpleCircleButton( + this, + st::defaultRippleAnimationBgOver); + plus->resize(Size(st::ivZoomButtonsSize)); + plus->paintRequest() | rpl::on_next([=, fg = _st.itemFg] { + auto p = QPainter(plus); + p.setPen(fg); + p.setFont(st::normalFont); + p.drawText(plus->rect(), QChar('+'), style::al_center); + }, plus->lifetime()); + processTooltip(plus); + const auto step = [] { + return base::IsAltPressed() + ? kZoomTinyStep + : base::IsCtrlPressed() + ? kZoomSmallStep + : kZoomStep; + }; + plus->setClickedCallback([this, step] { + _delegate->ivSetZoom(_delegate->ivZoom() + step()); + }); + plus->show(); + const auto minus = Ui::CreateSimpleCircleButton( + this, + st::defaultRippleAnimationBgOver); + minus->resize(Size(st::ivZoomButtonsSize)); + minus->paintRequest() | rpl::on_next([=, fg = _st.itemFg] { + auto p = QPainter(minus); + const auto r = minus->rect(); + p.setPen(fg); + p.setFont(st::normalFont); + p.drawText( + QRectF(r).translated(0, style::ConvertFloatScale(-1)), + QChar(0x2013), + style::al_center); + }, minus->lifetime()); + processTooltip(minus); + minus->setClickedCallback([this, step] { + _delegate->ivSetZoom(_delegate->ivZoom() - step()); + }); + minus->show(); + + { + const auto maxWidthText = u"000%"_q; + _text.setText(_st.itemStyle, maxWidthText); + Ui::Menu::ItemBase::setMinWidth( + _text.maxWidth() + + st::ivResetZoomInnerPadding + + resetLabel->width() + + plus->width() + + minus->width() + + _st.itemPadding.right() * 2); + } + + _delegate->ivZoomValue( + ) | rpl::on_next([this](int value) { + _text.setText(_st.itemStyle, QString::number(value) + '%'); + update(); + }, lifetime()); + + rpl::combine( + sizeValue(), + reset->sizeValue() + ) | rpl::on_next([=](const QSize &size, const QSize &) { + reset->setFullWidth(0 + + resetLabel->width() + + st::ivResetZoomInnerPadding); + resetLabel->moveToLeft( + (reset->width() - resetLabel->width()) / 2, + (reset->height() - resetLabel->height()) / 2); + reset->moveToRight( + _st.itemPadding.right(), + (size.height() - reset->height()) / 2); + plus->moveToRight( + _st.itemPadding.right() + reset->width(), + (size.height() - plus->height()) / 2); + minus->moveToRight( + _st.itemPadding.right() + plus->width() + reset->width(), + (size.height() - minus->height()) / 2); + }, lifetime()); +} + +void ZoomMenuAction::paintEvent(QPaintEvent *event) { + auto p = QPainter(this); + p.setPen(_st.itemFg); + _text.draw(p, { + .position = QPoint( + _st.itemIconPosition.x(), + (height() - _text.minHeight()) / 2), + .outerWidth = width(), + .availableWidth = width(), + }); +} + +QString ZoomMenuAction::tooltipText() const { +#ifdef Q_OS_MAC + return tr::lng_iv_zoom_tooltip_cmd(tr::now); +#else + return tr::lng_iv_zoom_tooltip_ctrl(tr::now); +#endif +} + +QPoint ZoomMenuAction::tooltipPos() const { + return QCursor::pos(); +} + +bool ZoomMenuAction::tooltipWindowActive() const { + return true; +} + +} // namespace + +base::unique_qptr CreateZoomMenuAction( + not_null parent, + not_null delegate) { + return base::make_unique_q( + parent, + delegate, + parent->menu()->st()); +} + +} // namespace Iv diff --git a/Telegram/SourceFiles/iv/iv_zoom_controls.h b/Telegram/SourceFiles/iv/iv_zoom_controls.h new file mode 100644 index 0000000000..0d87346ebb --- /dev/null +++ b/Telegram/SourceFiles/iv/iv_zoom_controls.h @@ -0,0 +1,29 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/basic_types.h" +#include "base/unique_qptr.h" + +namespace Ui { +class PopupMenu; +} // namespace Ui + +namespace Ui::Menu { +class ItemBase; +} // namespace Ui::Menu + +namespace Iv { + +class Delegate; + +[[nodiscard]] base::unique_qptr CreateZoomMenuAction( + not_null parent, + not_null delegate); + +} // namespace Iv diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp index 41ae230731..e27e2262da 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp @@ -41,7 +41,7 @@ struct PendingHighlightKeyHasher { const PendingHighlightKey &key) const noexcept; }; -[[nodiscard]] size_t PendingHighlightKeyHasher::operator()( +size_t PendingHighlightKeyHasher::operator()( const PendingHighlightKey &key) const noexcept { auto result = size_t(qHash(key.text)); result = (result * 1315423911U) ^ size_t(qHash(key.language)); @@ -386,8 +386,8 @@ public: void setMediaBlockHost(MediaBlockHost *host); void setTextRepaintCallbacks( - Fn repaint, - Fn repaintRect); + Fn repaint, + Fn repaintRect); void setContent(MarkdownArticleContent content); @@ -398,15 +398,15 @@ public: void setVisibleTopBottom(int visibleTop, int visibleBottom); void paint( - Painter &p, - QRect clip, - MarkdownArticlePaintCaches caches, - MarkdownArticleSelection selection, - const MarkdownArticleSelectionEndpoints *endpoints); + Painter &p, + QRect clip, + MarkdownArticlePaintCaches caches, + MarkdownArticleSelection selection, + const MarkdownArticleSelectionEndpoints *endpoints); [[nodiscard]] MarkdownArticleHitTestResult hitTest( - QPoint point, - Ui::Text::StateRequest::Flags flags) const; + QPoint point, + Ui::Text::StateRequest::Flags flags) const; [[nodiscard]] int anchorTop(const QString &anchorId) const; @@ -417,28 +417,28 @@ public: [[nodiscard]] int segmentLength(int index) const; [[nodiscard]] int selectionOffsetFromHit( - const MarkdownArticleHitTestResult &result, - TextSelectType selectionType) const; + const MarkdownArticleHitTestResult &result, + TextSelectType selectionType) const; [[nodiscard]] TextSelection adjustSelection( - int segmentIndex, - TextSelection selection, - TextSelectType selectionType) const; + int segmentIndex, + TextSelection selection, + TextSelectType selectionType) const; [[nodiscard]] bool selectionContains( - MarkdownArticleSelection selection, - const MarkdownArticleSelectionEndpoints *endpoints, - const MarkdownArticleHitTestResult &result) const; + MarkdownArticleSelection selection, + const MarkdownArticleSelectionEndpoints *endpoints, + const MarkdownArticleHitTestResult &result) const; [[nodiscard]] TextForMimeData textForContext( - const MarkdownArticleHitTestResult &result) const; + const MarkdownArticleHitTestResult &result) const; [[nodiscard]] TextForMimeData textForSelection( - MarkdownArticleSelection selection, - const MarkdownArticleSelectionEndpoints *endpoints) const; + MarkdownArticleSelection selection, + const MarkdownArticleSelectionEndpoints *endpoints) const; [[nodiscard]] bool highlightProcessDone( - Spellchecker::HighlightProcessId processId); + Spellchecker::HighlightProcessId processId); void invalidatePaletteCache(); @@ -460,25 +460,25 @@ private: void refreshMediaBlockHosts(); [[nodiscard]] std::shared_ptr getOrCreateMediaBlock( - const PreparedBlock &prepared); + const PreparedBlock &prepared); template [[nodiscard]] std::shared_ptr getOrCreateMediaBlock( - PreparedMediaBlockId id, - Factory &&factory); + PreparedMediaBlockId id, + Factory &&factory); [[nodiscard]] Spellchecker::HighlightProcessId tryHighlightSyntax( - const QString &displayText, - const QString &language, - TextWithEntities &marked) override; + const QString &displayText, + const QString &language, + TextWithEntities &marked) override; [[nodiscard]] SegmentSpan candidateSegmentSpan(QPoint point) const; void clearPendingHighlightBlockPointers(); void registerPendingHighlightProcess( - const PendingHighlightKey &key, - Spellchecker::HighlightProcessId processId); + const PendingHighlightKey &key, + Spellchecker::HighlightProcessId processId); void registerPendingHighlightBlock(LaidOutBlock &block); @@ -499,7 +499,9 @@ private: int _height = 0; std::vector _blocks; std::unordered_map> _mediaBlocks; - std::unordered_map _relatedArticleThumbnails; + std::unordered_map< + uint64, + RelatedArticleThumbnailState> _relatedArticleThumbnails; std::unordered_map< PendingHighlightKey, Spellchecker::HighlightProcessId, @@ -521,7 +523,6 @@ MarkdownArticle::Impl::Impl(std::shared_ptr renderer) , _inlineFormulaObjects(CreateInlineFormulaObjectCache(_renderer)) { } - void MarkdownArticle::Impl::setRenderer(std::shared_ptr renderer) { _renderer = std::move(renderer); SetInlineFormulaObjectCacheRenderer(_inlineFormulaObjects, _renderer); @@ -529,13 +530,11 @@ void MarkdownArticle::Impl::setRenderer(std::shared_ptr renderer) invalidateLayout(); } - void MarkdownArticle::Impl::setMediaBlockHost(MediaBlockHost *host) { _mediaBlockHost = host; refreshMediaBlockHosts(); } - void MarkdownArticle::Impl::setTextRepaintCallbacks( Fn repaint, Fn repaintRect) { @@ -543,7 +542,6 @@ void MarkdownArticle::Impl::setTextRepaintCallbacks( _textRepaintRect = std::move(repaintRect); } - void MarkdownArticle::Impl::setContent(MarkdownArticleContent content) { clearMediaBlocks(); _relatedArticleThumbnails.clear(); @@ -553,8 +551,7 @@ void MarkdownArticle::Impl::setContent(MarkdownArticleContent content) { invalidateLayout(); } - -[[nodiscard]] int MarkdownArticle::Impl::maxWidth() { +int MarkdownArticle::Impl::maxWidth() { const auto &markdown = st::defaultMarkdown; return std::max( markdown.pageMaxWidth, @@ -563,13 +560,11 @@ void MarkdownArticle::Impl::setContent(MarkdownArticleContent content) { + 1); } - -[[nodiscard]] int MarkdownArticle::Impl::resizeGetHeight(int width) { +int MarkdownArticle::Impl::resizeGetHeight(int width) { relayout(width); return std::max(_height, 1); } - void MarkdownArticle::Impl::setVisibleTopBottom(int visibleTop, int visibleBottom) { if (visibleBottom <= visibleTop) { _visibleRange = std::nullopt; @@ -583,7 +578,6 @@ void MarkdownArticle::Impl::setVisibleTopBottom(int visibleTop, int visibleBotto refreshVisibleSegmentSpan(); } - void MarkdownArticle::Impl::paint( Painter &p, QRect clip, @@ -610,8 +604,7 @@ void MarkdownArticle::Impl::paint( clip); } - -[[nodiscard]] MarkdownArticleHitTestResult MarkdownArticle::Impl::hitTest( +MarkdownArticleHitTestResult MarkdownArticle::Impl::hitTest( QPoint point, Ui::Text::StateRequest::Flags flags) const { const auto span = candidateSegmentSpan(point); @@ -635,8 +628,7 @@ void MarkdownArticle::Impl::paint( return {}; } - -[[nodiscard]] int MarkdownArticle::Impl::anchorTop(const QString &anchorId) const { +int MarkdownArticle::Impl::anchorTop(const QString &anchorId) const { for (const auto &entry : _anchors) { if (entry.first == anchorId) { return entry.second; @@ -645,8 +637,7 @@ void MarkdownArticle::Impl::paint( return -1; } - -[[nodiscard]] bool MarkdownArticle::Impl::toggleDetails(const QString &anchorId) { +bool MarkdownArticle::Impl::toggleDetails(const QString &anchorId) { if (!ToggleDetailsBlock(&_content.blocks.blocks, anchorId)) { return false; } @@ -654,20 +645,17 @@ void MarkdownArticle::Impl::paint( return true; } - -[[nodiscard]] bool MarkdownArticle::Impl::segmentIsText(int index) const { +bool MarkdownArticle::Impl::segmentIsText(int index) const { const auto segment = FindSegment(&_segments, index); return segment && segment->isTextLeaf(); } - -[[nodiscard]] int MarkdownArticle::Impl::segmentLength(int index) const { +int MarkdownArticle::Impl::segmentLength(int index) const { const auto segment = FindSegment(&_segments, index); return segment ? SegmentLength(*segment) : 0; } - -[[nodiscard]] int MarkdownArticle::Impl::selectionOffsetFromHit( +int MarkdownArticle::Impl::selectionOffsetFromHit( const MarkdownArticleHitTestResult &result, TextSelectType selectionType) const { const auto segment = FindSegment(&_segments, result.segmentIndex); @@ -685,8 +673,7 @@ void MarkdownArticle::Impl::paint( return std::clamp(offset, 0, SegmentLength(*segment)); } - -[[nodiscard]] TextSelection MarkdownArticle::Impl::adjustSelection( +TextSelection MarkdownArticle::Impl::adjustSelection( int segmentIndex, TextSelection selection, TextSelectType selectionType) const { @@ -697,8 +684,7 @@ void MarkdownArticle::Impl::paint( return segment->leaf->adjustSelection(selection, selectionType); } - -[[nodiscard]] bool MarkdownArticle::Impl::selectionContains( +bool MarkdownArticle::Impl::selectionContains( MarkdownArticleSelection selection, const MarkdownArticleSelectionEndpoints *endpoints, const MarkdownArticleHitTestResult &result) const { @@ -726,8 +712,7 @@ void MarkdownArticle::Impl::paint( return (offset >= textSelection->from) && (offset < textSelection->to); } - -[[nodiscard]] TextForMimeData MarkdownArticle::Impl::textForContext( +TextForMimeData MarkdownArticle::Impl::textForContext( const MarkdownArticleHitTestResult &result) const { if (!result.valid() || !result.direct) { return TextForMimeData(); @@ -736,15 +721,13 @@ void MarkdownArticle::Impl::paint( return segment ? TextForSegment(*segment) : TextForMimeData(); } - -[[nodiscard]] TextForMimeData MarkdownArticle::Impl::textForSelection( +TextForMimeData MarkdownArticle::Impl::textForSelection( MarkdownArticleSelection selection, const MarkdownArticleSelectionEndpoints *endpoints) const { return TextForSelectedSegments(_segments, selection, endpoints); } - -[[nodiscard]] bool MarkdownArticle::Impl::highlightProcessDone( +bool MarkdownArticle::Impl::highlightProcessDone( Spellchecker::HighlightProcessId processId) { const auto i = _pendingHighlightEntries.find(processId); if (i == end(_pendingHighlightEntries)) { @@ -763,25 +746,21 @@ void MarkdownArticle::Impl::paint( return rebuilt; } - void MarkdownArticle::Impl::invalidatePaletteCache() { InvalidateInlineFormulaPaletteCache(_inlineFormulaObjects); ClearColorizedFormulaImages(&_blocks); } - void MarkdownArticle::Impl::invalidateRasterCache() { resetFormulaRasterCache(); InvalidateInlineFormulaRasterCache(_inlineFormulaObjects); ClearColorizedFormulaImages(&_blocks); } - -[[nodiscard]] MediaBlockHost *MarkdownArticle::Impl::mediaBlockHost() const { +MediaBlockHost *MarkdownArticle::Impl::mediaBlockHost() const { return _mediaBlockHost; } - void MarkdownArticle::Impl::invalidateLayout() { _width = -1; _height = 0; @@ -794,11 +773,10 @@ void MarkdownArticle::Impl::invalidateLayout() { _segmentBottoms.clear(); } -[[nodiscard]] int MarkdownArticle::Impl::currentDevicePixelRatio() const { +int MarkdownArticle::Impl::currentDevicePixelRatio() const { return std::max(style::DevicePixelRatio(), 1); } - void MarkdownArticle::Impl::rebuildVisibleSegmentLookup() { RebuildVisibleSegmentLookup( _segments, @@ -807,7 +785,6 @@ void MarkdownArticle::Impl::rebuildVisibleSegmentLookup() { refreshVisibleSegmentSpan(); } - void MarkdownArticle::Impl::refreshVisibleSegmentSpan() { _visibleSegmentSpan = _visibleRange ? LookupVisibleSegmentSpan( @@ -817,7 +794,6 @@ void MarkdownArticle::Impl::refreshVisibleSegmentSpan() { : SegmentSpan(); } - void MarkdownArticle::Impl::clearMediaBlocks() { for (const auto &[id, block] : _mediaBlocks) { if (block) { @@ -827,7 +803,6 @@ void MarkdownArticle::Impl::clearMediaBlocks() { _mediaBlocks.clear(); } - void MarkdownArticle::Impl::refreshMediaBlockHosts() { for (const auto &[id, block] : _mediaBlocks) { if (block) { @@ -836,8 +811,7 @@ void MarkdownArticle::Impl::refreshMediaBlockHosts() { } } - -[[nodiscard]] std::shared_ptr MarkdownArticle::Impl::getOrCreateMediaBlock( +std::shared_ptr MarkdownArticle::Impl::getOrCreateMediaBlock( const PreparedBlock &prepared) { switch (prepared.kind) { case PreparedBlockKind::Photo: @@ -893,9 +867,8 @@ void MarkdownArticle::Impl::refreshMediaBlockHosts() { } } - template -[[nodiscard]] std::shared_ptr MarkdownArticle::Impl::getOrCreateMediaBlock( +std::shared_ptr MarkdownArticle::Impl::getOrCreateMediaBlock( PreparedMediaBlockId id, Factory &&factory) { if (!id) { @@ -913,8 +886,7 @@ template return block; } - -[[nodiscard]] Spellchecker::HighlightProcessId MarkdownArticle::Impl::tryHighlightSyntax( +Spellchecker::HighlightProcessId MarkdownArticle::Impl::tryHighlightSyntax( const QString &displayText, const QString &language, TextWithEntities &marked) { @@ -933,8 +905,7 @@ template return processId; } - -[[nodiscard]] SegmentSpan MarkdownArticle::Impl::candidateSegmentSpan(QPoint point) const { +SegmentSpan MarkdownArticle::Impl::candidateSegmentSpan(QPoint point) const { if (_visibleRange && (_visibleRange->top <= point.y()) && (point.y() < _visibleRange->bottom)) { @@ -945,14 +916,12 @@ template return FullSegmentSpan(_segments); } - void MarkdownArticle::Impl::clearPendingHighlightBlockPointers() { for (auto &entry : _pendingHighlightEntries) { entry.second.blocks.clear(); } } - void MarkdownArticle::Impl::registerPendingHighlightProcess( const PendingHighlightKey &key, Spellchecker::HighlightProcessId processId) { @@ -961,7 +930,6 @@ void MarkdownArticle::Impl::registerPendingHighlightProcess( entry.key = key; } - void MarkdownArticle::Impl::registerPendingHighlightBlock(LaidOutBlock &block) { if (!block.syntaxHighlightProcessId) { return; @@ -975,7 +943,6 @@ void MarkdownArticle::Impl::registerPendingHighlightBlock(LaidOutBlock &block) { &block); } - void MarkdownArticle::Impl::registerPendingHighlightBlocks(std::vector &blocks) { for (auto &block : blocks) { registerPendingHighlightBlock(block); @@ -983,13 +950,11 @@ void MarkdownArticle::Impl::registerPendingHighlightBlocks(std::vector renderer) : _impl(std::make_unique(std::move(renderer))) { } diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h index 464b0e59c2..dffe99637a 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h @@ -27,6 +27,7 @@ struct MarkdownArticlePaintCaches { std::span colors; Fn repaint; Fn repaintRect; + std::optional supplementaryColorOverride; }; struct MarkdownArticleHitTestResult { diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp index 22a163a7cf..d8fcaed47a 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp @@ -415,6 +415,7 @@ void LayoutMediaCaption( if (prepared.text.text.isEmpty()) { return; } + block->supplementary = prepared.supplementary; const auto textBand = ArticleTextBand(left, width, markdown, context); LayoutMediaCaptionText( block, @@ -469,6 +470,15 @@ bool IsFlowKind(PreparedBlockKind kind) { || (kind == PreparedBlockKind::Heading); } +bool IsAnchorOnlyBlock(const PreparedBlock &block) { + return (block.kind == PreparedBlockKind::Paragraph) + && !block.anchorId.isEmpty() + && block.text.text.isEmpty() + && block.text.entities.empty() + && block.links.empty() + && block.children.empty(); +} + QString ListMarkerText(const PreparedBlock &block) { if (block.listKind == ListKind::Ordered) { const auto delimiter = (block.listDelimiter == ListDelimiter::Parenthesis) @@ -545,6 +555,9 @@ int TableCellTextMinResizeWidth( int BlockSkip( const PreparedBlock &block, const style::Markdown &markdown) { + if (IsAnchorOnlyBlock(block)) { + return 0; + } const auto &skips = markdown.blockSkips; switch (block.kind) { case PreparedBlockKind::Paragraph: @@ -670,7 +683,13 @@ LaidOutBlock LayoutFlowBlock( block.kind = prepared.kind; block.anchorId = prepared.anchorId; block.headingLevel = prepared.headingLevel; + block.supplementary = prepared.supplementary; block.textWidth = std::max(width, 1); + if (IsAnchorOnlyBlock(prepared)) { + block.textRect = QRect(left, top, block.textWidth, 0); + block.outer = block.textRect; + return block; + } const auto &textStyle = TextStyleFor(prepared, markdown); SetTextLeaf( @@ -853,6 +872,7 @@ LaidOutBlock LayoutTableBlock( block.anchorId = prepared.anchorId; block.tableBordered = prepared.tableBordered; block.tableStriped = prepared.tableStriped; + block.supplementary = prepared.supplementary; auto tableTop = top; if (!prepared.text.text.isEmpty()) { @@ -1094,6 +1114,7 @@ LaidOutBlock LayoutPlaceholderBlock( block.anchorId = prepared.anchorId; block.copyText = prepared.placeholder.copyText; block.labelText = prepared.placeholder.label; + block.supplementary = prepared.supplementary; const auto &style = markdown.placeholder; const auto blockWidth = std::max(width, 1); @@ -1330,6 +1351,7 @@ LaidOutBlock LayoutPhotoBlock( auto block = LaidOutBlock(); block.kind = PreparedBlockKind::Photo; block.anchorId = prepared.anchorId; + block.supplementary = prepared.supplementary; if (context.mediaBlockFactory) { block.mediaBlock = context.mediaBlockFactory(prepared); } @@ -1400,6 +1422,7 @@ LaidOutBlock LayoutVideoBlock( auto block = LaidOutBlock(); block.kind = PreparedBlockKind::Video; block.anchorId = prepared.anchorId; + block.supplementary = prepared.supplementary; if (context.mediaBlockFactory) { block.mediaBlock = context.mediaBlockFactory(prepared); } @@ -1470,6 +1493,7 @@ LaidOutBlock LayoutAudioBlock( auto block = LaidOutBlock(); block.kind = PreparedBlockKind::Audio; block.anchorId = prepared.anchorId; + block.supplementary = prepared.supplementary; if (context.mediaBlockFactory) { block.mediaBlock = context.mediaBlockFactory(prepared); } @@ -1526,6 +1550,7 @@ LaidOutBlock LayoutMapBlock( auto block = LaidOutBlock(); block.kind = PreparedBlockKind::Map; block.anchorId = prepared.anchorId; + block.supplementary = prepared.supplementary; if (context.mediaBlockFactory) { block.mediaBlock = context.mediaBlockFactory(prepared); } @@ -1591,6 +1616,7 @@ LaidOutBlock LayoutChannelBlock( auto block = LaidOutBlock(); block.kind = PreparedBlockKind::Channel; block.anchorId = prepared.anchorId; + block.supplementary = prepared.supplementary; if (context.mediaBlockFactory) { block.mediaBlock = context.mediaBlockFactory(prepared); } @@ -1645,6 +1671,7 @@ LaidOutBlock LayoutGroupedMediaBlock( auto block = LaidOutBlock(); block.kind = PreparedBlockKind::GroupedMedia; block.anchorId = prepared.anchorId; + block.supplementary = prepared.supplementary; if (context.mediaBlockFactory) { block.mediaBlock = context.mediaBlockFactory(prepared); } diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.h index 266fe66946..8d36a75844 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.h @@ -113,6 +113,7 @@ struct LaidOutBlock { bool overflowed = false; bool tableBordered = true; bool tableStriped = false; + bool supplementary = false; int segmentIndex = -1; int secondarySegmentIndex = -1; int tertiarySegmentIndex = -1; @@ -153,6 +154,7 @@ struct TableRowLayoutData { bool header = false; }; +[[nodiscard]] bool IsAnchorOnlyBlock(const PreparedBlock &block); [[nodiscard]] bool IsFlowKind(PreparedBlockKind kind); [[nodiscard]] QString ListMarkerText(const PreparedBlock &block); [[nodiscard]] int TextLineHeight(const style::TextStyle &style); diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_structure.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_structure.cpp index b542fb498b..3583d51b81 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_structure.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_structure.cpp @@ -104,6 +104,17 @@ namespace { && (next->kind == PreparedBlockKind::RelatedArticle); } +[[nodiscard]] const PreparedBlock *NextVisibleBlock( + const std::vector &blocks, + int index) { + for (auto i = index + 1, count = int(blocks.size()); i != count; ++i) { + if (!IsAnchorOnlyBlock(blocks[i])) { + return &blocks[i]; + } + } + return nullptr; +} + void PrepareNestedContext( LayoutContext *context, int left, @@ -186,6 +197,7 @@ void PrepareNestedContext( auto block = LaidOutBlock(); block.kind = PreparedBlockKind::ListItem; block.anchorId = prepared.anchorId; + block.supplementary = prepared.supplementary; block.listKind = prepared.listKind; block.listDelimiter = prepared.listDelimiter; block.taskState = prepared.taskState; @@ -314,12 +326,12 @@ void PrepareNestedContext( PrepareNestedContext(&childContext, listLeft, listWidth); auto y = top; - auto first = true; + auto previous = static_cast(nullptr); for (const auto &child : prepared.children) { - if (!first) { + const auto anchorOnly = IsAnchorOnlyBlock(child); + if (previous && !anchorOnly) { y += prepared.tight ? 0 : BlockSkip(child, markdown); } - first = false; auto laidOut = (child.kind == PreparedBlockKind::ListItem) ? LayoutListItemBlock( @@ -349,6 +361,9 @@ void PrepareNestedContext( childContext); y = BlockBottom(laidOut); block.children.push_back(std::move(laidOut)); + if (!anchorOnly) { + previous = &child; + } } block.outer = QRect( @@ -441,6 +456,7 @@ void PrepareNestedContext( block.kind = PreparedBlockKind::Details; block.anchorId = prepared.anchorId; block.collapsed = prepared.collapsed; + block.supplementary = prepared.supplementary; const auto &details = markdown.details; const auto headerWidth = std::max(width, 1); const auto iconWidth = details.icon.width(); @@ -553,6 +569,7 @@ void PrepareNestedContext( auto block = LaidOutBlock(); block.kind = PreparedBlockKind::EmbedPost; block.anchorId = prepared.anchorId; + block.supplementary = prepared.supplementary; block.thumbnailPhotoId = prepared.embedPost.authorPhotoId; if (prepared.embedPost.authorPhotoId && mediaRuntime) { block.photoRuntime = mediaRuntime->resolvePhoto( @@ -937,8 +954,9 @@ int LayoutBlocks( auto previous = static_cast(nullptr); for (auto i = 0, count = int(prepared.size()); i != count; ++i) { const auto &block = prepared[i]; - const auto next = (i + 1 < count) ? &prepared[i + 1] : nullptr; - if (previous) { + const auto anchorOnly = IsAnchorOnlyBlock(block); + const auto next = NextVisibleBlock(prepared, i); + if (previous && !anchorOnly) { y += BlockSkip(*previous, block, context, markdown); } const auto band = BlockBand( @@ -987,7 +1005,9 @@ int LayoutBlocks( } y = BlockBottom(laidOut); blocks->push_back(std::move(laidOut)); - previous = █ + if (!anchorOnly) { + previous = █ + } } return y; } diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp index a211110e8e..f8847f27f5 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp @@ -134,7 +134,9 @@ void PaintTextLeaf( leaf.draw(p, { .position = rect.topLeft(), .availableWidth = availableWidth, - .geometry = TextGeometry(availableWidth), + .geometry = elisionLines + ? Ui::Text::SimpleGeometry(availableWidth, elisionLines, 0, true) + : TextGeometry(availableWidth), .align = align, .clip = clip, .palette = &p.textPalette(), @@ -148,6 +150,27 @@ void PaintTextLeaf( }); } +[[nodiscard]] std::optional QuoteSupplementaryColor( + const MarkdownArticlePaintCaches &caches) { + if (!caches.blockquote) { + return {}; + } + return anim::color( + caches.blockquote->bg, + caches.blockquote->icon, + 0.75); +} + +void SetTextLeafPen( + Painter &p, + const LaidOutBlock &block, + const style::Markdown &markdown, + const MarkdownArticlePaintCaches &caches) { + p.setPen(!block.supplementary + ? markdown.textColor->c + : caches.supplementaryColorOverride.value_or(markdown.textColor->c)); +} + void PaintRelatedArticleTextLeaf( Painter &p, const Ui::Text::String &leaf, @@ -302,7 +325,7 @@ void PaintTableBlock( const PaintSelectionState &selectionState, QRect clip) { if (!block.textRect.isEmpty()) { - p.setPen(markdown.textColor->c); + SetTextLeafPen(p, block, markdown, caches); PaintTextLeaf( p, block.leaf, @@ -586,6 +609,8 @@ void PaintQuoteBlock( p.restore(); } + auto overriden = caches; + overriden.supplementaryColorOverride = QuoteSupplementaryColor(caches); PaintBlocks( p, block.children, @@ -595,7 +620,7 @@ void PaintQuoteBlock( devicePixelRatio, outerWidth, markdown, - caches, + overriden, selectionState, clip.intersected(block.contentRect)); } @@ -641,7 +666,7 @@ void PaintPlaceholderBlock( p.restore(); } if (!block.textRect.isEmpty()) { - p.setPen(markdown.textColor->c); + SetTextLeafPen(p, block, markdown, caches); PaintTextLeaf( p, block.leaf, @@ -752,7 +777,7 @@ void PaintEmbedPostBlock( clip.intersected(block.bodyRect)); } if (!block.textRect.isEmpty()) { - p.setPen(markdown.textColor->c); + SetTextLeafPen(p, block, markdown, caches); PaintTextLeaf( p, block.leaf, @@ -777,7 +802,7 @@ void PaintMediaCaption( if (block.textRect.isEmpty()) { return; } - p.setPen(markdown.textColor->c); + SetTextLeafPen(p, block, markdown, caches); PaintTextLeaf( p, block.leaf, @@ -914,7 +939,7 @@ void PaintRelatedArticleBlock( } } if (!block.labelRect.isEmpty()) { - p.setPen(style.titleFg->c); + p.setPen(markdown.textColor->c); PaintRelatedArticleTextLeaf( p, block.labelLeaf, @@ -925,7 +950,7 @@ void PaintRelatedArticleBlock( style.titleLines); } if (!block.subtitleRect.isEmpty()) { - p.setPen(style.subtitleFg->c); + p.setPen(markdown.textColor->c); PaintRelatedArticleTextLeaf( p, block.subtitleLeaf, @@ -936,7 +961,7 @@ void PaintRelatedArticleBlock( style.subtitleLines); } if (!block.actionRect.isEmpty()) { - p.setPen(style.footerFg->c); + p.setPen(markdown.supplementaryTextColor->c); PaintRelatedArticleTextLeaf( p, block.actionLeaf, @@ -1133,7 +1158,7 @@ void PaintBlock( if (!block.headerRect.isEmpty()) { p.fillRect(block.headerRect, markdown.relatedArticle.headerBg->c); } - p.setPen(markdown.textColor->c); + SetTextLeafPen(p, block, markdown, caches); PaintTextLeaf( p, block.leaf, diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.cpp index 9491af7294..c4e816ad77 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.cpp @@ -338,6 +338,9 @@ void CollectSelectableSegments( case PreparedBlockKind::Paragraph: case PreparedBlockKind::Heading: case PreparedBlockKind::Details: { + if (block.leaf.isEmpty() && block.textRect.isEmpty()) { + break; + } auto segment = SelectableSegment(); segment.kind = SelectableSegmentKind::TextLeaf; segment.leaf = &block.leaf; diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_text.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_text.cpp index 24fa970178..5252d01e9d 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_text.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_text.cpp @@ -152,33 +152,27 @@ PreparedLinkClickHandler::PreparedLinkClickHandler(PreparedLink link) : _link(std::move(link)) { } - void PreparedLinkClickHandler::onClick(ClickContext) const { } - -[[nodiscard]] const PreparedLink &PreparedLinkClickHandler::link() const { +const PreparedLink &PreparedLinkClickHandler::link() const { return _link; } - QString PreparedLinkClickHandler::url() const { return _link.target; } - QString PreparedLinkClickHandler::copyToClipboardText() const { return CopyTextForLink(_link); } - QString PreparedLinkClickHandler::copyToClipboardContextItemText() const { return copyToClipboardText().isEmpty() ? QString() : CopyLabelForLink(_link); } - ClickHandler::TextEntity PreparedLinkClickHandler::getTextEntity() const { return TextEntityForLink(_link); } @@ -713,8 +707,8 @@ bool InlineFormulaSharedState::failed() const { return !measured().success; } -std::optional -InlineFormulaSharedState::vertical(const style::TextStyle &textStyle) const { +auto InlineFormulaSharedState::vertical(const style::TextStyle &textStyle) const +-> std::optional { const auto &formula = measured(); const auto geometry = InlineFormulaGeometryFrom(formula); if (formula.success && (geometry.imageHeight > 0)) { @@ -879,8 +873,8 @@ QString InlineFormulaObject::entityData() { return QString(); } -std::optional -InlineFormulaObject::vertical(const style::TextStyle &textStyle) { +auto InlineFormulaObject::vertical(const style::TextStyle &textStyle) +-> std::optional { return _state ? _state->vertical(textStyle) : std::nullopt; } @@ -970,8 +964,8 @@ QString InlineIvImageObject::entityData() { return QString(); } -std::optional -InlineIvImageObject::vertical(const style::TextStyle &textStyle) { +auto InlineIvImageObject::vertical(const style::TextStyle &textStyle) +-> std::optional { if (_height > 0) { const auto line = textStyle.font->height; const auto above = _height - (_height / 2); @@ -1077,11 +1071,11 @@ std::unique_ptr InlineFormulaObjectCache::create( std::move(state)); } -std::shared_ptr -InlineFormulaObjectCache::lookupOrCreate( +auto InlineFormulaObjectCache::lookupOrCreate( const PreparedFormulaMeasurementSignature &signature, const style::TextStyle &textStyle, - const std::vector *formulas) { + const std::vector *formulas) +-> std::shared_ptr { if (const auto i = _states.find(signature); i != end(_states)) { return i->second; } diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_common.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_common.h index d85c20993a..0d22fcad53 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_common.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_common.h @@ -162,8 +162,8 @@ inline rpl::producer MediaRuntime::channelJoinedChanges() const { return rpl::never(); } -inline std::shared_ptr -MediaRuntime::hostedMediaBlockFactory() const { +inline auto MediaRuntime::hostedMediaBlockFactory() const +-> std::shared_ptr { return nullptr; } @@ -217,9 +217,13 @@ struct OpenOptions { std::shared_ptr clickHandlerContextRef; std::function openSource; std::function)> share; - std::function - ivWebviewDataRequest; - std::function activateMedia; + std::function ivWebviewDataRequest; + std::function activateMedia; + rpl::producer<> downloadTaskFinished; }; struct ParseOptions { diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.cpp index 03bbae82af..60af834bbd 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "iv/markdown/iv_markdown_parse.h" #include "iv/markdown/iv_markdown_view.h" #include "iv/iv_delegate_impl.h" +#include "iv/iv_zoom_controls.h" #include "lang/lang_keys.h" #include "ui/layers/layer_manager.h" #include "ui/layers/show.h" @@ -849,6 +850,9 @@ void Controller::showMenu() { &st::menuIconShare); } + _menu->addSeparator(); + _menu->addAction(CreateZoomMenuAction(_menu, _delegate)); + _menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); _menu->popup(_window->body()->mapToGlobal( QPoint(_window->body()->width(), 0) + st::ivMenuPosition)); diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_history_view_media.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_history_view_media.cpp index b64262bcb8..4148557288 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_history_view_media.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_history_view_media.cpp @@ -87,29 +87,24 @@ IvHistoryViewDelegate::IvHistoryViewDelegate( , _session(session) { } - HistoryView::Context IvHistoryViewDelegate::elementContext() { return HistoryView::Context::TTLViewer; } - HistoryView::ElementChatMode IvHistoryViewDelegate::elementChatMode() { return HistoryView::ElementChatMode::Default; } - bool IvHistoryViewDelegate::elementAnimationsPaused() { return false; } - void IvHistoryViewDelegate::elementOpenPhoto( not_null photo, FullMsgId context) { controller()->openPhoto(photo, { .id = context }); } - void IvHistoryViewDelegate::elementOpenDocument( not_null document, FullMsgId context, @@ -120,14 +115,12 @@ void IvHistoryViewDelegate::elementOpenDocument( { .id = context }); } - void IvHistoryViewDelegate::elementCancelUpload(const FullMsgId &context) { if (const auto item = _session->message(context)) { controller()->cancelUploadLayer(item); } } - void IvHistoryViewDelegate::elementShowTooltip( const TextWithEntities &text, Fn hiddenCallback) { @@ -286,18 +279,15 @@ IvHistoryViewBlock::IvHistoryViewBlock( }, _lifetime); } - -[[nodiscard]] uint64 IvHistoryViewBlock::stableId() const { +uint64 IvHistoryViewBlock::stableId() const { return _stableId; } - -[[nodiscard]] bool IvHistoryViewBlock::supported() const { +bool IvHistoryViewBlock::supported() const { return _supported; } - -[[nodiscard]] int IvHistoryViewBlock::resizeGetHeight(int width) { +int IvHistoryViewBlock::resizeGetHeight(int width) { if (!_media) { return 0; } @@ -305,7 +295,6 @@ IvHistoryViewBlock::IvHistoryViewBlock( return _media->resizeGetHeight(_requestedWidth); } - void IvHistoryViewBlock::setGeometry(QRect geometry) { if (!_media) { _geometry = geometry; @@ -319,17 +308,14 @@ void IvHistoryViewBlock::setGeometry(QRect geometry) { _geometry = QRect(geometry.topLeft(), _media->currentSize()); } - -[[nodiscard]] QRect IvHistoryViewBlock::geometry() const { +QRect IvHistoryViewBlock::geometry() const { return _geometry; } - -[[nodiscard]] int IvHistoryViewBlock::firstLineBaseline() const { +int IvHistoryViewBlock::firstLineBaseline() const { return _geometry.y(); } - void IvHistoryViewBlock::paint( Painter &p, QRect clip, @@ -357,24 +343,21 @@ void IvHistoryViewBlock::paint( p.restore(); } - -[[nodiscard]] ClickHandlerPtr IvHistoryViewBlock::linkAt(QPoint point) const { +ClickHandlerPtr IvHistoryViewBlock::linkAt(QPoint point) const { return resolveHit(point).link; } - -[[nodiscard]] MediaActivation IvHistoryViewBlock::activationAt(QPoint point) const { +MediaActivation IvHistoryViewBlock::activationAt(QPoint point) const { return resolveHit(point).activation; } - -[[nodiscard]] MediaBlockSelectionData IvHistoryViewBlock::selectionData() const { +MediaBlockSelectionData IvHistoryViewBlock::selectionData() const { return { .copyText = _copyText, }; } -[[nodiscard]] IvHistoryViewHit IvHistoryViewBlock::resolveHit(QPoint point) const { +IvHistoryViewHit IvHistoryViewBlock::resolveHit(QPoint point) const { auto result = IvHistoryViewHit(); if (!_supported || !_media || !_geometry.contains(point)) { return result; @@ -382,8 +365,7 @@ void IvHistoryViewBlock::paint( return resolveLocalHit(point - _geometry.topLeft()); } - -[[nodiscard]] IvHistoryViewHit IvHistoryViewBlock::resolveLocalHit(QPoint point) const { +IvHistoryViewHit IvHistoryViewBlock::resolveLocalHit(QPoint point) const { auto result = IvHistoryViewHit(); if (!_media) { return result; @@ -397,14 +379,12 @@ void IvHistoryViewBlock::paint( return classifyState(state); } - -[[nodiscard]] IvHistoryViewHit IvHistoryViewBlock::classifyState( +IvHistoryViewHit IvHistoryViewBlock::classifyState( const HistoryView::TextState &state) const { return classifyHandler(state.link); } - -[[nodiscard]] IvHistoryViewHit IvHistoryViewBlock::classifyHandler( +IvHistoryViewHit IvHistoryViewBlock::classifyHandler( const ClickHandlerPtr &handler) const { auto result = IvHistoryViewHit(); if (!handler) { @@ -459,8 +439,7 @@ void IvHistoryViewBlock::paint( return result; } - -[[nodiscard]] bool IvHistoryViewBlock::probeSupport() { +bool IvHistoryViewBlock::probeSupport() { if (!_media) { return false; } @@ -476,8 +455,7 @@ void IvHistoryViewBlock::paint( return false; } - -[[nodiscard]] bool IvHistoryViewBlock::supportsHitClassification() { +bool IvHistoryViewBlock::supportsHitClassification() { const auto width = std::max(_layoutHint.width(), 1); _media->resizeGetHeight(width); const auto size = _media->currentSize(); @@ -501,18 +479,15 @@ void IvHistoryViewBlock::paint( return true; } - void IvHistoryViewBlock::handleViewRepaint(QRect rect) { Q_UNUSED(rect); requestRepaint(QRect()); } - void IvHistoryViewBlock::handleItemRepaint() { requestRepaint(QRect()); } - void IvHistoryViewBlock::handleViewResize() { if (!_media) { return; diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_media_block.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_media_block.cpp index 47e91bd484..63498b72a6 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_media_block.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_media_block.cpp @@ -474,7 +474,6 @@ ImageBackedMediaBlock::ImageBackedMediaBlock( } } - ImageBackedMediaBlock::ImageBackedMediaBlock( const PreparedVideoBlockData &prepared, std::shared_ptr mediaRuntime) @@ -487,7 +486,6 @@ ImageBackedMediaBlock::ImageBackedMediaBlock( , _documentId(prepared.media.id) { } - ImageBackedMediaBlock::ImageBackedMediaBlock( const PreparedMapBlockData &prepared, std::shared_ptr mediaRuntime) @@ -510,11 +508,11 @@ ImageBackedMediaBlock::ImageBackedMediaBlock( } } -[[nodiscard]] uint64 ImageBackedMediaBlock::stableId() const { +uint64 ImageBackedMediaBlock::stableId() const { return _stableId; } -[[nodiscard]] int ImageBackedMediaBlock::resizeGetHeight(int width) { +int ImageBackedMediaBlock::resizeGetHeight(int width) { return MediaHeightForWidth(width, _aspectWidth, _aspectHeight); } @@ -523,11 +521,11 @@ void ImageBackedMediaBlock::setGeometry(QRect geometry) { ensureResolved(geometry.size()); } -[[nodiscard]] QRect ImageBackedMediaBlock::geometry() const { +QRect ImageBackedMediaBlock::geometry() const { return _geometry; } -[[nodiscard]] int ImageBackedMediaBlock::firstLineBaseline() const { +int ImageBackedMediaBlock::firstLineBaseline() const { return _geometry.y(); } @@ -567,17 +565,17 @@ void ImageBackedMediaBlock::paint( p.restore(); } -[[nodiscard]] ClickHandlerPtr ImageBackedMediaBlock::linkAt(QPoint point) const { +ClickHandlerPtr ImageBackedMediaBlock::linkAt(QPoint point) const { Q_UNUSED(point); return nullptr; } -[[nodiscard]] MediaActivation ImageBackedMediaBlock::activationAt( +MediaActivation ImageBackedMediaBlock::activationAt( QPoint point) const { return _geometry.contains(point) ? _activation : MediaActivation(); } -[[nodiscard]] MediaBlockSelectionData ImageBackedMediaBlock::selectionData() const { +MediaBlockSelectionData ImageBackedMediaBlock::selectionData() const { return { .copyText = _copyText, }; @@ -677,7 +675,7 @@ void ImageBackedMediaBlock::subscribeImage(const std::shared_ptrloading(); } else if (_documentRuntime) { @@ -688,7 +686,7 @@ void ImageBackedMediaBlock::subscribeImage(const std::shared_ptrprogress(); } else if (_documentRuntime) { @@ -766,18 +764,15 @@ AudioMediaBlock::AudioMediaBlock( } } - -[[nodiscard]] uint64 AudioMediaBlock::stableId() const { +uint64 AudioMediaBlock::stableId() const { return _stableId; } - -[[nodiscard]] int AudioMediaBlock::resizeGetHeight(int width) { +int AudioMediaBlock::resizeGetHeight(int width) { rebuildLayout(width); return _height; } - void AudioMediaBlock::setGeometry(QRect geometry) { if (_layoutWidth != std::max(geometry.width(), 1)) { rebuildLayout(geometry.width()); @@ -788,17 +783,14 @@ void AudioMediaBlock::setGeometry(QRect geometry) { applyGeometry(); } - -[[nodiscard]] QRect AudioMediaBlock::geometry() const { +QRect AudioMediaBlock::geometry() const { return _geometry; } - -[[nodiscard]] int AudioMediaBlock::firstLineBaseline() const { +int AudioMediaBlock::firstLineBaseline() const { return _firstLineBaseline; } - void AudioMediaBlock::paint( Painter &p, QRect clip, @@ -838,19 +830,16 @@ void AudioMediaBlock::paint( p.restore(); } - -[[nodiscard]] ClickHandlerPtr AudioMediaBlock::linkAt(QPoint point) const { +ClickHandlerPtr AudioMediaBlock::linkAt(QPoint point) const { Q_UNUSED(point); return nullptr; } - -[[nodiscard]] MediaActivation AudioMediaBlock::activationAt(QPoint point) const { +MediaActivation AudioMediaBlock::activationAt(QPoint point) const { return _geometry.contains(point) ? _activation : MediaActivation(); } - -[[nodiscard]] MediaBlockSelectionData AudioMediaBlock::selectionData() const { +MediaBlockSelectionData AudioMediaBlock::selectionData() const { return { .copyText = _copyText, }; @@ -901,7 +890,6 @@ void AudioMediaBlock::rebuildLayout(int width) { + padding.bottom(); } - void AudioMediaBlock::applyGeometry() { const auto &card = st::defaultMarkdown.audio; const auto &padding = card.padding; @@ -1020,18 +1008,15 @@ ChannelMediaBlock::ChannelMediaBlock( } } - -[[nodiscard]] uint64 ChannelMediaBlock::stableId() const { +uint64 ChannelMediaBlock::stableId() const { return _stableId; } - -[[nodiscard]] int ChannelMediaBlock::resizeGetHeight(int width) { +int ChannelMediaBlock::resizeGetHeight(int width) { rebuildLayout(width); return _height; } - void ChannelMediaBlock::setGeometry(QRect geometry) { if (_layoutWidth != std::max(geometry.width(), 1)) { rebuildLayout(geometry.width()); @@ -1042,17 +1027,14 @@ void ChannelMediaBlock::setGeometry(QRect geometry) { applyGeometry(); } - -[[nodiscard]] QRect ChannelMediaBlock::geometry() const { +QRect ChannelMediaBlock::geometry() const { return _geometry; } - -[[nodiscard]] int ChannelMediaBlock::firstLineBaseline() const { +int ChannelMediaBlock::firstLineBaseline() const { return _firstLineBaseline; } - void ChannelMediaBlock::paint( Painter &p, QRect clip, @@ -1099,8 +1081,7 @@ void ChannelMediaBlock::paint( p.restore(); } - -[[nodiscard]] ClickHandlerPtr ChannelMediaBlock::linkAt(QPoint point) const { +ClickHandlerPtr ChannelMediaBlock::linkAt(QPoint point) const { if (_joinVisible && _joinLink && !_actionRect.isEmpty() @@ -1110,8 +1091,7 @@ void ChannelMediaBlock::paint( return nullptr; } - -[[nodiscard]] MediaActivation ChannelMediaBlock::activationAt(QPoint point) const { +MediaActivation ChannelMediaBlock::activationAt(QPoint point) const { if (!_geometry.contains(point)) { return {}; } @@ -1121,8 +1101,7 @@ void ChannelMediaBlock::paint( return _openActivation; } - -[[nodiscard]] MediaBlockSelectionData ChannelMediaBlock::selectionData() const { +MediaBlockSelectionData ChannelMediaBlock::selectionData() const { return { .copyText = _copyText, }; @@ -1145,7 +1124,6 @@ void ChannelMediaBlock::resolveChannel() { } } - void ChannelMediaBlock::rebuildLayout(int width) { resolveChannel(); const auto &card = st::defaultMarkdown.channel; @@ -1212,7 +1190,6 @@ void ChannelMediaBlock::rebuildLayout(int width) { _height = padding.top() + _cardContentHeight + padding.bottom(); } - void ChannelMediaBlock::applyGeometry() { const auto &card = st::defaultMarkdown.channel; const auto &padding = card.padding; @@ -1244,7 +1221,6 @@ void ChannelMediaBlock::applyGeometry() { } } - void ChannelMediaBlock::handleJoinedChange() { if (_geometry.width() <= 0 && _layoutWidth <= 0) { _channelResolved = false; @@ -1405,18 +1381,15 @@ GroupedMediaBlock::GroupedMediaBlock( } } - -[[nodiscard]] uint64 GroupedMediaBlock::stableId() const { +uint64 GroupedMediaBlock::stableId() const { return _stableId; } - -[[nodiscard]] int GroupedMediaBlock::resizeGetHeight(int width) { +int GroupedMediaBlock::resizeGetHeight(int width) { rebuildLayout(width); return _height; } - void GroupedMediaBlock::setGeometry(QRect geometry) { rebuildLayout(geometry.width()); const auto contentWidth = std::max(_contentWidth, 1); @@ -1430,17 +1403,14 @@ void GroupedMediaBlock::setGeometry(QRect geometry) { applyGeometry(); } - -[[nodiscard]] QRect GroupedMediaBlock::geometry() const { +QRect GroupedMediaBlock::geometry() const { return _geometry; } - -[[nodiscard]] int GroupedMediaBlock::firstLineBaseline() const { +int GroupedMediaBlock::firstLineBaseline() const { return _geometry.y(); } - void GroupedMediaBlock::paint( Painter &p, QRect clip, @@ -1473,8 +1443,7 @@ void GroupedMediaBlock::paint( p.restore(); } - -[[nodiscard]] ClickHandlerPtr GroupedMediaBlock::linkAt(QPoint point) const { +ClickHandlerPtr GroupedMediaBlock::linkAt(QPoint point) const { if (_intent == PreparedGroupedMediaIntent::Slideshow) { if (_previousRect.contains(point)) { return _previousLink; @@ -1485,8 +1454,7 @@ void GroupedMediaBlock::paint( return nullptr; } - -[[nodiscard]] MediaActivation GroupedMediaBlock::activationAt(QPoint point) const { +MediaActivation GroupedMediaBlock::activationAt(QPoint point) const { if (!_geometry.contains(point)) { return {}; } else if (_intent == PreparedGroupedMediaIntent::Slideshow) { @@ -1508,14 +1476,12 @@ void GroupedMediaBlock::paint( return {}; } - -[[nodiscard]] MediaBlockSelectionData GroupedMediaBlock::selectionData() const { +MediaBlockSelectionData GroupedMediaBlock::selectionData() const { return { .copyText = _copyText, }; } - void GroupedMediaBlock::rebuildLayout(int width) { _layoutWidth = std::max(width, 1); _contentWidth = _layoutWidth; @@ -1572,7 +1538,6 @@ void GroupedMediaBlock::rebuildLayout(int width) { _useCollageLayout = true; } - void GroupedMediaBlock::clearCollageLayout() { _contentWidth = _layoutWidth; _height = fallbackHeight(_layoutWidth); @@ -1583,8 +1548,7 @@ void GroupedMediaBlock::clearCollageLayout() { } } - -[[nodiscard]] int GroupedMediaBlock::fallbackHeight(int width) const { +int GroupedMediaBlock::fallbackHeight(int width) const { if (_fallbackSize.isEmpty()) { return std::max(st::defaultMarkdown.placeholder.minHeight, 1); } @@ -1594,7 +1558,6 @@ void GroupedMediaBlock::clearCollageLayout() { _fallbackSize.height()); } - void GroupedMediaBlock::applyGeometry() { _previousRect = QRect(); _nextRect = QRect(); @@ -1624,7 +1587,6 @@ void GroupedMediaBlock::applyGeometry() { } } - void GroupedMediaBlock::resolveRuntime(ItemState &item) { if (item.runtimeResolved) { return; @@ -1649,7 +1611,6 @@ void GroupedMediaBlock::resolveRuntime(ItemState &item) { } } - void GroupedMediaBlock::resolveImages(ItemState &item) { if (item.rect.isEmpty()) { return; @@ -1681,7 +1642,6 @@ void GroupedMediaBlock::resolveImages(ItemState &item) { } } - void GroupedMediaBlock::subscribeImage( const std::shared_ptr &image, const ItemState *item) { @@ -1698,7 +1658,6 @@ void GroupedMediaBlock::subscribeImage( }); } - void GroupedMediaBlock::handleImageUpdate(int index) { if (index < 0 || index >= int(_items.size())) { return; @@ -1711,7 +1670,6 @@ void GroupedMediaBlock::handleImageUpdate(int index) { requestRepaint(_items[index].rect); } - void GroupedMediaBlock::paintItem(Painter &p, const ItemState &item) const { if (item.rect.isEmpty()) { return; @@ -1739,8 +1697,7 @@ void GroupedMediaBlock::paintItem(Painter &p, const ItemState &item) const { } } - -[[nodiscard]] bool GroupedMediaBlock::itemLoading(const ItemState &item) const { +bool GroupedMediaBlock::itemLoading(const ItemState &item) const { if (item.photoRuntime) { return item.photoRuntime->loading(); } else if (item.documentRuntime) { @@ -1749,8 +1706,7 @@ void GroupedMediaBlock::paintItem(Painter &p, const ItemState &item) const { return false; } - -[[nodiscard]] double GroupedMediaBlock::itemProgress(const ItemState &item) const { +double GroupedMediaBlock::itemProgress(const ItemState &item) const { if (item.photoRuntime) { return item.photoRuntime->progress(); } else if (item.documentRuntime) { @@ -1759,7 +1715,6 @@ void GroupedMediaBlock::paintItem(Painter &p, const ItemState &item) const { return 0.; } - void GroupedMediaBlock::paintActiveItem(Painter &p) const { const auto item = activeItem(); if (!item) { @@ -1794,7 +1749,6 @@ void GroupedMediaBlock::paintActiveItem(Painter &p) const { } } - void GroupedMediaBlock::paintNavigation(Painter &p) const { if ((_intent != PreparedGroupedMediaIntent::Slideshow) || (_items.size() < 2)) { @@ -1821,7 +1775,6 @@ void GroupedMediaBlock::paintNavigation(Painter &p) const { } } - void GroupedMediaBlock::ensureNavigationLinks() { if ((_intent != PreparedGroupedMediaIntent::Slideshow) || (_items.size() < 2) @@ -1842,7 +1795,6 @@ void GroupedMediaBlock::ensureNavigationLinks() { }); } - void GroupedMediaBlock::updateNavigationRects() { if ((_intent != PreparedGroupedMediaIntent::Slideshow) || (_items.size() < 2) @@ -1874,7 +1826,6 @@ void GroupedMediaBlock::updateNavigationRects() { size); } - void GroupedMediaBlock::stepActiveIndex(int delta) { if ((_intent != PreparedGroupedMediaIntent::Slideshow) || (_items.size() < 2)) { @@ -1906,8 +1857,7 @@ void GroupedMediaBlock::stepActiveIndex(int delta) { requestRepaint(previousGeometry.united(_geometry)); } - -[[nodiscard]] int GroupedMediaBlock::activeItemHeight(int width) const { +int GroupedMediaBlock::activeItemHeight(int width) const { if (const auto item = activeItem()) { return MediaHeightForWidth( width, @@ -1917,15 +1867,13 @@ void GroupedMediaBlock::stepActiveIndex(int delta) { return fallbackHeight(width); } - -[[nodiscard]] GroupedMediaBlock::ItemState *GroupedMediaBlock::activeItem() { +GroupedMediaBlock::ItemState *GroupedMediaBlock::activeItem() { return (_activeIndex >= 0 && _activeIndex < int(_items.size())) ? &_items[_activeIndex] : nullptr; } - -[[nodiscard]] const GroupedMediaBlock::ItemState *GroupedMediaBlock::activeItem() const { +const GroupedMediaBlock::ItemState *GroupedMediaBlock::activeItem() const { return (_activeIndex >= 0 && _activeIndex < int(_items.size())) ? &_items[_activeIndex] : nullptr; diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare.h index f8b6191aa7..e9a60f144b 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare.h @@ -250,6 +250,7 @@ struct PreparedBlock { bool collapsed = false; bool depthClamped = false; bool tight = false; + bool supplementary = false; }; struct PreparedRenderDocument { diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_blocks.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_blocks.cpp index 9b6169b778..883338883b 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_blocks.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_blocks.cpp @@ -564,7 +564,6 @@ void PrepareFootnotes(PrepareState *state) { return PrepareChildren(node, {}, state); } - [[nodiscard]] std::vector PrepareFlowBlock( const MarkdownNode &node, PreparedBlockKind kind, diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_blocks.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_blocks.cpp index eff09dbca7..25592fcb26 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_blocks.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_blocks.cpp @@ -736,6 +736,7 @@ using NativeIvHtmlAttributes = std::vector; SortPreparedIvRichText(&caption); block.text = std::move(caption.text); block.links = std::move(caption.links); + block.supplementary = true; if (data.vblocks().v.isEmpty()) { block.children.push_back( PrepareNativeIvEmbedPostFallbackParagraph(block.embedPost.url)); @@ -841,7 +842,10 @@ using NativeIvHtmlAttributes = std::vector; &block.children, PreparedBlockKind::Paragraph, 0, - std::move(cite))) { + std::move(cite), + QString(), + false, + true)) { return false; } if (block.children.empty()) { @@ -1441,7 +1445,9 @@ void MarkNativeIvTableSlots( PreparedBlockKind::Paragraph, 0, std::move(prepared), - std::move(anchorId)); + std::move(anchorId), + false, + true); }, [&](const MTPDpageBlockHeader &data) { return AppendNativeIvFlowBlock( result, diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.cpp index ffdc4229c6..566baaef8c 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.cpp @@ -581,7 +581,8 @@ bool AppendPreparedIvRichBlock( int headingLevel, PreparedIvRichText prepared, QString anchorId, - bool allowEmpty) { + bool allowEmpty, + bool supplementary) { SortPreparedIvRichText(&prepared); if (prepared.text.text.isEmpty() && !allowEmpty) { return true; @@ -592,6 +593,7 @@ bool AppendPreparedIvRichBlock( block.text = std::move(prepared.text); block.links = std::move(prepared.links); block.anchorId = std::move(anchorId); + block.supplementary = supplementary; result->push_back(std::move(block)); return true; } @@ -626,6 +628,7 @@ bool PrepareNativeIvPhotoBlock( block.text = std::move(caption.text); block.links = std::move(caption.links); block.anchorId = std::move(anchorId); + block.supplementary = true; block.photo.id = GeneratePreparedMediaBlockId(state); block.photo.photoId = info->id; block.photo.width = info->width; @@ -658,6 +661,7 @@ bool PrepareNativeIvVideoBlock( block.text = std::move(caption.text); block.links = std::move(caption.links); block.anchorId = std::move(anchorId); + block.supplementary = true; block.video.id = GeneratePreparedMediaBlockId(state); block.video.media.kind = PreparedMediaItemKind::Document; block.video.media.id = info->id; @@ -686,6 +690,7 @@ bool PrepareNativeIvAudioBlock( block.text = std::move(caption.text); block.links = std::move(caption.links); block.anchorId = std::move(anchorId); + block.supplementary = true; block.audio.id = GeneratePreparedMediaBlockId(state); block.audio.documentId = info->id; block.audio.title = info->title; @@ -730,6 +735,7 @@ bool PrepareNativeIvMapBlock( block.text = std::move(caption.text); block.links = std::move(caption.links); block.anchorId = std::move(anchorId); + block.supplementary = true; prepared.id = GeneratePreparedMediaBlockId(state); block.map = std::move(prepared); result->push_back(std::move(block)); @@ -807,6 +813,7 @@ bool PrepareNativeIvGroupedMediaBlock( SortPreparedIvRichText(&preparedCaption); block.text = std::move(preparedCaption.text); block.links = std::move(preparedCaption.links); + block.supplementary = true; result->push_back(std::move(block)); return true; } @@ -842,6 +849,7 @@ bool PrepareNativeIvPlaceholderBlock( block.text = std::move(prepared.text); block.links = std::move(prepared.links); block.anchorId = std::move(anchorId); + block.supplementary = true; block.placeholder.label = label; block.placeholder.embed = std::move(embed); block.placeholder.copyText = NativeIvPlaceholderCopyText( diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.h index 9f87ddf04c..27a6734271 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.h @@ -71,6 +71,7 @@ void RememberNativeIvDocument( int headingLevel, PreparedIvRichText prepared, QString anchorId = QString(), - bool allowEmpty = false); + bool allowEmpty = false, + bool supplementary = false); } // namespace Iv::Markdown diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_view.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_view.cpp index 844baf9b47..f71979d78d 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_view.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_view.cpp @@ -336,6 +336,12 @@ void MarkdownPreviewRoot::setup() { }, lifetime()); } + rpl::duplicate( + _options.downloadTaskFinished + ) | rpl::on_next([=] { + update(); + }, lifetime()); + _devicePixelRatio = style::DevicePixelRatio(); prepareArticle(); } diff --git a/Telegram/cmake/td_iv.cmake b/Telegram/cmake/td_iv.cmake index 5c6d6bd150..b6514ff213 100644 --- a/Telegram/cmake/td_iv.cmake +++ b/Telegram/cmake/td_iv.cmake @@ -19,6 +19,8 @@ PRIVATE iv/iv_pch.h iv/iv_prepare.cpp iv/iv_prepare.h + iv/iv_zoom_controls.cpp + iv/iv_zoom_controls.h ) nice_target_sources(td_iv ${src_loc}