Files
AyuGramDesktop/Telegram/SourceFiles/iv/markdown/iv_markdown_view.cpp
T
2026-05-19 12:49:08 +04:00

462 lines
12 KiB
C++

#include "iv/markdown/iv_markdown_view.h"
#include "iv/markdown/iv_markdown_view_widget.h"
#include "iv/iv_delegate.h"
#include <QtCore/QElapsedTimer>
#include <QtGui/QScreen>
#include "base/weak_ptr.h"
#include "core/credits_amount.h"
#include "core/click_handler_types.h"
#include "core/file_utilities.h"
#include "lang/lang_keys.h"
#include "logs.h"
#include "ui/style/style_core_scale.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/scroll_area.h"
#include <algorithm>
#include <memory>
#include <optional>
#include <utility>
#include "styles/style_iv.h"
#include "styles/style_layers.h"
namespace Iv::Markdown {
namespace {
[[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);
}
[[nodiscard]] QVariant CurrentClickHandlerContext(const OpenOptions &options) {
return options.clickHandlerContextRef
? *options.clickHandlerContextRef
: options.clickHandlerContext;
}
} // 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);
[[nodiscard]] rpl::producer<int> scrollTopValue() const;
private:
void setup();
void prepareArticle();
void activateLink(const PreparedLink &link, Qt::MouseButton button);
void applyPreparedContent(MarkdownArticleContent prepared, int prepareMs);
void updateBodyVisibleTopBottom();
void updateChildrenGeometry(QSize size);
void updateFailureGeometry();
void logPreparationSummary(
const PrepareFailureStatus &failure,
const PrepareDebugStats &debug,
int prepareMs,
int layoutMs) const;
const OpenOptions _options;
const std::shared_ptr<const PreparedDocument> _document;
std::optional<MarkdownArticleContent> _preparedContent;
const Fn<void(Event)> _callback;
Ui::ScrollArea *_scroll = nullptr;
MarkdownDocumentWidget *_body = nullptr;
Ui::FlatLabel *_failure = nullptr;
Ui::LinkButton *_failureOpen = nullptr;
const std::shared_ptr<MathRenderer> _renderer;
QString _pendingFragment;
int _devicePixelRatio = 0;
};
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() {
_scroll = Ui::CreateChild<Ui::ScrollArea>(this, st::boxScroll);
_body = _scroll->setOwnedWidget(object_ptr<MarkdownDocumentWidget>(_scroll));
_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));
_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) {
return _options.activateMedia
? _options.activateMedia(activation, button)
: false;
});
if (_options.delegate) {
_body->setZoom(_options.delegate->ivZoom());
}
}
_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();
}, lifetime());
style::PaletteChanged() | rpl::on_next([=] {
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) {
_body->setZoom(value);
}
}, 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()));
}
void MarkdownPreviewRoot::activateLink(
const PreparedLink &link,
Qt::MouseButton button) {
if (button != Qt::LeftButton && button != Qt::MiddleButton) {
return;
}
switch (link.kind) {
case PreparedLinkKind::External:
HiddenUrlClickHandler::Open(
link.target,
CurrentClickHandlerContext(_options));
break;
case PreparedLinkKind::Anchor:
case PreparedLinkKind::Footnote:
case PreparedLinkKind::FootnoteBacklink:
if (!scrollToAnchor(link.target)) {
DEBUG_LOG(("Native Markdown IV: unresolved anchor: %1").arg(
link.target));
}
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::applyPreparedContent(
MarkdownArticleContent prepared,
int prepareMs) {
const auto failure = prepared.failure;
const auto debug = prepared.debug;
if (failure.failed()) {
_scroll->hide();
if (_body) {
_body->hide();
}
_failure->show();
if (_options.sourcePath.isEmpty()) {
_failureOpen->hide();
} else {
_failureOpen->show();
}
_failure->raise();
_failureOpen->raise();
updateFailureGeometry();
logPreparationSummary(failure, debug, prepareMs, 0);
return;
}
if (!_body) {
logPreparationSummary(failure, debug, prepareMs, 0);
return;
}
auto article = std::make_shared<MarkdownArticle>(_renderer);
article->setContent(std::move(prepared));
updateChildrenGeometry(size());
_body->setArticle(std::move(article));
if (_options.delegate) {
_body->setZoom(_options.delegate->ivZoom());
}
updateBodyVisibleTopBottom();
_scroll->show();
_body->show();
_failure->hide();
_failureOpen->hide();
if (!_pendingFragment.isEmpty()) {
const auto scrolled = scrollToAnchor(_pendingFragment);
static_cast<void>(scrolled);
_pendingFragment.clear();
}
logPreparationSummary(
failure,
debug,
prepareMs,
_body->lastRelayoutMs());
}
bool MarkdownPreviewRoot::scrollToAnchor(const QString &anchorId) {
if (!_body || !_scroll || anchorId.isEmpty()) {
return false;
}
const auto top = _body->anchorTop(anchorId);
if (top < 0) {
return false;
}
_scroll->scrollToY(top, top + 1);
return true;
}
rpl::producer<int> MarkdownPreviewRoot::scrollTopValue() const {
return _scroll
? _scroll->scrollTopValue()
: rpl::single(0);
}
bool ScrollMarkdownPreviewToAnchor(
Ui::RpWidget *preview,
const QString &anchorId) {
const auto root = dynamic_cast<MarkdownPreviewRoot*>(preview);
return root ? root->scrollToAnchor(anchorId) : false;
}
rpl::producer<int> MarkdownPreviewScrollTopValue(Ui::RpWidget *preview) {
const auto root = dynamic_cast<MarkdownPreviewRoot*>(preview);
return root ? root->scrollTopValue() : rpl::single(0);
}
void MarkdownPreviewRoot::updateBodyVisibleTopBottom() {
if (_body) {
const auto scrollTop = _scroll->scrollTop();
_body->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());
}
}
void MarkdownPreviewRoot::updateChildrenGeometry(QSize size) {
_scroll->setGeometry(QRect(QPoint(), size));
if (_body) {
_body->resizeToWidth(_scroll->width());
updateBodyVisibleTopBottom();
}
updateFailureGeometry();
}
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);
}
} // namespace Iv::Markdown