Files
AyuGramDesktop/Telegram/SourceFiles/iv/markdown/iv_markdown_view.cpp
T
2026-07-11 15:10:15 +04:00

1170 lines
32 KiB
C++

/*
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/markdown/iv_markdown_view.h"
#include "base/algorithm.h"
#include "base/weak_ptr.h"
#include "base/platform/base_platform_info.h"
#include "core/click_handler_types.h"
#include "core/credits_amount.h"
#include "core/file_utilities.h"
#include "iv/markdown/iv_markdown_article_text.h"
#include "iv/markdown/iv_markdown_embed_overlay.h"
#include "iv/markdown/iv_markdown_prepare_links.h"
#include "iv/markdown/iv_markdown_view_widget.h"
#include "iv/iv_delegate.h"
#include "lang/lang_keys.h"
#include "ui/controls/jump_down_button.h"
#include "ui/effects/animations.h"
#include "ui/layers/generic_box.h"
#include "ui/layers/layer_manager.h"
#include "ui/style/style_core_scale.h"
#include "ui/text/text.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/elastic_scroll.h"
#include "ui/basic_click_handlers.h"
#include "ui/integration.h"
#include "ui/rect.h"
#include "logs.h"
#include "styles/style_iv.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include "styles/style_window.h"
#include <QtCore/QElapsedTimer>
#include <QtGui/QPainter>
#include <QtGui/QScreen>
#include <algorithm>
#include <cstdlib>
#include <memory>
#include <optional>
#include <utility>
namespace Iv::Markdown {
namespace {
constexpr auto kZoomStep = 10;
#ifndef NDEBUG
[[nodiscard]] QString PrepareTerminalFailureName(
PrepareTerminalFailure failure) {
switch (failure) {
case PrepareTerminalFailure::None:
return u"none"_q;
case PrepareTerminalFailure::InvalidRequest:
return u"invalid-request"_q;
case PrepareTerminalFailure::InvalidStyle:
return u"invalid-style"_q;
case PrepareTerminalFailure::DocumentTooLarge:
return u"document-too-large"_q;
case PrepareTerminalFailure::InternalError:
return u"internal-error"_q;
}
return u"unknown"_q;
}
[[nodiscard]] QString PrepareFailureReasonText(
const PrepareFailureStatus &failure) {
return !failure.debugReason.isEmpty()
? failure.debugReason
: PrepareTerminalFailureName(failure.terminal);
}
#endif
[[nodiscard]] QVariant CurrentClickHandlerContext(const OpenOptions &options) {
return options.clickHandlerContextRef
? *options.clickHandlerContextRef
: options.clickHandlerContext;
}
[[nodiscard]] const PreparedFootnote *FindFootnote(
const std::vector<PreparedFootnote> &footnotes,
const QString &target) {
const auto i = std::find_if(
footnotes.begin(),
footnotes.end(),
[&](const PreparedFootnote &footnote) {
return (footnote.label == target);
});
return (i != footnotes.end()) ? &*i : nullptr;
}
[[nodiscard]] int FootnoteLabelContentWidth(
not_null<Ui::FlatLabel*> label,
int maxWidth) {
const auto heightForWidth = [&](int width) {
label->resizeToWidth(width);
return label->heightNoMargins();
};
const auto natural = label->naturalWidth();
const auto minWidth = std::min(st::markdownFootnoteLabel.minWidth, maxWidth);
auto result = std::max(
(natural >= 0) ? std::min(natural, maxWidth) : maxWidth,
minWidth);
if (result >= label->textMaxWidth()) {
return result;
}
auto large = result;
auto small = std::max(result / 2, minWidth);
const auto largeHeight = heightForWidth(large);
while (large - small > 1) {
const auto middle = (large + small) / 2;
if (largeHeight == heightForWidth(middle)) {
large = middle;
} else {
small = middle;
}
}
return large;
}
[[nodiscard]] bool ActivateExternalLink(
const PreparedLink &link,
Qt::MouseButton button,
QVariant context) {
const auto data = ExternalEntityLinkData(link);
if (!data) {
return false;
}
const auto handler = Ui::Integration::Instance().createLinkHandler(
*data,
Ui::Text::MarkedContext());
if (!handler) {
return false;
}
auto click = ClickContext();
click.button = button;
click.other = std::move(context);
handler->onClick(std::move(click));
return true;
}
[[nodiscard]] std::optional<QString> CurrentPageExternalLinkFragment(
const PreparedLink &link,
const OpenOptions &options) {
if (link.kind != PreparedLinkKind::External
|| link.target.isEmpty()
|| options.sourceUrl.isEmpty()) {
return std::nullopt;
}
const auto hash = link.target.indexOf(QChar('#'));
const auto base = (hash < 0) ? link.target : link.target.mid(0, hash);
if (base != options.sourceUrl
&& base != UrlClickHandler::EncodeForOpening(options.sourceUrl)) {
return std::nullopt;
}
auto fragment = (hash < 0) ? QString() : link.target.mid(hash + 1);
return NormalizeFragmentId(std::move(fragment));
}
[[nodiscard]] bool ShowWrongLayoutBar(const OpenOptions &options) {
return (options.viewerKind != ViewerKind::LocalFile)
&& !options.sourceUrl.isEmpty()
&& (options.currentPageId != 0);
}
} // namespace
class MarkdownPreviewRoot final : public Ui::RpWidget {
public:
MarkdownPreviewRoot(
QWidget *parent,
const PreparedDocument &document,
Fn<void(Event)> callback,
const OpenOptions &options);
MarkdownPreviewRoot(
QWidget *parent,
MarkdownArticleContent content,
std::shared_ptr<MathRenderer> renderer,
Fn<void(Event)> callback,
const OpenOptions &options);
bool scrollToAnchor(
const QString &anchorId,
MarkdownPreviewScrollMode mode);
void scrollToY(int top, MarkdownPreviewScrollMode mode);
[[nodiscard]] int scrollTop() const;
[[nodiscard]] rpl::producer<int> scrollTopValue() const;
bool updateContent(MarkdownArticleContent prepared, OpenOptions options);
[[nodiscard]] auto searchSources() const
-> std::vector<MarkdownArticleSearchSource>;
void setSearchMatches(
std::vector<MarkdownArticleSearchMatch> matches,
int current);
bool expandDetails(const QString &anchorId);
void scrollToSegment(int segmentIndex, int topMargin);
private:
struct PendingEmbedState {
PreparedPlaceholderBlockId placeholderId;
uint64 generation = 0;
};
void setup();
void prepareArticle();
void activateLink(const PreparedLink &link, Qt::MouseButton button);
void closeEmbed();
void openEmbedLink(QString url);
void showFootnote(const PreparedLink &link, Qt::MouseButton button);
[[nodiscard]] bool showEmbed(const MediaActivation &activation);
void fillFootnoteBox(
not_null<Ui::GenericBox*> box,
PreparedFootnote footnote);
void applyPreparedContent(MarkdownArticleContent prepared, int prepareMs);
void scrollToTop();
void scrollToYAnimated(int top);
void updateBodyVisibleTopBottom();
void updateScrollToTopVisibility();
void startScrollToTopButtonAnimation(bool shown);
void updateScrollToTopPosition();
void updateChildrenGeometry(QSize size);
void updateWrongLayoutGeometry();
void updateFailureGeometry();
void logPreparationSummary(
const PrepareFailureStatus &failure,
const PrepareDebugStats &debug,
int prepareMs,
int layoutMs) const;
OpenOptions _options;
const std::shared_ptr<const PreparedDocument> _document;
std::optional<MarkdownArticleContent> _preparedContent;
const Fn<void(Event)> _callback;
std::vector<PreparedFootnote> _footnotes;
std::unique_ptr<Ui::LayerManager> _footnoteLayerManager;
EmbedOverlay *_embedOverlay = nullptr;
Ui::ElasticScroll *_scroll = nullptr;
Ui::RpWidget *_scrollContent = nullptr;
Ui::JumpDownButton *_scrollToTop = nullptr;
MarkdownDocumentWidget *_body = nullptr;
Ui::RpWidget *_wrongLayoutBar = nullptr;
Ui::LinkButton *_wrongLayout = nullptr;
Ui::FlatLabel *_failure = nullptr;
Ui::LinkButton *_failureOpen = nullptr;
const std::shared_ptr<MathRenderer> _renderer;
std::shared_ptr<MarkdownArticle> _article;
QString _pendingFragment;
int _devicePixelRatio = 0;
PendingEmbedState _pendingEmbed;
Ui::Animations::Simple _scrollToAnimation;
Ui::Animations::Simple _scrollToTopShown;
bool _scrollToTopIsShown = false;
};
MarkdownPreviewRoot::MarkdownPreviewRoot(
QWidget *parent,
const PreparedDocument &document,
Fn<void(Event)> callback,
const OpenOptions &options)
: Ui::RpWidget(parent)
, _options(options)
, _document(std::make_shared<PreparedDocument>(document))
, _callback(std::move(callback))
, _renderer(std::make_shared<MathRenderer>())
, _pendingFragment(options.initialFragment) {
setup();
}
MarkdownPreviewRoot::MarkdownPreviewRoot(
QWidget *parent,
MarkdownArticleContent content,
std::shared_ptr<MathRenderer> renderer,
Fn<void(Event)> callback,
const OpenOptions &options)
: Ui::RpWidget(parent)
, _options(options)
, _preparedContent(std::move(content))
, _callback(std::move(callback))
, _renderer(renderer ? std::move(renderer) : std::make_shared<MathRenderer>())
, _pendingFragment(options.initialFragment) {
setup();
}
void MarkdownPreviewRoot::setup() {
_footnoteLayerManager = std::make_unique<Ui::LayerManager>(not_null{ this });
_footnoteLayerManager->setHideByBackgroundClick(true);
_scroll = Ui::CreateChild<Ui::ElasticScroll>(this, st::boxScroll);
using OverscrollType = Ui::ElasticScroll::OverscrollType;
_scroll->setOverscrollTypes(OverscrollType::Real, OverscrollType::Real);
_scroll->setOverscrollBg(st::windowBg->c);
_scrollContent = _scroll->setOwnedWidget(object_ptr<Ui::RpWidget>(_scroll));
_body = Ui::CreateChild<MarkdownDocumentWidget>(_scrollContent);
_scrollToTop = Ui::CreateChild<Ui::JumpDownButton>(_scroll, st::dialogsToUp);
_scrollToTop->setClickedCallback([=] { scrollToTop(); });
_scrollToTop->setAccessibleName(tr::lng_sr_scroll_to_top(tr::now));
_scrollToTop->raise();
if (ShowWrongLayoutBar(_options)) {
_wrongLayoutBar = Ui::CreateChild<Ui::RpWidget>(_scrollContent);
_wrongLayoutBar->paintRequest(
) | rpl::on_next([=](QRect clip) {
QPainter(_wrongLayoutBar).fillRect(clip, st::windowBgOver);
}, _wrongLayoutBar->lifetime());
_wrongLayout = Ui::CreateChild<Ui::LinkButton>(
_wrongLayoutBar,
tr::lng_iv_wrong_layout(tr::now),
st::ivWrongLayoutLink);
_wrongLayout->setClickedCallback([=] {
_callback({
.type = Event::Type::Report,
.webpageId = _options.currentPageId,
.context = CurrentClickHandlerContext(_options),
});
});
_wrongLayoutBar->hide();
}
_failure = Ui::CreateChild<Ui::FlatLabel>(
this,
tr::lng_markdown_preview_cant(tr::now),
st::defaultMarkdown.failure.label);
_failureOpen = Ui::CreateChild<Ui::LinkButton>(
this,
tr::lng_markdown_preview_open_file(tr::now));
_embedOverlay = Ui::CreateChild<EmbedOverlay>(
this,
[=](QString url) {
openEmbedLink(std::move(url));
},
_options.ivWebviewStorageId);
_embedOverlay->hide();
_scroll->hide();
if (_body) {
_body->hide();
_body->setClickHandlerContext(
CurrentClickHandlerContext(_options),
_options.clickHandlerContextRef);
_body->setLinkActivationCallback([=](
const PreparedLink &link,
Qt::MouseButton button) {
activateLink(link, button);
});
_body->setMediaActivationCallback([=](
const MediaActivation &activation,
Qt::MouseButton button) {
if (activation.kind == MediaActivationKind::Embed) {
return (button == Qt::LeftButton || button == Qt::MiddleButton)
? showEmbed(activation)
: false;
}
return _options.activateMedia
? _options.activateMedia(activation, button)
: false;
});
if (_options.delegate) {
const auto delegate = _options.delegate;
_body->setZoom(delegate->ivZoom());
_body->setZoomStepCallback([=](int steps) {
delegate->ivSetZoom(delegate->ivZoom() + steps * kZoomStep);
if (_options.zoomActivated) {
_options.zoomActivated();
}
});
}
_body->heightValue(
) | rpl::on_next([=](int) {
updateChildrenGeometry(size());
}, _body->lifetime());
}
_failure->hide();
_failureOpen->hide();
_failureOpen->setClickedCallback([=] {
if (!_options.sourcePath.isEmpty()) {
File::Launch(_options.sourcePath);
}
});
sizeValue() | rpl::on_next([=](QSize size) {
updateChildrenGeometry(size);
}, lifetime());
rpl::combine(
_scroll->scrollTopValue(),
_scroll->heightValue()
) | rpl::on_next([=](int, int) {
updateBodyVisibleTopBottom();
updateScrollToTopVisibility();
updateScrollToTopPosition();
}, lifetime());
style::PaletteChanged() | rpl::on_next([=] {
_scroll->setOverscrollBg(st::windowBg->c);
if (_body && !_body->isHidden()) {
_body->refreshPalette();
}
}, lifetime());
screenValue() | rpl::on_next([=](not_null<QScreen*>) {
const auto devicePixelRatio = style::DevicePixelRatio();
if (devicePixelRatio == _devicePixelRatio) {
return;
}
_devicePixelRatio = devicePixelRatio;
if (_body && !_body->isHidden()) {
_body->invalidateRasterCache();
}
}, lifetime());
if (_options.delegate) {
_options.delegate->ivZoomValue(
) | rpl::on_next([=](int value) {
if (!_body) {
return;
}
const auto scrollTop = _scroll ? _scroll->scrollTop() : 0;
const auto anchor = (scrollTop > 0)
? _body->scrollAnchorForTop(scrollTop)
: std::nullopt;
_body->setZoom(value);
updateChildrenGeometry(size());
if (anchor) {
const auto top = _body->scrollTopForAnchor(*anchor);
if (top >= 0) {
scrollToY(top, MarkdownPreviewScrollMode::Instant);
}
}
}, lifetime());
}
rpl::duplicate(
_options.downloadTaskFinished
) | rpl::on_next([=] {
update();
}, lifetime());
_devicePixelRatio = style::DevicePixelRatio();
prepareArticle();
}
void MarkdownPreviewRoot::prepareArticle() {
if (_preparedContent) {
applyPreparedContent(std::move(*_preparedContent), 0);
_preparedContent.reset();
return;
}
if (_renderer) {
_renderer->resetDebugCounters();
}
auto timer = QElapsedTimer();
timer.start();
auto prepared = PrepareSynchronously({
.document = _document,
.renderer = _renderer,
.dimensions = CaptureMarkdownPrepareDimensions(),
.sourcePath = _options.sourcePath,
});
applyPreparedContent(std::move(prepared), int(timer.elapsed()));
}
bool MarkdownPreviewRoot::updateContent(
MarkdownArticleContent prepared,
OpenOptions options) {
_options = std::move(options);
if (!_options.initialFragment.isEmpty()) {
_pendingFragment = _options.initialFragment;
}
if (_body) {
_body->setClickHandlerContext(
CurrentClickHandlerContext(_options),
_options.clickHandlerContextRef);
}
applyPreparedContent(std::move(prepared), 0);
return true;
}
void MarkdownPreviewRoot::activateLink(
const PreparedLink &link,
Qt::MouseButton button) {
if (button != Qt::LeftButton && button != Qt::MiddleButton) {
return;
}
switch (link.kind) {
case PreparedLinkKind::External: {
if (const auto fragment = CurrentPageExternalLinkFragment(
link,
_options)) {
if (fragment->isEmpty()) {
scrollToY(0, MarkdownPreviewScrollMode::Animated);
} else if (!scrollToAnchor(
*fragment,
MarkdownPreviewScrollMode::Animated)) {
DEBUG_LOG(("Native Markdown IV: unresolved anchor: %1").arg(
*fragment));
}
return;
}
if (!ActivateExternalLink(
link,
button,
CurrentClickHandlerContext(_options))) {
DEBUG_LOG(("Native Markdown IV: failed external link activation: %1").arg(
link.target));
}
} break;
case PreparedLinkKind::InstantViewPage: {
auto target = link.target;
if (!link.fragment.isEmpty()) {
target += u"#"_q + link.fragment;
}
_callback({
.type = Event::Type::OpenPage,
.webpageId = link.webpageId,
.url = std::move(target),
.context = CurrentClickHandlerContext(_options),
});
} break;
case PreparedLinkKind::Anchor:
case PreparedLinkKind::FootnoteBacklink:
if (!scrollToAnchor(link.target, MarkdownPreviewScrollMode::Animated)) {
DEBUG_LOG(("Native Markdown IV: unresolved anchor: %1").arg(
link.target));
}
break;
case PreparedLinkKind::Footnote:
showFootnote(link, button);
break;
case PreparedLinkKind::LocalFile: {
auto target = link.target;
if (!link.fragment.isEmpty()) {
target += u"#"_q + link.fragment;
}
_callback({
.type = Event::Type::OpenFile,
.url = std::move(target),
.context = CurrentClickHandlerContext(_options),
});
} break;
case PreparedLinkKind::RejectedRelative:
DEBUG_LOG(("Native Markdown IV: "
"rejected relative markdown link: %1").arg(
link.target));
break;
case PreparedLinkKind::ToggleDetails:
if (_body && !_body->toggleDetails(link.target)) {
DEBUG_LOG(("Native Markdown IV: failed details toggle: %1").arg(
link.target));
}
break;
}
}
void MarkdownPreviewRoot::closeEmbed() {
if (_body) {
_body->clearAllPlaceholderLoading();
}
const auto hadPending = bool(_pendingEmbed.placeholderId);
_pendingEmbed.placeholderId = {};
if (!_embedOverlay) {
return;
}
if (hadPending) {
_embedOverlay->cancelPreload();
} else {
_embedOverlay->closeEmbed();
}
}
void MarkdownPreviewRoot::openEmbedLink(QString url) {
if (url.isEmpty()) {
return;
}
closeEmbed();
HiddenUrlClickHandler::Open(url, CurrentClickHandlerContext(_options));
}
void MarkdownPreviewRoot::showFootnote(
const PreparedLink &link,
Qt::MouseButton button) {
Q_UNUSED(button);
const auto found = FindFootnote(_footnotes, link.target);
if (!found) {
DEBUG_LOG(("Native Markdown IV: unresolved footnote: %1").arg(
link.target));
return;
}
if (!_footnoteLayerManager) {
DEBUG_LOG(("Native Markdown IV: missing footnote layer manager: %1").arg(
link.target));
return;
}
const auto footnote = *found;
_footnoteLayerManager->showBox(Box([=](not_null<Ui::GenericBox*> box) {
fillFootnoteBox(box, footnote);
}));
}
bool MarkdownPreviewRoot::showEmbed(const MediaActivation &activation) {
if (activation.kind != MediaActivationKind::Embed
|| !activation.embed
|| !activation.placeholderId) {
return false;
}
if (Platform::IsLinux()) {
closeEmbed();
return _embedOverlay
? _embedOverlay->showExternalEmbed(activation.embed)
: false;
}
const auto placeholderId = activation.placeholderId;
const auto generation = ++_pendingEmbed.generation;
if (_body && _pendingEmbed.placeholderId) {
_body->clearPlaceholderLoading(_pendingEmbed.placeholderId);
}
if (_pendingEmbed.placeholderId && _embedOverlay) {
_embedOverlay->cancelPreload();
}
_pendingEmbed.placeholderId = placeholderId;
if (_body) {
_body->setPlaceholderLoading(placeholderId);
}
const auto finishPending = [=] {
if (_pendingEmbed.generation != generation
|| (_pendingEmbed.placeholderId.value != placeholderId.value)) {
return;
}
if (_body) {
_body->clearPlaceholderLoading(placeholderId);
}
_pendingEmbed.placeholderId = {};
};
if (!_embedOverlay) {
finishPending();
return false;
}
const auto started = _embedOverlay->preloadEmbed(
activation.embed,
finishPending,
finishPending);
if (!started) {
finishPending();
}
return started;
}
void MarkdownPreviewRoot::fillFootnoteBox(
not_null<Ui::GenericBox*> box,
PreparedFootnote footnote) {
box->setStyle(st::markdownFootnoteBox);
box->setNoContentMargin(true);
box->setCloseByOutsideClick(true);
box->clearButtons();
auto label = object_ptr<Ui::FlatLabel>(box, st::markdownFootnoteLabel);
label->setMarkedText(footnote.text);
label->setTryMakeSimilarLines(true);
for (const auto &link : footnote.links) {
label->setLink(link.index, CreatePreparedLinkHandler(link));
}
label->setClickHandlerFilter([=](
const ClickHandlerPtr &handler,
Qt::MouseButton button) {
if (const auto prepared = ExtractPreparedLink(handler)) {
activateLink(*prepared, button);
return false;
}
return true;
});
const auto skips = rect::m::sum::h(st::markdownFootnotePadding);
const auto contentWidth = FootnoteLabelContentWidth(
label.get(),
std::max(
st::boxWideWidth - skips,
st::markdownFootnoteLabel.minWidth));
label->resizeToWidth(contentWidth);
box->setWidth(contentWidth + skips);
box->addRow(std::move(label), st::markdownFootnotePadding);
}
void MarkdownPreviewRoot::applyPreparedContent(
MarkdownArticleContent prepared,
int prepareMs) {
const auto failure = prepared.failure;
const auto debug = prepared.debug;
closeEmbed();
if (failure.failed()) {
_article = nullptr;
_footnotes.clear();
_scrollToAnimation.stop();
startScrollToTopButtonAnimation(false);
_scroll->hide();
if (_body) {
_body->setArticle(nullptr);
_body->hide();
}
if (_wrongLayoutBar) {
_wrongLayoutBar->hide();
}
_failure->show();
if (_options.sourcePath.isEmpty()) {
_failureOpen->hide();
} else {
_failureOpen->show();
}
_failure->raise();
_failureOpen->raise();
updateFailureGeometry();
logPreparationSummary(failure, debug, prepareMs, 0);
return;
}
_footnotes = prepared.footnotes;
if (!_body) {
_article = nullptr;
logPreparationSummary(failure, debug, prepareMs, 0);
return;
}
const auto restoreArticle = !_article;
if (!_article) {
_article = std::make_shared<MarkdownArticle>(
st::defaultMarkdown,
_renderer);
}
_article->setContent(std::move(prepared));
_body->setClickHandlerContext(
CurrentClickHandlerContext(_options),
_options.clickHandlerContextRef);
if (restoreArticle) {
updateChildrenGeometry(size());
_body->setArticle(_article);
if (_options.delegate) {
_body->setZoom(_options.delegate->ivZoom());
}
} else {
_body->articleContentChanged();
}
_scroll->show();
_body->show();
if (_wrongLayoutBar) {
_wrongLayoutBar->show();
}
updateChildrenGeometry(size());
updateBodyVisibleTopBottom();
_failure->hide();
_failureOpen->hide();
if (!_pendingFragment.isEmpty()) {
const auto scrolled = scrollToAnchor(
_pendingFragment,
MarkdownPreviewScrollMode::Instant);
static_cast<void>(scrolled);
_pendingFragment.clear();
}
_scrollToTop->raise();
updateScrollToTopVisibility();
updateScrollToTopPosition();
logPreparationSummary(
failure,
debug,
prepareMs,
_body->lastRelayoutMs());
}
bool MarkdownPreviewRoot::scrollToAnchor(
const QString &anchorId,
MarkdownPreviewScrollMode mode) {
if (!_body || !_scroll || anchorId.isEmpty()) {
return false;
}
auto top = _body->anchorTop(anchorId);
if (top < 0) {
if (!_body->expandDetailsToAnchor(anchorId)) {
return false;
}
top = _body->anchorTop(anchorId);
if (top < 0) {
return false;
}
}
scrollToY(top, mode);
return true;
}
void MarkdownPreviewRoot::scrollToY(
int top,
MarkdownPreviewScrollMode mode) {
if (!_scroll) {
return;
}
switch (mode) {
case MarkdownPreviewScrollMode::Instant:
_scrollToAnimation.stop();
_scroll->scrollToY(top);
updateScrollToTopVisibility();
break;
case MarkdownPreviewScrollMode::Animated:
scrollToYAnimated(top);
break;
}
}
int MarkdownPreviewRoot::scrollTop() const {
return _scroll ? _scroll->scrollTop() : 0;
}
rpl::producer<int> MarkdownPreviewRoot::scrollTopValue() const {
return _scroll
? _scroll->scrollTopValue()
: rpl::single(0) | rpl::type_erased;
}
auto MarkdownPreviewRoot::searchSources() const
-> std::vector<MarkdownArticleSearchSource> {
return _article
? _article->searchSources()
: std::vector<MarkdownArticleSearchSource>();
}
void MarkdownPreviewRoot::setSearchMatches(
std::vector<MarkdownArticleSearchMatch> matches,
int current) {
if (_body) {
_body->setSearchMatches(std::move(matches), current);
}
}
bool MarkdownPreviewRoot::expandDetails(const QString &anchorId) {
return _body ? _body->expandDetailsBlock(anchorId) : false;
}
void MarkdownPreviewRoot::scrollToSegment(
int segmentIndex,
int topMargin) {
if (!_body || !_scroll) {
return;
}
const auto rect = _body->segmentRect(segmentIndex);
if (rect.isEmpty()) {
return;
}
const auto current = _scroll->scrollTop();
const auto height = _scroll->height();
const auto from = rect.y() - topMargin;
const auto till = rect.y() + rect.height();
auto target = current;
if (from < current) {
target = from;
} else if (till > current + height) {
target = std::min(till - height, from);
}
scrollToYAnimated(target);
}
bool ScrollMarkdownPreviewToAnchor(
Ui::RpWidget *preview,
const QString &anchorId,
MarkdownPreviewScrollMode mode) {
const auto root = dynamic_cast<MarkdownPreviewRoot*>(preview);
return root ? root->scrollToAnchor(anchorId, mode) : false;
}
void ScrollMarkdownPreviewToY(
Ui::RpWidget *preview,
int top,
MarkdownPreviewScrollMode mode) {
if (const auto root = dynamic_cast<MarkdownPreviewRoot*>(preview)) {
root->scrollToY(top, mode);
}
}
int MarkdownPreviewScrollTop(Ui::RpWidget *preview) {
const auto root = dynamic_cast<MarkdownPreviewRoot*>(preview);
return root ? root->scrollTop() : 0;
}
rpl::producer<int> MarkdownPreviewScrollTopValue(Ui::RpWidget *preview) {
const auto root = dynamic_cast<MarkdownPreviewRoot*>(preview);
return root ? root->scrollTopValue() : rpl::single(0);
}
auto MarkdownPreviewSearchSources(Ui::RpWidget *preview)
-> std::vector<MarkdownArticleSearchSource> {
const auto root = dynamic_cast<MarkdownPreviewRoot*>(preview);
return root
? root->searchSources()
: std::vector<MarkdownArticleSearchSource>();
}
bool ExpandMarkdownPreviewDetails(
Ui::RpWidget *preview,
const QString &anchorId) {
const auto root = dynamic_cast<MarkdownPreviewRoot*>(preview);
return root ? root->expandDetails(anchorId) : false;
}
void ScrollMarkdownPreviewToSegment(
Ui::RpWidget *preview,
int segmentIndex,
int topMargin) {
if (const auto root = dynamic_cast<MarkdownPreviewRoot*>(preview)) {
root->scrollToSegment(segmentIndex, topMargin);
}
}
void SetMarkdownPreviewSearchMatches(
Ui::RpWidget *preview,
std::vector<MarkdownArticleSearchMatch> matches,
int current) {
if (const auto root = dynamic_cast<MarkdownPreviewRoot*>(preview)) {
root->setSearchMatches(std::move(matches), current);
}
}
void MarkdownPreviewRoot::scrollToTop() {
if (!_scroll || _scrollToAnimation.animating()) {
return;
}
if (_scroll->scrollTop() == 0) {
return;
}
scrollToYAnimated(0);
}
void MarkdownPreviewRoot::scrollToYAnimated(int top) {
if (!_scroll) {
return;
}
const auto scrollTo = std::clamp(top, 0, _scroll->scrollTopMax());
_scrollToAnimation.stop();
auto scrollTop = _scroll->scrollTop();
if (scrollTop == scrollTo) {
updateScrollToTopVisibility();
return;
}
const auto maxAnimatedDelta = _scroll->height();
if (scrollTo + maxAnimatedDelta < scrollTop) {
scrollTop = scrollTo + maxAnimatedDelta;
_scroll->scrollToY(scrollTop);
} else if (scrollTo - maxAnimatedDelta > scrollTop) {
scrollTop = scrollTo - maxAnimatedDelta;
_scroll->scrollToY(scrollTop);
}
startScrollToTopButtonAnimation(false);
const auto scroll = [=] {
if (!_scroll) {
return;
}
const auto animated = qRound(_scrollToAnimation.value(scrollTo));
const auto animatedDelta = animated - scrollTo;
const auto realDelta = _scroll->scrollTop() - scrollTo;
if (base::OppositeSigns(realDelta, animatedDelta)) {
_scrollToAnimation.stop();
} else if (std::abs(realDelta) > std::abs(animatedDelta)) {
_scroll->scrollToY(animated);
}
};
_scrollToAnimation.start(
scroll,
scrollTop,
scrollTo,
st::slideDuration,
anim::sineInOut);
}
void MarkdownPreviewRoot::updateBodyVisibleTopBottom() {
if (_body) {
const auto scrollTop = _scroll->scrollTop();
_body->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());
}
}
void MarkdownPreviewRoot::updateScrollToTopVisibility() {
if (_scrollToAnimation.animating()) {
return;
}
const auto scrollTop = _scroll->scrollTop();
const auto scrollTopMax = _scroll->scrollTopMax();
const auto nearBottom = (scrollTop + st::historyToDownShownAfter / 2)
>= scrollTopMax;
startScrollToTopButtonAnimation(
!_scroll->isHidden()
&& (scrollTop > (st::historyToDownShownAfter / 2))
&& !nearBottom);
}
void MarkdownPreviewRoot::startScrollToTopButtonAnimation(bool shown) {
if (_scrollToTopIsShown == shown) {
return;
}
_scrollToTopIsShown = shown;
_scrollToTopShown.start(
[=] { updateScrollToTopPosition(); },
_scrollToTopIsShown ? 0. : 1.,
_scrollToTopIsShown ? 1. : 0.,
st::historyToDownDuration);
}
void MarkdownPreviewRoot::updateScrollToTopPosition() {
const auto top = anim::interpolate(
0,
_scrollToTop->height() + st::connectingMargin.top(),
_scrollToTopShown.value(_scrollToTopIsShown ? 1. : 0.));
_scrollToTop->moveToRight(
st::historyToDownPosition.x(),
_scroll->height() - top);
const auto shouldBeHidden
= !_scrollToTopIsShown && !_scrollToTopShown.animating();
if (shouldBeHidden != _scrollToTop->isHidden()) {
_scrollToTop->setVisible(!shouldBeHidden);
}
}
void MarkdownPreviewRoot::updateChildrenGeometry(QSize size) {
_scroll->setGeometry(QRect(QPoint(), size));
const auto contentWidth = std::max(_scroll->width(), 1);
auto bodyWidth = contentWidth;
if (_body) {
bodyWidth = std::min(bodyWidth, _body->maxWidth());
_body->resizeToWidth(bodyWidth);
_body->moveToLeft(
std::max((contentWidth - bodyWidth) / 2, 0),
0,
contentWidth);
updateBodyVisibleTopBottom();
}
if (_scrollContent) {
const auto bodyHeight = _body ? _body->height() : 0;
const auto footerHeight = (_wrongLayoutBar
&& !_wrongLayoutBar->isHidden())
? st::ivWrongLayoutBarHeight
: 0;
_scrollContent->resize(
contentWidth,
std::max(_scroll->height(), bodyHeight + footerHeight));
}
updateWrongLayoutGeometry();
if (_embedOverlay) {
_embedOverlay->updateGeometry(QRect(QPoint(), size), bodyWidth);
}
_scrollToTop->raise();
updateScrollToTopPosition();
updateFailureGeometry();
}
void MarkdownPreviewRoot::updateWrongLayoutGeometry() {
if (!_wrongLayoutBar || !_wrongLayout) {
return;
}
const auto footerHeight = st::ivWrongLayoutBarHeight;
const auto contentWidth = _scrollContent
? std::max(_scrollContent->width(), 1)
: std::max(width(), 1);
const auto contentHeight = _scrollContent
? _scrollContent->height()
: height();
_wrongLayoutBar->setGeometry(
0,
std::max(contentHeight - footerHeight, 0),
contentWidth,
footerHeight);
_wrongLayout->resizeToNaturalWidth(contentWidth);
_wrongLayout->moveToLeft(
std::max((contentWidth - _wrongLayout->width()) / 2, 0),
std::max((footerHeight - _wrongLayout->height()) / 2, 0),
contentWidth);
}
void MarkdownPreviewRoot::updateFailureGeometry() {
const auto availableWidth = std::max(width(), 1);
const auto failureWidth = std::min(
availableWidth,
st::defaultMarkdown.failure.width);
_failure->resizeToWidth(failureWidth);
_failureOpen->resizeToNaturalWidth(failureWidth);
const auto openVisible = !_failureOpen->isHidden();
const auto totalHeight = _failure->height()
+ (openVisible
? (st::defaultMarkdown.failure.skip + _failureOpen->height())
: 0);
const auto top = std::max((height() - totalHeight) / 2, 0);
_failure->moveToLeft(
(availableWidth - failureWidth) / 2,
top,
availableWidth);
if (openVisible) {
_failureOpen->moveToLeft(
(availableWidth - _failureOpen->width()) / 2,
top + _failure->height() + st::defaultMarkdown.failure.skip,
availableWidth);
}
}
void MarkdownPreviewRoot::logPreparationSummary(
const PrepareFailureStatus &failure,
const PrepareDebugStats &debug,
int prepareMs,
int layoutMs) const {
#ifndef NDEBUG
const auto counters = _renderer ? _renderer->debugCounters() : FormulaDebugCounters();
const auto reason = PrepareFailureReasonText(failure);
DEBUG_LOG((
failure.failed()
? "Native Markdown IV: unexpected preview prepare failure (%1 ms prepare, %2 ms layout, %3 ms formulas, cache hits=%4 misses=%5 bytes=%6, terminal=%7): %8"
: "Native Markdown IV: preview prepare success (%1 ms prepare, %2 ms layout, %3 ms formulas, cache hits=%4 misses=%5 bytes=%6, terminal=%7): %8"
).arg(prepareMs
).arg(layoutMs
).arg(debug.formulaMeasureMs
).arg(counters.hits
).arg(counters.misses
).arg(qlonglong(counters.cacheBytes)
).arg(reason
).arg(_options.sourcePath));
#else
Q_UNUSED(failure);
Q_UNUSED(debug);
Q_UNUSED(prepareMs);
Q_UNUSED(layoutMs);
#endif
}
std::unique_ptr<Ui::RpWidget> CreateMarkdownPreviewWidget(
QWidget *parent,
const PreparedDocument &document,
Fn<void(Event)> callback,
const OpenOptions &options) {
return std::make_unique<MarkdownPreviewRoot>(
parent,
document,
std::move(callback),
options);
}
std::unique_ptr<Ui::RpWidget> CreateMarkdownPreviewWidget(
QWidget *parent,
MarkdownArticleContent content,
std::shared_ptr<MathRenderer> renderer,
Fn<void(Event)> callback,
const OpenOptions &options) {
return std::make_unique<MarkdownPreviewRoot>(
parent,
std::move(content),
std::move(renderer),
std::move(callback),
options);
}
bool UpdateMarkdownPreviewWidget(
Ui::RpWidget *preview,
MarkdownArticleContent content,
const OpenOptions &options) {
if (const auto root = dynamic_cast<MarkdownPreviewRoot*>(preview)) {
return root->updateContent(std::move(content), options);
}
return false;
}
} // namespace Iv::Markdown