Embeds almost work now.

This commit is contained in:
John Preston
2026-05-15 23:35:19 +04:00
parent e467bc7da9
commit 2c8ba4e694
10 changed files with 786 additions and 117 deletions
+4
View File
@@ -259,6 +259,7 @@ MarkdownFailure {
MarkdownEmbedOverlay {
scrimBg: color;
bg: color;
loading: InfiniteRadialAnimation;
size: size;
margin: margins;
padding: margins;
@@ -559,6 +560,9 @@ markdownEmbedOverlayError: FlatLabel(defaultFlatLabel) {
markdownEmbedOverlay: MarkdownEmbedOverlay {
scrimBg: layerBg;
bg: windowBg;
loading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
color: windowSubTextFg;
}
size: size(384px, 694px);
margin: margins(12px, 12px, 12px, 12px);
padding: margins(8px, 8px, 8px, 8px);
+1
View File
@@ -636,6 +636,7 @@ Markdown::OpenOptions Shown::markdownOpenOptions(
}
return Webview::DataResult::Failed;
},
.ivWebviewStorageId = _session->local().resolveStorageIdOther(),
.activateMedia = [=](
const Markdown::MediaActivation &activation,
Qt::MouseButton button) {
@@ -168,7 +168,8 @@ void SetTextLeafPen(
const MarkdownArticlePaintCaches &caches) {
p.setPen(!block.supplementary
? markdown.textColor->c
: caches.supplementaryColorOverride.value_or(markdown.textColor->c));
: caches.supplementaryColorOverride.value_or(
markdown.supplementaryTextColor->c));
}
void PaintRelatedArticleTextLeaf(
@@ -18,6 +18,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <functional>
#include <memory>
#include "webview/webview_common.h"
namespace Ui {
class DynamicImage;
class Show;
@@ -183,6 +185,7 @@ struct EmbedRequest {
int width = 0;
int height = 0;
bool fullWidth = false;
bool fixedHeight = false;
bool allowScrolling = false;
[[nodiscard]] explicit operator bool() const {
@@ -220,6 +223,7 @@ struct OpenOptions {
std::function<Webview::DataResult(
QByteArray,
Webview::DataRequest)> ivWebviewDataRequest;
Webview::StorageId ivWebviewStorageId;
std::function<bool(
const MediaActivation &,
Qt::MouseButton)> activateMedia;
@@ -435,6 +435,9 @@ void Controller::updateOptions(OpenOptions options) {
if (refreshed.sourceUrl.isEmpty()) {
refreshed.sourceUrl = _options.sourceUrl;
}
if (!refreshed.ivWebviewStorageId) {
refreshed.ivWebviewStorageId = _options.ivWebviewStorageId;
}
if (refreshed.viewerKind == ViewerKind::Auto) {
refreshed.viewerKind = _options.viewerKind;
}
@@ -13,7 +13,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/style/style_core_direction.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/shadow.h"
#include "ui/wrap/padding_wrap.h"
#include "webview/webview_data_stream_memory.h"
#include "webview/webview_embed.h"
@@ -22,15 +21,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_layers.h"
#include "styles/style_iv.h"
#include <QtCore/QEvent>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
#include <QtGui/QKeyEvent>
#include <QtGui/QMouseEvent>
#include <QtGui/QPainter>
#include <QtGui/QWindow>
#include <QtWidgets/QApplication>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
namespace Iv::Markdown {
@@ -75,16 +79,121 @@ namespace {
}
[[nodiscard]] int ParsePositiveInt(const QJsonValue &value) {
if (!value.isDouble()) {
auto parsed = 0.;
if (value.isDouble()) {
parsed = value.toDouble();
} else if (value.isString()) {
auto text = value.toString().trimmed();
if (text.endsWith(u"px"_q)) {
text.chop(2);
}
auto ok = false;
parsed = text.toDouble(&ok);
if (!ok) {
return 0;
}
} else {
return 0;
}
const auto parsed = value.toDouble();
if (parsed <= 0.
|| parsed > static_cast<double>(std::numeric_limits<int>::max())) {
return 0;
}
const auto result = static_cast<int>(parsed);
return (parsed == result) ? result : 0;
return static_cast<int>(std::round(parsed));
}
[[nodiscard]] int ResizeFrameHeight(const QJsonObject &object) {
if (object.value("eventType").toString() != u"resize_frame"_q) {
return 0;
}
return ParsePositiveInt(
object.value("eventData").toObject().value("height"));
}
[[nodiscard]] int ResizeFrameHeight(const QJsonDocument &message) {
if (message.isArray()) {
const auto array = message.array();
if (array.size() < 2 || array[0].toString() != u"resize_frame"_q) {
return 0;
}
return ParsePositiveInt(array[1].toObject().value("height"));
}
return message.isObject() ? ResizeFrameHeight(message.object()) : 0;
}
[[nodiscard]] QJsonDocument NavigationReadyMessage(
QByteArray resourceId,
QString token,
QString url) {
auto object = QJsonObject{
{ u"event"_q, u"navigation_ready"_q },
};
if (!resourceId.isEmpty()) {
object.insert(u"resourceId"_q, QString::fromUtf8(resourceId));
}
if (!token.isEmpty()) {
object.insert(u"token"_q, token);
}
if (!url.isEmpty()) {
object.insert(u"url"_q, url);
}
return QJsonDocument(object);
}
[[nodiscard]] QByteArray EmbedInitScript() {
return QByteArray(
"(function(){"
"window.TelegramWebviewProxy={"
"postEvent:function(eventType,eventData){"
"if(window.external&&typeof window.external.invoke==='function'){"
"try{"
"window.external.invoke(JSON.stringify([eventType,eventData]));"
"}catch(e){}"
"}"
"}"
"};"
"if(window!==window.top){"
"return;"
"}"
"function resourceId(){"
"var path='';"
"try{path=String(window.location.pathname||'');}catch(e){}"
"while(path.charAt(0)==='/'){path=path.slice(1);}"
"return path;"
"}"
"function token(){"
"var hash='';"
"try{hash=String(window.location.hash||'');}catch(e){}"
"while(hash.charAt(0)==='#'){hash=hash.slice(1);}"
"return hash;"
"}"
"function url(){"
"try{return String(window.location.href||'');}catch(e){return '';}"
"}"
"function report(){"
"if(!window.external||typeof window.external.invoke!=='function'){"
"return;"
"}"
"try{"
"window.external.invoke(JSON.stringify({"
"event:'navigation_ready',"
"resourceId:resourceId(),"
"token:token(),"
"url:url()"
"}));"
"}catch(e){"
"}"
"}"
"if(document.readyState==='complete'){"
"report();"
"}else{"
"var handler=function(){"
"window.removeEventListener('load',handler,false);"
"report();"
"};"
"window.addEventListener('load',handler,false);"
"}"
"})();");
}
} // namespace
@@ -93,23 +202,50 @@ EmbedOverlay::EmbedOverlay(
QWidget *parent,
const base::flat_map<QByteArray, QByteArray> *resources,
std::function<void(QString)> linkActivationCallback,
Webview::StorageId storageId,
std::function<Webview::DataResult(QByteArray, Webview::DataRequest)>
dataRequestHandler)
: Ui::RpWidget(parent)
, _webviewParent(parent)
, _resources(resources)
, _linkActivationCallback(std::move(linkActivationCallback))
, _dataRequestHandler(std::move(dataRequestHandler)) {
, _storageId(std::move(storageId))
, _dataRequestHandler(std::move(dataRequestHandler))
, _loadingAnimation(
[=] {
if (!anim::Disabled()) {
update(_contentGeometry);
if (_content) {
_content->update();
}
}
},
st::markdownEmbedOverlay.loading) {
setObjectName(u"nativeIvEmbedOverlay"_q);
setAttribute(Qt::WA_OpaquePaintEvent);
setMouseTracking(true);
hide();
QWidget::hide();
_content = Ui::CreateChild<Ui::RpWidget>(this);
_content->setObjectName(u"nativeIvEmbedOverlayShell"_q);
_content->paintRequest(
) | rpl::on_next([=] {
if (!_loading || _contentGeometry.isEmpty()) {
return;
}
auto p = QPainter(_content);
const auto size = st::markdownEmbedOverlay.loading.size;
const auto loader = style::centerrect(
bodyRectInContent(),
QRect(QPoint(), size));
_loadingAnimation.draw(p, loader.topLeft(), size, _content->width());
}, _content->lifetime());
_content->show();
}
EmbedOverlay::~EmbedOverlay() = default;
EmbedOverlay::~EmbedOverlay() {
destroyWebview();
removeEscapeFilter();
}
bool EmbedOverlay::showEmbed(const EmbedRequest &request) {
if (!request) {
@@ -118,12 +254,22 @@ bool EmbedOverlay::showEmbed(const EmbedRequest &request) {
if (isHidden()) {
_focusRestore = QApplication::focusWidget();
}
destroyWebview();
_request = request;
_preferredBodySize = QSize();
_pendingPreferredBodySize = QSize();
_readyNavigationToken = QString();
_cssToQtScale = 1.;
_readyFromResource = false;
_ready = false;
_loading = true;
clearWebviewError();
QWidget::show();
raise();
installEscapeFilter();
raiseSurfaces();
updateContentGeometry();
_loadingAnimation.start();
_content->update();
if (!_webview) {
const auto available = Webview::Availability();
if (available.error != Webview::Available::Error::None) {
@@ -133,10 +279,18 @@ bool EmbedOverlay::showEmbed(const EmbedRequest &request) {
}
ensureWebview();
if (_webview && _webview->widget()) {
_webview->widget()->show();
if (_resources
&& (_resources->find(request.resourceId) != _resources->end())) {
_webview->navigateToData(QString::fromUtf8(request.resourceId));
const auto hasResource = _resources
&& (_resources->find(request.resourceId) != _resources->end());
_readyFromResource = hasResource;
_readyNavigationToken = hasResource
? QString::number(++_navigationGeneration)
: QString();
updateWebviewGeometry();
if (hasResource) {
_webview->navigateToData(
QString::fromUtf8(request.resourceId)
+ u"#"_q
+ _readyNavigationToken);
} else if (!request.fallbackUrl.isEmpty()) {
_webview->navigate(request.fallbackUrl);
} else {
@@ -145,26 +299,41 @@ bool EmbedOverlay::showEmbed(const EmbedRequest &request) {
if (_error && !_error->isHidden()) {
_error->raise();
}
_webview->focus();
} else {
showWebviewError();
}
return true;
}
void EmbedOverlay::hide() {
void EmbedOverlay::closeEmbed() {
if (isHidden()) {
return;
}
_loading = false;
_loadingAnimation.stop(anim::type::instant);
if (_content) {
_content->update();
}
destroyWebview();
removeEscapeFilter();
QWidget::hide();
clearWebviewError();
_request = EmbedRequest();
_preferredBodySize = QSize();
_pendingPreferredBodySize = QSize();
_readyNavigationToken = QString();
_cssToQtScale = 1.;
_readyFromResource = false;
_ready = false;
restoreFocus();
}
void EmbedOverlay::updateGeometry(QRect geometry) {
void EmbedOverlay::updateGeometry(QRect geometry, int contentWidth) {
setGeometry(geometry);
_contentWidth = contentWidth;
updateContentGeometry();
if (!isHidden()) {
raise();
raiseSurfaces();
}
}
@@ -172,25 +341,63 @@ void EmbedOverlay::testHandleWebviewMessage(const QJsonDocument &message) {
handleWebviewMessage(message);
}
void EmbedOverlay::testHandleNavigationDone(bool success) {
if (success) {
handleWebviewMessage(NavigationReadyMessage(
_readyFromResource ? _request.resourceId : QByteArray(),
_readyFromResource ? _readyNavigationToken : QString(),
_readyFromResource ? QString() : _request.fallbackUrl));
return;
}
handleNavigationDone(success);
}
bool EmbedOverlay::testLoadingCoverVisible() const {
return _loading;
}
const Webview::StorageId &EmbedOverlay::testEffectiveStorageId() const {
return _storageId;
}
bool EmbedOverlay::eventFilter(QObject *object, QEvent *event) {
const auto type = event->type();
if (isHidden()
|| (type != QEvent::ShortcutOverride && type != QEvent::KeyPress)
|| !eventFromOverlayWindow(object)) {
return QObject::eventFilter(object, event);
}
const auto keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent->key() != Qt::Key_Escape) {
return QObject::eventFilter(object, event);
}
keyEvent->accept();
if (type == QEvent::KeyPress) {
closeEmbed();
}
return true;
}
void EmbedOverlay::paintEvent(QPaintEvent *e) {
Q_UNUSED(e);
auto p = QPainter(this);
p.fillRect(e->rect(), st::markdownEmbedOverlay.scrimBg);
const auto full = rect();
p.fillRect(full, st::markdownEmbedOverlay.scrimBg);
if (_contentGeometry.isEmpty()) {
return;
}
Ui::Shadow::paint(
p,
_contentGeometry,
width(),
st::boxRoundShadow,
Ui::CachedCornersMasks(Ui::CachedCornerRadius::Small));
Ui::FillRoundRect(
p,
_contentGeometry,
st::markdownEmbedOverlay.bg,
Ui::PrepareCornerPixmaps(
st::roundRadiusSmall,
st::markdownEmbedOverlay.bg));
if (_request.fullWidth) {
p.fillRect(_contentGeometry, st::markdownEmbedOverlay.bg);
} else {
Ui::FillRoundRect(
p,
_contentGeometry,
st::markdownEmbedOverlay.bg,
Ui::PrepareCornerPixmaps(
st::roundRadiusSmall,
st::markdownEmbedOverlay.bg));
}
}
void EmbedOverlay::mousePressEvent(QMouseEvent *e) {
@@ -201,36 +408,76 @@ void EmbedOverlay::mousePressEvent(QMouseEvent *e) {
void EmbedOverlay::mouseReleaseEvent(QMouseEvent *e) {
const auto outside = !_contentGeometry.contains(e->pos());
if (_pressedOutside && outside && e->button() == Qt::LeftButton) {
hide();
closeEmbed();
}
_pressedOutside = false;
e->accept();
}
void EmbedOverlay::installEscapeFilter() {
if (!_escapeFilterInstalled) {
qApp->installEventFilter(this);
_escapeFilterInstalled = true;
}
}
void EmbedOverlay::removeEscapeFilter() {
if (_escapeFilterInstalled) {
qApp->removeEventFilter(this);
_escapeFilterInstalled = false;
}
}
bool EmbedOverlay::eventFromOverlayWindow(QObject *object) const {
const auto overlayWindow = window();
const auto activeWindow = QApplication::activeWindow();
if (!overlayWindow) {
return true;
} else if (const auto widget = qobject_cast<QWidget*>(object)) {
return widget->window() == overlayWindow;
} else if (const auto qwindow = qobject_cast<QWindow*>(object)) {
return (qwindow == overlayWindow->windowHandle())
|| !activeWindow
|| (activeWindow == overlayWindow);
}
return !activeWindow || (activeWindow == overlayWindow);
}
Webview::WindowConfig EmbedOverlay::makeWindowConfig() const {
return {
.opaqueBg = st::markdownEmbedOverlay.bg->c,
.storageId = _storageId,
.safe = true,
};
}
void EmbedOverlay::ensureWebview() {
if (_webview && _webview->widget()) {
updateContentGeometry();
return;
}
const auto generation = ++_webviewGeneration;
_webview = std::make_unique<Webview::Window>(
_content,
Webview::WindowConfig{
.opaqueBg = st::markdownEmbedOverlay.bg->c,
.safe = true,
});
_webviewParent ? _webviewParent.data() : this,
makeWindowConfig());
const auto raw = _webview.get();
const auto widget = raw->widget();
if (!widget) {
_webview = nullptr;
showWebviewError();
return;
}
widget->show();
widget->hide();
QObject::connect(widget, &QObject::destroyed, this, [=] {
if (!_webview || _webview.get() != raw) {
if (_webviewGeneration != generation
|| !_webview
|| _webview.get() != raw) {
return;
}
crl::on_main(this, [=] {
if (_webview && _webview.get() == raw) {
if (_webviewGeneration == generation
&& _webview
&& _webview.get() == raw) {
_webview = nullptr;
showWebviewError({ u"Error: WebView has crashed."_q });
}
@@ -239,6 +486,7 @@ void EmbedOverlay::ensureWebview() {
raw->setDataRequestHandler([=](Webview::DataRequest request) {
return handleDataRequest(std::move(request));
});
raw->init(EmbedInitScript());
raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) {
Q_UNUSED(newWindow);
@@ -251,9 +499,20 @@ void EmbedOverlay::ensureWebview() {
}
return false;
});
raw->setNavigationDoneHandler([=](bool success) {
crl::on_main(this, [=] {
if (_webviewGeneration == generation
&& _webview
&& (_webview.get() == raw)) {
handleNavigationDone(success);
}
});
});
raw->setMessageHandler([=](const QJsonDocument &message) {
crl::on_main(this, [=] {
if (_webview && (_webview.get() == raw)) {
if (_webviewGeneration == generation
&& _webview
&& (_webview.get() == raw)) {
handleWebviewMessage(message);
}
});
@@ -265,8 +524,33 @@ void EmbedOverlay::handleWebviewMessage(const QJsonDocument &message) {
if (!_request) {
return;
}
if (const auto height = ResizeFrameHeight(message)) {
applyPreferredBodyHeight(cssPixelsToQt(height));
return;
}
const auto object = message.object();
if (object.value("event").toString() != u"preferred_size"_q) {
const auto event = object.value("event").toString();
if (event == u"navigation_ready"_q) {
if (!_webview || !_webview->widget()) {
showWebviewError();
return;
}
if (_readyFromResource) {
if (object.value("token").toString() != _readyNavigationToken) {
return;
}
if (normalizedRequestId(
object.value("resourceId").toString().toStdString())
!= _request.resourceId) {
return;
}
} else if (object.value("url").toString().isEmpty()) {
return;
}
setReady();
return;
}
if (event != u"preferred_size"_q) {
return;
}
const auto width = ParsePositiveInt(object.value("width"));
@@ -274,6 +558,14 @@ void EmbedOverlay::handleWebviewMessage(const QJsonDocument &message) {
if (!width || !height) {
return;
}
auto viewportWidth = ParsePositiveInt(object.value("viewportWidth"));
if (!viewportWidth) {
viewportWidth = ParsePositiveInt(object.value("viewport_width"));
}
if (!viewportWidth) {
viewportWidth = ParsePositiveInt(object.value("innerWidth"));
}
updateCssToQtScale(viewportWidth);
auto resourceId = object.value("resourceId").toString();
if (resourceId.isEmpty()) {
resourceId = object.value("resource_id").toString();
@@ -282,70 +574,118 @@ void EmbedOverlay::handleWebviewMessage(const QJsonDocument &message) {
&& normalizedRequestId(resourceId.toStdString()) != _request.resourceId) {
return;
}
const auto size = QSize(width, height);
if (_preferredBodySize == size) {
applyPreferredBodySize(QSize(cssPixelsToQt(width), cssPixelsToQt(height)));
}
void EmbedOverlay::handleNavigationDone(bool success) {
if (success || !_request || _ready || isHidden()) {
return;
}
applyPreferredBodySize(size);
showWebviewError();
}
void EmbedOverlay::setReady() {
if (_ready || !_webview || !_webview->widget()) {
return;
}
_ready = true;
if (_pendingPreferredBodySize.isValid()
&& (_pendingPreferredBodySize.width() > 0
|| _pendingPreferredBodySize.height() > 0)) {
_preferredBodySize = _pendingPreferredBodySize;
_pendingPreferredBodySize = QSize();
updateContentGeometry();
} else {
updateWebviewGeometry();
}
_loading = false;
_loadingAnimation.stop(anim::type::normal);
_content->update();
clearWebviewError();
_webview->widget()->show();
_webview->widget()->raise();
_webview->focus();
update();
_content->update();
}
void EmbedOverlay::applyPreferredBodySize(QSize size) {
if (!size.isValid()) {
return;
}
if (!_ready) {
_pendingPreferredBodySize = size;
return;
}
if (_preferredBodySize == size) {
return;
}
_preferredBodySize = size;
updateContentGeometry();
}
void EmbedOverlay::applyPreferredBodyHeight(int height) {
if (height <= 0) {
return;
}
applyPreferredBodySize(QSize(_preferredBodySize.width(), height));
}
void EmbedOverlay::updateCssToQtScale(int viewportWidth) {
const auto body = bodyGeometry();
if (viewportWidth <= 0 || body.width() <= 0) {
return;
}
_cssToQtScale = std::clamp(body.width() / double(viewportWidth), 0.25, 4.);
}
void EmbedOverlay::updateContentGeometry() {
if (!_content) {
return;
}
const auto available = rect().marginsRemoved(
st::markdownEmbedOverlay.margin + st::boxRoundShadow.extend);
const auto margin = st::markdownEmbedOverlay.margin;
const auto available = _request.fullWidth
? rect().marginsRemoved({ 0, margin.top(), 0, margin.bottom() })
: rect().marginsRemoved(margin);
if (available.isEmpty()) {
_contentGeometry = QRect();
_content->setGeometry(_contentGeometry);
if (_webview && _webview->widget()) {
_webview->widget()->setGeometry(QRect());
}
updateWebviewGeometry();
if (_error) {
_error->setGeometry(QRect());
}
update();
return;
}
const auto padding = st::markdownEmbedOverlay.padding;
const auto padding = contentPadding();
const auto horizontalPadding = padding.left() + padding.right();
const auto verticalPadding = padding.top() + padding.bottom();
const auto availableWidth = std::max(available.width(), 1);
const auto availableHeight = std::max(available.height(), 1);
const auto availableBodyWidth = std::max(
availableWidth - horizontalPadding,
1);
const auto availableBodyHeight = std::max(
availableHeight - verticalPadding,
1);
const auto desiredBodyWidth = [&] {
if (_preferredBodySize.width() > 0) {
return _preferredBodySize.width();
}
if (_request.width > 0) {
return _request.width;
}
return st::markdownEmbedOverlay.size.width();
}();
const auto width = _request.fullWidth
? availableWidth
: std::min(
std::clamp(desiredBodyWidth, 1, availableBodyWidth)
+ horizontalPadding,
: std::clamp(
(_contentWidth > 0)
? _contentWidth
: st::markdownEmbedOverlay.size.width(),
1,
availableWidth);
const auto bodyWidth = std::max(width - horizontalPadding, 1);
const auto desiredBodyHeight = [&] {
if (_preferredBodySize.height() > 0) {
return _preferredBodySize.height();
}
if (_request.height > 0) {
if (_request.width > 0) {
return std::max(
static_cast<int>(std::round(
bodyWidth
* (double(_request.height) / _request.width))),
1);
}
return _request.height;
}
return st::markdownEmbedOverlay.size.height();
@@ -358,12 +698,10 @@ void EmbedOverlay::updateContentGeometry() {
available,
QRect(0, 0, width, height));
_content->setGeometry(_contentGeometry);
const auto body = bodyRect();
if (_webview && _webview->widget()) {
_webview->widget()->setGeometry(body);
}
updateWebviewGeometry();
if (_error && _errorLabel) {
_errorLabel->setContextCopyText(_request.fallbackUrl);
const auto body = bodyRectInContent();
_error->resizeToWidth(std::max(body.width(), 1));
_error->moveToLeft(
body.x(),
@@ -372,6 +710,39 @@ void EmbedOverlay::updateContentGeometry() {
_error->raise();
}
update();
_content->update();
}
void EmbedOverlay::updateWebviewGeometry() {
if (_webview && _webview->widget()) {
_webview->widget()->setGeometry(bodyGeometry());
if (!_webview->widget()->isHidden()) {
_webview->widget()->raise();
}
}
}
void EmbedOverlay::raiseSurfaces() {
raise();
if (_webview && _webview->widget() && !_webview->widget()->isHidden()) {
_webview->widget()->raise();
}
}
void EmbedOverlay::hideWebview() {
if (_webview && _webview->widget()) {
_webview->widget()->hide();
}
}
void EmbedOverlay::destroyWebview() {
++_webviewGeneration;
if (_webview) {
if (const auto widget = _webview->widget()) {
widget->hide();
}
_webview = nullptr;
}
}
void EmbedOverlay::showWebviewError() {
@@ -382,9 +753,11 @@ void EmbedOverlay::showWebviewError() {
}
void EmbedOverlay::showWebviewError(const TextWithEntities &text) {
if (_webview && _webview->widget()) {
_webview->widget()->hide();
}
_ready = false;
_loading = false;
_loadingAnimation.stop(anim::type::normal);
_content->update();
hideWebview();
if (!_error) {
_error = Ui::CreateChild<Ui::PaddingWrap<Ui::FlatLabel>>(
_content,
@@ -425,13 +798,27 @@ void EmbedOverlay::restoreFocus() {
_focusRestore = nullptr;
}
QRect EmbedOverlay::bodyRect() const {
const auto padding = st::markdownEmbedOverlay.padding;
QRect EmbedOverlay::bodyGeometry() const {
return _contentGeometry.isEmpty()
? QRect()
: _contentGeometry.marginsRemoved(contentPadding());
}
QRect EmbedOverlay::bodyRectInContent() const {
const auto body = bodyGeometry();
return QRect(
padding.left(),
padding.top(),
std::max(_contentGeometry.width() - padding.left() - padding.right(), 1),
std::max(_contentGeometry.height() - padding.top() - padding.bottom(), 1));
body.topLeft() - _contentGeometry.topLeft(),
body.size());
}
QMargins EmbedOverlay::contentPadding() const {
return _request.fullWidth ? QMargins() : st::markdownEmbedOverlay.padding;
}
int EmbedOverlay::cssPixelsToQt(int value) const {
return (value > 0)
? std::max(static_cast<int>(std::round(value * _cssToQtScale)), 1)
: 0;
}
QByteArray EmbedOverlay::normalizedRequestId(const std::string &id) const {
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/flat_map.h"
#include "iv/markdown/iv_markdown_common.h"
#include "ui/effects/radial_animation.h"
#include "ui/rp_widget.h"
#include <functional>
@@ -19,8 +20,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtCore/QRect>
#include <QtCore/QSize>
class QEvent;
class QJsonDocument;
class QMouseEvent;
class QObject;
class QPaintEvent;
class QWidget;
struct TextWithEntities;
@@ -33,6 +36,7 @@ class RpWidget;
} // namespace Ui
namespace Webview {
struct WindowConfig;
class Window;
} // namespace Webview
@@ -44,47 +48,80 @@ public:
QWidget *parent,
const base::flat_map<QByteArray, QByteArray> *resources,
std::function<void(QString)> linkActivationCallback,
Webview::StorageId storageId,
std::function<Webview::DataResult(QByteArray, Webview::DataRequest)>
dataRequestHandler);
~EmbedOverlay();
[[nodiscard]] bool showEmbed(const EmbedRequest &request);
void hide();
void updateGeometry(QRect geometry);
void closeEmbed();
void updateGeometry(QRect geometry, int contentWidth);
void testHandleWebviewMessage(const QJsonDocument &message);
void testHandleNavigationDone(bool success);
[[nodiscard]] bool testLoadingCoverVisible() const;
[[nodiscard]] const Webview::StorageId &testEffectiveStorageId() const;
protected:
bool eventFilter(QObject *object, QEvent *event) override;
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
private:
void installEscapeFilter();
void removeEscapeFilter();
[[nodiscard]] bool eventFromOverlayWindow(QObject *object) const;
void ensureWebview();
[[nodiscard]] Webview::WindowConfig makeWindowConfig() const;
void handleWebviewMessage(const QJsonDocument &message);
void handleNavigationDone(bool success);
void setReady();
void applyPreferredBodySize(QSize size);
void applyPreferredBodyHeight(int height);
void updateCssToQtScale(int viewportWidth);
void updateContentGeometry();
void updateWebviewGeometry();
void raiseSurfaces();
void hideWebview();
void destroyWebview();
void showWebviewError();
void showWebviewError(const TextWithEntities &text);
void clearWebviewError();
void restoreFocus();
[[nodiscard]] QRect bodyRect() const;
[[nodiscard]] QRect bodyGeometry() const;
[[nodiscard]] QRect bodyRectInContent() const;
[[nodiscard]] QMargins contentPadding() const;
[[nodiscard]] int cssPixelsToQt(int value) const;
[[nodiscard]] QByteArray normalizedRequestId(const std::string &id) const;
[[nodiscard]] Webview::DataResult handleDataRequest(
Webview::DataRequest request);
const QPointer<QWidget> _webviewParent;
const base::flat_map<QByteArray, QByteArray> *const _resources = nullptr;
const std::function<void(QString)> _linkActivationCallback;
const Webview::StorageId _storageId;
const std::function<Webview::DataResult(QByteArray, Webview::DataRequest)>
_dataRequestHandler;
Ui::RpWidget *_content = nullptr;
Ui::PaddingWrap<Ui::FlatLabel> *_error = nullptr;
Ui::FlatLabel *_errorLabel = nullptr;
std::unique_ptr<Webview::Window> _webview;
Ui::InfiniteRadialAnimation _loadingAnimation;
EmbedRequest _request;
QRect _contentGeometry;
QSize _preferredBodySize;
QSize _pendingPreferredBodySize;
QString _readyNavigationToken;
QPointer<QWidget> _focusRestore;
int _contentWidth = 0;
int _navigationGeneration = 0;
int _webviewGeneration = 0;
double _cssToQtScale = 1.;
bool _pressedOutside = false;
bool _readyFromResource = false;
bool _ready = false;
bool _loading = false;
bool _escapeFilterInstalled = false;
};
@@ -262,6 +262,17 @@ using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
return QByteArray(
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
"<script>"
"window.TelegramWebviewProxy={"
"postEvent:function(eventType,eventData){"
"if(window.external&&typeof window.external.invoke==='function'){"
"try{"
"window.external.invoke(JSON.stringify([eventType,eventData]));"
"}catch(e){}"
"}"
"}"
"};"
"</script>"
"<style>"
"html,body{margin:0;padding:0;background:#fff;color:#000;}"
"body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;}"
@@ -285,6 +296,46 @@ using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
"if(value<1){return 0;}"
"return Math.min(value,max||100000);"
"}"
"function resourceId(){"
"var path='';"
"try{path=String(window.location.pathname||'');}catch(e){}"
"while(path.charAt(0)==='/'){path=path.slice(1);}"
"return path;"
"}"
"function documentHeight(doc){"
"if(!doc){return 0;}"
"var body=doc.body;"
"var root=doc.documentElement;"
"var height=0;"
"if(body){"
"var bodyRect=body.getBoundingClientRect();"
"height=Math.max("
"height,"
"body.scrollHeight,"
"body.offsetHeight,"
"body.clientHeight,"
"bodyRect.height);"
"}"
"if(root){"
"var rootRect=root.getBoundingClientRect();"
"height=Math.max("
"height,"
"root.scrollHeight,"
"root.offsetHeight,"
"root.clientHeight,"
"rootRect.height);"
"}"
"return clamp(height,100000);"
"}"
"function iframeDocumentHeight(iframe){"
"try{"
"return documentHeight("
"iframe.contentDocument"
"||(iframe.contentWindow&&iframe.contentWindow.document));"
"}catch(e){"
"return 0;"
"}"
"}"
"function findWrap(iframe){"
"var node=iframe;"
"while(node&&node!==document.body){"
@@ -304,6 +355,15 @@ using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
"}"
"}"
"function seedIframeHeight(iframe){"
"if(iframe.getAttribute('data-native-iv-resized')==='1'){"
"return;"
"}"
"var measured=iframeDocumentHeight(iframe);"
"if(measured){"
"iframe.setAttribute('data-native-iv-measured','1');"
"iframe.style.height=measured+'px';"
"return;"
"}"
"var wrap=findWrap(iframe);"
"if(!wrap){return;}"
"var fixed=clamp(wrap.getAttribute('data-height'),100000);"
@@ -311,9 +371,6 @@ using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
"iframe.style.height=fixed+'px';"
"return;"
"}"
"if(iframe.getAttribute('data-native-iv-resized')==='1'){"
"return;"
"}"
"var ratio=Number(wrap.getAttribute('data-aspect-ratio'));"
"if(!isFinite(ratio)||ratio<=0){"
"return;"
@@ -352,6 +409,11 @@ using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
"function measurePreferredSize(){"
"var width=0;"
"var height=0;"
"var viewportWidth=clamp("
"Math.max("
"window.innerWidth||0,"
"document.documentElement?document.documentElement.clientWidth:0),"
"100000);"
"var body=document.body;"
"if(body){"
"var bodyRect=body.getBoundingClientRect();"
@@ -379,7 +441,8 @@ using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
"}"
"return{"
"width:clamp(width,100000),"
"height:clamp(height,100000)"
"height:clamp(height,100000),"
"viewportWidth:viewportWidth"
"};"
"}"
"function reportPreferredSize(){"
@@ -396,8 +459,11 @@ using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
"try{"
"window.external.invoke(JSON.stringify({"
"event:'preferred_size',"
"resourceId:resourceId(),"
"width:size.width,"
"height:size.height"
"height:size.height,"
"viewportWidth:size.viewportWidth,"
"devicePixelRatio:window.devicePixelRatio||1"
"}));"
"}catch(e){"
"}"
@@ -464,9 +530,11 @@ using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
"if(document.body){"
"observer.observe(document.body);"
"}"
"}else{"
"setInterval(scheduleMeasure,250);"
"}"
"setInterval(function(){"
"syncIframes();"
"scheduleMeasure();"
"},250);"
"syncIframes();"
"scheduleMeasure();"
"})();"
@@ -599,19 +667,31 @@ using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
auto aspectRatio = QByteArray();
auto iframeAttributes = NativeIvHtmlAttributes();
const auto autosize = !data.vw();
const auto fullWidth = data.is_full_width() || (data.vw() && !data.vw()->v);
if (autosize) {
iframeWidth = "100%";
} else if (data.is_full_width() || !data.vw()->v) {
} else if (fullWidth) {
width = "100%";
height = QByteArray::number(data.vh()->v) + "px";
if (data.vh()) {
if (data.vw()->v) {
aspectRatio = QByteArray::number(
double(data.vh()->v) / std::max(data.vw()->v, 1),
'g',
16);
} else {
height = QByteArray::number(data.vh()->v) + "px";
}
}
iframeWidth = "100%";
iframeHeight = height;
} else {
width = QByteArray::number(data.vw()->v) + "px";
aspectRatio = QByteArray::number(
double(data.vh()->v) / std::max(data.vw()->v, 1),
'g',
16);
if (data.vh()) {
aspectRatio = QByteArray::number(
double(data.vh()->v) / std::max(data.vw()->v, 1),
'g',
16);
}
}
if (!iframeWidth.isEmpty()) {
iframeAttributes.push_back({ "width", iframeWidth });
@@ -675,7 +755,7 @@ using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
content += NativeIvCaptionHtml(data.vcaption(), state);
}
auto figureAttributes = NativeIvHtmlAttributes();
if (!data.is_full_width()) {
if (!fullWidth) {
figureAttributes.push_back({ "class", "nowide" });
}
return NativeIvHtmlTag("figure", figureAttributes, content);
@@ -702,7 +782,8 @@ using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
: qs(*data.vurl()),
.width = data.vw() ? data.vw()->v : 0,
.height = data.vh() ? data.vh()->v : 0,
.fullWidth = data.is_full_width(),
.fullWidth = data.is_full_width() || (data.vw() && !data.vw()->v),
.fixedHeight = (data.vh() != nullptr),
.allowScrolling = data.is_allow_scrolling(),
};
return PrepareNativeIvPlaceholderBlock(
@@ -260,6 +260,7 @@ void MarkdownPreviewRoot::setup() {
[=](QString url) {
openEmbedLink(std::move(url));
},
_options.ivWebviewStorageId,
_options.ivWebviewDataRequest);
_embedOverlay->hide();
@@ -434,7 +435,7 @@ void MarkdownPreviewRoot::openEmbedLink(QString url) {
return;
}
if (_embedOverlay) {
_embedOverlay->hide();
_embedOverlay->closeEmbed();
}
HiddenUrlClickHandler::Open(url, CurrentClickHandlerContext(_options));
}
@@ -509,7 +510,7 @@ void MarkdownPreviewRoot::applyPreparedContent(
const auto failure = prepared.failure;
const auto debug = prepared.debug;
if (_embedOverlay) {
_embedOverlay->hide();
_embedOverlay->closeEmbed();
}
if (failure.failed()) {
_article = nullptr;
@@ -626,10 +627,9 @@ void MarkdownPreviewRoot::updateBodyVisibleTopBottom() {
void MarkdownPreviewRoot::updateChildrenGeometry(QSize size) {
_scroll->setGeometry(QRect(QPoint(), size));
auto bodyWidth = std::max(_scroll->width(), 1);
if (_body) {
const auto bodyWidth = std::min(
std::max(_scroll->width(), 1),
_body->maxWidth());
bodyWidth = std::min(bodyWidth, _body->maxWidth());
_body->resizeToWidth(bodyWidth);
_body->moveToLeft(
std::max((_scroll->width() - bodyWidth) / 2, 0),
@@ -638,7 +638,7 @@ void MarkdownPreviewRoot::updateChildrenGeometry(QSize size) {
updateBodyVisibleTopBottom();
}
if (_embedOverlay) {
_embedOverlay->updateGeometry(QRect(QPoint(), size));
_embedOverlay->updateGeometry(QRect(QPoint(), size), bodyWidth);
}
updateFailureGeometry();
}
+163 -12
View File
@@ -2455,7 +2455,7 @@ template <typename Object>
[[nodiscard]] QRect NativeIvOverlayAvailableRect(const EmbedOverlay *overlay) {
return overlay
? overlay->rect().marginsRemoved(
st::markdownEmbedOverlay.margin + st::boxRoundShadow.extend)
st::markdownEmbedOverlay.margin)
: QRect();
}
@@ -5489,8 +5489,19 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
}
const auto previewEmbedLabel = u"native-iv-embed-preview-overlay"_q;
const auto previewEmbedBlock = MTP_pageBlockEmbed(
MTP_flags(
MTPDpageBlockEmbed::Flag::f_allow_scrolling
| MTPDpageBlockEmbed::Flag::f_url
| MTPDpageBlockEmbed::Flag::f_w),
MTP_string("https://example.com/embed"),
MTP_string(),
MTP_long(0),
MTP_int(640),
MTP_int(0),
NativeIvCaption(u"Embed caption"_q));
auto previewEmbedSource = NativeIvSource(QVector<MTPPageBlock>{
placeholderFixtures[0].block,
previewEmbedBlock,
});
const auto previewEmbedPrepared = TryPrepareNativeInstantView({
.source = &previewEmbedSource,
@@ -5509,12 +5520,19 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
window.setGeometry(QRect(0, 0, 420, 320));
window.show();
FlushPendingWidgetEvents();
const auto expectedStorageId = Webview::StorageId{
u"native-iv-preview-overlay"_q,
QByteArray("phase-3"),
};
auto previewOptions = OpenOptions();
previewOptions.ivWebviewStorageId = expectedStorageId;
auto preview = CreateMarkdownPreviewWidget(
window.body(),
std::move(previewEmbedPrepared.content),
std::make_shared<MathRenderer>(),
[](Event) {
});
},
previewOptions);
preview->setGeometry(QRect(QPoint(), window.body()->size()));
preview->show();
FlushPendingWidgetEvents();
@@ -5540,9 +5558,17 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
== nullptr,
previewEmbedLabel + u" overlay close removed"_q,
ok);
if (overlay) {
Check(
overlay->testEffectiveStorageId().path == expectedStorageId.path
&& overlay->testEffectiveStorageId().token
== expectedStorageId.token,
previewEmbedLabel + u" overlay storage id propagated"_q,
ok);
}
if (body && overlay && overlayShell) {
auto previewProbeSource = NativeIvSource(QVector<MTPPageBlock>{
placeholderFixtures[0].block,
previewEmbedBlock,
});
const auto previewProbePrepared = TryPrepareNativeInstantView({
.source = &previewProbeSource,
@@ -5589,15 +5615,10 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
overlayShell->isVisible(),
previewEmbedLabel + u" overlay visible after click"_q,
ok);
const auto preferredBodySize = QSize(180, 96);
overlay->testHandleWebviewMessage(
NativeIvPreferredSizeMessage(preferredBodySize));
FlushPendingWidgetEvents();
Check(
overlayShell->size()
== NativeIvOverlayShellSizeForBody(preferredBodySize),
overlay->testLoadingCoverVisible(),
previewEmbedLabel
+ u" overlay resize applies preferred body size"_q,
+ u" loading cover visible before navigation done"_q,
ok);
const auto availableRect = NativeIvOverlayAvailableRect(
overlay);
@@ -5605,16 +5626,146 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
availableRect.isValid(),
previewEmbedLabel + u" overlay available rect"_q,
ok);
const auto overlayBeforeResize = RenderWidgetForTest(overlay);
Check(
!overlayBeforeResize.isNull(),
previewEmbedLabel
+ u" overlay render before preferred size"_q,
ok);
const auto loadingGeometry = overlayShell->geometry();
auto scrimPoint = QPoint();
auto scrimPixel = uint(0);
auto canCompareScrimPixel = false;
if (availableRect.isValid() && !overlayBeforeResize.isNull()) {
scrimPoint = QPoint(
std::max(availableRect.left() / 2, 0),
std::clamp(
availableRect.center().y(),
0,
overlayBeforeResize.height() - 1));
canCompareScrimPixel
= !overlayShell->geometry().contains(scrimPoint);
Check(
canCompareScrimPixel,
previewEmbedLabel
+ u" scrim sample sits outside shell"_q,
ok);
if (canCompareScrimPixel) {
scrimPixel = overlayBeforeResize.pixel(scrimPoint);
}
}
const auto preferredBodySize = QSize(180, 96);
overlay->testHandleWebviewMessage(
NativeIvPreferredSizeMessage(preferredBodySize));
FlushPendingWidgetEvents();
Check(
overlay->testLoadingCoverVisible(),
previewEmbedLabel
+ u" loading cover persists after preferred size"_q,
ok);
Check(
overlayShell->geometry() == loadingGeometry,
previewEmbedLabel
+ u" overlay defers preferred size while loading"_q,
ok);
const auto overlayAfterResize = RenderWidgetForTest(overlay);
Check(
!overlayAfterResize.isNull(),
previewEmbedLabel
+ u" overlay render after preferred size"_q,
ok);
if (canCompareScrimPixel && !overlayAfterResize.isNull()) {
Check(
!overlayShell->geometry().contains(scrimPoint),
previewEmbedLabel
+ u" scrim sample stays outside shell"_q,
ok);
if (!overlayShell->geometry().contains(scrimPoint)) {
Check(
overlayAfterResize.pixel(scrimPoint)
== scrimPixel,
previewEmbedLabel
+ u" scrim pixel stable after resize"_q,
ok);
}
}
if (availableRect.isValid()) {
overlay->testHandleWebviewMessage(
NativeIvPreferredSizeMessage(QSize(
availableRect.width() * 2,
availableRect.height() * 2)));
FlushPendingWidgetEvents();
Check(
overlay->testLoadingCoverVisible(),
previewEmbedLabel
+ u" loading cover persists before navigation complete"_q,
ok);
Check(
overlayShell->geometry() == loadingGeometry,
previewEmbedLabel
+ u" overlay defers clamped resize while loading"_q,
ok);
const auto overlayAfterClamp = RenderWidgetForTest(
overlay);
Check(
!overlayAfterClamp.isNull(),
previewEmbedLabel
+ u" overlay render after clamped resize"_q,
ok);
if (canCompareScrimPixel && !overlayAfterClamp.isNull()) {
Check(
!overlayShell->geometry().contains(scrimPoint),
previewEmbedLabel
+ u" scrim sample stays outside clamped shell"_q,
ok);
if (!overlayShell->geometry().contains(scrimPoint)) {
Check(
overlayAfterClamp.pixel(scrimPoint)
== scrimPixel,
previewEmbedLabel
+ u" scrim pixel stable after clamped resize"_q,
ok);
}
}
overlay->testHandleNavigationDone(true);
FlushPendingWidgetEvents();
Check(
!overlay->testLoadingCoverVisible(),
previewEmbedLabel
+ u" loading cover hides after navigation done"_q,
ok);
Check(
overlayShell->geometry() == availableRect,
previewEmbedLabel
+ u" overlay resize clamps to available rect"_q,
+ u" overlay shell stays clamped after navigation done"_q,
ok);
overlay->closeEmbed();
FlushPendingWidgetEvents();
Check(
!overlayShell->isVisible(),
previewEmbedLabel
+ u" overlay hides before failed navigation retry"_q,
ok);
SendMouseClick(body, clickBounds->center(), Qt::LeftButton);
FlushPendingWidgetEvents();
Check(
overlay->testLoadingCoverVisible(),
previewEmbedLabel
+ u" loading cover visible before failed navigation"_q,
ok);
overlay->testHandleNavigationDone(false);
FlushPendingWidgetEvents();
const auto overlayError = preview->findChild<QWidget*>(
u"nativeIvEmbedOverlayErrorWrap"_q);
Check(
!overlay->testLoadingCoverVisible(),
previewEmbedLabel
+ u" loading cover hides after failed navigation"_q,
ok);
Check(
overlayError && overlayError->isVisible(),
previewEmbedLabel
+ u" overlay shows error after failed navigation"_q,
ok);
}
}