mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-11 06:03:40 +00:00
Embeds almost work now.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user