First attempt to add other media types to IV.

This commit is contained in:
John Preston
2026-05-08 10:37:13 +04:00
parent bd5080d75d
commit 4bc2ffcf4f
22 changed files with 4440 additions and 331 deletions
+113
View File
@@ -81,6 +81,11 @@ MarkdownBlockSkips {
displayMath: pixels;
table: pixels;
photo: pixels;
video: pixels;
audio: pixels;
map: pixels;
channel: pixels;
groupedMedia: pixels;
placeholder: pixels;
}
MarkdownList {
@@ -151,6 +156,49 @@ MarkdownPhoto {
progressSize: pixels;
progressWidth: pixels;
}
MarkdownAudio {
padding: margins;
captionSkip: pixels;
textSkip: pixels;
border: pixels;
borderFg: color;
bg: color;
radius: pixels;
titleStyle: TextStyle;
titleFg: color;
subtitleStyle: TextStyle;
subtitleFg: color;
}
MarkdownChannelButton {
padding: margins;
border: pixels;
borderFg: color;
bg: color;
radius: pixels;
textStyle: TextStyle;
textFg: color;
}
MarkdownChannel {
padding: margins;
border: pixels;
borderFg: color;
bg: color;
radius: pixels;
textSkip: pixels;
buttonSkip: pixels;
titleStyle: TextStyle;
titleFg: color;
subtitleStyle: TextStyle;
subtitleFg: color;
button: MarkdownChannelButton;
}
MarkdownGroupedMedia {
padding: margins;
captionSkip: pixels;
itemSkip: pixels;
radius: pixels;
overlayOpacity: double;
}
MarkdownFailure {
label: FlatLabel;
width: pixels;
@@ -178,6 +226,9 @@ Markdown {
details: MarkdownDetails;
placeholder: MarkdownPlaceholder;
photo: MarkdownPhoto;
audio: MarkdownAudio;
channel: MarkdownChannel;
groupedMedia: MarkdownGroupedMedia;
failure: MarkdownFailure;
}
@@ -206,6 +257,11 @@ defaultMarkdownBlockSkips: MarkdownBlockSkips {
displayMath: 20px;
table: 20px;
photo: 20px;
video: 20px;
audio: 20px;
map: 20px;
channel: 20px;
groupedMedia: 20px;
placeholder: 20px;
}
defaultMarkdownList: MarkdownList {
@@ -283,6 +339,60 @@ defaultMarkdownPhoto: MarkdownPhoto {
progressSize: 36px;
progressWidth: 3px;
}
defaultMarkdownAudioTitleStyle: TextStyle(defaultMarkdownBodyStyle) {
font: font(16px semibold);
}
defaultMarkdownAudioSubtitleStyle: TextStyle(defaultMarkdownDetailsSummaryStyle) {
}
defaultMarkdownAudio: MarkdownAudio {
padding: margins(8px, 12px, 8px, 12px);
captionSkip: 14px;
textSkip: 8px;
border: 1px;
borderFg: inputBorderFg;
bg: windowBgOver;
radius: 6px;
titleStyle: defaultMarkdownAudioTitleStyle;
titleFg: windowFg;
subtitleStyle: defaultMarkdownAudioSubtitleStyle;
subtitleFg: windowSubTextFg;
}
defaultMarkdownChannelTitleStyle: TextStyle(defaultMarkdownAudioTitleStyle) {
}
defaultMarkdownChannelSubtitleStyle: TextStyle(defaultMarkdownDetailsSummaryStyle) {
}
defaultMarkdownChannelButtonStyle: TextStyle(defaultMarkdownDetailsSummaryStyle) {
}
defaultMarkdownChannelButton: MarkdownChannelButton {
padding: margins(10px, 12px, 10px, 12px);
border: 1px;
borderFg: windowActiveTextFg;
bg: windowBg;
radius: 6px;
textStyle: defaultMarkdownChannelButtonStyle;
textFg: windowActiveTextFg;
}
defaultMarkdownChannel: MarkdownChannel {
padding: margins(8px, 12px, 8px, 12px);
border: 1px;
borderFg: inputBorderFg;
bg: windowBgOver;
radius: 6px;
textSkip: 8px;
buttonSkip: 8px;
titleStyle: defaultMarkdownChannelTitleStyle;
titleFg: windowFg;
subtitleStyle: defaultMarkdownChannelSubtitleStyle;
subtitleFg: windowSubTextFg;
button: defaultMarkdownChannelButton;
}
defaultMarkdownGroupedMedia: MarkdownGroupedMedia {
padding: margins(4px, 0px, 0px, 0px);
captionSkip: 12px;
itemSkip: 4px;
radius: 6px;
overlayOpacity: 0.2;
}
defaultMarkdownFailureLabel: FlatLabel(defaultFlatLabel) {
minWidth: 280px;
textFg: windowSubTextFg;
@@ -346,5 +456,8 @@ defaultMarkdown: Markdown {
details: defaultMarkdownDetails;
placeholder: defaultMarkdownPlaceholder;
photo: defaultMarkdownPhoto;
audio: defaultMarkdownAudio;
channel: defaultMarkdownChannel;
groupedMedia: defaultMarkdownGroupedMedia;
failure: defaultMarkdownFailure;
}
+444 -13
View File
@@ -50,12 +50,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/basic_click_handlers.h"
#include "ui/dynamic_image.h"
#include "ui/dynamic_thumbnails.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 <QtCore/QByteArray>
#include <QtCore/QFileInfo>
#include <QtGui/QGuiApplication>
@@ -64,6 +68,39 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <optional>
namespace Iv {
struct NativeIvChannelContext {
uint64 channelId = 0;
QString username;
};
[[nodiscard]] NativeIvChannelContext ParseNativeIvChannelContext(
const QString &context) {
const auto separator = context.indexOf(u'\n');
return {
.channelId = (separator >= 0)
? context.mid(0, separator).toULongLong()
: context.toULongLong(),
.username = (separator >= 0) ? context.mid(separator + 1) : QString(),
};
}
[[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;
}
namespace {
constexpr auto kGeoPointScale = 1;
@@ -242,13 +279,297 @@ private:
};
[[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),
};
}
class CachedPageDocumentRuntime final : public Markdown::DocumentRuntime {
public:
CachedPageDocumentRuntime(
not_null<Main::Session*> session,
not_null<DocumentData*> document,
::Data::FileOrigin origin)
: _session(session)
, _document(document)
, _origin(std::move(origin))
, _media(document->createMediaView()) {
}
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> thumbnail(
QSize size) const override {
Q_UNUSED(size);
return Ui::MakeDocumentThumbnailFit(_document, _origin);
}
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> full(
QSize size) const override {
Q_UNUSED(size);
return Ui::MakeDocumentThumbnail(_document, _origin);
}
[[nodiscard]] bool loaded() const override {
return _media->loaded();
}
[[nodiscard]] bool loading() const override {
return _document->displayLoading();
}
[[nodiscard]] double progress() const override {
return _document->progress();
}
void open(Qt::MouseButton button) const override {
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),
});
}
}
private:
const not_null<Main::Session*> _session;
const not_null<DocumentData*> _document;
const ::Data::FileOrigin _origin;
const std::shared_ptr<::Data::DocumentMedia> _media;
};
class CachedPageMapDynamicImage final : public Ui::DynamicImage {
public:
CachedPageMapDynamicImage(
not_null<::Data::CloudImage*> data,
not_null<Main::Session*> session,
::Data::FileOrigin origin)
: _data(data)
, _session(session)
, _origin(std::move(origin)) {
}
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> clone() override {
return std::make_shared<CachedPageMapDynamicImage>(
_data,
_session,
_origin);
}
[[nodiscard]] QImage image(int size) override {
Q_UNUSED(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 subscribeToUpdates(Fn<void()> callback) override {
_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();
});
}
private:
const not_null<::Data::CloudImage*> _data;
const not_null<Main::Session*> _session;
const ::Data::FileOrigin _origin;
std::shared_ptr<QImage> _view;
QImage _prepared;
int _paletteVersion = 0;
rpl::lifetime _subscription;
};
class CachedPageMapRuntime final : public Markdown::MapRuntime {
public:
CachedPageMapRuntime(
not_null<Main::Session*> 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)) {
}
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> thumbnail(
QSize size) const override {
Q_UNUSED(size);
ensureLoaded();
return std::make_shared<CachedPageMapDynamicImage>(
&_image,
_session,
_origin);
}
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> full(
QSize size) const override {
Q_UNUSED(size);
ensureLoaded();
return std::make_shared<CachedPageMapDynamicImage>(
&_image,
_session,
_origin);
}
[[nodiscard]] bool loaded() const override {
ensureLoaded();
return _image.loadedOnce();
}
[[nodiscard]] bool loading() const override {
ensureLoaded();
return _image.loading();
}
[[nodiscard]] double progress() const override {
ensureLoaded();
return _image.loadedOnce() ? 1. : 0.;
}
private:
void ensureLoaded() const {
_image.load(_session, _origin);
}
const not_null<Main::Session*> _session;
const ::Data::FileOrigin _origin;
mutable ::Data::CloudImage _image;
};
class CachedPageChannelRuntime final : public Markdown::ChannelRuntime {
public:
CachedPageChannelRuntime(
not_null<ChannelData*> channel,
QString context,
Fn<void(QString)> openChannel,
Fn<void(QString)> joinChannel)
: _channel(channel)
, _context(std::move(context))
, _openChannel(std::move(openChannel))
, _joinChannel(std::move(joinChannel)) {
}
[[nodiscard]] bool joinVisible() const override {
return !_channel->amIn();
}
void open(Qt::MouseButton button) const override {
if ((button == Qt::LeftButton || button == Qt::MiddleButton)
&& _openChannel) {
_openChannel(_context);
}
}
void join(Qt::MouseButton button) const override {
if ((button == Qt::LeftButton || button == Qt::MiddleButton)
&& _joinChannel) {
_joinChannel(_context);
}
}
private:
const not_null<ChannelData*> _channel;
const QString _context;
const Fn<void(QString)> _openChannel;
const Fn<void(QString)> _joinChannel;
};
class CachedPageMediaRuntime final : public Markdown::MediaRuntime {
public:
CachedPageMediaRuntime(
not_null<Main::Session*> session,
not_null<WebPageData*> page)
not_null<WebPageData*> page,
Fn<void(QString)> openChannel,
Fn<void(QString)> joinChannel)
: _session(session)
, _page(page) {
, _page(page)
, _openChannel(std::move(openChannel))
, _joinChannel(std::move(joinChannel)) {
}
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> resolveInlineImage(
@@ -274,13 +595,73 @@ public:
fileOrigin());
}
[[nodiscard]] std::shared_ptr<Markdown::DocumentRuntime> resolveDocument(
uint64 documentId) const override {
const auto document = _session->data().document(DocumentId(documentId));
if (document->isNull()) {
return nullptr;
}
return std::make_shared<CachedPageDocumentRuntime>(
_session,
document,
fileOrigin());
}
[[nodiscard]] std::shared_ptr<Markdown::MapRuntime> resolveMap(
double latitude,
double longitude,
uint64 accessHash,
QSize size,
int zoom) const override {
return std::make_shared<CachedPageMapRuntime>(
_session,
fileOrigin(),
latitude,
longitude,
accessHash,
size,
zoom);
}
[[nodiscard]] std::shared_ptr<Markdown::ChannelRuntime> resolveChannel(
uint64 channelId,
const QString &username) const override {
const auto channel = _session->data().channel(ChannelId(channelId));
subscribeToChannel(channelId, channel);
return std::make_shared<CachedPageChannelRuntime>(
channel,
SerializeNativeIvChannelContext(channelId, username),
_openChannel,
_joinChannel);
}
[[nodiscard]] rpl::producer<uint64> channelJoinedChanges() const override {
return _channelJoinedChanges.events();
}
private:
void subscribeToChannel(
uint64 channelId,
not_null<ChannelData*> channel) const {
if (_channelJoinedSubscriptions.find(channelId)
!= end(_channelJoinedSubscriptions)) {
return;
}
Info::Profile::AmInChannelValue(channel) | rpl::on_next([=](bool) {
_channelJoinedChanges.fire_copy(channelId);
}, _channelJoinedSubscriptions[channelId]);
}
[[nodiscard]] ::Data::FileOrigin fileOrigin() const {
return ::Data::FileOriginWebPage{ _page->url };
}
const not_null<Main::Session*> _session;
const not_null<WebPageData*> _page;
const Fn<void(QString)> _openChannel;
const Fn<void(QString)> _joinChannel;
mutable base::flat_map<uint64, rpl::lifetime> _channelJoinedSubscriptions;
mutable rpl::event_stream<uint64> _channelJoinedChanges;
};
@@ -405,7 +786,9 @@ public:
not_null<Delegate*> delegate,
not_null<Main::Session*> session,
not_null<Data*> data,
QString hash);
QString hash,
Fn<void(QString)> openChannel,
Fn<void(QString)> joinChannel);
[[nodiscard]] bool showing(
not_null<Main::Session*> session,
@@ -506,6 +889,8 @@ private:
const not_null<Delegate*> _delegate;
const not_null<Main::Session*> _session;
const Fn<void(QString)> _openChannel;
const Fn<void(QString)> _joinChannel;
std::shared_ptr<Main::SessionShow> _show;
QString _id;
std::unique_ptr<Controller> _controller;
@@ -568,9 +953,13 @@ Shown::Shown(
not_null<Delegate*> delegate,
not_null<Main::Session*> session,
not_null<Data*> data,
QString hash)
QString hash,
Fn<void(QString)> openChannel,
Fn<void(QString)> joinChannel)
: _delegate(delegate)
, _session(session) {
, _session(session)
, _openChannel(std::move(openChannel))
, _joinChannel(std::move(joinChannel)) {
prepare(data, hash);
}
@@ -877,7 +1266,11 @@ void Shown::showMarkdownWindowed(
std::shared_ptr<Markdown::MediaRuntime> Shown::createMediaRuntime(
not_null<WebPageData*> page) const {
return std::make_shared<CachedPageMediaRuntime>(_session, page);
return std::make_shared<CachedPageMediaRuntime>(
_session,
page,
_openChannel,
_joinChannel);
}
bool Shown::activateMarkdownMedia(
@@ -902,6 +1295,24 @@ bool Shown::activateMarkdownMedia(
}
activation.photo->open(button);
return true;
case Markdown::MediaActivationKind::Document:
if (!activation.document) {
return false;
}
activation.document->open(button);
return true;
case Markdown::MediaActivationKind::OpenChannel:
if (!activation.channel) {
return false;
}
activation.channel->open(button);
return true;
case Markdown::MediaActivationKind::JoinChannel:
if (!activation.channel) {
return false;
}
activation.channel->join(button);
return true;
}
return false;
}
@@ -1261,6 +1672,8 @@ void Shown::update(not_null<Data*> data) {
void Shown::showJoinedTooltip() {
if (_controller) {
_controller->showJoinedTooltip();
} else if (_markdownController) {
_markdownController->showJoinedTooltip();
}
}
@@ -1353,7 +1766,17 @@ void Instance::show(
_shown->moveTo(data, hash);
return;
}
_shown = std::make_unique<Shown>(_delegate, session, data, hash);
_shown = std::make_unique<Shown>(
_delegate,
session,
data,
hash,
[=](QString context) {
processOpenChannel(context);
},
[=](QString context) {
processJoinChannel(context);
});
_shownSession = session;
_shown->events() | rpl::on_next([=](Controller::Event event) {
using Type = Controller::Event::Type;
@@ -1730,17 +2153,21 @@ WebPageData *Instance::processReceivedPage(
void Instance::processOpenChannel(const QString &context) {
if (!_shownSession) {
return;
} else if (const auto channelId = ChannelId(context.toLongLong())) {
}
const auto parsed = ParseNativeIvChannelContext(context);
if (const auto channelId = ChannelId(parsed.channelId)) {
const auto channel = _shownSession->data().channel(channelId);
if (channel->isLoaded()) {
if (const auto controller = _shownSession->tryResolveWindow(channel)) {
controller->showPeerHistory(channel);
_shown = nullptr;
}
} else if (!channel->username().isEmpty()) {
} else if (const auto username = ResolveNativeIvChannelUsername(
channel->username(),
parsed.username); !username.isEmpty()) {
if (const auto controller = _shownSession->tryResolveWindow(channel)) {
controller->showPeerByLink({
.usernameOrId = channel->username(),
.usernameOrId = username,
});
_shown = nullptr;
}
@@ -1751,15 +2178,19 @@ void Instance::processOpenChannel(const QString &context) {
void Instance::processJoinChannel(const QString &context) {
if (!_shownSession) {
return;
} else if (const auto channelId = ChannelId(context.toLongLong())) {
}
const auto parsed = ParseNativeIvChannelContext(context);
if (const auto channelId = ChannelId(parsed.channelId)) {
const auto channel = _shownSession->data().channel(channelId);
_joining[_shownSession].emplace(channel);
if (channel->isLoaded()) {
_shownSession->api().joinChannel(channel);
} else if (!channel->username().isEmpty()) {
} else if (const auto username = ResolveNativeIvChannelUsername(
channel->username(),
parsed.username); !username.isEmpty()) {
if (const auto controller = _shownSession->tryResolveWindow(channel)) {
controller->showPeerByLink({
.usernameOrId = channel->username(),
.usernameOrId = username,
.joinChannel = true,
});
}
@@ -229,13 +229,35 @@ void RebuildVisibleSegmentLookup(
auto result = HitSegmentBoundary(
segment,
after ? SegmentLength(segment) : 0);
if (segment.block) {
result.mediaActivation = segment.block->activation;
if (const auto prepared = PreparedLinkForMediaActivation(
result.mediaActivation)) {
const auto applyActivation = [&](const MediaActivation &activation) {
result.mediaActivation = activation;
if (const auto prepared = PreparedLinkForMediaActivation(activation)) {
result.preparedLink = prepared;
result.state.link = CreatePreparedLinkHandler(*prepared);
}
};
if (segment.block) {
if (!segment.block->actionRect.isEmpty()
&& segment.block->actionRect.contains(point)
&& segment.block->channelRuntime
&& segment.block->channelRuntime->joinVisible()) {
applyActivation(segment.block->actionActivation);
} else if (segment.block->kind == PreparedBlockKind::GroupedMedia) {
auto matchedItem = false;
for (const auto &item : segment.block->groupedMediaItems) {
if (!item.rect.contains(point)) {
continue;
}
applyActivation(item.activation);
matchedItem = true;
break;
}
if (!matchedItem) {
applyActivation(segment.block->activation);
}
} else {
applyActivation(segment.block->activation);
}
}
result.direct = true;
return result;
@@ -548,6 +570,18 @@ public:
ClearColorizedFormulaImages(&_blocks);
}
void invalidateLayout() {
_width = -1;
_height = 0;
clearPendingHighlightBlockPointers();
_blocks.clear();
_anchors.clear();
_segments.clear();
_visibleSegmentSpan = {};
_segmentTops.clear();
_segmentBottoms.clear();
}
private:
[[nodiscard]] int currentDevicePixelRatio() const {
return std::max(style::DevicePixelRatio(), 1);
@@ -634,18 +668,6 @@ private:
}
}
void invalidateLayout() {
_width = -1;
_height = 0;
clearPendingHighlightBlockPointers();
_blocks.clear();
_anchors.clear();
_segments.clear();
_visibleSegmentSpan = {};
_segmentTops.clear();
_segmentBottoms.clear();
}
void resetFormulaRasterCache() {
_formulaRenders.clear();
_formulaRenders.resize(_content.formulas.size());
@@ -727,6 +749,10 @@ void MarkdownArticle::setContent(MarkdownArticleContent content) {
_impl->setContent(std::move(content));
}
void MarkdownArticle::invalidateLayout() {
_impl->invalidateLayout();
}
int MarkdownArticle::maxWidth() const {
return const_cast<Impl*>(_impl.get())->maxWidth();
}
@@ -108,6 +108,7 @@ public:
void setRenderer(std::shared_ptr<MathRenderer> renderer);
void setContent(MarkdownArticleContent content);
void invalidateLayout();
[[nodiscard]] int maxWidth() const;
[[nodiscard]] int resizeGetHeight(int width);
void setVisibleTopBottom(int visibleTop, int visibleBottom);
@@ -9,11 +9,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "iv/markdown/iv_markdown_article_text.h"
#include "lang/lang_keys.h"
#include "spellcheck/spellcheck_highlight_syntax.h"
#include "ui/grouped_layout.h"
#include "styles/style_iv.h"
#include "styles/style_widgets.h"
#include <algorithm>
#include <cmath>
#include <utility>
namespace Iv::Markdown {
@@ -29,6 +31,7 @@ constexpr auto kIvMarkedTextOptions = TextParseOptions{
constexpr auto kCodeTabColumns = 4;
constexpr auto kCodeTrailingGuard = 0x2060;
const auto kPhotoCopyLabel = u"Photo"_q;
const auto kUsernamePrefix = u"@"_q;
[[nodiscard]] style::align CellAlign(TableAlignment alignment) {
switch (alignment) {
@@ -148,6 +151,219 @@ const auto kPhotoCopyLabel = u"Photo"_q;
return result;
}
template <typename Runtime>
void ResolveRuntimeImages(
const std::shared_ptr<Runtime> &runtime,
QSize size,
std::shared_ptr<Ui::DynamicImage> *thumbnail,
std::shared_ptr<Ui::DynamicImage> *full) {
if (!runtime) {
return;
}
if (thumbnail) {
*thumbnail = runtime->thumbnail(size);
}
if (full) {
*full = runtime->full(size);
}
}
void SetPlainTextLeaf(
Ui::Text::String *leaf,
const style::TextStyle &textStyle,
const QString &text,
int width) {
*leaf = Ui::Text::String(TextMinResizeWidth(width));
leaf->setMarkedText(
textStyle,
TextWithEntities::Simple(text),
kIvMarkedTextOptions);
}
[[nodiscard]] int LeafHeight(
const Ui::Text::String &leaf,
const style::TextStyle &textStyle,
int width) {
return std::max(
leaf.countHeight(width, true),
TextLineHeight(textStyle));
}
[[nodiscard]] int MediaHeightForWidth(
int width,
int aspectWidth,
int aspectHeight) {
aspectWidth = std::max(aspectWidth, 1);
aspectHeight = std::max(aspectHeight, 1);
return std::max(
int((int64(width) * aspectHeight + aspectWidth - 1) / aspectWidth),
1);
}
[[nodiscard]] int GroupedMediaMinWidth(int width, int spacing) {
return std::max((width - 2 * spacing) / 3, 1);
}
void LayoutMediaCaption(
LaidOutBlock *block,
const PreparedBlock &prepared,
const std::vector<PreparedFormulaSlot> *formulas,
InlineFormulaObjectCache *inlineFormulaObjects,
const std::shared_ptr<MediaRuntime> &mediaRuntime,
const style::Markdown &markdown,
int left,
int top,
int width,
int skip,
int *bottom) {
if (prepared.text.text.isEmpty()) {
return;
}
block->textWidth = std::max(width, 1);
SetTextLeaf(
&block->leaf,
markdown.body,
prepared.text,
formulas,
inlineFormulaObjects,
mediaRuntime,
block->textWidth);
BindLinks(&block->leaf, prepared.links);
const auto captionTop = top + skip;
const auto captionHeight = std::max(
block->leaf.countHeight(block->textWidth, true),
TextLineHeight(markdown.body));
block->textRect = QRect(left, captionTop, block->textWidth, captionHeight);
*bottom = captionTop + captionHeight;
}
[[nodiscard]] QString AudioTitleText(const PreparedAudioBlockData &audio) {
if (!audio.title.isEmpty()) {
return audio.title;
}
if (!audio.fileName.isEmpty()) {
return audio.fileName;
}
return tr::lng_in_dlg_audio_file(tr::now);
}
[[nodiscard]] QString AudioSubtitleText(const PreparedAudioBlockData &audio) {
if (!audio.performer.isEmpty()) {
return audio.performer;
}
if (!audio.fileName.isEmpty() && audio.fileName != AudioTitleText(audio)) {
return audio.fileName;
}
return QString();
}
[[nodiscard]] QString AudioCopyText(const PreparedAudioBlockData &audio) {
const auto title = AudioTitleText(audio);
const auto subtitle = AudioSubtitleText(audio);
return subtitle.isEmpty() ? title : (title + u"\n"_q + subtitle);
}
[[nodiscard]] QString ChannelSubtitleText(
const PreparedChannelBlockData &channel) {
return channel.username.isEmpty()
? QString()
: (kUsernamePrefix + channel.username);
}
[[nodiscard]] QString ChannelCopyText(const PreparedChannelBlockData &channel) {
const auto subtitle = ChannelSubtitleText(channel);
return subtitle.isEmpty()
? channel.title
: (channel.title + u"\n"_q + subtitle);
}
[[nodiscard]] QString GroupedMediaCopyText(
const PreparedGroupedMediaBlockData &grouped) {
auto photos = 0;
auto videos = 0;
for (const auto &item : grouped.items) {
if (item.media.kind == PreparedMediaItemKind::Photo) {
++photos;
} else {
++videos;
}
}
if (photos && !videos) {
return tr::lng_media_selected_photo(tr::now, lt_count, photos);
} else if (videos && !photos) {
return tr::lng_media_selected_video(tr::now, lt_count, videos);
}
return QString();
}
[[nodiscard]] QString GroupedMediaItemCopyText(PreparedMediaItemKind kind) {
return (kind == PreparedMediaItemKind::Photo)
? kPhotoCopyLabel
: tr::lng_in_dlg_video(tr::now);
}
[[nodiscard]] int GroupedMediaLayoutWidth(
const std::vector<Ui::GroupMediaLayout> &layout) {
auto result = 0;
for (const auto &part : layout) {
result = std::max(
result,
part.geometry.x() + part.geometry.width());
}
return result;
}
[[nodiscard]] int GroupedMediaLayoutHeight(
const std::vector<Ui::GroupMediaLayout> &layout) {
auto result = 0;
for (const auto &part : layout) {
result = std::max(
result,
part.geometry.y() + part.geometry.height());
}
return result;
}
void ResolveGroupedMediaItemLayout(
LaidOutGroupedMediaItem *item,
const PreparedGroupedMediaItemData &prepared,
const std::shared_ptr<MediaRuntime> &mediaRuntime,
QRect rect) {
if (!item) {
return;
}
item->kind = prepared.media.kind;
item->copyText = GroupedMediaItemCopyText(prepared.media.kind);
item->rect = rect;
if (prepared.media.kind == PreparedMediaItemKind::Photo) {
if (mediaRuntime) {
item->photoRuntime = mediaRuntime->resolvePhoto(prepared.media.id);
}
ResolveRuntimeImages(
item->photoRuntime,
rect.size(),
&item->thumbnailImage,
&item->fullImage);
if (item->photoRuntime) {
item->activation.kind = MediaActivationKind::Photo;
item->activation.photo = item->photoRuntime;
}
return;
}
if (mediaRuntime) {
item->documentRuntime = mediaRuntime->resolveDocument(prepared.media.id);
}
ResolveRuntimeImages(
item->documentRuntime,
rect.size(),
&item->thumbnailImage,
&item->fullImage);
if (item->documentRuntime) {
item->activation.kind = MediaActivationKind::Document;
item->activation.document = item->documentRuntime;
}
}
} // namespace
[[nodiscard]] int SingleDigitOrderedMarkerWidth(
@@ -286,10 +502,20 @@ int BlockSkip(
return skips.table;
case PreparedBlockKind::Photo:
return skips.photo;
case PreparedBlockKind::Video:
return skips.video;
case PreparedBlockKind::Audio:
return skips.audio;
case PreparedBlockKind::Map:
return skips.map;
case PreparedBlockKind::Channel:
return skips.channel;
case PreparedBlockKind::Placeholder:
return skips.placeholder;
case PreparedBlockKind::Details:
return skips.paragraph;
case PreparedBlockKind::GroupedMedia:
return skips.groupedMedia;
}
return 0;
}
@@ -371,6 +597,17 @@ LaidOutBlock LayoutFlowBlock(
int left,
int top,
int width) {
if (prepared.kind == PreparedBlockKind::GroupedMedia) {
return LayoutGroupedMediaBlock(
prepared,
formulas,
inlineFormulaObjects,
mediaRuntime,
markdown,
left,
top,
width);
}
auto block = LaidOutBlock();
block.kind = prepared.kind;
block.anchorId = prepared.anchorId;
@@ -717,28 +954,18 @@ LaidOutBlock LayoutPlaceholderBlock(
style.labelStyle);
auto bottom = top + mediaHeight;
if (!prepared.text.text.isEmpty()) {
block.textWidth = contentWidth;
SetTextLeaf(
&block.leaf,
markdown.body,
prepared.text,
formulas,
inlineFormulaObjects,
mediaRuntime,
block.textWidth);
BindLinks(&block.leaf, prepared.links);
const auto captionTop = bottom + style.captionSkip;
const auto captionHeight = std::max(
block.leaf.countHeight(block.textWidth, true),
TextLineHeight(markdown.body));
block.textRect = QRect(
contentLeft,
captionTop,
block.textWidth,
captionHeight);
bottom = captionTop + captionHeight;
}
LayoutMediaCaption(
&block,
prepared,
formulas,
inlineFormulaObjects,
mediaRuntime,
markdown,
contentLeft,
bottom,
contentWidth,
style.captionSkip,
&bottom);
block.contentRect = QRect(
left,
@@ -770,22 +997,21 @@ LaidOutBlock LayoutPhotoBlock(
const auto mediaWidth = std::max(
blockWidth - style.padding.left() - style.padding.right(),
1);
const auto aspectWidth = std::max(prepared.photo.width, 1);
const auto aspectHeight = std::max(prepared.photo.height, 1);
const auto mediaHeight = std::max(
int((int64(mediaWidth) * aspectHeight + aspectWidth - 1) / aspectWidth),
1);
const auto mediaHeight = MediaHeightForWidth(
mediaWidth,
prepared.photo.width,
prepared.photo.height);
block.mediaRect = QRect(mediaLeft, mediaTop, mediaWidth, mediaHeight);
block.visibleMediaRect = block.mediaRect;
if (mediaRuntime) {
block.photoRuntime = mediaRuntime->resolvePhoto(prepared.photo.photoId);
}
if (block.photoRuntime) {
const auto size = QSize(mediaWidth, mediaHeight);
block.thumbnailImage = block.photoRuntime->thumbnail(size);
block.fullImage = block.photoRuntime->full(size);
}
ResolveRuntimeImages(
block.photoRuntime,
QSize(mediaWidth, mediaHeight),
&block.thumbnailImage,
&block.fullImage);
if (!prepared.photo.urlOverride.isEmpty()) {
block.activation.kind = MediaActivationKind::ExternalUrl;
block.activation.url = prepared.photo.urlOverride;
@@ -795,28 +1021,18 @@ LaidOutBlock LayoutPhotoBlock(
}
auto bottom = mediaTop + mediaHeight + style.padding.bottom();
if (!prepared.text.text.isEmpty()) {
block.textWidth = mediaWidth;
SetTextLeaf(
&block.leaf,
markdown.body,
prepared.text,
formulas,
inlineFormulaObjects,
mediaRuntime,
block.textWidth);
BindLinks(&block.leaf, prepared.links);
const auto captionTop = bottom + style.captionSkip;
const auto captionHeight = std::max(
block.leaf.countHeight(block.textWidth, true),
TextLineHeight(markdown.body));
block.textRect = QRect(
mediaLeft,
captionTop,
block.textWidth,
captionHeight);
bottom = captionTop + captionHeight;
}
LayoutMediaCaption(
&block,
prepared,
formulas,
inlineFormulaObjects,
mediaRuntime,
markdown,
mediaLeft,
bottom,
mediaWidth,
style.captionSkip,
&bottom);
block.contentRect = QRect(
mediaLeft,
@@ -827,4 +1043,454 @@ LaidOutBlock LayoutPhotoBlock(
return block;
}
LaidOutBlock LayoutVideoBlock(
const PreparedBlock &prepared,
std::vector<PreparedFormulaSlot> *formulas,
InlineFormulaObjectCache *inlineFormulaObjects,
const std::shared_ptr<MediaRuntime> &mediaRuntime,
const style::Markdown &markdown,
int left,
int top,
int width) {
auto block = LaidOutBlock();
block.kind = PreparedBlockKind::Video;
block.anchorId = prepared.anchorId;
block.copyText = tr::lng_in_dlg_video(tr::now);
const auto &style = markdown.photo;
const auto blockWidth = std::max(width, 1);
const auto mediaLeft = left + style.padding.left();
const auto mediaTop = top + style.padding.top();
const auto mediaWidth = std::max(
blockWidth - style.padding.left() - style.padding.right(),
1);
const auto mediaHeight = MediaHeightForWidth(
mediaWidth,
prepared.video.media.width,
prepared.video.media.height);
block.mediaRect = QRect(mediaLeft, mediaTop, mediaWidth, mediaHeight);
block.visibleMediaRect = block.mediaRect;
if (mediaRuntime) {
block.documentRuntime = mediaRuntime->resolveDocument(
prepared.video.media.id);
}
ResolveRuntimeImages(
block.documentRuntime,
QSize(mediaWidth, mediaHeight),
&block.thumbnailImage,
&block.fullImage);
if (block.documentRuntime) {
block.activation.kind = MediaActivationKind::Document;
block.activation.document = block.documentRuntime;
}
auto bottom = mediaTop + mediaHeight + style.padding.bottom();
LayoutMediaCaption(
&block,
prepared,
formulas,
inlineFormulaObjects,
mediaRuntime,
markdown,
mediaLeft,
bottom,
mediaWidth,
style.captionSkip,
&bottom);
block.contentRect = QRect(
mediaLeft,
mediaTop,
mediaWidth,
std::max(bottom - mediaTop, mediaHeight));
block.outer = QRect(left, top, blockWidth, std::max(bottom - top, mediaHeight));
return block;
}
LaidOutBlock LayoutAudioBlock(
const PreparedBlock &prepared,
std::vector<PreparedFormulaSlot> *formulas,
InlineFormulaObjectCache *inlineFormulaObjects,
const std::shared_ptr<MediaRuntime> &mediaRuntime,
const style::Markdown &markdown,
int left,
int top,
int width) {
auto block = LaidOutBlock();
block.kind = PreparedBlockKind::Audio;
block.anchorId = prepared.anchorId;
block.labelText = AudioTitleText(prepared.audio);
block.copyText = AudioCopyText(prepared.audio);
const auto &card = markdown.audio;
const auto &padding = card.padding;
const auto &titleStyle = card.titleStyle;
const auto &subtitleStyle = card.subtitleStyle;
const auto subtitleText = AudioSubtitleText(prepared.audio);
const auto blockWidth = std::max(width, 1);
const auto contentLeft = left + padding.left();
const auto contentWidth = std::max(
blockWidth - padding.left() - padding.right(),
1);
block.labelWidth = contentWidth;
SetPlainTextLeaf(
&block.labelLeaf,
titleStyle,
block.labelText,
block.labelWidth);
const auto titleHeight = LeafHeight(
block.labelLeaf,
titleStyle,
block.labelWidth);
auto subtitleHeight = 0;
if (!subtitleText.isEmpty()) {
block.subtitleWidth = contentWidth;
SetPlainTextLeaf(
&block.subtitleLeaf,
subtitleStyle,
subtitleText,
block.subtitleWidth);
subtitleHeight = LeafHeight(
block.subtitleLeaf,
subtitleStyle,
block.subtitleWidth);
}
const auto textSkip = subtitleHeight ? card.textSkip : 0;
const auto textHeight = titleHeight + textSkip + subtitleHeight;
const auto cardHeight = padding.top() + textHeight + padding.bottom();
block.mediaRect = QRect(left, top, blockWidth, cardHeight);
block.visibleMediaRect = block.mediaRect;
block.labelRect = QRect(
contentLeft,
top + padding.top(),
block.labelWidth,
titleHeight);
if (subtitleHeight) {
block.subtitleRect = QRect(
contentLeft,
block.labelRect.y() + block.labelRect.height() + textSkip,
block.subtitleWidth,
subtitleHeight);
}
block.firstLineBaseline = LeafFirstLineBaseline(
block.labelLeaf,
block.labelRect,
titleStyle);
if (mediaRuntime) {
block.documentRuntime = mediaRuntime->resolveDocument(
prepared.audio.documentId);
}
if (block.documentRuntime) {
block.activation.kind = MediaActivationKind::Document;
block.activation.document = block.documentRuntime;
}
auto bottom = top + cardHeight;
LayoutMediaCaption(
&block,
prepared,
formulas,
inlineFormulaObjects,
mediaRuntime,
markdown,
contentLeft,
bottom,
contentWidth,
card.captionSkip,
&bottom);
block.contentRect = QRect(left, top, blockWidth, std::max(bottom - top, cardHeight));
block.outer = block.contentRect;
return block;
}
LaidOutBlock LayoutMapBlock(
const PreparedBlock &prepared,
std::vector<PreparedFormulaSlot> *formulas,
InlineFormulaObjectCache *inlineFormulaObjects,
const std::shared_ptr<MediaRuntime> &mediaRuntime,
const style::Markdown &markdown,
int left,
int top,
int width) {
auto block = LaidOutBlock();
block.kind = PreparedBlockKind::Map;
block.anchorId = prepared.anchorId;
block.copyText = tr::lng_maps_point(tr::now);
const auto &style = markdown.photo;
const auto blockWidth = std::max(width, 1);
const auto mediaLeft = left + style.padding.left();
const auto mediaTop = top + style.padding.top();
const auto mediaWidth = std::max(
blockWidth - style.padding.left() - style.padding.right(),
1);
const auto mediaHeight = MediaHeightForWidth(
mediaWidth,
prepared.map.width,
prepared.map.height);
block.mediaRect = QRect(mediaLeft, mediaTop, mediaWidth, mediaHeight);
block.visibleMediaRect = block.mediaRect;
if (mediaRuntime) {
block.mapRuntime = mediaRuntime->resolveMap(
prepared.map.latitude,
prepared.map.longitude,
prepared.map.accessHash,
QSize(mediaWidth, mediaHeight),
prepared.map.zoom);
}
ResolveRuntimeImages(
block.mapRuntime,
QSize(mediaWidth, mediaHeight),
&block.thumbnailImage,
&block.fullImage);
if (!prepared.map.url.isEmpty()) {
block.activation.kind = MediaActivationKind::ExternalUrl;
block.activation.url = prepared.map.url;
}
auto bottom = mediaTop + mediaHeight + style.padding.bottom();
LayoutMediaCaption(
&block,
prepared,
formulas,
inlineFormulaObjects,
mediaRuntime,
markdown,
mediaLeft,
bottom,
mediaWidth,
style.captionSkip,
&bottom);
block.contentRect = QRect(
mediaLeft,
mediaTop,
mediaWidth,
std::max(bottom - mediaTop, mediaHeight));
block.outer = QRect(left, top, blockWidth, std::max(bottom - top, mediaHeight));
return block;
}
LaidOutBlock LayoutChannelBlock(
const PreparedBlock &prepared,
std::vector<PreparedFormulaSlot> *formulas,
InlineFormulaObjectCache *inlineFormulaObjects,
const std::shared_ptr<MediaRuntime> &mediaRuntime,
const style::Markdown &markdown,
int left,
int top,
int width) {
auto block = LaidOutBlock();
block.kind = PreparedBlockKind::Channel;
block.anchorId = prepared.anchorId;
block.labelText = prepared.channel.title;
block.copyText = ChannelCopyText(prepared.channel);
const auto &card = markdown.channel;
const auto &padding = card.padding;
const auto &button = card.button;
const auto &buttonPadding = button.padding;
const auto &titleStyle = card.titleStyle;
const auto &subtitleStyle = card.subtitleStyle;
const auto &actionStyle = button.textStyle;
const auto subtitleText = ChannelSubtitleText(prepared.channel);
const auto blockWidth = std::max(width, 1);
const auto contentLeft = left + padding.left();
const auto contentWidth = std::max(
blockWidth - padding.left() - padding.right(),
1);
if (mediaRuntime) {
block.channelRuntime = mediaRuntime->resolveChannel(
prepared.channel.channelId,
prepared.channel.username);
}
const auto joinVisible = block.channelRuntime
&& block.channelRuntime->joinVisible();
if (block.channelRuntime) {
block.activation.kind = MediaActivationKind::OpenChannel;
block.activation.channel = block.channelRuntime;
}
if (joinVisible) {
block.actionActivation.kind = MediaActivationKind::JoinChannel;
block.actionActivation.channel = block.channelRuntime;
}
auto actionTextHeight = 0;
auto actionOuterWidth = 0;
auto actionOuterHeight = 0;
if (joinVisible) {
SetPlainTextLeaf(
&block.actionLeaf,
actionStyle,
tr::lng_iv_join_channel(tr::now),
contentWidth);
block.actionWidth = std::max(block.actionLeaf.maxWidth(), 1);
actionTextHeight = LeafHeight(
block.actionLeaf,
actionStyle,
block.actionWidth);
actionOuterWidth = block.actionWidth
+ buttonPadding.left()
+ buttonPadding.right();
actionOuterHeight = actionTextHeight
+ buttonPadding.top()
+ buttonPadding.bottom();
}
block.labelWidth = std::max(
contentWidth
- (joinVisible ? (actionOuterWidth + card.buttonSkip) : 0),
1);
SetPlainTextLeaf(
&block.labelLeaf,
titleStyle,
block.labelText,
block.labelWidth);
const auto titleHeight = LeafHeight(
block.labelLeaf,
titleStyle,
block.labelWidth);
auto subtitleHeight = 0;
if (!subtitleText.isEmpty()) {
block.subtitleWidth = block.labelWidth;
SetPlainTextLeaf(
&block.subtitleLeaf,
subtitleStyle,
subtitleText,
block.subtitleWidth);
subtitleHeight = LeafHeight(
block.subtitleLeaf,
subtitleStyle,
block.subtitleWidth);
}
const auto textSkip = subtitleHeight ? card.textSkip : 0;
const auto textHeight = titleHeight + textSkip + subtitleHeight;
const auto cardContentHeight = std::max(textHeight, actionOuterHeight);
const auto cardHeight = padding.top() + cardContentHeight + padding.bottom();
block.mediaRect = QRect(left, top, blockWidth, cardHeight);
block.visibleMediaRect = block.mediaRect;
const auto textTop = top + padding.top()
+ std::max((cardContentHeight - textHeight) / 2, 0);
block.labelRect = QRect(
contentLeft,
textTop,
block.labelWidth,
titleHeight);
if (subtitleHeight) {
block.subtitleRect = QRect(
contentLeft,
block.labelRect.y() + block.labelRect.height() + textSkip,
block.subtitleWidth,
subtitleHeight);
}
if (joinVisible) {
block.actionRect = QRect(
left + blockWidth - padding.right() - actionOuterWidth,
top + padding.top()
+ std::max((cardContentHeight - actionOuterHeight) / 2, 0),
actionOuterWidth,
actionOuterHeight);
}
block.firstLineBaseline = LeafFirstLineBaseline(
block.labelLeaf,
block.labelRect,
titleStyle);
block.contentRect = block.mediaRect;
block.outer = block.mediaRect;
return block;
}
LaidOutBlock LayoutGroupedMediaBlock(
const PreparedBlock &prepared,
const std::vector<PreparedFormulaSlot> *formulas,
InlineFormulaObjectCache *inlineFormulaObjects,
const std::shared_ptr<MediaRuntime> &mediaRuntime,
const style::Markdown &markdown,
int left,
int top,
int width) {
auto block = LaidOutBlock();
block.kind = PreparedBlockKind::GroupedMedia;
block.anchorId = prepared.anchorId;
block.copyText = GroupedMediaCopyText(prepared.groupedMedia);
const auto &style = markdown.groupedMedia;
const auto blockWidth = std::max(width, 1);
const auto mediaLeft = left + style.padding.left();
const auto mediaTop = top + style.padding.top();
const auto mediaWidth = std::max(
blockWidth - style.padding.left() - style.padding.right(),
1);
auto sizes = std::vector<QSize>();
sizes.reserve(prepared.groupedMedia.items.size());
for (const auto &item : prepared.groupedMedia.items) {
sizes.push_back(QSize(
std::max(item.media.width, 1),
std::max(item.media.height, 1)));
}
auto layout = Ui::LayoutMediaGroup(
sizes,
mediaWidth,
GroupedMediaMinWidth(mediaWidth, style.itemSkip),
style.itemSkip);
block.groupedMediaItems.reserve(layout.size());
for (auto i = 0, count = std::min(
int(layout.size()),
int(prepared.groupedMedia.items.size())); i != count; ++i) {
auto item = LaidOutGroupedMediaItem();
ResolveGroupedMediaItemLayout(
&item,
prepared.groupedMedia.items[i],
mediaRuntime,
layout[i].geometry.translated(mediaLeft, mediaTop));
block.groupedMediaItems.push_back(std::move(item));
}
const auto mediaHeight = GroupedMediaLayoutHeight(layout);
const auto laidOutWidth = GroupedMediaLayoutWidth(layout);
block.mediaRect = QRect(
mediaLeft,
mediaTop,
std::max(laidOutWidth, 1),
std::max(mediaHeight, 1));
block.visibleMediaRect = block.mediaRect;
auto bottom = block.mediaRect.y() + block.mediaRect.height()
+ style.padding.bottom();
LayoutMediaCaption(
&block,
prepared,
formulas,
inlineFormulaObjects,
mediaRuntime,
markdown,
mediaLeft,
bottom,
block.mediaRect.width(),
style.captionSkip,
&bottom);
block.contentRect = QRect(
mediaLeft,
mediaTop,
block.mediaRect.width(),
std::max(bottom - mediaTop, block.mediaRect.height()));
block.outer = QRect(
left,
top,
blockWidth,
std::max(bottom - top, block.mediaRect.height()));
return block;
}
} // namespace Iv::Markdown
@@ -49,10 +49,25 @@ struct LaidOutTableRow {
bool header = false;
};
struct LaidOutGroupedMediaItem {
PreparedMediaItemKind kind = PreparedMediaItemKind::Photo;
QString copyText;
QRect rect;
std::shared_ptr<PhotoRuntime> photoRuntime;
std::shared_ptr<DocumentRuntime> documentRuntime;
std::shared_ptr<Ui::DynamicImage> thumbnailImage;
std::shared_ptr<Ui::DynamicImage> fullImage;
MediaActivation activation;
mutable bool thumbnailSubscribed = false;
mutable bool fullSubscribed = false;
};
struct LaidOutBlock {
PreparedBlockKind kind = PreparedBlockKind::Paragraph;
Ui::Text::String leaf;
Ui::Text::String labelLeaf;
Ui::Text::String subtitleLeaf;
Ui::Text::String actionLeaf;
Ui::Text::String marker;
Ui::Text::String fallbackLeaf;
QString copyText;
@@ -60,6 +75,7 @@ struct LaidOutBlock {
QString codeLanguage;
Spellchecker::HighlightProcessId syntaxHighlightProcessId = 0;
std::vector<LaidOutBlock> children;
std::vector<LaidOutGroupedMediaItem> groupedMediaItems;
std::vector<LaidOutTableRow> tableRows;
std::vector<int> tableColumnWidths;
QRect outer;
@@ -68,6 +84,8 @@ struct LaidOutBlock {
QRect iconRect;
QRect textRect;
QRect labelRect;
QRect subtitleRect;
QRect actionRect;
QRect markerRect;
QRect contentRect;
QRect formulaRect;
@@ -80,6 +98,8 @@ struct LaidOutBlock {
QString anchorId;
int textWidth = 0;
int labelWidth = 0;
int subtitleWidth = 0;
int actionWidth = 0;
int markerWidth = 0;
int firstLineBaseline = -1;
int headingLevel = 0;
@@ -94,9 +114,13 @@ struct LaidOutBlock {
int segmentIndex = -1;
int secondarySegmentIndex = -1;
std::shared_ptr<PhotoRuntime> photoRuntime;
std::shared_ptr<DocumentRuntime> documentRuntime;
std::shared_ptr<MapRuntime> mapRuntime;
std::shared_ptr<ChannelRuntime> channelRuntime;
std::shared_ptr<Ui::DynamicImage> thumbnailImage;
std::shared_ptr<Ui::DynamicImage> fullImage;
MediaActivation activation;
MediaActivation actionActivation;
mutable bool thumbnailSubscribed = false;
mutable bool fullSubscribed = false;
mutable QImage colorizedFormulaImage;
@@ -211,5 +235,50 @@ void RepopulateCodeBlockLeaf(
int left,
int top,
int width);
[[nodiscard]] LaidOutBlock LayoutVideoBlock(
const PreparedBlock &prepared,
std::vector<PreparedFormulaSlot> *formulas,
InlineFormulaObjectCache *inlineFormulaObjects,
const std::shared_ptr<MediaRuntime> &mediaRuntime,
const style::Markdown &markdown,
int left,
int top,
int width);
[[nodiscard]] LaidOutBlock LayoutAudioBlock(
const PreparedBlock &prepared,
std::vector<PreparedFormulaSlot> *formulas,
InlineFormulaObjectCache *inlineFormulaObjects,
const std::shared_ptr<MediaRuntime> &mediaRuntime,
const style::Markdown &markdown,
int left,
int top,
int width);
[[nodiscard]] LaidOutBlock LayoutMapBlock(
const PreparedBlock &prepared,
std::vector<PreparedFormulaSlot> *formulas,
InlineFormulaObjectCache *inlineFormulaObjects,
const std::shared_ptr<MediaRuntime> &mediaRuntime,
const style::Markdown &markdown,
int left,
int top,
int width);
[[nodiscard]] LaidOutBlock LayoutChannelBlock(
const PreparedBlock &prepared,
std::vector<PreparedFormulaSlot> *formulas,
InlineFormulaObjectCache *inlineFormulaObjects,
const std::shared_ptr<MediaRuntime> &mediaRuntime,
const style::Markdown &markdown,
int left,
int top,
int width);
[[nodiscard]] LaidOutBlock LayoutGroupedMediaBlock(
const PreparedBlock &prepared,
const std::vector<PreparedFormulaSlot> *formulas,
InlineFormulaObjectCache *inlineFormulaObjects,
const std::shared_ptr<MediaRuntime> &mediaRuntime,
const style::Markdown &markdown,
int left,
int top,
int width);
} // namespace Iv::Markdown
@@ -57,6 +57,11 @@ namespace {
case PreparedBlockKind::DisplayMath:
case PreparedBlockKind::Table:
case PreparedBlockKind::Photo:
case PreparedBlockKind::Video:
case PreparedBlockKind::Audio:
case PreparedBlockKind::Map:
case PreparedBlockKind::Channel:
case PreparedBlockKind::GroupedMedia:
case PreparedBlockKind::Placeholder:
case PreparedBlockKind::Details:
return false;
@@ -554,6 +559,46 @@ namespace {
left,
top,
width);
case PreparedBlockKind::Video:
return LayoutVideoBlock(
prepared,
formulas,
inlineFormulaObjects,
mediaRuntime,
markdown,
left,
top,
width);
case PreparedBlockKind::Audio:
return LayoutAudioBlock(
prepared,
formulas,
inlineFormulaObjects,
mediaRuntime,
markdown,
left,
top,
width);
case PreparedBlockKind::Map:
return LayoutMapBlock(
prepared,
formulas,
inlineFormulaObjects,
mediaRuntime,
markdown,
left,
top,
width);
case PreparedBlockKind::Channel:
return LayoutChannelBlock(
prepared,
formulas,
inlineFormulaObjects,
mediaRuntime,
markdown,
left,
top,
width);
case PreparedBlockKind::Placeholder:
return LayoutPlaceholderBlock(
prepared,
@@ -459,7 +459,181 @@ void PaintPhotoProgress(
-int(std::round(360. * 16. * std::clamp(progress, 0., 1.))));
}
void PaintPhotoBlock(
[[nodiscard]] QPainterPath RoundedRectPath(QRect rect, int radius) {
auto path = QPainterPath();
path.addRoundedRect(QRectF(rect), radius, radius);
return path;
}
[[nodiscard]] bool MediaLoading(const LaidOutBlock &block) {
if (block.photoRuntime) {
return block.photoRuntime->loading();
} else if (block.documentRuntime) {
return block.documentRuntime->loading();
} else if (block.mapRuntime) {
return block.mapRuntime->loading();
}
return false;
}
[[nodiscard]] double MediaProgress(const LaidOutBlock &block) {
if (block.photoRuntime) {
return block.photoRuntime->progress();
} else if (block.documentRuntime) {
return block.documentRuntime->progress();
} else if (block.mapRuntime) {
return block.mapRuntime->progress();
}
return 0.;
}
[[nodiscard]] bool MediaLoading(const LaidOutGroupedMediaItem &item) {
if (item.photoRuntime) {
return item.photoRuntime->loading();
} else if (item.documentRuntime) {
return item.documentRuntime->loading();
}
return false;
}
[[nodiscard]] double MediaProgress(const LaidOutGroupedMediaItem &item) {
if (item.photoRuntime) {
return item.photoRuntime->progress();
} else if (item.documentRuntime) {
return item.documentRuntime->progress();
}
return 0.;
}
void PaintMediaCaption(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &markdown,
const MarkdownArticlePaintCaches &caches,
const PaintSelectionState &selectionState,
QRect clip) {
if (block.textRect.isEmpty()) {
return;
}
p.setPen(markdown.textColor->c);
PaintTextLeaf(
p,
block.leaf,
caches,
block.textRect,
block.textWidth,
clip,
style::al_left,
TextSelectionForSegmentIndex(
selectionState,
block.secondarySegmentIndex));
}
void PaintImageBackedMedia(
Painter &p,
QRect rect,
const QString &copyText,
const std::shared_ptr<Ui::DynamicImage> &thumbnailImage,
const std::shared_ptr<Ui::DynamicImage> &fullImage,
bool *thumbnailSubscribed,
bool *fullSubscribed,
bool loading,
double progress,
const style::Markdown &markdown,
const MarkdownArticlePaintCaches &caches,
bool selected,
QRect clip) {
const auto visible = clip.intersected(rect);
if (!visible.isEmpty()) {
p.save();
p.setClipRect(visible);
p.fillRect(rect, st::windowBgOver->c);
SubscribeDynamicImage(
thumbnailImage,
caches.repaint,
thumbnailSubscribed);
SubscribeDynamicImage(
fullImage,
caches.repaint,
fullSubscribed);
const auto paintedThumb = PaintDynamicImage(
p,
thumbnailImage,
rect);
const auto paintedFull = PaintDynamicImage(
p,
fullImage,
rect);
if (!paintedThumb && !paintedFull) {
p.setPen(st::windowSubTextFg->c);
p.drawText(
rect,
Qt::AlignCenter | Qt::TextWordWrap,
copyText);
}
if (loading) {
PaintPhotoProgress(
p,
rect,
markdown.photo,
progress);
}
if (selected) {
p.fillRect(rect, p.textPalette().selectOverlay);
}
p.restore();
}
}
void PaintImageBackedMediaBlock(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &markdown,
const MarkdownArticlePaintCaches &caches,
const PaintSelectionState &selectionState,
QRect clip) {
PaintImageBackedMedia(
p,
block.mediaRect,
block.copyText,
block.thumbnailImage,
block.fullImage,
&block.thumbnailSubscribed,
&block.fullSubscribed,
MediaLoading(block),
MediaProgress(block),
markdown,
caches,
block.segmentIndex >= 0
&& WholeSegmentSelected(selectionState, block.segmentIndex),
clip);
PaintMediaCaption(p, block, markdown, caches, selectionState, clip);
}
void PaintCardSurface(
Painter &p,
QRect rect,
int border,
const style::color &borderFg,
const style::color &bg,
int radius) {
if (rect.isEmpty()) {
return;
}
const auto half = border / 2.;
const auto inner = QRectF(rect).marginsRemoved({
half,
half,
half,
half,
});
auto hq = PainterHighQualityEnabler(p);
p.setPen(QPen(borderFg->c, border));
p.setBrush(bg->c);
p.drawRoundedRect(inner, radius, radius);
}
void PaintAudioBlock(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &markdown,
@@ -468,38 +642,33 @@ void PaintPhotoBlock(
QRect clip) {
const auto visible = clip.intersected(block.visibleMediaRect);
if (!visible.isEmpty()) {
const auto &style = markdown.audio;
p.save();
p.setClipRect(visible);
p.fillRect(block.mediaRect, st::windowBgOver->c);
SubscribeDynamicImage(
block.thumbnailImage,
caches.repaint,
&block.thumbnailSubscribed);
SubscribeDynamicImage(
block.fullImage,
caches.repaint,
&block.fullSubscribed);
const auto paintedThumb = PaintDynamicImage(
PaintCardSurface(
p,
block.thumbnailImage,
block.mediaRect);
const auto paintedFull = PaintDynamicImage(
block.mediaRect,
style.border,
style.borderFg,
style.bg,
style.radius);
p.setPen(style.titleFg->c);
PaintTextLeaf(
p,
block.fullImage,
block.mediaRect);
if (!paintedThumb && !paintedFull) {
p.setPen(st::windowSubTextFg->c);
p.drawText(
block.mediaRect,
Qt::AlignCenter | Qt::TextWordWrap,
block.copyText);
}
if (block.photoRuntime && block.photoRuntime->loading()) {
PaintPhotoProgress(
block.labelLeaf,
caches,
block.labelRect,
block.labelWidth,
visible);
if (!block.subtitleRect.isEmpty()) {
p.setPen(style.subtitleFg->c);
PaintTextLeaf(
p,
block.mediaRect,
markdown.photo,
block.photoRuntime->progress());
block.subtitleLeaf,
caches,
block.subtitleRect,
block.subtitleWidth,
visible);
}
if (block.segmentIndex >= 0
&& WholeSegmentSelected(selectionState, block.segmentIndex)) {
@@ -507,20 +676,171 @@ void PaintPhotoBlock(
}
p.restore();
}
if (!block.textRect.isEmpty()) {
p.setPen(markdown.textColor->c);
PaintMediaCaption(p, block, markdown, caches, selectionState, clip);
}
void PaintChannelBlock(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &markdown,
const MarkdownArticlePaintCaches &caches,
const PaintSelectionState &selectionState,
QRect clip) {
const auto visible = clip.intersected(block.visibleMediaRect);
if (!visible.isEmpty()) {
const auto &style = markdown.channel;
const auto &button = style.button;
p.save();
p.setClipRect(visible);
PaintCardSurface(
p,
block.mediaRect,
style.border,
style.borderFg,
style.bg,
style.radius);
p.setPen(style.titleFg->c);
PaintTextLeaf(
p,
block.leaf,
block.labelLeaf,
caches,
block.textRect,
block.textWidth,
clip,
style::al_left,
TextSelectionForSegmentIndex(
selectionState,
block.secondarySegmentIndex));
block.labelRect,
block.labelWidth,
visible);
if (!block.subtitleRect.isEmpty()) {
p.setPen(style.subtitleFg->c);
PaintTextLeaf(
p,
block.subtitleLeaf,
caches,
block.subtitleRect,
block.subtitleWidth,
visible);
}
if (block.channelRuntime
&& block.channelRuntime->joinVisible()
&& !block.actionRect.isEmpty()) {
const auto innerRect = block.actionRect.marginsRemoved(button.padding);
const auto half = button.border / 2.;
const auto outer = QRectF(block.actionRect).marginsRemoved({
half,
half,
half,
half,
});
{
auto hq = PainterHighQualityEnabler(p);
p.setPen(QPen(button.borderFg->c, button.border));
p.setBrush(button.bg->c);
p.drawRoundedRect(outer, button.radius, button.radius);
}
p.setPen(button.textFg->c);
PaintTextLeaf(
p,
block.actionLeaf,
caches,
innerRect,
block.actionWidth,
visible,
style::al_center);
}
if (block.segmentIndex >= 0
&& WholeSegmentSelected(selectionState, block.segmentIndex)) {
p.fillRect(block.visibleMediaRect, p.textPalette().selectOverlay);
}
p.restore();
}
PaintMediaCaption(p, block, markdown, caches, selectionState, clip);
}
void PaintPhotoBlock(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &markdown,
const MarkdownArticlePaintCaches &caches,
const PaintSelectionState &selectionState,
QRect clip) {
PaintImageBackedMediaBlock(
p,
block,
markdown,
caches,
selectionState,
clip);
}
void PaintVideoBlock(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &markdown,
const MarkdownArticlePaintCaches &caches,
const PaintSelectionState &selectionState,
QRect clip) {
PaintImageBackedMediaBlock(
p,
block,
markdown,
caches,
selectionState,
clip);
}
void PaintMapBlock(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &markdown,
const MarkdownArticlePaintCaches &caches,
const PaintSelectionState &selectionState,
QRect clip) {
PaintImageBackedMediaBlock(
p,
block,
markdown,
caches,
selectionState,
clip);
}
void PaintGroupedMediaBlock(
Painter &p,
const LaidOutBlock &block,
const style::Markdown &markdown,
const MarkdownArticlePaintCaches &caches,
const PaintSelectionState &selectionState,
QRect clip) {
const auto selected = (block.segmentIndex >= 0)
&& WholeSegmentSelected(selectionState, block.segmentIndex);
const auto visible = clip.intersected(block.visibleMediaRect);
if (!visible.isEmpty()) {
const auto &style = markdown.groupedMedia;
p.save();
p.setClipRect(visible);
const auto path = RoundedRectPath(block.mediaRect, style.radius);
p.setClipPath(path, Qt::IntersectClip);
for (const auto &item : block.groupedMediaItems) {
PaintImageBackedMedia(
p,
item.rect,
item.copyText,
item.thumbnailImage,
item.fullImage,
&item.thumbnailSubscribed,
&item.fullSubscribed,
MediaLoading(item),
MediaProgress(item),
markdown,
caches,
false,
clip);
}
if (selected) {
auto overlay = p.textPalette().selectOverlay->c;
overlay.setAlphaF(std::clamp(style.overlayOpacity, 0., 1.));
p.fillPath(path, overlay);
}
p.restore();
}
PaintMediaCaption(p, block, markdown, caches, selectionState, clip);
}
void PaintDetailsBlock(
@@ -748,6 +1068,42 @@ void PaintBlock(
selectionState,
clip);
break;
case PreparedBlockKind::Video:
PaintVideoBlock(
p,
block,
markdown,
caches,
selectionState,
clip);
break;
case PreparedBlockKind::Audio:
PaintAudioBlock(
p,
block,
markdown,
caches,
selectionState,
clip);
break;
case PreparedBlockKind::Map:
PaintMapBlock(
p,
block,
markdown,
caches,
selectionState,
clip);
break;
case PreparedBlockKind::Channel:
PaintChannelBlock(
p,
block,
markdown,
caches,
selectionState,
clip);
break;
case PreparedBlockKind::Placeholder:
PaintPlaceholderBlock(
p,
@@ -757,6 +1113,15 @@ void PaintBlock(
selectionState,
clip);
break;
case PreparedBlockKind::GroupedMedia:
PaintGroupedMediaBlock(
p,
block,
markdown,
caches,
selectionState,
clip);
break;
case PreparedBlockKind::Details:
PaintDetailsBlock(
p,
@@ -101,18 +101,31 @@ const auto kPhotoCopyLabel = u"Photo"_q;
[[nodiscard]] TextForMimeData CopyTextForMediaBlock(
const QString &label,
const Ui::Text::String &captionLeaf) {
auto result = TextForMimeData::Simple(label);
auto result = label.isEmpty()
? TextForMimeData()
: TextForMimeData::Simple(label);
if (!captionLeaf.isEmpty()) {
result.append(u"\n"_q);
if (!result.empty()) {
result.append(u"\n"_q);
}
result.append(captionLeaf.toTextForMimeData());
}
return result;
}
[[nodiscard]] TextForMimeData CopyTextForSingleMediaBlock(
const LaidOutBlock &block,
const QString &fallback = QString()) {
const auto label = !block.copyText.isEmpty()
? block.copyText
: !block.labelText.isEmpty()
? block.labelText
: fallback;
return CopyTextForMediaBlock(label, block.leaf);
}
[[nodiscard]] TextForMimeData CopyTextForPhotoBlock(const LaidOutBlock &block) {
return CopyTextForMediaBlock(
block.copyText.isEmpty() ? kPhotoCopyLabel : block.copyText,
block.leaf);
return CopyTextForSingleMediaBlock(block, kPhotoCopyLabel);
}
[[nodiscard]] TextForMimeData CopyTextForPlaceholderBlock(
@@ -350,11 +363,18 @@ void CollectSelectableSegments(
}
} break;
case PreparedBlockKind::Placeholder:
case PreparedBlockKind::Photo: {
case PreparedBlockKind::Photo:
case PreparedBlockKind::Video:
case PreparedBlockKind::Audio:
case PreparedBlockKind::Map:
case PreparedBlockKind::Channel:
case PreparedBlockKind::GroupedMedia: {
auto segment = SelectableSegment();
segment.kind = (block.kind == PreparedBlockKind::Photo)
? SelectableSegmentKind::Photo
: SelectableSegmentKind::Placeholder;
: (block.kind == PreparedBlockKind::Placeholder)
? SelectableSegmentKind::Placeholder
: SelectableSegmentKind::Media;
segment.block = &block;
segment.outerRect = block.mediaRect;
segment.length = 1;
@@ -541,6 +561,10 @@ TextForMimeData TextForSegment(
return segment.block
? CopyTextForPhotoBlock(*segment.block)
: TextForMimeData();
case SelectableSegmentKind::Media:
return segment.block
? CopyTextForSingleMediaBlock(*segment.block)
: TextForMimeData();
}
return TextForMimeData();
}
@@ -18,6 +18,7 @@ enum class SelectableSegmentKind {
Table,
Placeholder,
Photo,
Media,
};
struct SelectableSegment {
@@ -932,9 +932,9 @@ Ui::Text::CustomEmojiSemantics InlineIvImageObject::semantics() {
void InlineIvImageObject::paint(QPainter &p, const Context &context) {
if (_image) {
if (!_subscribed && _repaint) {
if (!_subscribed) {
_subscribed = true;
_image->subscribeToUpdates(_repaint);
_image->subscribeToUpdates(_repaint ? _repaint : [] {});
}
if (const auto image = _image->image(std::max(_width, _height));
!image.isNull()) {
@@ -11,6 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtCore/QString>
#include <QtCore/QVariant>
#include <rpl/never.h>
#include <rpl/producer.h>
#include <functional>
#include <memory>
@@ -39,6 +42,42 @@ public:
virtual void open(Qt::MouseButton button) const = 0;
};
class DocumentRuntime {
public:
virtual ~DocumentRuntime() = default;
[[nodiscard]] virtual std::shared_ptr<Ui::DynamicImage> thumbnail(
QSize size) const = 0;
[[nodiscard]] virtual std::shared_ptr<Ui::DynamicImage> full(
QSize size) const = 0;
[[nodiscard]] virtual bool loaded() const = 0;
[[nodiscard]] virtual bool loading() const = 0;
[[nodiscard]] virtual double progress() const = 0;
virtual void open(Qt::MouseButton button) const = 0;
};
class MapRuntime {
public:
virtual ~MapRuntime() = default;
[[nodiscard]] virtual std::shared_ptr<Ui::DynamicImage> thumbnail(
QSize size) const = 0;
[[nodiscard]] virtual std::shared_ptr<Ui::DynamicImage> full(
QSize size) const = 0;
[[nodiscard]] virtual bool loaded() const = 0;
[[nodiscard]] virtual bool loading() const = 0;
[[nodiscard]] virtual double progress() const = 0;
};
class ChannelRuntime {
public:
virtual ~ChannelRuntime() = default;
[[nodiscard]] virtual bool joinVisible() const = 0;
virtual void open(Qt::MouseButton button) const = 0;
virtual void join(Qt::MouseButton button) const = 0;
};
class MediaRuntime {
public:
virtual ~MediaRuntime() = default;
@@ -48,18 +87,43 @@ public:
QSize size) const = 0;
[[nodiscard]] virtual std::shared_ptr<PhotoRuntime> resolvePhoto(
uint64 photoId) const = 0;
[[nodiscard]] virtual std::shared_ptr<DocumentRuntime> resolveDocument(
uint64 documentId) const {
return nullptr;
}
[[nodiscard]] virtual std::shared_ptr<MapRuntime> resolveMap(
double latitude,
double longitude,
uint64 accessHash,
QSize size,
int zoom) const {
return nullptr;
}
[[nodiscard]] virtual std::shared_ptr<ChannelRuntime> resolveChannel(
uint64 channelId,
const QString &username) const {
return nullptr;
}
[[nodiscard]] virtual rpl::producer<uint64> channelJoinedChanges() const {
return rpl::never<uint64>();
}
};
enum class MediaActivationKind {
None,
ExternalUrl,
Photo,
Document,
OpenChannel,
JoinChannel,
};
struct MediaActivation {
MediaActivationKind kind = MediaActivationKind::None;
QString url;
std::shared_ptr<PhotoRuntime> photo;
std::shared_ptr<DocumentRuntime> document;
std::shared_ptr<ChannelRuntime> channel;
};
enum class ViewerKind {
@@ -363,6 +363,12 @@ bool Controller::active() const {
return _window && _window->isActiveWindow();
}
void Controller::showJoinedTooltip() {
if (_show) {
_show->showToast(tr::lng_action_you_joined(tr::now));
}
}
void Controller::minimize() {
if (_window) {
_window->setWindowState(_window->windowState() | Qt::WindowMinimized);
@@ -51,6 +51,7 @@ public:
void updateOptions(OpenOptions options = {});
[[nodiscard]] bool active() const;
void showJoinedTooltip();
void minimize();
[[nodiscard]] rpl::producer<Event> events() const {
@@ -121,6 +121,12 @@ NativeInstantViewPrepareResult TryPrepareNativeInstantView(
if (request.source->webpagePhoto) {
RememberNativeIvPhoto(&state, *request.source->webpagePhoto);
}
for (const auto &document : request.source->page.data().vdocuments().v) {
RememberNativeIvDocument(&state, document);
}
if (request.source->webpageDocument) {
RememberNativeIvDocument(&state, *request.source->webpageDocument);
}
if (!PrepareNativeIvBlocks(
request.source->page.data().vblocks().v,
@@ -36,6 +36,11 @@ enum class PreparedBlockKind {
Table,
Details,
Photo,
Video,
Audio,
Map,
Channel,
GroupedMedia,
Placeholder,
};
@@ -103,6 +108,54 @@ struct PreparedPhotoBlockData {
bool viewerOpen = false;
};
enum class PreparedMediaItemKind {
Photo,
Document,
};
struct PreparedMediaItemData {
PreparedMediaItemKind kind = PreparedMediaItemKind::Photo;
uint64 id = 0;
int width = 0;
int height = 0;
};
struct PreparedVideoBlockData {
PreparedMediaItemData media;
};
struct PreparedAudioBlockData {
uint64 documentId = 0;
QString title;
QString performer;
QString fileName;
int duration = 0;
};
struct PreparedMapBlockData {
double latitude = 0.;
double longitude = 0.;
uint64 accessHash = 0;
int width = 0;
int height = 0;
int zoom = 0;
QString url;
};
struct PreparedChannelBlockData {
uint64 channelId = 0;
QString title;
QString username;
};
struct PreparedGroupedMediaItemData {
PreparedMediaItemData media;
};
struct PreparedGroupedMediaBlockData {
std::vector<PreparedGroupedMediaItemData> items;
};
struct PreparedPlaceholderBlockData {
QString label;
QString copyText;
@@ -119,6 +172,11 @@ struct PreparedBlock {
QString formulaTex;
QString anchorId;
PreparedPhotoBlockData photo;
PreparedVideoBlockData video;
PreparedAudioBlockData audio;
PreparedMapBlockData map;
PreparedChannelBlockData channel;
PreparedGroupedMediaBlockData groupedMedia;
PreparedPlaceholderBlockData placeholder;
ListKind listKind = ListKind::Bullet;
ListDelimiter listDelimiter = ListDelimiter::None;
@@ -507,11 +507,7 @@ void SortPreparedIvRichText(PreparedIvRichText *text) {
}, [&](const MTPDpageBlockPhoto &data) {
return PrepareNativeIvPhotoBlock(data, result, state);
}, [&](const MTPDpageBlockVideo &data) {
return PrepareNativeIvPlaceholderBlock(
u"Video Placeholder"_q,
data.vcaption(),
result,
state);
return PrepareNativeIvVideoBlock(data, result, state);
}, [&](const MTPDpageBlockCover &data) {
return PrepareNativeIvBlock(data.vcover(), result, state);
}, [&](const MTPDpageBlockEmbed &data) {
@@ -527,27 +523,23 @@ void SortPreparedIvRichText(PreparedIvRichText *text) {
result,
state);
}, [&](const MTPDpageBlockCollage &data) {
return PrepareNativeIvPlaceholderBlock(
u"Collage placeholder"_q,
return PrepareNativeIvGroupedMediaBlock(
data.vitems().v,
data.vcaption(),
u"Collage placeholder"_q,
result,
state);
}, [&](const MTPDpageBlockSlideshow &data) {
return PrepareNativeIvPlaceholderBlock(
return PrepareNativeIvGroupedMediaBlock(
data.vitems().v,
data.vcaption(),
u"Grouped Media Placeholder"_q,
data.vcaption(),
result,
state);
}, [&](const MTPDpageBlockChannel &) {
return PrepareNativeIvPlainPlaceholderBlock(
u"Channel Placeholder"_q,
result);
}, [&](const MTPDpageBlockChannel &data) {
return PrepareNativeIvChannelBlock(data, result, state);
}, [&](const MTPDpageBlockAudio &data) {
return PrepareNativeIvPlaceholderBlock(
u"Audio File Placeholder"_q,
data.vcaption(),
result,
state);
return PrepareNativeIvAudioBlock(data, result, state);
}, [&](const MTPDpageBlockKicker &data) {
return AppendNativeIvFlowBlock(
result,
@@ -573,11 +565,7 @@ void SortPreparedIvRichText(PreparedIvRichText *text) {
u"Related Articles Placeholder"_q,
result);
}, [&](const MTPDpageBlockMap &data) {
return PrepareNativeIvPlaceholderBlock(
u"Map Placeholder"_q,
data.vcaption(),
result,
state);
return PrepareNativeIvMapBlock(data, result, state);
});
}
@@ -6,8 +6,13 @@ For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "iv/markdown/iv_markdown_prepare_native_richtext.h"
struct GeoPointLocation;
#include "data/data_location.h"
#include "iv/markdown/iv_markdown_prepare_links.h"
#include "ui/basic_click_handlers.h"
#include "history/history_location_manager.h"
#include <QtCore/QUrl>
@@ -41,6 +46,78 @@ void ShiftEntities(EntitiesInText *entities, int delta) {
return nullptr;
}
[[nodiscard]] const NativeIvDocumentInfo *FindNativeIvDocument(
uint64 documentId,
const NativeIvPrepareState &state) {
for (const auto &document : state.documents) {
if (document.id == documentId) {
return &document;
}
}
return nullptr;
}
void MergeNativeIvDocumentInfo(
NativeIvDocumentInfo *existing,
NativeIvDocumentInfo info) {
if (!existing || (existing->id != info.id)) {
return;
}
if (existing->width <= 0 && info.width > 0) {
existing->width = info.width;
}
if (existing->height <= 0 && info.height > 0) {
existing->height = info.height;
}
if (existing->fileName.isEmpty() && !info.fileName.isEmpty()) {
existing->fileName = std::move(info.fileName);
}
if (existing->title.isEmpty() && !info.title.isEmpty()) {
existing->title = std::move(info.title);
}
if (existing->performer.isEmpty() && !info.performer.isEmpty()) {
existing->performer = std::move(info.performer);
}
if (existing->duration <= 0 && info.duration > 0) {
existing->duration = info.duration;
}
if (!existing->isVideoFile && info.isVideoFile) {
existing->isVideoFile = true;
}
}
[[nodiscard]] bool PrepareNativeIvGroupedMediaItem(
const MTPPageBlock &item,
PreparedGroupedMediaItemData *result,
const NativeIvPrepareState &state) {
return item.match([&](const MTPDpageBlockPhoto &data) {
const auto info = FindNativeIvPhoto(uint64(data.vphoto_id().v), state);
if (!info || info->width <= 0 || info->height <= 0) {
return false;
}
result->media.kind = PreparedMediaItemKind::Photo;
result->media.id = info->id;
result->media.width = info->width;
result->media.height = info->height;
return true;
}, [&](const MTPDpageBlockVideo &data) {
const auto info = FindNativeIvDocument(uint64(data.vvideo_id().v), state);
if (!info
|| !info->isVideoFile
|| info->width <= 0
|| info->height <= 0) {
return false;
}
result->media.kind = PreparedMediaItemKind::Document;
result->media.id = info->id;
result->media.width = info->width;
result->media.height = info->height;
return true;
}, [](const auto &) {
return false;
});
}
void SortPreparedIvRichText(PreparedIvRichText *text) {
SortEntities(&text->text);
}
@@ -430,6 +507,51 @@ void RememberNativeIvPhoto(
state->photos.push_back(info);
}
void RememberNativeIvDocument(
NativeIvPrepareState *state,
const MTPDocument &document) {
auto info = NativeIvDocumentInfo{
.id = document.match([](const auto &data) {
return data.vid().v;
}),
};
document.match([](const MTPDdocumentEmpty &) {
}, [&](const MTPDdocument &data) {
const auto assignDimensions = [&](int width, int height, bool force) {
if (width <= 0 || height <= 0) {
return;
}
if (force || info.width <= 0 || info.height <= 0) {
info.width = width;
info.height = height;
}
};
for (const auto &attribute : data.vattributes().v) {
attribute.match([&](const MTPDdocumentAttributeAudio &data) {
info.duration = data.vduration().v;
info.title = qs(data.vtitle().value_or_empty());
info.performer = qs(data.vperformer().value_or_empty());
}, [&](const MTPDdocumentAttributeFilename &data) {
info.fileName = qs(data.vfile_name());
}, [&](const MTPDdocumentAttributeImageSize &data) {
assignDimensions(data.vw().v, data.vh().v, false);
}, [&](const MTPDdocumentAttributeVideo &data) {
info.isVideoFile = true;
assignDimensions(data.vw().v, data.vh().v, true);
}, [&](const auto &) {});
}
});
if (!info.id) {
return;
}
if (const auto existing = FindNativeIvDocument(info.id, *state)) {
const auto index = existing - state->documents.data();
MergeNativeIvDocumentInfo(&state->documents[index], std::move(info));
return;
}
state->documents.push_back(std::move(info));
}
bool PrepareNativeIvRichText(
const MTPRichText &text,
PreparedIvRichText *result,
@@ -507,6 +629,201 @@ bool PrepareNativeIvPhotoBlock(
return true;
}
bool PrepareNativeIvVideoBlock(
const MTPDpageBlockVideo &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
const auto info = FindNativeIvDocument(uint64(data.vvideo_id().v), *state);
if (!info
|| !info->isVideoFile
|| info->width <= 0
|| info->height <= 0) {
return PrepareNativeIvPlaceholderBlock(
u"Video Placeholder"_q,
data.vcaption(),
result,
state);
}
auto caption = PreparedIvRichText();
auto anchorId = QString();
if (!PrepareNativeIvCaption(data.vcaption(), &caption, &anchorId, state)) {
return false;
}
SortPreparedIvRichText(&caption);
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Video;
block.text = std::move(caption.text);
block.links = std::move(caption.links);
block.anchorId = std::move(anchorId);
block.video.media.kind = PreparedMediaItemKind::Document;
block.video.media.id = info->id;
block.video.media.width = info->width;
block.video.media.height = info->height;
result->push_back(std::move(block));
return true;
}
bool PrepareNativeIvAudioBlock(
const MTPDpageBlockAudio &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
const auto info = FindNativeIvDocument(uint64(data.vaudio_id().v), *state);
if (!info) {
return PrepareNativeIvPlaceholderBlock(
u"Audio File Placeholder"_q,
data.vcaption(),
result,
state);
}
auto caption = PreparedIvRichText();
auto anchorId = QString();
if (!PrepareNativeIvCaption(data.vcaption(), &caption, &anchorId, state)) {
return false;
}
SortPreparedIvRichText(&caption);
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Audio;
block.text = std::move(caption.text);
block.links = std::move(caption.links);
block.anchorId = std::move(anchorId);
block.audio.documentId = info->id;
block.audio.title = info->title;
block.audio.performer = info->performer;
block.audio.fileName = info->fileName;
block.audio.duration = info->duration;
result->push_back(std::move(block));
return true;
}
bool PrepareNativeIvMapBlock(
const MTPDpageBlockMap &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
auto prepared = PreparedMapBlockData();
const auto supported = data.vgeo().match([&](const MTPDgeoPoint &geo) {
if (!geo.vaccess_hash().v || data.vw().v <= 0 || data.vh().v <= 0) {
return false;
}
prepared.latitude = geo.vlat().v;
prepared.longitude = geo.vlong().v;
prepared.accessHash = geo.vaccess_hash().v;
prepared.width = data.vw().v;
prepared.height = data.vh().v;
prepared.zoom = data.vzoom().v;
prepared.url = LocationClickHandler::Url(Data::LocationPoint(geo));
return true;
}, [](const auto &) {
return false;
});
if (!supported) {
return PrepareNativeIvPlaceholderBlock(
u"Map Placeholder"_q,
data.vcaption(),
result,
state);
}
auto caption = PreparedIvRichText();
auto anchorId = QString();
if (!PrepareNativeIvCaption(data.vcaption(), &caption, &anchorId, state)) {
return false;
}
SortPreparedIvRichText(&caption);
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Map;
block.text = std::move(caption.text);
block.links = std::move(caption.links);
block.anchorId = std::move(anchorId);
block.map = std::move(prepared);
result->push_back(std::move(block));
return true;
}
bool PrepareNativeIvChannelBlock(
const MTPDpageBlockChannel &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *) {
auto prepared = PreparedChannelBlockData();
const auto supported = data.vchannel().match([&](const MTPDchannel &channel) {
prepared.channelId = channel.vid().v;
prepared.title = qs(channel.vtitle());
prepared.username = qs(channel.vusername().value_or_empty());
return true;
}, [&](const MTPDchannelForbidden &channel) {
prepared.channelId = channel.vid().v;
prepared.title = qs(channel.vtitle());
return true;
}, [&](const MTPDchat &channel) {
prepared.channelId = channel.vid().v;
prepared.title = qs(channel.vtitle());
return true;
}, [&](const MTPDchatForbidden &channel) {
prepared.channelId = channel.vid().v;
prepared.title = qs(channel.vtitle());
return true;
}, [](const auto &) {
return false;
});
if (!supported || !prepared.channelId || prepared.title.isEmpty()) {
return PrepareNativeIvPlainPlaceholderBlock(
u"Channel Placeholder"_q,
result);
}
auto block = PreparedBlock();
block.kind = PreparedBlockKind::Channel;
block.channel = std::move(prepared);
result->push_back(std::move(block));
return true;
}
bool PrepareNativeIvGroupedMediaBlock(
const QVector<MTPPageBlock> &items,
const MTPPageCaption &caption,
QString placeholderLabel,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
if (items.size() < 2) {
return PrepareNativeIvPlaceholderBlock(
std::move(placeholderLabel),
caption,
result,
state);
}
auto block = PreparedBlock();
block.kind = PreparedBlockKind::GroupedMedia;
block.groupedMedia.items.reserve(items.size());
for (const auto &item : items) {
auto prepared = PreparedGroupedMediaItemData();
if (!PrepareNativeIvGroupedMediaItem(item, &prepared, *state)) {
return PrepareNativeIvPlaceholderBlock(
std::move(placeholderLabel),
caption,
result,
state);
}
block.groupedMedia.items.push_back(std::move(prepared));
}
if (block.groupedMedia.items.empty()) {
return PrepareNativeIvPlaceholderBlock(
std::move(placeholderLabel),
caption,
result,
state);
}
auto preparedCaption = PreparedIvRichText();
if (!PrepareNativeIvCaption(
caption,
&preparedCaption,
&block.anchorId,
state)) {
return false;
}
SortPreparedIvRichText(&preparedCaption);
block.text = std::move(preparedCaption.text);
block.links = std::move(preparedCaption.links);
result->push_back(std::move(block));
return true;
}
namespace {
[[nodiscard]] QString NativeIvPlaceholderCopyText(
@@ -19,6 +19,9 @@ struct PreparedIvRichText {
void RememberNativeIvPhoto(
NativeIvPrepareState *state,
const MTPPhoto &photo);
void RememberNativeIvDocument(
NativeIvPrepareState *state,
const MTPDocument &document);
[[nodiscard]] bool PrepareNativeIvPlainPlaceholderBlock(
QString label,
std::vector<PreparedBlock> *result);
@@ -31,6 +34,28 @@ void RememberNativeIvPhoto(
const MTPDpageBlockPhoto &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state);
[[nodiscard]] bool PrepareNativeIvVideoBlock(
const MTPDpageBlockVideo &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state);
[[nodiscard]] bool PrepareNativeIvAudioBlock(
const MTPDpageBlockAudio &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state);
[[nodiscard]] bool PrepareNativeIvMapBlock(
const MTPDpageBlockMap &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state);
[[nodiscard]] bool PrepareNativeIvChannelBlock(
const MTPDpageBlockChannel &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state);
[[nodiscard]] bool PrepareNativeIvGroupedMediaBlock(
const QVector<MTPPageBlock> &items,
const MTPPageCaption &caption,
QString placeholderLabel,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state);
[[nodiscard]] bool PrepareNativeIvRichText(
const MTPRichText &text,
PreparedIvRichText *result,
@@ -58,9 +58,21 @@ struct NativeIvPhotoInfo {
int height = 0;
};
struct NativeIvDocumentInfo {
uint64 id = 0;
int width = 0;
int height = 0;
QString fileName;
QString title;
QString performer;
int duration = 0;
bool isVideoFile = false;
};
struct NativeIvPrepareState {
MarkdownArticleContent result;
std::vector<NativeIvPhotoInfo> photos;
std::vector<NativeIvDocumentInfo> documents;
int nextGeneratedId = 0;
void setFailure(
@@ -198,8 +198,10 @@ private:
Ui::FlatLabel *_failure = nullptr;
Ui::LinkButton *_failureOpen = nullptr;
const std::shared_ptr<MathRenderer> _renderer;
std::shared_ptr<MarkdownArticle> _article;
QString _pendingFragment;
int _devicePixelRatio = 0;
rpl::lifetime _channelJoinedLifetime;
};
@@ -451,7 +453,9 @@ void MarkdownPreviewRoot::applyPreparedContent(
int prepareMs) {
const auto failure = prepared.failure;
const auto debug = prepared.debug;
_channelJoinedLifetime.destroy();
if (failure.failed()) {
_article = nullptr;
_footnotes.clear();
_scroll->hide();
if (_body) {
@@ -473,14 +477,27 @@ void MarkdownPreviewRoot::applyPreparedContent(
_footnotes = prepared.footnotes;
if (!_body) {
_article = nullptr;
logPreparationSummary(failure, debug, prepareMs, 0);
return;
}
if (prepared.mediaRuntime) {
prepared.mediaRuntime->channelJoinedChanges(
) | rpl::on_next([=](uint64) {
if (_body && !_body->isHidden() && _article) {
_article->invalidateLayout();
updateChildrenGeometry(size());
_body->update();
}
}, _channelJoinedLifetime);
}
auto article = std::make_shared<MarkdownArticle>(_renderer);
article->setContent(std::move(prepared));
_article = article;
updateChildrenGeometry(size());
_body->setArticle(std::move(article));
_body->setArticle(article);
if (_options.delegate) {
_body->setZoom(_options.delegate->ivZoom());
}
File diff suppressed because it is too large Load Diff